mirror of https://github.com/status-im/consul.git
acl: allow auth methods created in the primary datacenter to optionally create global tokens (#7899)
This commit is contained in:
parent
a78aa80d25
commit
833211c14c
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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, "", " ")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue