Artifact 8e316fc1a2622770b4c54e75d3755650bee4cecac7ebbea5c695412b9ff398e3:
- File
internal/oltp/caps.go
— part of check-in
[be3686ccbe]
at
2026-08-22 14:19:19
on branch fossil-caps
— caps phase 1: fossil cap strings in OLTP, EffectiveCaps expansion, roles→caps migration
RFC 0004 §3/§5. v never implies u; a excludes s x y; s is universal; blank nobody+anonymous = private. Migration runs on every open, idempotent, never clobbers explicit caps. (user: dmestas, size: 5690) [annotate] [blame] [check-ins using] [more...]
// caps.go — fossil's capability model, quarry-native (RFC 0004). An // account's authority on a repository is an unordered, case-sensitive // string of fossil capability letters, expanded through fossil's four // user categories. The expansion rules are fossil's exactly, including // the deliberate trap: v (developer) does NOT imply u (reader). // // Letters quarry has no surface for yet are stored and inert: imports // stay lossless, and a letter starts working the day its surface ships, // with no migration. package oltp import "strings" const capsSchema = ` CREATE TABLE IF NOT EXISTS repo_caps ( account TEXT NOT NULL, repo TEXT NOT NULL, caps TEXT NOT NULL, -- fossil cap string, verbatim PRIMARY KEY (account, repo) ); CREATE TABLE IF NOT EXISTS repo_category_caps ( repo TEXT NOT NULL, category TEXT NOT NULL, -- nobody | anonymous | reader | developer caps TEXT NOT NULL, PRIMARY KEY (repo, category) );` // accountMigrations: legacy_pw carries an imported fossil user's RFC // §6.3 hash until their first successful login rehashes to argon2. var accountMigrations = []string{ `ALTER TABLE account ADD COLUMN legacy_pw TEXT NOT NULL DEFAULT ''`, } // Stock category defaults — fossil's shipped values (caps-index §ucat). const ( StockNobody = "gjorz" StockAnonymous = "hmnc" StockReader = "kptw" StockDeveloper = "ei" ) // allCaps is the closed fossil vocabulary, for s-expansion display; the // letters d, u, v, L are meta/legacy and excluded from concrete grants. const allCaps = "abcefghijklmnopqrstwxyz234567ABCD" // CapSet is an expanded, concrete capability set. Always produced by // EffectiveCaps — never construct one from raw stored strings, which // are unexpanded. type CapSet string // HasAny reports whether the set holds any of the asked letters. The // two umbrella caps resolve here: s grants everything; a grants // everything except s, x, y (fossil's Admin-vs-Setup rule). func (c CapSet) HasAny(letters string) bool { for _, l := range letters { if strings.ContainsRune(string(c), l) { return true } if strings.ContainsRune(string(c), 's') { return true } if strings.ContainsRune(string(c), 'a') && !strings.ContainsRune("sxy", l) { return true } } return false } // capUnion merges cap strings, deduplicated, order-free. func capUnion(sets ...string) string { var b strings.Builder seen := map[rune]bool{} for _, s := range sets { for _, r := range s { if !seen[r] { seen[r] = true b.WriteRune(r) } } } return b.String() } // SetCaps stores (replacing) one account's explicit caps on a repo. func (d *DB) SetCaps(account, repo, caps string) error { if caps == "" { _, err := d.db.Exec(`DELETE FROM repo_caps WHERE account = ? AND repo = ?`, account, repo) return err } _, err := d.db.Exec(`INSERT INTO repo_caps (account, repo, caps) VALUES (?, ?, ?) ON CONFLICT (account, repo) DO UPDATE SET caps = excluded.caps`, account, repo, caps) return err } // CapsOf returns the stored, UNEXPANDED cap string ("" when none). func (d *DB) CapsOf(account, repo string) (string, error) { var caps string err := d.db.QueryRow(`SELECT caps FROM repo_caps WHERE account = ? AND repo = ?`, account, repo).Scan(&caps) if err != nil { return "", nil } return caps, nil } // SetCategoryCaps overrides one category's caps for one repo. An empty // string is a REAL value — fossil "takes a repo private" by blanking // nobody and anonymous, so absence and emptiness must differ. func (d *DB) SetCategoryCaps(repo, category, caps string) error { _, err := d.db.Exec(`INSERT INTO repo_category_caps (repo, category, caps) VALUES (?, ?, ?) ON CONFLICT (repo, category) DO UPDATE SET caps = excluded.caps`, repo, category, caps) return err } // CategoryCaps returns a repo's category string, stock when unset. func (d *DB) CategoryCaps(repo, category string) string { var caps string err := d.db.QueryRow(`SELECT caps FROM repo_category_caps WHERE repo = ? AND category = ?`, repo, category).Scan(&caps) if err == nil { return caps } switch category { case "nobody": return StockNobody case "anonymous": return StockAnonymous case "reader": return StockReader case "developer": return StockDeveloper } return "" } // EffectiveCaps expands an account's authority on a repo (RFC 0004 §3): // every visitor holds nobody; a signed-in account adds anonymous and // its explicit caps; u pulls in the reader category, v the developer // category (v never implies u); a site admin holds s everywhere. func (d *DB) EffectiveCaps(account, repo string) CapSet { eff := d.CategoryCaps(repo, "nobody") if account != "" { eff = capUnion(eff, d.CategoryCaps(repo, "anonymous")) own, _ := d.CapsOf(account, repo) eff = capUnion(eff, own) if acct, found := d.AccountByName(account); found && acct.Admin { eff = capUnion(eff, "s") } } if strings.ContainsRune(eff, 'u') { eff = capUnion(eff, d.CategoryCaps(repo, "reader")) } if strings.ContainsRune(eff, 'v') { eff = capUnion(eff, d.CategoryCaps(repo, "developer")) } return CapSet(eff) } // MigrateRolesToCaps translates legacy repo_role rows into cap strings: // read→u, write→uv, admin→a. INSERT OR IGNORE is the idempotency — // explicit caps are never clobbered, and reruns are free. repo_role // itself stays readable for one release (RFC 0004 §5). func (d *DB) MigrateRolesToCaps() error { _, err := d.db.Exec(`INSERT OR IGNORE INTO repo_caps (account, repo, caps) SELECT account, repo, CASE role WHEN 'read' THEN 'u' WHEN 'write' THEN 'uv' WHEN 'admin' THEN 'a' ELSE '' END FROM repo_role WHERE role IN ('read','write','admin')`) return err }