feat(forgejo): deny non-admin repository creation; reconcile the live fleet #670
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/forgejo-repo-creation-lockdown"
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?
Why
sean/ex_mcpwas created on 2026-09-11 straight against the Forgejo API. It got noForgejoRepository, noForgejoPushMirrorand no branch protection, so it had no off-cluster backup — and Binjovi pins it inapps/binjovi_api/mix.exsat2bbb22ba, a commit that existed only on one Forgejo feature branch.make rebuildwipes Forgejo. The next rebuild would have destroyed the source of Binjovi's own MCP server, exactly assean/linuxwas lost on 2026-05-31. Every guard in the suite passed the whole time.The fork is now backed up at
codeberg.org/someara/ex-mcp(46 refs, verified identical). This change stops it happening again.The hole
make forgejo-credmints awrite:repositoryPAT, and in Forgejo that scope authorizesPOST /user/repos. Token scopes cannot separate "write repository content" from "create a repository", so the limit has to be set on the server.The fix
FORGEJO__REPOSITORY__MAX_CREATION_LIMIT=0— denies every non-admin creator.FORGEJO__ADMIN__DISABLE_REGULAR_ORG_CREATION=true— required, not belt-and-braces. Forgejo's documentation says a user can bypass the creation limit by making an organization; without this the first setting is decorative.forgejo-operator is unaffected — checked, not assumed
This was the risk worth proving, because if the operator lost repo creation a cold
make rebuildcould not seed the fleet. Against Forgejo v16.0.2, the deployed version:The operator authenticates as the
gitea_adminsite admin, and both its paths pass that admin asdoer:POST /admin/users/{u}/reposrouters/api/v1/repo/repo.go:268—CreateRepository(ctx, ctx.Doer(), owner, …)POST /repos/migrate(cold-rebuild seed)routers/api/v1/repo/migrate.go:180—CreateRepositoryDirectly(ctx, ctx.Doer(), repoOwner, …)Break-glass is unchanged and explicit:
make forgejo-cred-admin.Guards
tests/gitmirror-fleet.shcould never have caught this. It walks the files that exist, and says so itself: "this checks declared state only". A repository that was never declared is invisible to it.tests/forgejo-repo-creation-locked.sh(hermetic, in CI) — keeps both settings in place, and refuses an explicit-1. Negative control checked: removing either setting fails it.tests/forgejo-repo-reconcile/run-all.sh(live,NOT_IN_CI) — asks the live server and compares both directions. Same split astests/upstream-inventory.sh/tests/upstream-drift, and listed with a reason. It uses/repos/searchbecause/users/{owner}/reposneedsread:user, which the routine token does not hold — an asymmetry that is itself the point: the contributor token may create a repository but may not list them.It currently reports five undeclared repositories:
pipelinesandpipelines-operatorbelong to the retired system and may be deliberate orphans. Triaging those, and onboardingex-mcpas a full Binjovi project, are follow-ups — this change is the enforcement and the detection.Verification
tests/check.shgreen (116 guards; 106 gated by CI).git ls-remoteidentical on both sides, and the pinned commit re-fetched from Codeberg alone with a matching tree hash.https://claude.ai/code/session_01XdBRc9CHgFhAxCyHbdVtt6
`sean/ex_mcp` was created on 2026-09-11 straight against the Forgejo API. It got no ForgejoRepository, no ForgejoPushMirror and no branch protection, so it had no off-cluster backup. Binjovi pins that repository in apps/binjovi_api/mix.exs at a commit that existed only on one Forgejo feature branch. `make rebuild` wipes Forgejo, so the next rebuild would have destroyed the source of Binjovi's own MCP server. This is how `sean/linux` was lost on 2026-05-31. The contributor token from `make forgejo-cred` holds `write:repository`, and in Forgejo that scope authorizes POST /user/repos. Token scopes cannot separate writing repository content from creating a repository, so the limit belongs on the server. MAX_CREATION_LIMIT=0 denies every non-admin creator. DISABLE_REGULAR_ORG_CREATION closes the organization bypass that the Forgejo documentation names; without it the first setting is decorative. forgejo-operator is unaffected. It authenticates as the `gitea_admin` site admin, and both its create path (routers/api/v1/repo/repo.go:268) and its cold-rebuild migrate path (routers/api/v1/repo/migrate.go:180) pass that admin as `doer`. services/repository/create.go:201 reads `!doer.IsAdmin && !u.CanCreateRepo()`, so the operator passes through and a cold `make rebuild` still seeds the fleet. Checked against Forgejo v16.0.2, the deployed version. tests/gitmirror-fleet.sh could not have caught this. It walks the files that exist and says so itself: "this checks declared state only". tests/forgejo-repo-reconcile/run-all.sh asks the live server instead and compares both directions. It is a report, not a merge gate, and is listed in NOT_IN_CI beside tests/upstream-drift for the same reason. It uses /repos/search because /users/{owner}/repos needs read:user, which the routine token does not hold. It currently names five undeclared repositories. Claude-Session: https://claude.ai/code/session_01XdBRc9CHgFhAxCyHbdVtt6