graphql_devise
graphql_devise copied to clipboard
Authenticate non-root fields
Is it possible to authenticate non-root fields?
After some hurdles I managed to set this gem up with graphql 2.0.14. I got to the point where I set authenticate_default: false
in my main schema and if I set authenticate: true
on one of the root fields in QueryType I'm getting an error if I want to access it while not being authenticated. However if I add authenticate: true
to one of the non-root fields it doesn't seem to have any effect.
In my case I have:
# ./app/graphql/types/photo_type.rb
module Types
class PhotoType < GraphQL::Schema::Object
field_class GraphqlDevise::Types::BaseField
description 'A Photo'
field :id, String, null: false, authenticate: true
[ snip ]
end
end
I can access id without being authenticated.
Is it possible to have this type of granular authentication? If yes, what am I doing wrong?
Hey @janosrusiczki! You are not doing something wrong, this is the correct behavior of the gem as commented in https://github.com/graphql-devise/graphql_devise/blob/c9011c62f13e0c2838cd4ccbda87ab11742558a4/lib/graphql_devise/schema_plugin.rb#L26
This was done for performance reasons at first as not doing this would mean we have to run the authentication code for every field and that might happen a lot of times in a single query. I'll try to run some benchmarks and see what's the actual effect of enabling the feature for nested fields
I agree, it's probably a big overhead to check all the fields.
Maybe a global flag in an initializer: granular authentication on / off?
Or some way to enable it on each type? Default being disabled.
We need to think about this a bit more, but I kind of feel we might not want to allow this unless it has a very low impact to performance. For a more granular access control I think it would be better to use an authorization gem. This one was kind of an inspiration for this project, but I don't see much activity on the project.
Anyway, there might be a fast way to handle the scenario where nested fields don't set the authenticate
value at least. Or as you said, make it optional so the project using the gem can decide. I might also be overthinking this since the code is relatively fast and checking in every field might have a very low impact. I'll look into it
I give a look at graphql-guard gem. It does not work on the newer version of ruby-graphql, and since all the interface it was written on top of was removed, I think it'll not be updated to work with. It was written on top of accepts_definitions
, to_graphql
, and graphql_definition
. All of them were removed on version 2.0 and I think this is the reason behind the inactivity of that repository.
Maybe instead of adding this on top of this gem, what about building one, not-opinative gem, giving a way for the developer to implement his own "guard" method, other than the default one, that I think should be as simple as "authenticated", but on the field, something like authorized: ->(user, context) { sample_code }
That's a good idea. That might be a good alternative. graphql-guard definitively looks abandoned. But I still have to look into supporting this in this gem. As I said before, maybe performance won't be too affected if we check every level of the query (at least for presence of the authenticate
key)
I think that I have a hint about why it's abandoned looking into this doc
Ps: no end code down there, just some ideas about it for anyone that comes here
Doing something like this should work.
Configure authenticate_default
to true
module Types
class BaseField < GraphQL::Schema::Field
include ::GraphqlDevise::FieldAuthentication
attr_accessor :authorize
def initialize(*args, authorize: true, **kwargs, &block)
super(*args, **kwargs, &block)
@authorize = authorize
end
def authorized?(obj, args, ctx)
authorize.instance_of?(Proc) ? authorize.call(obj, args, ctx) : authorize
end
argument_class Types::BaseArgument
end
end
And it gives you some power to use it like:
class Authorize < ApplicationService
def initialize(action, restrict: false)
@action = action
@restrict = restrict
@authorizer = YourAuthorizerClass.new
end
def call
proc do |obj, _args, ctx|
user_allowed?(ctx[:current_resource], obj)
end
end
private
attr_accessor :action, :restrict, :authorizer
def user_allowed?(user, record)
authorizer.authorize?(user, record, action, restrict:)
end
end
module AuthTypes
ADMIN = ::Authorize.call(:any, restrict: true)
READ = ::Authorize.call(:show, restrict: false)
CREATE = ::Authorize.call(:create, restrict: false)
UPDATE = ::Authorize.call(:update, restrict: false)
LIST = ::Authorize.call(:index, restrict: false)
DESTROY = ::Authorize.call(:destroy, restrict: false)
end
module Types
class UserType < Types::BaseObject
field :id, ID, null: false, authorize: AuthTypes::READ
field :name, String, null: true, authorize: AuthTypes::READ
field :title, String, null: true, authorize: AuthTypes::READ
field :email, String, null: false, authorize: AuthTypes::READ
field :encrypted_password, String, null: false, authorize: AuthTypes::ADMIN
field :reset_password_token, String, authorize: AuthTypes::ADMIN
field :reset_password_sent_at, GraphQL::Types::ISO8601DateTime, authorize: AuthTypes::ADMIN
field :remember_created_at, GraphQL::Types::ISO8601DateTime, authorize: AuthTypes::ADMIN
field :created_at, GraphQL::Types::ISO8601DateTime, null: false, authorize: AuthTypes::ADMIN
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false, authorize: AuthTypes::ADMIN
end
end