AppRole authentication backend

This commit is contained in:
vishalnayak 2016-05-30 14:30:01 -04:00
parent 06b1835469
commit a6907769b0
15 changed files with 4268 additions and 13 deletions

View File

@ -0,0 +1,131 @@
package approle
import (
"fmt"
"sync"
"github.com/hashicorp/vault/helper/locksutil"
"github.com/hashicorp/vault/helper/salt"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
type backend struct {
*framework.Backend
// The salt value to be used by the information to be accessed only
// by this backend.
salt *salt.Salt
// Guard to clean-up the expired SecretID entries
tidySecretIDCASGuard uint32
// Lock to make changes to Role entries. This is a low-traffic
// operation. So, using a single lock would suffice.
roleLock *sync.RWMutex
// Map of locks to make changes to the storage entries of RoleIDs
// generated. This will be initiated to a predefined number of locks
// when the backend is created, and will be indexed based on the salted
// RoleIDs.
roleIDLocksMap map[string]*sync.RWMutex
// Map of locks to make changes to the storage entries of SecretIDs
// generated. This will be initiated to a predefined number of locks
// when the backend is created, and will be indexed based on the HMAC-ed
// SecretIDs.
secretIDLocksMap map[string]*sync.RWMutex
}
func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
b, err := Backend(conf)
if err != nil {
return nil, err
}
return b.Setup(conf)
}
func Backend(conf *logical.BackendConfig) (*backend, error) {
// Initialize the salt
salt, err := salt.NewSalt(conf.StorageView, &salt.Config{
HashFunc: salt.SHA256Hash,
})
if err != nil {
return nil, err
}
// Create a backend object
b := &backend{
// Set the salt object for the backend
salt: salt,
// Create the lock for making changes to the Roles registered with the backend
roleLock: &sync.RWMutex{},
// Create the map of locks to modify the generated RoleIDs.
roleIDLocksMap: map[string]*sync.RWMutex{},
// Create the map of locks to modify the generated SecretIDs.
secretIDLocksMap: map[string]*sync.RWMutex{},
}
// Create 256 locks each for managing RoleID and SecretIDs. This will avoid
// a superfluous number of locks directly proportional to the number of RoleID
// and SecretIDs. These locks can be accessed by indexing based on the first two
// characters of a randomly generated UUID.
if err = locksutil.CreateLocks(b.roleIDLocksMap, 256); err != nil {
return nil, fmt.Errorf("failed to create role ID locks: %v", err)
}
if err = locksutil.CreateLocks(b.secretIDLocksMap, 256); err != nil {
return nil, fmt.Errorf("failed to create secret ID locks: %v", err)
}
// Have an extra lock to use in case the indexing does not result in a lock.
// This happens if the indexing value is not beginning with hex characters.
// These locks can be used for listing purposes as well.
b.secretIDLocksMap["custom"] = &sync.RWMutex{}
b.roleIDLocksMap["custom"] = &sync.RWMutex{}
// Attach the paths and secrets that are to be handled by the backend
b.Backend = &framework.Backend{
// Register a periodic function that deletes the expired SecretID entries
PeriodicFunc: b.periodicFunc,
Help: backendHelp,
AuthRenew: b.pathLoginRenew,
PathsSpecial: &logical.Paths{
Unauthenticated: []string{
"login",
},
},
Paths: framework.PathAppend(
rolePaths(b),
[]*framework.Path{
pathLogin(b),
pathTidySecretID(b),
},
),
}
return b, nil
}
// periodicFunc of the backend will be invoked once a minute by the RollbackManager.
// RoleRole backend utilizes this function to delete expired SecretID entries.
// This could mean that the SecretID may live in the backend upto 1 min after its
// expiration. The deletion of SecretIDs are not security sensitive and it is okay
// to delay the removal of SecretIDs by a minute.
func (b *backend) periodicFunc(req *logical.Request) error {
// Initiate clean-up of expired SecretID entries
b.tidySecretID(req.Storage)
return nil
}
const backendHelp = `
Any registered Role can authenticate itself with Vault. The credentials
depends on the constraints that are set on the Role. One common required
credential is the 'role_id' which is a unique identifier of the Role.
It can be retrieved from the 'role/<appname>/role-id' endpoint.
The default constraint configuration is 'bind_secret_id', which requires
the credential 'secret_id' to be presented during login. Refer to the
documentation for other types of constraints.`

View File

@ -0,0 +1,25 @@
package approle
import (
"testing"
"github.com/hashicorp/vault/logical"
)
func createBackendWithStorage(t *testing.T) (*backend, logical.Storage) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b, err := Backend(config)
if err != nil {
t.Fatal(err)
}
if b == nil {
t.Fatalf("failed to create backend")
}
_, err = b.Backend.Setup(config)
if err != nil {
t.Fatal(err)
}
return b, config.StorageView
}

View File

