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:
R.B. Boyer 2020-05-04 17:02:57 -05:00 committed by GitHub
parent 08b335d8d6
commit 22eb016153
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 381 additions and 6 deletions

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package api
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
@ -182,8 +183,9 @@ type ACLBindingRule struct {
type ACLAuthMethod struct {
Name string
Type string
DisplayName string `json:",omitempty"`
Description string `json:",omitempty"`
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

View File

@ -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 != "" {

View File

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

View File

@ -6,6 +6,7 @@ import (
"fmt"
"io"
"strings"
"time"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl/authmethod"
@ -28,9 +29,11 @@ type cmd struct {
name string
displayName string
description string
config string
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"))

View File

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