diff options
author | Martin Polden <mpolden@mpolden.no> | 2021-09-01 09:08:56 +0200 |
---|---|---|
committer | Martin Polden <mpolden@mpolden.no> | 2021-09-01 09:09:06 +0200 |
commit | 689fa4af9a00c1897837ce081eb1ca20c8d4672e (patch) | |
tree | a461c661bc31953e3d779107e164cde14d99efc1 | |
parent | 33d2f7ad62958addde4e115f96e53a8108110cf2 (diff) |
Implement vespa api-key
-rw-r--r-- | client/go/cmd/api_key.go | 56 | ||||
-rw-r--r-- | client/go/cmd/api_key_test.go | 21 | ||||
-rw-r--r-- | client/go/cmd/cert.go | 2 | ||||
-rw-r--r-- | client/go/cmd/command_tester.go | 46 | ||||
-rw-r--r-- | client/go/cmd/config_test.go | 27 | ||||
-rw-r--r-- | client/go/go.mod | 1 | ||||
-rw-r--r-- | client/go/vespa/crypto.go | 13 | ||||
-rw-r--r-- | client/go/vespa/crypto_test.go | 22 |
8 files changed, 143 insertions, 45 deletions
diff --git a/client/go/cmd/api_key.go b/client/go/cmd/api_key.go new file mode 100644 index 00000000000..a4e1cff40e3 --- /dev/null +++ b/client/go/cmd/api_key.go @@ -0,0 +1,56 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// vespa api-key command +// Author: mpolden +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/vespa-engine/vespa/util" + "github.com/vespa-engine/vespa/vespa" +) + +var overwriteKey bool + +func init() { + rootCmd.AddCommand(apiKeyCmd) + apiKeyCmd.Flags().BoolVarP(&overwriteKey, "force", "f", false, "Force overwrite of existing API key") + apiKeyCmd.MarkPersistentFlagRequired(applicationFlag) +} + +var apiKeyCmd = &cobra.Command{ + Use: "api-key", + Short: "Create a new user API key for authentication with Vespa Cloud", + Example: "$ vespa api-key -a my-tenant.my-app.my-instance", + Args: cobra.ExactArgs(0), + Run: func(cmd *cobra.Command, args []string) { + configDir := configDir("") + if configDir == "" { + return + } + app, err := vespa.ApplicationFromString(getApplication()) + if err != nil { + printErr(err, "Could not parse application") + return + } + apiKeyFile := filepath.Join(configDir, app.Tenant+".api-key.pem") + if util.PathExists(apiKeyFile) && !overwriteKey { + printErrHint(fmt.Errorf("File %s already exists", apiKeyFile), "Use -f to overwrite it") + return + } + apiKey, err := vespa.CreateAPIKey() + if err != nil { + printErr(err, "Could not create API key") + return + } + if err := os.WriteFile(apiKeyFile, apiKey, 0600); err == nil { + printSuccess("API key written to ", apiKeyFile) + } else { + printErr(err, "Failed to write ", apiKeyFile) + + } + }, +} diff --git a/client/go/cmd/api_key_test.go b/client/go/cmd/api_key_test.go new file mode 100644 index 00000000000..736d5a33744 --- /dev/null +++ b/client/go/cmd/api_key_test.go @@ -0,0 +1,21 @@ +// Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +// Author: mpolden + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAPIKey(t *testing.T) { + configDir := t.TempDir() + keyFile := configDir + "/.vespa/t1.api-key.pem" + + out := execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, configDir: configDir}, t, nil) + assert.Equal(t, "Success: API key written to "+keyFile+"\n", out) + + out = execute(command{args: []string{"api-key", "-a", "t1.a1.i1"}, configDir: configDir}, t, nil) + assert.Equal(t, "Error: File "+keyFile+" already exists\nHint: Use -f to overwrite it\n", out) +} diff --git a/client/go/cmd/cert.go b/client/go/cmd/cert.go index 3d2bd37affd..04c5fc2d272 100644 --- a/client/go/cmd/cert.go +++ b/client/go/cmd/cert.go @@ -23,7 +23,7 @@ func init() { var certCmd = &cobra.Command{ Use: "cert", - Short: "Create a new private key and self-signed certificate for a cloud deployment", + Short: "Create a new private key and self-signed certificate for Vespa Cloud deployment", Example: "$ vespa cert -a my-tenant.my-app.my-instance", Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { diff --git a/client/go/cmd/command_tester.go b/client/go/cmd/command_tester.go index 46c37e0fbd2..074089b399d 100644 --- a/client/go/cmd/command_tester.go +++ b/client/go/cmd/command_tester.go @@ -15,12 +15,19 @@ import ( "time" "github.com/logrusorgru/aurora" + "github.com/spf13/pflag" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/vespa-engine/vespa/util" ) -func executeCommand(t *testing.T, client *mockHttpClient, args []string, moreArgs []string) string { +type command struct { + configDir string + args []string + moreArgs []string +} + +func execute(cmd command, t *testing.T, client *mockHttpClient) string { if client != nil { util.ActiveHttpClient = client } @@ -28,27 +35,40 @@ func executeCommand(t *testing.T, client *mockHttpClient, args []string, moreArg // Never print colors in tests color = aurora.NewAurora(false) - // Use a separate config dir for each test - os.Setenv("VESPA_CLI_HOME", t.TempDir()) - if len(args) > 0 && args[0] != "config" { - viper.Reset() // Reset config unless we're testing the config sub-command + // Set config dir. Use a separate one per test if none is specified + if cmd.configDir == "" { + cmd.configDir = t.TempDir() + viper.Reset() } + os.Setenv("VESPA_CLI_HOME", cmd.configDir) - // Reset to default target - persistent flags in Cobra persists over tests - log.SetOutput(bytes.NewBufferString("")) - rootCmd.SetArgs([]string{"status", "-t", "local"}) - rootCmd.Execute() + // Reset flags to their default value - persistent flags in Cobra persists over tests + rootCmd.Flags().VisitAll(func(f *pflag.Flag) { + switch v := f.Value.(type) { + case pflag.SliceValue: + _ = v.Replace([]string{}) + default: + switch v.Type() { + case "bool", "string", "int": + _ = v.Set(f.DefValue) + } + } + }) // Capture stdout and execute command - b := bytes.NewBufferString("") - log.SetOutput(b) - rootCmd.SetArgs(append(args, moreArgs...)) + var b bytes.Buffer + log.SetOutput(&b) + rootCmd.SetArgs(append(cmd.args, cmd.moreArgs...)) rootCmd.Execute() - out, err := ioutil.ReadAll(b) + out, err := ioutil.ReadAll(&b) assert.Empty(t, err, "No error") return string(out) } +func executeCommand(t *testing.T, client *mockHttpClient, args []string, moreArgs []string) string { + return execute(command{args: args, moreArgs: moreArgs}, t, client) +} + type mockHttpClient struct { // The HTTP status code that will be returned from the next invocation. Default: 200 nextStatus int diff --git a/client/go/cmd/config_test.go b/client/go/cmd/config_test.go index ff97157ba07..caa116c0441 100644 --- a/client/go/cmd/config_test.go +++ b/client/go/cmd/config_test.go @@ -7,19 +7,20 @@ import ( ) func TestConfig(t *testing.T) { - assert.Equal(t, "invalid option or value: \"foo\": \"bar\"\n", executeCommand(t, nil, []string{"config", "set", "foo", "bar"}, nil)) - assert.Equal(t, "foo = <unset>\n", executeCommand(t, nil, []string{"config", "get", "foo"}, nil)) - assert.Equal(t, "target = local\n", executeCommand(t, nil, []string{"config", "get", "target"}, nil)) - assert.Equal(t, "", executeCommand(t, nil, []string{"config", "set", "target", "cloud"}, nil)) - assert.Equal(t, "target = cloud\n", executeCommand(t, nil, []string{"config", "get", "target"}, nil)) - assert.Equal(t, "", executeCommand(t, nil, []string{"config", "set", "target", "http://127.0.0.1:8080"}, nil)) - assert.Equal(t, "", executeCommand(t, nil, []string{"config", "set", "target", "https://127.0.0.1"}, nil)) - assert.Equal(t, "target = https://127.0.0.1\n", executeCommand(t, nil, []string{"config", "get", "target"}, nil)) + configDir := t.TempDir() + assert.Equal(t, "invalid option or value: \"foo\": \"bar\"\n", execute(command{configDir: configDir, args: []string{"config", "set", "foo", "bar"}}, t, nil)) + assert.Equal(t, "foo = <unset>\n", execute(command{configDir: configDir, args: []string{"config", "get", "foo"}}, t, nil)) + assert.Equal(t, "target = local\n", execute(command{configDir: configDir, args: []string{"config", "get", "target"}}, t, nil)) + assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "target", "cloud"}}, t, nil)) + assert.Equal(t, "target = cloud\n", execute(command{configDir: configDir, args: []string{"config", "get", "target"}}, t, nil)) + assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "target", "http://127.0.0.1:8080"}}, t, nil)) + assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "target", "https://127.0.0.1"}}, t, nil)) + assert.Equal(t, "target = https://127.0.0.1\n", execute(command{configDir: configDir, args: []string{"config", "get", "target"}}, t, nil)) - assert.Equal(t, "invalid application: \"foo\"\n", executeCommand(t, nil, []string{"config", "set", "application", "foo"}, nil)) - assert.Equal(t, "application = <unset>\n", executeCommand(t, nil, []string{"config", "get", "application"}, nil)) - assert.Equal(t, "", executeCommand(t, nil, []string{"config", "set", "application", "t1.a1.i1"}, nil)) - assert.Equal(t, "application = t1.a1.i1\n", executeCommand(t, nil, []string{"config", "get", "application"}, nil)) + assert.Equal(t, "invalid application: \"foo\"\n", execute(command{configDir: configDir, args: []string{"config", "set", "application", "foo"}}, t, nil)) + assert.Equal(t, "application = <unset>\n", execute(command{configDir: configDir, args: []string{"config", "get", "application"}}, t, nil)) + assert.Equal(t, "", execute(command{configDir: configDir, args: []string{"config", "set", "application", "t1.a1.i1"}}, t, nil)) + assert.Equal(t, "application = t1.a1.i1\n", execute(command{configDir: configDir, args: []string{"config", "get", "application"}}, t, nil)) - assert.Equal(t, "target = https://127.0.0.1\napplication = t1.a1.i1\n", executeCommand(t, nil, []string{"config", "get"}, nil)) + assert.Equal(t, "target = https://127.0.0.1\napplication = t1.a1.i1\n", execute(command{configDir: configDir, args: []string{"config", "get"}}, t, nil)) } diff --git a/client/go/go.mod b/client/go/go.mod index ff444215332..4b72925a0d1 100644 --- a/client/go/go.mod +++ b/client/go/go.mod @@ -7,6 +7,7 @@ require ( github.com/mattn/go-colorable v0.0.9 github.com/mattn/go-isatty v0.0.3 github.com/spf13/cobra v1.2.1 + github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.8.1 github.com/stretchr/testify v1.7.0 ) diff --git a/client/go/vespa/crypto.go b/client/go/vespa/crypto.go index fd28a95b3c4..a6f52aa77e2 100644 --- a/client/go/vespa/crypto.go +++ b/client/go/vespa/crypto.go @@ -88,6 +88,19 @@ func CreateKeyPair() (PemKeyPair, error) { return PemKeyPair{Certificate: pemCertificate, PrivateKey: pemPrivateKey}, nil } +// CreateAPIKey creates a EC private key encoded as PEM +func CreateAPIKey() ([]byte, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, fmt.Errorf("failed to generate private key: %w", err) + } + privateKeyDER, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return nil, err + } + return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKeyDER}), nil +} + type RequestSigner struct { now func() time.Time rnd io.Reader diff --git a/client/go/vespa/crypto_test.go b/client/go/vespa/crypto_test.go index 693da04f70b..00be4298392 100644 --- a/client/go/vespa/crypto_test.go +++ b/client/go/vespa/crypto_test.go @@ -1,12 +1,7 @@ package vespa import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/x509" "encoding/base64" - "encoding/pem" - "io" "math/rand" "net/http" "strings" @@ -26,7 +21,10 @@ func TestCreateKeyPair(t *testing.T) { func TestSignRequest(t *testing.T) { fixedTime := time.Unix(0, 0) rnd := rand.New(rand.NewSource(0)) // Fixed seed for testing purposes - privateKey := pemECPrivateKey(t, rnd) + privateKey, err := CreateAPIKey() + if err != nil { + t.Fatal(err) + } rs := RequestSigner{ now: func() time.Time { return fixedTime }, rnd: rnd, @@ -54,15 +52,3 @@ func TestSignRequest(t *testing.T) { _, err = base64.StdEncoding.DecodeString(auth) assert.Nil(t, err) } - -func pemECPrivateKey(t *testing.T, rnd io.Reader) []byte { - key, err := ecdsa.GenerateKey(elliptic.P256(), rnd) - if err != nil { - t.Fatal(err) - } - der, err := x509.MarshalECPrivateKey(key) - if err != nil { - t.Fatal(err) - } - return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der}) -} |