AppRole authentication backend
This commit is contained in:
parent
06b1835469
commit
a6907769b0
|
@ -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.`
|
|
@ -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
|
||||
}
|
|
@ -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.`
|
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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.`
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue