Fix #346: create/import owner binding #356

Merged
crueber merged 2 commits from fix/issue-346 into main 2026-09-12 00:10:36 +00:00
Owner

Closes #346 (prerequisite #348 landed as 222e3f7; the admission helper is shaped for #347 reuse per the reuse contract on CheckCreateOwner).

What

Creation/import owner admission: owner must equal the principal name or be an org the principal belongs to (any roster role — v1 member-may-create, documented); else 403 naming the allowed owners with nothing created. Host admins bypass (documented; auth-none anon carries Admin so zero-config preserved). Anonymous 401, probe errors 503.

Where enforced (all BEFORE any namespace write)

  • Explicit create PUT + POST twin via the new api.CreateOwnerGate seam (replaces OrgGate/IsOrgMember — deleted, no shim)
  • Import Begin via RoleService.CheckCreateOwner (+ deny installs no running entry/stream/task)
  • Mirror create-from-URL twin via a new hook (nil = legacy-open), wired in composition
  • UI: New/Import owner fields become self+orgs dropdowns (headless allowedOwners helper; server 403 is the real gate; admins use the API for foreign namespaces)

Admin override decision

Global host admins bypass (p.Admin short-circuit in the helper). Rationale: operators must be able to repair/squat-handle namespaces; all existing admin-path tests pass unchanged.

Out of scope (lands in #347)

SSH/HTTP push guardrails + auto-create-on-push reuse the same helper verbatim (reuse contract documented on CheckCreateOwner); 17_ssh.md untouched deliberately.

Tests

  • identity: CheckCreateOwner matrix (11 cases) + MemberOrgs + probe-error; -race green; 97.2%
  • repoimport: checkCreate matrix vs REAL identity service (9 cases) + Begin deny-no-partial-state + probe-503; -race green; 95.7%
  • api: gate matrix (member/nonmember/self/foreign/admin/probe-503) on PUT + POST; -race green; 95.3%
  • mirror: admitted/foreign-403-nothing-created/unavailable-503/nil-hook; -race green; 96.8%
  • web: allowedOwners node tests (member/sort/failure); full node suite = baseline-identical (13 pre-existing dist-dependent failures, +2 new passes)
  • gofmt/vet clean; make cover blocked only by pre-existing server dist-dependent failures (verified identical on origin/main)
  • No new deps. No browser drive (shared daemon loopback guard per workspace rules); vite build unavailable offline (no pnpm) — JSX reuses in-file Show/For/select patterns.
Closes #346 (prerequisite #348 landed as 222e3f7; the admission helper is shaped for #347 reuse per the reuse contract on CheckCreateOwner). ## What Creation/import owner admission: owner must equal the principal name or be an org the principal belongs to (any roster role — v1 member-may-create, documented); else 403 naming the allowed owners with nothing created. Host admins bypass (documented; auth-none anon carries Admin so zero-config preserved). Anonymous 401, probe errors 503. ## Where enforced (all BEFORE any namespace write) - Explicit create PUT + POST twin via the new api.CreateOwnerGate seam (replaces OrgGate/IsOrgMember — deleted, no shim) - Import Begin via RoleService.CheckCreateOwner (+ deny installs no running entry/stream/task) - Mirror create-from-URL twin via a new hook (nil = legacy-open), wired in composition - UI: New/Import owner fields become self+orgs dropdowns (headless allowedOwners helper; server 403 is the real gate; admins use the API for foreign namespaces) ## Admin override decision Global host admins bypass (p.Admin short-circuit in the helper). Rationale: operators must be able to repair/squat-handle namespaces; all existing admin-path tests pass unchanged. ## Out of scope (lands in #347) SSH/HTTP push guardrails + auto-create-on-push reuse the same helper verbatim (reuse contract documented on CheckCreateOwner); 17_ssh.md untouched deliberately. ## Tests - identity: CheckCreateOwner matrix (11 cases) + MemberOrgs + probe-error; -race green; 97.2% - repoimport: checkCreate matrix vs REAL identity service (9 cases) + Begin deny-no-partial-state + probe-503; -race green; 95.7% - api: gate matrix (member/nonmember/self/foreign/admin/probe-503) on PUT + POST; -race green; 95.3% - mirror: admitted/foreign-403-nothing-created/unavailable-503/nil-hook; -race green; 96.8% - web: allowedOwners node tests (member/sort/failure); full node suite = baseline-identical (13 pre-existing dist-dependent failures, +2 new passes) - gofmt/vet clean; make cover blocked only by pre-existing server dist-dependent failures (verified identical on origin/main) - No new deps. No browser drive (shared daemon loopback guard per workspace rules); vite build unavailable offline (no pnpm) — JSX reuses in-file Show/For/select patterns.
Shared admission helper identity.CheckCreateOwner (+ MemberOrgs) replaces
the #210 org-only OrgGate/IsOrgMember seam (deleted, no shim): owner must
equal the principal name or be a member org (any roster role, v1
member-may-create); host admins bypass (auth-none anon carries Admin, so
zero-config preserved); anonymous 401, foreign owner 403 naming allowed
owners, probe errors 503. Enforced before any namespace write in explicit
create (PUT + POST twin via CreateOwnerGate), import Begin, and the mirror
create-from-URL twin (new hook, nil legacy-open). UI: New/Import owner
fields become self+orgs dropdowns (allowedOwners lib, server 403 is the
gate; admins use the API for foreign namespaces). Reuse contract for #347
(push auto-create) documented on the helper. Docs: 01 §5.2 + Decisions,
10 auth + Decisions, 11 mirror + Decisions, 07_api, 12_web_ui, 14.
Sign in to join this conversation.
No description provided.