Skip to main content

GraphQL Object Authorization

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

  1. Read Authorization: Controls whether a user can access an object at all
  2. 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 permissions
  • expose_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

When to Add Authorization

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:

  1. Look for a policy class matching your resource (e.g., ChartPolicy for Chart objects)
  2. If no custom policy exists, fall back to ApplicationPolicy
  3. 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:

  1. Consider business workflows: Think about which user types need to access this object type
  2. 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:

  1. Object Resolution: GraphQL resolves a field that returns an object
  2. ActionPolicy Authorization: ActionPolicy handles the authorization check, calling the appropriate policy method
  3. Custom Logic Evaluation: If custom policy logic exists, it's executed first
  4. IAM Engine Delegation: If no custom logic exists (or custom logic delegates), ActionPolicy calls the IAM engine
  5. Permission Validation: IAM engine checks role permissions against the inventory

Example Flow for ChartType

  1. GraphQL resolves a chart object
  2. ChartType.authorized?(chart, context) is called
  3. This calls current_user.can?('chart:read', chart)
  4. ActionPolicy creates an instance of ChartPolicy and calls can?('chart:read', chart) on it
  5. IAM engine checks if user's role has 'chart:read' permission
  6. If authorized, chart data is returned

Troubleshooting

Common Issues

  1. Objects Return Error: Check if the read permission exists and the user's role has access
  2. Authorization Fields Missing: Ensure expose_authorization_rules is configured correctly
  3. 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.

References