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/transport/transport_test.go +12−4
| @@ -9,6 +9,7 @@ import ( | |||
| 9 | 9 | "strings" | |
| 10 | 10 | "testing" | |
| 11 | 11 | ||
| 12 | "github.com/rodent-software/vole/internal/access" | ||
| 12 | 13 | "github.com/rodent-software/vole/internal/acl" | |
| 13 | 14 | "github.com/rodent-software/vole/internal/gitx" | |
| 14 | 15 | "github.com/rodent-software/vole/internal/repo" | |
| @@ -48,9 +49,9 @@ func TestIsPush(t *testing.T) { | |||
| 48 | 49 | } | |
| 49 | 50 | ||
| 50 | 51 | // makeRepo creates a repo with one commit and returns its name. | |
| 51 | func makeRepo(t *testing.T, root string, public bool) string { | ||
| 52 | func makeRepo(t *testing.T, root string, public bool, acls *access.Store) string { | ||
| 52 | 53 | t.Helper() | |
| 53 | st := repo.New(root, "/bin/true") | ||
| 54 | st := repo.New(root, "/bin/true", acls) | ||
| 54 | 55 | m, err := st.Create("demo", "owner", "") | |
| 55 | 56 | if err != nil { | |
| 56 | 57 | t.Fatal(err) | |
| @@ -87,8 +88,15 @@ func makeRepo(t *testing.T, root string, public bool) string { | |||
| 87 | 88 | func newProxy(t *testing.T, public bool) *Proxy { | |
| 88 | 89 | t.Helper() | |
| 89 | 90 | root := filepath.Join(t.TempDir(), "repos") | |
| 90 | makeRepo(t, root, public) | ||
| 91 | st := repo.New(root, "/bin/true") | ||
| 91 | // The creator and the proxy are two Store values over one root, so they have to | ||
| 92 | // share the database the rules were written to. | ||
| 93 | acls, err := access.Open(filepath.Join(filepath.Dir(root), "users.db")) | ||
| 94 | if err != nil { | ||
| 95 | t.Fatal(err) | ||
| 96 | } | ||
| 97 | t.Cleanup(func() { acls.Close() }) | ||
| 98 | makeRepo(t, root, public, acls) | ||
| 99 | st := repo.New(root, "/bin/true", acls) | ||
| 92 | 100 | return New(Config{ | |
| 93 | 101 | Root: root, | |
| 94 | 102 | Open: st.Open, | |