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/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.

Identify accounts by their registered name

62576fa8157d431dcf2d360f3ebe481de77bda03 · Keenan Nemetz · 14d ago · parent 642e5994

Identify accounts by their registered name

Drop the internal account id and display name. Users, keys, passkeys,
invites, sessions, ACL entries, approvals, locks and CI records now all
use the registered name as the single identity.

The roster migrates in place: old id-keyed tables are rebuilt around the
name, and access-rule/lock references are rewritten through the same
mapping. Display-name editing and whoami --id are removed.

server/ssh/admin.go +50−85

@@ -1,7 +1,7 @@
1 1 package ssh
2 2
3 3 // Remote administration over SSH. The command grammar is fixed; nothing here
4 // goes through a shell. Authorization is the account id from the SSH handshake,
4 // goes through a shell. Authorization is the account name from the SSH handshake,
5 5 // and every successful or rejected command is recorded with that identity.
6 6
7 7 import (
@@ -41,38 +41,38 @@ func (e *adminUsageError) Error() string { return e.msg }
41 41 type adminRender func(stdout, stderr io.Writer)
42 42
43 43 // serveAdmin runs one `vole-admin` command and returns the process exit code.
44 func (s *Server) serveAdmin(ch xssh.Channel, argv []string, user, accountID, remote string) int {
44 func (s *Server) serveAdmin(ch xssh.Channel, argv []string, user, remote string) int {
45 45 jsonOut, args, err := splitAdminJSON(argv)
46 46 if err != nil {
47 return s.adminError(ch, jsonOut, "usage", err, user, accountID, remote)
47 return s.adminError(ch, jsonOut, "usage", err, user, remote)
48 48 }
49 if !s.cfg.Identity.IsAdmin(accountID) {
49 if !s.cfg.Identity.IsAdmin(user) {
50 50 return s.adminError(ch, jsonOut, "permission_denied",
51 fmt.Errorf("instance admin required for %s", user), user, accountID, remote)
51 fmt.Errorf("instance admin required for %s", user), user, remote)
52 52 }
53 53 if s.cfg.Instance == nil {
54 54 return s.adminError(ch, jsonOut, "unavailable",
55 errors.New("admin API is not wired into this server"), user, accountID, remote)
55 errors.New("admin API is not wired into this server"), user, remote)
56 56 }
57 57 if len(args) < 2 {
58 58 return s.adminError(ch, jsonOut, "usage", &adminUsageError{msg: "usage: vole-admin <resource> <action> [args]"},
59 user, accountID, remote)
59 user, remote)
60 60 }
61 61
62 62 data, render, err := s.runAdmin(args, user, ch)
63 63 if err != nil {
64 return s.adminError(ch, jsonOut, "failed", err, user, accountID, remote)
64 return s.adminError(ch, jsonOut, "failed", err, user, remote)
65 65 }
66 66 if jsonOut {
67 67 _ = json.NewEncoder(ch).Encode(adminResponse{OK: true, Data: data})
68 68 } else if render != nil {
69 69 render(ch, ch.Stderr())
70 70 }
71 s.auditAdmin(user, accountID, remote, strings.Join(args, " ")+" ok")
71 s.auditAdmin(user, remote, strings.Join(args, " ")+" ok")
72 72 return 0
73 73 }
74 74
75 func (s *Server) adminError(ch xssh.Channel, jsonOut bool, code string, err error, user, accountID, remote string) int {
75 func (s *Server) adminError(ch xssh.Channel, jsonOut bool, code string, err error, user, remote string) int {
76 76 exitCode := 1
77 77 if _, ok := err.(*adminUsageError); ok {
78 78 exitCode = 129
@@ -82,7 +82,7 @@ func (s *Server) adminError(ch xssh.Channel, jsonOut bool, code string, err erro
82 82 } else {
83 83 fmt.Fprintf(ch.Stderr(), "vole: %v\n", err)
84 84 }
85 s.auditAdmin(user, accountID, remote, "failed: "+err.Error())
85 s.auditAdmin(user, remote, "failed: "+err.Error())
86 86 return exitCode
87 87 }
88 88
@@ -136,11 +136,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
136 136 if a.Admin {
137 137 role = " admin"
138 138 }
139 display := a.Display
140 if display == "" {
141 display = "-"
142 }
143 fmt.Fprintf(out, "%-16s %s %-24s %-8s%s keys=%d\n", a.Name, a.ID, display, a.Status, role, len(a.Keys))
139 fmt.Fprintf(out, "%-32s %-8s%s keys=%d\n", a.Name, a.Status, role, len(a.Keys))
144 140 for _, k := range a.Keys {
145 141 fmt.Fprintf(out, " %s %s %s\n", k.Fingerprint, k.Type, k.Comment)
146 142 }
@@ -149,41 +145,36 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
149 145
150 146 case "show":
151 147 if len(args) != 1 {
152 return nil, nil, &adminUsageError{msg: "usage: vole-admin user show <name|id>"}
148 return nil, nil, &adminUsageError{msg: "usage: vole-admin user show <name>"}
153 149 }
154 150 a, err := s.cfg.Identity.Find(args[0])
155 151 if err != nil {
156 152 return nil, nil, err
157 153 }
158 154 return userData(a), func(out, _ io.Writer) {
159 display := a.Display
160 if display == "" {
161 display = "-"
162 }
163 fmt.Fprintf(out, "name: %s\ndisplay: %s\nid: %s\nstatus: %s\nadmin: %t\ncreated: %s\n",
164 a.Name, display, a.ID, a.Status, a.Admin, a.Created.Format(time.RFC3339))
155 fmt.Fprintf(out, "name: %s\nstatus: %s\nadmin: %t\ncreated: %s\n",
156 a.Name, a.Status, a.Admin, a.Created.Format(time.RFC3339))
165 157 for _, k := range a.Keys {
166 158 fmt.Fprintf(out, "key: %s %s %s\n", k.Fingerprint, k.Type, k.Comment)
167 159 }
168 160 }, nil
169 161
170 162 case "create":
171 f, err := parseAdminFlags(args, adminFlagSpec{long: "no-key", boolean: true}, adminFlagSpec{long: "display"})
163 f, err := parseAdminFlags(args, adminFlagSpec{long: "no-key", boolean: true})
172 164 if err != nil {
173 165 return nil, nil, err
174 166 }
175 167 if len(f.rest) != 1 {
176 return nil, nil, &adminUsageError{msg: "usage: vole-admin user create [--no-key] [--display <label>] <name> (public key on stdin unless --no-key)"}
168 return nil, nil, &adminUsageError{msg: "usage: vole-admin user create [--no-key] <name> (public key on stdin unless --no-key)"}
177 169 }
178 display := f.one("display")
179 170 if f.has("no-key") {
180 a, inv, err := s.cfg.Identity.CreateWithInvite(f.rest[0], display, actor, 0)
171 a, inv, err := s.cfg.Identity.CreateWithInvite(f.rest[0], actor, 0)
181 172 if err != nil {
182 173 return nil, nil, err
183 174 }
184 175 data := map[string]any{"account": userData(a), "invite": s.inviteData(inv)}
185 176 return data, func(out, errOut io.Writer) {
186 fmt.Fprintf(out, "%s\t%s\n", a.Name, a.ID)
177 fmt.Fprintln(out, a.Name)
187 178 s.renderInvite(inv, out)
188 179 }, nil
189 180 }
@@ -191,12 +182,12 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
191 182 if err != nil {
192 183 return nil, nil, err
193 184 }
194 a, err := s.cfg.Identity.Create(f.rest[0], display, key, actor)
185 a, err := s.cfg.Identity.Create(f.rest[0], key, actor)
195 186 if err != nil {
196 187 return nil, nil, err
197 188 }
198 189 return userData(a), func(out, errOut io.Writer) {
199 fmt.Fprintf(out, "%s\t%s\n", a.Name, a.ID)
190 fmt.Fprintln(out, a.Name)
200 191 if len(a.Keys) > 0 {
201 192 fmt.Fprintf(errOut, "key %s\n", a.Keys[0].Fingerprint)
202 193 }
@@ -204,7 +195,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
204 195
205 196 case "invite":
206 197 if len(args) != 1 {
207 return nil, nil, &adminUsageError{msg: "usage: vole-admin user invite <name|id>"}
198 return nil, nil, &adminUsageError{msg: "usage: vole-admin user invite <name>"}
208 199 }
209 200 inv, err := s.cfg.Identity.Invite(args[0], actor, 0)
210 201 if err != nil {
@@ -216,7 +207,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
216 207
217 208 case "credentials":
218 209 if len(args) != 1 {
219 return nil, nil, &adminUsageError{msg: "usage: vole-admin user credentials <name|id>"}
210 return nil, nil, &adminUsageError{msg: "usage: vole-admin user credentials <name>"}
220 211 }
221 212 creds, err := s.cfg.Identity.Credentials(args[0])
222 213 if err != nil {
@@ -245,7 +236,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
245 236
246 237 case "rm-credential":
247 238 if len(args) != 2 {
248 return nil, nil, &adminUsageError{msg: "usage: vole-admin user rm-credential <name|id> <credential-id>"}
239 return nil, nil, &adminUsageError{msg: "usage: vole-admin user rm-credential <name> <credential-id>"}
249 240 }
250 241 id, err := identity.ParseCredentialID(args[1])
251 242 if err != nil {
@@ -260,7 +251,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
260 251
261 252 case "sessions":
262 253 if len(args) != 1 {
263 return nil, nil, &adminUsageError{msg: "usage: vole-admin user sessions <name|id>"}
254 return nil, nil, &adminUsageError{msg: "usage: vole-admin user sessions <name>"}
264 255 }
265 256 a, err := s.cfg.Identity.Find(args[0])
266 257 if err != nil {
@@ -269,7 +260,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
269 260 if s.cfg.Instance == nil || s.cfg.Instance.Sessions == nil {
270 261 return nil, nil, errors.New("session store is not wired into this server")
271 262 }
272 list, err := s.cfg.Instance.Sessions.List(a.ID)
263 list, err := s.cfg.Instance.Sessions.List(a.Name)
273 264 if err != nil {
274 265 return nil, nil, err
275 266 }
@@ -295,7 +286,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
295 286 }
296 287 if f.has("all") {
297 288 if len(f.rest) != 0 {
298 return nil, nil, &adminUsageError{msg: "usage: vole-admin user revoke-sessions [--all | <name|id>]"}
289 return nil, nil, &adminUsageError{msg: "usage: vole-admin user revoke-sessions [--all | <name>]"}
299 290 }
300 291 n, err := s.cfg.Instance.Sessions.RevokeAll()
301 292 if err != nil {
@@ -306,13 +297,13 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
306 297 }, nil
307 298 }
308 299 if len(f.rest) != 1 {
309 return nil, nil, &adminUsageError{msg: "usage: vole-admin user revoke-sessions [--all | <name|id>]"}
300 return nil, nil, &adminUsageError{msg: "usage: vole-admin user revoke-sessions [--all | <name>]"}
310 301 }
311 302 a, err := s.cfg.Identity.Find(f.rest[0])
312 303 if err != nil {
313 304 return nil, nil, err
314 305 }
315 n, err := s.cfg.Instance.Sessions.RevokeAccount(a.ID)
306 n, err := s.cfg.Instance.Sessions.RevokeAccount(a.Name)
316 307 if err != nil {
317 308 return nil, nil, err
318 309 }
@@ -322,7 +313,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
322 313
323 314 case "add-key":
324 315 if len(args) != 1 {
325 return nil, nil, &adminUsageError{msg: "usage: vole-admin user add-key <name|id>"}
316 return nil, nil, &adminUsageError{msg: "usage: vole-admin user add-key <name>"}
326 317 }
327 318 key, err := readAdminKey(in)
328 319 if err != nil {
@@ -340,7 +331,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
340 331
341 332 case "rm-key":
342 333 if len(args) != 2 {
343 return nil, nil, &adminUsageError{msg: "usage: vole-admin user rm-key <name|id> <fingerprint>"}
334 return nil, nil, &adminUsageError{msg: "usage: vole-admin user rm-key <name> <fingerprint>"}
344 335 }
345 336 if err := s.cfg.Identity.RemoveKey(args[0], args[1], actor); err != nil {
346 337 return nil, nil, err
@@ -349,35 +340,19 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
349 340 fmt.Fprintf(out, "revoked %s\n", args[1])
350 341 }, nil
351 342
352 case "rename":
353 return nil, nil, &adminUsageError{msg: "account names are permanent; use vole-admin user display <name|id> <display-name>"}
354
355 case "display":
356 if len(args) != 2 {
357 return nil, nil, &adminUsageError{msg: "usage: vole-admin user display <name|id> <display-name>"}
358 }
359 a, err := s.cfg.Identity.SetDisplay(args[0], args[1], actor)
360 if err != nil {
361 return nil, nil, err
362 }
363 return userData(a), func(out, _ io.Writer) {
364 if a.Display == "" {
365 fmt.Fprintf(out, "cleared the display name for %s; showing %s\n", a.Name, a.Name)
366 } else {
367 fmt.Fprintf(out, "%s now shows as %s\n", a.Name, a.Display)
368 }
369 }, nil
343 case "rename", "display":
344 return nil, nil, &adminUsageError{msg: "account names are permanent and are the only identifier an account has"}
370 345
371 346 case "retire", "rm", "delete":
372 347 if len(args) != 1 {
373 return nil, nil, &adminUsageError{msg: "usage: vole-admin user retire <name|id>"}
348 return nil, nil, &adminUsageError{msg: "usage: vole-admin user retire <name>"}
374 349 }
375 350 a, err := s.cfg.Identity.Retire(args[0], actor)
376 351 if err != nil {
377 352 return nil, nil, err
378 353 }
379 354 return userData(a), func(out, errOut io.Writer) {
380 fmt.Fprintf(out, "retired %s\t%s\n", a.Name, a.ID)
355 fmt.Fprintln(out, "retired "+a.Name)
381 356 fmt.Fprintln(errOut, "keys stop working within 5s; the name is never reissued")
382 357 }, nil
383 358
@@ -386,7 +361,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
386 361
387 362 case "grant-admin", "revoke-admin":
388 363 if len(args) != 1 {
389 return nil, nil, &adminUsageError{msg: "usage: vole-admin user " + action + " <name|id>"}
364 return nil, nil, &adminUsageError{msg: "usage: vole-admin user " + action + " <name>"}
390 365 }
391 366 a, err := s.cfg.Identity.SetAdmin(args[0], action == "grant-admin", actor)
392 367 if err != nil {
@@ -394,9 +369,9 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea
394 369 }
395 370 return userData(a), func(out, _ io.Writer) {
396 371 if action == "grant-admin" {
397 fmt.Fprintf(out, "granted instance admin to %s\t%s\n", a.Name, a.ID)
372 fmt.Fprintln(out, "granted instance admin to "+a.Name)
398 373 } else {
399 fmt.Fprintf(out, "revoked instance admin from %s\t%s\n", a.Name, a.ID)
374 fmt.Fprintln(out, "revoked instance admin from "+a.Name)
400 375 }
401 376 }, nil
402 377 }
@@ -471,7 +446,7 @@ func (s *Server) adminRepo(action string, args []string, actor string) (any, adm
471 446 if !owner.Active() {
472 447 return nil, nil, fmt.Errorf("owner %q is retired", owner.Name)
473 448 }
474 m, err := s.cfg.Instance.Repos.Create(f.rest[0], owner.ID, f.one("description"))
449 m, err := s.cfg.Instance.Repos.Create(f.rest[0], owner.Name, f.one("description"))
475 450 if err != nil {
476 451 return nil, nil, err
477 452 }
@@ -554,7 +529,7 @@ func (s *Server) adminACL(action string, args []string, actor string) (any, admi
554 529 if err != nil {
555 530 return nil, nil, err
556 531 }
557 if err := a.SetEntry(account.ID, role.String()); err != nil {
532 if err := a.SetEntry(account.Name, role.String()); err != nil {
558 533 return nil, nil, err
559 534 }
560 535 return finish()
@@ -631,7 +606,7 @@ func (s *Server) adminACL(action string, args []string, actor string) (any, admi
631 606 if err != nil {
632 607 return "", err
633 608 }
634 return account.ID, nil
609 return account.Name, nil
635 610 }
636 611 switch {
637 612 case f.has("clear"):
@@ -700,20 +675,12 @@ func (s *Server) aclResult(m *repo.Meta, a *access.ACL) (any, adminRender, error
700 675 entries := make([]map[string]string, 0, len(a.Entries))
701 676 for _, principal := range a.Principals() {
702 677 entries = append(entries, map[string]string{
703 "id": principal,
704 "name": s.cfg.Identity.Name(principal),
705 "display": s.cfg.Identity.Label(principal),
706 "role": a.Entries[principal],
707 })
708 }
709 runners := make([]map[string]string, 0, len(a.Runners))
710 for _, runner := range a.Runners {
711 runners = append(runners, map[string]string{
712 "id": runner,
713 "name": s.cfg.Identity.Name(runner),
714 "display": s.cfg.Identity.Label(runner),
678 "name": principal,
679 "role": a.Entries[principal],
715 680 })
716 681 }
682 runners := make([]string, 0, len(a.Runners))
683 runners = append(runners, a.Runners...)
717 684 data := map[string]any{
718 685 "repo": m.Name,
719 686 "public": a.Can("", access.Read),
@@ -732,7 +699,7 @@ func (s *Server) aclResult(m *repo.Meta, a *access.ACL) (any, adminRender, error
732 699 fmt.Fprintf(out, "repo: %s (%s)\n", m.Name, vis)
733 700 fmt.Fprintf(out, "default: %s\n", a.Default)
734 701 for _, e := range entries {
735 fmt.Fprintf(out, " %-16s %-28s %s\n", e["display"], e["id"], e["role"])
702 fmt.Fprintf(out, " %-32s %s\n", e["name"], e["role"])
736 703 }
737 704 if len(a.Protected) > 0 {
738 705 fmt.Fprintf(out, "protected: %s\n", strings.Join(a.Protected, ", "))
@@ -748,7 +715,7 @@ func (s *Server) aclResult(m *repo.Meta, a *access.ACL) (any, adminRender, error
748 715 return
749 716 }
750 717 for _, r := range runners {
751 fmt.Fprintf(out, "runner %-28s %s\n", r["id"], r["display"])
718 fmt.Fprintf(out, "runner %s\n", r)
752 719 }
753 720 fmt.Fprintln(out, "(admins may also report CI results)")
754 721 }, nil
@@ -790,9 +757,7 @@ func userData(a *identity.Account) map[string]any {
790 757 return nil
791 758 }
792 759 return map[string]any{
793 "id": a.ID,
794 760 "name": a.Name,
795 "display": a.Display,
796 761 "status": a.Status,
797 762 "admin": a.Admin,
798 763 "keys": a.Keys,
@@ -800,7 +765,7 @@ func userData(a *identity.Account) map[string]any {
800 765 }
801 766 }
802 767
803 func (s *Server) auditAdmin(user, accountID, remote, detail string) {
768 func (s *Server) auditAdmin(user, remote, detail string) {
804 769 if s.cfg.AuditPath == "" {
805 770 return
806 771 }
@@ -811,8 +776,8 @@ func (s *Server) auditAdmin(user, accountID, remote, detail string) {
811 776 return
812 777 }
813 778 defer f.Close()
814 fmt.Fprintf(f, "%s\tadmin\t%s\t%s\t%s\t%s\n",
815 time.Now().UTC().Format(time.RFC3339), user, accountID, remote, detail)
779 fmt.Fprintf(f, "%s\tadmin\t%s\t%s\t%s\n",
780 time.Now().UTC().Format(time.RFC3339), user, remote, detail)
816 781 }
817 782
818 783 // adminFlagSpec and parseAdminFlags are a small local flag parser for the