Authorization on a field in graphql-ruby - ruby

From this guide I don't understand how we can add authorization on a field in graphql-ruby.
I understand how we can deny access to an entire object but I don't see how we can prevent access to just a field of an object.
In my usecase, I want to prevent some queries to access a String field in my UserType.
Is it possible? With the field helper?

You could use scoping in the GraphQL-ruby or use gem graphql-guard.
For scoping to work, you should be either using Pundit scope or CanCan accessible_by.
FYI, I haven't used it yet anywhere but seems like both could solve your problem to me.

I used gem graphql-guard.
GUARD = lambda do |obj, args, ctx|
...some logics...
end
field :some_field, String, null: false, guard: GUARD
And when I wanted to use only some of the arguments like lambda do |_, args, _|

Here's an example:
module Types
class User < Types::BaseObject
field :private_string, String, null: true do
def authorized?(object, args, context)
# Do whatever logic you want and return true if the user is authorized.
object.id == context[:current_user].id
end
end
end
end
class MySchema < GraphQL::Schema
# You'll need this in your schema to handle an unauthorized field.
def self.unauthorized_field(error)
raise GraphQL::ExecutionError, "The field #{error.field.graphql_name} on an object of type #{error.type.graphql_name} was hidden due to permissions"
end
end

Related

Resolvers in GraphQL do not seem to get context

I am using resolvers inside query_type:
module Types
class QueryType < Types::BaseObject
...
field :gather_things,
resolver: Resolvers::GatherThings,
null: true do
argument :scope, String, required: false
argument :scope_id, ID, required: false
...
And in my resolvers folder there is a base file:
module Resolvers
class Base < GraphQL::Schema::Resolver
def current_user_id
#current_user_id ||= context[:current_user].id
end
end
end
However, when I try to use current_user_id inside the resolver code, it breaks:
module Resolvers
class GatherThings < Resolvers::Base
...
def things_resolver(scope, scope_id)
if scope.nil?
Thing.from_user(current_user_id)
...
end
end
...
Saying : "undefined method 'id' for nil:NilClass"
Any clue?
I later found out that something was nullifying my session upstream.
This was due to the use of JWTs for session management and mixing them with cookies for OmniAuth.
I have to work out on a solution for using both depending on the intended use.

Metrics/AbcSize Too High: How do I decrease the ABC in this method?

