Table of Contents

Authorization

Unavailable This feature is yet to be published. Give it a thumbs up.
An old version is available, but it should not be used.

This gem has an authorization feature implemented using a hidden directive. It leverages the ability of directives to have events triggered during requests, the ability to add directives to both fields and types (which embeds the events), and the ability to use shared directives.

Setting Up

The hidden directive @authorize is responsible for dealing with the authorize events and holding the possible settings as its arguments. It is the component that you will set up for controlling the authorization.

Parameters

You can add parameters (aka arguments) to the directive by calling parameter. It works just as any argument addition, including the one_of special type.

GraphQL::Authorization.parameter(:reading, :boolean)
# Same as
GraphQL::Directive::AuthorizeDirective.argument(...)

LEVELS = %w[guest support admin]
GraphQL::Authorization.parameter(:level, :one_of, LEVELS)

Rules

You can add rules (aka event listeners) to the directive by calling rule. It works just like adding an authorize event listeners to directives, including the filters.

GraphQL::Authorization.rule do |context|
  accept! if context.current_user.admin?
end
# Same as
GraphQL::Directive::AuthorizeDirective.on(:authorize) { ... }

GraphQL::Authorization.rule(on: 'User') { ... }

Using

You have some options for attaching the directive to fields:

Directly

You can use the use method on the fields where you want to attach a directive, where you can also set up the values for parameters.

use :authorize, reading: true

Field List

You can use the directive attacher method for any field list.

attach :authorize, reading: true, to: %i[users user]

Creator

You can use the create method to initialize an instance and attach it to fields provided in the for named argument, which works with any field list.

GraphQL::Authorization.create(reading: true, for: [
  GraphQL::AppSchema[:query][:users],
  GraphQL::AppSchema[:query][:user],
  # OR
  GraphQL::AppSchema[:query],
  # OR
  GraphQL::UserSource[:query],
])

Especial Methods

The directive has some special instance methods that can be used to control the result of the authorization process.

accept!

It will approve the authorization and prevent any other callback from being invoked.

reject!

It will disapprove the authorization and prevent any other callback from being invoked.

apply!(value)

It will conditionally approve, disapprove, or move to the next callback based on the provided value (true, false, and nil, respectively).

CanCanCan Implementation

Here is an example of how you can implement this feature when combined with sources and the famous authorization gem CanCanCan.

# app/graphql/authorization.rb
module GraphQL
  # Add the action parameter for the fields
  Authorization.parameter(:action, :string, null: false)

  # Add the rule based on the ability
  Authorization.rule do |context, field, event|
    reject! unless context.current_user

    # Get the model from the return type
    model = field.type_class.assigned_class
    # If the field has an id argument, get it as part of the check
    args = { id: event.argument(:id) } if field.has_argument?(:id)

    # Apply the result of the ability
    apply! context.current_user.ability.can?(args.action, model, *args)
  end

  # Create shared instances for common actions
  index_action   = Authorization.new(action: 'index')
  show_action    = Authorization.new(action: 'show')
  create_action  = Authorization.new(action: 'create')
  update_action  = Authorization.new(action: 'update')
  destroy_action = Authorization.new(action: 'destroy')

  # Create a new prepare callback to use the accessible_by
  before_index = -> do
    model = field.type_class.assigned_class
    model.accessible_by(context.current_user.ability)
  end

  # Force the Type Map to load all the dependencies so classes are defined
  type_map.load_dependencies!

  # We create a module to hook into the attach fields process of sources
  ActiveRecordSource.extend Module.new do
    # Call a helper based on the type being attached
    def attach_fields!(type, fields)
      helper = "add_#{type}_authorization"
      send(helper, fields) if respond_to?(helper)
      super
    end

    # helper for query fields
    def add_query_authorization(fields)
      fields[singular.to_sym]&.use(show_action)
      fields[plural.to_sym]&.use(show_action)
      fields[plural.to_sym]&.prepare(&before_index)
    end

    # helper for mutation fields
    def add_mutation_authorization(fields)
      fields[:"create_#{singular}"]&.use(create_action)
      fields[:"update_#{singular}"]&.use(update_action)
      fields[:"destroy_#{singular}"]&.use(destroy_action)
    end
  end
end

# app/graphql/app_schema.rb
module GraphQL
  class AppSchema < GraphQL::Schema
    load_scalars %i[bigint date_time]

    load_directory 'sources'

    require_relative 'authorization'
  end
end

The above will also work with custom fields and custom actions.

field(:friends, 'User') do
  use :authorize, action: 'index'
end
# OR
field(:approve_user, 'User') do
  argument :id, null: false
  use :authorize, action: 'approve'
end