§22 S3: scope-role enforcement + collection-grain visibility (@S3) — v0.42.0
Implement slice S3 of the §22 three-tier refactor: the four-layer
most-permissive scope-role resolver (§B.2) over {owner, contributor}
grants at {global, project, collection}, with the §22.5 visibility gate
enforced at the collection grain.
- migration 030: memberships.scope_type += 'global' (the global RFC
Contributor tier; sentinel scope_id '*').
- auth.effective_scope_role folds global → project → collection,
most-permissive, no negative override; can_read_collection /
can_contribute_in_collection / is_collection_superuser /
can_create_collection gate reads, writes, admin, and create.
- collection-grain visibility: a gated collection is hidden from the
public (404, omitted from the directory) yet visible+listed for a
scope-role holder; a collection may be set only as strict or stricter
than its project (public < unlisted < gated), validated at create and
clamped at the mirror.
- entry-scoped authority (mark-reviewed, graduate, branch read/contribute,
PR/discussion/contribution moderation) re-pointed from the project grain
to the entry's collection.
- create-collection authority widened to a project/global-scope grant
holder (§B.1), not only a deployment owner/admin.
Keystone reconciliation (session 0076): a plain granted account is a
granted *account*, not a write-everywhere global role; the implicit-public
write baseline is grandfathered onto the migration-seeded `default`
collection only, so the N=1 deployment loses no capability. Reinterprets
§B.1/§B.3 literally — flagged for the SPEC merge (S6).
Completes @S3 (C1.1–C1.8). Tests: test_s3_scope_roles_vertical.py (8 C.1
scenarios + visibility/strictness), test_migration_030_global_scope.py.
Full backend suite 493 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
-- migrate:no-foreign-keys
|
||||
--
|
||||
-- §22 three-tier — S3. Admit a *global*-scope grant to the memberships table.
|
||||
--
|
||||
-- §B.2's resolver folds four layers (global → project → collection → per-entry).
|
||||
-- Migration 029 created `memberships` with scope_type ∈ {project, collection}
|
||||
-- only; the global tier was left to S3. A global grant is how a "global RFC
|
||||
-- Contributor" (a contributor who may propose in every collection of every
|
||||
-- project, distinct from a deployment owner/admin) is represented — see
|
||||
-- docs/design/2026-06-05-three-tier-projects-collections.md §B.2/§B.3 and the
|
||||
-- C.1 "cleo" scenario.
|
||||
--
|
||||
-- SQLite can't ALTER a CHECK constraint in place, so the table is rebuilt by the
|
||||
-- create-copy-drop-rename procedure (the 028/029 pattern). The global scope uses
|
||||
-- a stable sentinel scope_id of '*' (one global tier per deployment); the
|
||||
-- UNIQUE(scope_type, scope_id, user_id) then admits exactly one global grant per
|
||||
-- user, mirroring the project/collection rows.
|
||||
|
||||
CREATE TABLE memberships__new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
scope_type TEXT NOT NULL CHECK (scope_type IN ('global', 'project', 'collection')),
|
||||
scope_id TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK (role IN ('owner', 'contributor')),
|
||||
granted_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
granted_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (scope_type, scope_id, user_id)
|
||||
);
|
||||
INSERT INTO memberships__new (id, scope_type, scope_id, user_id, role, granted_by, granted_at)
|
||||
SELECT id, scope_type, scope_id, user_id, role, granted_by, granted_at FROM memberships;
|
||||
DROP TABLE memberships;
|
||||
ALTER TABLE memberships__new RENAME TO memberships;
|
||||
CREATE INDEX idx_memberships_user ON memberships(user_id);
|
||||
CREATE INDEX idx_memberships_scope ON memberships(scope_type, scope_id);
|
||||
Reference in New Issue
Block a user