Skip to content

Repo write authorization (Phase 1): owner-only push behind an opt-in flag #31

@FrozenRaspberry

Description

@FrozenRaspberry

Background

The README ("Known limitations") and docs/OSS-READINESS-AUDIT.md both note that push is authenticated but not authorized:

"Push authorization is still not capability-complete. A valid DID signature is authentication, not authorization; unprotected repo branches do not yet enforce owner/UCAN capability checks."

docs/MAINTAINER-ROADMAP.md lists this under Security hardening as the first item. This issue proposes a concrete, non-breaking first slice and offers to implement it.

Current behavior

For POST /{owner}/{repo}/git-receive-pack:

  • require_signature (auth/mod.rs) verifies an RFC 9421 Ed25519 signature and injects AuthenticatedDid.
  • In git_receive_pack (api/repos.rs), an owner check only runs when the target branch is protected (is_branch_protected). Pushes to unprotected branches are accepted from any authenticated signer.

Because did:key is self-certifying, "authenticated" only proves the caller controls the key for the DID they present — any party can generate a keypair, derive its did:key, sign, and push. There is no allow-list and no registration requirement on this path. Net effect: a non-owner can write to unprotected branches of an arbitrary repo.

protect and visibility endpoints already gate on the owner via a require_owner-style check, so the idiom exists; it's just not applied to the general push path.

Proposal — Phase 1 (this work)

Follow the project's established "opt-in hardening flag before mandatory" pattern (cf. GITLAWB_REQUIRE_SIGNED_PEER_WRITES):

  • Add GITLAWB_ENFORCE_OWNER_PUSH (default false -> current behavior, no break to live/test nodes).
  • When true: in git_receive_pack, require the authenticated DID to equal the repo owner_did (matching the existing full-DID-or-short-form comparison used in branch protection/visibility) before any ref update is applied; reject unsigned/non-owner pushes with a clear error.
  • Factor the repeated owner-match logic into one small shared helper (currently duplicated across branch protection, protect, and visibility).
  • Tests: first coverage for the push authorization path (owner allowed, non-owner rejected, flag off = legacy behavior).
  • Docs: .env.example + a line in docs/RUN-A-NODE.md.

Out of scope (proposed Phase 2)

  • Collaborator write lists and UCAN git/push capability delegation (the caps::GIT_PUSH type already exists; the UCAN chain is validated structurally in require_ucan_chain but capabilities aren't yet checked against the operation). This would let non-owners be granted scoped push rights — the natural escape hatch once owner-only is the default.
  • Making enforcement the default after a compatibility window.

Related (not asserted, worth a look)

pulls/{n}/merge and hooks are write endpoints on the same auth layer; they may warrant the same owner/capability gating. Happy to audit separately to keep this PR focused.

Offer

I'd like to implement Phase 1 if the approach (opt-in flag, owner-only, helper extraction) looks right to you. Open to a different flag name / default / error semantics — flagging the design here first per CONTRIBUTING before sending a PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions