Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1426 from hashicorp/appgroup-backend
AppRole backend
- Loading branch information
Showing
15 changed files
with
4,268 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} | ||
} |
Oops, something went wrong.