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.

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/hook/lfs_test.go +9−3

@@ -4,6 +4,7 @@ import (
4 4 "bytes"
5 5 "crypto/sha256"
6 6 "encoding/hex"
7 "path/filepath"
7 8 "strings"
8 9 "testing"
9 10
@@ -17,7 +18,8 @@ import (
17 18 // is enough to test what pre-receive sees at a pushed tip.
18 19 func filesFixture(t *testing.T, files map[string][]byte) *fixture {
19 20 t.Helper()
20 st := repo.New(t.TempDir(), "/bin/true")
21 root := t.TempDir()
22 st := repo.New(filepath.Join(root, "repos"), "/bin/true", testACLs(t, root))
21 23 m, err := st.Create("demo", "owner", "")
22 24 if err != nil {
23 25 t.Fatal(err)
@@ -42,7 +44,7 @@ func filesFixture(t *testing.T, files map[string][]byte) *fixture {
42 44 if err != nil {
43 45 t.Fatal(err)
44 46 }
45 return &fixture{t: t, m: m, head: commit}
47 return &fixture{t: t, m: m, head: commit, usersDB: testUsersDB(root)}
46 48 }
47 49
48 50 func pointerFor(content []byte) (lfs.Pointer, []byte) {
@@ -136,7 +138,8 @@ func TestTextFileIsNotWarnedAbout(t *testing.T) {
136 138 func TestDeletedPointerDoesNotBlockItsOwnRemoval(t *testing.T) {
137 139 // A broken pointer already in the tree must be removable; otherwise the only
138 140 // way out of the mistake is rewriting history.
139 st := repo.New(t.TempDir(), "/bin/true")
141 root := t.TempDir()
142 st := repo.New(filepath.Join(root, "repos"), "/bin/true", testACLs(t, root))
140 143 m, err := st.Create("demo", "owner", "")
141 144 if err != nil {
142 145 t.Fatal(err)
@@ -188,6 +191,9 @@ func TestDeletedPointerDoesNotBlockItsOwnRemoval(t *testing.T) {
188 191 RepoDir: m.Dir,
189 192 Actor: "writer",
190 193 LFS: testStore(t),
194 // The rules live in the instance database, so the gate needs it even when a
195 // test is only about pointers.
196 UsersDB: testUsersDB(root),
191 197 In: strings.NewReader(gitx.ZeroSHA + " " + c2 + " refs/heads/main\n"),
192 198 Out: &out,
193 199 })