209 lines
5.5 KiB
Go
209 lines
5.5 KiB
Go
package clicmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/iceBear67/simplepages/api"
|
|
"github.com/iceBear67/simplepages/internal/cliutil"
|
|
)
|
|
|
|
func keyCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "key",
|
|
Short: "Manage API keys",
|
|
Sub: []*cliutil.Command{
|
|
keyCreateCmd(g),
|
|
keyListCmd(g),
|
|
keyRevokeCmd(g),
|
|
},
|
|
}
|
|
}
|
|
|
|
func keyCreateCmd(g *Globals) *cliutil.Command {
|
|
var (
|
|
name string
|
|
admin bool
|
|
expires string
|
|
)
|
|
return &cliutil.Command{
|
|
Name: "create",
|
|
Short: "Mint a key",
|
|
Long: "The token is printed once and cannot be retrieved again — the server\n" +
|
|
"stores only its hash. Redirect it straight into a file or a secret\n" +
|
|
"store; do not let it reach a CI log.\n\n" +
|
|
"Without --admin the key is scoped to one project and can do nothing\n" +
|
|
"outside it. Requires an admin key either way.",
|
|
Flags: func(fs *flag.FlagSet) {
|
|
fs.StringVar(&name, "name", "", "`label` recorded with the key, e.g. github-actions")
|
|
fs.BoolVar(&admin, "admin", false, "mint an admin key instead of a project key")
|
|
fs.StringVar(&expires, "expires", "", "expire after this `duration`, e.g. 90d; default never")
|
|
},
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
if err := exactArgs(args, 0, "no arguments"); err != nil {
|
|
return err
|
|
}
|
|
req := api.CreateKeyRequest{Name: name}
|
|
if expires != "" {
|
|
d, err := cliutil.ParseDuration(expires)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d <= 0 {
|
|
return errors.New("--expires must be positive")
|
|
}
|
|
// Computed here, sent absolute: a clock difference between this
|
|
// machine and the server then shifts nothing.
|
|
t := time.Now().Add(d).UTC().Truncate(time.Second)
|
|
req.ExpiresAt = &t
|
|
}
|
|
|
|
c, err := g.Client()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var out api.CreateKeyResponse
|
|
if admin {
|
|
out, err = c.CreateAdminKey(ctx, req)
|
|
} else {
|
|
var project string
|
|
if project, err = g.ProjectName(); err != nil {
|
|
return fmt.Errorf("%w, or pass --admin for a server-wide key", err)
|
|
}
|
|
out, err = c.CreateProjectKey(ctx, project, req)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p, err := g.Printer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if p.Format == cliutil.FormatJSON {
|
|
return p.JSON(out)
|
|
}
|
|
// Table format prints the token on a line of its own so it survives
|
|
// a copy-paste and so `pages key create | tail -1` is not tempting.
|
|
if err := keyTable(out.Key).Write(g.Out); err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(g.Out, "\n%s\n", out.Token)
|
|
fmt.Fprintln(g.Err, "this token is shown once and cannot be recovered; store it now")
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func keyListCmd(g *Globals) *cliutil.Command {
|
|
return &cliutil.Command{
|
|
Name: "list",
|
|
Short: "List keys",
|
|
Long: "With --project, lists that project's keys; a project key may list its\n" +
|
|
"own. Without it, lists every key on the server and requires an admin\n" +
|
|
"key. Secrets are never listed — only key ids.",
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
if err := exactArgs(args, 0, "no arguments"); err != nil {
|
|
return err
|
|
}
|
|
if err := g.Resolve(); err != nil {
|
|
return err
|
|
}
|
|
c, err := g.Client()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var list api.KeyList
|
|
if g.Project != "" {
|
|
list, err = c.ListProjectKeys(ctx, g.Project)
|
|
} else {
|
|
list, err = c.ListKeys(ctx)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if list.Keys == nil {
|
|
list.Keys = []api.Key{}
|
|
}
|
|
p, err := g.Printer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return p.Print(list, func() *cliutil.Table {
|
|
t := cliutil.NewTable("ID", "SCOPE", "PROJECT", "NAME", "CREATED", "EXPIRES", "LAST USED", "STATE")
|
|
for _, k := range list.Keys {
|
|
t.Row(k.ID, k.Scope, cliutil.Str(k.Project), cliutil.Str(k.Name),
|
|
cliutil.Time(k.CreatedAt), cliutil.TimePtr(k.ExpiresAt),
|
|
cliutil.TimePtr(k.LastUsed), keyState(k))
|
|
}
|
|
return t
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
func keyRevokeCmd(g *Globals) *cliutil.Command {
|
|
var yes bool
|
|
return &cliutil.Command{
|
|
Name: "revoke",
|
|
Args: "<key-id>",
|
|
Short: "Revoke a key",
|
|
Long: "Takes effect immediately across the server. The key id is the middle\n" +
|
|
"segment of a token (pgs_<key-id>_<secret>) and is what `key list`\n" +
|
|
"shows. Revoking an already-revoked key succeeds.",
|
|
Flags: func(fs *flag.FlagSet) {
|
|
fs.BoolVar(&yes, "yes", false, "do not ask for confirmation")
|
|
},
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
if err := exactArgs(args, 1, "one key id"); err != nil {
|
|
return err
|
|
}
|
|
id := args[0]
|
|
if !yes {
|
|
if err := cliutil.Confirm(g.In, g.Err, fmt.Sprintf("Revoke key %s?", id)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
c, err := g.Client()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := c.RevokeKey(ctx, id); err != nil {
|
|
return err
|
|
}
|
|
p, err := g.Printer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.Printf("revoked key %s\n", id)
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// keyState collapses the two timestamps that decide whether a key still works.
|
|
func keyState(k api.Key) string {
|
|
switch {
|
|
case k.Revoked():
|
|
return "revoked"
|
|
case k.ExpiresAt != nil && k.ExpiresAt.Before(time.Now()):
|
|
return "expired"
|
|
default:
|
|
return "active"
|
|
}
|
|
}
|
|
|
|
func keyTable(k api.Key) *cliutil.Table {
|
|
t := cliutil.NewTable()
|
|
t.Row("id", k.ID)
|
|
t.Row("scope", k.Scope)
|
|
t.Row("project", cliutil.Str(k.Project))
|
|
t.Row("name", cliutil.Str(k.Name))
|
|
t.Row("created_at", cliutil.Time(k.CreatedAt))
|
|
t.Row("expires_at", cliutil.TimePtr(k.ExpiresAt))
|
|
return t
|
|
}
|