Artifact 7dba95ebc444058560ca4c8e4a6e90f4329882c5fda68a06b257b0e347b722c2:
- File
cmd/quarry/repocmd.go
— part of check-in
[86880118d5]
at
2026-08-18 02:09:54
on branch trunk
— account: manage identity through the API, and stop lying about why
Six ops (account.list/add/passwd/delete/grant/revoke, admin) plus quarry accounts on top of them. Creating a login or granting someone read on a repository no longer needs shell access on the server.
quarry account still opens OLTP directly and has to: the FIRST account must exist before any server runs or any admin exists to authenticate as. Everything after that is the API, so the CLI has no privileged path.
The verifier never crosses the seam in either direction, and account.add /passwd scrub the password out of any error before it travels โ an error is the one value that reaches a log, an HTTP body and a terminal at once.
Verify returns a three-valued Decision instead of a bool. Denied and Insufficient are not the same thing and the wire has separate codes for them: an authenticated client short of a capability was being told 401 with a challenge, which invites it to retry the same password forever. Now 403, and deliberately without a challenge. A harness test pinned the old behaviour and was updated with the reason. (user: dmestas, size: 8434) [annotate] [blame] [check-ins using]
package main import ( "encoding/json" "flag" "fmt" "net/http" "net/url" "os" "strings" "time" ) // repoCmd is the client-side verb for managing repositories. Unlike // `account` and `user`, which open the forge database directly, this // talks to a RUNNING server over the API โ because registering a // repository is not a config edit, it is a live operation: the server // opens an index, starts a pipeline and begins serving. Two processes // writing that registry would disagree about which repositories exist. // // It is therefore also the first proof that the API is sufficient: the // CLI has no privileged path, so anything it can do, any client can. func repoCmd(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: quarry repo new|clone|list|remove [flags]") } verb, rest := args[0], args[1:] fs := flag.NewFlagSet("repo "+verb, flag.ContinueOnError) server := fs.String("server", envOr("QUARRY_SERVER", "http://127.0.0.1:8080"), "quarry base URL") user := fs.String("user", envOr("QUARRY_USER", ""), "admin account") password := fs.String("password", "", "password (or QUARRY_PASSWORD)") name := fs.String("name", "", "repository name") path := fs.String("path", "", "store directory (default: the server's repositories root)") desc := fs.String("description", "", "one-line summary") from := fs.String("from", "", "clone this URL") every := fs.Int("every", 0, "mirror interval in seconds (clone only; 0 = server default)") noMirror := fs.Bool("no-mirror", false, "do not record a mirror when cloning") if err := fs.Parse(rest); err != nil { return err } if r := fs.Args(); len(r) > 0 { return fmt.Errorf("unexpected argument %q", r[0]) } pw := *password if pw == "" { pw = os.Getenv("QUARRY_PASSWORD") } c := &apiClient{base: strings.TrimRight(*server, "/"), user: *user, pass: pw} switch verb { case "list": var out struct { Repos []struct { Name, Description string Checkins int } `json:"repos"` } if err := c.call("GET", "/api/v1/repos", nil, &out); err != nil { return err } for _, r := range out.Repos { fmt.Printf("%-24s %6d %s\n", r.Name, r.Checkins, r.Description) } return nil case "new", "clone": if *name == "" { return fmt.Errorf("repo %s wants --name", verb) } if verb == "clone" && *from == "" { return fmt.Errorf("repo clone wants --from URL") } body := map[string]any{ "name": *name, "path": *path, "description": *desc, "create": verb == "new", "clone": *from, "noMirror": *noMirror, "mirrorEvery": *every, } var out struct { Name, Path string Mirrored bool } if err := c.call("POST", "/api/v1/repos", body, &out); err != nil { return err } fmt.Printf("%s created at %s\n", out.Name, out.Path) if out.Mirrored { fmt.Printf("mirroring %s\n", *from) } fmt.Printf("\n zit clone %s/r/%s %s\n\n", c.base, out.Name, out.Name) return nil case "remove": if *name == "" { return fmt.Errorf("repo remove wants --name") } if err := c.call("POST", "/api/v1/repos/remove", map[string]any{"name": *name}, nil); err != nil { return err } fmt.Printf("%s is no longer served (its store on disk is untouched)\n", *name) return nil } return fmt.Errorf("unknown verb %q: want new|clone|list|remove", verb) } // accountAPICmd manages accounts through the API rather than the // database. `quarry account` still opens OLTP directly โ it has to, so // the FIRST account can be created before any server is running or any // admin exists to authenticate as. Past that, the API is the surface, // and driving it from here is what keeps it honest. func accountAPICmd(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: quarry accounts list|add|passwd|grant|revoke|delete [flags]") } verb, rest := args[0], args[1:] fs := flag.NewFlagSet("accounts "+verb, flag.ContinueOnError) server := fs.String("server", envOr("QUARRY_SERVER", "http://127.0.0.1:8080"), "quarry base URL") as := fs.String("as", envOr("QUARRY_USER", ""), "admin account to act as") asPassword := fs.String("as-password", "", "that account's password (or QUARRY_PASSWORD)") name := fs.String("user", "", "the account to operate on") password := fs.String("password", "", "new password (or QUARRY_NEW_PASSWORD)") admin := fs.Bool("admin", false, "make a site admin") repo := fs.String("repo", "", "repository, for grant and revoke") role := fs.String("role", "read", "read|write|admin") if err := fs.Parse(rest); err != nil { return err } if r := fs.Args(); len(r) > 0 { return fmt.Errorf("unexpected argument %q", r[0]) } c := &apiClient{base: strings.TrimRight(*server, "/"), user: *as, pass: firstNonEmpty(*asPassword, os.Getenv("QUARRY_PASSWORD"))} switch verb { case "list": var out struct { Accounts []struct { Name string Admin bool Created string Grants []struct{ Repo, Role string } } } if err := c.call("GET", "/api/v1/accounts", nil, &out); err != nil { return err } for _, a := range out.Accounts { kind := "" if a.Admin { kind = " (site admin)" } fmt.Printf("%-24s%s\n", a.Name, kind) for _, g := range a.Grants { fmt.Printf(" %-20s %s\n", g.Repo, g.Role) } } return nil case "add", "passwd": if *name == "" { return fmt.Errorf("accounts %s wants --user", verb) } pw := firstNonEmpty(*password, os.Getenv("QUARRY_NEW_PASSWORD")) if pw == "" { return fmt.Errorf("no password: use --password or QUARRY_NEW_PASSWORD") } path, body := "/api/v1/accounts", map[string]any{"name": *name, "password": pw, "admin": *admin} if verb == "passwd" { path = "/api/v1/accounts/passwd" body = map[string]any{"name": *name, "password": pw} } if err := c.call("POST", path, body, nil); err != nil { return err } fmt.Printf("%s: password set\n", *name) return nil case "grant", "revoke": if *name == "" || *repo == "" { return fmt.Errorf("accounts %s wants --user and --repo", verb) } body := map[string]any{"name": *name, "repo": *repo} if verb == "grant" { body["role"] = *role } if err := c.call("POST", "/api/v1/accounts/"+verb, body, nil); err != nil { return err } if verb == "grant" { fmt.Printf("%s: %s on %s\n", *name, *role, *repo) } else { fmt.Printf("%s: no access to %s\n", *name, *repo) } return nil case "delete": if *name == "" { return fmt.Errorf("accounts delete wants --user") } if err := c.call("POST", "/api/v1/accounts/delete", map[string]any{"name": *name}, nil); err != nil { return err } fmt.Printf("%s deleted\n", *name) return nil } return fmt.Errorf("unknown verb %q: want list|add|passwd|grant|revoke|delete", verb) } func firstNonEmpty(vals ...string) string { for _, v := range vals { if v != "" { return v } } return "" } func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // apiClient is the smallest thing that can drive quarry's API with an // admin credential. Basic auth because that is what the API already // takes (0001 ยง7: one credential per user). type apiClient struct { base, user, pass string } func (c *apiClient) call(method, path string, body any, out any) error { var rdr *strings.Reader if body != nil { raw, err := json.Marshal(body) if err != nil { return err } rdr = strings.NewReader(string(raw)) } var req *http.Request var err error if rdr != nil { req, err = http.NewRequest(method, c.base+path, rdr) } else { req, err = http.NewRequest(method, c.base+path, nil) } if err != nil { return err } req.Header.Set("Content-Type", "application/json") if c.user != "" { req.SetBasicAuth(c.user, c.pass) } // Generous: a clone of a large repository is minutes, and the server // holds the request open for it. resp, err := (&http.Client{Timeout: 30 * time.Minute}).Do(req) if err != nil { return fmt.Errorf("%s: %w", urlOf(c.base, path), err) } defer resp.Body.Close() if resp.StatusCode >= 400 { var e struct { Error string `json:"error"` } json.NewDecoder(resp.Body).Decode(&e) if e.Error == "" { e.Error = resp.Status } return fmt.Errorf("%s", e.Error) } if out == nil { return nil } return json.NewDecoder(resp.Body).Decode(out) } func urlOf(base, path string) string { u, err := url.Parse(base + path) if err != nil { return base + path } return u.String() }