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., ["moderator"], or ["SADM"] for site admin)
pub scope: Option<Box<str>>, // Optional scope (e.g., "apkg:publish")
}Roles come from a fixed hierarchy — public, follower, supporter, contributor,
moderator, leader (each inherits the lower ones) — plus the site-admin role SADM,
which is checked separately by require_admin rather than through ABAC.
2. Action (What)
The operation being attempted, in resource:operation format:
file:read → Read a file
file:write → Modify, delete, tag, or restore a file
file:create → Upload/create a file
action:read → View an action token
action:write → Change an action's status (accept/reject/dismiss), delete, or publish a draft
action:create → Create an action token
profile:read → Read a profile
profile:write → Update own profile
profile:admin → Administer another community member's profileThe operation string maps directly to the middleware on the route — for example
PATCH /api/files/{id} and DELETE /api/files/{id} both run check_perm_file("write"),
so deletion is gated by the same file:write check rather than a separate file:delete.
Action bodies are immutable; status is not
An action’s signed payload (type, audience, visibility) is content-addressed and fixed at
creation. What action:write controls is the mutable status — accepting, rejecting,
dismissing, deleting, or publishing a scheduled draft — never the payload itself.
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 has(&self, key: &str, value: &str) -> bool; // get(key) == value
fn contains(&self, key: &str, value: &str) -> bool; // list contains value
}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
A rule holds a list of conditions; all conditions must match for the rule to fire (implicit AND — there is no separate And/Or operator). Each condition uses one of:
Comparison: Equals, NotEquals, GreaterThan, LessThan
Set/string: Contains, NotContains, In
Role: HasRole — checks if the subject has a specific role
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. The tenant is treated as owner-equivalent, so a community profile owns the resources stored on it.
if resource.owner == subject.id_tag → Owner access
if subject.id_tag == tenant_id_tag → Owner access (tenant = owner equivalent)Separately, the leader role short-circuits to ALLOW for any operation (see the evaluation order below), so community leaders effectively have owner-level reach.
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, the system creates a share entry in the database (file → recipient with a permission char) and an FSHR action token for federation. The FSHR subType maps to an access level: WRITE → Write, COMMENT → Comment, otherwise → Read. Shares attached to a folder are inherited by descendant files (the parent chain is walked, bounded to 64 levels).
The recipient gets Confirmation status and must accept the share, after which the file appears in their listing with the granted level.
Scoped Access Tokens
Access tokens can carry a scope that limits them to a single resource (used by share links):
- File scope format:
file:{file_id}:{R|C|W}→ Read / Comment / Write on that file - A scope for a folder extends to files nested under it; a scope for a document root extends to its child files
apkg:publishis the only non-file scope (package upload), and grants no file access
Role-Based File Access (Communities)
For files owned by a community (a file whose owner is the tenant), member roles determine access:
- leader, moderator, contributor → Write access
- Any other community role → Read access
This applies only 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 by operation:
1. Leader role override → ALLOW (the "leader" role can do everything)
2. write / update / delete:
a. Ownership → ALLOW
b. Pre-computed access_level == "write" → ALLOW
c. Otherwise → DENY
3. read:
a. Pre-computed access_level in {read, comment, write} → ALLOW
b. Visibility vs. subject's access level → ALLOW/DENY
(for Direct visibility, audience membership also grants access)
4. create:
a. ALLOW at this layer (quota/role checks live in the
collection-policy middleware, not here)
5. admin (e.g. profile:admin):
a. Subject is moderator or higher → ALLOW, else DENY
6. Default → DENYThe single pre-computed access_level attribute is what carries share, scoped-token,
ownership, and community-role grants into the check — the read and write branches above
just compare against it.
Complete 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: []
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
POST /api/actions + check_perm_create("action", "create")
POST /api/actions/:id/accept|reject|dismiss + check_perm_action("write")
DEL /api/actions/:id + check_perm_action("write")
# Files
POST /api/files + check_perm_create("file", "create")
PATCH /api/files/:id + check_perm_file("write")
DEL /api/files/:id + check_perm_file("write")
# Profiles
PATCH /api/me + check_perm_profile("write")
PATCH /api/admin/profiles/:id + check_perm_profile("admin")
# Site administration (separate from ABAC — requires SADM role)
* /api/admin/tenants/* + require_adminObject-bound middleware (check_perm_file, check_perm_action, check_perm_profile)
loads the resource, computes the subject’s relationship to the owner, and runs the full
evaluation flow. check_perm_create evaluates the collection policy (role/quota) before
the object exists. require_admin is a plain SADM-role gate, not an ABAC check.
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.
The attribute keys are what policy conditions reference. Visibility is exposed as its
lowercase string form (public, verified, second_degree, follower, connected,
direct), not the single-char DB code.
File Attributes (FileAttrs)
file_id → Content-addressed ID
owner_id_tag → File owner identity
mime_type → Content type
visibility → public/verified/second_degree/follower/connected/direct
access_level → Pre-computed grant: none/read/comment/write/admin
following → Subject follows owner (bool)
connected → Subject connected to owner (bool)
tags → File tags (list)Action Attributes (ActionAttrs)
type → Action type (e.g. POST, CONN, FSHR)
sub_type → Action sub-type
owner_id_tag → Storage tenant (alias of tenant_id_tag)
issuer_id_tag → Action creator identity (may differ from tenant)
parent_id → Parent action ID
root_id → Root action ID
audience_tag → Target recipient(s) (list)
visibility → public/verified/.../direct
following → Subject follows issuer (bool)
connected → Subject connected to issuer (bool)Profile Attributes (ProfileAttrs)
id_tag → Profile identity
profile_type → "community" or empty
owner_id_tag → Owning tenant (alias of tenant_tag)
roles → Subject's roles on this profile (list)
status → Profile status
visibility → public/.../direct
following → Subject follows profile (bool)
connected → Subject connected to profile (bool)Subject Attributes for CREATE (SubjectAttrs)
id_tag → Requesting user identity
roles → User roles (list)
tier → "free", "standard", "premium"
quota_remaining_bytes → Remaining storage quota (bytes)
rate_limit_remaining → Remaining requests this hour
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