§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:
Ben Stull
2026-06-05 18:07:51 -07:00
parent 39ce54fbcc
commit c2f566512a
18 changed files with 877 additions and 97 deletions
+2 -2
View File
@@ -218,7 +218,7 @@ def make_router(
can_merge = (
viewer is not None
and (
auth.is_project_superuser(viewer, rfc["project_id"])
auth.is_collection_superuser(viewer, rfc["collection_id"])
or viewer.gitea_login in owners
or viewer.gitea_login in arbiters
)
@@ -795,7 +795,7 @@ def _can_graduate(rfc, viewer) -> bool:
if viewer is None:
return False
# §6.1 admin/owner or §22.6 project_admin OR §6.3 RFC owners/arbiters.
if auth.is_project_superuser(viewer, rfc["project_id"]):
if auth.is_collection_superuser(viewer, rfc["collection_id"]):
return True
owners = json.loads(rfc["owners_json"] or "[]")
arbiters = json.loads(rfc["arbiters_json"] or "[]")