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.
Unify account terminology and clarify ACL commands
2e024cc96a391c0959ad2914caeed78b600fcfc1 · Keenan Nemetz · 14d ago · parent ab326d3a
Unify account terminology and clarify ACL commands Use account consistently in the CLI, help, docs, and remote admin messages. Rename the CLI account command from user to account while retaining the old user spelling as an alias. Replace positional ACL mutations with explicit verbs such as set-role, protect add, runners remove, and checks enable. Update local and remote admin parsing, hints, docs, and tests. Tidy repository filter alignment and remove duplicated issue/change counts from page subtitles.
server/ssh/admin.go +209−115
| @@ -108,8 +108,9 @@ func splitAdminJSON(argv []string) (jsonOut bool, rest []string, err error) { | |||
| 108 | 108 | func (s *Server) runAdmin(args []string, actor string, in io.Reader) (any, adminRender, error) { | |
| 109 | 109 | resource, action, rest := args[0], args[1], args[2:] | |
| 110 | 110 | switch resource { | |
| 111 | case "user": | ||
| 112 | return s.adminUser(action, rest, actor, in) | ||
| 111 | case "account", "user": | ||
| 112 | // "user" is the historical spelling of this resource. | ||
| 113 | return s.adminAccount(action, rest, actor, in) | ||
| 113 | 114 | case "repo": | |
| 114 | 115 | return s.adminRepo(action, rest, actor) | |
| 115 | 116 | case "acl": | |
| @@ -118,17 +119,17 @@ func (s *Server) runAdmin(args []string, actor string, in io.Reader) (any, admin | |||
| 118 | 119 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("unknown admin resource %q", resource)} | |
| 119 | 120 | } | |
| 120 | 121 | ||
| 121 | func (s *Server) adminUser(action string, args []string, actor string, in io.Reader) (any, adminRender, error) { | ||
| 122 | func (s *Server) adminAccount(action string, args []string, actor string, in io.Reader) (any, adminRender, error) { | ||
| 122 | 123 | switch action { | |
| 123 | 124 | case "list": | |
| 124 | 125 | all := s.cfg.Identity.All() | |
| 125 | 126 | data := make([]map[string]any, 0, len(all)) | |
| 126 | 127 | for _, a := range all { | |
| 127 | data = append(data, userData(a)) | ||
| 128 | data = append(data, accountData(a)) | ||
| 128 | 129 | } | |
| 129 | 130 | return data, func(out, _ io.Writer) { | |
| 130 | 131 | if len(all) == 0 { | |
| 131 | fmt.Fprintln(out, "no users") | ||
| 132 | fmt.Fprintln(out, "no accounts") | ||
| 132 | 133 | return | |
| 133 | 134 | } | |
| 134 | 135 | for _, a := range all { | |
| @@ -141,13 +142,13 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 141 | 142 | ||
| 142 | 143 | case "show": | |
| 143 | 144 | if len(args) != 1 { | |
| 144 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user show <name>"} | ||
| 145 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account show <name>"} | ||
| 145 | 146 | } | |
| 146 | 147 | a, err := s.cfg.Identity.Find(args[0]) | |
| 147 | 148 | if err != nil { | |
| 148 | 149 | return nil, nil, err | |
| 149 | 150 | } | |
| 150 | return userData(a), func(out, _ io.Writer) { | ||
| 151 | return accountData(a), func(out, _ io.Writer) { | ||
| 151 | 152 | fmt.Fprintf(out, "name: %s\nstatus: %s\nrole: %s\nadmin: %t\ncreated: %s\n", | |
| 152 | 153 | a.Name, a.Status, a.Role, a.Admin, a.Created.Format(time.RFC3339)) | |
| 153 | 154 | for _, k := range a.Keys { | |
| @@ -163,7 +164,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 163 | 164 | return nil, nil, err | |
| 164 | 165 | } | |
| 165 | 166 | if len(f.rest) != 1 { | |
| 166 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user create [--no-key] [--role <role>] <name> (public key on stdin unless --no-key)"} | ||
| 167 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account create [--no-key] [--role <role>] <name> (public key on stdin unless --no-key)"} | ||
| 167 | 168 | } | |
| 168 | 169 | role, err := access.ParseRole(f.one("role")) | |
| 169 | 170 | if err != nil { | |
| @@ -174,7 +175,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 174 | 175 | if err != nil { | |
| 175 | 176 | return nil, nil, err | |
| 176 | 177 | } | |
| 177 | data := map[string]any{"account": userData(a), "invite": s.inviteData(inv)} | ||
| 178 | data := map[string]any{"account": accountData(a), "invite": s.inviteData(inv)} | ||
| 178 | 179 | return data, func(out, errOut io.Writer) { | |
| 179 | 180 | fmt.Fprintln(out, a.Name) | |
| 180 | 181 | s.renderInvite(inv, out) | |
| @@ -188,7 +189,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 188 | 189 | if err != nil { | |
| 189 | 190 | return nil, nil, err | |
| 190 | 191 | } | |
| 191 | return userData(a), func(out, errOut io.Writer) { | ||
| 192 | return accountData(a), func(out, errOut io.Writer) { | ||
| 192 | 193 | fmt.Fprintln(out, a.Name) | |
| 193 | 194 | if len(a.Keys) > 0 { | |
| 194 | 195 | fmt.Fprintf(errOut, "key %s\n", a.Keys[0].Fingerprint) | |
| @@ -197,7 +198,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 197 | 198 | ||
| 198 | 199 | case "role": | |
| 199 | 200 | if len(args) != 2 { | |
| 200 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user role <name> <none|read|contribute|maintain|admin>"} | ||
| 201 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account role <name> <none|read|contribute|maintain|admin>"} | ||
| 201 | 202 | } | |
| 202 | 203 | role, err := access.ParseRole(args[1]) | |
| 203 | 204 | if err != nil { | |
| @@ -207,13 +208,13 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 207 | 208 | if err != nil { | |
| 208 | 209 | return nil, nil, err | |
| 209 | 210 | } | |
| 210 | return userData(a), func(out, _ io.Writer) { | ||
| 211 | return accountData(a), func(out, _ io.Writer) { | ||
| 211 | 212 | fmt.Fprintf(out, "%s: %s\n", a.Name, a.Role) | |
| 212 | 213 | }, nil | |
| 213 | 214 | ||
| 214 | 215 | case "invite": | |
| 215 | 216 | if len(args) != 1 { | |
| 216 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user invite <name>"} | ||
| 217 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account invite <name>"} | ||
| 217 | 218 | } | |
| 218 | 219 | inv, err := s.cfg.Identity.Invite(args[0], actor, 0) | |
| 219 | 220 | if err != nil { | |
| @@ -225,7 +226,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 225 | 226 | ||
| 226 | 227 | case "credentials": | |
| 227 | 228 | if len(args) != 1 { | |
| 228 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user credentials <name>"} | ||
| 229 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account credentials <name>"} | ||
| 229 | 230 | } | |
| 230 | 231 | creds, err := s.cfg.Identity.Credentials(args[0]) | |
| 231 | 232 | if err != nil { | |
| @@ -254,7 +255,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 254 | 255 | ||
| 255 | 256 | case "rm-credential": | |
| 256 | 257 | if len(args) != 2 { | |
| 257 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user rm-credential <name> <credential-id>"} | ||
| 258 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account rm-credential <name> <credential-id>"} | ||
| 258 | 259 | } | |
| 259 | 260 | id, err := identity.ParseCredentialID(args[1]) | |
| 260 | 261 | if err != nil { | |
| @@ -269,7 +270,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 269 | 270 | ||
| 270 | 271 | case "sessions": | |
| 271 | 272 | if len(args) != 1 { | |
| 272 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user sessions <name>"} | ||
| 273 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account sessions <name>"} | ||
| 273 | 274 | } | |
| 274 | 275 | a, err := s.cfg.Identity.Find(args[0]) | |
| 275 | 276 | if err != nil { | |
| @@ -304,7 +305,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 304 | 305 | } | |
| 305 | 306 | if f.has("all") { | |
| 306 | 307 | if len(f.rest) != 0 { | |
| 307 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user revoke-sessions [--all | <name>]"} | ||
| 308 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account revoke-sessions [--all | <name>]"} | ||
| 308 | 309 | } | |
| 309 | 310 | n, err := s.cfg.Instance.Sessions.RevokeAll() | |
| 310 | 311 | if err != nil { | |
| @@ -315,7 +316,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 315 | 316 | }, nil | |
| 316 | 317 | } | |
| 317 | 318 | if len(f.rest) != 1 { | |
| 318 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user revoke-sessions [--all | <name>]"} | ||
| 319 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account revoke-sessions [--all | <name>]"} | ||
| 319 | 320 | } | |
| 320 | 321 | a, err := s.cfg.Identity.Find(f.rest[0]) | |
| 321 | 322 | if err != nil { | |
| @@ -325,13 +326,13 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 325 | 326 | if err != nil { | |
| 326 | 327 | return nil, nil, err | |
| 327 | 328 | } | |
| 328 | return map[string]any{"revoked": n, "account": userData(a)}, func(out, _ io.Writer) { | ||
| 329 | return map[string]any{"revoked": n, "account": accountData(a)}, func(out, _ io.Writer) { | ||
| 329 | 330 | fmt.Fprintf(out, "revoked %d session(s) for %s\n", n, a.Name) | |
| 330 | 331 | }, nil | |
| 331 | 332 | ||
| 332 | 333 | case "add-key": | |
| 333 | 334 | if len(args) != 1 { | |
| 334 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user add-key <name>"} | ||
| 335 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account add-key <name>"} | ||
| 335 | 336 | } | |
| 336 | 337 | key, err := readAdminKey(in) | |
| 337 | 338 | if err != nil { | |
| @@ -342,14 +343,14 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 342 | 343 | return nil, nil, err | |
| 343 | 344 | } | |
| 344 | 345 | a, _ := s.cfg.Identity.Find(args[0]) | |
| 345 | data := map[string]any{"fingerprint": fp, "account": userData(a)} | ||
| 346 | data := map[string]any{"fingerprint": fp, "account": accountData(a)} | ||
| 346 | 347 | return data, func(out, _ io.Writer) { | |
| 347 | 348 | fmt.Fprintf(out, "added key %s for %s\n", fp, a.Name) | |
| 348 | 349 | }, nil | |
| 349 | 350 | ||
| 350 | 351 | case "rm-key": | |
| 351 | 352 | if len(args) != 2 { | |
| 352 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user rm-key <name> <fingerprint>"} | ||
| 353 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account rm-key <name> <fingerprint>"} | ||
| 353 | 354 | } | |
| 354 | 355 | if err := s.cfg.Identity.RemoveKey(args[0], args[1], actor); err != nil { | |
| 355 | 356 | return nil, nil, err | |
| @@ -363,29 +364,29 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 363 | 364 | ||
| 364 | 365 | case "retire", "rm", "delete": | |
| 365 | 366 | if len(args) != 1 { | |
| 366 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user retire <name>"} | ||
| 367 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account retire <name>"} | ||
| 367 | 368 | } | |
| 368 | 369 | a, err := s.cfg.Identity.Retire(args[0], actor) | |
| 369 | 370 | if err != nil { | |
| 370 | 371 | return nil, nil, err | |
| 371 | 372 | } | |
| 372 | return userData(a), func(out, errOut io.Writer) { | ||
| 373 | return accountData(a), func(out, errOut io.Writer) { | ||
| 373 | 374 | fmt.Fprintln(out, "retired "+a.Name) | |
| 374 | 375 | fmt.Fprintln(errOut, "keys stop working within 5s; the name is never reissued") | |
| 375 | 376 | }, nil | |
| 376 | 377 | ||
| 377 | 378 | case "backup": | |
| 378 | return nil, nil, &adminUsageError{msg: "user backup writes to the server's filesystem; run it locally"} | ||
| 379 | return nil, nil, &adminUsageError{msg: "account backup writes to the server's filesystem; run it locally"} | ||
| 379 | 380 | ||
| 380 | 381 | case "grant-admin", "revoke-admin": | |
| 381 | 382 | if len(args) != 1 { | |
| 382 | return nil, nil, &adminUsageError{msg: "usage: vole-admin user " + action + " <name>"} | ||
| 383 | return nil, nil, &adminUsageError{msg: "usage: vole-admin account " + action + " <name>"} | ||
| 383 | 384 | } | |
| 384 | 385 | a, err := s.cfg.Identity.SetAdmin(args[0], action == "grant-admin", actor) | |
| 385 | 386 | if err != nil { | |
| 386 | 387 | return nil, nil, err | |
| 387 | 388 | } | |
| 388 | return userData(a), func(out, _ io.Writer) { | ||
| 389 | return accountData(a), func(out, _ io.Writer) { | ||
| 389 | 390 | if action == "grant-admin" { | |
| 390 | 391 | fmt.Fprintln(out, "granted instance admin to "+a.Name) | |
| 391 | 392 | } else { | |
| @@ -393,7 +394,7 @@ func (s *Server) adminUser(action string, args []string, actor string, in io.Rea | |||
| 393 | 394 | } | |
| 394 | 395 | }, nil | |
| 395 | 396 | } | |
| 396 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("unknown user command %q", action)} | ||
| 397 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("unknown account command %q", action)} | ||
| 397 | 398 | } | |
| 398 | 399 | ||
| 399 | 400 | func (s *Server) adminRepo(action string, args []string, actor string) (any, adminRender, error) { | |
| @@ -455,7 +456,7 @@ func (s *Server) adminRepo(action string, args []string, actor string) (any, adm | |||
| 455 | 456 | return nil, nil, err | |
| 456 | 457 | } | |
| 457 | 458 | if len(f.rest) != 1 || f.one("owner") == "" { | |
| 458 | return nil, nil, &adminUsageError{msg: "usage: vole-admin repo create --owner <user> <name> [--description <text>]"} | ||
| 459 | return nil, nil, &adminUsageError{msg: "usage: vole-admin repo create --owner <account> <name> [--description <text>]"} | ||
| 459 | 460 | } | |
| 460 | 461 | owner, err := s.cfg.Identity.Find(f.one("owner")) | |
| 461 | 462 | if err != nil { | |
| @@ -477,7 +478,7 @@ func (s *Server) adminRepo(action string, args []string, actor string) (any, adm | |||
| 477 | 478 | } | |
| 478 | 479 | return data, func(out, errOut io.Writer) { | |
| 479 | 480 | fmt.Fprintf(out, "created %s (id %s)\n", m.Name, m.ID) | |
| 480 | fmt.Fprintf(errOut, "new repositories are private; publish with: vole admin acl default %s read\n", m.Name) | ||
| 481 | fmt.Fprintf(errOut, "new repositories are private; publish with: vole admin acl set-default-role %s read\n", m.Name) | ||
| 481 | 482 | }, nil | |
| 482 | 483 | ||
| 483 | 484 | case "rename": | |
| @@ -506,10 +507,78 @@ func (s *Server) adminRepo(action string, args []string, actor string) (any, adm | |||
| 506 | 507 | } | |
| 507 | 508 | ||
| 508 | 509 | func (s *Server) adminACL(action string, args []string, actor string) (any, adminRender, error) { | |
| 509 | if len(args) == 0 { | ||
| 510 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl <show|set|default|protect|approvals|checks|runners> <repo> ..."} | ||
| 510 | var ( | ||
| 511 | repoName string | ||
| 512 | mode string | ||
| 513 | accountName string | ||
| 514 | roleName string | ||
| 515 | approvals int | ||
| 516 | values []string | ||
| 517 | ) | ||
| 518 | switch action { | ||
| 519 | case "show": | ||
| 520 | if len(args) != 1 { | ||
| 521 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl show <repo>"} | ||
| 522 | } | ||
| 523 | repoName = args[0] | ||
| 524 | |||
| 525 | case "set-role": | ||
| 526 | if len(args) != 3 { | ||
| 527 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl set-role <repo> <account> <none|read|contribute|maintain|admin>"} | ||
| 528 | } | ||
| 529 | repoName, accountName, roleName = args[0], args[1], args[2] | ||
| 530 | |||
| 531 | case "set-default-role": | ||
| 532 | if len(args) != 2 { | ||
| 533 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl set-default-role <repo> <none|read|contribute|maintain|admin>"} | ||
| 534 | } | ||
| 535 | repoName, roleName = args[0], args[1] | ||
| 536 | |||
| 537 | case "set-approvals": | ||
| 538 | if len(args) != 2 { | ||
| 539 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl set-approvals <repo> <n>"} | ||
| 540 | } | ||
| 541 | repoName = args[0] | ||
| 542 | n, err := strconv.Atoi(args[1]) | ||
| 543 | if err != nil || n < 0 { | ||
| 544 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("%q is not a number of approvals", args[1])} | ||
| 545 | } | ||
| 546 | approvals = n | ||
| 547 | |||
| 548 | case "checks": | ||
| 549 | if len(args) != 2 { | ||
| 550 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl checks enable|disable <repo>"} | ||
| 551 | } | ||
| 552 | mode, repoName = args[0], args[1] | ||
| 553 | if mode != "enable" && mode != "disable" { | ||
| 554 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("checks takes enable or disable, not %q", mode)} | ||
| 555 | } | ||
| 556 | |||
| 557 | case "protect", "runners": | ||
| 558 | if len(args) < 2 { | ||
| 559 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("usage: vole-admin acl %s list|add|remove|set|clear <repo> ...", action)} | ||
| 560 | } | ||
| 561 | mode, repoName, values = args[0], args[1], args[2:] | ||
| 562 | switch mode { | ||
| 563 | case "list", "clear": | ||
| 564 | if len(values) > 0 { | ||
| 565 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("acl %s %s takes only a repository", action, mode)} | ||
| 566 | } | ||
| 567 | case "add", "remove", "set": | ||
| 568 | if len(values) == 0 { | ||
| 569 | if action == "protect" { | ||
| 570 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("acl protect %s needs at least one ref pattern", mode)} | ||
| 571 | } | ||
| 572 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("acl runners %s needs at least one account", mode)} | ||
| 573 | } | ||
| 574 | default: | ||
| 575 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("unknown acl %s command %q", action, mode)} | ||
| 576 | } | ||
| 577 | |||
| 578 | default: | ||
| 579 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl <show|set-role|set-default-role|set-approvals|checks|protect|runners> <repo> ..."} | ||
| 511 | 580 | } | |
| 512 | repoName := args[0] | ||
| 581 | |||
| 513 | 582 | m, err := s.cfg.Instance.Repos.Open(repoName) | |
| 514 | 583 | if err != nil { | |
| 515 | 584 | return nil, nil, fmt.Errorf("repository %q not found", repoName) | |
| @@ -530,20 +599,14 @@ func (s *Server) adminACL(action string, args []string, actor string) (any, admi | |||
| 530 | 599 | ||
| 531 | 600 | switch action { | |
| 532 | 601 | case "show": | |
| 533 | if len(args) != 1 { | ||
| 534 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl show <repo>"} | ||
| 535 | } | ||
| 536 | 602 | return s.aclResult(m, a) | |
| 537 | 603 | ||
| 538 | case "set": | ||
| 539 | if len(args) != 3 { | ||
| 540 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl set <repo> <user> <none|read|contribute|maintain|admin>"} | ||
| 541 | } | ||
| 542 | role, err := access.ParseRole(args[2]) | ||
| 604 | case "set-role": | ||
| 605 | role, err := access.ParseRole(roleName) | ||
| 543 | 606 | if err != nil { | |
| 544 | 607 | return nil, nil, err | |
| 545 | 608 | } | |
| 546 | account, err := s.cfg.Identity.Find(args[1]) | ||
| 609 | account, err := s.cfg.Identity.Find(accountName) | ||
| 547 | 610 | if err != nil { | |
| 548 | 611 | return nil, nil, err | |
| 549 | 612 | } | |
| @@ -552,73 +615,70 @@ func (s *Server) adminACL(action string, args []string, actor string) (any, admi | |||
| 552 | 615 | } | |
| 553 | 616 | return finish() | |
| 554 | 617 | ||
| 555 | case "default": | ||
| 556 | if len(args) != 2 { | ||
| 557 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl default <repo> <none|read|contribute|maintain|admin>"} | ||
| 558 | } | ||
| 559 | role, err := access.ParseRole(args[1]) | ||
| 618 | case "set-default-role": | ||
| 619 | role, err := access.ParseRole(roleName) | ||
| 560 | 620 | if err != nil { | |
| 561 | 621 | return nil, nil, err | |
| 562 | 622 | } | |
| 563 | 623 | a.Default = role.String() | |
| 564 | 624 | return finish() | |
| 565 | 625 | ||
| 566 | case "protect": | ||
| 567 | if len(args) < 2 { | ||
| 568 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl protect <repo> <ref-pattern>..."} | ||
| 569 | } | ||
| 570 | a.Protected = append([]string{}, args[1:]...) | ||
| 626 | case "set-approvals": | ||
| 627 | a.ApprovalsRequired = &approvals | ||
| 571 | 628 | return finish() | |
| 572 | 629 | ||
| 573 | case "approvals": | ||
| 574 | if len(args) != 2 { | ||
| 575 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl approvals <repo> <n>"} | ||
| 576 | } | ||
| 577 | n, err := strconv.Atoi(args[1]) | ||
| 578 | if err != nil || n < 0 { | ||
| 579 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("%q is not a number of approvals", args[1])} | ||
| 580 | } | ||
| 581 | a.ApprovalsRequired = &n | ||
| 630 | case "checks": | ||
| 631 | v := mode == "enable" | ||
| 632 | a.ChecksRequired = &v | ||
| 582 | 633 | return finish() | |
| 583 | 634 | ||
| 584 | case "checks": | ||
| 585 | if len(args) != 2 { | ||
| 586 | return nil, nil, &adminUsageError{msg: "usage: vole-admin acl checks <repo> <on|off>"} | ||
| 587 | } | ||
| 588 | switch strings.ToLower(args[1]) { | ||
| 589 | case "on", "true", "yes", "1": | ||
| 590 | v := true | ||
| 591 | a.ChecksRequired = &v | ||
| 592 | case "off", "false", "no", "0": | ||
| 593 | v := false | ||
| 594 | a.ChecksRequired = &v | ||
| 595 | default: | ||
| 596 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("%q is not on or off", args[1])} | ||
| 635 | case "protect": | ||
| 636 | if mode == "list" { | ||
| 637 | return s.aclProtectedResult(m, a) | ||
| 638 | } | ||
| 639 | switch mode { | ||
| 640 | case "clear": | ||
| 641 | a.Protected = nil | ||
| 642 | case "set": | ||
| 643 | a.Protected = append([]string(nil), values...) | ||
| 644 | case "add": | ||
| 645 | for _, pattern := range values { | ||
| 646 | found := false | ||
| 647 | for _, existing := range a.Protected { | ||
| 648 | if existing == pattern { | ||
| 649 | found = true | ||
| 650 | break | ||
| 651 | } | ||
| 652 | } | ||
| 653 | if !found { | ||
| 654 | a.Protected = append(a.Protected, pattern) | ||
| 655 | } | ||
| 656 | } | ||
| 657 | case "remove": | ||
| 658 | next := append([]string(nil), a.Protected...) | ||
| 659 | for _, pattern := range values { | ||
| 660 | kept := make([]string, 0, len(next)) | ||
| 661 | found := false | ||
| 662 | for _, existing := range next { | ||
| 663 | if existing == pattern { | ||
| 664 | found = true | ||
| 665 | continue | ||
| 666 | } | ||
| 667 | kept = append(kept, existing) | ||
| 668 | } | ||
| 669 | if !found { | ||
| 670 | return nil, nil, fmt.Errorf("%q is not protected for %s", pattern, m.Name) | ||
| 671 | } | ||
| 672 | next = kept | ||
| 673 | } | ||
| 674 | a.Protected = next | ||
| 597 | 675 | } | |
| 598 | 676 | return finish() | |
| 599 | 677 | ||
| 600 | 678 | case "runners": | |
| 601 | f, err := parseAdminFlags(args[1:], | ||
| 602 | adminFlagSpec{long: "add", boolean: true}, | ||
| 603 | adminFlagSpec{long: "remove", boolean: true}, | ||
| 604 | adminFlagSpec{long: "clear", boolean: true}) | ||
| 605 | if err != nil { | ||
| 606 | return nil, nil, err | ||
| 679 | if mode == "list" { | ||
| 680 | return s.aclRunnersResult(m, a) | ||
| 607 | 681 | } | |
| 608 | names := f.rest | ||
| 609 | edits := 0 | ||
| 610 | for _, b := range []string{"add", "remove", "clear"} { | ||
| 611 | if f.has(b) { | ||
| 612 | edits++ | ||
| 613 | } | ||
| 614 | } | ||
| 615 | if edits > 1 { | ||
| 616 | return nil, nil, &adminUsageError{msg: "use one of --add, --remove or --clear"} | ||
| 617 | } | ||
| 618 | if len(names) == 0 && !f.has("clear") && !f.has("add") && !f.has("remove") { | ||
| 619 | return s.aclResult(m, a) | ||
| 620 | } | ||
| 621 | var next []string | ||
| 622 | 682 | resolve := func(name string) (string, error) { | |
| 623 | 683 | account, err := s.cfg.Identity.Find(name) | |
| 624 | 684 | if err != nil { | |
| @@ -626,24 +686,34 @@ func (s *Server) adminACL(action string, args []string, actor string) (any, admi | |||
| 626 | 686 | } | |
| 627 | 687 | return account.Name, nil | |
| 628 | 688 | } | |
| 629 | switch { | ||
| 630 | case f.has("clear"): | ||
| 631 | if len(names) > 0 { | ||
| 632 | return nil, nil, &adminUsageError{msg: "--clear takes no names"} | ||
| 633 | } | ||
| 634 | case f.has("add"), f.has("remove"): | ||
| 635 | if len(names) == 0 { | ||
| 636 | return nil, nil, &adminUsageError{msg: "--add and --remove need at least one account"} | ||
| 689 | var next []string | ||
| 690 | switch mode { | ||
| 691 | case "clear": | ||
| 692 | // next stays empty | ||
| 693 | case "set": | ||
| 694 | next = make([]string, 0, len(values)) | ||
| 695 | for _, name := range values { | ||
| 696 | id, err := resolve(name) | ||
| 697 | if err != nil { | ||
| 698 | return nil, nil, err | ||
| 699 | } | ||
| 700 | next = append(next, id) | ||
| 637 | 701 | } | |
| 702 | case "add": | ||
| 638 | 703 | next = append(next, a.Runners...) | |
| 639 | for _, name := range names { | ||
| 704 | for _, name := range values { | ||
| 640 | 705 | id, err := resolve(name) | |
| 641 | 706 | if err != nil { | |
| 642 | 707 | return nil, nil, err | |
| 643 | 708 | } | |
| 644 | if !f.has("remove") { | ||
| 645 | next = append(next, id) | ||
| 646 | continue | ||
| 709 | next = append(next, id) | ||
| 710 | } | ||
| 711 | case "remove": | ||
| 712 | next = append(next, a.Runners...) | ||
| 713 | for _, name := range values { | ||
| 714 | id, err := resolve(name) | ||
| 715 | if err != nil { | ||
| 716 | return nil, nil, err | ||
| 647 | 717 | } | |
| 648 | 718 | kept, found := next[:0], false | |
| 649 | 719 | for _, r := range next { | |
| @@ -658,14 +728,6 @@ func (s *Server) adminACL(action string, args []string, actor string) (any, admi | |||
| 658 | 728 | } | |
| 659 | 729 | next = kept | |
| 660 | 730 | } | |
| 661 | default: | ||
| 662 | for _, name := range names { | ||
| 663 | id, err := resolve(name) | ||
| 664 | if err != nil { | ||
| 665 | return nil, nil, err | ||
| 666 | } | ||
| 667 | next = append(next, id) | ||
| 668 | } | ||
| 669 | 731 | } | |
| 670 | 732 | a.SetRunners(next) | |
| 671 | 733 | return finish() | |
| @@ -673,6 +735,38 @@ func (s *Server) adminACL(action string, args []string, actor string) (any, admi | |||
| 673 | 735 | return nil, nil, &adminUsageError{msg: fmt.Sprintf("unknown acl command %q", action)} | |
| 674 | 736 | } | |
| 675 | 737 | ||
| 738 | func (s *Server) aclProtectedResult(m *repo.Meta, a *access.ACL) (any, adminRender, error) { | ||
| 739 | protected := append([]string(nil), a.Protected...) | ||
| 740 | data := map[string]any{"repo": m.Name, "protected": protected} | ||
| 741 | return data, func(out, _ io.Writer) { | ||
| 742 | if len(a.Protected) == 0 { | ||
| 743 | fmt.Fprintf(out, "protected: none for %s\n", m.Name) | ||
| 744 | return | ||
| 745 | } | ||
| 746 | fmt.Fprintf(out, "protected: %s\n", strings.Join(a.Protected, ", ")) | ||
| 747 | }, nil | ||
| 748 | } | ||
| 749 | |||
| 750 | func (s *Server) aclRunnersResult(m *repo.Meta, a *access.ACL) (any, adminRender, error) { | ||
| 751 | runners := append([]string(nil), a.Runners...) | ||
| 752 | data := map[string]any{"repo": m.Name, "runners": runners} | ||
| 753 | return data, func(out, _ io.Writer) { | ||
| 754 | for _, r := range a.Runners { | ||
| 755 | fmt.Fprintf(out, "runner %s\n", r) | ||
| 756 | } | ||
| 757 | if len(a.Runners) > 0 { | ||
| 758 | fmt.Fprintln(out, "(admins may also report CI results)") | ||
| 759 | return | ||
| 760 | } | ||
| 761 | fmt.Fprintf(out, "runners: none named; only admins may report CI results for %s\n", m.Name) | ||
| 762 | for _, p := range a.Principals() { | ||
| 763 | if a.CanReport(p) { | ||
| 764 | fmt.Fprintf(out, " admin %s\n", p) | ||
| 765 | } | ||
| 766 | } | ||
| 767 | }, nil | ||
| 768 | } | ||
| 769 | |||
| 676 | 770 | func (s *Server) repoData(m *repo.Meta) (map[string]any, *access.ACL, error) { | |
| 677 | 771 | a, err := m.ACL() | |
| 678 | 772 | if err != nil { | |
| @@ -770,7 +864,7 @@ func (s *Server) renderInvite(inv *identity.Invite, out io.Writer) { | |||
| 770 | 864 | fmt.Fprintf(out, "expires: %s\n", inv.Expires.Format(time.RFC3339)) | |
| 771 | 865 | } | |
| 772 | 866 | ||
| 773 | func userData(a *identity.Account) map[string]any { | ||
| 867 | func accountData(a *identity.Account) map[string]any { | ||
| 774 | 868 | if a == nil { | |
| 775 | 869 | return nil | |
| 776 | 870 | } | |