Access Control

Understanding ReBAC permission system and authorization

Updated Nov 27, 2025
Edit on GitHub

Access Control

The SSO platform implements a Relationship-Based Access Control (ReBAC) system inspired by Google Zanzibar. This provides fine-grained, scalable authorization that supports both direct user permissions and inherited permissions through organizational relationships.

Overview

The access control system replaces traditional role-based access control (RBAC) with a more flexible ReBAC model. Instead of assigning fixed roles, the system grants permissions based on relationships between users, organizations, and resources.

Relation Tuples

At the core of the ReBAC system are relation tuples, which represent permissions using the format:

object#relation@subject

Components

  • Object: The resource being accessed, consisting of:
    • namespace: The resource type (organization, service, plan, etc.)
    • object_id: The specific resource instance ID
  • Relation: The permission type (owner, admin, member, viewer, etc.)
  • Subject: The entity receiving the permission, which can be:
    • A direct user grant: user:user_id
    • A userset (inherited permission): namespace:object_id#relation

Examples

Direct User Permission:

organization:org-123#admin@user:user-456

User user-456 has the admin relation on organization org-123.

Userset Permission (Inherited):

service:svc-abc#viewer@organization:org-123#member

All members of organization org-123 inherit the viewer relation on service svc-abc.

Namespaces

The system defines several resource namespaces:

Namespace Description
organization Multi-tenant organizations
service Applications using the SSO
plan Subscription plans
webhook Webhook configurations
api_key API authentication keys
user End-users

Permission Relations

Standard relations supported by the system:

Relation Description Typical Use
owner Full control including deletion Organization creator
admin Administrative access, can manage members Organization administrators
member Basic membership access Regular organization members
viewer Read-only access Auditors, observers
editor Modify resource content Content managers
manager Manage resource settings Resource managers

Permission Hierarchy

The system implements hierarchical permissions through userset expansion. When checking if a user has a permission, the authorization engine:

  1. Checks direct grants - Does the user have a direct permission tuple?
  2. Expands usersets - Does the user belong to any groups that grant this permission?
  3. Recursive expansion - Continues expanding until all paths are checked (max depth: 10)

Example: Organization Membership

When a user joins an organization as a member:

  1. System creates: organization:org-123#member@user:user-456
  2. Service access is granted via: service:svc-abc#viewer@organization:org-123#member
  3. User can now view the service through inherited permission

Roles vs Permissions

The system maintains the concept of “roles” (Owner, Admin, Member) for user-facing interfaces, but internally these map to specific permission relations:

Role Permissions Granted
Owner owner relation on the organization
Admin admin relation on the organization
Member member relation on the organization

Important: The API enforces permissions (e.g., write:users, manage:services), not roles. Roles are semantic groupings of permissions for organizational convenience.

Permission Checks

The authorization engine implements Zanzibar’s Check algorithm with optimizations:

PermissionsStore::check(
    db,
    "organization",  // namespace
    "org-123",       // object_id
    "admin",         // relation
    "user-456"       // user_id
)

Algorithm

  1. Direct Check: Query for user:user-456 with the exact relation
  2. Userset Expansion: Find all userset grants for this permission
  3. Recursive Check: For each userset, check if the user has the required relation
  4. Depth Limiting: Prevents circular dependencies (max depth: 10)
  5. Cycle Detection: Tracks visited checks to avoid infinite loops

Permission Operations

Granting Permissions

// Direct user permission
let tuple = RelationTuple::user(
    Namespace::Organization,
    "org-123",
    "admin",
    "user-456"
);
PermissionsStore::grant(db, tuple).await?;

// Userset permission
let tuple = RelationTuple::userset(
    Namespace::Service,
    "svc-abc",
    "viewer",
    Namespace::Organization,
    "org-123",
    PermissionRelation::Member
);
PermissionsStore::grant(db, tuple).await?;

Revoking Permissions

PermissionsStore::revoke(
    db,
    "organization",
    "org-123",
    "admin",
    "user",
    "user-456",
    None  // subject_relation
).await?;

Expanding Permissions

The Expand algorithm returns all users who have a specific permission:

// Get all users who can view this service
let user_ids = PermissionsStore::expand(
    db,
    "service",
    "svc-abc",
    "viewer"
).await?;

Database Schema

The permissions table stores relation tuples:

CREATE TABLE permissions (
    id TEXT PRIMARY KEY,
    namespace TEXT NOT NULL,
    object_id TEXT NOT NULL,
    relation TEXT NOT NULL,
    subject_type TEXT NOT NULL,
    subject_id TEXT NOT NULL,
    subject_relation TEXT,
    created_at TEXT NOT NULL
);

Indexes

The table includes optimized indexes for:

  • Permission checks: (namespace, object_id, relation, subject_type, subject_id)
  • Userset expansion: (namespace, object_id, relation)
  • User permission listing: (subject_type, subject_id)
  • Object cleanup: (namespace, object_id)
  • Uniqueness: Composite unique constraint on all tuple fields

Service vs Organization API

The SSO platform provides two distinct APIs with different authorization contexts:

Organization API

  • Authentication: JWT tokens issued to human administrators
  • Authorization: Based on organization membership permissions
  • Use Case: Human administrators managing their organization
  • Endpoints: /api/organizations/*, /api/user/*

Service API

  • Authentication: API keys with SHA256 hashing
  • Authorization: Service-scoped permissions
  • Use Case: Machine-to-machine service interactions
  • Endpoints: /api/service/*

Security Considerations

Circular Dependencies

The permission check algorithm includes cycle detection to prevent infinite loops when permission relationships form cycles.

Performance

  • Permissions checks are optimized with database indexes
  • The system uses SQLite with WAL mode for concurrent reads
  • Maximum recursion depth prevents expensive queries
  • Visited set prevents redundant checks

Cleanup

When deleting resources, all associated permissions must be cleaned up:

PermissionsStore::delete_object_permissions(
    db,
    "organization",
    "org-123"
).await?;

Common Patterns

Organization Membership

// Grant membership
PermissionsStore::grant_org_membership(
    db, "org-123", "user-456", "member"
).await?;

// Check membership
let is_member = PermissionsStore::is_org_member(
    db, "org-123", "user-456"
).await?;

Service Access

// Grant entire organization access to service
PermissionsStore::grant_service_access_to_org(
    db, "svc-abc", "org-123", "viewer"
).await?;

Admin Checks

// Check if user is owner or admin
let is_admin = PermissionsStore::is_org_owner_or_admin(
    db, "org-123", "user-456"
).await?;

Migration from Roles

The platform migrated from a traditional membership-based roles system to ReBAC. The migration populated the permissions table with relation tuples derived from the legacy memberships table, preserving all existing access relationships.

Organizations using the API experienced no disruption, as the role-based concepts remain in the API surface while the underlying enforcement uses ReBAC.