Ctrl K
ComunidadGuías…

docs/access-control.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
```
╔════════════════════════════════════════════════════════════════════════════╗
║                                                                            ║
║   ACCESS CONTROL                                                           ║
║   Layered, cascading permission resolution — one SQL fragment, 7 priorities║
║                                                                            ║
╚════════════════════════════════════════════════════════════════════════════╝
```

# Access Control

Cloud Codex uses a **layered, cascading** permission system. Access is
evaluated from broad/structural (admin → workspace ownership) down to
fine-grained (per-user JSON grants on an archive). The whole cascade resolves
in a single SQL `WHERE` fragment defined in
[`routes/helpers/ownership.js`](../cloudcodex/routes/helpers/ownership.js) —
no in-application loop, no per-step round trip.

---

## Roles and Hierarchy

```
   Super Admin
     └── Workspace Owner               (users FK in workspaces.owner_id)
           └── Squad Owner             (role = 'owner' in squad_members)
                 └── Squad Admin       (role = 'admin')
                       └── Squad Member (role = 'member', per-flag perms)
                             └── Direct Archive Grants
                                  (read_access / write_access JSON arrays)
```

Roles higher in the chain implicitly grant everything below. There is no
group that sits between "workspace owner" and "squad member" beyond the
admin flag.

---

## Archive (Read / Write) Resolution

When checking whether a user can read or write an archive, the system OR-s
together seven conditions inside a single `EXISTS` subquery. **Any match
grants access.** The conditions, in conceptual priority order:

```
                       ┌────────────────────────────────┐
   request to read ──► │  is the user a super admin?    │ ─── yes ──► ALLOW
   or write archive    └──────────────┬─────────────────┘
                                      │ no
                                      ▼
                       ┌────────────────────────────────┐
                       │  is user.id in read_access /   │ ─── yes ──► ALLOW
                       │  write_access JSON array?      │
                       └──────────────┬─────────────────┘
                                      │ no
                                      ▼
                       ┌────────────────────────────────┐
                       │  is user the archive creator?  │ ─── yes ──► ALLOW
                       └──────────────┬─────────────────┘
                                      │ no
                                      ▼
                       ┌────────────────────────────────┐
                       │  is user the workspace owner   │ ─── yes ──► ALLOW
                       │  (squads.workspace_id →        │
                       │   workspaces.owner_id = id) ?  │
                       └──────────────┬─────────────────┘
                                      │ no
                                      ▼
                       ┌────────────────────────────────┐
                       │  is user a squad member with   │ ─── yes ──► ALLOW
                       │  role='owner' OR matching      │
                       │  can_read / can_write flag ?   │
                       └──────────────┬─────────────────┘
                                      │ no
                                      ▼
                       ┌────────────────────────────────┐
                       │  is user a member of any squad │ ─── yes ──► ALLOW
                       │  listed in read_access_squads /│
                       │  write_access_squads ?         │
                       └──────────────┬─────────────────┘
                                      │ no
                                      ▼
                       ┌────────────────────────────────┐
                       │  is read_access_workspace /    │ ─── yes ──► ALLOW
                       │  write_access_workspace TRUE   │
                       │  AND user in any squad in the  │
                       │  same workspace ?              │
                       └──────────────┬─────────────────┘
                                      │ no
                                      ▼
                                    DENY
```

The SQL fragments that enforce this are
`readAccessWhere(alias)` / `writeAccessWhere(alias)` (with parameters from
`readAccessParams(user)` / `writeAccessParams(user)`) in
[`routes/helpers/ownership.js`](../cloudcodex/routes/helpers/ownership.js).

Write access always requires also having read access (write implies read
in practice, since write grants are generally given alongside read grants
by the UI).

---

## Archive Ownership (Management Operations)

Destructive or management operations on an archive (delete, manage access grants) require being an **archive owner**. This check is narrower than general write access:

- Super admin
- Archive creator (`archives.created_by`)
- Workspace owner (via squad → workspace chain)
- Squad member with `role = 'owner'`

---

## Per-Log Access

