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.
4.9 KiB
4.9 KiB
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)
- Customizable permissions, not just role labels.
- Per-secret access: choose which roles (multiple) can see each secret.
- Same per-item access control across all Ops and Facilities content (knowledge base, processes, secrets, area leads).
- 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
permissionsis 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)whererole_or_customis either a built-inmember_rolevalue or aspace_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 mirrorhasPermission()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)whereentity_type in ('secret','kb','process','area_lead')androleis a built-in role, a custom-role slug, or the sentinelarea_lead:<area_code>. - An item with zero ACL rows falls back to its existing visibility column
(
knowledge_base.visibility, the board/admin default forsecrets), so this is additive and backward compatible. - RLS SELECT on
secrets/knowledge_basebecomes: existing rule OR anEXISTSagainstops_acljoined to the caller's effective roles (built-in role + assigned custom roles + area-lead memberships). Write stays admin/board (plusops.*.writepermission once layer A lands).
C. Area-lead roles
- Reuse
space_custom_roleswith a flagis_area_lead booleanand anarea_codelink, OR addspace_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_aclSELECT anduser_has_permission) treats a member who holds an area-lead assignment as also holdingarea_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./opscreate/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)anduser_has_permission(uid, sid, perm). - Rewrite
secretsandknowledge_baseSELECT policies to add the ACL branch. Keep the old branch so nothing that works today breaks. - Seed sensible default
space_role_permissionsper 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_baseis 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_permissionhelper 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.