rmosolgo/graphql-ruby

Issue overriding the "authorized?" method in BaseField

codingwaysarg opened this issue · 2 comments

Describe the bug

Im overriding the authorized? method based on the documentation, in order to support an "authenticate" key for my fields.


module Types
  class BaseField < GraphQL::Schema::Field
    include ApolloFederation::Field
    argument_class Types::BaseArgument

    def initialize(*args, authenticate: true, **kwargs, &block)
      @authenticate = authenticate
      super(*args, **kwargs, &block)
    end

    attr_reader :authenticate

    def authorized?(object, args, context)
      super && (!@authenticate || context[:current_user].present?)
    end
  end
end

Then, in order to test, my query_type looks like this:


module Types
  class QueryType < Types::BaseObject
    field :current_user, Types::UserType, null: true, authenticate: false

    def current_user
      User.first
    end
  end
end

As you can see, i'm passing authenticate: false to the field.
The thing is that, when I call this query:


query {
  currentUser {
    id
  }
}

it looks like the "id" field inside my user_type is still checking for the authenticate option, even its parent explicitly says authenticate: false.

I understand thats because in the initialize method, the argument authenticate is "true" by default, because that is what i want. I would like all fields to require authentication by default, except the ones i mark with authenticate: false, but I dont know if fields inside a type can also behave with the same option set on the parent.

Versions

graphql version: 2.3.1
rails (or other framework): 7.1.3.2
apollo-federation: 3.8.5

GraphQL schema

Include relevant types and fields (in Ruby is best, in GraphQL IDL is ok). Any custom extensions, etc?

module Types
  class UserType < Types::BaseObject
    key fields: :id

    field :id, ID, null: false
    field :email, String, null: false
    field :company_id, ID, null: false
    field :company, Types::CompanyType, null: false

    def company
      { __typename: 'Company', id: object[:company_id] }
    end
  end
end


# frozen_string_literal: true

class ApiSchema < GraphQL::Schema
  GraphQL::Types::Relay::PageInfo.include ApolloFederation::Object
  GraphQL::Types::Relay::PageInfo.shareable

  include ApolloFederation::Schema
  federation version: '2.0'

  mutation(Types::MutationType)
  query(Types::QueryType)

  use GraphQL::Dataloader

  max_query_string_tokens(5000)
  validate_max_errors(100)

  def self.unauthorized_object(error)
    raise GraphQL::ExecutionError, "An object of type #{error.type.graphql_name} was hidden due to permissions"
  end

  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

module Types
  class BaseObject < GraphQL::Schema::Object
    include ApolloFederation::Object
    edge_type_class(Types::BaseEdge)
    connection_type_class(Types::BaseConnection)
    field_class Types::BaseField
    underscore_reference_keys true
  end
end

GraphQL query

Example GraphQL query and response (if query execution is involved)

query {
  currentUser {
    id
  }
}
{
  "errors": [
    {
      "message": "The field id on an object of type User was hidden due to permissions",
      "path": [
        "currentUser",
        "id"
      ],
      "extensions": {
        "serviceName": "users",
        "code": "DOWNSTREAM_SERVICE_ERROR",
        "stacktrace": [
          "GraphQLError: The field id on an object of type User was hidden due to permissions",
          "    at Object.err (/home/jpmermoz/Code/Other/customer-api-federation/node_modules/@apollo/federation-internals/dist/error.js:11:32)",
          "    at downstreamServiceError (/home/jpmermoz/Code/Other/customer-api-federation/node_modules/@apollo/gateway/dist/executeQueryPlan.js:523:120)",
          "    at /home/jpmermoz/Code/Other/customer-api-federation/node_modules/@apollo/gateway/dist/executeQueryPlan.js:341:59",
          "    at Array.map (<anonymous>)",
          "    at sendOperation (/home/jpmermoz/Code/Other/customer-api-federation/node_modules/@apollo/gateway/dist/executeQueryPlan.js:341:44)",
          "    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)",
          "    at async /home/jpmermoz/Code/Other/customer-api-federation/node_modules/@apollo/gateway/dist/executeQueryPlan.js:255:49",
          "    at async executeNode (/home/jpmermoz/Code/Other/customer-api-federation/node_modules/@apollo/gateway/dist/executeQueryPlan.js:200:17)",
          "    at async executeNode (/home/jpmermoz/Code/Other/customer-api-federation/node_modules/@apollo/gateway/dist/executeQueryPlan.js:174:40)",
          "    at async /home/jpmermoz/Code/Other/customer-api-federation/node_modules/@apollo/gateway/dist/executeQueryPlan.js:96:35"
        ]
      }
    }
  ],
  "data": {
    "currentUser": null
  }
}

Steps to reproduce

Steps to reproduce the behavior

Expected behavior

Return the user with its ID

Actual behavior

I have permissions for the user field, but I cannot query fields inside it, like the ID field.

Hi! Thanks for the detailed report and sorry for the trouble.

Given the setup you shared, if you want the ID field to not require authentication, you have to configure it with authenticate: false, for example:

field :id, ID, authenticate: false # don't require logging in for this field 

Alternatively, you could update your BaseField definition to handle this special case, for example:

    def authorized?(object, args, context)
      # Allow this field if either: 
      # - authenticate: false was configured, 
      # - or, a current_user is present, 
      # - or, it's an ID field
      super && (!@authenticate || context[:current_user].present? || graphql_name == "id")
    end

(Maybe that solution isn't exactly what you want, but hopefully that gives the idea ...)

Would one of those approaches work for you?

Hi, thank you for your quick answer!
I was looking for a more "generic" solution that wouldnt involve puting authenticate: false on every field.

I ended up doing something like this:


module Types
  class BaseField < GraphQL::Schema::Field
    include ApolloFederation::Field
    argument_class Types::BaseArgument

    def initialize(*args, authenticate: true, **kwargs, &block)
      @authenticate = authenticate
      super(*args, **kwargs, &block)
    end

    attr_reader :authenticate

    def authorized?(object, args, context)
      return super unless root_level_field?
      super && (!@authenticate || context[:current_user].present?)
    end

    def root_level_field?
      %w(Types::QueryType Types::MutationType).include?(owner.name)
    end
  end
end

Of course this will work only for root level fields (defined in query_type or mutation_type) but is enough for know.
Thank you!