Skip to content

Commit

Permalink
cmd: add kes ls command
Browse files Browse the repository at this point in the history
This commit adds the `kes ls` command that lists keys, policies and
identities.
```
Usage:
    kes ls [-a KEY] [-k] [--json] [-i] [-p] [-s HOST[:PORT]] [PREFIX]

Options:
    -a, --api-key KEY           API key to authenticate to the KES server.
                                Defaults to $MINIO_KES_API_KEY.
    -s, --server HOST[:PORT]    Use the server HOST[:PORT] instead of
                                $MINIO_KES_SERVER.
        --json                  Print output in JSON format.
    -i, --identity              List identities.
    -p, --policy                List policy names.
    -k, --insecure              Skip server certificate verification.
```

This command replaces `kes key ls`, `kes policy ls` and `kes identity ls`
mid-term.

Signed-off-by: Andreas Auernhammer <[email protected]>
  • Loading branch information
aead committed May 16, 2024
1 parent 802ce81 commit ef5d2f8
Show file tree
Hide file tree
Showing 11 changed files with 426 additions and 107 deletions.
4 changes: 2 additions & 2 deletions cmd/kes/color-option.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ package main

import (
"errors"
"os"
"strings"

tui "github.com/charmbracelet/lipgloss"
"github.com/minio/kes/internal/cli"
"github.com/muesli/termenv"
flag "github.com/spf13/pflag"
)
Expand All @@ -29,7 +29,7 @@ var _ flag.Value = (*colorOption)(nil)

func (c *colorOption) Colorize() bool {
v := strings.ToLower(c.value)
return v == "always" || ((v == "auto" || v == "") && isTerm(os.Stdout))
return v == "always" || ((v == "auto" || v == "") && cli.IsTerminal())
}

func (c *colorOption) String() string { return c.value }
Expand Down
29 changes: 29 additions & 0 deletions cmd/kes/flags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package main

import (
"github.com/minio/kes/internal/cli"
flag "github.com/spf13/pflag"
)

// Use register functions for common flags exposed by
// many commands. Common flags should have common names
// to make command usage consistent.

// flagsInsecureSkipVerify adds a bool flag '-k, --insecure'
// that sets insecureSkipVerify to true if provided on the
// command line.
func flagsInsecureSkipVerify(f *flag.FlagSet, insecureSkipVerify *bool) {
f.BoolVarP(insecureSkipVerify, "insecure", "k", false, "Skip server certificate verification")
}

func flagsAPIKey(f *flag.FlagSet, apiKey *string) {
f.StringVarP(apiKey, "api-key", "a", cli.Env(cli.EnvAPIKey), "API key to authenticate to the KES server")
}

func flagsOutputJSON(f *flag.FlagSet, jsonOutput *bool) {
f.BoolVar(jsonOutput, "json", false, "Print output in JSON format")
}

func flagsServer(f *flag.FlagSet, host *string) {
f.StringVarP(host, "server", "s", cli.Env(cli.EnvServer), "Use the server HOST[:PORT]")
}
12 changes: 8 additions & 4 deletions cmd/kes/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ func newIdentityCmd(args []string) {
}

