GraphQL Object 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 implement authorization for GraphQL object types in Arc. Object authorization ensures that users can only access objects they have permission to read, and provides additional authorization rules for specific actions on those objects.
Overview
Our object authorization system works at two levels:
- Read Authorization: Controls whether a user can access an object at all
- Action Authorization: Exposes specific permission checks as fields on the object
When GraphQL resolves an object type, it automatically calls the object's authorized?
method to verify read access. If authorization fails, an error is thrown to the client.
Key Components
Types::Authorizable
Module
The Types::Authorizable
module (app/graphql/types/authorizable.rb) provides helper methods for configuring object authorization:
authorize_read!
: Sets up automatic read authorization using IAM permissionsexpose_authorization_rules
: Adds authorization check fields to the GraphQL type (see ActionPolicy GraphQL docs)
Object Policies
By default, authorization delegates to the IAM engine via ApplicationPolicy
. Custom policy classes in app/policies/
are only needed when you require custom authorization logic before falling back to IAM checks.
Adding Authorization to a New Object Type
These steps are only necessary when the GraphQL Object being authorized contains PHI or other meaningful information that requires protection.
For objects that don't contain sensitive data, you can bypass the authorization check by adding this method to the GraphQL Object:
def self.authorized?(**args)
true
end
Step 1: Define Read Permission
Ensure the read permission exists in the inventory:
# modules/iam/app/permissions/inventory.yml
resources:
chart:
- action: read
description: Read access to Chart and Profile records
Step 2: Create the Object Type
Create your object type and include the Types::Authorizable
module:
# app/graphql/types/chart_type.rb
module Types
class ChartType < Types::BaseObject
include Types::Authorizable
# Configure automatic read authorization
authorize_read! 'chart:read'
# Expose action-specific authorization rules as fields
expose_authorization_rules 'chart:update', field_name: :can_update
field :id, ID, null: false
field :profile, ProfileType, null: false
# ... other fields
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.,
ChartPolicy
forChart
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 (as shown in Custom Policy Methods below)
- Complex business rules that go beyond simple role-based permissions
Creating a Custom Policy (If Needed)
If custom logic is required, create a policy class inheriting from ApplicationPolicy
:
# app/policies/chart_policy.rb
class ChartPolicy < ApplicationPolicy; end
Step 4: Update Role Permissions
Since the authorization system follows a deny-by-default approach, you must explicitly grant the required permissions to the appropriate roles. Users will receive authorization errors when accessing objects unless their role has the necessary permissions.
Identifying Required Roles
Before updating role permissions, determine which roles should have access to your object:
- Consider business workflows: Think about which user types need to access this object type
- Check similar resources: Look at permissions for similar object types in existing role files
PM Validation
Important: Validate your role assignments with a PM before implementing them. Share:
- Which roles you plan to grant the permission to
- Your reasoning for each role assignment
- Any potential impact on user workflows
This validation is critical because the deny-by-default policy means any role not explicitly granted permission will be unable to access the object, potentially breaking existing user workflows.
Updating Role Files
Once validated, add the required read permission to the appropriate role files:
# modules/iam/app/permissions/admin.yml
permissions:
chart:
read: {}
update: {}
# modules/iam/app/permissions/ecm_cm.yml
permissions:
chart:
read: {}
# modules/iam/app/permissions/chw.yml
permissions:
chart:
read: {}
Testing Access
After updating role permissions, verify that:
- Users with the granted roles can access the object
- Users without the permission receive an error
- No existing workflows are broken
Authorization Patterns
1. Basic Read Authorization
The simplest pattern uses only IAM-based read authorization:
module Types
class CarePodType < Types::BaseObject
include Types::Authorizable
authorize_read! 'care_pod:read'
field :id, ID, null: false
field :name, String, null: false
# ... other fields
end
end
2. Expose permission Checks as Fields
Expose additional authorization checks as GraphQL fields:
module Types
class DocumentType < Types::BaseObject
include Types::Authorizable
authorize_read! 'document:read'
# Expose action permissions as fields
expose_authorization_rules 'document:update', field_name: :can_update
expose_authorization_rules 'document:delete', field_name: :can_delete
field :id, ID, null: false
# ... other fields
end
end
This allows frontend clients to check permissions:
query {
document(id: "123") {
id
description
canUpdate {
value # Boolean field indicating if user can update
message
}
canDelete {
value # Boolean field indicating if user can delete
message
}
}
}
3. Custom Policy Methods
Use custom policy methods for complex authorization logic:
# app/policies/chart_policy.rb
module Types
class ChartType < Types::BaseObject
include Types::Authorizable
authorize_read! :read?
# Use custom policy method with specific policy class
expose_authorization_rules :create?, with: CHW::EncounterPolicy, field_name: :can_create_chw_encounter
# Use custom policy method from the default ChartPolicy
expose_authorization_rules :read_for_auth_prompt?, field_name: "can_read"
end
end
# app/policies/chart_policy.rb
class ChartPolicy < ApplicationPolicy
def read?
# Custom logic to determine if the user can read the chart
return true unless Arc::Feature.enabled_for_user?(:rbac_chart_read)
can?('chart:read', record)
end
end
Authorization Flow
When GraphQL resolves an object, the authorization flow works as follows:
- Object Resolution: GraphQL resolves a field that returns an object
- ActionPolicy Authorization: ActionPolicy handles the authorization check, calling the appropriate policy method
- Custom Logic Evaluation: If custom policy logic exists, it's executed first
- IAM Engine Delegation: If no custom logic exists (or custom logic delegates), ActionPolicy calls the IAM engine
- Permission Validation: IAM engine checks role permissions against the inventory
Example Flow for ChartType
- GraphQL resolves a chart object
ChartType.authorized?(chart, context)
is called- This calls
current_user.can?('chart:read', chart)
- ActionPolicy creates an instance of
ChartPolicy
and callscan?('chart:read', chart)
on it - IAM engine checks if user's role has 'chart:read' permission
- If authorized, chart data is returned
Troubleshooting
Common Issues
- Objects Return Error: Check if the read permission exists and the user's role has access
- Authorization Fields Missing: Ensure
expose_authorization_rules
is configured correctly - Policy Method Not Found: Verify the policy class exists and the method is defined
Debugging Object Authorization
Test authorization in Rails console:
# Test read permission directly
chart = Chart.find(123)
user = User.find(456)
user.can?('chart:read', chart)
# Test GraphQL type authorization
Types::ChartType.authorized?(chart, { current_user: user })
Performance Considerations
Authorization checks are called for every object, so keep policy methods efficient.