From f9956c1c461e4d0903df33bf5bb9bfd479dc21cf Mon Sep 17 00:00:00 2001 From: s-christoff Date: Mon, 4 May 2020 16:21:28 -0500 Subject: [PATCH] cli: Add -config flag to "acl authmethod update/create" (#7776) --- .../authmethod/create/authmethod_create.go | 28 ++++ .../create/authmethod_create_test.go | 94 +++++++++++- .../authmethod/update/authmethod_update.go | 53 ++++++- .../update/authmethod_update_test.go | 134 ++++++++++++++++++ 4 files changed, 301 insertions(+), 8 deletions(-) diff --git a/command/acl/authmethod/create/authmethod_create.go b/command/acl/authmethod/create/authmethod_create.go index b4151f166f..507667cfec 100644 --- a/command/acl/authmethod/create/authmethod_create.go +++ b/command/acl/authmethod/create/authmethod_create.go @@ -1,6 +1,7 @@ package authmethodcreate import ( + "encoding/json" "flag" "fmt" "io" @@ -33,6 +34,7 @@ type cmd struct { k8sHost string k8sCACert string k8sServiceAccountJWT string + config string showMeta bool format string @@ -105,6 +107,14 @@ func (c *cmd) init() { authmethod.PrettyFormat, fmt.Sprintf("Output format {%s}", strings.Join(authmethod.GetSupportedFormats(), "|")), ) + c.flags.StringVar( + &c.config, + "config", + "", + "The configuration for the auth method. Must be JSON. May be prefixed with '@' "+ + "to indicate that the value is a file path to load the config from. '-' may also be "+ + "given to indicate that the config is available on stdin", + ) c.http = &flags.HTTPFlags{} flags.Merge(c.flags, c.http.ClientFlags()) @@ -141,6 +151,24 @@ func (c *cmd) Run(args []string) int { Description: c.description, } + if c.config != "" { + if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" { + c.UI.Error(fmt.Sprintf("Cannot use command line arguments with '-config' flags")) + return 1 + } + data, err := helpers.LoadDataSource(c.config, c.testStdin) + if err != nil { + c.UI.Error(fmt.Sprintf("Error loading configuration file: %v", err)) + return 1 + } + err = json.Unmarshal([]byte(data), &newAuthMethod.Config) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing JSON configuration file: %v", err)) + return 1 + } + + } + if c.authMethodType == "kubernetes" { if c.k8sHost == "" { c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-host' flag")) diff --git a/command/acl/authmethod/create/authmethod_create_test.go b/command/acl/authmethod/create/authmethod_create_test.go index 6ffd6dc155..d48af64b92 100644 --- a/command/acl/authmethod/create/authmethod_create_test.go +++ b/command/acl/authmethod/create/authmethod_create_test.go @@ -2,6 +2,7 @@ package authmethodcreate import ( "encoding/json" + "io" "io/ioutil" "os" "path/filepath" @@ -282,7 +283,7 @@ func TestAuthMethodCreateCommand_k8s(t *testing.T) { cmd := New(ui) code := cmd.Run(args) - require.Equal(t, code, 0) + require.Equal(t, 0, code) require.Empty(t, ui.ErrorWriter.String()) got := getTestMethod(t, client, name) @@ -334,6 +335,96 @@ func TestAuthMethodCreateCommand_k8s(t *testing.T) { }) } +func TestAuthMethodCreateCommand_config(t *testing.T) { + t.Parallel() + + testDir := testutil.TempDir(t, "auth-method") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + client := a.Client() + + checkMethod := func(t *testing.T, methodName string) { + + method, _, err := client.ACL().AuthMethodRead( + methodName, + &api.QueryOptions{Token: "root"}, + ) + require.NoError(t, err) + require.NotNil(t, method) + require.Equal(t, "foo", method.Config["SessionID"]) + } + + t.Run("config file", func(t *testing.T) { + configFile := filepath.Join(testDir, "config.json") + jsonConfig := `{"SessionID":"foo"}` + require.NoError(t, ioutil.WriteFile(configFile, []byte(jsonConfig), 0644)) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-type=testing", + "-name=test", + "-config=@" + configFile, + } + ui := cli.NewMockUi() + cmd := New(ui) + code := cmd.Run(args) + require.Equal(t, 0, code) + require.Empty(t, ui.ErrorWriter.String()) + checkMethod(t, "test") + }) + + t.Run("config std-in", func(t *testing.T) { + stdinR, stdinW := io.Pipe() + ui := cli.NewMockUi() + cmd := New(ui) + cmd.testStdin = stdinR + go func() { + stdinW.Write([]byte(`{"SessionID":"foo"}`)) + stdinW.Close() + }() + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-type=testing", + "-name=test2", + "-config=-", + } + code := cmd.Run(args) + require.Equal(t, 0, code) + require.Empty(t, ui.ErrorWriter.String()) + checkMethod(t, "test2") + + }) + t.Run("config string", func(t *testing.T) { + ui := cli.NewMockUi() + cmd := New(ui) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-type=testing", + "-name=test3", + "-config=" + `{"SessionID":"foo"}`, + } + code := cmd.Run(args) + require.Equal(t, 0, code) + require.Empty(t, ui.ErrorWriter.String()) + checkMethod(t, "test3") + }) +} + func getTestMethod(t *testing.T, client *api.Client, methodName string) *api.ACLAuthMethod { t.Helper() @@ -357,4 +448,5 @@ func getTestName(t *testing.T) string { id, err := uuid.GenerateUUID() require.NoError(t, err) return "test-" + id + } diff --git a/command/acl/authmethod/update/authmethod_update.go b/command/acl/authmethod/update/authmethod_update.go index 483d1df7e7..4fe25cb995 100644 --- a/command/acl/authmethod/update/authmethod_update.go +++ b/command/acl/authmethod/update/authmethod_update.go @@ -1,6 +1,7 @@ package authmethodupdate import ( + "encoding/json" "flag" "fmt" "io" @@ -27,9 +28,9 @@ type cmd struct { name string - displayName string - description string - + displayName string + description string + config string k8sHost string k8sCACert string k8sServiceAccountJWT string @@ -73,6 +74,15 @@ func (c *cmd) init() { "A description of the auth method.", ) + c.flags.StringVar( + &c.config, + "config", + "", + "The configuration for the auth method. Must be JSON. The config is updated as one field"+ + "May be prefixed with '@' to indicate that the value is a file path to load the config from. "+ + "'-' may also be given to indicate that the config are available on stdin. ", + ) + c.flags.StringVar( &c.k8sHost, "kubernetes-host", @@ -100,6 +110,7 @@ func (c *cmd) init() { c.flags.BoolVar(&c.noMerge, "no-merge", false, "Do not merge the current auth method "+ "information with what is provided to the command. Instead overwrite all fields "+ "with the exception of the name which is immutable.") + c.flags.StringVar( &c.format, "format", @@ -158,7 +169,21 @@ func (c *cmd) Run(args []string) int { DisplayName: c.displayName, Description: c.description, } - + if c.config != "" { + if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" { + c.UI.Error(fmt.Sprintf("Cannot use command line arguments with '-config' flag")) + return 1 + } + data, err := helpers.LoadDataSource(c.config, c.testStdin) + if err != nil { + c.UI.Error(fmt.Sprintf("Error loading configuration file: %v", err)) + return 1 + } + if err := json.Unmarshal([]byte(data), &method.Config); err != nil { + c.UI.Error(fmt.Sprintf("Error parsing JSON for auth method config: %v", err)) + return 1 + } + } if currentAuthMethod.Type == "kubernetes" { if c.k8sHost == "" { c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-host' flag")) @@ -180,12 +205,26 @@ func (c *cmd) Run(args []string) int { } else { methodCopy := *currentAuthMethod method = &methodCopy - + if c.description != "" { + method.Description = c.description + } if c.displayName != "" { method.DisplayName = c.displayName } - if c.description != "" { - method.Description = c.description + if c.config != "" { + if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" { + c.UI.Error(fmt.Sprintf("Cannot use command line arguments with '-config' flag")) + return 1 + } + data, err := helpers.LoadDataSource(c.config, c.testStdin) + if err != nil { + c.UI.Error(fmt.Sprintf("Error loading configuration file: %v", err)) + return 1 + } + if err := json.Unmarshal([]byte(data), &method.Config); err != nil { + c.UI.Error(fmt.Sprintf("Error parsing JSON for auth method config: %v", err)) + return 1 + } } if method.Config == nil { method.Config = make(map[string]interface{}) diff --git a/command/acl/authmethod/update/authmethod_update_test.go b/command/acl/authmethod/update/authmethod_update_test.go index a8435d3c4c..88aa5dfa8d 100644 --- a/command/acl/authmethod/update/authmethod_update_test.go +++ b/command/acl/authmethod/update/authmethod_update_test.go @@ -2,6 +2,7 @@ package authmethodupdate import ( "encoding/json" + "io" "io/ioutil" "os" "path/filepath" @@ -743,6 +744,138 @@ func TestAuthMethodUpdateCommand_k8s_noMerge(t *testing.T) { }) } +func TestAuthMethodUpdateCommand_config(t *testing.T) { + t.Parallel() + testDir := testutil.TempDir(t, "auth-method") + defer os.RemoveAll(testDir) + + a := agent.NewTestAgent(t, ` + primary_datacenter = "dc1" + acl { + enabled = true + tokens { + master = "root" + } + }`) + + defer a.Shutdown() + testrpc.WaitForLeader(t, a.RPC, "dc1") + + client := a.Client() + + createAuthMethod := func(t *testing.T) string { + id, err := uuid.GenerateUUID() + require.NoError(t, err) + + methodName := "test" + id + + _, _, err = client.ACL().AuthMethodCreate( + &api.ACLAuthMethod{ + Name: methodName, + Type: "testing", + Description: "test", + Config: map[string]interface{}{ + "SessionID": "big", + }, + }, + &api.WriteOptions{Token: "root"}, + ) + require.NoError(t, err) + + return methodName + } + + readUpdate := func(t *testing.T, methodName string) { + + method, _, err := client.ACL().AuthMethodRead( + methodName, + &api.QueryOptions{Token: "root"}, + ) + require.NoError(t, err) + require.NotNil(t, method) + require.Equal(t, "update", method.Config["SessionID"]) + } + + t.Run("config file", func(t *testing.T) { + methodName := createAuthMethod(t) + configFile := filepath.Join(testDir, "config.json") + jsonConfig := `{"SessionID":"update"}` + require.NoError(t, ioutil.WriteFile(configFile, []byte(jsonConfig), 0644)) + + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=" + methodName, + "-no-merge=true", + "-config=@" + configFile, + } + ui := cli.NewMockUi() + cmd := New(ui) + code := cmd.Run(args) + require.Equal(t, 0, code) + require.Empty(t, ui.ErrorWriter.String()) + readUpdate(t, methodName) + }) + + t.Run("config stdin", func(t *testing.T) { + methodName := createAuthMethod(t) + ui := cli.NewMockUi() + cmd := New(ui) + stdinR, stdinW := io.Pipe() + cmd.testStdin = stdinR + + go func() { + stdinW.Write([]byte(`{"SessionID":"update"}`)) + stdinW.Close() + }() + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=" + methodName, + "-no-merge=true", + "-config=-", + } + + code := cmd.Run(args) + require.Equal(t, 0, code) + require.Empty(t, ui.ErrorWriter.String()) + readUpdate(t, methodName) + }) + + t.Run("config string", func(t *testing.T) { + methodName := createAuthMethod(t) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=" + methodName, + "-no-merge=true", + "-config=" + `{"SessionID":"update"}`, + } + ui := cli.NewMockUi() + cmd := New(ui) + code := cmd.Run(args) + require.Equal(t, 0, code) + require.Empty(t, ui.ErrorWriter.String()) + readUpdate(t, methodName) + }) + t.Run("config with no merge", func(t *testing.T) { + methodName := createAuthMethod(t) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-name=" + methodName, + "-no-merge=false", + "-config=" + `{"SessionID":"update"}`, + } + ui := cli.NewMockUi() + cmd := New(ui) + code := cmd.Run(args) + require.Equal(t, 0, code) + require.Empty(t, ui.ErrorWriter.String()) + readUpdate(t, methodName) + }) +} + func getTestMethod(t *testing.T, client *api.Client, methodName string) *api.ACLAuthMethod { t.Helper() @@ -766,4 +899,5 @@ func getTestName(t *testing.T) string { id, err := uuid.GenerateUUID() require.NoError(t, err) return "test-" + id + }