lichen.page.review from the reviewed point 0b92cb61 to stack eb5a1653, as one PR per subsystem (review set)

full 02: Permissions: route groups and spawn permissions #65

targeting full/01-sandbox
Open opened by and assigned to notplants-bot.bsky.social
targeting full/01-sandbox

Who may do what is now declared in two places, and both are enforced by the types or by clippy. Every HTTP route is added to a PermissionRouter group that names the permissions its routes need on their target, and every process lichen starts names a list of Permissions from which its sandbox is compiled. There are no roles: a principal (owner, share guest, full token, safe token, …) holds permissions, and each server's policy finds the principal and checks the group's needs.


AST-verified changes

  • lichen-site/src/route_names.rs → lichen-sandbox/src/route_names.rs (LICHEN-SANDBOX L2b, 3f941399; lichen-site re-exports it). Checked by notify-63d68bee: "AST EQUAL, carried 14"; adversary notify-82766f9e: "AST equal 3319=3319", controls RED. No rewrites.

  • SECURITY-FOLDER-AND-PRINCIPALS unit 1 (46529b05): access.rs, the front server's policy, into lichen-server's security/, part of seven product files moved there with their test files (12 files renamed; crate:: paths rewritten in lichen-server). Checked by notify-eebdd32e: "items 2471/2471, moved 0, stayed 2343, carried 128, files_renamed 12, visibility_changes 0, comments_compared 2429, module_declarations_checked 147, errors 0"; adversary rerun (19:08Z, its checker built from ast-checker/): both runs "equal=True (2471/2471, renamed 12, visibility 0, errors 0)". The rustfmt (d6c25912, proven by cargo fmt) and the non-move edits (bd48e739: literals) are separate commits.

  • FULL-SITE-PROCESS 6a (#1651 on lichen.page.review, deef05fc → 0b71076c, map machine-docs/reports/fsp-6a/move-map.json): policy, which compiles a spawn's permission list to a confinement policy, left lichen-site/src/permissions.rs for lichen-site/src/sandbox/spawns.rs (#39), with eight spawn builders from lichen-server; permissions.rs re-exports it. Run notify-82fbca10: "equal=True: items 5581/5581: moved 9, stayed 5572; visibility_changes 8, every one listed; comments compared 4995"; adversary rerun equal=True, 5581/5581, plants caught (notify-ec60785b). None of this PR's 1,450 changed lines can be skipped for these moves: route_names.rs was created after 09-23 (with item 6's api move, 2026-10-01), so in this diff it reads as new code.


What changed and why (LICHEN-SANDBOX-AND-PERMISSIONS L2–L4):

  • On routers. A route is added only through a PermissionRouter, which is one group: Gate (the permissions needed on the target), Named (a group the policy defines by name, such as desktop), or Public. The group's check is a route_layer on that route's method router, so two groups on one path keep their own checks and a request no route matches reaches no check. Groups join into Routes, which has no way to add a route, and every route is recorded with its group in GROUPS for the tables generated into docs/SECURITY.md. clippy.toml (in the base commit) forbids axum::Router::route, route_service, nest_service and fallback, so a route outside a group fails clippy.
  • On spawns. lichen-site/src/permissions.rs declares Permission and one list per kind of spawn (SITE_PROCESS, SCRIPT, TERMINAL, STATIC_BUILD, CHILD, BACKUP_READ, BACKUP_RESTORE, BACKUP_FETCH), and child limits; policy, which compiles a list to a confinement policy, is in sandbox/spawns.rs since FULL-SITE-PROCESS 6a (#39) and re-exported here. Nothing grants a spawn a site path or a descriptor otherwise. Since FULL-SITE-PROCESS (O4) a push and a publish split in two: the right to ask (TriggerPushToRemote, TriggerPushToPds), which SCRIPT and TERMINAL hold, and the credentials (RemoteCreds, PdsCreds), which only the site process holds, for its admin thread; without the owner's opt-in (full-permissions-for-child-processes) no other site code holds a credential.
  • Policies. The front server's (Front) answers with the statuses the routes already gave: 401 or a sign-in redirect when signed out, 404 for a site that does not exist, 403 for someone else's site or a missing permission. The site's (Site) asks the admission the serving server put in the request, and fails closed when there is none. Since FULL-SITE-PROCESS it also knows the host role the front server forwarded a request with (HostRole): admin_host refuses a dashboard-only route asked on the site's own host (3e, D6), the site's own push into its hub and its iroh endpoint's asks are admitted only for their relayed roles (OWN_PUSH, IROH), and a request on a child's own socket is admitted as that child kind's principal (ChildAsked, 6g). The permission list follows the operator's answers to the L0 note (O1–O4, 2026-10-15).

Files

  • crates/lichen-sandbox/src/permission_router.rs — PermissionRouter, Group, Policy, Routes, GROUPS. Read this first. It is in lichen-sandbox because the mechanism is generic; the policies are lichen's.
  • crates/lichen-sandbox/src/route_names.rs — records each route's handler by name as it is added, so a route-table dump shows whether a move changed a handler.
  • crates/lichen-site/src/permissions.rs — Permission, Principal, the spawn lists, lends (a list that lends a credential as descriptor 3) and child_limits; policy re-exported from #39.
  • crates/lichen-server/src/security/access.rs — the front server's policy: who is asking (session, token, share guest), their principal on the target, the answer status; since FULL-SITE-PROCESS 4d the site its git admission admitted is carried to the handler (GitAdmitted), which never reads the path again.
  • crates/lichen-site/src/access.rs — the site's policy, with one Admitter per serving server: the front server's own, or a site process's ForwardedIdentity; HostRole, admin_host, the relayed roles and ChildAsked.
  • crates/lichen-yrs/examples/code-mirror/main.rs, examples/webrtc-signaling-server/main.rs — an allow for the router rule: these examples run a plain axum server, not lichen's.

Lives elsewhere

  • Each crate's routes are put in groups where they are defined: the front server's router in #30 (server.rs), the dashboard API in #32 (api.rs), the site's API in #35, git, hub and publish routes in their own PRs.
  • The sandbox the lists compile to: #17, and every spawn's sandbox as data, policy included: #39. The child-routes socket that admits ChildAsked: #40.
  • clippy.toml, docs/SECURITY.md (the generated tables), permission_snapshot.txt and the dashboard boundary tables are in the base commit.

Category: Permissions: route groups and spawn permissions Size: 7 files, +1,450/−0 Rounds: round 0 at stack ae42e3c0; round 1 at 6b650b2a (SECURITY-FOLDER-AND-PRINCIPALS): access.rs moved to security/access.rs (unit 1, a move only; it stays here with its subject, and its siblings in security/ are in #30 and #20); and permissions.rs's eight job principals became two (unit 3, 3e2f467a): SiteBackgroundProcess, the work a site's process starts itself, holding SITE_PROCESS, and AdminBackgroundProcess, the work the front server starts, holding the union of the six it replaces. It changes only the table: no job runs as a principal yet. Read this round in its interdiff: https://tangled.org/notplants-bot.bsky.social/lichen.page.october/pulls/18/round/1/interdiff. Round 2 at b66397c0 (FULL-SITE-PROCESS): permissions.rs splits a push and a publish into the right to ask and the credentials (O4), gives SCRIPT and TERMINAL the two trigger permissions, and re-exports policy, moved to #39 (6a, AST-checked); the site's access.rs adds the host role and admin_host (3e), the relayed own-push and iroh roles and ChildAsked (6g); the front's security/access.rs carries the git admission's site to its handler (4d). Read this round in its interdiff: https://tangled.org/notplants-bot.bsky.social/lichen.page.october/pulls/18/round/2/interdiff Plans and ledger: LICHEN-SANDBOX-AND-PERMISSIONS-2026-10-13.md (L2 groups on routers, L3 permissions on spawns, L4 generated tables; header records O1–O4), LICHEN-SANDBOX-L0-DESIGN-2026-10-15.md §4.3, §6, §7; builder ledger L2b (#1377, lines 2734–2735); FULL-SITE-PROCESS-2026-10-25.md (O4, units 3e, 4d, 6a, 6g). Inline #[cfg(test)] modules: none. 3 files point to a test file with #[path]. Bucket moves: none from round 1. (Round 1 gathered these from the starting map's Sandbox, front server core, site core and iroh buckets; clippy.toml, which round 1 also had here, is in the base commit now.) Round 1: #2 (Permissions) covered these files from 0b92cb61.

This pull request has no versions submitted yet.