Permission System

Cloudillo’s permission system has two pillars that work together to control access to all resources:

  1. ABAC Policies (profile/community-level) — Configurable TOP and BOTTOM policy rules that define hard constraints and guarantees
  2. 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 access

Pillar 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 profile
Action 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 resource

BOTTOM 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 access

Policy 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:

  1. A share entry in the database (linking file to recipient with R/W permission)
  2. 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 file

Scoped 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 → DENY

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 DENY

Evaluation 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
   → ALLOW

Integration 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_admin

Each 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: ALLOW

Example 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: DENY

Charlie reads (connected to Alice):

Permission Check:
    Subject: charlie (Connected access level)
    Object: { owner: alice, visibility: C }
    Visibility check: Connected.can_access(Connected) = true
    Decision: ALLOW

Example 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/S

Action 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 verified

Security 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