Merge branch 'master-oss' into cubbyhole-the-world

This commit is contained in:
Jeff Mitchell 2016-05-11 19:29:52 -04:00
commit ce5614bf9b
6 changed files with 489 additions and 129 deletions

View File

@ -46,6 +46,12 @@ FEATURES:
standby nodes are `standby.vault.service.consul`. Sealed vaults are marked
critical and are not listed by default in Consul's service discovery. See
the documentation for details. [GH-1349]
* **Explicit Maximum Token TTLs using Token Roles**: If using token roles, you
can now set explicit maximum TTLs on tokens that do not honor changes in the
system- or mount-set values. This is useful, for instance, when the max TTL
of the system or the `auth/token` mount must be set high to accommodate
certain needs but you want more granular restrictions on tokens being issued
directly from `auth/token`. [GH-1399]
IMPROVEMENTS:

View File

@ -139,6 +139,7 @@ func TestLogical_StandbyRedirect(t *testing.T) {
"ttl": float64(0),
"creation_ttl": float64(0),
"role": "",
"explicit_max_ttl": float64(0),
},
"warnings": nilWarnings,
"wrap_info": nil,

View File

@ -303,6 +303,7 @@ func TestSysGenerateRoot_Update_OTP(t *testing.T) {
"ttl": float64(0),
"path": "auth/token/root",
"role": "",
"explicit_max_ttl": float64(0),
}
resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self")
@ -385,6 +386,7 @@ func TestSysGenerateRoot_Update_PGP(t *testing.T) {
"ttl": float64(0),
"path": "auth/token/root",
"role": "",
"explicit_max_ttl": float64(0),
}
resp = testHttpGet(t, newRootToken, addr+"/v1/auth/token/lookup-self")

View File

