vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Phase 4: access rules live in the instance database
ec5c519cb9fc4277baa8263cfd31891a95f1d7ac · Keenan Nemetz · 16d ago · parent 90fdaf39
Phase 4: access rules live in the instance database The ACL was one acl.json on refs/vole/acl: a ref only the server writes, read on every push and every request. It is configuration the instance owns, keyed by account id, and keeping it in git cost a commit, a compare-and-swap retry and a rev-parse per read to store a handful of rows — the argument that already moved the roster out of data/system.git, applied to the rules that name its accounts. It is now a row in data/users.db beside the roster, in the same file as the ids it refers to: - access.Store: repo_acl(repo_id, acl, updated, updated_by), one row per repository, keyed by the stable repository id so a rename moves nothing. - Meta.ACL reads the row; a repository with no row is deny-everything, exactly what a missing ref meant. - Meta.SetACL is one upsert, so SQLite orders concurrent edits and the CAS loop with its retries is gone. The author stays in the row and in data/audit.log, which is what the commit message used to say. - Deleting a repository forgets its rules, after the directory is gone. - repo.Store.ImportACLs reads refs/vole/acl once at startup for any repository without a row, so an existing instance keeps its rules. It never overwrites a row, and a second run imports nothing. - The index's acl_sha is a digest of the rules rather than the ACL commit id. - pre-receive reads the rules from VOLE_USERS_DB and makes GIT_DIR absolute before deriving the repository from it, since git hands a hook ".". What the ref bought — rules that travel with a clone — is what this gives up, deliberately: the rules are account ids, so a copy in a clone is unreadable without this instance's roster, and the backup that holds the accounts now holds them too. Measured: internal/repo imports rules from a real refs/vole/acl commit and is idempotent on a second run; internal/access covers round-trip, replacement, deletion and the audit line; the full suite — auth, site visibility, transport and pushes — runs against rules read from SQLite.
README.md +5−3
| @@ -28,14 +28,16 @@ Working today: | |||
| 28 | 28 | - A read-only web interface for public repositories, with no login | |
| 29 | 29 | - Anonymous HTTP clone for public repositories (useful for CI, or a network that | |
| 30 | 30 | blocks port 22) | |
| 31 | - Per-repo roles (read / write / admin) stored *in the repository* at | ||
| 32 | `refs/vole/acl`, so access rules travel with a clone | ||
| 31 | - Per-repo roles (read / write / admin) stored in the instance database beside the | ||
| 32 | roster, keyed by account id, so a rename or a re-import does not disturb them and | ||
| 33 | the one backup that holds the accounts holds the rules that name them | ||
| 33 | 34 | - Policy enforced in `pre-receive`, so a rejected push moves no refs at all | |
| 34 | 35 | - Protected refs, per-blob size limits, and a push cap enforced by git's own | |
| 35 | 36 | `receive.maxInputSize` | |
| 36 | 37 | - The user roster lives in one SQLite database, `data/users.db`: an account is a | |
| 37 | 38 | stable id plus a mutable name, with its SSH public keys, and a retired name is | |
| 38 | never issued twice. Nothing stored is secret. A running instance re-reads the | ||
| 39 | never issued twice. The same file holds each repository's access rules, keyed by | ||
| 40 | account id — they are names for accounts, so they belong with the accounts. Nothing stored is secret. A running instance re-reads the | ||
| 39 | 41 | database every few seconds, so `vole user add-key` or `vole user retire` applies | |
| 40 | 42 | without a restart and revocation takes effect the same way; an append-only | |
| 41 | 43 | `data/audit.log` records every change. A clone has no copy of the roster — the | |