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

Pundit Integration

GraphQL::Pro includes an integration for powering GraphQL authorization with Pundit policies.

Why bother? You could put your authorization code in your GraphQL types themselves, but writing a separate authorization layer gives you a few advantages:

Getting Started

NOTE: Requires the latest gems, so make sure your Gemfile has:

# For PunditIntegration:
gem "graphql-pro", ">=1.7.9"
# For list scoping:
gem "graphql", ">=1.8.7"

Then, bundle install.

Whenever you run queries, include :current_user in the context:

context = {
  current_user: current_user,
  # ...
}
MySchema.execute(..., context: context)

And read on about the different features of the integration:

Authorizing Objects

You can specify Pundit roles that must be satisfied in order for viewers to see objects of a certain type. To get started, include the ObjectIntegration in your base object class:

# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  # Add the Pundit integration:
  include GraphQL::Pro::PunditIntegration::ObjectIntegration
  # By default, require staff:
  pundit_role :staff
  # Or, to require no permissions by default:
  # pundit_role nil
end

Now, anyone trying to read a GraphQL object will have to pass the #staff? check on that object’s policy.

Then, each child class can override that parent configuration. For example, allow all viewers to read the Query root:

class Types::Query < Types::BaseObject
  # Allow anyone to see the query root
  pundit_role nil
end

Policies and Methods

For each object returned by GraphQL, the integration matches it to a policy and method.

The policy is found using Pundit.policy!, which looks up a policy using the object’s class name.

Then, GraphQL will call a method on the policy to see whether the object is permitted or not. This method is assigned in the object class, for example:

class Types::Employee < Types::BaseObject
  # Only show employee objects to their bosses,
  # or when that employee is the current viewer
  pundit_role :employer_or_self
  # ...
end

That configuration will call #employer_or_self? on the corresponding Pundit policy.

Bypassing Policies

The integration requires that every object with a pundit_role has a corresponding policy class. To allow objects to skip authorization, you can pass nil as the role:

class Types::PublicProfile < Types::BaseObject
  # Anyone can see this
  pundit_role nil
end

Handling Unauthorized Objects

When any Policy method returns false, the unauthorized object is passed to Schema.unauthorized_object, as described in Handling unauthorized objects.

Scopes

The Pundit integration adds Pundit scopes to GraphQL-Ruby’s list scoping feature. Any list or connection will be scoped. If a scope is missing, the query will crash rather than risk leaking unfiltered data.

To scope lists of interface or union type, include the integration in your base union class and base interface module:

class BaseUnion < GraphQL::Schema::Union
  include GraphQL::Pro::PunditIntegration::UnionIntegration
end

module BaseInterface
  include GraphQL::Schema::Interface
  include GraphQL::Pro::PunditIntegration::InterfaceIntegration
end

Note that Pundit scopes are best for database relations, but don’t play well with Arrays. See below for bypassing Pundit if you want to return an Array.

Bypassing scopes

To allow an unscoped relation to be returned from a field, disable scoping with scope: false, for example:

# Allow anyone to browse the job postings
field :job_postings, [Types::JobPosting], null: false,
  scope: false

Authorizing Fields

You can also require certain checks on a field-by-field basis. First, include the integration in your base field class:

# app/graphql/types/base_field.rb
class Types::BaseField < GraphQL::Schema::Field
  # Add the Pundit integration:
  include GraphQL::Pro::PunditIntegration::FieldIntegration
  # By default, don't require a role at field-level:
  pundit_role nil
end

If you haven’t already done so, you should also hook up your base field class to your base object and base interface:

# app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
  field_class Types::BaseField
end
# app/graphql/types/base_interface.rb
module Types::BaseInterface
  # ...
  field_class Types::BaseField
end

Then, you can add pundit_role: options to your fields:

class Types::JobPosting < Types::BaseObject
  # Allow signed-in users to browse listings
  pundit_role :signed_in

  # But, only allow `JobPostingPolicy#staff?` users to see
  # who has applied
  field :applicants, [Types::User], null: true,
    pundit_role: :staff
end

