GraphQL Integration

If your application exposes a GraphQL API, the action_policy-graphql gem brings Action Policy’s authorization model to GraphQL Ruby — with field-level authorization, scope filtering, and even exposing permission rules to clients.

Installation

Add the gem alongside action_policy:

# Gemfile
gem "action_policy"
gem "action_policy-graphql"

Setup

Include the behaviour module in your base types and mutations:

app/graphql/types/base_object.rb
class Types::BaseObject < GraphQL::Schema::Object
include ActionPolicy::GraphQL::Behaviour
end
# app/graphql/mutations/base_mutation.rb
class Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation
include ActionPolicy::GraphQL::Behaviour
end

Field-Level Authorization

The simplest authorization pattern is per-field. Add authorize: true to any field:

class Types::QueryType < Types::BaseObject
# This field checks PostPolicy#show? before resolving
field :post, Types::PostType, null: true, authorize: true do
argument :id, ID, required: true
end
def post(id:)
Post.find(id)
end
end

You can also specify a different rule:

field :admin_stats, Types::StatsType, null: true, authorize: { to: :view_stats? }

Or a different policy class:

field :secret_report, Types::ReportType, null: true,
authorize: { with: AdminPolicy, to: :view_reports? }

Preauthorization

Sometimes you want to authorize before the resolver runs (and before even querying the database). Use preauthorize:

class Types::QueryType < Types::BaseObject
# Check HomePolicy#index? using the implicit target
field :posts, [Types::PostType], null: false,
preauthorize: { with: PostPolicy, to: :index? }
def posts
Post.all
end
end

The difference from authorize:

  • authorize: true runs after the resolver, checking the returned object
  • preauthorize: runs before the resolver, using the implicit target (often nil)

Mutation Authorization

In mutations, call authorize! directly in the resolve method:

class Mutations::UpdatePost < Mutations::BaseMutation
argument :id, ID, required: true
argument :title, String, required: true
field :post, Types::PostType, null: true
field :errors, [String], null: false
def resolve(id:, title:)
post = Post.find(id)
authorize! post, to: :update?
if post.update(title: title)
{ post: post, errors: [] }
else
{ post: nil, errors: post.errors.full_messages }
end
end
end

Scope Filtering (Connections)

For list fields that return multiple records, use authorized_scope: true to apply scope filtering:

class Types::QueryType < Types::BaseObject
# Applies PostPolicy's relation scope to filter results
field :posts, [Types::PostType], null: false, authorized_scope: true
def posts
Post.all
end
end

This calls the same relation_scope block you defined in PostPolicy. Guests see only published posts; admins see everything — the same rules apply whether the request comes through REST or GraphQL.

Exposing Authorization Rules to Clients

One powerful feature of action_policy-graphql is letting clients query what actions a user can perform. This enables frontend code to conditionally render edit buttons, delete links, and other permission-dependent UI elements.

Add expose_authorization_rules to your type:

class Types::PostType < Types::BaseObject
expose_authorization_rules :edit?, :destroy?, :publish?
field :id, ID, null: false
field :title, String, null: false
field :body, String, null: true
end

Clients can then query:

{
post(id: "1") {
id
title
canEdit {
value
message
}
canDestroy {
value
message
}
}
}

The response includes both the boolean result and the failure reason message:

{
"data": {
"post": {
"id": "1",
"title": "Hello World",
"canEdit": {
"value": true,
"message": null
},
"canDestroy": {
"value": false,
"message": "Only administrators can delete posts"
}
}
}
}

Exception Handling

Unauthorized access raises ActionPolicy::Unauthorized. Handle it at the schema level:

class AppSchema < GraphQL::Schema
rescue_from(ActionPolicy::Unauthorized) do |error, _obj, _args, _ctx, _field|
raise GraphQL::ExecutionError.new(
error.message,
extensions: { code: "FORBIDDEN" }
)
end
end

This converts the Ruby exception into a GraphQL error that clients can handle appropriately.

Setting the Authorization Context

The authorization context (the user) is typically passed through the GraphQL context:

app/controllers/graphql_controller.rb
class GraphqlController < ApplicationController
def execute
context = {
current_user: current_user
}
result = AppSchema.execute(params[:query], context: context, ...)
render json: result
end
end

Then configure Action Policy to read from the GraphQL context:

class Types::BaseObject < GraphQL::Schema::Object
include ActionPolicy::GraphQL::Behaviour
# Tell Action Policy where to find the user in the GraphQL context
authorize :user, through: :current_user
end
Powered by WebContainers