Entity Permissions (ACLs)¶
Per-resource access control: "can user X view document Y?" Resources are registered with Sentinel using a generic model. Access is resolved through ownership, visibility, and explicit shares.
# Register a resource when it's created
await sentinel.permissions.register_resource(
resource_type="document",
resource_id=doc.id,
workspace_id=user.workspace_id,
owner_id=user.user_id,
visibility="workspace",
)
# Check access
allowed = await sentinel.permissions.can(token, "document", doc.id, "edit")
# Share with another user
await sentinel.permissions.share(
token=token,
resource_type="document",
resource_id=doc.id,
grantee_type="user",
grantee_id=collaborator_id,
permission="edit",
)
Resource Model¶
Every resource is identified by three fields, making the system generic across services:
| Field | Type | Example |
|---|---|---|
service_name |
string | "docu-store" |
resource_type |
string | "document" |
resource_id |
UUID | "a1b2c3d4-..." |
Additional fields on registration:
| Field | Type | Description |
|---|---|---|
workspace_id |
UUID | The workspace this resource belongs to |
owner_id |
UUID | The user who owns the resource |
visibility |
string | "private" or "workspace" (default: "workspace") |
The (service_name, resource_type, resource_id) tuple is unique. Registration is idempotent (upsert with ON CONFLICT DO NOTHING).
Visibility¶
| Mode | Behavior |
|---|---|
private |
Only the owner and users/groups with explicit shares can access |
workspace |
All workspace members can view; editors can also edit |
Change visibility after registration:
PATCH /permissions/{permission_id}/visibility
X-Service-Key: your-service-key
{ "visibility": "private" }
Permission Resolution¶
The check_permission function follows a 7-step algorithm, short-circuiting at the first match:
flowchart TD
Start([check_permission]) --> S1
S1{1. Resource registered?}
S1 -->|No| D1[DENY]
S1 -->|Yes| S2
S2{2. Same workspace?}
S2 -->|No| D2[DENY]
S2 -->|Yes| S3
S3{3. Is owner?}
S3 -->|Yes| A1[ALLOW]
S3 -->|No| S4
S4{4. Admin or owner role?}
S4 -->|Yes| A2[ALLOW]
S4 -->|No| S5
S5{5. Workspace-visible?}
S5 -->|No| S6
S5 -->|Yes| S5a{view?}
S5a -->|Yes| A3[ALLOW]
S5a -->|No| S5b{edit + editor role?}
S5b -->|Yes| A4[ALLOW]
S5b -->|No| S6
S6{6. User share?}
S6 -->|view share + view action| A5[ALLOW]
S6 -->|edit share + edit action| A6[ALLOW]
S6 -->|No match| S7
S7{7. Group share?}
S7 -->|view share + view action| A7[ALLOW]
S7 -->|edit share + edit action| A8[ALLOW]
S7 -->|No match| D3[DENY]
style D1 fill:#f44,color:#fff
style D2 fill:#f44,color:#fff
style D3 fill:#f44,color:#fff
style A1 fill:#4a4,color:#fff
style A2 fill:#4a4,color:#fff
style A3 fill:#4a4,color:#fff
style A4 fill:#4a4,color:#fff
style A5 fill:#4a4,color:#fff
style A6 fill:#4a4,color:#fff
style A7 fill:#4a4,color:#fff
style A8 fill:#4a4,color:#fff
Step by Step¶
- Resource registered? Look up by
(service_name, resource_type, resource_id). Not found = deny. - Same workspace? The resource's
workspace_idmust match the user's JWTwid. Cross-workspace = deny. - Owner? If
owner_id == user_id, full access (view and edit). - Admin/owner role? Workspace admins and owners have full access to all resources in the workspace.
- Workspace-visible? If
visibility = "workspace": all members can view; users witheditorrole can also edit. Viewers cannot edit unless they have an explicit share. - User share? Check
resource_sharesforgrantee_type = "user",grantee_id = user_id. Aviewshare grants view; aneditshare grants both view and edit. - Group share? Check
resource_sharesforgrantee_type = "group",grantee_id IN (user's group IDs from JWT). Same permission logic as user shares. - Default deny.
Share Types¶
Shares grant access to individual users or groups:
grantee_type |
permission |
Effect |
|---|---|---|
user |
view |
User can view the resource |
user |
edit |
User can view and edit the resource |
group |
view |
All group members can view |
group |
edit |
All group members can view and edit |
Creating Shares¶
The grantee must belong to the same workspace as the resource. Sharing with a user who isn't a workspace member or a group from another workspace returns 400 Bad Request.
# Share with a user (must be a workspace member)
await sentinel.permissions.share(
token=token,
resource_type="document",
resource_id=doc_id,
grantee_type="user",
grantee_id=user_id,
permission="edit",
)
# Share with a group
await sentinel.permissions.share(
token=token,
resource_type="document",
resource_id=doc_id,
grantee_type="group",
grantee_id=group_id,
permission="view",
)
Revoking Shares¶
DELETE /permissions/{permission_id}/share
X-Service-Key: your-service-key
{ "grantee_type": "user", "grantee_id": "user-uuid" }
Accessible Resources Lookup¶
List all resource IDs a user can access, useful for filtered list views:
resource_ids, has_full_access = await sentinel.permissions.accessible(
token=token,
resource_type="document",
action="view",
workspace_id=workspace_id,
)
if has_full_access:
# Admin/owner — skip filtering, show everything
docs = await get_all_docs(workspace_id)
else:
docs = await get_docs_by_ids(resource_ids)
When has_full_access is True (admin/owner with no limit), resource_ids may be empty -- the caller should skip ID filtering entirely.
Database Schema¶
resource_permissions One row per registered resource
UNIQUE(service_name, resource_type, resource_id)
FK workspace_id -> workspaces (CASCADE)
FK owner_id -> users (SET NULL)
CHECK visibility IN ('private', 'workspace')
resource_shares Grants on resources
FK resource_permission_id -> resource_permissions (CASCADE)
UNIQUE(resource_permission_id, grantee_type, grantee_id)
CHECK grantee_type IN ('user', 'group')
CHECK permission IN ('view', 'edit')