Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AppRole backend #1426

Merged
merged 1 commit into from Jul 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
131 changes: 131 additions & 0 deletions builtin/credential/approle/backend.go
@@ -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.`
25 changes: 25 additions & 0 deletions builtin/credential/approle/backend_test.go
@@ -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
}
105 changes: 105 additions & 0 deletions builtin/credential/approle/path_login.go
@@ -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.`
55 changes: 55 additions & 0 deletions builtin/credential/approle/path_login_test.go
@@ -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")
}
}