Landing: - Rebuilt app/(landing)/page.tsx from a 539-line client monolith to a ~150-line server component in the /resources editorial style. - New scoped app/(landing)/landing.css mirrors the resources palette (#0c0c0c / lime #d4ff00 / #252525), IBM Plex Mono + Libre Baskerville loaded in (landing)/layout.tsx, faint grid overlay. - Removed the animated Mini* previews and inline Ico* set (clashed with the editorial look). Split into components/landing/icons.tsx and components/landing/resource-showcase.tsx for separation. - Resource showcase: one row of six square tiles with the real project SVG logos linking to atlas/space-after-dark/proposal-duel/ zine/learner/governance. Responsive 6 -> 3 -> 2. - BrandMark accepts an optional style prop. Fix: - KB search crashed the Ops page. filteredLeads referenced l.member_name (the column is lead_handle), so undefined.toLowerCase() threw on every keystroke since the filter memos recompute on shared search state across tabs. All Ops filter predicates are now null-safe via a shared has() helper; filteredSecrets no longer assumes a non-null title. Docs: - docs/PERMISSIONS_DESIGN.md: concrete plan for customizable permissions, per-item Ops ACLs, and area-lead roles. That work is security-sensitive (RLS rewrites on secrets/knowledge_base) and is scoped as its own next pass, not bundled here. 266/266 tests pass. Production build clean. No migration this pass.
102 lines
4.9 KiB
Markdown
102 lines
4.9 KiB
Markdown
# Permissions, Ops ACLs, and Area-Lead Roles — Design
|
|
|
|
Status: designed, not yet implemented. This is the plan for the next focused
|
|
pass. It is security-sensitive (it rewrites RLS on `secrets` and
|
|
`knowledge_base`), so it ships on its own, with its own tests, not bundled
|
|
with unrelated UI work.
|
|
|
|
## Goals (from the user)
|
|
|
|
1. Customizable permissions, not just role labels.
|
|
2. Per-secret access: choose which roles (multiple) can see each secret.
|
|
3. Same per-item access control across all Ops and Facilities content
|
|
(knowledge base, processes, secrets, area leads).
|
|
4. Area-lead roles: an admin can create them. Each shows as "vacant" until a
|
|
member is assigned, either from a dedicated picker or by clicking a member
|
|
in the directory. Being the lead of an area grants the permissions attached
|
|
wherever that area-lead role is referenced.
|
|
|
|
## Model
|
|
|
|
Three layers, smallest blast radius first.
|
|
|
|
### A. Permission catalog + role grants
|
|
|
|
- `permissions` is a fixed code list in app code (not a table): e.g.
|
|
`ops.kb.read`, `ops.kb.write`, `ops.secrets.read`, `ops.secrets.write`,
|
|
`members.manage`, `payments.manage`, `governance.manage`, etc.
|
|
- New table `space_role_permissions (space_id, role_or_custom, permission)`
|
|
where `role_or_custom` is either a built-in `member_role` value or a
|
|
`space_custom_roles.slug`. Admin always has all permissions implicitly
|
|
(never lockable out — guard in code and RLS).
|
|
- A SQL helper `user_has_permission(uid, sid, perm text) returns boolean`
|
|
(SECURITY DEFINER, fixed search_path) used by RLS and a TS mirror
|
|
`hasPermission()` used in server actions for early rejection + UI gating.
|
|
|
|
### B. Per-item ACLs for Ops content
|
|
|
|
- New table `ops_acl (id, space_id, entity_type, entity_id, role text)`
|
|
where `entity_type in ('secret','kb','process','area_lead')` and `role`
|
|
is a built-in role, a custom-role slug, or the sentinel
|
|
`area_lead:<area_code>`.
|
|
- An item with zero ACL rows falls back to its existing visibility column
|
|
(`knowledge_base.visibility`, the board/admin default for `secrets`), so
|
|
this is additive and backward compatible.
|
|
- RLS SELECT on `secrets` / `knowledge_base` becomes: existing rule OR an
|
|
`EXISTS` against `ops_acl` joined to the caller's effective roles
|
|
(built-in role + assigned custom roles + area-lead memberships). Write
|
|
stays admin/board (plus `ops.*.write` permission once layer A lands).
|
|
|
|
### C. Area-lead roles
|
|
|
|
- Reuse `space_custom_roles` with a flag `is_area_lead boolean` and an
|
|
`area_code` link, OR add `space_area_lead_roles (id, space_id, area_code,
|
|
name, color)`. Prefer the dedicated table: it is the thing that renders
|
|
"vacant".
|
|
- `area_lead_assignments (space_id, area_lead_role_id, member_id)`. Absence
|
|
of a row => the role renders "Vacant". Assignment is created from a picker
|
|
in `/customize` -> Areas or from a "Make area lead" action on a member row
|
|
in the directory.
|
|
- Effective-roles resolution (used by `ops_acl` SELECT and
|
|
`user_has_permission`) treats a member who holds an area-lead assignment as
|
|
also holding `area_lead:<area_code>`, so any Ops item whose ACL includes
|
|
that sentinel becomes visible to them, and any permission granted to that
|
|
area-lead role applies.
|
|
|
|
## UI surfaces
|
|
|
|
- `/customize` -> new "Permissions" panel: a role x permission matrix.
|
|
- `/customize` -> Areas: per-area "Lead role" with a member picker; shows
|
|
"Vacant" when unassigned.
|
|
- `/ops` create/edit modals (KB, process, secret): a multi-select of roles +
|
|
area-lead roles that may access this item.
|
|
- `/members`: row action "Assign as area lead" -> area picker.
|
|
|
|
## Migration plan
|
|
|
|
`scripts/023_permissions.sql` + `schema.sql` Section 15:
|
|
- `space_role_permissions`, `ops_acl`, `space_area_lead_roles`,
|
|
`area_lead_assignments`. All with full RLS (SELECT for space members,
|
|
write for admin/board, delete admin).
|
|
- Helper functions `user_effective_roles(uid, sid)` and
|
|
`user_has_permission(uid, sid, perm)`.
|
|
- Rewrite `secrets` and `knowledge_base` SELECT policies to add the ACL
|
|
branch. Keep the old branch so nothing that works today breaks.
|
|
- Seed sensible default `space_role_permissions` per space (admin: all;
|
|
board: ops + members + governance; treasurer: payments; member: kb.read).
|
|
- Backfill: none required (empty ACL == current behavior).
|
|
- Refresh `types/database.ts`, `DB_SCHEMA_MAP.md`, `DATABASE_SCHEMA.md`,
|
|
`API_REFERENCE.md`.
|
|
|
|
## Risks / why this is its own pass
|
|
|
|
- RLS on `secrets`/`knowledge_base` is the highest-risk surface in the app.
|
|
A wrong policy either leaks a credential cross-role or locks admins out.
|
|
Needs dedicated RLS tests (the `__tests__` governance/RLS pattern) before
|
|
it goes near production.
|
|
- Permission resolution must never let a member escalate themselves (the
|
|
migration-015 trigger pattern). The `user_has_permission` helper must be
|
|
SECURITY DEFINER with a pinned search_path, same as the existing helpers.
|
|
- Ship behind the additive fallback so existing spaces are unaffected until
|
|
an admin opts in by setting ACLs/permissions.
|