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:
# Gemfilegem "action_policy"gem "action_policy-graphql"Setup
Include the behaviour module in your base types and mutations:
class Types::BaseObject < GraphQL::Schema::Object include ActionPolicy::GraphQL::Behaviourend
# app/graphql/mutations/base_mutation.rbclass Mutations::BaseMutation < GraphQL::Schema::RelayClassicMutation include ActionPolicy::GraphQL::BehaviourendField-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) endendYou 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 endendThe difference from authorize:
authorize: trueruns after the resolver, checking the returned objectpreauthorize:runs before the resolver, using the implicit target (oftennil)
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 endendScope 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 endendThis 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: trueendClients 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" } ) endendThis 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:
class GraphqlController < ApplicationController def execute context = { current_user: current_user } result = AppSchema.execute(params[:query], context: context, ...) render json: result endendThen 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_userend