Skip to main content

Authorization Flow

Work in Progress

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 the resolve 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 an authorize_read! method that simplifies object authorization by automatically generating the authorized? 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:

  1. Looks for a policy class matching your resource (e.g., CarePodPolicy for CarePod objects)
  2. If no custom policy exists, falls back to ApplicationPolicy
  3. 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.

info

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: {}