⚡️ Pro Feature ⚡️ This feature is bundled with GraphQL-Pro.

Authorization Framework

NOTE: A new Pundit integration and CanCan integration are available. They leverage GraphQL-Ruby’s new built-in auth system and has better support for inheritance and customization. If possible, use those instead!


GraphQL::Pro provides a comprehensive, unified authorization framework for the GraphQL runtime.

Fields and types can be authorized at runtime, rejected during validation, or hidden entirely. Default authorization can be applied at schema-level

GraphQL::Pro integrates out-of-the-box Pundit support and CanCan support and supports custom authorization strategies

Configuration

To use authorization, specify an authorization strategy in your schema:

class Schema < GraphQL::Schema
  # ...
  authorization :pundit
  # or:
  # authorization :cancan
  # authorization CustomAuthClass
end

(See below for details on these strategies.)

Then, provide a current_user: in your execution context:

# Authenticate somehow:
current_user = User.find(session[:current_user_id])
# Then pass the user as `current_user:`
result = MySchema.execute(query_string, context: { current_user: current_user })

current_user will be used by the authorization hooks as described below.

Fallback Authorization

You can specify a fallback auth configuration for the entire schema:

class Schema < GraphQL::Schema
  # Always require logged-in users to see anything:
  authorization(..., fallback: { view: :logged_in })
end

This rule will be applied to fields which don’t have a rule of their own or a rule on their return type.

Current User

You can customize the current_user: context key with authorization(..., current_user: ...):

class Schema < GraphQL::Schema
  # Current user is identified as `ctx[:viewer]`
  authorization :pundit, current_user: :viewer
end

The authorization will use the specified key to find the current user in ctx.

Runtime Authorization

When a resolve function returns an object or list of objects, you can assert that the current user has permission to access that object. The authorize keyword defines a runtime permission.

You can specify a permission at field-level, for example:

# Only allow access to this `balance` if current user is the owner:
field :balance, AccountBalanceType, authorize: :owner

# This is the same:
field :balance, AccountBalanceType do
  authorize :owner
  # ...
end

Also, you can specify authentication at type-level, for example:

class AccountBalanceType < GraphQL::Schema::Object
  # Only billing administrators can see
  # objects of this type:
  authorize :billing_administrator
  # ...
end

Field-level and type-level permissions are additive: both checks must pass for a user to access an object.

Type-level permissions are applied according to an object’s runtime type (unions and interfaces don’t have authorization checks).

If an object doesn’t pass permission checks, it is removed from the response. If the object is part of a list, it is removed from the list. You can override this behavior with the unauthorized_object hook.

Authorize Values by Parent

You can also limit access to fields based on their parent objects with parent_role:. For example, to restrict a student’s GPA to that student:

class StudentType < GraphQL::Schema::Object
  field :name, String, null: false
  field :gpa, Float, null: true do 
    # only show `Student.gpa` if the
    # student is the viewer:
    authorize parent_role: :current_user
  end
end

This way, you can serve a subset of fields based on the object being queried.

Unauthorized Object

When an object fails a runtime authorization check, the default behavior is:

You can override this behavior by providing a schema-level unauthorized_object function:

class Schema < GraphQL::Schema
  # Override this hook to handle cases when `authorized?` returns false for an object:
  def self.unauthorized_object(error)
    # Add a top-level error to the response instead of returning nil:
    raise GraphQL::ExecutionError, "An object of type #{error.type.graphql_name} was hidden due to permissions"
  end
end

The function is used to handle unauthorized objects:

You could refer to the basic authorization guide for more details.

Access Authorization

You can prevent access to fields and types from certain users. (They can see them, but if they request them, the request is rejected with an error message.) Use the access: keyword for this feature.

class AddressType < GraphQL::Schema::Object
  # Non-owners may see this type, but they may not request them. 
  access :owner

  # Non-owners may see this field, but they may not request them. 
  field :telephone_number, String, null: true, access: :owner
end

When a user requests access to an unpermitted field, GraphQL returns an error message. You can customize this error message by providing an unauthorized_fields hook:

class Schema < GraphQL::Schema
  # Override this hook to handle cases when `authorized?` returns false for a field:
  def self.unauthorized_field(error)
    # Add a top-level error to the response instead of returning nil:
    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

The function is used to handle unauthorized fields:

You could refer to the basic authorization guide for more details.

Visibility Authorization

You can hide fields and types from certain users. If they request these types or fields, the error message says that they don’t exist at all.

