graphql_devise icon indicating copy to clipboard operation
graphql_devise copied to clipboard

Authenticate non-root fields

Open janosrusiczki opened this issue 2 years ago • 6 comments

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?

janosrusiczki avatar Sep 26 '22 08:09 janosrusiczki

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

mcelicalderon avatar Sep 27 '22 17:09 mcelicalderon

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.

janosrusiczki avatar Sep 28 '22 16:09 janosrusiczki

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

mcelicalderon avatar Sep 28 '22 22:09 mcelicalderon

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 }

tatosjb avatar Nov 26 '22 00:11 tatosjb

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)

mcelicalderon avatar Nov 28 '22 21:11 mcelicalderon

I think that I have a hint about why it's abandoned looking into this doc image

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

tatosjb avatar Nov 29 '22 01:11 tatosjb