@ -11,8 +11,8 @@ import (
"time"
"github.com/armon/go-metrics"
"github.com/fatih/structs"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/policyutil"
"github.com/hashicorp/vault/helper/salt"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical"
@ -154,6 +154,12 @@ func NewTokenStore(c *Core, config *logical.BackendConfig) (*TokenStore, error)
Default: "",
Description: tokenPathSuffixHelp + pathSuffixSanitize.String(),
},
"explicit_max_ttl": &framework.FieldSchema{
Type: framework.TypeDurationSecond,
Default: 0,
Description: tokenExplicitMaxTTLHelp,
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
@ -418,6 +424,7 @@ type TokenEntry struct {
NumUses int // Used to restrict the number of uses (zero is unlimited). This is to support one-time-tokens (generalized).
CreationTime int64 // Time of token creation
TTL time.Duration // Duration set when token was created
ExplicitMaxTTL time.Duration // Explicit maximum TTL on the token
Role string // If set, the role that was used for parameters at creation time
}
@ -438,8 +445,12 @@ type tsRoleEntry struct {
Period time.Duration `json:"period" mapstructure:"period" structs:"period"`
// If set, a suffix will be set on the token path, making it easier to
// revoke using 'revoke-prefix'.
// revoke using 'revoke-prefix'
PathSuffix string `json:"path_suffix" mapstructure:"path_suffix" structs:"path_suffix"`
// If set, the token entry will have an explicit maximum TTL set, rather
// than deferring to role/mount values
ExplicitMaxTTL time.Duration `json:"explicit_max_ttl" mapstructure:"explicit_max_ttl" structs:"explicit_max_ttl"`
}
// SetExpirationManager is used to provide the token store with
@ -991,8 +1002,12 @@ func (ts *TokenStore) handleCreateCommon(
if len(data.Policies) == 0 {
data.Policies = role.AllowedPolicies
} else {
if !strutil.StrListSubset(role.AllowedPolicies, data.Policies) {
return logical.ErrorResponse("token policies must be subset of the role's allowed policies"), logical.ErrInvalidRequest
// Sanitize passed-in and role policies before comparison
sanitizedInputPolicies := policyutil.SanitizePolicies(data.Policies)
sanitizedRolePolicies := policyutil.SanitizePolicies(role.AllowedPolicies)
if !strutil.StrListSubset(sanitizedRolePolicies, sanitizedInputPolicies) {
return logical.ErrorResponse(fmt.Sprintf("token policies (%v) must be subset of the role's allowed policies (%v)", sanitizedInputPolicies, sanitizedRolePolicies)), logical.ErrInvalidRequest
}
}
@ -1001,9 +1016,15 @@ func (ts *TokenStore) handleCreateCommon(
// When a role is not in use, only permit policies to be a subset unless
// the client has root or sudo privileges
case !isSudo && !strutil.StrListSubset(parent.Policies, data.Policies):
case !isSudo:
// Sanitize passed-in and parent policies before comparison
sanitizedInputPolicies := policyutil.SanitizePolicies(data.Policies)
sanitizedParentPolicies := policyutil.SanitizePolicies(parent.Policies)
if !strutil.StrListSubset(sanitizedParentPolicies, sanitizedInputPolicies) {
return logical.ErrorResponse("child policies must be subset of parent"), logical.ErrInvalidRequest
}
}
// Use a map to filter out/prevent duplicates
policyMap := map[string]bool{}
@ -1061,6 +1082,7 @@ func (ts *TokenStore) handleCreateCommon(
}
te.TTL = dur
} else if data.Lease != "" {
// This block is compatibility
dur, err := time.ParseDuration(data.Lease)
if err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
@ -1084,14 +1106,35 @@ func (ts *TokenStore) handleCreateCommon(
}
}
resp := &logical.Response{}
if role != nil && role.ExplicitMaxTTL != 0 {
sysView := ts.System()
// Limit the lease duration
if sysView.MaxLeaseTTL() != time.Duration(0) && role.ExplicitMaxTTL > sysView.MaxLeaseTTL() {
return logical.ErrorResponse(fmt.Sprintf(
"role explicit max TTL of %d is greater than system/mount allowed value of %d seconds",
role.ExplicitMaxTTL.Seconds(), sysView.MaxLeaseTTL().Seconds())), logical.ErrInvalidRequest
}
if te.TTL > role.ExplicitMaxTTL {
resp.AddWarning(fmt.Sprintf(
"Requested TTL higher than role explicit max TTL; value being capped to %d seconds",
role.ExplicitMaxTTL.Seconds()))
te.TTL = role.ExplicitMaxTTL
}
te.ExplicitMaxTTL = role.ExplicitMaxTTL
}
// Create the token
if err := ts.create(&te); err != nil {
return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest
}
// Generate the response
resp := &logical.Response{
Auth: &logical.Auth{
resp.Auth = &logical.Auth{
DisplayName: te.DisplayName,
Policies: te.Policies,
Metadata: te.Meta,
@ -1101,7 +1144,6 @@ func (ts *TokenStore) handleCreateCommon(
},
ClientToken: te.ID,
Accessor: te.Accessor,
},
}
if ts.policyLookupFunc != nil {
@ -1236,6 +1278,7 @@ func (ts *TokenStore) handleLookup(
"creation_ttl": int64(out.TTL.Seconds()),
"ttl": int64(0),
"role": out.Role,
"explicit_max_ttl": int64(out.ExplicitMaxTTL.Seconds()),
},
}
@ -1311,8 +1354,6 @@ func (ts *TokenStore) authRenew(
return nil, fmt.Errorf("request auth is nil")
}
f := framework.LeaseExtend(req.Auth.Increment, 0, ts.System())
te, err := ts.Lookup(req.Auth.ClientToken)
if err != nil {
return nil, fmt.Errorf("error looking up token: %s", err)
@ -1321,6 +1362,8 @@ func (ts *TokenStore) authRenew(
return nil, fmt.Errorf("no token entry found during lookup")
}
f := framework.LeaseExtend(req.Auth.Increment, te.ExplicitMaxTTL, ts.System())
// No role? Use normal LeaseExtend semantics
if te.Role == "" {
return f(req, d)
@ -1339,7 +1382,13 @@ func (ts *TokenStore) authRenew(
// periodic token is always the same (the role's period value). It is not
// subject to normal maximum TTL checks that would come from calling
// LeaseExtend, so we fast path it.
if role.Period != 0 {
//
// The one wrinkle here is if the token has an explicit max TTL. Roles
// don't support having both configured, but they could be changed. We
// don't support tokens that are both periodic and have an explicit max
// TTL, so if the token has one, we treat it as a regular token even if the
// role is periodic.
if role.Period != 0 && te.ExplicitMaxTTL == 0 {
req.Auth.TTL = role.Period
return &logical.Response{Auth: req.Auth}, nil
}
@ -1400,12 +1449,14 @@ func (ts *TokenStore) tokenStoreRoleRead(
}
resp := &logical.Response{
Data: structs.New(role).Map(),
}
// Make the period nicer
if role.Period != 0 {
resp.Data["period"] = role.Period.Seconds()
Data: map[string]interface{}{
"period": int64(role.Period.Seconds()),
"explicit_max_ttl": int64(role.ExplicitMaxTTL.Seconds()),
"allowed_policies": role.AllowedPolicies,
"name": role.Name,
"orphan": role.Orphan,
"path_suffix": role.PathSuffix,
},
}
return resp, nil
@ -1461,13 +1512,36 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate(
entry.Period = time.Second * time.Duration(data.Get("period").(int))
}
var resp *logical.Response
explicitMaxTTLInt, ok := data.GetOk("explicit_max_ttl")
if ok {
entry.ExplicitMaxTTL = time.Second * time.Duration(explicitMaxTTLInt.(int))
} else if req.Operation == logical.CreateOperation {
entry.ExplicitMaxTTL = time.Second * time.Duration(data.Get("explicit_max_ttl").(int))
}
if entry.ExplicitMaxTTL != 0 {
sysView := ts.System()
if sysView.MaxLeaseTTL() != time.Duration(0) && entry.ExplicitMaxTTL > sysView.MaxLeaseTTL() {
if resp == nil {
resp = &logical.Response{}
}
resp.AddWarning(fmt.Sprintf(
"Given explicit max TTL of %d is greater than system/mount allowed value of %d seconds; until this is fixed attempting to create tokens against this role will result in an error",
entry.ExplicitMaxTTL.Seconds(), sysView.MaxLeaseTTL().Seconds()))
}
}
pathSuffixInt, ok := data.GetOk("path_suffix")
if ok {
pathSuffix := pathSuffixInt.(string)
if pathSuffix != "" {
matched := pathSuffixSanitize.MatchString(pathSuffix)
if !matched {
return logical.ErrorResponse(fmt.Sprintf("given role path suffix contains invalid characters; must match %s", pathSuffixSanitize.String())), nil
return logical.ErrorResponse(fmt.Sprintf(
"given role path suffix contains invalid characters; must match %s",
pathSuffixSanitize.String())), nil
}
entry.PathSuffix = pathSuffix
}
@ -1485,6 +1559,12 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate(
entry.AllowedPolicies = strings.Split(data.Get("allowed_policies").(string), ",")
}
// Explicit max TTLs and periods cannot be used at the same time since the
// purpose of a periodic token is to escape max TTL semantics
if entry.Period > 0 && entry.ExplicitMaxTTL > 0 {
return logical.ErrorResponse("a role cannot be used to issue both periodic tokens and tokens with explicit max TTLs"), logical.ErrInvalidRequest
}
// Store it
jsonEntry, err := logical.StorageEntryJSON(fmt.Sprintf("%s%s", rolesPrefix, name), entry)
if err != nil {
@ -1494,7 +1574,7 @@ func (ts *TokenStore) tokenStoreRoleCreateUpdate(
return nil, err
}
return nil, nil
return resp, nil
}
const (
@ -1533,5 +1613,11 @@ will contain the given suffix as a part of
their path. This can be used to assist use
of the 'revoke-prefix' endpoint later on.
The given suffix must match the regular
expression `
expression.`
tokenExplicitMaxTTLHelp = `If set, tokens created via this role
carry an explicit maximum TTL. During renewal,
the current maximum TTL values of the role
and the mount are not checked for changes,
and any updates to these values will have
no effect on the token being renewed.`
)

View File

@ -979,6 +979,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) {
"creation_ttl": int64(0),
"ttl": int64(0),
"role": "",
"explicit_max_ttl": int64(0),
}
if resp.Data["creation_time"].(int64) == 0 {
@ -1014,6 +1015,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) {
"creation_ttl": int64(3600),
"ttl": int64(3600),
"role": "",
"explicit_max_ttl": int64(0),
}
if resp.Data["creation_time"].(int64) == 0 {
@ -1055,6 +1057,7 @@ func TestTokenStore_HandleRequest_Lookup(t *testing.T) {
"creation_ttl": int64(3600),
"ttl": int64(3600),
"role": "",
"explicit_max_ttl": int64(0),
}
if resp.Data["creation_time"].(int64) == 0 {
@ -1119,6 +1122,7 @@ func TestTokenStore_HandleRequest_LookupSelf(t *testing.T) {
"creation_ttl": int64(0),
"ttl": int64(0),
"role": "",
"explicit_max_ttl": int64(0),
}
if resp.Data["creation_time"].(int64) == 0 {
@ -1265,9 +1269,10 @@ func TestTokenStore_RoleCRUD(t *testing.T) {
expected := map[string]interface{}{
"name": "test",
"orphan": true,
"period": float64(259200),
"period": int64(259200),
"allowed_policies": []string{"test1", "test2"},
"path_suffix": "happenin",
"explicit_max_ttl": int64(0),
}
if !reflect.DeepEqual(expected, resp.Data) {
@ -1305,9 +1310,57 @@ func TestTokenStore_RoleCRUD(t *testing.T) {
expected = map[string]interface{}{
"name": "test",
"orphan": true,
"period": float64(284400),
"period": int64(284400),
"allowed_policies": []string{"test3"},
"path_suffix": "happenin",
"explicit_max_ttl": int64(0),
}
if !reflect.DeepEqual(expected, resp.Data) {
t.Fatalf("expected:\n%v\nactual:\n%v\n", expected, resp.Data)
}
// Now test setting explicit max ttl at the same time as period, which
// should be an error
req.Operation = logical.CreateOperation
req.Data = map[string]interface{}{
"explicit_max_ttl": "5",
}
resp, err = core.HandleRequest(req)
if err == nil {
t.Fatalf("expected error")
}
// Now set explicit max ttl and clear the period
req.Operation = logical.CreateOperation
req.Data = map[string]interface{}{
"explicit_max_ttl": "5",
"period": "0s",
}
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
}
req.Operation = logical.ReadOperation
req.Data = map[string]interface{}{}
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
}
if resp == nil {
t.Fatalf("got a nil response")
}
expected = map[string]interface{}{
"name": "test",
"orphan": true,
"explicit_max_ttl": int64(5),
"allowed_policies": []string{"test3"},
"path_suffix": "happenin",
"period": int64(0),
}
if !reflect.DeepEqual(expected, resp.Data) {
@ -1623,3 +1676,205 @@ func TestTokenStore_RolePeriod(t *testing.T) {
}
}
}
func TestTokenStore_RoleExplicitMaxTTL(t *testing.T) {
core, _, _, root := TestCoreWithTokenStore(t)
core.defaultLeaseTTL = 5 * time.Second
core.maxLeaseTTL = 5 * time.Hour
// Note: these requests are sent to Core since Core handles registration
// with the expiration manager and we need the storage to be consistent
// Make sure we can't make it larger than the system/mount max; we should get a warning on role write and an error on token creation
req := logical.TestRequest(t, logical.UpdateOperation, "auth/token/roles/test")
req.ClientToken = root
req.Data = map[string]interface{}{
"explicit_max_ttl": "100h",
}
resp, err := core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
}
if resp == nil {
t.Fatalf("expected a warning")
}
req.Operation = logical.UpdateOperation
req.Path = "auth/token/create/test"
resp, err = core.HandleRequest(req)
if err == nil {
t.Fatalf("expected an error")
}
// Reset to a good explicit max
req = logical.TestRequest(t, logical.UpdateOperation, "auth/token/roles/test")
req.ClientToken = root
req.Data = map[string]interface{}{
"explicit_max_ttl": "6s",
}
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
}
if resp != nil {
t.Fatalf("expected a nil response")
}
// This first set of logic is to verify that a normal non-root token will
// be given a TTL of 5 seconds, and that renewing will cause the TTL to
// increase
{
req.Path = "auth/token/create"
req.Data = map[string]interface{}{
"policies": []string{"default"},
}
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
}
if resp.Auth.ClientToken == "" {
t.Fatalf("bad: %#v", resp)
}
req.ClientToken = resp.Auth.ClientToken
req.Operation = logical.ReadOperation
req.Path = "auth/token/lookup-self"
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
ttl := resp.Data["ttl"].(int64)
if ttl > 5 {
t.Fatalf("TTL too large")
}
// Let the TTL go down a bit to 3 seconds
time.Sleep(2 * time.Second)
req.Operation = logical.UpdateOperation
req.Path = "auth/token/renew-self"
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
}
req.Operation = logical.ReadOperation
req.Path = "auth/token/lookup-self"
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
ttl = resp.Data["ttl"].(int64)
if ttl < 4 {
t.Fatalf("TTL too small after renewal")
}
}
// Now we create a token against the role. After renew our max should still
// be the same.
{
req.ClientToken = root
req.Operation = logical.UpdateOperation
req.Path = "auth/token/create/test"
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
}
if resp == nil {
t.Fatal("response was nil")
}
if resp.Auth == nil {
t.Fatal(fmt.Sprintf("response auth was nil, resp is %#v", *resp))
}
if resp.Auth.ClientToken == "" {
t.Fatalf("bad: %#v", resp)
}
req.ClientToken = resp.Auth.ClientToken
req.Operation = logical.ReadOperation
req.Path = "auth/token/lookup-self"
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
ttl := resp.Data["ttl"].(int64)
if ttl > 6 {
t.Fatalf("TTL too big")
}
maxTTL := resp.Data["explicit_max_ttl"].(int64)
if maxTTL != 6 {
t.Fatalf("expected 6 for explicit max TTL, got %d", maxTTL)
}
// Let the TTL go down a bit to 3 seconds (4 against explicit max)
time.Sleep(2 * time.Second)
req.Operation = logical.UpdateOperation
req.Path = "auth/token/renew-self"
req.Data = map[string]interface{}{
"increment": 300,
}
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
}
req.Operation = logical.ReadOperation
req.Path = "auth/token/lookup-self"
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
ttl = resp.Data["ttl"].(int64)
if ttl > 4 {
t.Fatalf("TTL too big")
}
// Let the TTL go down a bit more to 2 seconds (2 against explicit max)
time.Sleep(2 * time.Second)
req.Operation = logical.UpdateOperation
req.Path = "auth/token/renew-self"
req.Data = map[string]interface{}{
"increment": 300,
}
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
}
req.Operation = logical.ReadOperation
req.Path = "auth/token/lookup-self"
resp, err = core.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
ttl = resp.Data["ttl"].(int64)
if ttl > 2 {
t.Fatalf("TTL too big")
}
// It should expire
time.Sleep(3 * time.Second)
req.Operation = logical.UpdateOperation
req.Path = "auth/token/renew-self"
req.Data = map[string]interface{}{
"increment": 300,
}
resp, err = core.HandleRequest(req)
if err == nil {
t.Fatalf("expected error")
}
req.Operation = logical.ReadOperation
req.Path = "auth/token/lookup-self"
resp, err = core.HandleRequest(req)
if err == nil {
t.Fatalf("expected error")
}
}
}

View File

@ -601,7 +601,7 @@ of the header should be "X-Vault-Token" and the value should be the token.
each renewal. So long as they continue to be renewed, they will never
expire. The parameter is an integer duration of seconds. Tokens issued
track updates to the role value; the new period takes effect upon next
renew.
renew. This cannot be used in conjunction with `explicit_max_ttl`.
</li>
<li>
<span class="param">path_suffix</span>
@ -614,6 +614,16 @@ of the header should be "X-Vault-Token" and the value should be the token.
part of their path, and then tokens with the old suffix can be revoked
via `sys/revoke-prefix`.
</li>
<li>
<span class="param">explicit_max_ttl</span>
<span class="param-flags">optional</span>
If set, tokens created with this role have an explicit max TTL set upon
them. This maximum token TTL *cannot* be changed later, and unlike with
normal tokens, updates to the role or the system/mount max TTL value
will have no effect at renewal time -- the token will never be able to
be renewed or used past the value set at issue time. This cannot be
used in conjunction with `period`.
</li>
</ul>
</dd>