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/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.

internal/hook/hook_test.go +28−5

@@ -2,24 +2,30 @@ package hook
2 2
3 3 import (
4 4 "bytes"
5 "path/filepath"
5 6 "strings"
6 7 "testing"
7 8
9 "github.com/rodent-software/vole/internal/access"
8 10 "github.com/rodent-software/vole/internal/acl"
9 11 "github.com/rodent-software/vole/internal/gitx"
10 12 "github.com/rodent-software/vole/internal/repo"
11 13 )
12 14
13 15 type fixture struct {
14 t *testing.T
15 m *repo.Meta
16 head string
16 t *testing.T
17 m *repo.Meta
18 head string
19 usersDB string
17 20 }
18 21
19 22 func newFixture(t *testing.T, blob string) *fixture {
20 23 t.Helper()
21 24 root := t.TempDir()
22 st := repo.New(root, "/bin/true")
25 // Access rules live in the instance database, so a fixture that pushes has to give
26 // the gate the same file the repository's rules were written to.
27 acls := testACLs(t, root)
28 st := repo.New(filepath.Join(root, "repos"), "/bin/true", acls)
23 29 m, err := st.Create("demo", "owner", "")
24 30 if err != nil {
25 31 t.Fatal(err)
@@ -51,7 +57,21 @@ func newFixture(t *testing.T, blob string) *fixture {
51 57 if err != nil {
52 58 t.Fatal(err)
53 59 }
54 return &fixture{t: t, m: m, head: commit}
60 return &fixture{t: t, m: m, head: commit, usersDB: testUsersDB(root)}
61 }
62
63 // testUsersDB is the instance database a test fixture uses: one file holding the
64 // roster and the access rules, which is what the server has.
65 func testUsersDB(root string) string { return filepath.Join(root, "users.db") }
66
67 func testACLs(t *testing.T, root string) *access.Store {
68 t.Helper()
69 acls, err := access.Open(testUsersDB(root))
70 if err != nil {
71 t.Fatal(err)
72 }
73 t.Cleanup(func() { acls.Close() })
74 return acls
55 75 }
56 76
57 77 func (f *fixture) run(actor, ref string, maxBlob int64) (int, string) {
@@ -66,6 +86,9 @@ func (f *fixture) run(actor, ref string, maxBlob int64) (int, string) {
66 86 func (f *fixture) runWith(cfg Config, ref string) (int, string) {
67 87 f.t.Helper()
68 88 cfg.RepoDir = f.m.Dir
89 if cfg.UsersDB == "" {
90 cfg.UsersDB = f.usersDB
91 }
69 92 var out bytes.Buffer
70 93 cfg.In = strings.NewReader(gitx.ZeroSHA + " " + f.head + " " + ref + "\n")
71 94 cfg.Out = &out