From 833211c14cfecd64e36ab238ca943ceb97acee37 Mon Sep 17 00:00:00 2001 From: "R.B. Boyer" Date: Mon, 1 Jun 2020 11:44:47 -0500 Subject: [PATCH] acl: allow auth methods created in the primary datacenter to optionally create global tokens (#7899) --- agent/consul/acl_endpoint.go | 20 +++- agent/consul/acl_endpoint_test.go | 102 ++++++++++++++++++ agent/structs/acl.go | 4 + api/acl.go | 4 + .../authmethod/create/authmethod_create.go | 17 ++- .../create/authmethod_create_test.go | 79 ++++++++++++++ command/acl/authmethod/formatter.go | 19 ++-- .../authmethod/update/authmethod_update.go | 28 +++-- website/pages/api-docs/acl/auth-methods.mdx | 8 ++ .../docs/commands/acl/auth-method/create.mdx | 4 + .../docs/commands/acl/auth-method/update.mdx | 4 + 11 files changed, 266 insertions(+), 23 deletions(-) diff --git a/agent/consul/acl_endpoint.go b/agent/consul/acl_endpoint.go index 4a0b44cb2f..f71dad9ce6 100644 --- a/agent/consul/acl_endpoint.go +++ b/agent/consul/acl_endpoint.go @@ -442,9 +442,6 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs. if token.AuthMethod == "" { return fmt.Errorf("AuthMethod field is required during Login") } - if !token.Local { - return fmt.Errorf("Cannot create Global token via Login") - } } else { if token.AuthMethod != "" { return fmt.Errorf("AuthMethod field is disallowed outside of Login") @@ -2128,6 +2125,16 @@ func (a *ACL) AuthMethodSet(args *structs.ACLAuthMethodSetRequest, reply *struct } } + switch method.TokenLocality { + case "local", "": + case "global": + if !a.srv.InACLDatacenter() { + return fmt.Errorf("Invalid Auth Method: TokenLocality 'global' can only be used in the primary datacenter") + } + default: + return fmt.Errorf("Invalid Auth Method: TokenLocality should be one of 'local' or 'global'") + } + // Instantiate a validator but do not cache it yet. This will validate the // configuration. validator, err := authmethod.NewValidator(a.srv.logger, method) @@ -2386,6 +2393,13 @@ func (a *ACL) tokenSetFromAuthMethod( EnterpriseMeta: *targetMeta, } + if method.TokenLocality == "global" { + if !a.srv.InACLDatacenter() { + return errors.New("creating global tokens via auth methods is only permitted in the primary datacenter") + } + createReq.ACLToken.Local = false + } + createReq.ACLToken.ACLAuthMethodEnterpriseMeta.FillWithEnterpriseMeta(entMeta) // 5. return token information like a TokenCreate would diff --git a/agent/consul/acl_endpoint_test.go b/agent/consul/acl_endpoint_test.go index 1471ee10f7..fac6247af5 100644 --- a/agent/consul/acl_endpoint_test.go +++ b/agent/consul/acl_endpoint_test.go @@ -5106,6 +5106,108 @@ func TestACLEndpoint_Login_with_MaxTokenTTL(t *testing.T) { require.Equal(t, got, expect) } +func TestACLEndpoint_Login_with_TokenLocality(t *testing.T) { + t.Parallel() + + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLsEnabled = true + c.ACLMasterToken = "root" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec := rpcClient(t, s1) + defer codec.Close() + + testrpc.WaitForLeader(t, s1.RPC, "dc1") + + acl := ACL{srv: s1} + + testSessionID := testauth.StartSession() + defer testauth.ResetSession(testSessionID) + + testauth.InstallSessionToken( + testSessionID, + "fake-web", // no rules + "default", "web", "abc123", + ) + + cases := map[string]struct { + tokenLocality string + expectLocal bool + }{ + "empty": {tokenLocality: "", expectLocal: true}, + "local": {tokenLocality: "local", expectLocal: true}, + "global": {tokenLocality: "global", expectLocal: false}, + } + + for name, tc := range cases { + tc := tc + t.Run(name, func(t *testing.T) { + method, err := upsertTestCustomizedAuthMethod(codec, "root", "dc1", func(method *structs.ACLAuthMethod) { + method.TokenLocality = tc.tokenLocality + method.Config = map[string]interface{}{ + "SessionID": testSessionID, + } + }) + require.NoError(t, err) + + _, err = upsertTestBindingRule( + codec, "root", "dc1", method.Name, + "", + structs.BindingRuleBindTypeService, + "web", + ) + require.NoError(t, err) + + // Create a token. + req := structs.ACLLoginRequest{ + Auth: &structs.ACLLoginParams{ + AuthMethod: method.Name, + BearerToken: "fake-web", + Meta: map[string]string{"pod": "pod1"}, + }, + Datacenter: "dc1", + } + + resp := structs.ACLToken{} + require.NoError(t, acl.Login(&req, &resp)) + + secretID := resp.SecretID + + got := &resp + got.CreateIndex = 0 + got.ModifyIndex = 0 + got.AccessorID = "" + got.SecretID = "" + got.Hash = nil + + defaultEntMeta := structs.DefaultEnterpriseMeta() + expect := &structs.ACLToken{ + AuthMethod: method.Name, + Description: `token created via login: {"pod":"pod1"}`, + Local: tc.expectLocal, + CreateTime: got.CreateTime, + ServiceIdentities: []*structs.ACLServiceIdentity{ + {ServiceName: "web"}, + }, + EnterpriseMeta: *defaultEntMeta, + } + expect.ACLAuthMethodEnterpriseMeta.FillWithEnterpriseMeta(defaultEntMeta) + require.Equal(t, got, expect) + + // Now turn around and nuke it. + logoutReq := structs.ACLLogoutRequest{ + Datacenter: "dc1", + WriteRequest: structs.WriteRequest{Token: secretID}, + } + + var ignored bool + require.NoError(t, acl.Logout(&logoutReq, &ignored)) + }) + } +} + func TestACLEndpoint_Login_k8s(t *testing.T) { t.Parallel() diff --git a/agent/structs/acl.go b/agent/structs/acl.go index e58287266d..769f66e731 100644 --- a/agent/structs/acl.go +++ b/agent/structs/acl.go @@ -1052,6 +1052,10 @@ type ACLAuthMethod struct { // MaxTokenTTL this is the maximum life of a token created by this method. MaxTokenTTL time.Duration `json:",omitempty"` + // TokenLocality defines the kind of token that this auth method produces. + // This can be either 'local' or 'global'. If empty 'local' is assumed. + TokenLocality string `json:",omitempty"` + // Configuration is arbitrary configuration for the auth method. This // should only contain primitive values and containers (such as lists and // maps). diff --git a/api/acl.go b/api/acl.go index 70b2e65895..618a49d6b4 100644 --- a/api/acl.go +++ b/api/acl.go @@ -187,6 +187,10 @@ type ACLAuthMethod struct { Description string `json:",omitempty"` MaxTokenTTL time.Duration `json:",omitempty"` + // TokenLocality defines the kind of token that this auth method produces. + // This can be either 'local' or 'global'. If empty 'local' is assumed. + TokenLocality string `json:",omitempty"` + // Configuration is arbitrary configuration for the auth method. This // should only contain primitive values and containers (such as lists and // maps). diff --git a/command/acl/authmethod/create/authmethod_create.go b/command/acl/authmethod/create/authmethod_create.go index 7fca8ab6c3..b648b64980 100644 --- a/command/acl/authmethod/create/authmethod_create.go +++ b/command/acl/authmethod/create/authmethod_create.go @@ -32,6 +32,7 @@ type cmd struct { displayName string description string maxTokenTTL time.Duration + tokenLocality string config string k8sHost string @@ -87,6 +88,13 @@ func (c *cmd) init() { 0, "Duration of time all tokens created by this auth method should be valid for", ) + c.flags.StringVar( + &c.tokenLocality, + "token-locality", + "", + "Defines the kind of token that this auth method should produce. "+ + "This can be either 'local' or 'global'. If empty the value of 'local' is assumed.", + ) c.flags.StringVar( &c.k8sHost, @@ -157,10 +165,11 @@ func (c *cmd) Run(args []string) int { } newAuthMethod := &api.ACLAuthMethod{ - Type: c.authMethodType, - Name: c.name, - DisplayName: c.displayName, - Description: c.description, + Type: c.authMethodType, + Name: c.name, + DisplayName: c.displayName, + Description: c.description, + TokenLocality: c.tokenLocality, } if c.maxTokenTTL > 0 { newAuthMethod.MaxTokenTTL = c.maxTokenTTL diff --git a/command/acl/authmethod/create/authmethod_create_test.go b/command/acl/authmethod/create/authmethod_create_test.go index 6f4a07d07c..3a6b929ad5 100644 --- a/command/acl/authmethod/create/authmethod_create_test.go +++ b/command/acl/authmethod/create/authmethod_create_test.go @@ -153,6 +153,36 @@ func TestAuthMethodCreateCommand(t *testing.T) { } require.Equal(t, expect, got) }) + + t.Run("create testing with token type global", func(t *testing.T) { + name := getTestName(t) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-type=testing", + "-name", name, + "-description=desc", + "-display-name=display", + "-token-locality=global", + } + + ui := cli.NewMockUi() + cmd := New(ui) + + code := cmd.Run(args) + require.Equal(t, code, 0, "err: "+ui.ErrorWriter.String()) + require.Empty(t, ui.ErrorWriter.String()) + + got := getTestMethod(t, client, name) + expect := &api.ACLAuthMethod{ + Name: name, + Type: "testing", + DisplayName: "display", + Description: "desc", + TokenLocality: "global", + } + require.Equal(t, expect, got) + }) } func TestAuthMethodCreateCommand_JSON(t *testing.T) { @@ -272,6 +302,55 @@ func TestAuthMethodCreateCommand_JSON(t *testing.T) { "Config": nil, }, raw) }) + + t.Run("create testing with token type global", func(t *testing.T) { + name := getTestName(t) + args := []string{ + "-http-addr=" + a.HTTPAddr(), + "-token=root", + "-type=testing", + "-name", name, + "-description=desc", + "-display-name=display", + "-token-locality=global", + "-format=json", + } + + ui := cli.NewMockUi() + cmd := New(ui) + + code := cmd.Run(args) + out := ui.OutputWriter.String() + + require.Equal(t, code, 0) + require.Empty(t, ui.ErrorWriter.String()) + require.Contains(t, out, name) + + got := getTestMethod(t, client, name) + expect := &api.ACLAuthMethod{ + Name: name, + Type: "testing", + DisplayName: "display", + Description: "desc", + TokenLocality: "global", + } + require.Equal(t, expect, got) + + var raw map[string]interface{} + require.NoError(t, json.Unmarshal([]byte(out), &raw)) + delete(raw, "CreateIndex") + delete(raw, "ModifyIndex") + delete(raw, "Namespace") + + require.Equal(t, map[string]interface{}{ + "Name": name, + "Type": "testing", + "DisplayName": "display", + "Description": "desc", + "TokenLocality": "global", + "Config": nil, + }, raw) + }) } func TestAuthMethodCreateCommand_k8s(t *testing.T) { diff --git a/command/acl/authmethod/formatter.go b/command/acl/authmethod/formatter.go index 139a07f65c..2628f55805 100644 --- a/command/acl/authmethod/formatter.go +++ b/command/acl/authmethod/formatter.go @@ -49,17 +49,20 @@ type prettyFormatter struct { func (f *prettyFormatter) FormatAuthMethod(method *api.ACLAuthMethod) (string, error) { var buffer bytes.Buffer - buffer.WriteString(fmt.Sprintf("Name: %s\n", method.Name)) - buffer.WriteString(fmt.Sprintf("Type: %s\n", method.Type)) + buffer.WriteString(fmt.Sprintf("Name: %s\n", method.Name)) + buffer.WriteString(fmt.Sprintf("Type: %s\n", method.Type)) if method.Namespace != "" { - buffer.WriteString(fmt.Sprintf("Namespace: %s\n", method.Namespace)) + buffer.WriteString(fmt.Sprintf("Namespace: %s\n", method.Namespace)) } if method.DisplayName != "" { - buffer.WriteString(fmt.Sprintf("DisplayName: %s\n", method.DisplayName)) + buffer.WriteString(fmt.Sprintf("DisplayName: %s\n", method.DisplayName)) } - buffer.WriteString(fmt.Sprintf("Description: %s\n", method.Description)) + buffer.WriteString(fmt.Sprintf("Description: %s\n", method.Description)) if method.MaxTokenTTL > 0 { - buffer.WriteString(fmt.Sprintf("MaxTokenTTL: %s\n", method.MaxTokenTTL)) + buffer.WriteString(fmt.Sprintf("MaxTokenTTL: %s\n", method.MaxTokenTTL)) + } + if method.TokenLocality != "" { + buffer.WriteString(fmt.Sprintf("TokenLocality: %s\n", method.TokenLocality)) } if len(method.NamespaceRules) > 0 { buffer.WriteString(fmt.Sprintln("NamespaceRules:")) @@ -69,8 +72,8 @@ func (f *prettyFormatter) FormatAuthMethod(method *api.ACLAuthMethod) (string, e } } if f.showMeta { - buffer.WriteString(fmt.Sprintf("Create Index: %d\n", method.CreateIndex)) - buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", method.ModifyIndex)) + buffer.WriteString(fmt.Sprintf("Create Index: %d\n", method.CreateIndex)) + buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", method.ModifyIndex)) } buffer.WriteString(fmt.Sprintln("Config:")) output, err := json.MarshalIndent(method.Config, "", " ") diff --git a/command/acl/authmethod/update/authmethod_update.go b/command/acl/authmethod/update/authmethod_update.go index c29d696c9f..5b6f98dbf3 100644 --- a/command/acl/authmethod/update/authmethod_update.go +++ b/command/acl/authmethod/update/authmethod_update.go @@ -29,10 +29,11 @@ type cmd struct { name string - displayName string - description string - maxTokenTTL time.Duration - config string + displayName string + description string + maxTokenTTL time.Duration + tokenLocality string + config string k8sHost string k8sCACert string @@ -85,6 +86,13 @@ func (c *cmd) init() { 0, "Duration of time all tokens created by this auth method should be valid for", ) + c.flags.StringVar( + &c.tokenLocality, + "token-locality", + "", + "Defines the kind of token that this auth method should produce. "+ + "This can be either 'local' or 'global'. If empty the value of 'local' is assumed.", + ) c.flags.StringVar( &c.config, @@ -179,10 +187,11 @@ func (c *cmd) Run(args []string) int { var method *api.ACLAuthMethod if c.noMerge { method = &api.ACLAuthMethod{ - Name: currentAuthMethod.Name, - Type: currentAuthMethod.Type, - DisplayName: c.displayName, - Description: c.description, + Name: currentAuthMethod.Name, + Type: currentAuthMethod.Type, + DisplayName: c.displayName, + Description: c.description, + TokenLocality: c.tokenLocality, } if c.maxTokenTTL > 0 { method.MaxTokenTTL = c.maxTokenTTL @@ -239,6 +248,9 @@ func (c *cmd) Run(args []string) int { if c.maxTokenTTL > 0 { method.MaxTokenTTL = c.maxTokenTTL } + if c.tokenLocality != "" { + method.TokenLocality = c.tokenLocality + } if err := c.enterprisePopulateAuthMethod(method); err != nil { c.UI.Error(err.Error()) return 1 diff --git a/website/pages/api-docs/acl/auth-methods.mdx b/website/pages/api-docs/acl/auth-methods.mdx index 3a695dea5d..14845fead7 100644 --- a/website/pages/api-docs/acl/auth-methods.mdx +++ b/website/pages/api-docs/acl/auth-methods.mdx @@ -61,6 +61,10 @@ The table below shows this endpoint's support for This must be set to a nonzero value for `type=oidc`. +- `TokenLocality` `(string: "")` - Defines the kind of token that this auth method + should produce. This can be either `"local"` or `"global"`. If empty the + value of `"local"` is assumed. Added in Consul 1.8.0. + - `Config` `(map[string]string: )` - The raw configuration to use for the chosen auth method. Contents will vary depending upon the type chosen. For more information on configuring specific auth method types, see the [auth @@ -236,6 +240,10 @@ The table below shows this endpoint's support for This must be set to a nonzero value for `type=oidc`. +- `TokenLocality` `(string: "")` - Defines the kind of token that this auth method + should produce. This can be either `"local"` or `"global"`. If empty the + value of `"local"` is assumed. Added in Consul 1.8.0. + - `Config` `(map[string]string: )` - The raw configuration to use for the chosen auth method. Contents will vary depending upon the type chosen. For more information on configuring specific auth method types, see the [auth diff --git a/website/pages/docs/commands/acl/auth-method/create.mdx b/website/pages/docs/commands/acl/auth-method/create.mdx index de957da89a..23aaa52442 100644 --- a/website/pages/docs/commands/acl/auth-method/create.mdx +++ b/website/pages/docs/commands/acl/auth-method/create.mdx @@ -37,6 +37,10 @@ Usage: `consul acl auth-method create [options] [args]` - `-max-token-ttl=` - Duration of time all tokens created by this auth method should be valid for. Added in Consul 1.8.0. +- `-token-locality=` - Defines the kind of token that this auth method + should produce. This can be either 'local' or 'global'. If empty the value of + 'local' is assumed. Added in Consul 1.8.0. + - `-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 diff --git a/website/pages/docs/commands/acl/auth-method/update.mdx b/website/pages/docs/commands/acl/auth-method/update.mdx index 16d2f29181..d1017b1602 100644 --- a/website/pages/docs/commands/acl/auth-method/update.mdx +++ b/website/pages/docs/commands/acl/auth-method/update.mdx @@ -33,6 +33,10 @@ Usage: `consul acl auth-method update [options] [args]` - `-max-token-ttl=` - Duration of time all tokens created by this auth method should be valid for. Added in Consul 1.8.0. +- `-token-locality=` - Defines the kind of token that this auth method + should produce. This can be either 'local' or 'global'. If empty the value of + 'local' is assumed. Added in Consul 1.8.0. + - `-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