Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/vole.git

Write over SSH with a registered key:

git clone ssh://git@rodent.software:2222/vole.git

A free and open-source forge that lives in your Git repo.

#59

Bound what one repository and one account can cost

merged · by keenan 12d ago · merged into main at e5542ac6 by keenan 12d ago

keenan opened this 12d ago

Closes #13.

What was wrong

Nothing bounded a repository except the per-push receive_max_bytes and the per-object caps. A contributor could add refs in their own namespace without limit, any writer could add issues and comments, and a maintainer could add tags; every listing, comparison and page load grew with the count. A single account could make an anonymous reader wait and could grow the disk until an admin cleaned up by hand.

What this does

Repository quotas in pre-receive (server/hook/quotas.go). They describe the repository the push would leave, not the one it found, and a refusal moves no refs:

setting default what it counts
repo_max_refs 500 branches and tags, with the refs the push creates or deletes
repo_max_metadata_entries 2000 documents on the metadata branch: issues, requests, comments, approvals, runs, releases
repo_max_metadata_bytes 32 MiB their total size
repo_max_bytes 5 GiB git objects on disk
repo_max_lfs_bytes 10 GiB LFS objects, logs and artifacts, enforced on upload with 413

The values arrive the same way the blob limit already does: instance config → SSH server environment → hook, so there is one setting, not a second policy. Byte growth is bounded by the cap plus one permitted push: a repository that is already over repo_max_bytes refuses further pushes, and each push is bounded by receive_max_bytes.

Account write budget (account_writes_per_hour, 300). Keyed by account, not address, because the account is what a write is attributed to. Applies in beginWrite, so issues, comments, requests, labels and state changes share one budget; the refusal is a form-readable 429 with Retry-After.

Read caps. for-each-ref listings (branches in the repo shell, compare, settings, releases, CI) and tags are capped at 500; the quota check uses RefCount, which sees every ref. Activity feeds were already capped at 100 events and 200 state changes — verified, not changed.

Open registration baseline. Defaults are unchanged (false, role none). The role is now restricted to none, read or contribute: a self-registered account must not be able to move shared branches or approve reviews. Registration was already rate-limited per address by #55.

Notes

  • repo_max_lfs_bytes walks the repository’s object links once per upload. That is deliberate: it is the only place the bytes arrive, and an upload is a large object by definition. If it becomes hot, a cached per-repository counter is the next step, not removing the check.
  • The metadata count includes runs/. A repository at its metadata cap will refuse a runner’s record push too; the message says which quota, and an operator can raise it or prune records. That is the intended behaviour for a hard cap.
  • Quotas are off when the values are zero (tests, offline commands); the instance always sets them from config.

Verification

  • gofmt, go vet ./..., go test -race ./... clean.
  • ./vole workflow check and ./vole workflow run verify pass.

Tests

  • TestRefQuotaRefusesAPushThatWouldExceedIt: a second branch is refused at a limit of one, and moving an existing branch is not counted as a new ref.
  • TestMetadataQuotaCountsEntriesAndBytes: a two-issue tree is refused at a limit of one entry and at a small byte limit, and passes at a generous one.
  • TestRepoSizeQuotaRefusesAFullRepository, TestLFSQuotaRefusesAFullStore.
  • TestTransferRefusesAnUploadOverTheRepoQuota: a 413 on the wire, object not stored.
  • TestAccountWritesAreBudgeted: per-account budget, 429 + Retry-After, and an independent budget for another account.
  • TestValidationRefusesUnsafeCombinations: open_registration_role = maintain refused alongside the existing admin case.

Docs: docs/configuration.md (six settings and the role restriction), docs/operations.md (“Quotas”: what each bounds and what to do when one bites), docs/accounts-and-access.md (registration role).

Discussion (1)

keenan 12d ago

Amended on the branch (head e5542ac): the write budget now applies only below the maintain role.

Maintainers and admins are trusted roles — they can already change these rules, push shared branches and run the instance — so a write budget would slow their work without bounding anybody who could not do worse. beginWrite checks the account’s effective role on the repository, so an instance-wide maintainer on a public repository is exempt for the same reason a repository maintainer is.

The setting is renamed to match what it does: contributor_writes_per_hour (default 300), documented as applying to the untrusted contribute role. Read-only accounts are still budgeted, since they are below maintain too, though they cannot write in the first place.

This supersedes the ‘Account write budget’ paragraph and the configuration row in the description above. Repository quotas are unchanged and still apply to every role: they bound a repository, not a person.