Custom Roles (RBAC)¶
The Sentinel Auth implements NIST Core RBAC (Level 1) as a third authorization layer between workspace roles and entity ACLs. Custom roles allow services to define application-specific actions (like "export reports" or "manage templates") and assign them to users through named roles within a workspace.
Three-Tier Authorization Model¶
Workspace Roles (JWT) → coarse tier: owner/admin/editor/viewer
Custom Roles (API check) → action-based: "can user do reports:export?"
Entity ACLs (Zanzibar) → per-resource: "can user edit document X?"
| Tier | Storage | Check Speed | Use Case |
|---|---|---|---|
| Workspace Roles | JWT claims | Instant (local) | Broad access control (create, manage, delete) |
| Custom Roles | Database query | Sub-millisecond | Action-based authorization (export, approve, configure) |
| Entity ACLs | Database query | Sub-millisecond | Per-resource access (view doc X, edit project Y) |
Concepts¶
Service Actions¶
Service actions are the atomic units of authorization. Each action belongs to a service_name and has a unique action identifier. Actions are registered by backend services using service-key authentication.
Action naming convention: ^[a-z][a-z0-9_.:-]*$
Examples:
- reports:export
- templates:manage
- billing:view
- settings:admin.configure
Roles¶
Roles are workspace-scoped collections of service actions. They are created by workspace admins (or platform admins via the admin panel) and can contain actions from multiple services.
Example roles:
- Analyst: reports:export, reports:view, dashboards:view
- Template Manager: templates:create, templates:edit, templates:delete
- Billing Admin: billing:view, billing:manage, invoices:export
User Role Assignments¶
Users are assigned to roles within a workspace. A user can have multiple roles, and the effective set of allowed actions is the union of all their role assignments.
Security Properties¶
- Workspace-scoped only: Roles exist only within a workspace -- no global roles. This maintains tenant isolation.
- Namespaced actions: Actions are scoped by
service_nameto prevent namespace collisions between services. - Registered actions only: Actions must be pre-registered by services (using service-key auth) before they can be added to roles. This prevents privilege escalation through invented action names.
- Real-time checks: Action checks are always live database queries, never cached in JWTs. Revoking a role takes effect immediately.
- CASCADE delete: Deleting a workspace removes all its roles and user assignments. Deleting a user removes all their role assignments.
Workflow¶
1. Service Registers Actions¶
When your service starts up (or during deployment), register the actions it supports:
POST /roles/actions/register
X-Service-Key: sk_your_service_key
{
"service_name": "analytics",
"actions": [
{"action": "reports:export", "description": "Export reports as CSV/PDF"},
{"action": "reports:view", "description": "View report data"},
{"action": "dashboards:create", "description": "Create new dashboards"}
]
}
Registration is idempotent -- re-registering existing actions updates their descriptions without creating duplicates.
2. Admin Creates Roles¶
Through the admin panel or API, create roles in a workspace and assign actions:
POST /admin/workspaces/{workspace_id}/roles
{
"name": "Analyst",
"description": "Can view and export reports"
}
Then add actions to the role:
POST /admin/roles/{role_id}/actions
{
"service_action_ids": ["uuid-of-reports-export", "uuid-of-reports-view"]
}
3. Admin Assigns Users to Roles¶
4. Service Checks Actions at Runtime¶
Using the SDK:
from sentinel_auth.roles import RoleClient
roles = RoleClient(
base_url="http://identity-service:8000",
service_name="analytics",
service_key="sk_your_service_key",
)
# Check a single action
allowed = await roles.check_action(user_token, "reports:export", workspace_id)
# List all actions the user can perform
actions = await roles.get_user_actions(user_token, workspace_id)
# → ["reports:export", "reports:view"]
Or using the require_action dependency:
from sentinel_auth.dependencies import require_action
@router.get("/reports/export")
async def export_report(
user: AuthenticatedUser = Depends(require_action(roles, "reports:export")),
):
# User is guaranteed to have the reports:export action
return generate_report()
When to Use Each Tier¶
| Scenario | Tier |
|---|---|
| "Can this user create resources?" | Workspace role (require_role("editor")) |
| "Can this user export reports?" | Custom role (require_action(roles, "reports:export")) |
| "Can this user view this specific document?" | Entity ACL (permissions.can(token, "document", doc_id, "view")) |
Database Schema¶
Four tables support the RBAC system:
service_actions -- Registry of valid actions per service
├── UNIQUE(service_name, action)
└── INDEX(service_name)
roles -- Custom roles per workspace
├── FK workspace_id → workspaces (CASCADE)
├── FK created_by → users (SET NULL)
├── UNIQUE(workspace_id, name)
└── INDEX(workspace_id)
role_actions -- Links roles to registered actions
├── FK role_id → roles (CASCADE)
├── FK service_action_id → service_actions (CASCADE)
├── UNIQUE(role_id, service_action_id)
└── INDEX(role_id)
user_roles -- User-to-role assignments
├── FK user_id → users (CASCADE)
├── FK role_id → roles (CASCADE)
├── FK assigned_by → users (SET NULL)
├── UNIQUE(user_id, role_id)
└── INDEX(user_id), INDEX(role_id)
The core action check is a 4-table join that resolves in sub-millisecond time with the indexed foreign keys.