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.

The roster is a database, not a repository

427c47611f7db09e52618a0f2d05291a8641b2d0 · Keenan Nemetz · 17d ago · parent 0f442b1c

The roster is a database, not a repository

data/system.git is gone. Accounts, every name they have held and their SSH keys
live in data/users.db, next to the other authoritative store (runs.db) and the
disposable index.

  users       id, name, status, created
  user_names  every name ever held -- primary key, so a name is issued once
  user_keys   fingerprint, type, line -- primary key, so a key has one owner

Two invariants that used to be parse-time checks are now table keys, and a roster
change is one transaction instead of a commit, a CAS retry and a tree walk. The
snapshot, the five-second reload and the audit line per change are unchanged, so
`vole user add-key` still applies without a restart and revocation is still
immediate.

Clients no longer clone the roster, because there is nothing to clone. The one
view a client needs -- active id and name -- is served by a `vole-roster` SSH
verb over the key it already pushes with, so name resolution gained no endpoint
and no second credential. `vole request new --author keenan` now resolves through
that verb (TestRequestCLIResolvesNamesOverSSH: the request landed with
author_id = the account id, and an unknown name fails before pushing).

A WAL database cannot be backed up with cp, so `vole user backup <path>` takes a
consistent copy with VACUUM INTO (TestRosterIsOneDatabaseFile opens the copy and
resolves a renamed account and its key from it). This replaces the
"identity survives without a side database" criterion, which is withdrawn rather
than left looking unmet: the roster is now exactly one file, and the backup is
what proves it is not lost with the index.

Verified through the binary: init -> create -> add-key -> rename -> list ->
backup, the restored copy listing the same account, former name and key; then
scripts/ci.sh green (gofmt, vet, build, markdown links, go test -race ./...).

internal/hook/hook.go +7−7

@@ -29,9 +29,9 @@ type Config struct {
29 29 // ActorID is the account id resolved at the SSH handshake. ACL entries and
30 30 // approvals are keyed by it.
31 31 ActorID string
32 // SystemRepo, when set, is the roster repository, used only to render account
33 // ids as names in messages.
34 SystemRepo string
32 // UsersDB, when set, is the roster database, used only to render account ids
33 // as names in messages.
34 UsersDB string
35 35 // SpoolDir receives one file per accepted ref update, for the CI queue.
36 36 SpoolDir string
37 37 // RunsDB is the run queue, read here so required checks are enforced at the
@@ -82,7 +82,7 @@ func PreReceive(cfg Config) int {
82 82 return 1
83 83 }
84 84 // Names are a courtesy for the person reading the rejection; ids decide.
85 names := openNames(cfg.SystemRepo)
85 names := openNames(cfg.UsersDB)
86 86 who := func(accountID string) string { return names(accountID) }
87 87
88 88 role := a.RoleFor(actorID)
@@ -427,11 +427,11 @@ func repoNameFromDir(dir string) string {
427 427 // openNames returns a function rendering account ids as names, falling back to
428 428 // the id when the roster is unavailable. Display only: nothing is authorized by
429 429 // the result.
430 func openNames(systemRepo string) func(string) string {
431 if systemRepo == "" {
430 func openNames(usersDB string) func(string) string {
431 if usersDB == "" {
432 432 return func(accountID string) string { return accountID }
433 433 }
434 store, err := identity.Open(systemRepo)
434 store, err := identity.Open(usersDB)
435 435 if err != nil {
436 436 return func(accountID string) string { return accountID }
437 437 }