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.go +28−1

@@ -11,6 +11,7 @@ import (
11 11 "strings"
12 12 "time"
13 13
14 "github.com/rodent-software/vole/internal/access"
14 15 "github.com/rodent-software/vole/internal/acl"
15 16 "github.com/rodent-software/vole/internal/gitx"
16 17 "github.com/rodent-software/vole/internal/identity"
@@ -62,7 +63,33 @@ func PreReceive(cfg Config) int {
62 63 if cfg.In == nil {
63 64 cfg.In = strings.NewReader("")
64 65 }
65 m := &repo.Meta{Name: repoNameFromDir(cfg.RepoDir), Dir: cfg.RepoDir}
66 // Access rules are in the instance database, keyed by the repository's stable id,
67 // so the gate has to open the repository the same way the server does rather than
68 // assume a directory is a repository.
69 var (
70 acls *access.Store
71 err error
72 )
73 if cfg.UsersDB != "" {
74 acls, err = access.Open(cfg.UsersDB)
75 if err != nil {
76 reject(cfg.Out, "cannot open the access rules: %v", err)
77 return 1
78 }
79 defer acls.Close()
80 }
81 // GIT_DIR may be "." — that is what git hands a hook — so the directory is made
82 // absolute before anything is derived from it.
83 dir, err := filepath.Abs(cfg.RepoDir)
84 if err != nil {
85 reject(cfg.Out, "cannot resolve %s: %v", cfg.RepoDir, err)
86 return 1
87 }
88 m, mErr := repo.New(filepath.Dir(dir), "", acls).Open(repoNameFromDir(dir))
89 if mErr != nil {
90 reject(cfg.Out, "cannot open %s: %v", dir, mErr)
91 return 1
92 }
66 93 a, err := m.ACL()
67 94 if err != nil {
68 95 reject(cfg.Out, "cannot read repository access rules: %v", err)