I have recently started using Rubocop to "standardise" my code, and it has helped me optimise a lot of my code, as well as help me learn a lot of Ruby "tricks". I understand that I should use my own judgement and disable Cops where necessary, but I have found myself quite stuck with the below code:
def index
if params[:filters].present?
if params[:filters][:deleted].blank? || params[:filters][:deleted] == "false"
# if owned is true, then we don't need to filter by admin
params[:filters][:admin] = nil if params[:filters][:admin].present? && params[:filters][:owned] == "true"
# if admin is true, then must not filter by owned if false
params[:filters][:owned] = nil if params[:filters][:owned].present? && params[:filters][:admin] == "false"
companies_list =
case params[:filters][:admin]&.to_b
when true
current_user.admin_companies
when false
current_user.non_admin_companies
end
if params[:filters][:owned].present?
companies_list ||= current_user.companies
if params[:filters][:owned].to_b
companies_list = companies_list.where(owner: current_user)
else
companies_list = companies_list.where.not(owner: current_user)
end
end
else
# Filters for deleted companies
companies_list = {}
end
end
companies_list ||= current_user.companies
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
Among others, the error that I'm getting is the following:
C: Metrics/AbcSize: Assignment Branch Condition size for index is too high. [<13, 57, 16> 60.61/15]
I understand the maths behind it, but I don't know how to simplify this code to achieve the same result.
Could someone please give me some guidance on this?
Thanks in advance.
Well first and foremost, is this code fully tested, including all the myriad conditions? It's so complex that refactoring will surely be disastrous unless the test suite is rigorous. So, write a comprehensive test suite if you don't already have one. If there's already a test suite, make sure it tests all the conditions.
Second, apply the "fat model skinny controller" paradigm. So move all the complexity into a model, let's call it CompanyFilter
def index
companies_list = CompanyFilter.new(current_user, params).list
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
and move all those if/then/else statements into the CompanyFilter#list method
tests still pass? great, you'll still get the Rubocop warnings, but related to the CompanyFilter class.
Now you need to untangle all the conditions. It's a bit hard for me to understand what's going on, but it looks as if it should be reducible to a single case statement, with 5 possible outcomes. So the CompanyFilter class might look something like this:
class CompanyFilter
attr_accessors :current_user, :params
def initialize(current_user, params)
#current_user = current_user
#params = params
end
def list
case
when no_filter_specified
{}
when user_is_admin
#current_user.admin_companies
when user_is_owned
# etc
when # other condition
# etc
end
end
private
def no_filter_specified
#params[:filter].blank?
end
def user_is_admin
# returns boolean based on params hash
end
def user_is_owned
# returns boolean based on params hash
end
end
tests still passing? perfect! [Edit] Now you can move most of your controller tests into a model test for the CompanyFilter class.
Finally I would define all the different companies_list queries as scopes on the Company model, e.g.
class Company < ApplicationRecord
# some examples, I don't know what's appropriate in this app
scope :for_user, ->(user){ where("...") }
scope :administered_by, ->(user){ where("...") }
end
When composing database scopes ActiveRecord::SpawnMethods#merge is your friend.
Post.where(title: 'How to use .merge')
.merge(Post.where(published: true))
While it doesn't look like much it lets you programatically compose scopes without overelying on mutating assignment and if/else trees. You can for example compose an array of conditions and merge them together into a single ActiveRecord::Relation object with Array#reduce:
[Post.where(title: 'foo'), Post.where(author: 'bar')].reduce(&:merge)
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 AND "posts"."author" = $2 LIMIT $3
So lets combine that with a skinny controllers approach where you handle filtering in a seperate object:
class ApplicationFilter
include ActiveModel::Attributes
include ActiveModel::AttributeAssignment
attr_accessor :user
def initialize(**attributes)
super()
assign_attributes(attributes)
end
# A convenience method to both instanciate and apply the filters
def self.call(user, params, scope: model_class.all)
return scope unless params[:filters].present?
scope.merge(
new(
permit_params(params).merge(user: user)
).to_scope
)
end
def to_scope
filters.map { |filter| apply_filter(filter) }
.compact
.select {|f| f.respond_to?(:merge) }
.reduce(&:merge)
end
private
# calls a filter_by_foo method if present or
# defaults to where(key => value)
def apply_filter(attribute)
if respond_to? "filter_by_#{attribute}"
send("filter_by_#{attribute}")
else
self.class.model_class.where(
attribute => send(attribute)
)
end
end
# Convention over Configuration is sexy.
def self.model_class
name.chomp("Filter").constantize
end
# filters the incoming params hash based on the attributes of this filter class
def self.permit_params
params.permit(filters).reject{ |k,v| v.blank? }
end
# provided for modularity
def self.filters
attribute_names
end
end
This uses some of the goodness provided by Rails to setup objects with attributes that will dynamically handle filtering attributes. It looks at the list of attributes you have declared and then slices those off the params and applies a method for that filter if present.
We can then write a concrete implementation:
class CompanyFilter < ApplicationFilter
attribute :admin, :boolean, default: false
attribute :owned, :boolean
private
def filter_by_admin
if admin
user.admin_companies
else
user.non_admin_companies
end
end
# this should be refactored to use an assocation on User
def filter_by_owned
case owned
when nil
nil
when true
Company.where(owner: user)
when false
Company.where.not(owner: user)
end
end
end
And you can call it with:
# scope is optional
#companies = CompanyFilter.call(current_user, params), scope: current_user.companies)

how to refactor IEX exchange data calls for grabbing data from api in ruby program

I am using the iex exchange api for grabbing info about stocks. It's working great but my code is very ugly was wondering how to refactor the code.
stock = IEX::Api::Client.new(
publishable_token: token,
endpoint: 'https://sandbox.iexapis.com/v1'
)
This is what is needed to grab the info. The problem is that I have to put the code above in every method that utilizes the api. For instance,
def self.stock_price(ticker)
stock = IEX::Api::Client.new(
publishable_token: token,
endpoint: 'https://sandbox.iexapis.com/v1'
)
company = stock.company(ticker)
quote = stock.quote(ticker.upcase)
puts "#{company.company_name}: #{quote.latest_price}"
end
def self.week_52_high(ticker)
stock = IEX::Api::Client.new(
publishable_token: token,
endpoint: 'https://sandbox.iexapis.com/v1'
)
key_stats = stock.key_stats(ticker)
puts "52-week high: #{key_stats.week_52_high}"
end
def self.week_52_low(ticker)
stock = IEX::Api::Client.new(
publishable_token: token,
endpoint: 'https://sandbox.iexapis.com/v1'
)
key_stats = stock.key_stats(ticker)
puts "52-week low: #{key_stats.week_52_low}"
end
Is there any way to factor that call out to a different file and call the method that way? The code is very repetitive as is. The "stock" variable is what i need to actually work with, should I made that a global variable? I've heard that's a no-no but is this case an exception? Also, where i have
publishable_token: token,
that token variable is actually my actual, hard-coded token, not the "token" variable you see, I simply changed it for security issues. What should I do instead of hard-coding it? The documentation says to turn it into an environment variable but i dont know what that means. Thanks in advance!
What should I do instead of hard-coding it? The documentation says to
turn it into an environment variable but i dont know what that means.
An environment variable is a variable whose value is set outside the the application, typically through functionality built into the operating system or shell. You need to check the documentation for your setup to see how to set env vars.
You can get env vars in Ruby through the ENV hash.
ENV['FOO']
ENV.fetch('FOO') # will raise a KeyError if it is not set instead of just returning nil
Rails 5.2 and up have secure credentials that can be used instead. It stores your credentials in an encrypted YAML file that can be checked into source control.
How do I refactor this?
One way to refactor this would be to use delegation instead of bunch of largely static methods:
require 'forwardable'
class MyClient
extend Forwardable
TOKEN = ENV.fetch('IEX_API_TOKEN')
ENDPOINT = ENV.fetch('IEX_API_ENDPOINT', 'https://sandbox.iexapis.com/v1')
def_delegators :#client, :company, :quote, :key_stats
def initialize(publishable_token: TOKEN, endpoint: ENDPOINT, client: nil)
# This is know as constructor injection and makes it easy to mock out
# the dependency in tests
#client = client || IEX::Api::Client.new(publishable_token: TOKEN, endpoint: ENDPOINT)
end
def stock_price(ticker)
company_name = company(ticker).company_name
price = quote(ticker.upcase).latest_price
puts "#{company_name}: #{price}"
end
def week_52_high(ticker)
puts "52-week high: #{key_stats(ticker).week_52_high}"
end
def week_52_low(ticker)
puts "52-week low: #{key_stats(ticker).week_52_low}"
end
end
#client = MyClient.new
#client.week_52_low(ticker)

