blob: 8f672efc69196c90d584fb30f3f55a3117ec5be3 [file] [log] [blame]
package aws
import (
"context"
"errors"
"testing"
"time"
"github.com/aws/aws-sdk-go/service/iam/iamiface"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/go-secure-stdlib/awsutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/queue"
)
// TestRotation verifies that the rotation code and priority queue correctly selects and rotates credentials
// for static secrets.
func TestRotation(t *testing.T) {
bgCTX := context.Background()
type credToInsert struct {
config staticRoleEntry // role configuration from a normal createRole request
age time.Duration // how old the cred should be - if this is longer than the config.RotationPeriod,
// the cred is 'pre-expired'
changed bool // whether we expect the cred to change - this is technically redundant to a comparison between
// rotationPeriod and age.
}
// due to a limitation with the mockIAM implementation, any cred you want to rotate must have
// username jane-doe and userid unique-id, since we can only pre-can one exact response to GetUser
cases := []struct {
name string
creds []credToInsert
}{
{
name: "refresh one",
creds: []credToInsert{
{
config: staticRoleEntry{
Name: "test",
Username: "jane-doe",
ID: "unique-id",
RotationPeriod: 2 * time.Second,
},
age: 5 * time.Second,
changed: true,
},
},
},
{
name: "refresh none",
creds: []credToInsert{
{
config: staticRoleEntry{
Name: "test",
Username: "jane-doe",
ID: "unique-id",
RotationPeriod: 1 * time.Minute,
},
age: 5 * time.Second,
changed: false,
},
},
},
{
name: "refresh one of two",
creds: []credToInsert{
{
config: staticRoleEntry{
Name: "toast",
Username: "john-doe",
ID: "other-id",
RotationPeriod: 1 * time.Minute,
},
age: 5 * time.Second,
changed: false,
},
{
config: staticRoleEntry{
Name: "test",
Username: "jane-doe",
ID: "unique-id",
RotationPeriod: 1 * time.Second,
},
age: 5 * time.Second,
changed: true,
},
},
},
{
name: "no creds to rotate",
creds: []credToInsert{},
},
}
ak := "long-access-key-id"
oldSecret := "abcdefghijklmnopqrstuvwxyz"
newSecret := "zyxwvutsrqponmlkjihgfedcba"
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b := Backend(config)
// insert all our creds
for i, cred := range c.creds {
// all the creds will be the same for every user, but that's okay
// since what we care about is whether they changed on a single-user basis.
miam, err := awsutil.NewMockIAM(
// blank list for existing user
awsutil.WithListAccessKeysOutput(&iam.ListAccessKeysOutput{
AccessKeyMetadata: []*iam.AccessKeyMetadata{
{},
},
}),
// initial key to store
awsutil.WithCreateAccessKeyOutput(&iam.CreateAccessKeyOutput{
AccessKey: &iam.AccessKey{
AccessKeyId: aws.String(ak),
SecretAccessKey: aws.String(oldSecret),
},
}),
awsutil.WithGetUserOutput(&iam.GetUserOutput{
User: &iam.User{
UserId: aws.String(cred.config.ID),
UserName: aws.String(cred.config.Username),
},
}),
)(nil)
if err != nil {
t.Fatalf("couldn't initialze mock IAM handler: %s", err)
}
b.iamClient = miam
err = b.createCredential(bgCTX, config.StorageView, cred.config, true)
if err != nil {
t.Fatalf("couldn't insert credential %d: %s", i, err)
}
item := &queue.Item{
Key: cred.config.Name,
Value: cred.config,
Priority: time.Now().Add(-1 * cred.age).Add(cred.config.RotationPeriod).Unix(),
}
err = b.credRotationQueue.Push(item)
if err != nil {
t.Fatalf("couldn't push item onto queue: %s", err)
}
}
// update aws responses, same argument for why it's okay every cred will be the same
miam, err := awsutil.NewMockIAM(
// old key
awsutil.WithListAccessKeysOutput(&iam.ListAccessKeysOutput{
AccessKeyMetadata: []*iam.AccessKeyMetadata{
{
AccessKeyId: aws.String(ak),
},
},
}),
// new key
awsutil.WithCreateAccessKeyOutput(&iam.CreateAccessKeyOutput{
AccessKey: &iam.AccessKey{
AccessKeyId: aws.String(ak),
SecretAccessKey: aws.String(newSecret),
},
}),
awsutil.WithGetUserOutput(&iam.GetUserOutput{
User: &iam.User{
UserId: aws.String("unique-id"),
UserName: aws.String("jane-doe"),
},
}),
)(nil)
if err != nil {
t.Fatalf("couldn't initialze mock IAM handler: %s", err)
}
b.iamClient = miam
req := &logical.Request{
Storage: config.StorageView,
}
err = b.rotateExpiredStaticCreds(bgCTX, req)
if err != nil {
t.Fatalf("got an error rotating credentials: %s", err)
}
// check our credentials
for i, cred := range c.creds {
entry, err := config.StorageView.Get(bgCTX, formatCredsStoragePath(cred.config.Name))
if err != nil {
t.Fatalf("got an error retrieving credentials %d", i)
}
var out awsCredentials
err = entry.DecodeJSON(&out)
if err != nil {
t.Fatalf("could not unmarshal storage view entry for cred %d to an aws credential: %s", i, err)
}
if cred.changed && out.SecretAccessKey != newSecret {
t.Fatalf("expected the key for cred %d to have changed, but it hasn't", i)
} else if !cred.changed && out.SecretAccessKey != oldSecret {
t.Fatalf("expected the key for cred %d to have stayed the same, but it changed", i)
}
}
})
}
}
type fakeIAM struct {
iamiface.IAMAPI
delReqs []*iam.DeleteAccessKeyInput
}
func (f *fakeIAM) DeleteAccessKey(r *iam.DeleteAccessKeyInput) (*iam.DeleteAccessKeyOutput, error) {
f.delReqs = append(f.delReqs, r)
return f.IAMAPI.DeleteAccessKey(r)
}
// TestCreateCredential verifies that credential creation firstly only deletes credentials if it needs to (i.e., two
// or more credentials on IAM), and secondly correctly deletes the oldest one.
func TestCreateCredential(t *testing.T) {
cases := []struct {
name string
username string
id string
deletedKey string
opts []awsutil.MockIAMOption
}{
{
name: "zero keys",
username: "jane-doe",
id: "unique-id",
opts: []awsutil.MockIAMOption{
awsutil.WithListAccessKeysOutput(&iam.ListAccessKeysOutput{
AccessKeyMetadata: []*iam.AccessKeyMetadata{},
}),
// delete should _not_ be called
awsutil.WithDeleteAccessKeyError(errors.New("should not have been called")),
awsutil.WithCreateAccessKeyOutput(&iam.CreateAccessKeyOutput{
AccessKey: &iam.AccessKey{
AccessKeyId: aws.String("key"),
SecretAccessKey: aws.String("itsasecret"),
},
}),
awsutil.WithGetUserOutput(&iam.GetUserOutput{
User: &iam.User{
UserId: aws.String("unique-id"),
UserName: aws.String("jane-doe"),
},
}),
},
},
{
name: "one key",
username: "jane-doe",
id: "unique-id",
opts: []awsutil.MockIAMOption{
awsutil.WithListAccessKeysOutput(&iam.ListAccessKeysOutput{
AccessKeyMetadata: []*iam.AccessKeyMetadata{
{AccessKeyId: aws.String("foo"), CreateDate: aws.Time(time.Now())},
},
}),
// delete should _not_ be called
awsutil.WithDeleteAccessKeyError(errors.New("should not have been called")),
awsutil.WithCreateAccessKeyOutput(&iam.CreateAccessKeyOutput{
AccessKey: &iam.AccessKey{
AccessKeyId: aws.String("key"),
SecretAccessKey: aws.String("itsasecret"),
},
}),
awsutil.WithGetUserOutput(&iam.GetUserOutput{
User: &iam.User{
UserId: aws.String("unique-id"),
UserName: aws.String("jane-doe"),
},
}),
},
},
{
name: "two keys",
username: "jane-doe",
id: "unique-id",
deletedKey: "foo",
opts: []awsutil.MockIAMOption{
awsutil.WithListAccessKeysOutput(&iam.ListAccessKeysOutput{
AccessKeyMetadata: []*iam.AccessKeyMetadata{
{AccessKeyId: aws.String("foo"), CreateDate: aws.Time(time.Time{})},
{AccessKeyId: aws.String("bar"), CreateDate: aws.Time(time.Now())},
},
}),
awsutil.WithCreateAccessKeyOutput(&iam.CreateAccessKeyOutput{
AccessKey: &iam.AccessKey{
AccessKeyId: aws.String("key"),
SecretAccessKey: aws.String("itsasecret"),
},
}),
awsutil.WithGetUserOutput(&iam.GetUserOutput{
User: &iam.User{
UserId: aws.String("unique-id"),
UserName: aws.String("jane-doe"),
},
}),
},
},
}
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
miam, err := awsutil.NewMockIAM(
c.opts...,
)(nil)
if err != nil {
t.Fatal(err)
}
fiam := &fakeIAM{
IAMAPI: miam,
}
b := Backend(config)
b.iamClient = fiam
err = b.createCredential(context.Background(), config.StorageView, staticRoleEntry{Username: c.username, ID: c.id}, true)
if err != nil {
t.Fatalf("got an error we didn't expect: %q", err)
}
if c.deletedKey != "" {
if len(fiam.delReqs) != 1 {
t.Fatalf("called the wrong number of deletes (called %d deletes)", len(fiam.delReqs))
}
actualKey := *fiam.delReqs[0].AccessKeyId
if c.deletedKey != actualKey {
t.Fatalf("we deleted the wrong key: %q instead of %q", actualKey, c.deletedKey)
}
}
})
}
}