mirror of https://github.com/status-im/consul.git
acl: add MaxTokenTTL field to auth methods (#7779)
When set to a non zero value it will limit the ExpirationTime of all tokens created via the auth method.
This commit is contained in:
parent
08b335d8d6
commit
22eb016153
|
@ -2111,6 +2111,16 @@ func (a *ACL) AuthMethodSet(args *structs.ACLAuthMethodSetRequest, reply *struct
|
|||
return fmt.Errorf("Invalid Auth Method: Type should be one of: %v", authmethod.Types())
|
||||
}
|
||||
|
||||
if method.MaxTokenTTL != 0 {
|
||||
if method.MaxTokenTTL > a.srv.config.ACLTokenMaxExpirationTTL {
|
||||
return fmt.Errorf("MaxTokenTTL %s cannot be more than %s",
|
||||
method.MaxTokenTTL, a.srv.config.ACLTokenMaxExpirationTTL)
|
||||
} else if method.MaxTokenTTL < a.srv.config.ACLTokenMinExpirationTTL {
|
||||
return fmt.Errorf("MaxTokenTTL %s cannot be less than %s",
|
||||
method.MaxTokenTTL, a.srv.config.ACLTokenMinExpirationTTL)
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate a validator but do not cache it yet. This will validate the
|
||||
// configuration.
|
||||
if _, err := authmethod.NewValidator(a.srv.logger, method); err != nil {
|
||||
|
@ -2323,6 +2333,7 @@ func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLToken) erro
|
|||
AuthMethod: auth.AuthMethod,
|
||||
ServiceIdentities: serviceIdentities,
|
||||
Roles: roleLinks,
|
||||
ExpirationTTL: method.MaxTokenTTL,
|
||||
EnterpriseMeta: *targetMeta,
|
||||
},
|
||||
WriteRequest: args.WriteRequest,
|
||||
|
|
|
@ -3468,6 +3468,89 @@ func TestACLEndpoint_AuthMethodSet(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("Create with MaxTokenTTL", func(t *testing.T) {
|
||||
reqMethod := newAuthMethod("test")
|
||||
reqMethod.MaxTokenTTL = 5 * time.Minute
|
||||
|
||||
req := structs.ACLAuthMethodSetRequest{
|
||||
Datacenter: "dc1",
|
||||
AuthMethod: reqMethod,
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
resp := structs.ACLAuthMethod{}
|
||||
|
||||
err := acl.AuthMethodSet(&req, &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the method directly to validate that it exists
|
||||
methodResp, err := retrieveTestAuthMethod(codec, "root", "dc1", resp.Name)
|
||||
require.NoError(t, err)
|
||||
method := methodResp.AuthMethod
|
||||
|
||||
require.Equal(t, method.Name, "test")
|
||||
require.Equal(t, method.Description, "test")
|
||||
require.Equal(t, method.Type, "testing")
|
||||
require.Equal(t, method.MaxTokenTTL, 5*time.Minute)
|
||||
})
|
||||
|
||||
t.Run("Update - change MaxTokenTTL", func(t *testing.T) {
|
||||
reqMethod := newAuthMethod("test")
|
||||
reqMethod.DisplayName = "updated display name 2"
|
||||
reqMethod.Description = "test modified 2"
|
||||
reqMethod.MaxTokenTTL = 8 * time.Minute
|
||||
|
||||
req := structs.ACLAuthMethodSetRequest{
|
||||
Datacenter: "dc1",
|
||||
AuthMethod: reqMethod,
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
resp := structs.ACLAuthMethod{}
|
||||
|
||||
err := acl.AuthMethodSet(&req, &resp)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Get the method directly to validate that it exists
|
||||
methodResp, err := retrieveTestAuthMethod(codec, "root", "dc1", resp.Name)
|
||||
require.NoError(t, err)
|
||||
method := methodResp.AuthMethod
|
||||
|
||||
require.Equal(t, method.Name, "test")
|
||||
require.Equal(t, method.DisplayName, "updated display name 2")
|
||||
require.Equal(t, method.Description, "test modified 2")
|
||||
require.Equal(t, method.Type, "testing")
|
||||
require.Equal(t, method.MaxTokenTTL, 8*time.Minute)
|
||||
})
|
||||
|
||||
t.Run("Create with MaxTokenTTL too small", func(t *testing.T) {
|
||||
reqMethod := newAuthMethod("test")
|
||||
reqMethod.MaxTokenTTL = 1 * time.Millisecond
|
||||
|
||||
req := structs.ACLAuthMethodSetRequest{
|
||||
Datacenter: "dc1",
|
||||
AuthMethod: reqMethod,
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
resp := structs.ACLAuthMethod{}
|
||||
|
||||
err := acl.AuthMethodSet(&req, &resp)
|
||||
testutil.RequireErrorContains(t, err, "MaxTokenTTL 1ms cannot be less than")
|
||||
})
|
||||
|
||||
t.Run("Create with MaxTokenTTL too big", func(t *testing.T) {
|
||||
reqMethod := newAuthMethod("test")
|
||||
reqMethod.MaxTokenTTL = 25 * time.Hour
|
||||
|
||||
req := structs.ACLAuthMethodSetRequest{
|
||||
Datacenter: "dc1",
|
||||
AuthMethod: reqMethod,
|
||||
WriteRequest: structs.WriteRequest{Token: "root"},
|
||||
}
|
||||
resp := structs.ACLAuthMethod{}
|
||||
|
||||
err := acl.AuthMethodSet(&req, &resp)
|
||||
testutil.RequireErrorContains(t, err, "MaxTokenTTL 25h0m0s cannot be more than")
|
||||
})
|
||||
}
|
||||
|
||||
func TestACLEndpoint_AuthMethodDelete(t *testing.T) {
|
||||
|
@ -4942,6 +5025,81 @@ func TestACLEndpoint_Login(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestACLEndpoint_Login_with_MaxTokenTTL(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",
|
||||
)
|
||||
|
||||
method, err := upsertTestCustomizedAuthMethod(codec, "root", "dc1", func(method *structs.ACLAuthMethod) {
|
||||
method.MaxTokenTTL = 5 * time.Minute
|
||||
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))
|
||||
|
||||
got := &resp
|
||||
got.CreateIndex = 0
|
||||
got.ModifyIndex = 0
|
||||
got.AccessorID = ""
|
||||
got.SecretID = ""
|
||||
got.Hash = nil
|
||||
|
||||
expect := &structs.ACLToken{
|
||||
AuthMethod: method.Name,
|
||||
Description: `token created via login: {"pod":"pod1"}`,
|
||||
Local: true,
|
||||
CreateTime: got.CreateTime,
|
||||
ExpirationTime: timePointer(got.CreateTime.Add(method.MaxTokenTTL)),
|
||||
ServiceIdentities: []*structs.ACLServiceIdentity{
|
||||
{ServiceName: "web"},
|
||||
},
|
||||
}
|
||||
require.Equal(t, got, expect)
|
||||
}
|
||||
|
||||
func TestACLEndpoint_Login_k8s(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package structs
|
|||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
@ -1048,6 +1049,9 @@ type ACLAuthMethod struct {
|
|||
// Description is just an optional bunch of explanatory text.
|
||||
Description string `json:",omitempty"`
|
||||
|
||||
// MaxTokenTTL this is the maximum life of a token created by this method.
|
||||
MaxTokenTTL time.Duration `json:",omitempty"`
|
||||
|
||||
// Configuration is arbitrary configuration for the auth method. This
|
||||
// should only contain primitive values and containers (such as lists and
|
||||
// maps).
|
||||
|
@ -1060,6 +1064,47 @@ type ACLAuthMethod struct {
|
|||
RaftIndex `hash:"ignore"`
|
||||
}
|
||||
|
||||
func (m *ACLAuthMethod) MarshalJSON() ([]byte, error) {
|
||||
type Alias ACLAuthMethod
|
||||
exported := &struct {
|
||||
MaxTokenTTL string `json:",omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
MaxTokenTTL: m.MaxTokenTTL.String(),
|
||||
Alias: (*Alias)(m),
|
||||
}
|
||||
if m.MaxTokenTTL == 0 {
|
||||
exported.MaxTokenTTL = ""
|
||||
}
|
||||
|
||||
return json.Marshal(exported)
|
||||
}
|
||||
|
||||
func (m *ACLAuthMethod) UnmarshalJSON(data []byte) (err error) {
|
||||
type Alias ACLAuthMethod
|
||||
aux := &struct {
|
||||
MaxTokenTTL interface{}
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(m),
|
||||
}
|
||||
if err = lib.UnmarshalJSON(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
if aux.MaxTokenTTL != nil {
|
||||
switch v := aux.MaxTokenTTL.(type) {
|
||||
case string:
|
||||
if m.MaxTokenTTL, err = time.ParseDuration(v); err != nil {
|
||||
return err
|
||||
}
|
||||
case float64:
|
||||
m.MaxTokenTTL = time.Duration(v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ACLReplicationType string
|
||||
|
||||
const (
|
||||
|
|
39
api/acl.go
39
api/acl.go
|
@ -1,6 +1,7 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
@ -184,6 +185,7 @@ type ACLAuthMethod struct {
|
|||
Type string
|
||||
DisplayName string `json:",omitempty"`
|
||||
Description string `json:",omitempty"`
|
||||
MaxTokenTTL time.Duration `json:",omitempty"`
|
||||
|
||||
// Configuration is arbitrary configuration for the auth method. This
|
||||
// should only contain primitive values and containers (such as lists and
|
||||
|
@ -198,6 +200,43 @@ type ACLAuthMethod struct {
|
|||
Namespace string `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (m *ACLAuthMethod) MarshalJSON() ([]byte, error) {
|
||||
type Alias ACLAuthMethod
|
||||
exported := &struct {
|
||||
MaxTokenTTL string `json:",omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
MaxTokenTTL: m.MaxTokenTTL.String(),
|
||||
Alias: (*Alias)(m),
|
||||
}
|
||||
if m.MaxTokenTTL == 0 {
|
||||
exported.MaxTokenTTL = ""
|
||||
}
|
||||
|
||||
return json.Marshal(exported)
|
||||
}
|
||||
|
||||
func (m *ACLAuthMethod) UnmarshalJSON(data []byte) error {
|
||||
type Alias ACLAuthMethod
|
||||
aux := &struct {
|
||||
MaxTokenTTL string
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(m),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
var err error
|
||||
if aux.MaxTokenTTL != "" {
|
||||
if m.MaxTokenTTL, err = time.ParseDuration(aux.MaxTokenTTL); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type ACLAuthMethodListEntry struct {
|
||||
Name string
|
||||
Type string
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/command/acl/authmethod"
|
||||
|
@ -30,11 +31,12 @@ type cmd struct {
|
|||
name string
|
||||
displayName string
|
||||
description string
|
||||
maxTokenTTL time.Duration
|
||||
config string
|
||||
|
||||
k8sHost string
|
||||
k8sCACert string
|
||||
k8sServiceAccountJWT string
|
||||
config string
|
||||
|
||||
showMeta bool
|
||||
format string
|
||||
|
@ -77,6 +79,12 @@ func (c *cmd) init() {
|
|||
"",
|
||||
"A description of the auth method.",
|
||||
)
|
||||
c.flags.DurationVar(
|
||||
&c.maxTokenTTL,
|
||||
"max-token-ttl",
|
||||
0,
|
||||
"Duration of time all tokens created by this auth method should be valid for",
|
||||
)
|
||||
|
||||
c.flags.StringVar(
|
||||
&c.k8sHost,
|
||||
|
@ -150,6 +158,9 @@ func (c *cmd) Run(args []string) int {
|
|||
DisplayName: c.displayName,
|
||||
Description: c.description,
|
||||
}
|
||||
if c.maxTokenTTL > 0 {
|
||||
newAuthMethod.MaxTokenTTL = c.maxTokenTTL
|
||||
}
|
||||
|
||||
if c.config != "" {
|
||||
if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" {
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/agent"
|
||||
"github.com/hashicorp/consul/agent/connect"
|
||||
|
@ -121,6 +122,35 @@ func TestAuthMethodCreateCommand(t *testing.T) {
|
|||
}
|
||||
require.Equal(t, expect, got)
|
||||
})
|
||||
|
||||
t.Run("create testing with max token ttl", func(t *testing.T) {
|
||||
args := []string{
|
||||
"-http-addr=" + a.HTTPAddr(),
|
||||
"-token=root",
|
||||
"-type=testing",
|
||||
"-name=test",
|
||||
"-description=desc",
|
||||
"-display-name=display",
|
||||
"-max-token-ttl=5m",
|
||||
}
|
||||
|
||||
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, "test")
|
||||
expect := &api.ACLAuthMethod{
|
||||
Name: "test",
|
||||
Type: "testing",
|
||||
DisplayName: "display",
|
||||
Description: "desc",
|
||||
MaxTokenTTL: 5 * time.Minute,
|
||||
}
|
||||
require.Equal(t, expect, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthMethodCreateCommand_JSON(t *testing.T) {
|
||||
|
@ -190,6 +220,53 @@ func TestAuthMethodCreateCommand_JSON(t *testing.T) {
|
|||
}
|
||||
require.Equal(t, expect, got)
|
||||
})
|
||||
|
||||
t.Run("create testing with max token ttl", func(t *testing.T) {
|
||||
args := []string{
|
||||
"-http-addr=" + a.HTTPAddr(),
|
||||
"-token=root",
|
||||
"-type=testing",
|
||||
"-name=test",
|
||||
"-description=desc",
|
||||
"-display-name=display",
|
||||
"-max-token-ttl=5m",
|
||||
"-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, "test")
|
||||
|
||||
got := getTestMethod(t, client, "test")
|
||||
expect := &api.ACLAuthMethod{
|
||||
Name: "test",
|
||||
Type: "testing",
|
||||
DisplayName: "display",
|
||||
Description: "desc",
|
||||
MaxTokenTTL: 5 * time.Minute,
|
||||
}
|
||||
require.Equal(t, expect, got)
|
||||
|
||||
var raw map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal([]byte(out), &raw))
|
||||
delete(raw, "CreateIndex")
|
||||
delete(raw, "ModifyIndex")
|
||||
|
||||
require.Equal(t, map[string]interface{}{
|
||||
"Name": "test",
|
||||
"Type": "testing",
|
||||
"DisplayName": "display",
|
||||
"Description": "desc",
|
||||
"MaxTokenTTL": "5m0s",
|
||||
"Config": nil,
|
||||
}, raw)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthMethodCreateCommand_k8s(t *testing.T) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/consul/api"
|
||||
"github.com/hashicorp/consul/command/acl/authmethod"
|
||||
|
@ -30,7 +31,9 @@ type cmd struct {
|
|||
|
||||
displayName string
|
||||
description string
|
||||
maxTokenTTL time.Duration
|
||||
config string
|
||||
|
||||
k8sHost string
|
||||
k8sCACert string
|
||||
k8sServiceAccountJWT string
|
||||
|
@ -74,6 +77,13 @@ func (c *cmd) init() {
|
|||
"A description of the auth method.",
|
||||
)
|
||||
|
||||
c.flags.DurationVar(
|
||||
&c.maxTokenTTL,
|
||||
"max-token-ttl",
|
||||
0,
|
||||
"Duration of time all tokens created by this auth method should be valid for",
|
||||
)
|
||||
|
||||
c.flags.StringVar(
|
||||
&c.config,
|
||||
"config",
|
||||
|
@ -169,6 +179,10 @@ func (c *cmd) Run(args []string) int {
|
|||
DisplayName: c.displayName,
|
||||
Description: c.description,
|
||||
}
|
||||
if c.maxTokenTTL > 0 {
|
||||
method.MaxTokenTTL = c.maxTokenTTL
|
||||
}
|
||||
|
||||
if c.config != "" {
|
||||
if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" {
|
||||
c.UI.Error(fmt.Sprintf("Cannot use command line arguments with '-config' flag"))
|
||||
|
@ -184,6 +198,7 @@ func (c *cmd) Run(args []string) int {
|
|||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
if currentAuthMethod.Type == "kubernetes" {
|
||||
if c.k8sHost == "" {
|
||||
c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-host' flag"))
|
||||
|
@ -211,6 +226,9 @@ func (c *cmd) Run(args []string) int {
|
|||
if c.displayName != "" {
|
||||
method.DisplayName = c.displayName
|
||||
}
|
||||
if c.maxTokenTTL > 0 {
|
||||
method.MaxTokenTTL = c.maxTokenTTL
|
||||
}
|
||||
if c.config != "" {
|
||||
if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" {
|
||||
c.UI.Error(fmt.Sprintf("Cannot use command line arguments with '-config' flag"))
|
||||
|
|
|
@ -51,6 +51,14 @@ The table below shows this endpoint's support for
|
|||
- `DisplayName` `(string: "")` - An optional name to use instead of the `Name`
|
||||
field when displaying information about this auth method. Added in Consul 1.8.0.
|
||||
|
||||
- `MaxTokenTTL` `(duration: 0s)` - This specifies the maximum life of any token
|
||||
created by this auth method. When set it will initialize the
|
||||
[`ExpirationTime`](/api/acl/tokens.html#expirationtime) field on all tokens
|
||||
to a value of `Token.CreateTime + AuthMethod.MaxTokenTTL`. This field is not
|
||||
persisted beyond its initial use. Can be specified in the form of `"60s"` or
|
||||
`"5m"` (i.e., 60 seconds or 5 minutes, respectively). This value must be no
|
||||
smaller than 1 minute and no longer than 24 hours. 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
|
||||
|
@ -191,6 +199,14 @@ The table below shows this endpoint's support for
|
|||
- `DisplayName` `(string: "")` - An optional name to use instead of the `Name`
|
||||
field when displaying information about this auth method. Added in Consul 1.8.0.
|
||||
|
||||
- `MaxTokenTTL` `(duration: 0s)` - This specifies the maximum life of any token
|
||||
created by this auth method. When set it will initialize the
|
||||
[`ExpirationTime`](/api/acl/tokens.html#expirationtime) field on all tokens
|
||||
to a value of `Token.CreateTime + AuthMethod.MaxTokenTTL`. This field is not
|
||||
persisted beyond its initial use. Can be specified in the form of `"60s"` or
|
||||
`"5m"` (i.e., 60 seconds or 5 minutes, respectively). This value must be no
|
||||
smaller than 1 minute and no longer than 24 hours. 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
|
||||
|
|
Loading…
Reference in New Issue