Permission System
Cloudillo’s permission system has two pillars that work together to control access to all resources:
- ABAC Policies (profile/community-level) — Configurable TOP and BOTTOM policy rules that define hard constraints and guarantees
- Discretionary Access Control — The content creator’s own choices: visibility levels, explicit audience, file shares, and access grants
These two pillars combine in a layered evaluation:
1. TOP POLICY (ABAC) → Hard constraints — what is NEVER allowed
↓
2. BOTTOM POLICY (ABAC) → Hard guarantees — what is ALWAYS allowed
↓
3. DISCRETIONARY ACCESS → Creator's choices: visibility, shares, ownership
↓
4. DEFAULT DENY → If nothing matched, deny accessPillar 1: ABAC Policies
Attribute-Based Access Control evaluates rules based on attributes of users, resources, and context. In Cloudillo, ABAC is used for profile-level (community/company) policies that set boundaries around the discretionary access decisions.
The Four-Object Model
ABAC decisions involve four types of objects:
1. Subject (Who)
The user or entity requesting access.
pub struct AuthCtx {
pub tn_id: TnId, // Tenant ID (database key)
pub id_tag: Box<str>, // Identity (e.g., "alice.example.com")
pub roles: Box<[Box<str>]>, // Roles (e.g., ["USR", "moderator"])
pub scope: Option<Box<str>>, // Optional scope (e.g., "apkg:publish")
}2. Action (What)
The operation being attempted, in resource:operation format:
file:read → Read a file
file:write → Modify a file (CRDT/RTDB files only)
file:delete → Delete a file
action:read → View an action token
action:create → Create an action token
profile:update → Update a profile
profile:admin → Administrative access to profileAction tokens are immutable
Action tokens are cryptographically signed JWTs. Once created, they cannot be modified — only viewed, accepted/rejected (status change), or revoked. There is no action:write operation.
3. Object (Resource)
The resource being accessed. Must implement the AttrSet trait:
pub trait AttrSet: Send + Sync {
fn get(&self, key: &str) -> Option<&str>;
fn get_list(&self, key: &str) -> Option<Vec<&str>>;
fn contains(&self, key: &str, value: &str) -> bool;
}4. Environment (Context)
Contextual factors for the request:
pub struct Environment {
pub time: Timestamp, // Current Unix timestamp
}TOP Policy (Constraints)
Defines maximum permissions — what is never allowed, regardless of discretionary settings. Evaluated first; if a rule matches with Deny effect, access is immediately denied.
Use case: Community or company-wide restrictions.
Examples:
TopPolicy:
Rule 1:
Condition: visibility == "public" AND size > 100MB
Effect: DENY
# Community rule: files larger than 100MB cannot be shared publicly
Rule 2:
Condition: subject.banned == true
Effect: DENY
# Banned users cannot access any resourceBOTTOM Policy (Guarantees)
Defines minimum permissions — what is always allowed, regardless of other rules. Evaluated second; if a rule matches with Allow effect, access is immediately granted.
Use case: Platform guarantees and special role privileges.
Examples:
BottomPolicy:
Rule 1:
Condition: subject.id_tag == resource.owner
Effect: ALLOW
# Owner can always access their own resources
Rule 2:
Condition: subject.HasRole("leader")
Effect: ALLOW
# Community leaders have full accessPolicy Operators
ABAC supports these operators for building policy rules:
Comparison: Equals, NotEquals, GreaterThan, LessThan
Set: Contains, NotContains, In
Role: HasRole — checks if subject has a specific role
Logical: And, Or — combine conditions
Pillar 2: Discretionary Access Control
Between the TOP and BOTTOM policies, discretionary access control determines access based on the content creator’s own choices. This is the primary day-to-day access mechanism.
Ownership
The simplest check: owners always have full access to their own resources.
For communities, the tenant profile is treated as equivalent to the owner — community administrators (with the “leader” role) have the same access as the resource owner.
if resource.owner == subject.id_tag → ALLOW
if subject.id_tag == tenant_id_tag → ALLOW (tenant = owner equivalent)Visibility Levels
Content creators set visibility when creating resources. This is a discretionary choice stored as a single character in the database (or NULL for direct).
Hierarchy (most to least permissive):
| Code | Level | Who can access |
|---|---|---|
P |
Public | Anyone, including unauthenticated users |
V |
Verified | Any authenticated user from any federated instance |
2 |
SecondDegree | Friend of friend (reserved for voucher token system) |
F |
Follower | Authenticated users who follow the owner |
C |
Connected | Authenticated users with mutual connection |
NULL |
Direct | Only owner + explicit audience |
The system computes the subject’s access level based on their relationship with the resource owner, then checks if it meets the visibility requirement:
| Subject Access Level | Description |
|---|---|
Owner |
Is the resource owner (highest) |
Connected |
Has mutual CONN with owner |
Follower |
Has FLLW to owner |
SecondDegree |
Friend of friend (future) |
Verified |
Authenticated user |
Public |
Unauthenticated (lowest) |
These levels form an ordered enum (using Rust’s PartialOrd derive), where higher levels grant access to all visibility settings that lower levels can access.
Access check: subject_access_level.can_access(resource_visibility)
For Direct visibility, the system also checks explicit audience membership — if the subject’s identity is listed in the resource’s audience field, access is granted.
Explicit Access Grants
Beyond visibility, access can be granted explicitly through several mechanisms:
File Shares (FSHR Actions)
When a user shares a file with another user, the system creates:
- A share entry in the database (linking file to recipient with R/W permission)
- An FSHR action token for federation (so the recipient’s instance knows about the share)
The recipient gets Confirmation status and must accept the share. Once accepted, the file appears in their file list with the granted access level.
Share Flow:
Alice shares file with Bob (Write access)
↓
Create share_entry: file_id → bob, permission='W'
↓
Create FSHR action: audience=bob, subject=file_id, subType="WRITE"
↓
Bob accepts → file appears in Bob's file listing
↓
Bob can now read AND write the fileScoped Access Tokens
Access tokens can include a scope field that limits access to specific resources:
- Format:
file:{file_id}:{R|W} - Grants access only to the specified file with the specified permission level
- Child files within a document tree inherit the parent’s scope
Role-Based File Access (Communities)
For files owned by a community (tenant), user roles determine access:
- leader, moderator, contributor → Write access
- Any community role → Read access
This only applies to files owned by the community profile, not files owned by individual users.
Discretionary Evaluation Order
When neither TOP nor BOTTOM policy matches, the discretionary layer evaluates in this order:
1. Leader role override → ALLOW (leaders can do everything)
2. For write/update/delete operations:
a. Check ownership → ALLOW
b. Check explicit access_level = "write" → ALLOW
c. Otherwise → DENY
3. For read operations:
a. Check explicit access grants (shares, scoped tokens) → ALLOW
b. Check visibility against subject's access level → ALLOW/DENY
c. For Direct visibility, also check audience membership → ALLOW
4. For create operations:
a. Check collection policies (quota, tier) → ALLOW/DENY
5. Default → DENYComplete Evaluation Flow
When a permission check is requested, the full flow is:
1. Load Subject, Object, Environment
↓
2. TOP POLICY check
├─ If Deny → return DENY (hard constraint)
└─ No match → continue
↓
3. BOTTOM POLICY check
├─ If Allow → return ALLOW (hard guarantee)
└─ No match → continue
↓
4. DISCRETIONARY ACCESS check
├─ Ownership → ALLOW
├─ Explicit grants (shares, scoped tokens) → ALLOW
├─ Visibility check → ALLOW/DENY
└─ Audience membership (for Direct) → ALLOW
↓
5. DEFAULT DENYEvaluation Example
Request: Bob wants to read Alice’s connected-only file
Subject:
id_tag: "bob.example.com"
roles: ["USR"]
Action: "file:read"
Object:
owner: "alice.example.com"
visibility: 'C' (Connected)
file_id: "f1~abc123"
Evaluation:
1. TOP Policy: No blocking rules → continue
2. BOTTOM Policy: Not owner → continue
3. Discretionary:
a. Is owner? No (alice ≠ bob)
b. Explicit grants? No shares found
c. Visibility = Connected
d. Check connection:
- Alice has CONN to Bob? Yes
- Bob has CONN to Alice? Yes
- Subject access level = Connected
- Connected.can_access(Connected) = true
→ ALLOWIntegration with Routes
Cloudillo uses permission middleware to enforce access control on HTTP routes:
Protected Routes:
# Actions (immutable — read and create only)
GET /api/actions + check_perm_action("read")
POST /api/actions + check_perm_create("action", "create")
# Files
GET /api/files/:id + check_perm_file("read")
PATCH /api/files/:id + check_perm_file("write")
DEL /api/files/:id + check_perm_file("delete")
# Profiles
PATCH /api/me + check_perm_profile("update")
PATCH /api/admin/profiles/:id + require_adminEach middleware loads the resource, computes the subject’s relationship to the owner, and runs the full evaluation flow before the handler executes.
Examples
Example 1: Public File Access
Alice uploads a public file:
POST /api/files/default/logo.png
Authorization: Bearer <alice_token>
Body: <image data>
Response:
fileId: "f1~abc123"
visibility: "P"Bob reads the file (no authentication needed):
GET /api/files/f1~abc123
Permission Check:
Subject: unauthenticated (Public access level)
Action: file:read
Object: { owner: alice, visibility: P }
Visibility check: Public.can_access(Public) = true
Decision: ALLOWExample 2: Connected-Only File
Alice uploads a connected-only file:
POST /api/files/default/private-notes.pdf
Authorization: Bearer <alice_token>
Response:
fileId: "f1~xyz789"
visibility: "C"Bob tries to read (not connected):
Permission Check:
Subject: bob (Verified access level — no connection)
Object: { owner: alice, visibility: C }
Visibility check: Verified.can_access(Connected) = false
Decision: DENYCharlie reads (connected to Alice):
Permission Check:
Subject: charlie (Connected access level)
Object: { owner: alice, visibility: C }
Visibility check: Connected.can_access(Connected) = true
Decision: ALLOWExample 3: File Share Override
Alice has a Connected-only file, but wants to share it with Dave (who is not connected):
POST /api/files/f1~xyz789/shares
Authorization: Bearer <alice_token>
Body: { "idTag": "dave.example.com", "permission": "R" }Dave can now read the file despite not being connected:
Permission Check:
Subject: dave
Action: file:read
Object: { owner: alice, visibility: C }
1. TOP Policy: No blocking rules → continue
2. BOTTOM Policy: No match → continue
3. Discretionary:
a. Ownership? No
b. Explicit grants? YES — share entry found (permission=R)
→ ALLOW (share overrides visibility restriction)Attribute Set Implementations
Cloudillo implements the AttrSet trait for different resource types, providing consistent attribute access for permission evaluation.
File Attributes
file_id → Content-addressed ID
owner_id_tag → File owner identity
visibility → P/V/2/F/C or NULL
access_level → Pre-computed from shares/scopes ("read" or "write")
following → Subject follows owner (bool)
connected → Subject connected to owner (bool)Action Attributes
issuer_id_tag → Action creator identity
tenant_id_tag → Storage location (may differ from issuer)
audience_tag → Target recipient(s)
visibility → P/V/2/F/C or NULL
following → Subject follows issuer (bool)
connected → Subject connected to issuer (bool)
status → A/C/N/D/R/SAction status codes:
| Code | Status | Description |
|---|---|---|
A |
Active | Published and in good standing |
C |
Confirmation | Awaits user decision (e.g., CONN request, FSHR share) |
N |
Notification | Auto-processed, informational (e.g., mutual CONN, REACT) |
D |
Deleted | Rejected or revoked |
R |
Draft | Not yet published, editable |
S |
Scheduled | Draft with scheduled publish time |
Profile Attributes
id_tag → Profile identity
profile_type → "community" or empty
tenant_tag → Owner/issuer
roles → Subject's roles on this profile
status → Profile status
following → Subject follows profile (bool)
connected → Subject connected to profile (bool)Subject Attributes (for CREATE operations)
id_tag → Requesting user identity
roles → User roles
tier → "free", "standard", "premium"
quota_remaining → Remaining storage quota
banned → Whether user is banned
email_verified → Whether email is verifiedSecurity Best Practices
Default Deny
The system defaults to denying access unless explicitly allowed. Unknown visibility values are parsed as Direct (most restrictive). Unknown access levels default to None.
Validate Server-Side
Client-side visibility checks are for UX only (show/hide UI elements). The server always validates permissions before serving resources, regardless of client-side checks.
Audit Permission Denials
All permission denials are logged with debug-level tracing, including subject identity, action attempted, visibility level, access level, and relationship status.
Immutable Actions
Action tokens are cryptographically signed and content-addressed. They cannot be modified after creation. Visibility and audience are set at creation time and cannot be changed.
See Also
- Access Control — Token-based authentication
- Actions — Action tokens for relationships (CONN, FLLW, FSHR)
- System Architecture — Overall system design
- Identity System — User identity and authentication