Individual logs have their own `read_access` and `write_access` JSON columns as well. These are supplemental per-document grants that sit on top of archive-level access. In practice the UI mainly controls access at the archive level, but the data model supports document-level overrides.

---

## The `permissions` Table (Global Feature Flags)

Separate from access to specific content, users have a row in the `permissions` table controlling what they can *create*:

| Permission      | Effect                                       |
|-----------------|----------------------------------------------|
| `create_squad`  | Can create new squads                        |
| `create_archive`| Can create new archives                      |
| `create_log`    | Can create documents (TRUE by default)       |

**A global flag means "may create", never "may create anywhere."** The workspace
is the tenant boundary. A caller who supplies a workspace or squad id has to be
inside that workspace first: its owner, or a member of one of its squads. Admins
are exempt. A workspace whose owner account was deleted is not a public
workspace, and a squad that belongs to no workspace has no tenant to test
against, so both refuse everyone but an admin.

These global flags are checked next. If a user lacks the global flag, the system also checks:
- Whether they are the workspace owner (bypasses all)
- Whether they have the equivalent squad-member permission (`can_create_archive`, `can_create_log`)

User discovery is bounded the same way, with one deliberate widening.
`GET /api/users/search` shows a non-admin caller themselves, people who share a
workspace with them, the owners of those workspaces, and, only if the caller can
actually invite anyone, accounts that hold no `squad_members` row at all. That
last set is what keeps the squad invite picker working: every account starts
with no squad membership, so membership-only scoping made an SSO or
squad-less-invitation account invisible to everyone but a platform admin.

Say the cost out loud rather than inheriting it: **any account holding
`role IN ('owner','admin')` or `can_manage_members` on any squad, in any
workspace, or owning any workspace of its own, can retrieve the name and email
of every account on the install that has no `squad_members` row**, including
accounts in no workspace of theirs. Everything else stays inside the caller's
own workspaces.

---

## Squad Member Permissions

When a user is added to a squad, they get a row in `squad_members` with granular boolean flags:

| Flag                  | What it controls                                     |
|-----------------------|------------------------------------------------------|
| `can_read`            | View archives and documents in this squad            |
| `can_write`           | Edit documents in this squad's archives              |
| `can_create_log`      | Create new documents                                 |
| `can_create_archive`  | Create new archives inside this squad                |
| `can_manage_members`  | Invite/remove members and update their permissions   |
| `can_delete_version`  | Delete published version snapshots                   |
| `can_publish`         | Publish a new version snapshot                       |

Flags default to the most restrictive values. When a user is invited, the inviter specifies which flags to grant. Only workspace owners and squad creators can grant `can_manage_members` or invite users as `admin`.

---

## Publish Permission

Publishing a version snapshot has its own dedicated check (`canPublish`). A user can publish if **any** of the following are true:

- The archive has no squad (personal/standalone archive) — always allowed
- Super admin
- Workspace owner
- Squad member with `can_publish = TRUE`
- Squad member with `role = 'owner'`
- Archive creator

---

## 2FA and Session Security

Session tokens are 64-character cryptographically random strings, one per sign-in. The `sessions` table stores only a SHA-256 digest of each as its `id`. They are transmitted via the `Authorization: Bearer <token>` header (or the session cookie for browser redirects: `__Host-sessionToken` on https, `sessionToken` over plain http).

When 2FA is enabled on an account:
- **Email 2FA:** After a valid password login, a 6-digit code is emailed. The session is issued only after the code is verified.
- **TOTP 2FA:** A QR code is shown enabling any authenticator app. The 6-digit TOTP is verified against the stored secret before issuing a session.

Both codes travel by email: the login code for email 2FA, and the disable-confirmation code for *either* method. When mail is disabled on the instance, those two paths refuse with `503` rather than issuing a challenge that can never be answered; no token is minted and no code row is written. TOTP login is unaffected. An admin can recover an account locked out this way without touching the database: `POST /api/admin/users/:id/2fa/reset` clears the 2FA method, the TOTP secret, and any outstanding codes or setup/confirm tokens (`docs/api/admin.md`).

On password change, all sessions for that user except the current one are invalidated.