Skip to content
quarry Artifact [87a23abcad]
Login

Artifact [87a23abcad]

Artifact 87a23abcadd2e40fae43a45f12ebf1c43d23f44740b3a5118e29626798cc3306:


Summary

A fossil repository hosted on quarry brings its users and their exact capabilities with it, with no further setup. quarry replaces its ordered four-role model (read < write < admin) with fossil's capability model as the native authorization vocabulary: per-account-per-repository cap strings (gjorz, +hmnc, …), the four fossil user categories as quarry's defaults, and every quarry op gated by the capability letters that gate the equivalent fossil surface. Where a capability exists in both systems it carries over exactly; where it exists in only one, the rule is stated per letter in §6.

The one caveat is fossil's own: the user table lives in the repository database, not the artifact DAG, and fossil does not give it to anonymous cloners. The zero-setup promise therefore holds fully on the host-from-file path (zit import-fossil — the table is in the SQLite file) and on mirrors requires the owner's admin credential once (config pull user). We do not and must not bypass that boundary.

Motivation

"Host your fossil repo on quarry" is quarry's founding pitch. Today that migration silently drops every user and every permission: accounts must be re-created, roles re-granted, and quarry's four roles cannot express "wiki editor who cannot commit" at all — a shape fossil supports natively and real projects use. Import fidelity for content is already proven by the disposability harness; this RFC extends the same standard to people.

Two facts make this a refactor rather than a rewrite:

  1. Every authorization decision already flows through named chokepoints — the per-op Auth field, requiredRole(), sessionPlane.Allow, Verify, CanRead, TrustedAuthor. No page checks permissions ad hoc.
  2. The password material is already compatible: fossil's stored user.pw is the RFC §6.3 shared-secret derivation that internal/xfer implements today for sync auth.

The model

Cap strings

An account's authority on a repository is an unordered, case-sensitive string of capability letters, exactly fossil's vocabulary (caps-ref). Stored per (account, repo) in OLTP. Letters quarry does not implement are stored but inert — imports are lossless even where surfaces are missing.

Categories

Fossil's four categories become quarry's defaults, seeded per repository and editable in settings:

Category Stock caps quarry meaning
nobody gjorz every visitor, no session
anonymous hmnc (adds to nobody) any signed-in account with no explicit caps
reader kptw (adds; cap u inherits it) replaces role read's successor semantics
developer ei (adds; cap v inherits it) replaces role write's commit half

Effective caps = individual caps ∪ nobody ∪ (logged-in ? anonymous : ∅) ∪ (u ? reader : ∅) ∪ (v ? developer : ∅). v does not imply u — fossil's rule, kept deliberately, tested explicitly.

