diff --git a/go.mod b/go.mod index 377752c88a..9145591e39 100644 --- a/go.mod +++ b/go.mod @@ -54,6 +54,7 @@ require ( github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/aws/aws-sdk-go v1.55.8 github.com/aws/aws-sdk-go-v2/config v1.32.14 + github.com/aws/aws-sdk-go-v2/service/iam v1.53.8 github.com/cenkalti/backoff/v4 v4.3.0 github.com/chrismalek/oktasdk-go v0.0.0-20181212195951-3430665dfaa0 github.com/cockroachdb/cockroach-go/v2 v2.3.8 @@ -333,14 +334,14 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/apache/arrow-go/v18 v18.4.0 // indirect github.com/avast/retry-go/v4 v4.6.1 // indirect - github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect + github.com/aws/aws-sdk-go-v2 v1.41.6 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.21 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect github.com/aws/aws-sdk-go-v2/service/ec2 v1.297.0 // indirect @@ -352,8 +353,8 @@ require ( github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect - github.com/aws/smithy-go v1.24.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 + github.com/aws/smithy-go v1.25.0 // indirect github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect github.com/benbjohnson/immutable v0.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/go.sum b/go.sum index 6eda960b17..802b589b16 100644 --- a/go.sum +++ b/go.sum @@ -209,8 +209,8 @@ github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBea github.com/aws/aws-sdk-go v1.34.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= -github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY= -github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2 v1.41.6 h1:1AX0AthnBQzMx1vbmir3Y4WsnJgiydmnJjiLu+LvXOg= +github.com/aws/aws-sdk-go-v2 v1.41.6/go.mod h1:dy0UzBIfwSeot4grGvY1AqFWN5zgziMmWGzysDnHFcQ= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI= @@ -223,10 +223,10 @@ github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.21 h1:HFn8sVT87KWnGs2Q2gO/brP github.com/aws/aws-sdk-go-v2/feature/rds/auth v1.6.21/go.mod h1:BGZ/K6gLGJt8K36j6gcsD7WVxmWt0MGBYtr57iLweio= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.13 h1:uMC4oL6G3MNhodo358QEqSDjrgvzV3TUQ58nyQSGq2E= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.13/go.mod h1:Cer86AE2686DvVUe57LPve3jUBmbujuaonSX8pNzGgw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22 h1:GmLa5Kw1ESqtFpXsx5MmC84QWa/ZrLZvlJGa2y+4kcQ= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.22/go.mod h1:6sW9iWm9DK9YRpRGga/qzrzNLgKpT2cIxb7Vo2eNOp0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22 h1:dY4kWZiSaXIzxnKlj17nHnBcXXBfac6UlsAx2qL6XrU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.22/go.mod h1:KIpEUx0JuRZLO7U6cbV204cWAEco2iC3l061IxlwLtI= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ= @@ -235,6 +235,8 @@ github.com/aws/aws-sdk-go-v2/service/ec2 v1.297.0 h1:A+7NViqbMUCoTQFWjbSXdbzE4K5 github.com/aws/aws-sdk-go-v2/service/ec2 v1.297.0/go.mod h1:R+2BNtUfTfhPY0RH18oL02q116bakeBWjanrbnVBqkM= github.com/aws/aws-sdk-go-v2/service/ecs v1.77.0 h1:g3RYQmK6uRU5kOuwDthemuiiTbmwyGd8Wzf+k7cYWtk= github.com/aws/aws-sdk-go-v2/service/ecs v1.77.0/go.mod h1:QkWmubOYmjj3cHn7A4CoUU7BKJhVeo39Gp6NH7IyhZw= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.8 h1:p0oB4eZfBfBAOasnKvHJOlNcuHVE/ieuWs7uIZgQlyQ= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.8/go.mod h1:epCaPnGVdiX5ra1lHPfRkVuiQGxrdY8bRI2FBJU+6ok= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM= @@ -253,8 +255,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6f github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w= github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U= github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw= -github.com/aws/smithy-go v1.24.3 h1:XgOAaUgx+HhVBoP4v8n6HCQoTRDhoMghKqw4LNHsDNg= -github.com/aws/smithy-go v1.24.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.25.0 h1:Sz/XJ64rwuiKtB6j98nDIPyYrV1nVNJ4YU74gttcl5U= +github.com/aws/smithy-go v1.25.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= github.com/benbjohnson/immutable v0.4.0 h1:CTqXbEerYso8YzVPxmWxh2gnoRQbbB9X1quUC8+vGZA= diff --git a/sdk/helper/testcluster/blackbox/session_logical.go b/sdk/helper/testcluster/blackbox/session_logical.go index c3d8601b8e..8bd7a4a961 100644 --- a/sdk/helper/testcluster/blackbox/session_logical.go +++ b/sdk/helper/testcluster/blackbox/session_logical.go @@ -26,6 +26,14 @@ func (s *Session) MustRead(path string) *api.Secret { return secret } +func (s *Session) MustList(path string) *api.Secret { + s.t.Helper() + + secret, err := s.Client.Logical().List(path) + require.NoError(s.t, err) + return secret +} + // MustReadRequired is a stricter version of MustRead that fails if a 404/nil is returned func (s *Session) MustReadRequired(path string) *api.Secret { s.t.Helper() diff --git a/vault/external_tests/blackbox/plugins/aws/helpers.go b/vault/external_tests/blackbox/plugins/aws/helpers.go new file mode 100644 index 0000000000..c70e26ac90 --- /dev/null +++ b/vault/external_tests/blackbox/plugins/aws/helpers.go @@ -0,0 +1,265 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package aws + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "strings" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/sts" +) + +// getPolicyArnByName returns the ARN for a policy with the given name. +func getPolicyArnByName(ctx context.Context, iamClient *iam.Client, policyName string) (string, error) { + paginator := iam.NewListPoliciesPaginator(iamClient, &iam.ListPoliciesInput{Scope: "All"}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + return "", err + } + for _, p := range page.Policies { + if aws.ToString(p.PolicyName) == policyName { + return aws.ToString(p.Arn), nil + } + } + } + return "", fmt.Errorf("policy %s not found", policyName) +} + +// getRoleArnByName returns the ARN for a role with the given name. +func getRoleArnByName(ctx context.Context, iamClient *iam.Client, roleName string) (string, error) { + paginator := iam.NewListRolesPaginator(iamClient, &iam.ListRolesInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + return "", err + } + for _, r := range page.Roles { + if aws.ToString(r.RoleName) == roleName { + return aws.ToString(r.Arn), nil + } + } + } + return "", fmt.Errorf("role %s not found", roleName) +} + +// createTestIAMUser creates a new IAM user with a unique name, attaches the DemoUser policy, and returns the user/access key info and AWS region. +func createTestIAMUser(t *testing.T) ( + userName string, + accessKeyID string, + secretAccessKey string, + demoUserPolicyArn string, + assumedRoleArn string, + awsRegion string, +) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + t.Fatalf("failed to load AWS config: %v", err) + } + awsRegion = cfg.Region + if awsRegion == "" { + t.Fatalf("AWS region is empty in config") + } + iamClient := iam.NewFromConfig(cfg) + stsClient := sts.NewFromConfig(cfg) + + // Get current AWS account identity (for unique name) + caller, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + t.Fatalf("failed to get caller identity: %v", err) + } + accountID := aws.ToString(caller.Account) + + // Generate a random hex suffix for uniqueness + const randomSuffixByteLength = 4 + suffix := make([]byte, randomSuffixByteLength) + if _, err := rand.Read(suffix); err != nil { + t.Fatalf("failed to generate random suffix: %v", err) + } + hexSuffix := hex.EncodeToString(suffix) + userName = fmt.Sprintf("demo-GitHubActions-%s-%s", accountID, hexSuffix) + + // Lookup DemoUser policy ARN + demoUserPolicyArn, err = getPolicyArnByName(ctx, iamClient, "DemoUser") + if err != nil { + t.Fatalf("DemoUser policy not found: %v", err) + } + + // Lookup vault-assumed-role-credentials-demo role ARN + assumedRoleArn, err = getRoleArnByName(ctx, iamClient, "vault-assumed-role-credentials-demo") + if err != nil { + t.Fatalf("vault-assumed-role-credentials-demo role not found: %v", err) + } + + // Create IAM user + _, err = iamClient.CreateUser(ctx, &iam.CreateUserInput{ + UserName: aws.String(userName), + PermissionsBoundary: aws.String(demoUserPolicyArn), + }) + if err != nil { + t.Fatalf("failed to create IAM user: %v", err) + } + + // Attach policy to user + _, err = iamClient.AttachUserPolicy(ctx, &iam.AttachUserPolicyInput{ + UserName: aws.String(userName), + PolicyArn: aws.String(demoUserPolicyArn), + }) + if err != nil { + t.Fatalf("failed to attach policy: %v", err) + } + + // Create access key + keyOut, err := iamClient.CreateAccessKey(ctx, &iam.CreateAccessKeyInput{ + UserName: aws.String(userName), + }) + if err != nil { + t.Fatalf("failed to create access key: %v", err) + } + accessKeyID = aws.ToString(keyOut.AccessKey.AccessKeyId) + secretAccessKey = aws.ToString(keyOut.AccessKey.SecretAccessKey) + + // IAM is eventually consistent; wait briefly before verifying the user is readable. + t.Logf("Verifying IAM user %s exists...", userName) + waitTime := 10 * time.Second + verifyDeadline := time.Now().Add(waitTime * 2) + var lastErr error + for time.Now().Before(verifyDeadline) { + time.Sleep(waitTime) + _, lastErr = iamClient.GetUser(ctx, &iam.GetUserInput{UserName: aws.String(userName)}) + if lastErr == nil { + break + } + t.Logf("IAM user %q not readable yet; retrying: %v", userName, lastErr) + } + if lastErr != nil { + t.Fatalf("failed to verify IAM user %q: %v", userName, lastErr) + } + + return userName, accessKeyID, secretAccessKey, demoUserPolicyArn, assumedRoleArn, awsRegion +} + +// getAwsUsernameTemplate returns the username template string for Vault AWS config. +func getAwsUsernameTemplate(awsUserName string) string { + const prefix = `{{ if (eq .Type "STS") }}{{ printf "` + const stsSuffix = `-%s-%s" (random 20) (unix_time) | truncate 32 }}{{ else }}{{ printf "` + const iamUserSuffix = `-%s-%s" (unix_time) (random 20) | truncate 60 }}{{ end }}` + return prefix + awsUserName + stsSuffix + awsUserName + iamUserSuffix +} + +// getAllowDescribeRegionsPolicy returns a policy document allowing ec2:DescribeRegions. +func getAllowDescribeRegionsPolicy() string { + return `{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": ["ec2:DescribeRegions"], + "Resource": ["*"] + } + ] +}` +} + +// deleteIAMUserByAccessKey deletes the IAM user that owns the given access key. +func deleteIAMUserByAccessKey(t *testing.T, targetAccessKeyID string) { + t.Helper() + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + + cfg, err := config.LoadDefaultConfig(ctx) + if err != nil { + t.Fatalf("failed to load AWS config: %v", err) + } + iamClient := iam.NewFromConfig(cfg) + + paginator := iam.NewListUsersPaginator(iamClient, &iam.ListUsersInput{}) + for paginator.HasMorePages() { + page, err := paginator.NextPage(ctx) + if err != nil { + t.Fatalf("failed to list IAM users: %v", err) + } + for _, user := range page.Users { + userName := aws.ToString(user.UserName) + if !strings.Contains(userName, "demo-GitHubActions") { + continue + } + // List all access keys for this user + keyPaginator := iam.NewListAccessKeysPaginator(iamClient, &iam.ListAccessKeysInput{ + UserName: &userName, + }) + for keyPaginator.HasMorePages() { + keyPage, err := keyPaginator.NextPage(ctx) + if err != nil { + t.Logf("warning: failed to list access keys for user %q: %v", userName, err) + continue + } + for _, key := range keyPage.AccessKeyMetadata { + accessKeyId := aws.ToString(key.AccessKeyId) + if accessKeyId == targetAccessKeyID { + // Found the user with the target access key. Detach managed policies first, + // then delete all access keys, then delete the user. + policyPaginator := iam.NewListAttachedUserPoliciesPaginator(iamClient, &iam.ListAttachedUserPoliciesInput{ + UserName: &userName, + }) + for policyPaginator.HasMorePages() { + policyPage, err := policyPaginator.NextPage(ctx) + if err != nil { + t.Logf("warning: failed to list attached policies for user %q: %v", userName, err) + continue + } + for _, policy := range policyPage.AttachedPolicies { + policyArn := aws.ToString(policy.PolicyArn) + if _, err := iamClient.DetachUserPolicy(ctx, &iam.DetachUserPolicyInput{ + UserName: &userName, + PolicyArn: &policyArn, + }); err != nil { + t.Logf("warning: failed to detach policy %q from user %q: %v", policyArn, userName, err) + } + } + } + keyPaginator2 := iam.NewListAccessKeysPaginator(iamClient, &iam.ListAccessKeysInput{ + UserName: &userName, + }) + for keyPaginator2.HasMorePages() { + keyPage2, err := keyPaginator2.NextPage(ctx) + if err != nil { + t.Logf("warning: failed to list access keys for cleanup on user %q: %v", userName, err) + continue + } + for _, key2 := range keyPage2.AccessKeyMetadata { + accessKeyId2 := aws.ToString(key2.AccessKeyId) + if _, err := iamClient.DeleteAccessKey(ctx, &iam.DeleteAccessKeyInput{ + UserName: &userName, + AccessKeyId: &accessKeyId2, + }); err != nil { + t.Logf("warning: failed to delete access key %q for user %q: %v", accessKeyId2, userName, err) + } + } + } + // Delete the user + if _, err := iamClient.DeleteUser(ctx, &iam.DeleteUserInput{ + UserName: &userName, + }); err != nil { + t.Logf("warning: failed to delete user %q: %v", userName, err) + } + return + } + } + } + } + } +} diff --git a/vault/external_tests/blackbox/plugins/aws/secrets_aws_test.go b/vault/external_tests/blackbox/plugins/aws/secrets_aws_test.go new file mode 100644 index 0000000000..e41c9bff1b --- /dev/null +++ b/vault/external_tests/blackbox/plugins/aws/secrets_aws_test.go @@ -0,0 +1,102 @@ +// Copyright IBM Corp. 2025, 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package aws + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/sdk/helper/testcluster/blackbox" +) + +// TestAWS_GenerateNewUser verifies AWS secrets engine can generate IAM user credentials. +func TestAWS_GenerateNewUser(t *testing.T) { + t.Parallel() + v := blackbox.New(t) + + accessKey := os.Getenv("AWS_ACCESS_KEY_ID") + secretKey := os.Getenv("AWS_SECRET_ACCESS_KEY") + if accessKey == "" || secretKey == "" { + t.Log("AWS credentials not available - skipping AWS secrets engine test") + t.Skip("AWS credentials not available - skipping AWS secrets engine test") + } + + t.Logf("Creating test IAM user via helpers.go...") + userName, tempAccessKeyId, tempSecretAccessKey, demoUserPolicyArn, _, _ := createTestIAMUser(t) + t.Logf("Created test IAM user: %s", userName) + var newAccessKey string + t.Cleanup(func() { + if newAccessKey != "" { + t.Logf("Cleanup: deleting IAM user created by Vault with access key: %s", newAccessKey) + deleteIAMUserByAccessKey(t, newAccessKey) + } + + t.Logf("Cleanup: deleting IAM user by initial access key: %s", tempAccessKeyId) + deleteIAMUserByAccessKey(t, tempAccessKeyId) + }) + + path := fmt.Sprintf("aws-test-%d", time.Now().UnixNano()) + t.Logf("Enabling AWS secrets engine at path: %s", path) + v.MustEnableSecretsEngine(path, &api.MountInput{Type: "aws"}) + + t.Logf("Configuring AWS secrets engine with root credentials and username template for user: %s", userName) + v.MustWrite(fmt.Sprintf("%s/config/root", path), map[string]any{ + "access_key": tempAccessKeyId, + "secret_key": tempSecretAccessKey, + "region": "us-east-1", + "username_template": getAwsUsernameTemplate(userName), + }) + + roleName := "aws-enos-role" + t.Logf("Creating Vault AWS role: %s", roleName) + v.MustWrite(fmt.Sprintf("%s/roles/%s", path, roleName), map[string]any{ + "credential_type": "iam_user", + "permissions_boundary_arn": demoUserPolicyArn, + "policy_document": getAllowDescribeRegionsPolicy(), + }) + + t.Logf("Reading and verifying AWS role configuration for role: %s", roleName) + roleResp := v.MustRead(fmt.Sprintf("%s/roles/%s", path, roleName)) + if roleResp.Data == nil { + t.Fatal("Expected to read AWS role configuration") + } + + t.Logf("Listing AWS roles at path: %s/roles", path) + rolesList := v.MustList(fmt.Sprintf("%s/roles", path)) + if rolesList == nil || rolesList.Data == nil { + t.Fatal("No AWS roles created! (rolesList is nil or Data is nil)") + } + roleKeys, ok := rolesList.Data["keys"].([]interface{}) + if !ok || len(roleKeys) == 0 { + t.Fatal("No AWS roles created! (rolesList.Data['keys'] is empty or not a slice)") + } + t.Logf("Found AWS roles: %v", roleKeys) + + t.Logf("Reading root config to verify username template is set correctly") + rootUser := v.MustRead(fmt.Sprintf("%s/config/root", path)) + if rootUser == nil || rootUser.Data == nil { + t.Fatalf("Expected to read root config, got nil: %#v", rootUser) + } + if val, ok := rootUser.Data["username_template"]; !ok || val == nil { + t.Fatalf("username_template missing in root config: %#v", rootUser) + } + + t.Logf("Generating new credentials for IAM user using role: %s", roleName) + newUser := v.MustRead(fmt.Sprintf("%s/creds/%s", path, roleName)) + if newUser == nil || newUser.Data == nil { + t.Fatalf("Failed to generate new credentials for IAM user: %s", roleName) + } + if val, ok := newUser.Data["access_key"]; !ok || val == nil || val == tempAccessKeyId { + t.Fatalf("The new access key is empty or is matching the old one: %v", val) + } + + newAccessKey, ok = newUser.Data["access_key"].(string) + if !ok || newAccessKey == "" { + t.Fatalf("Could not extract access_key from new credentials: %v", newUser.Data["access_key"]) + } + t.Logf("Captured Vault-created access key for cleanup: %s", newAccessKey) +}