The view keyword specifies visibility permission:

class PassportApplicationType < GraphQL::Schema::Object
  # Every field on this type is invisible to non-admins
  view :admin 

  # This field is invisible to non-admins
  field :social_security_number, String, null: true, view: :admin
  # ...
end

Pundit

NOTE: A new Pundit integration is available. It leverages GraphQL-Ruby’s new built-in auth system and has better support for inheritance and customization. If possible, use that one instead!

GraphQL::Pro includes built-in support for Pundit:

class Schema < GraphQL::Schema
  authorization(:pundit)
end

Now, GraphQL will use your *Policy classes during execution. To find a policy class:

You can also specify a custom policy name. Use the pundit_policy_name: option, for example:

# A pundit policy:
class TotalBalancePolicy
  def initialize(user, obj)
    # ...
  end
  def admin?
    # ...
  end
end

field :balance, AccountBalanceType, authorize: { role: :admin, pundit_policy_name: "TotalBalancePolicy" }

The permission is defined as a hash with a role: key and pundit_policy_name: key. You can pass a hash for view: and access: too. For parent_role:, you can specify a name with parent_pundit_policy_name:.

For :pundit, methods will be called with an extra ?, so

view: :viewer
# => will call the policy's `#viewer?` method

Policy Namespace

If you put your policies in a namespace, provide that namespace as authorize(..., namespace:), for example:

authorize(:pundit, namespace: Policies)

Now, policies will be looked up by name inside Policies::, for example:

class AccountType < GraphQL::Schema::Object
  access :admin # will use Policies::AccountPolicy#admin?
  # ...
end

Policy Scopes

When a resolve function returns an ActiveRecord::Relation, the policy’s Scope class will be used if it’s available.

See Scoping for details.

CanCan

NOTE: A new CanCan integration is available. It leverages GraphQL-Ruby’s new built-in auth system and has better support for inheritance and customization. If possible, use that one instead!

GraphQL::Pro includes built-in support for CanCan:

class Schema < GraphQL::Schema
  authorization(:cancan)
end

GraphQL will initialize your Ability class at the beginning of the query and pass permissions to the #can? method.

field :phone_number, PhoneNumberType, authorize: :view
# => calls `can?(:view, phone_number)`

For compile-time checks (view and access), the object is always nil.

field :social_security_number, String, null: true, view: :admin
# => calls `can?(:admin, nil)`

accessible_by

When a resolve function returns an ActiveRecord::Relation, the relation’s accessible_by method will be used to scope the relation.

See Scoping for details.

Custom Ability Class

By default, GraphQL looks for a top-level Ability class. You can specify a different class with the ability_class: option. For example:

class Schema < GraphQL::Schema
  authorization(:cancan, ability_class: Permissions::CustomAbility)
end

Now, GraphQL will use Permissions::CustomAbility#can? to determine permissions.

Custom Authorization Strategy

You can provide custom authorization logic by providing a class:

class Schema < GraphQL::Schema
  # Custom authorization strategy class:
  authorization(MyAuthStrategy)
end

A custom strategy class must implement #initialize(ctx) and #allowed?(gate, object). Optionally, it may implement #scope(gate, relation). For example:

class MyAuthStrategy
  def initialize(ctx)
    @user = ctx[:custom_user]
  end

  def allowed?(gate, object)
    if object.nil?
      # This is a compile-time check,
      # so no object is available:
      if gate.role == :admin
        @user.admin?
      else
        @user.viewer?
      end
    else
      # This is a runtime check,
      # so we can use this specific object
      @user.can?(gate.role, object)
    end
  end

  def scope(gate, relation)
    # Filter an ActiveRecord::Relation
    # according to `@user` and `gate`
    # ...
  end
end

gate is the permission setting which responds to:

object is either:

For list types, each item of the list is authorized individually.

Scoping

Database query objects (ActiveRecord::Relations and Mongoid::Criterias) get special treatment. They get passed to scope handlers so that they can be filtered at database level (eg, SQL WHERE) instead of Ruby level (eg, .select).

ActiveRecord::Relations can be scoped with SQL by authorization strategies. The Pundit integration uses policy scopes and the CanCan integration uses accessible_by. Custom authorization strategies can implement #scope(gate, relation) to apply scoping to ActiveRecord::Relations.

Mongoid::Criterias are supported in the same way by Pundit policy scopes) and custom strategy’s #scope(gate, relation) methods, but they aren’t supported by CanCan (which doesn’t support Mongoid, as far as I can tell!).