open-vault/builtin/logical/aws/path_static_roles_test.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,
}
}