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.
Phase 4: access rules live in the instance database
ec5c519cb9fc4277baa8263cfd31891a95f1d7ac · Keenan Nemetz · 17d 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.
internal/index/index.go +7−2
| @@ -4,7 +4,9 @@ | |||
| 4 | 4 | package index | |
| 5 | 5 | ||
| 6 | 6 | import ( | |
| 7 | "crypto/sha256" | ||
| 7 | 8 | "database/sql" | |
| 9 | "encoding/hex" | ||
| 8 | 10 | "errors" | |
| 9 | 11 | "fmt" | |
| 10 | 12 | "time" | |
| @@ -243,8 +245,11 @@ func entryFor(m *repo.Meta) (Entry, error) { | |||
| 243 | 245 | } | |
| 244 | 246 | e.ACLDefault = a.Default | |
| 245 | 247 | e.ACLPrincipals = len(a.Entries) | |
| 246 | if sha, err := m.Git().RevParse(repo.ACLRef); err == nil { | ||
| 247 | e.ACLSHA = sha | ||
| 248 | // A digest rather than a commit id: the rules live in the instance database now, so | ||
| 249 | // the thing worth recording is which version of them this row was built from. | ||
| 250 | if raw, err := a.Bytes(); err == nil { | ||
| 251 | sum := sha256.Sum256(raw) | ||
| 252 | e.ACLSHA = hex.EncodeToString(sum[:]) | ||
| 248 | 253 | } | |
| 249 | 254 | return e, nil | |
| 250 | 255 | } | |