@ -0,0 +1,105 @@
package approle
import (
"fmt"
"time"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathLogin(b *backend) *framework.Path {
return &framework.Path{
Pattern: "login$",
Fields: map[string]*framework.FieldSchema{
"role_id": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Unique identifier of the Role. Required to be supplied when the 'bind_secret_id' constraint is set.",
},
"secret_id": &framework.FieldSchema{
Type: framework.TypeString,
Default: "",
Description: "SecretID belong to the App role",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathLoginUpdate,
},
HelpSynopsis: pathLoginHelpSys,
HelpDescription: pathLoginHelpDesc,
}
}
// Returns the Auth object indicating the authentication and authorization information
// if the credentials provided are validated by the backend.
func (b *backend) pathLoginUpdate(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
role, roleName, metadata, err := b.validateCredentials(req, data)
if err != nil || role == nil {
return logical.ErrorResponse(fmt.Sprintf("failed to validate SecretID: %s", err)), nil
}
auth := &logical.Auth{
Period: role.Period,
InternalData: map[string]interface{}{
"role_name": roleName,
},
Metadata: metadata,
Policies: role.Policies,
LeaseOptions: logical.LeaseOptions{
Renewable: true,
},
}
// If 'Period' is set, use the value of 'Period' as the TTL.
// Otherwise, set the normal TokenTTL.
if role.Period > time.Duration(0) {
auth.TTL = role.Period
} else {
auth.TTL = role.TokenTTL
}
return &logical.Response{
Auth: auth,
}, nil
}
// Invoked when the token issued by this backend is attempting a renewal.
func (b *backend) pathLoginRenew(req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := req.Auth.InternalData["role_name"].(string)
if roleName == "" {
return nil, fmt.Errorf("failed to fetch role_name during renewal")
}
// Ensure that the Role still exists.
role, err := b.roleEntry(req.Storage, roleName)
if err != nil {
return nil, fmt.Errorf("failed to validate role %s during renewal:%s", roleName, err)
}
if role == nil {
return nil, fmt.Errorf("role %s does not exist during renewal", roleName)
}
// If 'Period' is set on the Role, the token should never expire.
// Replenish the TTL with 'Period's value.
if role.Period > time.Duration(0) {
// If 'Period' was updated after the token was issued,
// token will bear the updated 'Period' value as its TTL.
req.Auth.TTL = role.Period
return &logical.Response{Auth: req.Auth}, nil
} else {
return framework.LeaseExtend(role.TokenTTL, role.TokenMaxTTL, b.System())(req, data)
}
}
const pathLoginHelpSys = "Issue a token based on the credentials supplied"
const pathLoginHelpDesc = `
While the credential 'role_id' is required at all times,
other credentials required depends on the properties App role
to which the 'role_id' belongs to. The 'bind_secret_id'
constraint (enabled by default) on the App role requires the
'secret_id' credential to be presented.
'role_id' is fetched using the 'role/<role_name>/role_id'
endpoint and 'secret_id' is fetched using the 'role/<role_name>/secret_id'
endpoint.`

View File

@ -0,0 +1,55 @@
package approle
import (
"testing"
"github.com/hashicorp/vault/logical"
)
func TestAppRole_RoleLogin(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
createRole(t, b, storage, "role1", "a,b,c")
roleRoleIDReq := &logical.Request{
Operation: logical.ReadOperation,
Path: "role/role1/role-id",
Storage: storage,
}
resp, err = b.HandleRequest(roleRoleIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleID := resp.Data["role_id"]
roleSecretIDReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/role1/secret-id",
Storage: storage,
}
resp, err = b.HandleRequest(roleSecretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
secretID := resp.Data["secret_id"]
loginData := map[string]interface{}{
"role_id": roleID,
"secret_id": secretID,
}
loginReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "login",
Storage: storage,
Data: loginData,
}
resp, err = b.HandleRequest(loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
if resp.Auth == nil {
t.Fatalf("expected a non-nil auth object in the response")
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,787 @@
package approle
import (
"reflect"
"testing"
"time"
"github.com/hashicorp/vault/helper/policyutil"
"github.com/hashicorp/vault/logical"
"github.com/mitchellh/mapstructure"
)
func TestAppRole_RoleIDUniqueness(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
roleData := map[string]interface{}{
"role_id": "role-id-123",
"policies": "a,b",
"secret_id_num_uses": 10,
"secret_id_ttl": 300,
"token_ttl": 400,
"token_max_ttl": 500,
}
roleReq := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/testrole1",
Storage: storage,
Data: roleData,
}
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleReq.Path = "role/testrole2"
resp, err = b.HandleRequest(roleReq)
if err == nil && !(resp != nil && resp.IsError()) {
t.Fatalf("expected an error: got resp:%#v", resp)
}
roleData["role_id"] = "role-id-456"
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleReq.Operation = logical.UpdateOperation
roleData["role_id"] = "role-id-123"
resp, err = b.HandleRequest(roleReq)
if err == nil && !(resp != nil && resp.IsError()) {
t.Fatalf("expected an error: got resp:%#v", resp)
}
roleReq.Path = "role/testrole1"
roleData["role_id"] = "role-id-456"
resp, err = b.HandleRequest(roleReq)
if err == nil && !(resp != nil && resp.IsError()) {
t.Fatalf("expected an error: got resp:%#v", resp)
}
roleIDData := map[string]interface{}{
"role_id": "role-id-456",
}
roleIDReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/testrole1/role-id",
Storage: storage,
Data: roleIDData,
}
resp, err = b.HandleRequest(roleIDReq)
if err == nil && !(resp != nil && resp.IsError()) {
t.Fatalf("expected an error: got resp:%#v", resp)
}
roleIDData["role_id"] = "role-id-123"
roleIDReq.Path = "role/testrole2/role-id"
resp, err = b.HandleRequest(roleIDReq)
if err == nil && !(resp != nil && resp.IsError()) {
t.Fatalf("expected an error: got resp:%#v", resp)
}
roleIDData["role_id"] = "role-id-2000"
resp, err = b.HandleRequest(roleIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleIDData["role_id"] = "role-id-1000"
roleIDReq.Path = "role/testrole1/role-id"
resp, err = b.HandleRequest(roleIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
}
func TestAppRole_RoleDeleteSecretID(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
createRole(t, b, storage, "role1", "a,b")
secretIDReq := &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "role/role1/secret-id",
}
// Create 3 secrets on the role
resp, err = b.HandleRequest(secretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
resp, err = b.HandleRequest(secretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
resp, err = b.HandleRequest(secretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
listReq := &logical.Request{
Operation: logical.ListOperation,
Storage: storage,
Path: "role/role1/secret-id",
}
resp, err = b.HandleRequest(listReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
secretIDAccessors := resp.Data["keys"].([]string)
if len(secretIDAccessors) != 3 {
t.Fatalf("bad: len of secretIDAccessors: expected:3 actual:%d", len(secretIDAccessors))
}
roleReq := &logical.Request{
Operation: logical.DeleteOperation,
Storage: storage,
Path: "role/role1",
}
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
resp, err = b.HandleRequest(listReq)
if err != nil || resp == nil || (resp != nil && !resp.IsError()) {
t.Fatalf("expected an error. err:%v resp:%#v", err, resp)
}
}
func TestAppRole_RoleSecretIDReadDelete(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
createRole(t, b, storage, "role1", "a,b")
secretIDReq := &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "role/role1/secret-id",
}
resp, err = b.HandleRequest(secretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
listReq := &logical.Request{
Operation: logical.ListOperation,
Storage: storage,
Path: "role/role1/secret-id",
}
resp, err = b.HandleRequest(listReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
hmacSecretID := resp.Data["keys"].([]string)[0]
hmacReq := &logical.Request{
Operation: logical.ReadOperation,
Storage: storage,
Path: "role/role1/secret-id/" + hmacSecretID,
}
resp, err = b.HandleRequest(hmacReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
if resp.Data == nil {
t.Fatal(err)
}
hmacReq.Operation = logical.DeleteOperation
resp, err = b.HandleRequest(hmacReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
hmacReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(hmacReq)
if resp != nil && resp.IsError() {
t.Fatalf("error response:%#v", err, resp)
}
if err == nil {
t.Fatalf("expected an error")
}
}
func TestAppRoleRoleListSecretID(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
createRole(t, b, storage, "role1", "a,b")
secretIDReq := &logical.Request{
Operation: logical.UpdateOperation,
Storage: storage,
Path: "role/role1/secret-id",
}
// Create 5 'secret_id's
resp, err = b.HandleRequest(secretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
resp, err = b.HandleRequest(secretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
resp, err = b.HandleRequest(secretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
resp, err = b.HandleRequest(secretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
resp, err = b.HandleRequest(secretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
listReq := &logical.Request{
Operation: logical.ListOperation,
Storage: storage,
Path: "role/role1/secret-id/",
}
resp, err = b.HandleRequest(listReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
secrets := resp.Data["keys"].([]string)
if len(secrets) != 5 {
t.Fatalf("bad: len of secrets: expected:5 actual:%d", len(secrets))
}
}
func TestAppRole_RoleList(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
createRole(t, b, storage, "role1", "a,b")
createRole(t, b, storage, "role2", "c,d")
createRole(t, b, storage, "role3", "e,f")
createRole(t, b, storage, "role4", "g,h")
createRole(t, b, storage, "role5", "i,j")
listReq := &logical.Request{
Operation: logical.ListOperation,
Path: "role",
Storage: storage,
}
resp, err = b.HandleRequest(listReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
actual := resp.Data["keys"].([]string)
expected := []string{"role1", "role2", "role3", "role4", "role5"}
if !policyutil.EquivalentPolicies(actual, expected) {
t.Fatalf("bad: listed roles: expected:%s\nactual:%s", expected, actual)
}
}
func TestAppRole_RoleSecretID(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
roleData := map[string]interface{}{
"policies": "p,q,r,s",
"secret_id_num_uses": 10,
"secret_id_ttl": 300,
"token_ttl": 400,
"token_max_ttl": 500,
}
roleReq := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/role1",
Storage: storage,
Data: roleData,
}
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleSecretIDReq := &logical.Request{
Operation: logical.UpdateOperation,
Path: "role/role1/secret-id",
Storage: storage,
}
resp, err = b.HandleRequest(roleSecretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
if resp.Data["secret_id"].(string) == "" {
t.Fatalf("failed to generate secret_id")
}
roleSecretIDReq.Path = "role/role1/custom-secret-id"
roleCustomSecretIDData := map[string]interface{}{
"secret_id": "abcd123",
}
roleSecretIDReq.Data = roleCustomSecretIDData
roleSecretIDReq.Operation = logical.UpdateOperation
resp, err = b.HandleRequest(roleSecretIDReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
if resp.Data["secret_id"] != "abcd123" {
t.Fatalf("failed to set specific secret_id to role")
}
}
func TestAppRole_RoleCRUD(t *testing.T) {
var resp *logical.Response
var err error
b, storage := createBackendWithStorage(t)
roleData := map[string]interface{}{
"policies": "p,q,r,s",
"secret_id_num_uses": 10,
"secret_id_ttl": 300,
"token_ttl": 400,
"token_max_ttl": 500,
"bound_cidr_list": "127.0.0.1/32,127.0.0.1/16",
}
roleReq := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/role1",
Storage: storage,
Data: roleData,
}
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)
}
expected := map[string]interface{}{
"bind_secret_id": true,
"policies": []string{"default", "p", "q", "r", "s"},
"secret_id_num_uses": 10,
"secret_id_ttl": 300,
"token_ttl": 400,
"token_max_ttl": 500,
"bound_cidr_list": "127.0.0.1/32,127.0.0.1/16",
}
var expectedStruct roleStorageEntry
err = mapstructure.Decode(expected, &expectedStruct)
if err != nil {
t.Fatal(err)
}
var actualStruct roleStorageEntry
err = mapstructure.Decode(resp.Data, &actualStruct)
if err != nil {
t.Fatal(err)
}
expectedStruct.RoleID = actualStruct.RoleID
if !reflect.DeepEqual(expectedStruct, actualStruct) {
t.Fatalf("bad:\nexpected:%#v\nactual:%#v\n", expectedStruct, actualStruct)
}
roleData = map[string]interface{}{
"role_id": "test_role_id",
"policies": "a,b,c,d",
"secret_id_num_uses": 100,
"secret_id_ttl": 3000,
"token_ttl": 4000,
"token_max_ttl": 5000,
}
roleReq.Data = roleData
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)
}
expected = map[string]interface{}{
"policies": []string{"a", "b", "c", "d", "default"},
"secret_id_num_uses": 100,
"secret_id_ttl": 3000,
"token_ttl": 4000,
"token_max_ttl": 5000,
}
err = mapstructure.Decode(expected, &expectedStruct)
if err != nil {
t.Fatal(err)
}
err = mapstructure.Decode(resp.Data, &actualStruct)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expectedStruct, actualStruct) {
t.Fatalf("bad:\nexpected:%#v\nactual:%#v\n", expectedStruct, actualStruct)
}
// RU for role_id field
roleReq.Path = "role/role1/role-id"
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["role_id"].(string) != "test_role_id" {
t.Fatalf("bad: role_id: expected:test_role_id actual:%s\n", resp.Data["role_id"].(string))
}
roleReq.Data = map[string]interface{}{"role_id": "custom_role_id"}
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["role_id"].(string) != "custom_role_id" {
t.Fatalf("bad: role_id: expected:custom_role_id actual:%s\n", resp.Data["role_id"].(string))
}
// RUD for bind_secret_id field
roleReq.Path = "role/role1/bind-secret-id"
roleReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleReq.Data = map[string]interface{}{"bind_secret_id": false}
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["bind_secret_id"].(bool) {
t.Fatalf("bad: bind_secret_id: expected:false actual:%t\n", resp.Data["bind_secret_id"].(bool))
}
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["bind_secret_id"].(bool) {
t.Fatalf("expected the default value of 'true' to be set")
}
// RUD for policiess field
roleReq.Path = "role/role1/policies"
roleReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleReq.Data = map[string]interface{}{"policies": "a1,b1,c1,d1"}
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 !reflect.DeepEqual(resp.Data["policies"].([]string), []string{"a1", "b1", "c1", "d1", "default"}) {
t.Fatalf("bad: policies: actual:%s\n", resp.Data["policies"].([]string))
}
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)
}
expectedPolicies := []string{"default"}
actualPolicies := resp.Data["policies"].([]string)
if !policyutil.EquivalentPolicies(expectedPolicies, actualPolicies) {
t.Fatalf("bad: policies: expected:%s actual:%s", expectedPolicies, actualPolicies)
}
// RUD for secret-id-num-uses field
roleReq.Path = "role/role1/secret-id-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)
}
roleReq.Data = map[string]interface{}{"secret_id_num_uses": 200}
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["secret_id_num_uses"].(int) != 200 {
t.Fatalf("bad: secret_id_num_uses: expected:200 actual:%d\n", resp.Data["secret_id_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["secret_id_num_uses"].(int) != 0 {
t.Fatalf("expected value to be reset")
}
// RUD for secret_id_ttl field
roleReq.Path = "role/role1/secret-id-ttl"
roleReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleReq.Data = map[string]interface{}{"secret_id_ttl": 3001}
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["secret_id_ttl"].(time.Duration) != 3001 {
t.Fatalf("bad: secret_id_ttl: expected:3001 actual:%d\n", resp.Data["secret_id_ttl"].(time.Duration))
}
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["secret_id_ttl"].(time.Duration) != 0 {
t.Fatalf("expected value to be reset")
}
// RUD for 'period' field
roleReq.Path = "role/role1/period"
roleReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleReq.Data = map[string]interface{}{"period": 9001}
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["period"].(time.Duration) != 9001 {
t.Fatalf("bad: period: expected:9001 actual:%d\n", resp.Data["9001"].(time.Duration))
}
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["period"].(time.Duration) != 0 {
t.Fatalf("expected value to be reset")
}
// RUD for token_ttl field
roleReq.Path = "role/role1/token-ttl"
roleReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleReq.Data = map[string]interface{}{"token_ttl": 4001}
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_ttl"].(time.Duration) != 4001 {
t.Fatalf("bad: token_ttl: expected:4001 actual:%d\n", resp.Data["token_ttl"].(time.Duration))
}
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_ttl"].(time.Duration) != 0 {
t.Fatalf("expected value to be reset")
}
// RUD for token_max_ttl field
roleReq.Path = "role/role1/token-max-ttl"
roleReq.Operation = logical.ReadOperation
resp, err = b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
roleReq.Data = map[string]interface{}{"token_max_ttl": 5001}
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_max_ttl"].(time.Duration) != 5001 {
t.Fatalf("bad: token_max_ttl: expected:5001 actual:%d\n", resp.Data["token_max_ttl"].(time.Duration))
}
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_max_ttl"].(time.Duration) != 0 {
t.Fatalf("expected value to be reset")
}
// Delete test for role
roleReq.Path = "role/role1"
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 != nil {
t.Fatalf("expected a nil response")
}
}
func createRole(t *testing.T, b *backend, s logical.Storage, roleName, policies string) {
roleData := map[string]interface{}{
"policies": policies,
"secret_id_num_uses": 10,
"secret_id_ttl": 300,
"token_ttl": 400,
"token_max_ttl": 500,
}
roleReq := &logical.Request{
Operation: logical.CreateOperation,
Path: "role/" + roleName,
Storage: s,
Data: roleData,
}
resp, err := b.HandleRequest(roleReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}
}

View File

@ -0,0 +1,100 @@
package approle
import (
"fmt"
"sync/atomic"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathTidySecretID(b *backend) *framework.Path {
return &framework.Path{
Pattern: "tidy/secret-id$",
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.UpdateOperation: b.pathTidySecretIDUpdate,
},
HelpSynopsis: pathTidySecretIDSyn,
HelpDescription: pathTidySecretIDDesc,
}
}
// tidySecretID is used to delete entries in the whitelist that are expired.
func (b *backend) tidySecretID(s logical.Storage) error {
grabbed := atomic.CompareAndSwapUint32(&b.tidySecretIDCASGuard, 0, 1)
if grabbed {
defer atomic.StoreUint32(&b.tidySecretIDCASGuard, 0)
} else {
return fmt.Errorf("SecretID tidy operation already running")
}
roleNameHMACs, err := s.List("secret_id/")
if err != nil {
return err
}
var result error
for _, roleNameHMAC := range roleNameHMACs {
// roleNameHMAC will already have a '/' suffix. Don't append another one.
secretIDHMACs, err := s.List(fmt.Sprintf("secret_id/%s", roleNameHMAC))
if err != nil {
return err
}
for _, secretIDHMAC := range secretIDHMACs {
// In order to avoid lock swroleing in case there is need to delete,
// grab the write lock.
lock := b.secretIDLock(secretIDHMAC)
lock.Lock()
// roleNameHMAC will already have a '/' suffix. Don't append another one.
entryIndex := fmt.Sprintf("secret_id/%s%s", roleNameHMAC, secretIDHMAC)
secretIDEntry, err := s.Get(entryIndex)
if err != nil {
lock.Unlock()
return fmt.Errorf("error fetching SecretID %s: %s", secretIDHMAC, err)
}
if secretIDEntry == nil {
result = multierror.Append(result, errwrap.Wrapf("[ERR] {{err}}", fmt.Errorf("entry for SecretID %s is nil", secretIDHMAC)))
lock.Unlock()
continue
}
if secretIDEntry.Value == nil || len(secretIDEntry.Value) == 0 {
lock.Unlock()
return fmt.Errorf("found entry for SecretID %s but actual SecretID is empty", secretIDHMAC)
}
var result secretIDStorageEntry
if err := secretIDEntry.DecodeJSON(&result); err != nil {
lock.Unlock()
return err
}
// ExpirationTime not being set indicates non-expiring SecretIDs
if !result.ExpirationTime.IsZero() && time.Now().After(result.ExpirationTime) {
if err := s.Delete(entryIndex); err != nil {
lock.Unlock()
return fmt.Errorf("error deleting SecretID %s from storage: %s", secretIDHMAC, err)
}
}
lock.Unlock()
}
}
return result
}
// pathTidySecretIDUpdate is used to delete the expired SecretID entries
func (b *backend) pathTidySecretIDUpdate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
return nil, b.tidySecretID(req.Storage)
}
const pathTidySecretIDSyn = "Trigger the clean-up of expired SecretID entries."
const pathTidySecretIDDesc = `SecretIDs will have expiratin time attached to them. The periodic function
of the backend will look for expired entries and delete them. This happens once in a minute. Invoking
this endpoint will trigger the clean-up action, without waiting for the backend's periodic function.`

View File

@ -0,0 +1,424 @@
package approle
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
// secretIDStorageEntry represents the information stored in storage
// when a SecretID is created. The structure of the SecretID storage
// entry is the same for all the types of SecretIDs generated.
type secretIDStorageEntry struct {
// Accessor for the SecretID. It is a random UUID serving as
// a secondary index for the SecretID. This uniquely identifies
// the SecretID it belongs to, and hence can be used for listing
// and deleting SecretIDs. Accessors cannot be used as valid
// SecretIDs during login.
SecretIDAccessor string `json:"secret_id_accessor" structs:"secret_id_accessor" mapstructure:"secret_id_accessor"`
// Number of times this SecretID can be used to perform the login operation
SecretIDNumUses int `json:"secret_id_num_uses" structs:"secret_id_num_uses" mapstructure:"secret_id_num_uses"`
// Duration after which this SecretID should expire. This is
// croleed by the backend mount's max TTL value.
SecretIDTTL time.Duration `json:"secret_id_ttl" structs:"secret_id_ttl" mapstructure:"secret_id_ttl"`
// The time when the SecretID was created
CreationTime time.Time `json:"creation_time" structs:"creation_time" mapstructure:"creation_time"`
// The time when the SecretID becomes eligible for tidy
// operation. Tidying is performed by the PeriodicFunc of the
// backend which is 1 minute apart.
ExpirationTime time.Time `json:"expiration_time" structs:"expiration_time" mapstructure:"expiration_time"`
// The time representing the last time this storage entry was modified
LastUpdatedTime time.Time `json:"last_updated_time" structs:"last_updated_time" mapstructure:"last_updated_time"`
// Metadata that belongs to the SecretID.
Metadata map[string]string `json:"metadata" structs:"metadata" mapstructure:"metadata"`
}
// Represents the payload of the storage entry of the accessor that maps to a unique
// SecretID. Note that SecretIDs should never be stored in plaintext anywhere in the
// backend. SecretIDHMAC will be used as an index to fetch the properties of the
// SecretID and to delete the SecretID.
type secretIDAccessorStorageEntry struct {
// Hash of the SecretID which can be used to find the storage index at which
// properties of SecretID is stored.
SecretIDHMAC string `json:"secret_id_hmac" structs:"secret_id_hmac" mapstructure:"secret_id_hmac"`
}
// Checks if all the CIDR blocks in the comma separated list are valid by parsing it.
func validateCIDRList(cidrList string) error {
if cidrList == "" {
return nil
}
cidrBlocks := strings.Split(cidrList, ",")
for _, block := range cidrBlocks {
if _, _, err := net.ParseCIDR(strings.TrimSpace(block)); err != nil {
return err
}
}
return nil
}
// Checks if the Role represented by the RoleID still exists
func (b *backend) validateRoleID(s logical.Storage, roleID string) (*roleStorageEntry, string, error) {
// Look for the storage entry that maps the roleID to role
roleIDIndex, err := b.roleIDEntry(s, roleID)
if err != nil {
return nil, "", err
}
if roleIDIndex == nil {
return nil, "", fmt.Errorf("failed to find secondary index for role_id:%s\n", roleID)
}
role, err := b.roleEntry(s, roleIDIndex.Name)
if err != nil {
return nil, "", err
}
if role == nil {
return nil, "", fmt.Errorf("role %s referred by the SecretID does not exist", roleIDIndex.Name)
}
return role, roleIDIndex.Name, nil
}
// Validates the supplied RoleID and SecretID
func (b *backend) validateCredentials(req *logical.Request, data *framework.FieldData) (*roleStorageEntry, string, map[string]string, error) {
var metadata map[string]string
// RoleID must be supplied during every login
roleID := strings.TrimSpace(data.Get("role_id").(string))
if roleID == "" {
return nil, "", metadata, fmt.Errorf("missing role_id")
}
// Validate the RoleID and get the Role entry
role, roleName, err := b.validateRoleID(req.Storage, roleID)
if err != nil {
return nil, "", metadata, err
}
if role == nil || roleName == "" {
return nil, "", metadata, fmt.Errorf("failed to validate role_id")
}
// Calculate the TTL boundaries since this reflects the properties of the token issued
if role.TokenTTL, role.TokenMaxTTL, err = b.SanitizeTTL(role.TokenTTL, role.TokenMaxTTL); err != nil {
return nil, "", metadata, err
}
if role.BindSecretID {
// If 'bind_secret_id' was set on role, look for the field 'secret_id'
// to be specified and validate it.
secretID := strings.TrimSpace(data.Get("secret_id").(string))
if secretID == "" {
return nil, "", metadata, fmt.Errorf("missing secret_id")
}
// Check if the SecretID supplied is valid. If use limit was specified
// on the SecretID, it will be decremented in this call.
var valid bool
valid, metadata, err = b.validateBindSecretID(req.Storage, roleName, secretID, role.HMACKey)
if err != nil {
return nil, "", metadata, err
}
if !valid {
return nil, "", metadata, fmt.Errorf("invalid secret_id: %s\n", secretID)
}
}
if role.BoundCIDRList != "" {
// If 'bound_cidr_list' was set, verify the CIDR restrictions
cidrBlocks := strings.Split(role.BoundCIDRList, ",")
for _, block := range cidrBlocks {
_, cidr, err := net.ParseCIDR(block)
if err != nil {
return nil, "", metadata, fmt.Errorf("invalid cidr: %s", err)
}
var addr string
if req.Connection != nil {
addr = req.Connection.RemoteAddr
}
if addr == "" || !cidr.Contains(net.ParseIP(addr)) {
return nil, "", metadata, fmt.Errorf("unauthorized source address")
}
}
}
return role, roleName, metadata, nil
}
// validateBindSecretID is used to determine if the given SecretID is a valid one.
func (b *backend) validateBindSecretID(s logical.Storage, roleName, secretID, hmacKey string) (bool, map[string]string, error) {
secretIDHMAC, err := createHMAC(hmacKey, secretID)
if err != nil {
return false, nil, fmt.Errorf("failed to create HMAC of secret_id: %s", err)
}
roleNameHMAC, err := createHMAC(hmacKey, roleName)
if err != nil {
return false, nil, fmt.Errorf("failed to create HMAC of role_name: %s", err)
}
entryIndex := fmt.Sprintf("secret_id/%s/%s", roleNameHMAC, secretIDHMAC)
// SecretID locks are always index based on secretIDHMACs. This helps
// acquiring the locks when the SecretIDs are listed. This allows grabbing
// the correct locks even if the SecretIDs are not known in plaintext.
lock := b.secretIDLock(secretIDHMAC)
lock.RLock()
result := secretIDStorageEntry{}
if entry, err := s.Get(entryIndex); err != nil {
lock.RUnlock()
return false, nil, err
} else if entry == nil {
lock.RUnlock()
return false, nil, nil
} else if err := entry.DecodeJSON(&result); err != nil {
lock.RUnlock()
return false, nil, err
}
// SecretIDNumUses will be zero only if the usage limit was not set at all,
// in which case, the SecretID will remain to be valid as long as it is not
// expired.
if result.SecretIDNumUses == 0 {
lock.RUnlock()
return true, result.Metadata, nil
}
// If the SecretIDNumUses is non-zero, it means that its use-count should be updated
// in the storage. Switch the lock from a `read` to a `write` and update
// the storage entry.
lock.RUnlock()
lock.Lock()
defer lock.Unlock()
// Lock switching may change the data. Refresh the contents.
result = secretIDStorageEntry{}
if entry, err := s.Get(entryIndex); err != nil {
return false, nil, err
} else if entry == nil {
return false, nil, nil
} else if err := entry.DecodeJSON(&result); err != nil {
return false, nil, err
}
// If there exists a single use left, delete the SecretID entry from
// the storage but do not fail the validation request. Subsequest
// requests to use the same SecretID will fail.
if result.SecretIDNumUses == 1 {
accessorEntryIndex := "accessor/" + b.salt.SaltID(result.SecretIDAccessor)
if err := s.Delete(accessorEntryIndex); err != nil {
return false, nil, fmt.Errorf("failed to delete accessor storage entry: %s", err)
}
if err := s.Delete(entryIndex); err != nil {
return false, nil, fmt.Errorf("failed to delete SecretID: %s", err)
}
} else {
// If the use count is greater than one, decrement it and update the last updated time.
result.SecretIDNumUses -= 1
result.LastUpdatedTime = time.Now()
if entry, err := logical.StorageEntryJSON(entryIndex, &result); err != nil {
return false, nil, fmt.Errorf("failed to decrement the use count for SecretID:%s", secretID)
} else if err = s.Put(entry); err != nil {
return false, nil, fmt.Errorf("failed to decrement the use count for SecretID:%s", secretID)
}
}
return true, result.Metadata, nil
}
// Creates a SHA256 HMAC of the given 'value' using the given 'key'
// and returns a hex encoded string.
func createHMAC(key, value string) (string, error) {
if key == "" {
return "", fmt.Errorf("invalid HMAC key")
}
hm := hmac.New(sha256.New, []byte(key))
hm.Write([]byte(value))
return hex.EncodeToString(hm.Sum(nil)), nil
}
// secretIDLock is used to get a lock from the pre-initialized map
// of locks. Map is indexed based on the first 2 characters of the
// secretIDHMAC. If the input is not hex encoded or if empty, a
// "custom" lock will be returned.
func (b *backend) secretIDLock(secretIDHMAC string) *sync.RWMutex {
var lock *sync.RWMutex
var ok bool
if len(secretIDHMAC) >= 2 {
lock, ok = b.secretIDLocksMap[secretIDHMAC[0:2]]
}
if !ok || lock == nil {
// Fall back for custom SecretIDs
lock = b.secretIDLocksMap["custom"]
}
return lock
}
// registerSecretIDEntry creates a new storage entry for the given SecretID.
func (b *backend) registerSecretIDEntry(s logical.Storage, roleName, secretID, hmacKey string, secretEntry *secretIDStorageEntry) (*secretIDStorageEntry, error) {
secretIDHMAC, err := createHMAC(hmacKey, secretID)
if err != nil {
return nil, fmt.Errorf("failed to create HMAC of secret_id: %s", err)
}
roleNameHMAC, err := createHMAC(hmacKey, roleName)
if err != nil {
return nil, fmt.Errorf("failed to create HMAC of role_name: %s", err)
}
entryIndex := fmt.Sprintf("secret_id/%s/%s", roleNameHMAC, secretIDHMAC)
lock := b.secretIDLock(secretIDHMAC)
lock.RLock()
entry, err := s.Get(entryIndex)
if err != nil {
lock.RUnlock()
return nil, err
}
if entry != nil {
lock.RUnlock()
return nil, fmt.Errorf("SecretID is already registered")
}
// If there isn't an entry for the secretID already, switch the read lock
// with a write lock and create an entry.
lock.RUnlock()
lock.Lock()
defer lock.Unlock()
// But before saving a new entry, check if the secretID entry was created during the lock switch.
entry, err = s.Get(entryIndex)
if err != nil {
return nil, err
}
if entry != nil {
return nil, fmt.Errorf("SecretID is already registered")
}
// Create a new entry for the SecretID
// Set the creation time for the SecretID
currentTime := time.Now()
secretEntry.CreationTime = currentTime
secretEntry.LastUpdatedTime = currentTime
// If SecretIDTTL is not specified or if it crosses the backend mount's limit,
// cap the expiration to backend's max. Otherwise, use it to determine the
// expiration time.
if secretEntry.SecretIDTTL < time.Duration(0) || secretEntry.SecretIDTTL > b.System().MaxLeaseTTL() {
secretEntry.ExpirationTime = currentTime.Add(b.System().MaxLeaseTTL())
} else if secretEntry.SecretIDTTL != time.Duration(0) {
// Set the ExpirationTime only if SecretIDTTL was set. SecretIDs should not
// expire by default.
secretEntry.ExpirationTime = currentTime.Add(secretEntry.SecretIDTTL)
}
// Before storing the SecretID, store its accessor.
if err := b.createAccessor(s, secretEntry, secretIDHMAC); err != nil {
return nil, err
}
if entry, err := logical.StorageEntryJSON(entryIndex, secretEntry); err != nil {
return nil, err
} else if err = s.Put(entry); err != nil {
return nil, err
}
return secretEntry, nil
}
// secretIDAccessorEntry is used to read the storage entry that maps an
// accessor to a secret_id. This method should be called when the lock
// for the corresponding SecretID is held.
func (b *backend) secretIDAccessorEntry(s logical.Storage, secretIDAccessor string) (*secretIDAccessorStorageEntry, error) {
if secretIDAccessor == "" {
return nil, fmt.Errorf("missing secretIDAccessor")
}
var result secretIDAccessorStorageEntry
// Create index entry, mapping the accessor to the token ID
entryIndex := "accessor/" + b.salt.SaltID(secretIDAccessor)
if entry, err := s.Get(entryIndex); err != nil {
return nil, err
} else if entry == nil {
return nil, nil
} else if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
// createAccessor creates an identifier for the SecretID. A storage index,
// mapping the accessor to the SecretID is also created. This method should
// be called when the lock for the corresponding SecretID is held.
func (b *backend) createAccessor(s logical.Storage, entry *secretIDStorageEntry, secretIDHMAC string) error {
// Create a random accessor
accessorUUID, err := uuid.GenerateUUID()
if err != nil {
return err
}
entry.SecretIDAccessor = accessorUUID
// Create index entry, mapping the accessor to the token ID
entryIndex := "accessor/" + b.salt.SaltID(entry.SecretIDAccessor)
if entry, err := logical.StorageEntryJSON(entryIndex, &secretIDAccessorStorageEntry{
SecretIDHMAC: secretIDHMAC,
}); err != nil {
return err
} else if err = s.Put(entry); err != nil {
return fmt.Errorf("failed to persist accessor index entry: %s", err)
}
return nil
}
// flushRoleSecrets deletes all the SecretIDs that belong to the given
// RoleID.
func (b *backend) flushRoleSecrets(s logical.Storage, roleName, hmacKey string) error {
roleNameHMAC, err := createHMAC(hmacKey, roleName)
if err != nil {
return fmt.Errorf("failed to create HMAC of role_name: %s", err)
}
// Acquire the custom lock to perform listing of SecretIDs
customLock := b.secretIDLock("")
customLock.RLock()
defer customLock.RUnlock()
secretIDHMACs, err := s.List(fmt.Sprintf("secret_id/%s/", roleNameHMAC))
if err != nil {
return err
}
for _, secretIDHMAC := range secretIDHMACs {
// Acquire the lock belonging to the SecretID
lock := b.secretIDLock(secretIDHMAC)
lock.Lock()
entryIndex := fmt.Sprintf("secret_id/%s/%s", roleNameHMAC, secretIDHMAC)
if err := s.Delete(entryIndex); err != nil {
lock.Unlock()
return fmt.Errorf("error deleting SecretID %s from storage: %s", secretIDHMAC, err)
}
lock.Unlock()
}
return nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/vault/version"
credAppId "github.com/hashicorp/vault/builtin/credential/app-id"
credAppRole "github.com/hashicorp/vault/builtin/credential/approle"
credAwsEc2 "github.com/hashicorp/vault/builtin/credential/aws-ec2"
credCert "github.com/hashicorp/vault/builtin/credential/cert"
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
@ -65,6 +66,7 @@ func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory {
"syslog": auditSyslog.Factory,
},
CredentialBackends: map[string]logical.Factory{
"approle": credAppRole.Factory,
"cert": credCert.Factory,
"aws-ec2": credAwsEc2.Factory,
"app-id": credAppId.Factory,

View File

@ -1,6 +1,9 @@
package strutil
import (
"encoding/base64"
"encoding/json"
"fmt"
"sort"
"strings"
)
@ -39,6 +42,73 @@ func ParseStrings(input string) []string {
return RemoveDuplicates(strings.Split(input, ","))
}
// Parses a comma separated list of `<key>=<value>` tuples into a
// map[string]string.
func ParseKeyValues(input string, out map[string]string) error {
keyValues := ParseStrings(input)
if len(keyValues) == 0 {
return nil
}
for _, keyValue := range keyValues {
shards := strings.Split(keyValue, "=")
key := strings.TrimSpace(shards[0])
value := strings.TrimSpace(shards[1])
if key == "" || value == "" {
return fmt.Errorf("invalid <key,value> pair: key:'%s' value:'%s'", key, value)
}
out[key] = value
}
return nil
}
// Parses arbitrary <key,value> tuples. The input can be one of
// the following:
// * JSON string
// * Base64 encoded JSON string
// * Comma separated list of `<key>=<value>` pairs
// * Base64 encoded string containing comma separated list of
// `<key>=<value>` pairs
//
// Input will be parsed into the output paramater, which should
// be a non-nil map[string]string.
func ParseArbitraryKeyValues(input string, out map[string]string) (string, error) {
input = strings.TrimSpace(input)
if input == "" {
return "", nil
}
if out == nil {
return "", fmt.Errorf("'out' is nil")
}
// Try to base64 decode the input. If successful, consider the decoded
// value as input.
inputBytes, err := base64.StdEncoding.DecodeString(input)
if err == nil {
input = string(inputBytes)
}
// Try to JSON unmarshal the input. If successful, consider that the
// metadata was supplied as JSON input.
err = json.Unmarshal([]byte(input), &out)
if err != nil {
// If JSON unmarshalling fails, consider that the input was
// supplied as a comma separated string of 'key=value' pairs.
if err = ParseKeyValues(input, out); err != nil {
return "", fmt.Errorf("failed to parse the input: %v", err)
}
}
// Validate the parsed input
for key, value := range out {
if key != "" && value == "" {
return "", fmt.Errorf("invalid value for key '%s'", key)
}
}
return input, nil
}
// Removes duplicate and empty elements from a slice of strings.
// This also converts the items in the slice to lower case and
// returns a sorted slice.

View File

@ -1,6 +1,10 @@
package strutil
import "testing"
import (
"encoding/base64"
"reflect"
"testing"
)
func TestStrutil_EquivalentSlices(t *testing.T) {
slice1 := []string{"test2", "test1", "test3"}
@ -15,7 +19,7 @@ func TestStrutil_EquivalentSlices(t *testing.T) {
}
}
func TestStrListContains(t *testing.T) {
func TestStrutil_ListContains(t *testing.T) {
haystack := []string{
"dev",
"ops",
@ -30,7 +34,7 @@ func TestStrListContains(t *testing.T) {
}
}
func TestStrListSubset(t *testing.T) {
func TestStrutil_ListSubset(t *testing.T) {
parent := []string{
"dev",
"ops",
@ -60,3 +64,117 @@ func TestStrListSubset(t *testing.T) {
t.Fatalf("Bad")
}
}
func TestStrutil_ParseKeyValues(t *testing.T) {
actual := make(map[string]string)
expected := map[string]string{
"key1": "value1",
"key2": "value2",
}
var input string
var err error
input = "key1=value1,key2=value2"
err = ParseKeyValues(input, actual)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
}
for k, _ := range actual {
delete(actual, k)
}
input = "key1 = value1, key2 = value2"
err = ParseKeyValues(input, actual)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
}
for k, _ := range actual {
delete(actual, k)
}
input = "key1 = value1, key2 = "
err = ParseKeyValues(input, actual)
if err == nil {
t.Fatal("expected an error")
}
for k, _ := range actual {
delete(actual, k)
}
input = "key1 = value1, = value2 "
err = ParseKeyValues(input, actual)
if err == nil {
t.Fatal("expected an error")
}
for k, _ := range actual {
delete(actual, k)
}
}
func TestStrutil_ParseArbitraryKeyValues(t *testing.T) {
actual := make(map[string]string)
expected := map[string]string{
"key1": "value1",
"key2": "value2",
}
var input string
var err error
// Test <key>=<value> as comma separated string
input = "key1=value1,key2=value2"
_, err = ParseArbitraryKeyValues(input, actual)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
}
for k, _ := range actual {
delete(actual, k)
}
// Test <key>=<value> as base64 encoded comma separated string
input = base64.StdEncoding.EncodeToString([]byte(input))
_, err = ParseArbitraryKeyValues(input, actual)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
}
for k, _ := range actual {
delete(actual, k)
}
// Test JSON encoded <key>=<value> tuples
input = `{"key1":"value1", "key2":"value2"}`
_, err = ParseArbitraryKeyValues(input, actual)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
}
for k, _ := range actual {
delete(actual, k)
}
// Test base64 encoded JSON string of <key>=<value> tuples
input = base64.StdEncoding.EncodeToString([]byte(input))
_, err = ParseArbitraryKeyValues(input, actual)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("bad: expected: %#v\nactual: %#v", expected, actual)
}
for k, _ := range actual {
delete(actual, k)
}
}

View File

@ -1,6 +1,9 @@
package logical
import "fmt"
import (
"fmt"
"time"
)
// Auth is the resulting authentication information that is part of
// Response for credential backends.
@ -10,7 +13,7 @@ type Auth struct {
// InternalData is JSON-encodable data that is stored with the auth struct.
// This will be sent back during a Renew/Revoke for storing internal data
// used for those operations.
InternalData map[string]interface{}
InternalData map[string]interface{} `json:"internal_data" mapstructure:"internal_data" structs:"internal_data"`
// DisplayName is a non-security sensitive identifier that is
// applicable to this Auth. It is used for logging and prefixing
@ -18,28 +21,33 @@ type Auth struct {
// the github credential backend. If the client token is used to
// generate a SQL credential, the user may be "github-armon-uuid".
// This is to help identify the source without using audit tables.
DisplayName string
DisplayName string `json:"display_name" mapstructure:"display_name" structs:"display_name"`
// Policies is the list of policies that the authenticated user
// is associated with.
Policies []string
Policies []string `json:"policies" mapstructure:"policies" structs:"policies"`
// Metadata is used to attach arbitrary string-type metadata to
// an authenticated user. This metadata will be outputted into the
// audit log.
Metadata map[string]string
Metadata map[string]string `json:"metadata" mapstructure:"metadata" structs:"metadata"`
// ClientToken is the token that is generated for the authentication.
// This will be filled in by Vault core when an auth structure is
// returned. Setting this manually will have no effect.
ClientToken string
ClientToken string `json:"client_token" mapstructure:"client_token" structs:"client_token"`
// Accessor is the identifier for the ClientToken. This can be used
// to perform management functionalities (especially revocation) when
// ClientToken in the audit logs are obfuscated. Accessor can be used
// to revoke a ClientToken and to lookup the capabilities of the ClientToken,
// both without actually knowing the ClientToken.
Accessor string
Accessor string `json:"accessor" mapstructure:"accessor" structs:"accessor"`
// Period indicates that the token generated using this Auth object
// should never expire. The token should be renewed within the duration
// specified by this period.
Period time.Duration `json:"period" mapstructure:"period" structs:"period"`
}
func (a *Auth) GoString() string {

View File

@ -231,6 +231,7 @@ func (c *Core) loadCredentials() error {
entry.Type = "aws-ec2"
needPersist = true
}
if entry.Table == "" {
entry.Table = c.auth.Type
needPersist = true

View File

@ -0,0 +1,767 @@
---
layout: "docs"
page_title: "Auth Backend: AppRole"
sidebar_current: "docs-auth-approle"
description: |-
The AppRole backend allows machines and services to authenticate with Vault.
---
# Auth Backend: AppRole
This backend allows machines and services (logically referred as `app`s) to
authenticate with Vault, by registering them as AppRoles. The open design of
AppRoles, enables a varied set of Apps to authenticate themselves. Since an
AppRole can represent a service, or a machine or anything that can be IDed,
this backend is a potential successor for the App-ID backend.
### AppRole
An AppRole represents a set of Vault policies, under a name. In essense, if a
machine needs to authenticate with Vault for a set of policies, an AppRole can
be registered under the machine's name with the desired set of policies. If a
service requires a set of Vault policies, an AppRole can be registered under
the service's name with the desired policies. The credentials presented at the
login endpoint depends on the constraints set on AppRoles.
### RoleID
RoleID is a credential to be used at the login endpoint. The credentials used
to fetch a Vault token depends on the configured contraints on the AppRole.
The credential `role_id` is a required argument for the login endpoint at all
times. RoleIDs by default are unique UUIDs that map to the human read-able
AppRole names. This credential lets the backend know which AppRole to refer to,
in verifying the set constraints. RoleID for an AppRole can be fetched via
`role/<role_name>/role-id` endpoint.
### SecretID
SecretID is a credential to be used at the login endpoint. By default, this
backend enables a login constraint on the AppRole, called `bind_secret_id`.
When this constraint is enabled, the login endpoint expects another credential,
`secret_id` to be presented, along with `role_id`. The backend supports both
creation of SecretID by the backend and setting custom SecretID by the client.
It is recommended that SecretIDs be generated by the backend. The ones
generated by the backend will be cryptographically strong random UUIDs.
SecretIDs have properties like usage-limit, TTLs and expirations; similar to
tokens. SecretID for an AppRole can be fetched via `role/<role_name>/secret-id`
endpoint.
### Pull And Push SecretID Modes
If the SecretID generated by the backend is fetched and used for login, it is
referred as `Pull` mode. If a "custom" SecretID is set against an AppRole by
the client, it is referred as a `Push` mode.
While the `user_id` of the App-ID backend worked in a `Push` mode, this backend
recommends the `Pull` mode. The `Pull` mode is supported in AppRole backend,
*only* to be able to make this backend to do all that App-ID did.
### AppRole Constraints
`role_id` is a required credential at the login endpoint. AppRole pointed to by
the `role_id` will have constraints set on it. This dictates other `required`
credentials for login. The `bind_secret_id` constraint requires `secret_id` to
be presented at the login endpoint. Going forward, this backend can support
more constraint parameters to support varied set of Apps. Some constraints will
not require a credential, but still enforce constraints for login. For
example, `bound_cidr_list` will only allow requests coming from IP addresses
belonging to configured CIDR blocks on the AppRole.
## Authentication
### Via the CLI
#### Enable AppRole authentication
```javascript
$ vault auth-enable approle
```
#### Create a role
```javascript
$ vault write auth/approle/role/testrole secret_id_ttl=10m token_ttl=20m token_max_ttl=30m secret_id_num_uses=40
```
#### Fetch the RoleID of the AppRole
```javascript
$ vault read auth/approle/role/testrole/role-id
```
```javascript
role_id db02de05-fa39-4855-059b-67221c5c2f63
```
#### Get a SecretID issued against the AppRole
```javascript
$ vault write auth/approle/role/testrole/secret-id metadata=@secret-metadata
```
```javascript
secret_id 6a174c20-f6de-a53c-74d2-6018fcceff64
secret_id_accessor c454f7e5-996e-7230-6074-6ef26b7bcf86
```
```javascript
$ cat secret-metadata
{
"secret_prefix": "test_secrets",
"secret_version": "v1"
}
```
*Note*: Metadata can be of the following formats.
* JSON string
* Base64 encoded JSON string
* String containing comma separated <key>=<value> pairs
* Base64 encoded string containing comma separated <key>=<value> pairs
#### Login to get a Vault Token
```javascript
$ vault write auth/approle/login role_id=db02de05-fa39-4855-059b-67221c5c2f63 secret_id=6a174c20-f6de-a53c-74d2-6018fcceff64
```
```javascript
token 65b74ffd-842c-fd43-1386-f7d7006e520a
token_accessor 3c29bc22-5c72-11a6-f778-2bc8f48cea0e
token_duration 1200
token_renewable true
token_policies [default]
```
### Via the API
#### Enable AppRole authentication
```javascript
$ curl -XPOST -H "X-Vault-Token:xxx" "http://127.0.0.1:8200/v1/sys/auth/approle" -d '{"type":"approle"}'
```
#### Create a role
```javascript
$ curl -XPOST -H "X-Vault-Token:xxx" "http://127.0.0.1:8200/v1/auth/approle/role/testrole" -d '{"secret_id_ttl":"10m", "token_ttl":"20m", "token_max_ttl":"30m", "secret_id_num_uses":40}'
```
#### Fetch the RoleID of the AppRole
```javascript
$ curl -XGET -H "X-Vault-Token:xxx" "http://127.0.0.1:8200/v1/auth/approle/role/testrole/role-id"
```
#### Get a SecretID issued against the AppRole
```javascript
$ curl -XPOST -H "X-Vault-Token:xxx" "http://127.0.0.1:8200/v1/auth/approle/role/testrole/secret-id" -d '{"metadata":"{\"secret_prefix\": \"test_secrets\",\"secret_version\": \"v1\"}"}'
```
#### Login to get a Vault Token
```javascript
$ curl -XPOST -H "X-Vault-Token:xxx" "http://127.0.0.1:8200/v1/auth/approle/login" -d '{"role_id":"50bec295-3535-0ddc-b729-e4d0773717b3","secret_id":"0c36edb2-8b34-c077-9e3a-9bdcbb4ab0df"}'
```
## API
### /auth/approle/role
#### List
<dl class="api">
<dt>Description</dt>
<dd>
Lists the existing AppRoles in the backend
</dd>
<dt>Method</dt>
<dd>`/auth/approle/role` (LIST) or `/auth/approle/role?list=true` (GET)</dd>
<dt>URL</dt>
<dd>`LIST/GET`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"auth": null,
"warnings": null,
"wrap_info": null,
"data": {
"keys": [
"dev",
"prod",
"test"
]
},
"lease_duration": 0,
"renewable": false,
"lease_id": ""
}
```
</dd>
</dl>
### /auth/approle/role/[role_name]
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Create a new AppRole or update an existing AppRole. This endpoint
supports both `create` and `update` capabilities.
</dd>
<dt>Method</dt>
<dd>`POST`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">role_name</span>
<span class="param-flags">required</span>
Name of the Role.
</li>
</ul>
<ul>
<li>
<span class="param">bind_secret_id</span>
<span class="param-flags">optional</span>
Impose secret_id to be presented when logging in using this Role.
Defaults to 'true'.
</li>
</ul>
<ul>
<li>
<span class="param">bound_cidr_list</span>
<span class="param-flags">optional</span>
Comma separated list of CIDR blocks, if set, specifies blocks of IP
addresses which can perform the login operation
</li>
</ul>
<ul>
<li>
<span class="param">policies</span>
<span class="param-flags">optional</span>
Comma separated list of policies on the Role.
</li>
</ul>
<ul>
<li>
<span class="param">secret_id_num_uses</span>
<span class="param-flags">optional</span>
Number of times a SecretID can access the Role, after which the SecretID will expire.
</li>
</ul>
<ul>
<li>
<span class="param">secret_id_ttl</span>
<span class="param-flags">optional</span>
Duration in seconds after which the issued SecretID should expire.
</li>
</ul>
<ul>
<li>
<span class="param">token_ttl</span>
<span class="param-flags">optional</span>
Duration in seconds after which the issued token should expire.
</li>
</ul>
<ul>
<li>
<span class="param">token_max_ttl</span>
<span class="param-flags">optional</span>
Duration in seconds after which the issued token should not be allowed to be renewed.
</li>
</ul>
<ul>
<li>
<span class="param">period</span>
<span class="param-flags">optional</span>
If set, indicates that the token generated using this Role
should never expire. The token should be renewed within the
duration specified by this value. The renewal duration will
be fixed, if this value is not modified. If the Period in the
Role is modified, the token will pick up the new value during
its next renewal.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>`204` response code.
</dd>
</dl>
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Reads the properties of an existing AppRole.
</dd>
<dt>Method</dt>
<dd>`GET`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]`</dd>
<dt>Parameters</dt>
<dd>
None.
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"auth": null,
"warnings": null,
"wrap_info": null,
"data": {
"token_ttl": 1200,
"token_max_ttl": 1800,
"secret_id_ttl": 600,
"secret_id_num_uses": 40,
"policies": [
"default"
],
"period": 0,
"bind_secret_id": true,
"bound_cidr_list": ""
},
"lease_duration": 0,
"renewable": false,
"lease_id": ""
}
```
</dd>
</dl>
#### DELETE
<dl class="api">
<dt>Description</dt>
<dd>
Deletes an existing AppRole from the backend.
</dd>
<dt>Method</dt>
<dd>`DELETE`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]`</dd>
<dt>Parameters</dt>
<dd>
None.
</dd>
<dt>Returns</dt>
<dd>`204` response code.
</dd>
</dl>
### /auth/approle/role/[role_name]/role-id
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Reads the RoleID of an existing AppRole.
</dd>
<dt>Method</dt>
<dd>`GET`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]/role-id`</dd>
<dt>Parameters</dt>
<dd>
None.
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"auth": null,
"warnings": null,
"wrap_info": null,
"data": {
"role_id": "e5a7b66e-5d08-da9c-7075-71984634b882"
},
"lease_duration": 0,
"renewable": false,
"lease_id": ""
}
```
</dd>
</dl>
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Updates the RoleID of an existing AppRole.
</dd>
<dt>Method</dt>
<dd>`POST`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]/role-id`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">role_id</span>
<span class="param-flags">required</span>
Value to be set as RoleID.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
`204` response code.
</dd>
</dl>
### /auth/approle/role/[role_name]/secret-id
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Generates and issues a new SecretID on an existing AppRole. The
response will also contain the `secret_id_accessor` which can be
used to read the properties of the SecretID and also to delete
the SecretID from the backend.
</dd>
<dt>Method</dt>
<dd>`POST`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]/secret-id`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">metadata</span>
<span class="param-flags">optional</span>
Metadata to be tied to the SecretID. This should be a JSON
formatted string containing the metadata in key value pairs.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"auth": null,
"warnings": null,
"wrap_info": null,
"data": {
"secret_id_accessor": "84896a0c-1347-aa90-a4f6-aca8b7558780",
"secret_id": "841771dc-11c9-bbc7-bcac-6a3945a69cd9"
},
"lease_duration": 0,
"renewable": false,
"lease_id": ""
}
```
</dd>
</dl>
#### List
<dl class="api">
<dt>Description</dt>
<dd>
Lists the accessors of all the SecretIDs issued against the AppRole.
This includes the accessors for the "custom" SecretIDs as well.
</dd>
<dt>Method</dt>
<dd>`LIST/GET`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]/secret-id` (LIST) or `/auth/approle/role/[role_name]/secret-id?list=true` (GET)</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"auth": null,
"warnings": null,
"wrap_info": null,
"data": {
"keys": [
"ce102d2a-8253-c437-bf9a-aceed4241491",
"a1c8dee4-b869-e68d-3520-2040c1a0849a",
"be83b7e2-044c-7244-07e1-47560ca1c787",
"84896a0c-1347-aa90-a4f6-aca8b7558780",
"239b1328-6523-15e7-403a-a48038cdc45a"
]
},
"lease_duration": 0,
"renewable": false,
"lease_id": ""
}
```
</dd>
</dl>
### /auth/approle/role/[role_name]/secret-id/<secret_id_accessor>
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Reads out the properties of the SecretID to which the supplied `secret_id_accessor` is an index of.
</dd>
<dt>Method</dt>
<dd>`GET`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]/secret-id/<secret_id_accessor>`</dd>
<dt>Parameters</dt>
<dd>
None.
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"auth": null,
"warnings": null,
"wrap_info": null,
"data": {
"secret_id_ttl": 600,
"secret_id_num_uses": 40,
"secret_id_accessor": "5e222f10-278d-a829-4e74-10d71977bb53",
"metadata": {
"version": "v1",
"prefix": "dev_secrets"
},
"last_updated_time": "2016-06-29T05:31:09.407042587Z",
"expiration_time": "2016-06-29T05:41:09.407042587Z",
"creation_time": "2016-06-29T05:31:09.407042587Z"
},
"lease_duration": 0,
"renewable": false,
"lease_id": ""
}
```
</dd>
</dl>
#### DELETE
<dl class="api">
<dt>Description</dt>
<dd>
Deletes the SecretID to which the supplied `secret_id_accessor` is an index of.
</dd>
<dt>Method</dt>
<dd>`DELETE`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]/secret-id/<secret_id_accessor>`</dd>
<dt>Parameters</dt>
<dd>
None.
</dd>
<dt>Returns</dt>
<dd>
`204` response code.
</dd>
</dl>
### /auth/approle/role/[role_name]/custom-secret-id
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Assigns a "custom" SecretID against an existing AppRole.
</dd>
<dt>Method</dt>
<dd>`POST`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]/custom-secret-id`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">secret_id</span>
<span class="param-flags">required</span>
SecretID to be attached to the Role.
</li>
</ul>
<ul>
<li>
<span class="param">metadata</span>
<span class="param-flags">optional</span>
Metadata to be tied to the SecretID. This should be a JSON
formatted string containing the metadata in key value pairs.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"auth": null,
"warnings": null,
"wrap_info": null,
"data": {
"secret_id_accessor": "a109dc4a-1fd3-6df6-feda-0ca28b2d4a81",
"secret_id": "testsecretid"
},
"lease_duration": 0,
"renewable": false,
"lease_id": ""
}
```
</dd>
</dl>
### /auth/approle/login
#### POST
<dl class="api">
<dt>Description</dt>
<dd>
Issues a Vault token based on the presented credentials. Credentials
other than `role_id`, to be presented depends on the constraints
set on the AppRole. If `bind_secret_id` is enabled, then parameter
`secret_id` becomes a required credential.
</dd>
<dt>Method</dt>
<dd>`POST`</dd>
<dt>URL</dt>
<dd>`/auth/approle/login`</dd>
<dt>Parameters</dt>
<dd>
<ul>
<li>
<span class="param">role_id</span>
<span class="param-flags">required</span>
RoleID of the AppRole.
</li>
</ul>
<ul>
<li>
<span class="param">secret_id</span>
<span class="param-flags">required when `bind_secret_id` is enabled</span>
SecretID belonging to AppRole.
</li>
</ul>
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"auth": {
"renewable": true,
"lease_duration": 1200,
"metadata": null,
"policies": [
"default"
],
"accessor": "fd6c9a00-d2dc-3b11-0be5-af7ae0e1d374",
"client_token": "5b1a0318-679c-9c45-e5c6-d1b9a9035d49"
},
"warnings": null,
"wrap_info": null,
"data": null,
"lease_duration": 0,
"renewable": false,
"lease_id": ""
}
```
</dd>
</dl>
### /auth/approle/role/[role_name]/policies
### /auth/approle/role/[role_name]/secret-id-num-uses
### /auth/approle/role/[role_name]/secret-id-ttl
### /auth/approle/role/[role_name]/token-ttl
### /auth/approle/role/[role_name]/token-max-ttl
### /auth/approle/role/[role_name]/bind-secret-id
### /auth/approle/role/[role_name]/bound-cidr-list
### /auth/approle/role/[role_name]/period
#### POST/GET/DELETE
<dl class="api">
<dt>Description</dt>
<dd>
Updates the respective property in the existing AppRole. All of these
parameters of the AppRole can be updated using the `/auth/approle/role/[role_name]`
endpoint directly. The endpoints for each field is provided separately
to be able to delegate specific endpoints using Vault's ACL system.
</dd>
<dt>Method</dt>
<dd>`POST/GET/DELETE`</dd>
<dt>URL</dt>
<dd>`/auth/approle/role/[role_name]/[field_name]`</dd>
<dt>Parameters</dt>
<dd>
Refer to `/auth/approle/role/[role_name]` endpoint.
</dd>
<dt>Returns</dt>
<dd>
Refer to `/auth/approle/role/[role_name]` endpoint.
</dd>
</dl>

View File

@ -174,6 +174,14 @@
<a href="/docs/auth/app-id.html">App ID</a>
</li>
<li<%= sidebar_current("docs-auth-approle") %>>
<a href="/docs/auth/approle.html">AppRole</a>
</li>
<li<%= sidebar_current("docs-auth-aws-ec2") %>>
<a href="/docs/auth/aws-ec2.html">AWS EC2 Auth</a>
</li>
<li<%= sidebar_current("docs-auth-github") %>>
<a href="/docs/auth/github.html">GitHub</a>
</li>
@ -188,10 +196,10 @@
<li<%= sidebar_current("docs-auth-cert") %>>
<a href="/docs/auth/cert.html">TLS Certificates</a>
</li>
</li>
<li<%= sidebar_current("docs-auth-token") %>>
<a href="/docs/auth/token.html">Tokens</a>
<a href="/docs/auth/token.html">Tokens</a>
</li>
<li<%= sidebar_current("docs-auth-userpass") %>>
@ -199,7 +207,7 @@
</li>
<li<%= sidebar_current("docs-auth-aws-ec2") %>>
<a href="/docs/auth/aws-ec2.html">AWS EC2 Auth</a>
<a href="/docs/auth/aws-ec2.html">AWS EC2</a>
</li>
</ul>
</li>