mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-14 18:47:01 +02:00
* 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>
494 lines
14 KiB
Go
494 lines
14 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
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,
|
|
}
|
|
}
|