Authorization Flow
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 document explains how Arc's authorization system works, including the flow of authorization requests, key components, and their interactions. Arc uses a combination of ActionPolicy and a custom IAM (Identity and Access Management) engine to provide role-based access control.
System Overview
Arc's authorization system follows a deny-by-default approach where users must have explicit permissions to access resources or perform actions. The system combines:
- ActionPolicy: Rails authorization framework for policy-based access control (GitHub)
- Custom IAM Engine: Role-based permission checking with YAML configuration
- GraphQL Integration: Automatic authorization for objects and mutations
Core Components
1. IAM Engine (modules/iam/app/models/iam/engine.rb
)
The IAM Engine is the core of the authorization system. It:
- Loads role policies and permission inventory from YAML files
- Validates permissions against the inventory
- Checks if users have required permissions based on their roles
- Provides the main
can?
method for permission checking
Permission Inventory (modules/iam/app/permissions/inventory.yml
)
Central registry that:
- Defines all available permissions in the system
- Uses
resource:action
format (e.g.,chart:read
,care_pod:delete
) - Provides permission descriptions for documentation
- Validates that only defined permissions can be used
Role Policies (modules/iam/app/permissions/*.yml
)
YAML files that:
- Map roles to their allowed permissions
- Follow the pattern:
role_name.yml
(e.g.,admin.yml
,ecm_cm.yml
) - Define which permissions each role has access to
2. ActionPolicy Gem
ActionPolicy is the authorization library that:
- Handles authorization calls throughout the application
- Supports custom policy logic for complex business rules
- Falls back to IAM Engine for role-based permission checking
- Integrates seamlessly with GraphQL mutations and objects
3. GraphQL Integration
Ruby-GraphQL has an authorization mechanism we use to ensure that the data we're returning to the user (Objects) and the actions that users execute (Mutations) are authorized.
Authorization Flow
1. GraphQL Authorization Layer
GraphQL serves as our first authorization layer. The Ruby-GraphQL framework has built-in authorization functionality that automatically calls authorization methods before processing requests:
- Objects: The framework calls
authorized?(object, context)
on each Object Type before returning it to the client, passing the entity being accessed (Ruby-GraphQL Object Authorization) - Mutations: The framework calls
authorized?()
before executing theresolve
method, passing all received arguments and the context (Ruby-GraphQL Mutation Authorization)
Inside these authorized?
methods, we call ActionPolicy's authorize!
method with the object, current user, and the specific permission we want to check:
# Object authorization example
module Types
class ChartType < Types::BaseObject
def authorized?(object, context)
authorize! object, to: 'chart:read'
end
end
end
# Mutation authorization example
module Mutations
class DeleteCarePod < BaseMutation
def authorized?(care_pod:)
authorize! care_pod, to: 'care_pod:delete'
end
end
end
Note: The
Types::Authorizable
helper module (used only for Objects) includes anauthorize_read!
method that simplifies object authorization by automatically generating theauthorized?
method for read permissions.
2. ActionPolicy
We use the ActionPolicy gem as our authorization framework. By default, authorization delegates to the IAM engine through ApplicationPolicy
.
Default Behavior (Most Common)
For most resources, no custom policy is needed. ActionPolicy automatically:
- Looks for a policy class matching your resource (e.g.,
CarePodPolicy
forCarePod
objects) - If no custom policy exists, falls back to
ApplicationPolicy
- Uses the
can?
method which delegates to the IAM engine
# No custom policy needed - uses ApplicationPolicy automatically
module Mutations
class DeleteCarePod < BaseMutation
def authorized?(care_pod:)
authorize! care_pod, to: 'care_pod:delete'
end
end
end
Custom Policies (When Needed)
Create custom policy classes only for complex business rules that go beyond role-based permissions:
# app/policies/user_policy.rb
class UserPolicy < ApplicationPolicy
def update?
# Custom rule: users can update their own profile
record.id == user.id || can?('user:update', record)
end
private
# This method is inherited from ApplicationPolicy
# def can?(action, record = nil)
# IAM::Engine.can?(user, IAM::Permission.from_str(action), record)
# end
end
3. IAM Engine
Last but not least, the Policy calls the IAM Engine for checking the user permission. The engine is responsible for checking if the role the current user has allows them to perform the action. The Engine maintains an in-memory copy of the YAML definition for each role and uses that to perform the check.
The IAM Engine logic is really simple for the time being, but we'll keep adding more functionality to it.
Configuration
The IAM Engine relies on two key configuration files:
Permission Inventory
Permissions follow the format resource:action
and must be defined in the inventory before use:
# modules/iam/app/permissions/inventory.yml
resources:
chart:
- action: read
description: Read access to Chart and Profile records
- action: update
description: Update demographics and relationships
Role Policies
Role policies define which permissions each role has:
# modules/iam/app/permissions/admin.yml
name: Admin
id: admin
permissions:
chart:
read: {}
create: {}
update: {}
care_pod:
read: {}
delete: {}
Related Documentation
- GraphQL Mutation Authorization: Detailed guide for implementing mutation authorization
- GraphQL Object Authorization: Guide for authorizing GraphQL object types
- Adding New Roles: Process for creating new roles in the system