Skip to content

filex — RBAC & per-file/folder permissions (API + MCP reference)

Added in v0.1.41+ (backend internal/acl). This documents the access model and every endpoint / MCP tool the feature exposes. Backwards compatible: RBAC is off per storage by default, so an untouched deployment behaves exactly as before.

Model

Two layers combine, then a ceiling is applied:

  1. Account role (users.role): admin (full panel, exempt from all ACL), user (explorer only; read+write; can hold owner grants), viewer (explorer only; read-only — view/download, no edit/convert/mutate).

    "Explorer only" is a URL as well as a permission: give a user or viewer account the address …/drive and they land on their Home page — their storages, what they opened last, what they starred — one click from the file manager itself (uploads, sharing, search and the editor), with no admin chrome and nothing extra to deploy. …/admin is the same application served under the operator's prefix; a non-admin who follows an old /admin/... link is handed on to /drive/ (and the backend re-checks the role on every /api/admin/* call regardless of which URL asked).

  2. Per-storage RBAC toggle (storages.rbac_enabled, default false):

    • OFF → storage visible to every authenticated user; capability = account role (user→editor, viewer→viewer, admin→owner). No grants needed.
    • ON → storage hidden; a non-admin sees only paths granted to them (directly or inherited from a parent folder).
  3. Item grant level (file_grants): viewer < editor < owner.

    • Inheritance: a folder grant cascades to descendants. Effective level = highest covering grant (direct or inherited), then capped by the account role (a viewer account stays viewer even if granted higher).
    • Only an owner of an item (or an admin) may see/manage its permissions.

⚠ One surface was missing until the release this note ships in: /api/files/versions had no ACL check at all, so a viewer account could restore an old version over any file's live bytes. Restore and snapshot now require editor; listing the timeline stays at viewer. See TRASH-VERSIONING.md.

Enforcement is server-side at every /api/files/* chokepoint AND the /api/ai (REST + MCP) surface, keyed off the authenticated user — so cookie sessions are filtered too, not just tokens. internal/confine (the token root: scope hard ceiling) still composes on top.

Endpoints — permissions panel (/api/files/permissions)

Mounted in the authenticated group. Every write requires the caller to be admin or hold owner on the target path.

MethodPathBody / queryNotes
GET/api/files/permissions?path=<adapter>://<rel>{direct[], inherited[], storage_rbac, effective}. Owner/admin only.
POST/api/files/permissions{path, user_id, level, is_dir?}Upsert a grant. 409 if storage RBAC off; 400 if granting a viewer account >viewer.
PATCH/api/files/permissions/{id}{level}Change a grant's level.
DELETE/api/files/permissions/{id}Revoke.
GET/api/files/permissions/resolve?email={found, user?} — existing account or not.
GET/api/files/permissions/users?q={users[]} autocomplete of existing accounts.
POST/api/files/permissions/invite{path, email, level, create_user?, role?}Existing user → grant; admin+create_user → new account+grant (temp password); else public share link. {mode, url?, temp_password?, emailed}. Mail sent only when SMTP is verified, else the link/password is returned for on-screen display.

Endpoint — "shared with me" (/api/files/manager/shared-with-me)

The permissions panel answers "who can see this folder". The reverse question — "what has been shared with me" — has its own endpoint, and it is what the explorer's navigation panel lists under Shared with me.

MethodPathQueryNotes
GET/api/files/manager/shared-with-melimit (100, max 500), offset{files[], storages[], total, limit, offset}. Any authenticated caller; no admin or owner requirement — you are asking about your own grants.

Three rules decide what is in it:

  • Grants only. A storage with RBAC off is reached by every authenticated account through their role, so a grant row there is inert and its files are the caller's own, not "shared with them". Only RBAC-enabled storages are consulted.
  • The item, not its contents. A grant on a folder lists the folder.
  • A whole-storage grant is a drive, not an item. It has no name to render, so it is reported in storages[] — the shared drives the UI marks in its storage list — instead of as a file row.

Tenant scope is applied explicitly here, not inherited: tenantstore wraps only the storage/user listing methods, so a per-grant read like this one has to gate itself or it hands one tenant the paths of another's shared folders.

Endpoints — self-service tokens (/api/tokens)

Any authenticated user (incl. non-admin) mints tokens bound to themselves, capped server-side:

MethodPathNotes
GET/api/tokensThe caller's own tokens (no secrets).
POST/api/tokens{label, scopes, expires_in_days?}. Always minted as kind: "user". Verb-scope ceiling: viewer→read/mcp only; user→read,write,delete,mcp; never admin. Empty scopes are never stored (would be "all"→escalation). A root:<adapter>://<rel> scope must be ⊆ the caller's own grants. Plaintext returned once.
PATCH/api/tokens/{id}Ownership-checked; label / usernames only. kind is admin-only.
DELETE/api/tokens/{id}Ownership-checked.

This surface — and the other three self-service credential surfaces, /api/auth/s3-keys, /api/auth/ssh-keys, /api/auth/nfs-exports — answer 403 to an app token (reason: "app_token", from one shared middleware, handlers.RequirePersonalCaller). A token acts AS its owner, so a shared integration token — the one a host app's proxy injects in front of many visitors — would otherwise let any of them list and revoke the credential that embed runs on, or mint a new S3 key bound to its owner. Cookie/OIDC sessions and user tokens are unaffected. See MCP.md → Token kinds; the escape hatch for a personal token that migration 00030 defaulted to app is PATCH /api/admin/ai-tokens/{id} {"kind":"user"}.

⚠ Kind is a different axis from role and from confinement. It does not widen or narrow what a caller may do — every check on this page still applies — it only decides whether the surfaces that belong to one identity are drawn at all.

Endpoints — admin (/api/admin, admin-only)

MethodPathNotes
GET/api/admin/grantsGlobal overview: every grant enriched with storage_name + user_email.
DELETE/api/admin/grants/{id}Admin override revoke.
POST/api/admin/settings/smtp-test{to?}{ok, error?, sent?}. Verifies the SMTP config (auth handshake) and, with to, sends a real test mail. SMTP config lives in the smtp.* settings keys (host/port/tls/from/username/password).

storages.rbac_enabled is set via the normal storage create/update payloads (POST/PATCH /api/admin/storages, field rbac_enabled).

MCP admin tools

Exposed on /api/ai/mcp for an API token carrying the admin scope (alongside the existing 59 admin_* tools):

ToolInputEffect
admin_grants_listList every grant (who/where/level).
admin_grant_set{body:{path, user_id, level}}Grant/upsert. Storage must have RBAC on; viewer accounts capped to viewer.
admin_grant_revoke{id}Revoke a grant by id.

The AI file surface (file_* tools + /api/ai/files|read|upload|...) is already gated by the bound user's grants + role ceiling via aiOps — a confined, non-admin token only sees/mutates what its user was granted.

Tests

backend/internal/acl/acl_test.go (resolution: Effective/CanSee/ceiling/prefix), backend/internal/api/handlers/tokens_self_test.go (scope-ceiling / escalation), backend/internal/api/handlers/grants_test.go (end-to-end: owner grant, viewer ceiling, owner-only panel, self-token limits, admin overview, non-admin 403).

Released under the MIT License.