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 GitHub
parent a78aa80d25
commit 833211c14c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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 == "" {
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

View File

@ -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()

View File

@ -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).

View File

@ -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).

View File

@ -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

View File

@ -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) {

View File

@ -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, "", " ")

View File

@ -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

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`.
- `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
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: <required>)` - 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

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
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
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

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
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
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