GraphQL Mutation Authorization
The authorization layer is under active development. Implementation details and documentation may change as we iterate. For questions or issues, please reach out to the team.
This guide explains how to properly implement authorization for GraphQL mutations in Arc. Our authorization system combines ActionPolicy with a custom IAM engine for role-based access control.
Adding Authorization to a New Mutation
Step 1: Define the Permission
First, ensure the required permission exists in the inventory:
# modules/iam/app/permissions/inventory.yml
resources:
care_pod:
- action: delete
description: Delete Care Pods
Step 2: Create the Mutation
Create your mutation class inheriting from BaseMutation:
# app/graphql/mutations/delete_care_pod.rb
module Mutations
class DeleteCarePod < BaseMutation
field :care_pod, Types::CarePodType, null: true
argument :care_pod_id, String, required: true, loads: Types::CarePodType
def resolve(care_pod:)
care_pod.destroy!
{
errors: [],
}
end
def authorized?(care_pod:)
authorize! care_pod, to: 'care_pod:delete'
end
def load_care_pod(care_pod_id)
CarePod.find(care_pod_id)
end
end
end
Step 3: Policy Configuration (Optional)
By default, authorization will automatically delegate to the IAM engine via the can?
method in ApplicationPolicy
. You do not need to create a custom policy class unless you require custom business logic.
When a Custom Policy is Not Needed
For most cases, the default behavior is sufficient. ActionPolicy will automatically:
- Look for a policy class matching your resource (e.g.,
CarePodPolicy
forCarePod
objects) - If no custom policy exists, fall back to
ApplicationPolicy
- Use the
can?
method which delegates to the IAM engine
When to Create a Custom Policy
Create a custom policy class only if you need custom business logic before IAM checks. For example:
- Feature flag checks before authorization
- Complex business rules that go beyond simple role-based permissions
If you need custom logic, see the Advanced: Custom Logic Before IAM Checks section below for implementation details.
Creating a Custom Policy (If Needed)
If custom logic is required, create a policy class inheriting from ApplicationPolicy
:
# app/policies/care_pod_policy.rb
class CarePodPolicy < ApplicationPolicy; end
Step 4: Update Role Permissions
Ensure the relevant roles have the required permission. Our system follows a deny-by-default approach, meaning users will receive authorization errors unless their role explicitly includes the permission:
# modules/iam/app/permissions/admin.yml
permissions:
care_pod:
delete: {}
Step 5: Add Tests
Write tests to verify both authorization and functionality of your mutation.
Create a test file in spec/graphql/mutations/
and use the shared authorization example from spec/support/graphql/mutation_authorization_shared_examples.rb
. The shared example expects query
and params
to be defined:
# spec/graphql/mutations/delete_care_pod_spec.rb
describe Mutations::DeleteCarePod do
# ... setup code (query, params, etc.) ...
include_examples 'GQL: Returns an error when the user is unauthorized',
required_permission: 'care_pod:delete'
it 'deletes the care pod successfully' do
expect { result }.to change { CarePod.count }.by(-1)
end
end
Authorization Flow
When a mutation executes, the authorization flow follows these steps:
- Object Loading: The
loads:
option automatically loads the object usingload_#{argument}
method - Object Read Authorization: When an object is loaded with
loads:
, GraphQL automatically calls the object type'sauthorized?
method to ensure the current user has permission to "read" that object - Mutation Authorization Check: The mutation's
authorized?
method is called with the loaded objects - Policy Evaluation: ActionPolicy's
authorize!
calls the appropriate policy method - IAM Engine Check: The policy uses
can?
method which delegates to the IAM engine - Permission Validation: IAM engine checks role permissions against the inventory
This means that before your mutation's custom authorization logic even runs, GraphQL has already verified that the user can access the loaded objects. This provides an additional layer of security by ensuring users can only attempt operations on objects they're allowed to see.
Advanced: Custom Logic Before IAM Checks
Sometimes you need custom business logic before falling back to IAM permissions. Here's how to implement this pattern:
Example: User Self-Update
# app/graphql/mutations/update_user.rb
module Mutations
class UpdateUser < BaseMutation
# ... fields and arguments ...
def authorized?(user:, **args)
# `user` is the user being updated, not the current_user
authorize! user, to: :update?
end
end
end
# app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy
def update?
# Custom logic: Users can always update their own information
# before checking IAM permissions
record.id == user.id || can?('user:update', record)
end
end
This pattern allows for adding custom checks before the IAM engine validates permissions. It is useful for scenarios where business rules require special handling, such as checking a Feature Flag.
Error Handling
Authorization Failures
When authorization fails, ActionPolicy
raises ActionPolicy::Unauthorized
exceptions, which are automatically handled by GraphQL-Ruby and returned as errors
in the response.
Custom Error Messages
You can customize authorization error messages using i18n files. Define custom messages in your locale file:
# config/locales/actionpolicy/en-US.yml
en-US:
action_policy:
policy:
care_pod:
delete?: "You don't have permission to delete care pods"
user:
update?: "You are not authorized to update user information"
The message key should match the policy class name and method. For example, care_pod.delete?
corresponds to the delete?
method in CarePodPolicy
. When authorization fails, ActionPolicy will automatically use these custom messages instead of the default ones.
Best Practices
1. Use Descriptive Permission Names
# Good
authorize! care_pod, to: 'care_pod:delete'
# Avoid
authorize! care_pod, to: 'destroy'
2. Implement Custom Load Methods When Needed
def load_care_pod(care_pod_id)
# Add custom loading logic, scoping, etc.
CarePod.where(organization: current_user.organization).find(care_pod_id)
end
3. Keep Authorization Logic in Policies
# Good: Logic in policy
def authorized?(user:)
authorize! user, to: :update?
end
# Avoid: Logic in mutation
def authorized?(user:)
user.id == current_user.id || authorize! user, to: 'user:update'
end
4. Document Custom Authorization Logic
def update?
# Business rule: Users should always be able to update their own profile
# regardless of their role permissions
record.id == user.id || can?('user:update', record)
end
Troubleshooting
Common Issues
- Permission Not Found: Ensure the permission exists in
inventory.yml
- Role Not Mapped: Check that the user's role has the required permission in the role policy file
- Object Loading Fails: Verify the
load_#{argument}
method and object existence
Debugging Authorization
Use the IAM engine directly in Rails console to debug:
# Check if a user can perform an action
IAM::Engine.can?(user, IAM::Permission.from_str('care_pod:delete'), care_pod)
# Check if a role is mapped
IAM::Engine.role_mapped?(user.role)