Bypassing writer accessor by manipulating instead of assigning

I wrote a very simple User class. The instance variable email has a reader accessor and my own writer accessor that validates the email address with a regex.
class User
attr_reader :email
def email=(value)
if (value =~ /^[a-z\d\-\_\+\.]+#([a-z\d\-]+\.)+[a-z]+$/)
#email = value
else
# bonus question: is ArgumentError the right error type to use here?
raise ArgumentError, "#{value} is not a valid email address."
end
end
end
I wrote the following test:
require 'test/unit'
require_relative '../lib/user'
class TC_UserTest < Test::Unit::TestCase
def setup
#user = User.new()
end
def test_email
# using the writer accessor
#user.email = 'user#example.com'
# bypassing the writer accessor. evil.
#user.email[4] = '#'
assert_equal('user#example.com', #user.email)
end
end
By using the reference given to me by the reader accessor, I am able to manipulate the email instance variable without going through the writer accessor.
The same principe would apply to any data type that allows manipulation without outright assigning a new value with =
Am I being overzealous? I just want to write robust code. Is there a way to ensure that my email address can only be set using the writer accessor?
I'm new to the language and I'm trying to get a feel for the best practices.
An option to make the test pass (and protect the #email variable) is to expose a duplicate.
def email
#email.dup
end
To do what you're trying to do, my advice is to move the regexp into its own validation method.
Better still, don't write an email regexp unless you really want to do it right.
Use a gem instead: https://github.com/SixArm/sixarm_ruby_email_address_validation
After you set the email, freeze it with http://ruby-doc.org/core-1.9.3/Object.html#method-i-freeze
Bonus answer: yes, ArgumentError is the right error type in general. If you're using Rails, consider using the Rails validation methods.
You can freeze value in writer, that way you'll be able to assign new one via writer, but already assigned would be immutable:
class User
attr_reader :email
def email=(value)
if (value =~ /^[a-z\d\-\_\+\.]+#([a-z\d\-]+\.)+[a-z]+$/)
# make email immutable:
#email = value.freeze
else
# bonus question: is ArgumentError the right error type to use here?
raise ArgumentError, "#{value} is not a valid email address."
end
end
end

Custom validation not triggering b/c my ruby isn't right

This is a painfully noob question, but I have to ask it. I want validation to trip if a particular field, let's call it :token isn't a particular string. So, I call my custom validation:
validate :use_beta_token
And then I define my validation method
def use_beta_token
errors.add(:token, "Incorrect beta token") if token not 'pizza'
end
Whenever I set the token to a string that isn't "pizza", and I test with valid? it's coming back true. What am I messing up here? I've also tried if token !== 'pizza', but that's not working either. I'm sure the answer is painfully obvious, but I can't seem to dig it up.
try
errors.add(:token, "Incorrect beta token") unless token == 'pizza'
the not method works like !, it's a unary boolean operator rather than a binary comparison operator.
as for how to write them, keep it concise. See the rails guide for examples.
One way to use custom validators for Rails 3 is to define your own Validator class that inherits from ActiveModel::Validator then implement the validate method and attach errors like so:
# define my validator
class MyValidator < ActiveModel::Validator
# implement the method where the validation logic must reside
def validate(record)
# do my validations on the record and add errors if necessary
record.errors[:token] << "Incorrect beta token" unless token == 'pizza'
end
end
Once you define your validator, you must then include it into your model so it can be used and apply it with the validates_with method.
class ModelName
# include my validator and validate the record
include ActiveModel::Validations
validates_with MyValidator
end

Resources