acl: allow auth methods created in the primary datacenter to optionally create global tokens (#7899)

This commit is contained in:
R.B. Boyer 2020-06-01 11:44:47 -05:00 committed by hashicorp-ci
parent c4b875cae4
commit 5404155d36
11 changed files with 266 additions and 23 deletions

View File

@ -442,9 +442,6 @@ func (a *ACL) tokenSetInternal(args *structs.ACLTokenSetRequest, reply *structs.
if token.AuthMethod == "" { if token.AuthMethod == "" {
return fmt.Errorf("AuthMethod field is required during Login") return fmt.Errorf("AuthMethod field is required during Login")
} }
if !token.Local {
return fmt.Errorf("Cannot create Global token via Login")
}
} else { } else {
if token.AuthMethod != "" { if token.AuthMethod != "" {
return fmt.Errorf("AuthMethod field is disallowed outside of Login") 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 // Instantiate a validator but do not cache it yet. This will validate the
// configuration. // configuration.
validator, err := authmethod.NewValidator(a.srv.logger, method) validator, err := authmethod.NewValidator(a.srv.logger, method)
@ -2386,6 +2393,13 @@ func (a *ACL) tokenSetFromAuthMethod(
EnterpriseMeta: *targetMeta, 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) createReq.ACLToken.ACLAuthMethodEnterpriseMeta.FillWithEnterpriseMeta(entMeta)
// 5. return token information like a TokenCreate would // 5. return token information like a TokenCreate would

View File

@ -5106,6 +5106,108 @@ func TestACLEndpoint_Login_with_MaxTokenTTL(t *testing.T) {
require.Equal(t, got, expect) 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) { func TestACLEndpoint_Login_k8s(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -1052,6 +1052,10 @@ type ACLAuthMethod struct {
// MaxTokenTTL this is the maximum life of a token created by this method. // MaxTokenTTL this is the maximum life of a token created by this method.
MaxTokenTTL time.Duration `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 // Configuration is arbitrary configuration for the auth method. This
// should only contain primitive values and containers (such as lists and // should only contain primitive values and containers (such as lists and
// maps). // maps).

View File

@ -187,6 +187,10 @@ type ACLAuthMethod struct {
Description string `json:",omitempty"` Description string `json:",omitempty"`
MaxTokenTTL time.Duration `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 // Configuration is arbitrary configuration for the auth method. This
// should only contain primitive values and containers (such as lists and // should only contain primitive values and containers (such as lists and
// maps). // maps).

View File

@ -32,6 +32,7 @@ type cmd struct {
displayName string displayName string
description string description string
maxTokenTTL time.Duration maxTokenTTL time.Duration
tokenLocality string
config string config string
k8sHost string k8sHost string
@ -87,6 +88,13 @@ func (c *cmd) init() {
0, 0,
"Duration of time all tokens created by this auth method should be valid for", "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.flags.StringVar(
&c.k8sHost, &c.k8sHost,
@ -157,10 +165,11 @@ func (c *cmd) Run(args []string) int {
} }
newAuthMethod := &api.ACLAuthMethod{ newAuthMethod := &api.ACLAuthMethod{
Type: c.authMethodType, Type: c.authMethodType,
Name: c.name, Name: c.name,
DisplayName: c.displayName, DisplayName: c.displayName,
Description: c.description, Description: c.description,
TokenLocality: c.tokenLocality,
} }
if c.maxTokenTTL > 0 { if c.maxTokenTTL > 0 {
newAuthMethod.MaxTokenTTL = c.maxTokenTTL newAuthMethod.MaxTokenTTL = c.maxTokenTTL

View File

@ -153,6 +153,36 @@ func TestAuthMethodCreateCommand(t *testing.T) {
} }
require.Equal(t, expect, got) 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) { func TestAuthMethodCreateCommand_JSON(t *testing.T) {
@ -272,6 +302,55 @@ func TestAuthMethodCreateCommand_JSON(t *testing.T) {
"Config": nil, "Config": nil,
}, raw) }, 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) { func TestAuthMethodCreateCommand_k8s(t *testing.T) {

View File

@ -49,17 +49,20 @@ type prettyFormatter struct {
func (f *prettyFormatter) FormatAuthMethod(method *api.ACLAuthMethod) (string, error) { func (f *prettyFormatter) FormatAuthMethod(method *api.ACLAuthMethod) (string, error) {
var buffer bytes.Buffer var buffer bytes.Buffer
buffer.WriteString(fmt.Sprintf("Name: %s\n", method.Name)) buffer.WriteString(fmt.Sprintf("Name: %s\n", method.Name))
buffer.WriteString(fmt.Sprintf("Type: %s\n", method.Type)) buffer.WriteString(fmt.Sprintf("Type: %s\n", method.Type))
if method.Namespace != "" { if method.Namespace != "" {
buffer.WriteString(fmt.Sprintf("Namespace: %s\n", method.Namespace)) buffer.WriteString(fmt.Sprintf("Namespace: %s\n", method.Namespace))
} }
if method.DisplayName != "" { 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 { 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 { if len(method.NamespaceRules) > 0 {
buffer.WriteString(fmt.Sprintln("NamespaceRules:")) buffer.WriteString(fmt.Sprintln("NamespaceRules:"))
@ -69,8 +72,8 @@ func (f *prettyFormatter) FormatAuthMethod(method *api.ACLAuthMethod) (string, e
} }
} }
if f.showMeta { if f.showMeta {
buffer.WriteString(fmt.Sprintf("Create Index: %d\n", method.CreateIndex)) buffer.WriteString(fmt.Sprintf("Create Index: %d\n", method.CreateIndex))
buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", method.ModifyIndex)) buffer.WriteString(fmt.Sprintf("Modify Index: %d\n", method.ModifyIndex))
} }
buffer.WriteString(fmt.Sprintln("Config:")) buffer.WriteString(fmt.Sprintln("Config:"))
output, err := json.MarshalIndent(method.Config, "", " ") output, err := json.MarshalIndent(method.Config, "", " ")

View File

@ -29,10 +29,11 @@ type cmd struct {
name string name string
displayName string displayName string
description string description string
maxTokenTTL time.Duration maxTokenTTL time.Duration
config string tokenLocality string
config string
k8sHost string k8sHost string
k8sCACert string k8sCACert string
@ -85,6 +86,13 @@ func (c *cmd) init() {
0, 0,
"Duration of time all tokens created by this auth method should be valid for", "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.flags.StringVar(
&c.config, &c.config,
@ -179,10 +187,11 @@ func (c *cmd) Run(args []string) int {
var method *api.ACLAuthMethod var method *api.ACLAuthMethod
if c.noMerge { if c.noMerge {
method = &api.ACLAuthMethod{ method = &api.ACLAuthMethod{
Name: currentAuthMethod.Name, Name: currentAuthMethod.Name,
Type: currentAuthMethod.Type, Type: currentAuthMethod.Type,
DisplayName: c.displayName, DisplayName: c.displayName,
Description: c.description, Description: c.description,
TokenLocality: c.tokenLocality,
} }
if c.maxTokenTTL > 0 { if c.maxTokenTTL > 0 {
method.MaxTokenTTL = c.maxTokenTTL method.MaxTokenTTL = c.maxTokenTTL
@ -239,6 +248,9 @@ func (c *cmd) Run(args []string) int {
if c.maxTokenTTL > 0 { if c.maxTokenTTL > 0 {
method.MaxTokenTTL = c.maxTokenTTL method.MaxTokenTTL = c.maxTokenTTL
} }
if c.tokenLocality != "" {
method.TokenLocality = c.tokenLocality
}
if err := c.enterprisePopulateAuthMethod(method); err != nil { if err := c.enterprisePopulateAuthMethod(method); err != nil {
c.UI.Error(err.Error()) c.UI.Error(err.Error())
return 1 return 1

View File

@ -61,6 +61,10 @@ The table below shows this endpoint's support for
This must be set to a nonzero value for `type=oidc`. 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: <required>)` - The raw configuration to use for - `Config` `(map[string]string: <required>)` - The raw configuration to use for
the chosen auth method. Contents will vary depending upon the type chosen. the chosen auth method. Contents will vary depending upon the type chosen.
For more information on configuring specific auth method types, see the [auth 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`. 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: <required>)` - The raw configuration to use for - `Config` `(map[string]string: <required>)` - The raw configuration to use for
the chosen auth method. Contents will vary depending upon the type chosen. the chosen auth method. Contents will vary depending upon the type chosen.
For more information on configuring specific auth method types, see the [auth For more information on configuring specific auth method types, see the [auth

View File

@ -37,6 +37,10 @@ Usage: `consul acl auth-method create [options] [args]`
- `-max-token-ttl=<duration>` - Duration of time all tokens created by this - `-max-token-ttl=<duration>` - Duration of time all tokens created by this
auth method should be valid for. Added in Consul 1.8.0. auth method should be valid for. Added in Consul 1.8.0.
- `-token-locality=<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=<string>` - The configuration for the auth method. Must be JSON. May - `-config=<string>` - 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 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 config from. '-' may also be given to indicate that the config is available on

View File

@ -33,6 +33,10 @@ Usage: `consul acl auth-method update [options] [args]`
- `-max-token-ttl=<duration>` - Duration of time all tokens created by this - `-max-token-ttl=<duration>` - Duration of time all tokens created by this
auth method should be valid for. Added in Consul 1.8.0. auth method should be valid for. Added in Consul 1.8.0.
- `-token-locality=<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=<string>` - The configuration for the auth method. Must be JSON. May - `-config=<string>` - 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 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 config from. '-' may also be given to indicate that the config is available on