a grants everything except s, x, y (fossil's Admin-vs-Setup rule). s grants everything. quarry's account.admin flag maps to s on every repository (unchanged semantics, new spelling).

Private repositories

Fossil "takes a repository private" by blanking nobody and anonymous. quarry's private-visibility flag becomes exactly that: visibility=private ⇔ those two category strings are empty for the repo, and CanRead becomes "effective caps contain any of gjoh2r". The invisible-not-403 posture is unchanged — a reader with no read-capability cannot distinguish the repo from a nonexistent one.

Op → capability table

The single normative artifact of this RFC. Caps is an OR-set: possession of any listed letter passes. (s) marks site/setup scope (repo-less ops). Ops absent from fossil keep the nearest fossil letter so imported users get coherent behavior (rule stated in §6).

quarry op Caps fossil surface
timeline.list, tree.list, file.get, file.history, checkin.get, checkin.diff, branch.list, tag.list, artifact.resolve, repo.counts, repo.about, repo.get, search.query o Read / check-out
archive.get z /zip /tarball
— sync clone/pull (xfer layer) g Clone
— rendered hyperlinks (web layer, §7.4) h Hyperlink antibot
wiki.list, wiki.get, wiki.history j RdWiki
wiki.save (new page) f NewWiki
wiki.save (existing page) k WrWiki
technote.list/get j technotes ride wiki caps
technote.add k (fossil: technote edit needs WrWiki-class)
ticket.list, ticket.get, ticket.config(read) r RdTkt
ticket.new n NewTkt
ticket.set (comment) c ApndTkt
ticket.set (fields) w WrTkt
attachment.get r/j per target Attach targets
forum.list, forum.get, forum.modlog 2 RdForum
forum.post, forum.reply, forum.vote, forum.poll, forum.subscribe 3 WrForum
— staged-posting bypass (TrustedAuthor) 4 WrTForum
forum.queue, forum.moderate, forum.hide, forum.moderation 5 ModForum
forum.announce 5 + A spirit; require 5 Announce is email-ish; 5 gates
chat.history / chat post C Chat
me.feed, account.passwd(self) p + login Password/self
people.list (emails/PII surfaces) e for PII fields; list itself h RdAddr
repo.update, repo.reindex, ticket.config(write), mirror.set/detach/writes/run a Admin
repo.add, repo.delete, repo.remove, account.add/delete/grant/revoke/list, fleet.status, fleet.sync s (site) Setup
— sync push (xfer) i + §6.3 secret Write/check-in
— private-branch sync x Private (stored, inert until zit surfaces it)

Stored-but-inert letters at ship: b B t q l m 6 7 y D d u v-internals where noted, i.e. anything whose row above doesn't exist yet keeps its letter in storage and does nothing. When quarry grows the surface, the letter starts working with no migration.

Storage & plumbing changes

OLTP (internal/oltp)

CREATE TABLE IF NOT EXISTS repo_caps (       -- replaces repo_role's meaning
  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 ( -- per-repo category defaults
  repo     TEXT NOT NULL,
  category TEXT NOT NULL,                    -- nobody|anonymous|reader|developer
  caps     TEXT NOT NULL,
  PRIMARY KEY (repo, category)
);
-- account gains: legacy_pw TEXT DEFAULT '' (imported §6.3 hash; cleared on
-- first successful login after argon2 rehash)

Migration: existing repo_role rows translate once at open — read→u, write→uv i? No: write→v plus u (quarry's write always included read) — i.e. read→u, write→uv, admin→a. repo_role is kept readable for one release, then dropped. PurgeRepo gains the new tables.

ops (internal/ops)

Op gains Caps string (OR-set). Auth stays for router-level coarse gating (Anonymous/Token/Admin unchanged as a floor), Caps refines it. Registry exposes CapsOf(op). ops.User unchanged.

Deciders (cmd/quarry, internal/web)

One new function is the heart of the change:

// EffectiveCaps: individual ∪ category expansion (§3), memoized per
// (account, repo) per request.
func (db *DB) EffectiveCaps(account, repo string) CapSet

sessionPlane.Allow(user, repo, need) → Allow(user, repo, op) checking EffectiveCaps ∩ CapsOf(op). requiredRole deleted. Verify (Basic/sync) gains the same check. Web mayAt/gateAt take an op or cap set instead of a level; CanRead = "any of gjoh2r"; TrustedAuthor = "has 4 or 5".

The h cap (web only)

Fossil's antibot rule. quarry implements it at the render layer: when the viewer lacks h, pages render with hyperlinks as plain text (fossil's behavior), not by withholding content. Off by default for existing quarry repos (nobody keeps h in quarry's default nobody string — see §6 defaults divergence note); imported fossil repos keep whatever their table says.

Import & transport

  1. Host-from-file (primary, zero-setup): zit import-fossil reads the fossil SQLite user table (login, cap, pw, info, mtime) and the category rows, and emits them; quarry ingest writes repo_caps, repo_category_caps, and accounts (creating quarry accounts with legacy_pw when the login doesn't exist; never overwriting an existing quarry account's password — collision rule: caps merge by union, credentials keep quarry's).
  2. Mirror: zit config pull user (same channel as shun) with the owner's upstream admin credential, stored per-mirror like sync creds; re-pulled on the mirror schedule so upstream user changes track.
  3. Login compat: login verifier tries argon2, then legacy_pw (= §6.3 hash via xfer.SharedSecret); success rehashes to argon2 and clears legacy_pw.

Divergences, stated

Testing

Build order

Phase Work Where
0 zit: import-fossil emits user table; config pull user; zit users read verb danmestas/zit (2 issues)
1 OLTP caps tables + EffectiveCaps + migration from repo_role quarry
2 ops.Caps on all ~45 ops per §4 table; deciders swapped; requiredRole deleted quarry
3 web gates (mayAt/gateAt/CanRead/TrustedAuthor) on caps; h render rule quarry
4 import wiring + legacy login verifier quarry (needs 0)
5 matrix harness test + settings UI for caps/categories quarry

Phases 1–3 land behind no flag — the migration makes existing grants equivalent, and the suite gates each phase. Phase 4 waits on zit.

Open questions