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>
30 lines
1.3 KiB
SQL
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);
|