| | @@ -108,10 +108,28 @@ |
| 108 | 108 | requireArgs(args, 3, "scuttlectl admin passwd <username>") |
| 109 | 109 | cmdAdminPasswd(api, args[2]) |
| 110 | 110 | default: |
| 111 | 111 | fmt.Fprintf(os.Stderr, "unknown subcommand: admin %s\n", args[1]) |
| 112 | 112 | os.Exit(1) |
| 113 | + } |
| 114 | + case "api-key", "api-keys": |
| 115 | + if len(args) < 2 { |
| 116 | + fmt.Fprintf(os.Stderr, "usage: scuttlectl api-key <list|create|revoke>\n") |
| 117 | + os.Exit(1) |
| 118 | + } |
| 119 | + switch args[1] { |
| 120 | + case "list": |
| 121 | + cmdAPIKeyList(api, *jsonFlag) |
| 122 | + case "create": |
| 123 | + requireArgs(args, 3, "scuttlectl api-key create --name <name> --scopes <scope1,scope2>") |
| 124 | + cmdAPIKeyCreate(api, args[2:], *jsonFlag) |
| 125 | + case "revoke": |
| 126 | + requireArgs(args, 3, "scuttlectl api-key revoke <id>") |
| 127 | + cmdAPIKeyRevoke(api, args[2]) |
| 128 | + default: |
| 129 | + fmt.Fprintf(os.Stderr, "unknown subcommand: api-key %s\n", args[1]) |
| 130 | + os.Exit(1) |
| 113 | 131 | } |
| 114 | 132 | case "channels", "channel": |
| 115 | 133 | if len(args) < 2 { |
| 116 | 134 | fmt.Fprintf(os.Stderr, "usage: scuttlectl channels <list|users <channel>>\n") |
| 117 | 135 | os.Exit(1) |
| | @@ -491,10 +509,88 @@ |
| 491 | 509 | fmt.Fprintf(tw, "password\t%s\n", creds.Password) |
| 492 | 510 | fmt.Fprintf(tw, "server\t%s\n", creds.Server) |
| 493 | 511 | tw.Flush() |
| 494 | 512 | fmt.Println("\nStore this password — it will not be shown again.") |
| 495 | 513 | } |
| 514 | + |
| 515 | +func cmdAPIKeyList(api *apiclient.Client, asJSON bool) { |
| 516 | + raw, err := api.ListAPIKeys() |
| 517 | + die(err) |
| 518 | + if asJSON { |
| 519 | + printJSON(raw) |
| 520 | + return |
| 521 | + } |
| 522 | + |
| 523 | + var keys []struct { |
| 524 | + ID string `json:"id"` |
| 525 | + Name string `json:"name"` |
| 526 | + Scopes []string `json:"scopes"` |
| 527 | + CreatedAt string `json:"created_at"` |
| 528 | + LastUsed *string `json:"last_used"` |
| 529 | + ExpiresAt *string `json:"expires_at"` |
| 530 | + Active bool `json:"active"` |
| 531 | + } |
| 532 | + must(json.Unmarshal(raw, &keys)) |
| 533 | + |
| 534 | + if len(keys) == 0 { |
| 535 | + fmt.Println("no API keys") |
| 536 | + return |
| 537 | + } |
| 538 | + |
| 539 | + tw := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) |
| 540 | + fmt.Fprintln(tw, "ID\tNAME\tSCOPES\tACTIVE\tLAST USED") |
| 541 | + for _, k := range keys { |
| 542 | + lastUsed := "-" |
| 543 | + if k.LastUsed != nil { |
| 544 | + lastUsed = *k.LastUsed |
| 545 | + } |
| 546 | + status := "yes" |
| 547 | + if !k.Active { |
| 548 | + status = "revoked" |
| 549 | + } |
| 550 | + fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", k.ID, k.Name, strings.Join(k.Scopes, ","), status, lastUsed) |
| 551 | + } |
| 552 | + tw.Flush() |
| 553 | +} |
| 554 | + |
| 555 | +func cmdAPIKeyCreate(api *apiclient.Client, args []string, asJSON bool) { |
| 556 | + fs := flag.NewFlagSet("api-key create", flag.ExitOnError) |
| 557 | + nameFlag := fs.String("name", "", "key name (required)") |
| 558 | + scopesFlag := fs.String("scopes", "", "comma-separated scopes (required)") |
| 559 | + expiresFlag := fs.String("expires", "", "expiry duration (e.g. 720h for 30 days)") |
| 560 | + _ = fs.Parse(args) |
| 561 | + |
| 562 | + if *nameFlag == "" || *scopesFlag == "" { |
| 563 | + fmt.Fprintln(os.Stderr, "usage: scuttlectl api-key create --name <name> --scopes <scope1,scope2> [--expires 720h]") |
| 564 | + os.Exit(1) |
| 565 | + } |
| 566 | + |
| 567 | + scopes := strings.Split(*scopesFlag, ",") |
| 568 | + raw, err := api.CreateAPIKey(*nameFlag, scopes, *expiresFlag) |
| 569 | + die(err) |
| 570 | + |
| 571 | + if asJSON { |
| 572 | + printJSON(raw) |
| 573 | + return |
| 574 | + } |
| 575 | + |
| 576 | + var key struct { |
| 577 | + ID string `json:"id"` |
| 578 | + Name string `json:"name"` |
| 579 | + Token string `json:"token"` |
| 580 | + } |
| 581 | + must(json.Unmarshal(raw, &key)) |
| 582 | + |
| 583 | + fmt.Printf("API key created: %s\n\n", key.Name) |
| 584 | + fmt.Printf(" Token: %s\n\n", key.Token) |
| 585 | + fmt.Println("Store this token — it will not be shown again.") |
| 586 | +} |
| 587 | + |
| 588 | +func cmdAPIKeyRevoke(api *apiclient.Client, id string) { |
| 589 | + die(api.RevokeAPIKey(id)) |
| 590 | + fmt.Printf("API key revoked: %s\n", id) |
| 591 | +} |
| 496 | 592 | |
| 497 | 593 | func usage() { |
| 498 | 594 | fmt.Fprintf(os.Stderr, `scuttlectl %s — scuttlebot management CLI |
| 499 | 595 | |
| 500 | 596 | Usage: |
| | @@ -526,10 +622,13 @@ |
| 526 | 622 | backend rename <old> <new> rename a backend |
| 527 | 623 | admin list list admin accounts |
| 528 | 624 | admin add <username> add admin (prompts for password) |
| 529 | 625 | admin remove <username> remove admin |
| 530 | 626 | admin passwd <username> change admin password (prompts) |
| 627 | + api-key list list API keys |
| 628 | + api-key create --name <name> --scopes <s1,s2> [--expires 720h] |
| 629 | + api-key revoke <id> revoke an API key |
| 531 | 630 | `, version) |
| 532 | 631 | } |
| 533 | 632 | |
| 534 | 633 | func printJSON(raw json.RawMessage) { |
| 535 | 634 | var buf []byte |
| 536 | 635 | |