Visibility modes: private, logged-in only vs private, owner/org only (replaces the single members-only private) #374
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#374
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's requested
The repo Access tab's visibility control currently offers:
publicprivate — members only"Members only" isn't the desired semantic. Replace the private option with two distinct private modes, chosen by who owns the repo:
The UI should present the applicable options based on whether the repo's owner is a user or an organization (the org-vs-user distinction rides the #348 listing/profile work).
Current state (code evidence)
web/src/pages/Access.jsx:111) renders exactly two options:publicandprivate — members only, postingvisibilitythrough the access API.VisibilityPublic/VisibilityPrivate(internal/identity/identity.go:101-102,validVisibility:105-107, enforced innormalizeAccessaccess.go:62-64).privatetoday (Resolve,internal/identity/access.go:215-250): role comes from explicitRoleBindings(includingteam:<org>/<slug>subjects), org-owner implicit admin (:234-236), host-admin, or the host-widep.Writefallback (:238-244). Anonymous gets read only when visibility is public (:246-248). So "private — members only" currently means: bindings + org owners + host admins + host-wide-write users — notably including any authenticated user with host-widewrite, and excluding ordinary org members without bindings (they only get in viateam:subjects or org-owner status).internal/api/placeholder.go:381-382).Proposed design
Visibilitywith:public(unchanged) — anonymous read allowed.authenticated(new) — "private, logged in only": any authenticated principal gets read; anonymous denied. Replaces the current intent ofprivatefor user-owned repos.private(semantics refined) — "visible only by owner/org": owner(s), org members, explicit bindings, host admin. For an org-owned repo this is the members-only mode the user wants; for a user-owned repo it means the owner + explicit bindings only.privateas its own value (recommended — the enum is additive; oldprivatedocs gain the refined semantics) and addauthenticated; the UI then shows the applicable pair per owner type. The old label "members only" disappears either way.Resolve:authenticated: after the existing binding/org-owner/admin resolution, an authenticated principal with no role getsRoleRead(insert before the anonymous-public branch; anonymous still denied).private(org-owned): org members (not just owners) get at leastRoleRead— today an org member without a binding gets nothing, which contradicts "members only". Requires a membership check for the owner org (isOrgMemberexists alongsideisOrgOwneror viagetMembers).private(user-owned): owner + bindings + admin only (current behavior, now correctly labeled).p.Writefallback (:238-244) needs review against #347: a host-write-only outsider should probably NOT read private repos — that's part of the same rule tightening. Decision point for the user.public+ the applicable private modes — for a user-owned repo:public/private — logged in only/private — owner only; for an org-owned repo:public/private — logged in only/private — org members only. The org-vs-user distinction comes from #348's owner-kind marker (orGetOrgexistence as fallback).normalizeAccess/validVisibility(access.go:62-64), the create path (placeholder.go:381-382), the SDK typedef (web/sdk/src/types.js:85), and the settings copy. Listings/explore (#345 filtering) must treatauthenticatedlikeprivatefor anonymous callers (hidden) and like public for authenticated ones (visible) — the listing filter needs the caller's auth state, which it has.privaterepos keepprivate(now refined semantics) — but note the semantic change means org members gain access and host-write outsiders lose it; enumerate that behavioral delta in the PR and flag it for the user if the host-write fallback stays.Acceptance criteria
authenticatedvisibility: any signed-in user can read the repo (API + git + web), anonymous gets 401/404 per the #345 convention.privateon an org-owned repo: org members read without explicit bindings; non-members (even authenticated with host-wide write, per decision) denied.privateon a user-owned repo: owner + explicit bindings + admin only.authenticatedandprivaterepos from anonymous callers;authenticatedrepos appear to any signed-in user.privatedocs migrate without data changes.Fix ready for review: #378 (branch fix/issue-374).
Rulings you asked for:
All acceptance cells tested (matrix + Resolve roles + SSH e2e incl. a mallory-denied-over-SSH cell); gates green (33 pkgs ok, race on identity/api/server, coverage ≥95, fmt/vet, web 745/0 + vite/esbuild). Not merging — awaiting review.
Review of PR #378 (fix/issue-374) — verified in scratch worktree at
9630c26. No browser (per instructions: tests + reasoning; web changes are select-option/badge logic covered by node unit tests).ADVERSARIAL AUTH REVIEW — all 9 points hold:
(1) Enum additive: VisibilityAuthenticated added alongside public/private (identity.go); no stored value migrated, no field renumber (string enum). normalizeAccess/validVisibility accept 3 spellings; EnsureRepoAccess materializes authenticated; unknown still falls back to public on create path, 400 on POST/PUT. Old private docs untouched. PASS.
(2) Resolve order (access.go:304-341): bindings → orgRosterRole → authenticated → admin → anon-public. Bindings/roster/authenticated all inside if !p.Anonymous, so anonymous resolves nothing except public (gate.go:45-50: anon gets 401 unless public). Authenticated stranger on private resolves '' → 403. No anon-reads-non-public path; no authed-reads-owner-only-private path. PASS.
(3) Host-write retired everywhere on reads: CheckRead early-allows admin only (gate.go:41); Resolve grants admin only; unfiltered() admin-only (visibility.go) so writers pay per-repo CheckRead probes in listings. Grep p.Write on read paths: only /me echo (discovery.go:166) and the documented repoimport checkCreate carve-out (service.go:518: host flag overcomes per-repo Forbidden only, 401/503 surface, roles consulted first — correct for a create-capability). SSH fetch (bind_ssh.go:78), HTTP git (smart.go gateRepoRead), LFS (lfs.go:65), health probe all funnel through CheckRead. PASS, carve-out documented in code + 01 Decisions.
(4) Org-member private read: orgRosterRole (one members.json GET — same probe the old owner check paid, no added round trip) maps owner→admin, member→read; non-member/absent-org/error → false. Non-member with host-write denied (matrix cell org-priv-writer → 403). PASS.
(5) User-owned private: owner binding + explicit bindings + admin only; matrix pins owner-unbound denial even for the owner (fail closed). PASS.
(6) Listings: anon hides authenticated+private (CheckRead anon branch); signed-in sees authenticated (Resolve RoleRead). Writers now probed per repo — same probes ordinary users already pay, control-plane/SWR-cached, never on the law-6 budgeted paths (visibility.go header states this). No dedicated measurement, but structurally cost-neutral vs existing filtered callers. Acceptable.
(7) UI: Access.jsx + Settings.jsx probe owner kind via orgs.get 404→null (email owners skip via shouldFetchTeams); user gets owner-only label, org gets org-members-only label; New.jsx neutral triple (owner/org only). Old 'members only' label gone from selects/badge/SDK (residual hits are historical-rationale comments only). Loading-state flicker (undefined→user options→org options) is cosmetic; server accepts all spellings regardless of owner. PASS.
(8) No regress #345/#346/#347/#370: scriptedGate updated to admin-only bypass (host-write private → 403); push path untouched (CheckPush separate, host-write already stripped in #347); ETag ~v flows the visibility string; CheckCreateOwner untouched; matchPrincipal/username aliasing untouched. Pushguard SSH e2e owner-bind + mallory-denied cell passes. PASS.
(9) Migration delta enumerated + honest in 01 Decisions #374 entry: org members GAIN read, host-write outsiders LOSE private read, operator guidance to flip all-logged-in repos to authenticated. PASS.
(10) VERIFIED RESULTS: go build clean; gofmt clean; go vet clean (identity/api/server/repoimport); -race green identity, api, repoimport, server (server: only TestUIAssetConcepts fails — fails identically on clean main, missing concepts/.gif build artifact, pre-existing env); coverage identity 95.9%, api 95.2%, repoimport 95.7%, server 98.3% (all ≥95 gate); node visibility.test.js 4/4 pass, full web suite 746/748 (2 fails are smoke.test.js vs foreign :8080 instance — identical on main, PR-documented); TestPushGuard pass incl. new mallory cell.
NITS (no fix pushed): isOrgMemberFor is production-dead (only test caller; Resolve uses orgRosterRole directly) — harmless tested helper; writers-listing probe cost unmeasured (reasoned cost-neutral).
MERGE RECOMMENDATION: ready to merge.
Fixed by PR #378 (review clean — adversarial auth-semantics pass, host-write retired, no regress on #345/#346/#347/#370), merged. Closing.