Push guardrails: SSH key must belong to the repo owner or an org member — host-wide write flag must not be the push gate #347
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#347
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?
⚠️ Sequencing: #348 must land first
This ticket depends on #348 (organizations: create-org flow, public org profile, management). The "org member" half of the push guardrail below has no meaning until organizations can be created and joined through the UI — today an org can only come into existence via the API (
POST /api/v1/orgs), with no create-org UI, no org profile, and no member-management discoverability (#348 covers all of that). Implementing the repo-scoped push check before #348 would mean testing org-membership resolution against orgs users cannot create or manage.Implementation order: #348 → #346 → #347 (the admission helper this guardrail reuses is specified in #346).
What's requested
Push guardrails: the SSH key (or HTTP credential) pushing to a repo must belong to a user who owns that repo, or a user attached to the owning org (member/owner per bindings), or holds an explicit role binding on it. A host-wide "write" flag must not be the gate.
Current state (code evidence) — the push gate is host-wide, not repo-scoped
PrincipalForName(internal/server/auth.go:907-947) returnsWrite/Adminbooleans (token mode: the aggregate of the principal's static tokens; oidc mode: the browser-login admission flags).internal/sshd/sshd.go:334-339) checks onlyp.Write— a single host-wide boolean stamped from the key's permissions (sshd.go:275,perms.Extensions["write"]) — before dispatchinggit-receive-pack.SSHReceivePack(internal/server/bind_ssh.go:91-103) then deliberately leaves Write/Admin unset on the principal (:97-102, documented 17_ssh.md §3 constraint) and never consults repo-scoped access — so any key whose principal holds host-wide write can push to ANY repo, including repos owned by other users/orgs.internal/server/smart.go) gates on read-class checks +AnonymousRead; verify and align — the fix must cover both transports with one repo-scoped rule.AutoCreateOnPushwidens the hole: a push to any<owner>/<newrepo>path can create that repo under an owner string the pusher has no relationship with (see #346 for the creation-side rule).Proposed design
sshd.goand the HTTP receive-pack handler), call the identity surface'sCheckRole/Resolvefor(owner, repo, principal)requiring write — the existing P6 resolution (internal/identity/access.go:215-250: bindings, org-owner, team membership) already computes exactly this.CheckRole(ctx, id.Owner, id.Name, resolvedPrincipal, RoleWrite)), not the flag-stripped struct. Amending 17_ssh.md §3 with the new check is part of the change (docs amendment duty, law 12).CheckCreateAccessfrom #346 rather than duplicating the rule.other/reporequires owner or org membership") — not a generic write-denied.Acceptance criteria
17_ssh.md§3 amended in the same change (the :97-102 documented constraint evolves);01_identity_permissions.mdmatrix updated if the resolution entry points change.Fix ready for review: #357 (branch fix/issue-347). Repo-scoped push rule on both transports + #346 admission for auto-create, per the issue design. Not merging.
Review: PR #357 (fix/issue-347) — push guardrails
Verdict: READY TO MERGE (no blocking findings; two non-blocking notes below). Verified in a scratch worktree at
61849d9(removed afterward); main worktree left clean; no live instance/docker/browser touched (tests + reasoning only, per brief — nothing here is browser-facing).Adversarial checklist (all pass)
Pre-existing failure (not this PR)
Non-blocking notes (follow-ups, not merge gates)
No fixes pushed (nothing correctness-grade to fix). MERGE RECOMMENDATION: ready to merge.
Fixed by PR #357 (review clean — all 10 adversarial checks pass, both transports e2e green, budgets hold), merged. Closing.