491 lines
13 KiB
Go
491 lines
13 KiB
Go
package aws
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/sdk/queue"
|
|
|
|
"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/framework"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
// TestStaticRolesValidation verifies that valid requests pass validation and that invalid requests fail validation.
|
|
// This includes the user already existing in IAM roles, and the rotation period being sufficiently long.
|
|
func TestStaticRolesValidation(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
bgCTX := context.Background() // for brevity
|
|
|
|
cases := []struct {
|
|
name string
|
|
opts []awsutil.MockIAMOption
|
|
requestData map[string]interface{}
|
|
isError bool
|
|
}{
|
|
{
|
|
name: "all good",
|
|
opts: []awsutil.MockIAMOption{
|
|
awsutil.WithGetUserOutput(&iam.GetUserOutput{User: &iam.User{UserName: aws.String("jane-doe"), UserId: aws.String("unique-id")}}),
|
|
awsutil.WithCreateAccessKeyOutput(&iam.CreateAccessKeyOutput{
|
|
AccessKey: &iam.AccessKey{
|
|
AccessKeyId: aws.String("abcdefghijklmnopqrstuvwxyz"),
|
|
SecretAccessKey: aws.String("zyxwvutsrqponmlkjihgfedcba"),
|
|
UserName: aws.String("jane-doe"),
|
|
},
|
|
}),
|
|
awsutil.WithListAccessKeysOutput(&iam.ListAccessKeysOutput{
|
|
AccessKeyMetadata: []*iam.AccessKeyMetadata{},
|
|
IsTruncated: aws.Bool(false),
|
|
}),
|
|
},
|
|
requestData: map[string]interface{}{
|
|
"name": "test",
|
|
"username": "jane-doe",
|
|
"rotation_period": "1d",
|
|
},
|
|
},
|
|
{
|
|
name: "bad user",
|
|
opts: []awsutil.MockIAMOption{
|
|
awsutil.WithGetUserError(errors.New("oh no")),
|
|
},
|
|
requestData: map[string]interface{}{
|
|
"name": "test",
|
|
"username": "jane-doe",
|
|
"rotation_period": "24h",
|
|
},
|
|
isError: true,
|
|
},
|
|
{
|
|
name: "user mismatch",
|
|
opts: []awsutil.MockIAMOption{
|
|
awsutil.WithGetUserOutput(&iam.GetUserOutput{User: &iam.User{UserName: aws.String("ms-impostor"), UserId: aws.String("fake-id")}}),
|
|
},
|
|
requestData: map[string]interface{}{
|
|
"name": "test",
|
|
"username": "jane-doe",
|
|
"rotation_period": "1d2h",
|
|
},
|
|
isError: true,
|
|
},
|
|
{
|
|
name: "bad rotation period",
|
|
opts: []awsutil.MockIAMOption{
|
|
awsutil.WithGetUserOutput(&iam.GetUserOutput{User: &iam.User{UserName: aws.String("jane-doe"), UserId: aws.String("unique-id")}}),
|
|
},
|
|
requestData: map[string]interface{}{
|
|
"name": "test",
|
|
"username": "jane-doe",
|
|
"rotation_period": "45s",
|
|
},
|
|
isError: true,
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
b := Backend(config)
|
|
miam, err := awsutil.NewMockIAM(c.opts...)(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b.iamClient = miam
|
|
if err := b.Setup(bgCTX, config); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
req := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Storage: config.StorageView,
|
|
Data: c.requestData,
|
|
Path: "static-roles/test",
|
|
}
|
|
_, err = b.pathStaticRolesWrite(bgCTX, req, staticRoleFieldData(req.Data))
|
|
if c.isError && err == nil {
|
|
t.Fatal("expected an error but didn't get one")
|
|
} else if !c.isError && err != nil {
|
|
t.Fatalf("got an unexpected error: %s", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestStaticRolesWrite validates that we can write a new entry for a new static role, and that we correctly
|
|
// do not write if the request is invalid in some way.
|
|
func TestStaticRolesWrite(t *testing.T) {
|
|
bgCTX := context.Background()
|
|
|
|
cases := []struct {
|
|
name string
|
|
opts []awsutil.MockIAMOption
|
|
data map[string]interface{}
|
|
expectedError bool
|
|
findUser bool
|
|
isUpdate bool
|
|
}{
|
|
{
|
|
name: "happy path",
|
|
opts: []awsutil.MockIAMOption{
|
|
awsutil.WithGetUserOutput(&iam.GetUserOutput{User: &iam.User{UserName: aws.String("jane-doe"), UserId: aws.String("unique-id")}}),
|
|
awsutil.WithListAccessKeysOutput(&iam.ListAccessKeysOutput{
|
|
AccessKeyMetadata: []*iam.AccessKeyMetadata{},
|
|
IsTruncated: aws.Bool(false),
|
|
}),
|
|
awsutil.WithCreateAccessKeyOutput(&iam.CreateAccessKeyOutput{
|
|
AccessKey: &iam.AccessKey{
|
|
AccessKeyId: aws.String("abcdefghijklmnopqrstuvwxyz"),
|
|
SecretAccessKey: aws.String("zyxwvutsrqponmlkjihgfedcba"),
|
|
UserName: aws.String("jane-doe"),
|
|
},
|
|
}),
|
|
},
|
|
data: map[string]interface{}{
|
|
"name": "test",
|
|
"username": "jane-doe",
|
|
"rotation_period": "1d",
|
|
},
|
|
// writes role, writes cred
|
|
findUser: true,
|
|
},
|
|
{
|
|
name: "no aws user",
|
|
opts: []awsutil.MockIAMOption{
|
|
awsutil.WithGetUserError(errors.New("no such user, etc etc")),
|
|
},
|
|
data: map[string]interface{}{
|
|
"name": "test",
|
|
"username": "a-nony-mous",
|
|
"rotation_period": "15s",
|
|
},
|
|
expectedError: true,
|
|
},
|
|
{
|
|
name: "update existing user",
|
|
opts: []awsutil.MockIAMOption{
|
|
awsutil.WithGetUserOutput(&iam.GetUserOutput{User: &iam.User{UserName: aws.String("john-doe"), UserId: aws.String("unique-id")}}),
|
|
awsutil.WithListAccessKeysOutput(&iam.ListAccessKeysOutput{
|
|
AccessKeyMetadata: []*iam.AccessKeyMetadata{},
|
|
IsTruncated: aws.Bool(false),
|
|
}),
|
|
awsutil.WithCreateAccessKeyOutput(&iam.CreateAccessKeyOutput{
|
|
AccessKey: &iam.AccessKey{
|
|
AccessKeyId: aws.String("abcdefghijklmnopqrstuvwxyz"),
|
|
SecretAccessKey: aws.String("zyxwvutsrqponmlkjihgfedcba"),
|
|
UserName: aws.String("john-doe"),
|
|
},
|
|
}),
|
|
},
|
|
data: map[string]interface{}{
|
|
"name": "johnny",
|
|
"rotation_period": "19m",
|
|
},
|
|
findUser: true,
|
|
isUpdate: true,
|
|
},
|
|
}
|
|
|
|
// if a user exists (user doesn't exist is tested in validation)
|
|
// we'll check how many keys the user has - if it's two, we delete one.
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
|
|
miam, err := awsutil.NewMockIAM(
|
|
c.opts...,
|
|
)(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
b := Backend(config)
|
|
b.iamClient = miam
|
|
if err := b.Setup(bgCTX, config); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// put a role in storage for update tests
|
|
staticRole := staticRoleEntry{
|
|
Name: "johnny",
|
|
Username: "john-doe",
|
|
ID: "unique-id",
|
|
RotationPeriod: 24 * time.Hour,
|
|
}
|
|
entry, err := logical.StorageEntryJSON(formatRoleStoragePath(staticRole.Name), staticRole)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = config.StorageView.Put(bgCTX, entry)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Storage: config.StorageView,
|
|
Data: c.data,
|
|
Path: "static-roles/" + c.data["name"].(string),
|
|
}
|
|
|
|
r, err := b.pathStaticRolesWrite(bgCTX, req, staticRoleFieldData(req.Data))
|
|
if c.expectedError && err == nil {
|
|
t.Fatal(err)
|
|
} else if c.expectedError {
|
|
return // save us some if statements
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatalf("got an error back unexpectedly: %s", err)
|
|
}
|
|
|
|
if c.findUser && r == nil {
|
|
t.Fatal("response was nil, but it shouldn't have been")
|
|
}
|
|
|
|
role, err := config.StorageView.Get(bgCTX, req.Path)
|
|
if c.findUser && (err != nil || role == nil) {
|
|
t.Fatalf("couldn't find the role we should have stored: %s", err)
|
|
}
|
|
var actualData staticRoleEntry
|
|
err = role.DecodeJSON(&actualData)
|
|
if err != nil {
|
|
t.Fatalf("couldn't convert storage data to role entry: %s", err)
|
|
}
|
|
|
|
// construct expected data
|
|
var expectedData staticRoleEntry
|
|
fieldData := staticRoleFieldData(c.data)
|
|
if c.isUpdate {
|
|
// data is johnny + c.data
|
|
expectedData = staticRole
|
|
}
|
|
|
|
if u, ok := fieldData.GetOk("username"); ok {
|
|
expectedData.Username = u.(string)
|
|
}
|
|
if r, ok := fieldData.GetOk("rotation_period"); ok {
|
|
expectedData.RotationPeriod = time.Duration(r.(int)) * time.Second
|
|
}
|
|
if n, ok := fieldData.GetOk("name"); ok {
|
|
expectedData.Name = n.(string)
|
|
}
|
|
|
|
// validate fields
|
|
if eu, au := expectedData.Username, actualData.Username; eu != au {
|
|
t.Fatalf("mismatched username, expected %q but got %q", eu, au)
|
|
}
|
|
if er, ar := expectedData.RotationPeriod, actualData.RotationPeriod; er != ar {
|
|
t.Fatalf("mismatched rotation period, expected %q but got %q", er, ar)
|
|
}
|
|
if en, an := expectedData.Name, actualData.Name; en != an {
|
|
t.Fatalf("mismatched role name, expected %q, but got %q", en, an)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestStaticRoleRead validates that we can read a configured role and correctly do not read anything if we
|
|
// request something that doesn't exist.
|
|
func TestStaticRoleRead(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
bgCTX := context.Background()
|
|
|
|
// test cases are run against an inmem storage holding a role called "test" attached to an IAM user called "jane-doe"
|
|
cases := []struct {
|
|
name string
|
|
roleName string
|
|
found bool
|
|
}{
|
|
{
|
|
name: "role name exists",
|
|
roleName: "test",
|
|
found: true,
|
|
},
|
|
{
|
|
name: "role name not found",
|
|
roleName: "toast",
|
|
found: false, // implied, but set for clarity
|
|
},
|
|
}
|
|
|
|
staticRole := staticRoleEntry{
|
|
Name: "test",
|
|
Username: "jane-doe",
|
|
RotationPeriod: 24 * time.Hour,
|
|
}
|
|
entry, err := logical.StorageEntryJSON(formatRoleStoragePath(staticRole.Name), staticRole)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = config.StorageView.Put(bgCTX, entry)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
req := &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Storage: config.StorageView,
|
|
Data: map[string]interface{}{
|
|
"name": c.roleName,
|
|
},
|
|
Path: formatRoleStoragePath(c.roleName),
|
|
}
|
|
|
|
b := Backend(config)
|
|
|
|
r, err := b.pathStaticRolesRead(bgCTX, req, staticRoleFieldData(req.Data))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.found {
|
|
if r == nil {
|
|
t.Fatal("response was nil, but it shouldn't have been")
|
|
}
|
|
} else {
|
|
if r != nil {
|
|
t.Fatal("response should have been nil on a non-existent role")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestStaticRoleDelete validates that we correctly remove a role on a delete request, and that we correctly do not
|
|
// remove anything if a role does not exist with that name.
|
|
func TestStaticRoleDelete(t *testing.T) {
|
|
bgCTX := context.Background()
|
|
|
|
// test cases are run against an inmem storage holding a role called "test" attached to an IAM user called "jane-doe"
|
|
cases := []struct {
|
|
name string
|
|
role string
|
|
found bool
|
|
}{
|
|
{
|
|
name: "role found",
|
|
role: "test",
|
|
found: true,
|
|
},
|
|
{
|
|
name: "role not found",
|
|
role: "tossed",
|
|
found: false,
|
|
},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
|
|
// fake an IAM
|
|
var iamfunc awsutil.IAMAPIFunc
|
|
if !c.found {
|
|
iamfunc = awsutil.NewMockIAM(awsutil.WithDeleteAccessKeyError(errors.New("shouldn't have called delete")))
|
|
} else {
|
|
iamfunc = awsutil.NewMockIAM()
|
|
}
|
|
miam, err := iamfunc(nil)
|
|
if err != nil {
|
|
t.Fatalf("couldn't initialize mockiam: %s", err)
|
|
}
|
|
|
|
b := Backend(config)
|
|
b.iamClient = miam
|
|
|
|
// put in storage
|
|
staticRole := staticRoleEntry{
|
|
Name: "test",
|
|
Username: "jane-doe",
|
|
RotationPeriod: 24 * time.Hour,
|
|
}
|
|
entry, err := logical.StorageEntryJSON(formatRoleStoragePath(staticRole.Name), staticRole)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
err = config.StorageView.Put(bgCTX, entry)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
l, err := config.StorageView.List(bgCTX, "")
|
|
if err != nil || len(l) != 1 {
|
|
t.Fatalf("couldn't add an entry to storage during test setup: %s", err)
|
|
}
|
|
|
|
// put in queue
|
|
err = b.credRotationQueue.Push(&queue.Item{
|
|
Key: staticRole.Name,
|
|
Value: staticRole,
|
|
Priority: time.Now().Add(90 * time.Hour).Unix(),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("couldn't add items to pq")
|
|
}
|
|
|
|
req := &logical.Request{
|
|
Operation: logical.ReadOperation,
|
|
Storage: config.StorageView,
|
|
Data: map[string]interface{}{
|
|
"name": c.role,
|
|
},
|
|
Path: formatRoleStoragePath(c.role),
|
|
}
|
|
|
|
r, err := b.pathStaticRolesDelete(bgCTX, req, staticRoleFieldData(req.Data))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if r != nil {
|
|
t.Fatal("response wasn't nil, but it should have been")
|
|
}
|
|
|
|
l, err = config.StorageView.List(bgCTX, "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.found && len(l) != 0 {
|
|
t.Fatal("size of role storage is non zero after delete")
|
|
} else if !c.found && len(l) != 1 {
|
|
t.Fatal("size of role storage changed after what should have been no deletion")
|
|
}
|
|
|
|
if c.found && b.credRotationQueue.Len() != 0 {
|
|
t.Fatal("size of queue is non-zero after delete")
|
|
} else if !c.found && b.credRotationQueue.Len() != 1 {
|
|
t.Fatal("size of queue changed after what should have been no deletion")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func staticRoleFieldData(data map[string]interface{}) *framework.FieldData {
|
|
schema := map[string]*framework.FieldSchema{
|
|
paramRoleName: {
|
|
Type: framework.TypeString,
|
|
Description: descRoleName,
|
|
},
|
|
paramUsername: {
|
|
Type: framework.TypeString,
|
|
Description: descUsername,
|
|
},
|
|
paramRotationPeriod: {
|
|
Type: framework.TypeDurationSecond,
|
|
Description: descRotationPeriod,
|
|
},
|
|
}
|
|
|
|
return &framework.FieldData{
|
|
Raw: data,
|
|
Schema: schema,
|
|
}
|
|
}
|