dragonflight/services/mam-api/src/db/migrations/026-project-access.sql
Zac ec026195eb feat(mam-api,web-ui): per-project RBAC (v2 auth layer)
Adds per-project access control on top of the flat v1 auth. admin keeps
global access; editor/viewer are scoped to projects granted to them (direct
or via group) at view (read-only) or edit (read-write) level.

- migration 026: project_access table + access_level enum
- src/auth/authz.js: central isAdmin/accessibleProjectIds/projectLevel/
  assertProjectAccess
- requireAdmin middleware; admin-gate /users, /auth/users, /groups
- enforce scoping on projects, assets, bins (list filter + per-resource
  view/edit + create checks); gate bulk asset maintenance + batch-trim
- grant API: GET/POST/DELETE /projects/:id/access
- web-ui: hide admin nav for non-admins, admin-route bounce, project
  "Manage access" modal, rewrite Policies tab
- tests: authz, project-access, assets-access (node:test, skip w/o DB)
- deferred routers carry TODO(authz) markers; .env.example documents the
  service-token-needs-admin/grants requirement

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 02:37:36 +00:00

30 lines
1.3 KiB
SQL

-- Migration 026 — per-project access grants (RBAC v2).
--
-- v1 auth is flat: any logged-in user can do everything. This adds per-project
-- scoping. A grant targets either a user or a group (polymorphic subject) and
-- carries a level: 'view' (read-only) or 'edit' (read-write). Admins bypass all
-- of this in code (authz.js) and need no rows here.
--
-- subject_id is intentionally NOT a foreign key — it points at either users.id
-- or groups.id depending on subject_type. Rows are cleaned up when the project
-- is deleted (FK cascade). A deleted user/group leaves an orphan row that
-- resolves to nobody (harmless); a later sweep can prune them if desired.
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'access_level') THEN
CREATE TYPE access_level AS ENUM ('view', 'edit');
END IF;
END $$;
CREATE TABLE IF NOT EXISTS project_access (
project_id UUID NOT NULL REFERENCES projects ON DELETE CASCADE,
subject_type TEXT NOT NULL CHECK (subject_type IN ('user', 'group')),
subject_id UUID NOT NULL,
level access_level NOT NULL DEFAULT 'view',
granted_by UUID REFERENCES users ON DELETE SET NULL,
granted_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (project_id, subject_type, subject_id)
);
CREATE INDEX IF NOT EXISTS idx_project_access_subject
ON project_access (subject_type, subject_id);