vault/builtin/logical/aws/rotation_test.go
hashicorp-copywrite[bot] 0b12cdcfd1
[COMPLIANCE] License changes (#22290)
* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Adding explicit MPL license for sub-package.

This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository.

* Updating the license from MPL to Business Source License.

Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl.

* add missing license headers

* Update copyright file headers to BUS-1.1

* Fix test that expected exact offset on hcl file

---------

Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
Co-authored-by: Sarah Thompson <sthompson@hashicorp.com>
Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
2023-08-10 18:14:03 -07:00

352 lines
9.6 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
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)
}
}
})
}
}