bold := tui.NewStyle()
if isTerm(os.Stdout) {
if cli.IsTerminal() {
bold = bold.Bold(true)
}
var buffer strings.Builder
Expand Down Expand Up @@ -418,7 +418,7 @@ func ofIdentityCmd(args []string) {
h := sha256.Sum256(cert.RawSubjectPublicKeyInfo)
identity = kes.Identity(hex.EncodeToString(h[:]))
}
if isTerm(os.Stdout) {
if cli.IsTerminal() {
var buffer strings.Builder
fmt.Fprintln(&buffer, "Identity:")
fmt.Fprintln(&buffer)
Expand Down Expand Up @@ -491,7 +491,9 @@ func infoIdentityCmd(args []string) {
dotDenyStyle = dotDenyStyle.Foreground(ColorDotDeny)
}

client := newClient(insecureSkipVerify)
client := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
if cmd.NArg() == 0 {
info, policy, err := client.DescribeSelf(ctx)
if err != nil {
Expand Down Expand Up @@ -619,7 +621,9 @@ func lsIdentityCmd(args []string) {
ctx, cancelCtx := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancelCtx()

enclave := newClient(insecureSkipVerify)
enclave := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
iter := &kes.ListIter[kes.Identity]{
NextFunc: enclave.ListIdentities,
}
Expand Down
38 changes: 27 additions & 11 deletions cmd/kes/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ func createKeyCmd(args []string) {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()

client := newClient(insecureSkipVerify)
client := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
for _, name := range cmd.Args() {
if err := client.CreateKey(ctx, name); err != nil {
if errors.Is(err, context.Canceled) {
Expand Down Expand Up @@ -169,7 +171,9 @@ func importKeyCmd(args []string) {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()

enclave := newClient(insecureSkipVerify)
enclave := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
if err = enclave.ImportKey(ctx, name, &kes.ImportKeyRequest{Key: key}); err != nil {
if errors.Is(err, context.Canceled) {
os.Exit(1)
Expand Down Expand Up @@ -229,7 +233,9 @@ func describeKeyCmd(args []string) {
defer cancelCtx()

name := cmd.Arg(0)
client := newClient(insecureSkipVerify)
client := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
info, err := client.DescribeKey(ctx, name)
if err != nil {
if errors.Is(err, context.Canceled) {
Expand Down Expand Up @@ -308,7 +314,9 @@ func lsKeyCmd(args []string) {
ctx, cancelCtx := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancelCtx()

enclave := newClient(insecureSkipVerify)
enclave := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
iter := &kes.ListIter[string]{
NextFunc: enclave.ListKeys,
}
Expand Down Expand Up @@ -380,7 +388,9 @@ func rmKeyCmd(args []string) {
ctx, cancelCtx := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancelCtx()

client := newClient(insecureSkipVerify)
client := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
for _, name := range cmd.Args() {
if err := client.DeleteKey(ctx, name); err != nil {
if errors.Is(err, context.Canceled) {
Expand Down Expand Up @@ -436,7 +446,9 @@ func encryptKeyCmd(args []string) {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()

client := newClient(insecureSkipVerify)
client := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
ciphertext, err := client.Encrypt(ctx, name, []byte(message), nil)
if err != nil {
if errors.Is(err, context.Canceled) {
Expand All @@ -445,7 +457,7 @@ func encryptKeyCmd(args []string) {
cli.Fatalf("failed to encrypt message: %v", err)
}

if isTerm(os.Stdout) {
if cli.IsTerminal() {
fmt.Printf("\nciphertext: %s\n", base64.StdEncoding.EncodeToString(ciphertext))
} else {
fmt.Printf(`{"ciphertext":"%s"}`, base64.StdEncoding.EncodeToString(ciphertext))
Expand Down Expand Up @@ -509,7 +521,9 @@ func decryptKeyCmd(args []string) {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()

client := newClient(insecureSkipVerify)
client := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
plaintext, err := client.Decrypt(ctx, name, ciphertext, associatedData)
if err != nil {
if errors.Is(err, context.Canceled) {
Expand All @@ -518,7 +532,7 @@ func decryptKeyCmd(args []string) {
cli.Fatalf("failed to decrypt ciphertext: %v", err)
}

if isTerm(os.Stdout) {
if cli.IsTerminal() {
fmt.Printf("\nplaintext: %s\n", base64.StdEncoding.EncodeToString(plaintext))
} else {
fmt.Printf(`{"plaintext":"%s"}`, base64.StdEncoding.EncodeToString(plaintext))
Expand Down Expand Up @@ -575,7 +589,9 @@ func dekCmd(args []string) {
ctx, cancelCtx := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancelCtx()

client := newClient(insecureSkipVerify)
client := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
key, err := client.GenerateKey(ctx, name, associatedData)
if err != nil {
if errors.Is(err, context.Canceled) {
Expand All @@ -588,7 +604,7 @@ func dekCmd(args []string) {
plaintext = base64.StdEncoding.EncodeToString(key.Plaintext)
ciphertext = base64.StdEncoding.EncodeToString(key.Ciphertext)
)
if isTerm(os.Stdout) {
if cli.IsTerminal() {
const format = "\nplaintext: %s\nciphertext: %s\n"
fmt.Printf(format, plaintext, ciphertext)
} else {
Expand Down
6 changes: 4 additions & 2 deletions cmd/kes/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ func logCmd(args []string) {
auditFlag = !auditFlag
}

client := newClient(insecureSkipVerify)
client := newClient(config{
InsecureSkipVerify: insecureSkipVerify,
})
ctx, cancelCtx := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancelCtx()

Expand Down Expand Up @@ -125,7 +127,7 @@ func printAuditLog(stream *kes.AuditStream) {
format = "%02d:%02d:%02d %s %s %s %s %s\n"
)

if isTerm(os.Stdout) {
if cli.IsTerminal() {
fmt.Println(tui.NewStyle().Bold(true).Underline(true).Render(header))
} else {
fmt.Println(header)
Expand Down
161 changes: 161 additions & 0 deletions cmd/kes/ls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright 2024 - MinIO, Inc. All rights reserved.
// Use of this source code is governed by the AGPLv3
// license that can be found in the LICENSE file.

package main

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"os/signal"
"slices"
"strings"

tui "github.com/charmbracelet/lipgloss"
"github.com/minio/kes/internal/cli"
"github.com/minio/kms-go/kes"
flag "github.com/spf13/pflag"
)

const lsUsage = `Usage:
kes ls [-a KEY] [-k] [--json] [-i] [-p] [-s HOST[:PORT]] [PREFIX]
Options:
-a, --api-key KEY API key to authenticate to the KES server.
Defaults to $MINIO_KES_API_KEY.
-s, --server HOST[:PORT] Use the server HOST[:PORT] instead of
$MINIO_KES_SERVER.
--json Print output in JSON format.
-i, --identity List identities.
-p, --policy List policy names.
-k, --insecure Skip server certificate verification.
`

func ls(args []string) {
var (
apiKey string
skipVerify bool
jsonOutput bool
host string
policies bool
identities bool
)

flags := flag.NewFlagSet(args[0], flag.ContinueOnError)
flagsAPIKey(flags, &apiKey)
flagsInsecureSkipVerify(flags, &skipVerify)
flagsOutputJSON(flags, &jsonOutput)
flagsServer(flags, &host)
flags.BoolVarP(&policies, "policy", "p", false, "")
flags.BoolVarP(&identities, "identity", "i", false, "")
flags.Usage = func() { fmt.Fprint(os.Stderr, lsUsage) }

if err := flags.Parse(args[1:]); err != nil {
cli.Exit(err)
}
if flags.NArg() > 1 {
cli.Exit("too many arguments")
}
if identities && policies {
cli.Exit("'-p / --policy' and '-i / --identity' must not be used at the same time")
}

// Define functions for listing keys, identities and policies.
// All a []string since we want to print the elements anyway.
listKeys := func(ctx context.Context, client *kes.Client, prefix string) ([]string, error) {
iter := kes.ListIter[string]{
NextFunc: client.ListKeys,
}
var names []string
for name, err := iter.SeekTo(ctx, prefix); err != io.EOF; name, err = iter.Next(ctx) {
if err != nil {
return nil, err
}
names = append(names, name)
}
return names, nil
}
listIdentities := func(ctx context.Context, client *kes.Client, prefix string) ([]string, error) {
iter := kes.ListIter[kes.Identity]{
NextFunc: client.ListIdentities,
}
var names []string
for id, err := iter.SeekTo(ctx, prefix); err != io.EOF; id, err = iter.Next(ctx) {
if err != nil {
return nil, err
}
names = append(names, id.String())
}
return names, nil
}
listPolicies := func(ctx context.Context, client *kes.Client, prefix string) ([]string, error) {
iter := kes.ListIter[string]{
NextFunc: client.ListPolicies,
}
var names []string
for name, err := iter.SeekTo(ctx, prefix); err != io.EOF; name, err = iter.Next(ctx) {
if err != nil {
return nil, err
}
names = append(names, name)
}
return names, nil
}

var prefix string
if flags.NArg() == 1 {
prefix = flags.Arg(0)
}

client := newClient(config{
Endpoint: host,
APIKey: apiKey,
InsecureSkipVerify: skipVerify,
})
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

var (
names []string
err error
)
switch {
case identities:
names, err = listIdentities(ctx, client, prefix)
case policies:
names, err = listPolicies(ctx, client, prefix)
default:
names, err = listKeys(ctx, client, prefix)
}
if err != nil {
cli.Exit(err)
}
slices.Sort(names)

if jsonOutput {
if err := json.NewEncoder(os.Stdout).Encode(names); err != nil {
cli.Exit(err)
}
return
}
if len(names) == 0 {
return
}

buf := &strings.Builder{}
switch s := tui.NewStyle().Underline(true); {
case identities:
fmt.Fprintln(buf, s.Render("Identity"))
case policies:
fmt.Fprintln(buf, s.Render("Policy"))
default:
fmt.Fprintln(buf, s.Render("Key"))
}
for _, name := range names {
fmt.Fprintln(buf, name)
}
fmt.Print(buf)
}
Loading

0 comments on commit ef5d2f8

Please sign in to comment.