Authorization Framework

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 has 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:

MySchema = GraphQL::Schema.define do
  # ...
  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:

MySchema = GraphQL::Schema.define do
  # 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: ...):

MySchema = GraphQL::Schema.define do
  # 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:

AccountBalanceType = GraphQL::ObjectType.define do
  name "AccountBalance"
  # 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:

StudentType = GraphQL::ObjectType.define do
  name "Student"
  field :name, !types.String
  field :gpa, types.Float 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:

MySchema = GraphQL::Schema.define do
  unauthorized_object ->(obj, ctx) { ... }
end
# OR
MySchema = GraphQL::Schema.define do
  unauthorized_object(MyUnauthorizedObjectHook)
end

The function is called with two arguments:

Within the function, you can:

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.

# Non-owners may _see_ these,
# but they may not request them:
field :telephone_number, types.String, access: :owner

AddressType = GraphQL::ObjectType.define do
  name "Address"
  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:

MySchema = GraphQL::Schema.define do
  # ...
  unauthorized_fields ->(irep_nodes, ctx) {
    GraphQL::AnalysisError.new("Sorry, you're not allowed to see that!")
  }
end

The hook should return a GraphQL::AnalysisError. It is called with:

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:

# These types and fields are
# invisible to non-admins:

# field-level:
field :social_security_number, types.String, view: :admin

# type-level:
PassportApplicationType = GraphQL::ObjectType.define do
  name "PassportApplication"
  view :admin
  # ...
end

Pundit

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

MySchema = GraphQL::Schema.define do
  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 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

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

MySchema = GraphQL::Schema.define do
  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, types.String, 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:

MySchema = GraphQL::Schema.define do
  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:

MySchema = GraphQL::Schema.define do
  # choose one:
  authorization(:pundit)
  # or:
  authorization(:cancan)
  # or:
  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

ActiveRecord::Relations get special treatment: they 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.