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) | |