Skip to content

Commit

Permalink
AppRole: Support restricted use tokens (hashicorp#2435)
Browse files Browse the repository at this point in the history
* approle: added token_num_uses to the role

* approle: added RUD tests for token_num_uses on role

* approle: doc: added token_num_uses
  • Loading branch information
vishalnayak authored Mar 3, 2017
1 parent bbe09f2 commit f4d74fe
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 4 deletions.
3 changes: 2 additions & 1 deletion builtin/credential/approle/path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
113 changes: 112 additions & 1 deletion builtin/credential/approle/path_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down Expand Up @@ -71,6 +74,7 @@ type roleIDStorageEntry struct {
// role/<role_name>/secret-id-ttl - For updating the param
// role/<role_name>/token-ttl - For updating the param
// role/<role_name>/token-max-ttl - For updating the param
// role/<role_name>/token-num-uses - For updating the param
// role/<role_name>/bind-secret-id - For updating the param
// role/<role_name>/bound-cidr-list - For updating the param
// role/<role_name>/period - For updating the param
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -1985,6 +2091,11 @@ The list operation on the 'role/<role_name>/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
Expand Down
48 changes: 47 additions & 1 deletion builtin/credential/approle/path_role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions logical/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions vault/request_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 8 additions & 1 deletion website/source/docs/auth/approle.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -328,6 +328,13 @@ $ curl -X POST \
time unit (`60m`) after which any SecretID expires.
</li>
</ul>
<ul>
<li>
<span class="param">token_num_uses</span>
<span class="param-flags">optional</span>
Number of times issued tokens can be used.
</li>
</ul>
<ul>
<li>
<span class="param">token_ttl</span>
Expand Down

0 comments on commit f4d74fe

Please sign in to comment.