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) } } }) } }