From f4d74fe4cc17a105833cf1cb432bf6d25b4c630f Mon Sep 17 00:00:00 2001 From: Vishal Nayak Date: Fri, 3 Mar 2017 09:31:20 -0500 Subject: [PATCH] AppRole: Support restricted use tokens (#2435) * approle: added token_num_uses to the role * approle: added RUD tests for token_num_uses on role * approle: doc: added token_num_uses --- builtin/credential/approle/path_login.go | 3 +- builtin/credential/approle/path_role.go | 113 ++++++++++++++++++- builtin/credential/approle/path_role_test.go | 48 +++++++- logical/auth.go | 3 + vault/request_handling.go | 1 + website/source/docs/auth/approle.html.md | 9 +- 6 files changed, 173 insertions(+), 4 deletions(-) diff --git a/builtin/credential/approle/path_login.go b/builtin/credential/approle/path_login.go index 65a9ec4f57ee..d40530e9efd5 100644 --- a/builtin/credential/approle/path_login.go +++ b/builtin/credential/approle/path_login.go @@ -39,7 +39,8 @@ func (b *backend) pathLoginUpdate(req *logical.Request, data *framework.FieldDat } auth := &logical.Auth{ - Period: role.Period, + NumUses: role.TokenNumUses, + Period: role.Period, InternalData: map[string]interface{}{ "role_name": roleName, }, diff --git a/builtin/credential/approle/path_role.go b/builtin/credential/approle/path_role.go index 9375a0788a7f..20b9a13d437c 100644 --- a/builtin/credential/approle/path_role.go +++ b/builtin/credential/approle/path_role.go @@ -36,6 +36,9 @@ type roleStorageEntry struct { // SecretID generated against the role will expire SecretIDTTL time.Duration `json:"secret_id_ttl" structs:"secret_id_ttl" mapstructure:"secret_id_ttl"` + // TokenNumUses defines the number of allowed uses of the token issued + TokenNumUses int `json:"token_num_uses" mapstructure:"token_num_uses" structs:"token_num_uses"` + // Duration before which an issued token must be renewed TokenTTL time.Duration `json:"token_ttl" structs:"token_ttl" mapstructure:"token_ttl"` @@ -71,6 +74,7 @@ type roleIDStorageEntry struct { // role//secret-id-ttl - For updating the param // role//token-ttl - For updating the param // role//token-max-ttl - For updating the param +// role//token-num-uses - For updating the param // role//bind-secret-id - For updating the param // role//bound-cidr-list - For updating the param // role//period - For updating the param @@ -123,6 +127,10 @@ will expire. Defaults to 0 meaning that the the secret_id is of unlimited use.`, Description: `Duration in seconds after which the issued SecretID should expire. Defaults to 0, meaning no expiration.`, }, + "token_num_uses": &framework.FieldSchema{ + Type: framework.TypeInt, + Description: `Number of times issued tokens can be used`, + }, "token_ttl": &framework.FieldSchema{ Type: framework.TypeDurationSecond, Description: `Duration in seconds after which the issued token should expire. Defaults @@ -284,7 +292,26 @@ TTL will be set to the value of this parameter.`, HelpSynopsis: strings.TrimSpace(roleHelp["role-period"][0]), HelpDescription: strings.TrimSpace(roleHelp["role-period"][1]), }, - + &framework.Path{ + Pattern: "role/" + framework.GenericNameRegex("role_name") + "/token-num-uses$", + Fields: map[string]*framework.FieldSchema{ + "role_name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Name of the role.", + }, + "token_num_uses": &framework.FieldSchema{ + Type: framework.TypeInt, + Description: `Number of times issued tokens can be used`, + }, + }, + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.UpdateOperation: b.pathRoleTokenNumUsesUpdate, + logical.ReadOperation: b.pathRoleTokenNumUsesRead, + logical.DeleteOperation: b.pathRoleTokenNumUsesDelete, + }, + HelpSynopsis: strings.TrimSpace(roleHelp["role-token-num-uses"][0]), + HelpDescription: strings.TrimSpace(roleHelp["role-token-num-uses"][1]), + }, &framework.Path{ Pattern: "role/" + framework.GenericNameRegex("role_name") + "/token-ttl$", Fields: map[string]*framework.FieldSchema{ @@ -772,6 +799,15 @@ func (b *backend) pathRoleCreateUpdate(req *logical.Request, data *framework.Fie role.SecretIDTTL = time.Second * time.Duration(data.Get("secret_id_ttl").(int)) } + if tokenNumUsesRaw, ok := data.GetOk("token_num_uses"); ok { + role.TokenNumUses = tokenNumUsesRaw.(int) + } else if req.Operation == logical.CreateOperation { + role.TokenNumUses = data.Get("token_num_uses").(int) + } + if role.TokenNumUses < 0 { + return logical.ErrorResponse("token_num_uses cannot be negative"), nil + } + if tokenTTLRaw, ok := data.GetOk("token_ttl"); ok { role.TokenTTL = time.Second * time.Duration(tokenTTLRaw.(int)) } else if req.Operation == logical.CreateOperation { @@ -1594,6 +1630,76 @@ func (b *backend) pathRolePeriodDelete(req *logical.Request, data *framework.Fie return nil, b.setRoleEntry(req.Storage, roleName, role, "") } +func (b *backend) pathRoleTokenNumUsesUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + roleName := data.Get("role_name").(string) + if roleName == "" { + return logical.ErrorResponse("missing role_name"), nil + } + + role, err := b.roleEntry(req.Storage, strings.ToLower(roleName)) + if err != nil { + return nil, err + } + if role == nil { + return nil, nil + } + + lock := b.roleLock(roleName) + + lock.Lock() + defer lock.Unlock() + + if tokenNumUsesRaw, ok := data.GetOk("token_num_uses"); ok { + role.TokenNumUses = tokenNumUsesRaw.(int) + return nil, b.setRoleEntry(req.Storage, roleName, role, "") + } else { + return logical.ErrorResponse("missing token_num_uses"), nil + } +} + +func (b *backend) pathRoleTokenNumUsesRead(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + roleName := data.Get("role_name").(string) + if roleName == "" { + return logical.ErrorResponse("missing role_name"), nil + } + + if role, err := b.roleEntry(req.Storage, strings.ToLower(roleName)); err != nil { + return nil, err + } else if role == nil { + return nil, nil + } else { + return &logical.Response{ + Data: map[string]interface{}{ + "token_num_uses": role.TokenNumUses, + }, + }, nil + } +} + +func (b *backend) pathRoleTokenNumUsesDelete(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + roleName := data.Get("role_name").(string) + if roleName == "" { + return logical.ErrorResponse("missing role_name"), nil + } + + role, err := b.roleEntry(req.Storage, strings.ToLower(roleName)) + if err != nil { + return nil, err + } + if role == nil { + return nil, nil + } + + lock := b.roleLock(roleName) + + lock.Lock() + defer lock.Unlock() + + role.TokenNumUses = data.GetDefaultOrZero("token_num_uses").(int) + + return nil, b.setRoleEntry(req.Storage, roleName, role, "") +} + func (b *backend) pathRoleTokenTTLUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) { roleName := data.Get("role_name").(string) if roleName == "" { @@ -1985,6 +2091,11 @@ The list operation on the 'role//secret-id' endpoint will return the 'secret_id_accessor's. This endpoint can be used to read the properties of the secret. If the 'secret_id_num_uses' field in the response is 0, it represents a non-expiring 'secret_id'.`, + }, + "role-token-num-uses": { + "Number of times issued tokens can be used", + `By default, this will be set to zero, indicating that the issued +tokens can be used any number of times.`, }, "role-token-ttl": { `Duration in seconds, the lifetime of the token issued by using the SecretID that diff --git a/builtin/credential/approle/path_role_test.go b/builtin/credential/approle/path_role_test.go index 45610a90c5a4..c1933854991f 100644 --- a/builtin/credential/approle/path_role_test.go +++ b/builtin/credential/approle/path_role_test.go @@ -582,6 +582,7 @@ func TestAppRole_RoleCRUD(t *testing.T) { "secret_id_ttl": 300, "token_ttl": 400, "token_max_ttl": 500, + "token_num_uses": 600, "bound_cidr_list": "127.0.0.1/32,127.0.0.1/16", } roleReq := &logical.Request{ @@ -609,6 +610,7 @@ func TestAppRole_RoleCRUD(t *testing.T) { "secret_id_ttl": 300, "token_ttl": 400, "token_max_ttl": 500, + "token_num_uses": 600, "bound_cidr_list": "127.0.0.1/32,127.0.0.1/16", } var expectedStruct roleStorageEntry @@ -738,7 +740,7 @@ func TestAppRole_RoleCRUD(t *testing.T) { t.Fatalf("expected the default value of 'true' to be set") } - // RUD for policiess field + // RUD for policies field roleReq.Path = "role/role1/policies" roleReq.Operation = logical.ReadOperation resp, err = b.HandleRequest(roleReq) @@ -860,6 +862,50 @@ func TestAppRole_RoleCRUD(t *testing.T) { t.Fatalf("expected value to be reset") } + // RUD for secret-id-num-uses field + roleReq.Path = "role/role1/token-num-uses" + roleReq.Operation = logical.ReadOperation + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + if resp.Data["token_num_uses"].(int) != 600 { + t.Fatalf("bad: token_num_uses: expected:600 actual:%d\n", resp.Data["token_num_uses"].(int)) + } + + roleReq.Data = map[string]interface{}{"token_num_uses": 60} + roleReq.Operation = logical.UpdateOperation + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + roleReq.Operation = logical.ReadOperation + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + if resp.Data["token_num_uses"].(int) != 60 { + t.Fatalf("bad: token_num_uses: expected:60 actual:%d\n", resp.Data["token_num_uses"].(int)) + } + + roleReq.Operation = logical.DeleteOperation + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + roleReq.Operation = logical.ReadOperation + resp, err = b.HandleRequest(roleReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("err:%v resp:%#v", err, resp) + } + + if resp.Data["token_num_uses"].(int) != 0 { + t.Fatalf("expected value to be reset") + } + // RUD for 'period' field roleReq.Path = "role/role1/period" roleReq.Operation = logical.ReadOperation diff --git a/logical/auth.go b/logical/auth.go index ff09d354e6a5..b45479058b4b 100644 --- a/logical/auth.go +++ b/logical/auth.go @@ -48,6 +48,9 @@ type Auth struct { // should never expire. The token should be renewed within the duration // specified by this period. Period time.Duration `json:"period" mapstructure:"period" structs:"period"` + + // Number of allowed uses of the issued token + NumUses int `json:"num_uses" mapstructure:"num_uses" structs:"num_uses"` } func (a *Auth) GoString() string { diff --git a/vault/request_handling.go b/vault/request_handling.go index c73270f2aa7e..ad37b5aee0b6 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -415,6 +415,7 @@ func (c *Core) handleLoginRequest(req *logical.Request) (*logical.Response, *log DisplayName: auth.DisplayName, CreationTime: time.Now().Unix(), TTL: auth.TTL, + NumUses: auth.NumUses, } te.Policies = policyutil.SanitizePolicies(te.Policies, true) diff --git a/website/source/docs/auth/approle.html.md b/website/source/docs/auth/approle.html.md index 4ff7761c0daa..bd9d07bb7f5b 100644 --- a/website/source/docs/auth/approle.html.md +++ b/website/source/docs/auth/approle.html.md @@ -90,7 +90,7 @@ $ vault auth-enable approle #### Create a role ```shell -$ vault write auth/approle/role/testrole secret_id_ttl=10m token_ttl=20m token_max_ttl=30m secret_id_num_uses=40 +$ vault write auth/approle/role/testrole secret_id_ttl=10m token_num_uses=10 token_ttl=20m token_max_ttl=30m secret_id_num_uses=40 ``` #### Fetch the RoleID of the AppRole @@ -328,6 +328,13 @@ $ curl -X POST \ time unit (`60m`) after which any SecretID expires. +
    +
  • + token_num_uses + optional + Number of times issued tokens can be used. +
  • +
  • token_ttl