Fix #345: public/private visibility #353

Merged
crueber merged 1 commit from fix/issue-345 into main 2026-09-11 23:26:59 +00:00
Owner

Fixes #345: visibility becomes the read authority for repo surfaces; listings filter by caller role.

Spec amendment (stated explicitly, 06 §8.3 / 07 §13 / Decisions)

anonymous_read no longer gates repo-scoped reads. Visibility (access.json, public-by-default, missing/empty/invalid resolves public) decides: public ⇒ readable without authentication (even with anonymous_read=false); private ⇒ 401 anonymous / 403 authenticated-without-read. The flag keeps only its non-repo meaning: owners/profile/org listings, /explore shell + text twin, setup.json, / + /{owner} shells, identity user/org surfaces. Nil read gate → legacy flag-only behavior everywhere. Built ON the #344 merge (ceb7331): login page/validation untouched — private shells keep the 401 + #344 login page / 307 hop for browsers.

Permission matrix (surface × visibility × principal)

Repo read surfaces = JSON API lanes, smart-HTTP upload-pack, LFS reads, bundle lists, SSH fetch, repo SPA shells. Writes/admin ops unchanged everywhere (authenticated + flags/bindings; anonymous never).

surface public + anon public + authed private + owner/bindings/org-owner/admin private + other authed private + anon
API repo reads 200 200 200 403 401+Bearer
smart-HTTP fetch 200 200 200 403 (401 prompt w/o cred, §4.2) 401+Bearer
LFS reads 200 200 200 403 401
bundles 200 200 200 403 401
SSH fetch n/a (key auth) 200 200 error, reason on stderr n/a
repo SPA shell 200 shell 200 200 200 shell, APIs 403 401 (#344 login page for browsers)
listings (owners/repos/detailed/explore) public slice (flag=true) / 401 (flag=false) public+own/org all public+own/org public slice / 401
writes/push/admin denied (401/403) flags/bindings allowed per role 403 unless flags denied

What changed

  • identity: CheckRead drops the anonymous_read conjunction (anon+public allows); new RepoVisibility (LRU-backed, missing→public, nil-svc→unknown).
  • api: dispatch + open() consult CheckRead BEFORE the flag for repo AuthRead; Visible{Repos,Owners,ReposByOwner} filters (admins/writers +0 probes); catalog rollup folds after filtering; summaryBody + RepoSizeRow carry visibility with ~v ETag suffix; new Env.RepoVisibility hook.
  • server: shared gateRepoRead (smart ×3, LFS); SSHUploadPack takes the principal + read-gates first; repoPageGated shells; /explore text twin filtered.
  • composition: apiEnv.RepoVisibility wired (law 8, MirrorSummary shape).
  • UI: header + row badges (lib/visibility.js), General-tab control (admin PUT preserving bindings), create flow already sent visibility; fork/import default public documented.
  • 401/403 (never 404) is deliberate: git needs a real 401 to erase dead credentials (law 9, 06 §8.4) and #344 keys on it; existence protection comes from filtered listings, not status codes.

Cost (law 6)

Listing filters = one conditional access.json GET per candidate repo (LRU version-hits carry no body); hot-path budgets untouched (push/refs/checkpoint never call here). Summary + rows reuse the trip the read gate already paid. make cover gate holds (api 95.3%, identity 97.2%, server 98.5%, sshd 96.5%).

Tests

Table-driven matrix cells: internal/api/visibility345_test.go (dispatch, ETag flip, listings, rollup filter, helpers), internal/identity/gate_test.go (flag-off matrix, RepoVisibility incl. corrupt/missing), internal/server/visibility345_test.go (gates, shell incl. #344 page, SSH ordering), web/test/unit/visibility.test.js. go vet ./... clean; -race clean; internal/e2e green; dependents (repoimport/pulls/issues/review/checks/social/notify/releases/tags/sizecatalog, cmd) green. No new deps.

Known environment gaps (pre-existing, fail on pristine scratch too)

  • TestUIAssetConcepts + TestSetupUIAndAssets-class shell tests need a full make web build (scratch has only a copied dist; no pnpm here).
  • 3 JS smoke tests need a live server on :8080 (ambient instance there answers 503 at /; left untouched).
  • Browser pass not run (shared daemon blocks loopback per task note); JSX reviewed against adjacent patterns, vite build left for CI.
Fixes #345: visibility becomes the read authority for repo surfaces; listings filter by caller role. ## Spec amendment (stated explicitly, 06 §8.3 / 07 §13 / Decisions) `anonymous_read` no longer gates repo-scoped reads. Visibility (`access.json`, public-by-default, missing/empty/invalid resolves public) decides: public ⇒ readable without authentication (even with `anonymous_read=false`); private ⇒ 401 anonymous / 403 authenticated-without-read. The flag keeps only its non-repo meaning: owners/profile/org listings, `/explore` shell + text twin, setup.json, `/` + `/{owner}` shells, identity user/org surfaces. Nil read gate → legacy flag-only behavior everywhere. Built ON the #344 merge (ceb7331): login page/validation untouched — private shells keep the 401 + #344 login page / 307 hop for browsers. ## Permission matrix (surface × visibility × principal) Repo read surfaces = JSON API lanes, smart-HTTP upload-pack, LFS reads, bundle lists, SSH fetch, repo SPA shells. Writes/admin ops unchanged everywhere (authenticated + flags/bindings; anonymous never). | surface | public + anon | public + authed | private + owner/bindings/org-owner/admin | private + other authed | private + anon | |---|---|---|---|---|---| | API repo reads | 200 | 200 | 200 | 403 | 401+Bearer | | smart-HTTP fetch | 200 | 200 | 200 | 403 (401 prompt w/o cred, §4.2) | 401+Bearer | | LFS reads | 200 | 200 | 200 | 403 | 401 | | bundles | 200 | 200 | 200 | 403 | 401 | | SSH fetch | n/a (key auth) | 200 | 200 | error, reason on stderr | n/a | | repo SPA shell | 200 shell | 200 | 200 | 200 shell, APIs 403 | 401 (#344 login page for browsers) | | listings (owners/repos/detailed/explore) | public slice (flag=true) / 401 (flag=false) | public+own/org | all | public+own/org | public slice / 401 | | writes/push/admin | denied (401/403) | flags/bindings | allowed per role | 403 unless flags | denied | ## What changed - identity: `CheckRead` drops the `anonymous_read` conjunction (anon+public allows); new `RepoVisibility` (LRU-backed, missing→public, nil-svc→unknown). - api: dispatch + `open()` consult `CheckRead` BEFORE the flag for repo AuthRead; `Visible{Repos,Owners,ReposByOwner}` filters (admins/writers +0 probes); catalog rollup folds after filtering; `summaryBody` + `RepoSizeRow` carry `visibility` with `~v` ETag suffix; new `Env.RepoVisibility` hook. - server: shared `gateRepoRead` (smart ×3, LFS); `SSHUploadPack` takes the principal + read-gates first; `repoPageGated` shells; `/explore` text twin filtered. - composition: `apiEnv.RepoVisibility` wired (law 8, MirrorSummary shape). - UI: header + row badges (`lib/visibility.js`), General-tab control (admin PUT preserving bindings), create flow already sent visibility; fork/import default public documented. - 401/403 (never 404) is deliberate: git needs a real 401 to erase dead credentials (law 9, 06 §8.4) and #344 keys on it; existence protection comes from filtered listings, not status codes. ## Cost (law 6) Listing filters = one conditional `access.json` GET per candidate repo (LRU version-hits carry no body); hot-path budgets untouched (push/refs/checkpoint never call here). Summary + rows reuse the trip the read gate already paid. `make cover` gate holds (api 95.3%, identity 97.2%, server 98.5%, sshd 96.5%). ## Tests Table-driven matrix cells: `internal/api/visibility345_test.go` (dispatch, ETag flip, listings, rollup filter, helpers), `internal/identity/gate_test.go` (flag-off matrix, RepoVisibility incl. corrupt/missing), `internal/server/visibility345_test.go` (gates, shell incl. #344 page, SSH ordering), `web/test/unit/visibility.test.js`. `go vet ./...` clean; `-race` clean; `internal/e2e` green; dependents (repoimport/pulls/issues/review/checks/social/notify/releases/tags/sizecatalog, cmd) green. No new deps. ## Known environment gaps (pre-existing, fail on pristine scratch too) - `TestUIAssetConcepts` + `TestSetupUIAndAssets`-class shell tests need a full `make web` build (scratch has only a copied dist; no pnpm here). - 3 JS smoke tests need a live server on :8080 (ambient instance there answers 503 at `/`; left untouched). - Browser pass not run (shared daemon blocks loopback per task note); JSX reviewed against adjacent patterns, vite build left for CI.
Visibility (access.json, public-by-default, missing resolves public)
becomes the read authority for every repo-scoped read surface: JSON API
lanes (dispatch + open consult CheckRead before the anonymous_read flag),
smart-HTTP upload-pack, LFS reads, bundle lists (shared gateRepoRead),
SSH fetch (Transport carries the principal; read gate runs first), and
repo SPA shells (repoPageGated serves public shells to anonymous callers).
anonymous_read keeps only its non-repo meaning (owners/profile/org
listings, /explore, setup.json, non-repo shells, identity user/org
surfaces). Spec amendments in docs/go/06_server_http.md,
docs/go/07_api.md (incl. permission matrix), docs/go/17_ssh.md.

Listings (owners, owner repos, both detaileds, /explore text twin) omit
repos the caller cannot read via the access LRU (admins/writers bypass
with +0 probes); activity/size aggregates fold the catalog after
filtering. summaryBody + detailed rows carry visibility with ~v ETag
coverage. UI: header + row badges, General-tab visibility control (admin
PUT preserving bindings), create flow already sent visibility; fork/import
default public documented. Private-denied reads stay 401/403 (law 9 git
credential-erase + #344 login page), never 404.

Tests: table-driven matrix cells (API dispatch, summary ETag flip,
listings filters, catalog rollup filter, identity gate + RepoVisibility,
server gates + shell + SSH, node badge lib); -race clean.
Sign in to join this conversation.
No description provided.