It will call the named role (eg, #staff?) on the parent object’s policy (eg JobPostingPolicy).

Authorizing Arguments

Similar to field-level checks, you can require certain permissions to use certain arguments. To do this, add the integration to your base argument class:

class Types::BaseArgument < GraphQL::Schema::Argument
  # Include the integration and default to no permissions required
  include GraphQL::Pro::PunditIntegration::ArgumentIntegration
  pundit_role nil
end

Then, make sure your base argument is hooked up to your base field and base input object:

class Types::BaseField < GraphQL::Schema::Field
  argument_class Types::BaseArgument
  # PS: see "Authorizing Fields" to make sure your base field is hooked up to objects, interfaces and mutations
end

class Types::BaseInputObject < GraphQL::Schema::InputObject
  argument_class Types::BaseArgument
end

Now, arguments accept a pundit_role: option, for example:

class Types::Company < Types::BaseObject
  field :employees, Types::Employee.connection_type, null: true do
    # Only admins can filter employees by email:
    argument :email, String, required: false, pundit_role: :admin
  end
end

The role will be called on the parent object’s policy, for example CompanyPolicy#admin? in the case above.

Authorizing Mutations

There are a few ways to authorize GraphQL mutations with the Pundit integration:

Also, you can configure unauthorized object handling

Setup

Add MutationIntegration to your base mutation, for example:

class Mutations::BaseMutation < GraphQL::Schema::Mutation
  include GraphQL::Pro::PunditIntegration::MutationIntegration

  # Also, to use argument-level authorization:
  argument_class Types::BaseArgument
end

Also, you’ll probably want a BaseMutationPayload where you can set a default role:

class Types::BaseMutationPayload < Types::BaseObject
  # If `BaseObject` requires some permissions, override that for mutation results.
  # Assume that anyone who can run a mutation can read their generated result types.
  pundit_role nil
end

And hook it up to your base mutation:

class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  object_class Types::BaseMutationPayload
end

Mutation-level roles

Each mutation can have a class-level pundit_role which will be checked before loading objects or resolving, for example:

class Mutations::PromoteEmployee < Mutations::BaseMutation
  pundit_role :admin
end

In the example above, PromoteEmployeePolicy#admin? will be checked before running the mutation.

Custom Policy Class

By default, Pundit uses the mutation’s class name to look up a policy. You can override this by defining self.policy_class on your mutation:

class Mutations::PromoteEmployee < Mutations::BaseMutation
  def self.policy_class
    ::UserPolicy
  end

  pundit_role :admin
end

Now, the mutation will check UserPolicy#admin? before running.

Another good approach is to have one policy per mutation. You can implement self.policy_class to look up a class within the mutation, for example:

class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  def self.policy_class
    # Look up a nested `Policy` constant:
    self.const_get(:Policy)
  end
end

Then, each mutation can define its policy inline, for example:

class Mutations::PromoteEmployee < Mutations::BaseMutation
  # This will be found by `BaseMutation.policy_class`, defined above:
  class Policy
    # ...
  end

  pundit_role :admin
end

Now, Mutations::PromoteEmployee::Policy#admin will be checked before running the mutation.

Authorizing Loaded Objects

Mutations can automatically load and authorize objects by ID using the loads: option.

Beyond the normal object reading permissions, you can add an additional role for the specific mutation input using a pundit_role: option:

class Mutations::FireEmployee < Mutations::BaseMutation
  argument :employee_id, ID, required: true,
    loads: Types::Employee,
    pundit_role: :supervisor,
end

In the case above, the mutation will halt unless the EmployeePolicy#supervisor? method returns true.

Unauthorized Mutations

By default, an authorization failure in a mutation will raise a Ruby exception. You can customize this by implementing #unauthorized_by_pundit(owner, value) in your base mutation, for example:

class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  def unauthorized_by_pundit(owner, value)
    # No error, just return nil:
    nil
  end
end

The method is called with:

Since it’s a mutation method, you can also access context in that method.

Whatever that method returns will be treated as an early return value for the mutation, so for example, you could return errors as data:

class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
  field :errors, [String], null: true

  def unauthorized_by_pundit(owner, value)
    # Return errors as data:
    { errors: ["Missing required permission: #{owner.pundit_role}, can't access #{value.inspect}"] }
  end
end