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()) 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 // Instantiate a validator but do not cache it yet. This will validate the
// configuration. // configuration.
if _, err := authmethod.NewValidator(a.srv.logger, method); err != nil { 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, AuthMethod: auth.AuthMethod,
ServiceIdentities: serviceIdentities, ServiceIdentities: serviceIdentities,
Roles: roleLinks, Roles: roleLinks,
ExpirationTTL: method.MaxTokenTTL,
EnterpriseMeta: *targetMeta, EnterpriseMeta: *targetMeta,
}, },
WriteRequest: args.WriteRequest, 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) { 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) { func TestACLEndpoint_Login_k8s(t *testing.T) {
t.Parallel() t.Parallel()

View File

@ -2,6 +2,7 @@ package structs
import ( import (
"encoding/binary" "encoding/binary"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"hash" "hash"
@ -1048,6 +1049,9 @@ type ACLAuthMethod struct {
// Description is just an optional bunch of explanatory text. // Description is just an optional bunch of explanatory text.
Description string `json:",omitempty"` 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 // 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).
@ -1060,6 +1064,47 @@ type ACLAuthMethod struct {
RaftIndex `hash:"ignore"` 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 type ACLReplicationType string
const ( const (

View File

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

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"strings" "strings"
"time"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl/authmethod" "github.com/hashicorp/consul/command/acl/authmethod"
@ -30,11 +31,12 @@ type cmd struct {
name string name string
displayName string displayName string
description string description string
maxTokenTTL time.Duration
config string
k8sHost string k8sHost string
k8sCACert string k8sCACert string
k8sServiceAccountJWT string k8sServiceAccountJWT string
config string
showMeta bool showMeta bool
format string format string
@ -77,6 +79,12 @@ func (c *cmd) init() {
"", "",
"A description of the auth method.", "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.flags.StringVar(
&c.k8sHost, &c.k8sHost,
@ -150,6 +158,9 @@ func (c *cmd) Run(args []string) int {
DisplayName: c.displayName, DisplayName: c.displayName,
Description: c.description, Description: c.description,
} }
if c.maxTokenTTL > 0 {
newAuthMethod.MaxTokenTTL = c.maxTokenTTL
}
if c.config != "" { if c.config != "" {
if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" { if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" {

View File

@ -8,6 +8,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"testing" "testing"
"time"
"github.com/hashicorp/consul/agent" "github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/connect"
@ -121,6 +122,35 @@ func TestAuthMethodCreateCommand(t *testing.T) {
} }
require.Equal(t, expect, got) 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) { func TestAuthMethodCreateCommand_JSON(t *testing.T) {
@ -190,6 +220,53 @@ func TestAuthMethodCreateCommand_JSON(t *testing.T) {
} }
require.Equal(t, expect, got) 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) { func TestAuthMethodCreateCommand_k8s(t *testing.T) {

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io" "io"
"strings" "strings"
"time"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl/authmethod" "github.com/hashicorp/consul/command/acl/authmethod"
@ -28,9 +29,11 @@ type cmd struct {
name string name string
displayName string displayName string
description string description string
config string maxTokenTTL time.Duration
config string
k8sHost string k8sHost string
k8sCACert string k8sCACert string
k8sServiceAccountJWT string k8sServiceAccountJWT string
@ -74,6 +77,13 @@ func (c *cmd) init() {
"A description of the auth method.", "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.flags.StringVar(
&c.config, &c.config,
"config", "config",
@ -169,6 +179,10 @@ func (c *cmd) Run(args []string) int {
DisplayName: c.displayName, DisplayName: c.displayName,
Description: c.description, Description: c.description,
} }
if c.maxTokenTTL > 0 {
method.MaxTokenTTL = c.maxTokenTTL
}
if c.config != "" { if c.config != "" {
if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" { if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" {
c.UI.Error(fmt.Sprintf("Cannot use command line arguments with '-config' flag")) 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 return 1
} }
} }
if currentAuthMethod.Type == "kubernetes" { if currentAuthMethod.Type == "kubernetes" {
if c.k8sHost == "" { if c.k8sHost == "" {
c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-host' flag")) c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-host' flag"))
@ -211,6 +226,9 @@ func (c *cmd) Run(args []string) int {
if c.displayName != "" { if c.displayName != "" {
method.DisplayName = c.displayName method.DisplayName = c.displayName
} }
if c.maxTokenTTL > 0 {
method.MaxTokenTTL = c.maxTokenTTL
}
if c.config != "" { if c.config != "" {
if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" { if c.k8sHost != "" || c.k8sCACert != "" || c.k8sServiceAccountJWT != "" {
c.UI.Error(fmt.Sprintf("Cannot use command line arguments with '-config' flag")) 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` - `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. 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 - `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
@ -191,6 +199,14 @@ The table below shows this endpoint's support for
- `DisplayName` `(string: "")` - An optional name to use instead of the `Name` - `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. 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 - `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