Phase A: Identity & permissions (internal/identity) #3

Closed
opened 2026-09-03 23:44:25 +00:00 by crueber · 4 comments
Owner

Phase A: Identity & permissions — internal/identity

Spec: docs/features/01_identity_permissions.md (normative) + shared primitives P1–P9 in docs/features/README.md.
Rollout: Wave A in docs/features/09_rollout.md §3 — lands FIRST, everything else binds to roles. No dependencies.

  • New package internal/identity (one new package, no new core package). Registers:
    • Seam 1 RouteProvider: every endpoint in 01 §8, on both lanes (api.Lanes for repo-scoped).
    • Seam 3 policy group sources: team:<org>/<slug> and role:<owner>/<repo>:<role> expansion (01 §6).
    • Seam 5 task kind: access-bootstrap migration (01 §10).
    • Seam 7 CLI: walhub access get/put (thin store client over the same CAS path).
  • Objects (overwritable-key families join the frozen list per 14 §14.11 rule 2 in the same change):
    • users/<principal>/profile.json (CAS'd, lazy-create; <principal> = lowercased email, %40 for @) + users/<principal>/invitations/index.json.
    • orgs/<org>/org.json, orgs/<org>/members.json (one CAS'd roster object), orgs/<org>/teams/<slug>.json (CAS'd). Org create = Create of org.json (409 if taken); everything else CAS Update(version) with retry-on-412.
    • repos/<o>/<r>/access.json (CAS'd): {version, visibility: public|private, role_bindings: [{subject: user:<email>|team:<org>/<slug>, role}], updated_at}. Full-document PUT, one binding per subject (400 on dup), sorted by subject.
  • Resolution (P6 verbatim, 01 §4): access.json bindings (direct + team expansion) → org ownership → auth principal write/admin flags → anonymous (read iff host anonymous_read AND visibility == "public").
  • require_read hook (01 §4.1): implements the named-but-unspec'd hook from 14 §14.10.1 — anonymous denied gets real 401 with WWW-Authenticate: Bearer, never in-band error.
  • Invitations (01 §7): Create-only objects (orgs/<org>/invitations/<id>.json, repos/<o>/<r>/meta/invitations/<id>.json), delete-on-accept/cancel, inbox index P4-style; accept writes the binding first, then deletes the invite (idempotent loser path, 409 when no longer pending).

Acceptance criteria

  • Endpoints in 01 §8 on both lanes; wire rules per 07 §2 (plain-text errors, [] not null, RFC 3339).
  • Role → capability matrix (01 §5) enforced; triage-without-write gates others' issues; maintain gates merges/protected refs; admin gates access.json/policy/settings.
  • CAS loops everywhere (bounded ≤ 5 attempts, then 409); no lock objects; legacy repos synthesize default access.json on read + access-bootstrap materializes lazily (01 §10).
  • make cover ≥ 95% on internal/identity; -race clean; table-driven httptest per handler (AGENTS.md law 11).
  • Doc's Decisions section updated if behavior deviates (AGENTS.md law 12).
# Phase A: Identity & permissions — `internal/identity` **Spec:** `docs/features/01_identity_permissions.md` (normative) + shared primitives P1–P9 in `docs/features/README.md`. **Rollout:** Wave A in `docs/features/09_rollout.md` §3 — lands FIRST, everything else binds to roles. No dependencies. ## Recommended implementation (verified against the doc) - **New package `internal/identity`** (one new package, no new core package). Registers: - Seam 1 RouteProvider: every endpoint in 01 §8, on both lanes (`api.Lanes` for repo-scoped). - Seam 3 policy group sources: `team:<org>/<slug>` and `role:<owner>/<repo>:<role>` expansion (01 §6). - Seam 5 task kind: `access-bootstrap` migration (01 §10). - Seam 7 CLI: `walhub access get/put` (thin store client over the same CAS path). - **Objects** (overwritable-key families join the frozen list per 14 §14.11 rule 2 in the same change): - `users/<principal>/profile.json` (CAS'd, lazy-create; `<principal>` = lowercased email, `%40` for `@`) + `users/<principal>/invitations/index.json`. - `orgs/<org>/org.json`, `orgs/<org>/members.json` (one CAS'd roster object), `orgs/<org>/teams/<slug>.json` (CAS'd). Org create = `Create` of `org.json` (409 if taken); everything else CAS `Update(version)` with retry-on-412. - `repos/<o>/<r>/access.json` (CAS'd): `{version, visibility: public|private, role_bindings: [{subject: user:<email>|team:<org>/<slug>, role}], updated_at}`. Full-document PUT, one binding per subject (400 on dup), sorted by subject. - **Resolution (P6 verbatim, 01 §4):** access.json bindings (direct + team expansion) → org ownership → auth principal write/admin flags → anonymous (read iff host `anonymous_read` AND `visibility == "public"`). - **`require_read` hook (01 §4.1):** implements the named-but-unspec'd hook from 14 §14.10.1 — anonymous denied gets real 401 with `WWW-Authenticate: Bearer`, never in-band error. - **Invitations (01 §7):** Create-only objects (`orgs/<org>/invitations/<id>.json`, `repos/<o>/<r>/meta/invitations/<id>.json`), delete-on-accept/cancel, inbox index P4-style; accept writes the binding first, then deletes the invite (idempotent loser path, 409 when no longer pending). ## Acceptance criteria - [ ] Endpoints in 01 §8 on both lanes; wire rules per 07 §2 (plain-text errors, `[]` not null, RFC 3339). - [ ] Role → capability matrix (01 §5) enforced; triage-without-write gates others' issues; maintain gates merges/protected refs; admin gates access.json/policy/settings. - [ ] CAS loops everywhere (bounded ≤ 5 attempts, then 409); no lock objects; legacy repos synthesize default access.json on read + `access-bootstrap` materializes lazily (01 §10). - [ ] `make cover` ≥ 95% on `internal/identity`; `-race` clean; table-driven httptest per handler (AGENTS.md law 11). - [ ] Doc's Decisions section updated if behavior deviates (AGENTS.md law 12).
Author
Owner

Wave A (identity + permissions) starting on branch feat/identity. Plan: new package internal/identity (orgs, teams, members, access.json resolution + LRU, invitations, require_read gate), Seam 1 both-lanes chaining via server.ChainAPI, Seam 3 team:/role: expansion helper in internal/policy, Seam 5 access-bootstrap op, Seam 7 walhub access get/put CLI, org settings + repo Access tab UI, SDK submodules, EVIDENCE entry. Will open a PR (no merge) with tests + coverage when the >=95% gate passes.

Wave A (identity + permissions) starting on branch feat/identity. Plan: new package internal/identity (orgs, teams, members, access.json resolution + LRU, invitations, require_read gate), Seam 1 both-lanes chaining via server.ChainAPI, Seam 3 team:/role: expansion helper in internal/policy, Seam 5 access-bootstrap op, Seam 7 walhub access get/put CLI, org settings + repo Access tab UI, SDK submodules, EVIDENCE entry. Will open a PR (no merge) with tests + coverage when the >=95% gate passes.
Author
Owner

Wave A done — PR #7 (#7), branch feat/identity, commit 3e622b5. NOT merged per plan. Summary: internal/identity end to end (orgs/teams/members/access.json/invitations/require_read), all four seams, frozen-list amendment in the same change, UI (org settings + Access tab) + SDK, EVIDENCE E2. Gates: identity 98.5%, make cover green, -race clean, 129/129 node, full short suite green, live curl + token-mode 401s + real-Chromium drive (dark+light, zero console errors). Deviations/notes 1–6 in the PR description (synthesis without email owners, auth-required preview, fail-closed invite roles, 2 additive collection endpoints, silent-empty role: synthesis, shared-file rebase note for Repos.jsx). Only feature files/hunks committed; pre-existing worktree changes untouched.

Wave A done — PR #7 (https://git.packden.us/crueber/walhub/pulls/7), branch feat/identity, commit 3e622b5. NOT merged per plan. Summary: internal/identity end to end (orgs/teams/members/access.json/invitations/require_read), all four seams, frozen-list amendment in the same change, UI (org settings + Access tab) + SDK, EVIDENCE E2. Gates: identity 98.5%, make cover green, -race clean, 129/129 node, full short suite green, live curl + token-mode 401s + real-Chromium drive (dark+light, zero console errors). Deviations/notes 1–6 in the PR description (synthesis without email owners, auth-required preview, fail-closed invite roles, 2 additive collection endpoints, silent-empty role: synthesis, shared-file rebase note for Repos.jsx). Only feature files/hunks committed; pre-existing worktree changes untouched.
Author
Owner

Wave A identity review round 1 done on PR #7 (full findings posted on the PR). Fixed + pushed in 6e67618: CAS-loop team-cleanup (was swallowing 412s), ?n= pagination for invite lists (P5), abort-safe org delete, team-cache eviction on NotFound. Tests: identity -race pass, 98.1% coverage; api/policy/server -race pass; build clean; JS 129/129. 3 non-blocking follow-ups recorded on the PR (anon link preview 401, top-level issuer cancel, discovery endpoints[]). Merge recommendation: ready to merge.

Wave A identity review round 1 done on PR #7 (full findings posted on the PR). Fixed + pushed in 6e67618: CAS-loop team-cleanup (was swallowing 412s), ?n= pagination for invite lists (P5), abort-safe org delete, team-cache eviction on NotFound. Tests: identity -race pass, 98.1% coverage; api/policy/server -race pass; build clean; JS 129/129. 3 non-blocking follow-ups recorded on the PR (anon link preview 401, top-level issuer cancel, discovery endpoints[]). Merge recommendation: ready to merge.
Author
Owner

Wave A complete: PR #7 (feat/identity) reviewed, review fixes pushed, and merged as 366971a. internal/identity at 98.1% coverage, -race clean. Closing.

Wave A complete: PR #7 (feat/identity) reviewed, review fixes pushed, and merged as 366971a. internal/identity at 98.1% coverage, -race clean. Closing.
crueber added this to the v1 milestone 2026-09-10 22:20:56 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#3
No description provided.