//go:build integration package integration import ( "testing" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/iam" "github.com/stretchr/testify/require" ) // Identifiers for the IAM User/AccessKey/Policy suite. Matching the E2E // source's names keeps a future side-by-side diff greppable. const ( iamUserAlice = "alice" iamUserBob = "bob" iamUserCharlie = "charlie" iamUserBobPath = "/engineering/" iamPolicyEC2ReadOnly = "EC2ReadOnly" iamPolicyFullAdmin = "FullAdmin" iamPolicyFullAdminPath = "/admin/" iamPolicyDenyTerminate = "DenyTerminate" iamPolicyIAMReadOnly = "IAMReadOnly" iamPolicyEC2DescribeAll = "EC2DescribeAll" ) // Inline policy documents mirroring the E2E source. const ( iamDocEC2ReadOnly = `{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["ec2:DescribeInstances", "ec2:DescribeVolumes", "ec2:DescribeVpcs"], "Resource": "*" }] }` iamDocFullAdmin = `{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": "*", "Resource": "*" }] }` iamDocDenyTerminate = `{ "Version": "2012-10-17", "Statement": [ {"Effect": "Allow", "Action": "ec2:*", "Resource": "*"}, {"Effect": "Deny", "Action": "ec2:TerminateInstances", "Resource": "*"} ] }` iamDocIAMReadOnly = `{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["iam:GetUser", "iam:ListUsers", "iam:ListPolicies", "iam:GetPolicy"], "Resource": "*" }] }` iamDocEC2DescribeAll = `{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": "ec2:Describe*", "Resource": "*" }] }` ) // iamPolicyARN builds arn:aws:iam:::policy/ for constructing a // deliberately-nonexistent policy ARN in negative-path assertions. Shared by // every file in this package that needs to address a policy that must not // resolve. func iamPolicyARN(account, key string) string { return "arn:aws:iam::" + account + ":policy/" + key } // iamRoleARN builds arn:aws:iam:::role/. func iamRoleARN(account, name string) string { return "arn:aws:iam::" + account + ":role/" + name } // iamGroupARN builds arn:aws:iam:::group/ (default path /). func iamGroupARN(account, name string) string { return "arn:aws:iam::" + account + ":group/" + name } // iamFindKeyStatus returns the Status of a specific access key for a user. // Fails the test if the key isn't found — the caller has already asserted // the key exists. func iamFindKeyStatus(t *testing.T, iamCli *iam.IAM, user, keyID string) string { t.Helper() out, err := iamCli.ListAccessKeys(&iam.ListAccessKeysInput{UserName: aws.String(user)}) require.NoError(t, err, "list-access-keys %s", user) for _, k := range out.AccessKeyMetadata { if aws.StringValue(k.AccessKeyId) == keyID { return aws.StringValue(k.Status) } } t.Fatalf("access key %s not found for user %s", keyID, user) return "" } // TestIAMUserCRUD proves CreateUser/GetUser/ListUsers/DeleteUser plus the // EntityAlreadyExists / NoSuchEntity negative paths, and DeleteUser // idempotency (a second delete surfaces NoSuchEntity rather than // silently succeeding). All IAM actions dispatch straight to gw.IAMService // with no NATS hop, so this needs no stub. func TestIAMUserCRUD(t *testing.T) { gw := StartGateway(t) iamCli := gw.IAMClient(t) // SeedBootstrap always seeds a root user, so a fresh gateway's account is // never truly empty. rootUsers, err := iamCli.ListUsers(&iam.ListUsersInput{}) require.NoError(t, err, "list-users (root)") require.NotEmpty(t, rootUsers.Users, "root list-users returned 0 — root account missing?") aliceOut, err := iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-user %s", iamUserAlice) require.Equal(t, iamUserAlice, aws.StringValue(aliceOut.User.UserName)) bobOut, err := iamCli.CreateUser(&iam.CreateUserInput{ UserName: aws.String(iamUserBob), Path: aws.String(iamUserBobPath), }) require.NoError(t, err, "create-user %s", iamUserBob) require.Equal(t, iamUserBobPath, aws.StringValue(bobOut.User.Path), "bob created without expected path") _, err = iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserAlice)}) requireAWSErrorCode(t, err, "EntityAlreadyExists") got, err := iamCli.GetUser(&iam.GetUserInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "get-user %s", iamUserAlice) require.Equal(t, iamUserAlice, aws.StringValue(got.User.UserName)) _, err = iamCli.GetUser(&iam.GetUserInput{UserName: aws.String("nonexistent")}) requireAWSErrorCode(t, err, "NoSuchEntity") listed, err := iamCli.ListUsers(&iam.ListUsersInput{}) require.NoError(t, err, "list-users") require.GreaterOrEqual(t, len(listed.Users), 3, "expected >=3 users (root+alice+bob), got %d", len(listed.Users)) eng, err := iamCli.ListUsers(&iam.ListUsersInput{PathPrefix: aws.String(iamUserBobPath)}) require.NoError(t, err, "list-users --path-prefix") require.Len(t, eng.Users, 1, "expected 1 user under %s, got %d", iamUserBobPath, len(eng.Users)) // DeleteUser idempotency: create a throwaway user, delete it, then delete // it again and assert the second delete surfaces NoSuchEntity rather than // silently succeeding (matches AWS semantics). const ephemeral = "iam-phase1-ephemeral" _, err = iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(ephemeral)}) require.NoError(t, err, "create-user %s", ephemeral) _, err = iamCli.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(ephemeral)}) require.NoError(t, err, "delete-user %s", ephemeral) _, err = iamCli.DeleteUser(&iam.DeleteUserInput{UserName: aws.String(ephemeral)}) requireAWSErrorCode(t, err, "NoSuchEntity") } // TestIAMAccessKeyLifecycle proves CreateAccessKey, ListAccessKeys, // UpdateAccessKey (deactivate/activate), DeleteAccessKey plus the // LimitExceeded (2-key cap) / NoSuchEntity negative paths. No NATS stub // needed — access keys are pure IAM state. func TestIAMAccessKeyLifecycle(t *testing.T) { gw := StartGateway(t) iamCli := gw.IAMClient(t) _, err := iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-user alice") _, err = iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserBob)}) require.NoError(t, err, "create-user bob") k1, err := iamCli.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-access-key 1") aliceKeyID := aws.StringValue(k1.AccessKey.AccessKeyId) require.NotEmpty(t, aliceKeyID, "empty AccessKeyId") require.NotEmpty(t, aws.StringValue(k1.AccessKey.SecretAccessKey), "empty SecretAccessKey") k2, err := iamCli.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-access-key 2") aliceKey2 := aws.StringValue(k2.AccessKey.AccessKeyId) require.NotEmpty(t, aliceKey2) _, err = iamCli.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(iamUserAlice)}) requireAWSErrorCode(t, err, "LimitExceeded") _, err = iamCli.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String("ghost")}) requireAWSErrorCode(t, err, "NoSuchEntity") keys, err := iamCli.ListAccessKeys(&iam.ListAccessKeysInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "list-access-keys alice") require.Len(t, keys.AccessKeyMetadata, 2, "alice key count") bobKeys, err := iamCli.ListAccessKeys(&iam.ListAccessKeysInput{UserName: aws.String(iamUserBob)}) require.NoError(t, err, "list-access-keys bob") require.Empty(t, bobKeys.AccessKeyMetadata, "bob should have 0 keys") _, err = iamCli.UpdateAccessKey(&iam.UpdateAccessKeyInput{ UserName: aws.String(iamUserAlice), AccessKeyId: aws.String(aliceKeyID), Status: aws.String(iam.StatusTypeInactive), }) require.NoError(t, err, "update-access-key deactivate") require.Equal(t, iam.StatusTypeInactive, iamFindKeyStatus(t, iamCli, iamUserAlice, aliceKeyID), "key not Inactive after update") _, err = iamCli.UpdateAccessKey(&iam.UpdateAccessKeyInput{ UserName: aws.String(iamUserAlice), AccessKeyId: aws.String(aliceKeyID), Status: aws.String(iam.StatusTypeActive), }) require.NoError(t, err, "update-access-key reactivate") _, err = iamCli.DeleteAccessKey(&iam.DeleteAccessKeyInput{ UserName: aws.String(iamUserAlice), AccessKeyId: aws.String(aliceKey2), }) require.NoError(t, err, "delete-access-key key2") after, err := iamCli.ListAccessKeys(&iam.ListAccessKeysInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "list-access-keys after delete") require.Len(t, after.AccessKeyMetadata, 1, "alice should have 1 key after delete") } // TestIAMUserAuthentication builds a scoped AWS client with alice's key and // confirms signing is honoured (active key) / rejected (deactivated key, bad // secret, bogus ID). Alice has no policy, so the authenticated call is // authz-denied — UnauthorizedOperation (not an authn error) proves the active key // signed and was accepted. The final root DescribeInstances call needs the // EC2 daemon subjects stubbed since it actually dispatches over NATS; every // other assertion here is rejected before dispatch. func TestIAMUserAuthentication(t *testing.T) { gw := StartGateway(t) iamCli := gw.IAMClient(t) stubEmptyInstanceBuckets(t, gw) gw.StubSubject(t, "ec2.DescribeInstances", mustMarshal(t, &ec2.DescribeInstancesOutput{})) _, err := iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-user alice") k, err := iamCli.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-access-key alice") aliceKeyID := aws.StringValue(k.AccessKey.AccessKeyId) aliceSecret := aws.StringValue(k.AccessKey.SecretAccessKey) // Active, correctly-signed key with no policy: the request authenticates // then default-denies. UnauthorizedOperation (vs the InvalidClientTokenId / // SignatureDoesNotMatch below) is the signal that authn succeeded. aliceCli := gw.ClientsWithCreds(t, aliceKeyID, aliceSecret) _, err = aliceCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) requireAWSErrorCode(t, err, "UnauthorizedOperation") _, err = iamCli.UpdateAccessKey(&iam.UpdateAccessKeyInput{ UserName: aws.String(iamUserAlice), AccessKeyId: aws.String(aliceKeyID), Status: aws.String(iam.StatusTypeInactive), }) require.NoError(t, err, "deactivate alice key") _, err = aliceCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) requireAWSErrorCode(t, err, "InvalidClientTokenId") _, err = iamCli.UpdateAccessKey(&iam.UpdateAccessKeyInput{ UserName: aws.String(iamUserAlice), AccessKeyId: aws.String(aliceKeyID), Status: aws.String(iam.StatusTypeActive), }) require.NoError(t, err, "reactivate alice key") badSecretCli := gw.ClientsWithCreds(t, aliceKeyID, "WRONG_SECRET_KEY_HERE_12345678901") _, err = badSecretCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) requireAWSErrorCode(t, err, "SignatureDoesNotMatch") fakeCli := gw.ClientsWithCreds(t, "AKIAXXXXXXXXXXXXXXXX", "doesntmatter") _, err = fakeCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) requireAWSErrorCode(t, err, "InvalidClientTokenId") _, err = gw.EC2Client(t).DescribeInstances(&ec2.DescribeInstancesInput{}) require.NoError(t, err, "root describe-instances") } // TestIAMPolicyCRUD proves CreatePolicy (5 variants), GetPolicy, // GetPolicyVersion, ListPolicies plus the EntityAlreadyExists / // MalformedPolicyDocument / NoSuchEntity negative paths. No NATS stub // needed — policies are pure IAM state. func TestIAMPolicyCRUD(t *testing.T) { gw := StartGateway(t) iamCli := gw.IAMClient(t) pol, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyEC2ReadOnly), PolicyDocument: aws.String(iamDocEC2ReadOnly), }) require.NoError(t, err, "create-policy %s", iamPolicyEC2ReadOnly) ec2roArn := aws.StringValue(pol.Policy.Arn) require.NotEmpty(t, ec2roArn, "empty policy ARN") // The remaining four policies have no cross-dependency, so fan them out // in parallel under a wrapping t.Run that blocks until all four complete. t.Run("create_policies_parallel", func(t *testing.T) { t.Run(iamPolicyFullAdmin, func(t *testing.T) { t.Parallel() _, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyFullAdmin), Path: aws.String(iamPolicyFullAdminPath), Description: aws.String("Full access to all services"), PolicyDocument: aws.String(iamDocFullAdmin), }) require.NoError(t, err, "create-policy %s", iamPolicyFullAdmin) }) t.Run(iamPolicyDenyTerminate, func(t *testing.T) { t.Parallel() _, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyDenyTerminate), PolicyDocument: aws.String(iamDocDenyTerminate), }) require.NoError(t, err, "create-policy %s", iamPolicyDenyTerminate) }) t.Run(iamPolicyIAMReadOnly, func(t *testing.T) { t.Parallel() _, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyIAMReadOnly), PolicyDocument: aws.String(iamDocIAMReadOnly), }) require.NoError(t, err, "create-policy %s", iamPolicyIAMReadOnly) }) t.Run(iamPolicyEC2DescribeAll, func(t *testing.T) { t.Parallel() _, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyEC2DescribeAll), PolicyDocument: aws.String(iamDocEC2DescribeAll), }) require.NoError(t, err, "create-policy %s", iamPolicyEC2DescribeAll) }) }) _, err = iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyEC2ReadOnly), PolicyDocument: aws.String(iamDocFullAdmin), }) requireAWSErrorCode(t, err, "EntityAlreadyExists") _, err = iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String("BadPolicy"), PolicyDocument: aws.String(`{"not valid"}`), }) requireAWSErrorCode(t, err, "MalformedPolicyDocument") got, err := iamCli.GetPolicy(&iam.GetPolicyInput{PolicyArn: aws.String(ec2roArn)}) require.NoError(t, err, "get-policy %s", iamPolicyEC2ReadOnly) require.Equal(t, iamPolicyEC2ReadOnly, aws.StringValue(got.Policy.PolicyName)) _, err = iamCli.GetPolicy(&iam.GetPolicyInput{PolicyArn: aws.String(iamPolicyARN(gw.AccountID, "Ghost"))}) requireAWSErrorCode(t, err, "NoSuchEntity") pv, err := iamCli.GetPolicyVersion(&iam.GetPolicyVersionInput{ PolicyArn: aws.String(ec2roArn), VersionId: aws.String("v1"), }) require.NoError(t, err, "get-policy-version v1") require.Equal(t, "v1", aws.StringValue(pv.PolicyVersion.VersionId)) all, err := iamCli.ListPolicies(&iam.ListPoliciesInput{}) require.NoError(t, err, "list-policies") require.GreaterOrEqual(t, len(all.Policies), 5, "expected >=5 policies, got %d", len(all.Policies)) pathed, err := iamCli.ListPolicies(&iam.ListPoliciesInput{ Scope: aws.String("Local"), PathPrefix: aws.String(iamPolicyFullAdminPath), }) require.NoError(t, err, "list-policies scope Local path %s", iamPolicyFullAdminPath) require.Len(t, pathed.Policies, 1) require.Equal(t, iamPolicyFullAdmin, aws.StringValue(pathed.Policies[0].PolicyName)) attached, err := iamCli.ListPolicies(&iam.ListPoliciesInput{ PathPrefix: aws.String(iamPolicyFullAdminPath), OnlyAttached: aws.Bool(true), }) require.NoError(t, err, "list-policies only attached") require.Empty(t, attached.Policies) awsScope, err := iamCli.ListPolicies(&iam.ListPoliciesInput{Scope: aws.String("AWS")}) require.NoError(t, err, "list-policies scope AWS") require.Empty(t, awsScope.Policies) } // TestIAMPolicyAttachmentEnforcement proves AttachUserPolicy idempotency, // ListAttachedUserPolicies, and enforcement across default-deny, explicit // allow, wildcard-allow + explicit-deny, root bypass, prefix-wildcard and // FullAdmin, then DetachUserPolicy. DescribeRegions isn't used here — the // canary actions are DescribeInstances/DescribeVpcs/DescribeKeyPairs, all of // which dispatch over NATS, so the allow-path calls need their daemon // subjects stubbed (deny-path calls never reach NATS). func TestIAMPolicyAttachmentEnforcement(t *testing.T) { gw := StartGateway(t) iamCli := gw.IAMClient(t) adminAccount := gw.AccountID stubEmptyInstanceBuckets(t, gw) gw.StubSubject(t, "ec2.DescribeInstances", mustMarshal(t, &ec2.DescribeInstancesOutput{})) gw.StubSubject(t, "ec2.DescribeVpcs", mustMarshal(t, &ec2.DescribeVpcsOutput{})) gw.StubSubject(t, "ec2.DescribeKeyPairs", mustMarshal(t, &ec2.DescribeKeyPairsOutput{})) _, err := iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-user alice") _, err = iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserBob)}) require.NoError(t, err, "create-user bob") _, err = iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserCharlie)}) require.NoError(t, err, "create-user charlie") aliceKey, err := iamCli.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-access-key alice") bobKey, err := iamCli.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(iamUserBob)}) require.NoError(t, err, "create-access-key bob") charlieKey, err := iamCli.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(iamUserCharlie)}) require.NoError(t, err, "create-access-key charlie") aliceCli := gw.ClientsWithCreds(t, aws.StringValue(aliceKey.AccessKey.AccessKeyId), aws.StringValue(aliceKey.AccessKey.SecretAccessKey)) bobCli := gw.ClientsWithCreds(t, aws.StringValue(bobKey.AccessKey.AccessKeyId), aws.StringValue(bobKey.AccessKey.SecretAccessKey)) charlieCli := gw.ClientsWithCreds(t, aws.StringValue(charlieKey.AccessKey.AccessKeyId), aws.StringValue(charlieKey.AccessKey.SecretAccessKey)) ec2roPolicy, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyEC2ReadOnly), PolicyDocument: aws.String(iamDocEC2ReadOnly), }) require.NoError(t, err, "create-policy EC2ReadOnly") ec2roArn := aws.StringValue(ec2roPolicy.Policy.Arn) iamroPolicy, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyIAMReadOnly), PolicyDocument: aws.String(iamDocIAMReadOnly), }) require.NoError(t, err, "create-policy IAMReadOnly") iamroArn := aws.StringValue(iamroPolicy.Policy.Arn) denyPolicy, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyDenyTerminate), PolicyDocument: aws.String(iamDocDenyTerminate), }) require.NoError(t, err, "create-policy DenyTerminate") denyArn := aws.StringValue(denyPolicy.Policy.Arn) descAllPolicy, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyEC2DescribeAll), PolicyDocument: aws.String(iamDocEC2DescribeAll), }) require.NoError(t, err, "create-policy EC2DescribeAll") descAllArn := aws.StringValue(descAllPolicy.Policy.Arn) fullAdminPolicy, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyFullAdmin), Path: aws.String(iamPolicyFullAdminPath), PolicyDocument: aws.String(iamDocFullAdmin), }) require.NoError(t, err, "create-policy FullAdmin") fullAdminArn := aws.StringValue(fullAdminPolicy.Policy.Arn) _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{UserName: aws.String(iamUserAlice), PolicyArn: aws.String(ec2roArn)}) require.NoError(t, err, "attach EC2ReadOnly") _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{UserName: aws.String(iamUserAlice), PolicyArn: aws.String(iamroArn)}) require.NoError(t, err, "attach IAMReadOnly") _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{UserName: aws.String(iamUserBob), PolicyArn: aws.String(denyArn)}) require.NoError(t, err, "attach DenyTerminate") attached, err := iamCli.ListAttachedUserPolicies(&iam.ListAttachedUserPoliciesInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "list-attached-user-policies alice") require.Len(t, attached.AttachedPolicies, 2, "alice attached count") _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{UserName: aws.String(iamUserAlice), PolicyArn: aws.String(ec2roArn)}) require.NoError(t, err, "idempotent attach") attached2, err := iamCli.ListAttachedUserPolicies(&iam.ListAttachedUserPoliciesInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "list-attached after idempotent attach") require.Len(t, attached2.AttachedPolicies, 2, "attached count must not grow on idempotent re-attach") _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{ UserName: aws.String(iamUserAlice), PolicyArn: aws.String(iamPolicyARN(adminAccount, "Ghost")), }) requireAWSErrorCode(t, err, "NoSuchEntity") _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{UserName: aws.String("ghost"), PolicyArn: aws.String(ec2roArn)}) requireAWSErrorCode(t, err, "NoSuchEntity") // --- Enforcement --- _, err = charlieCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) requireAWSErrorCode(t, err, "UnauthorizedOperation") _, err = charlieCli.IAM.ListUsers(&iam.ListUsersInput{}) requireAWSErrorCode(t, err, "AccessDenied") _, err = aliceCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) require.NoError(t, err, "alice ec2:DescribeInstances") _, err = aliceCli.EC2.DescribeVpcs(&ec2.DescribeVpcsInput{}) require.NoError(t, err, "alice ec2:DescribeVpcs") _, err = aliceCli.IAM.ListUsers(&iam.ListUsersInput{}) require.NoError(t, err, "alice iam:ListUsers") _, err = aliceCli.EC2.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{}) requireAWSErrorCode(t, err, "UnauthorizedOperation") _, err = aliceCli.IAM.CreateUser(&iam.CreateUserInput{UserName: aws.String("hack")}) requireAWSErrorCode(t, err, "AccessDenied") _, err = bobCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) require.NoError(t, err, "bob ec2:DescribeInstances") _, err = bobCli.EC2.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{}) require.NoError(t, err, "bob ec2:DescribeKeyPairs") _, err = bobCli.EC2.TerminateInstances(&ec2.TerminateInstancesInput{InstanceIds: []*string{aws.String("i-fake")}}) requireAWSErrorCode(t, err, "UnauthorizedOperation") _, err = bobCli.IAM.ListUsers(&iam.ListUsersInput{}) requireAWSErrorCode(t, err, "AccessDenied") _, err = gw.EC2Client(t).DescribeInstances(&ec2.DescribeInstancesInput{}) require.NoError(t, err, "root ec2:DescribeInstances") _, err = iamCli.ListUsers(&iam.ListUsersInput{}) require.NoError(t, err, "root iam:ListUsers") _, err = iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String("temp")}) require.NoError(t, err, "root create-user temp") _, err = iamCli.DeleteUser(&iam.DeleteUserInput{UserName: aws.String("temp")}) require.NoError(t, err, "root delete-user temp") _, err = iamCli.DetachUserPolicy(&iam.DetachUserPolicyInput{UserName: aws.String(iamUserAlice), PolicyArn: aws.String(ec2roArn)}) require.NoError(t, err, "detach EC2ReadOnly") _, err = iamCli.DetachUserPolicy(&iam.DetachUserPolicyInput{UserName: aws.String(iamUserAlice), PolicyArn: aws.String(iamroArn)}) require.NoError(t, err, "detach IAMReadOnly") _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{UserName: aws.String(iamUserAlice), PolicyArn: aws.String(descAllArn)}) require.NoError(t, err, "attach EC2DescribeAll") _, err = aliceCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) require.NoError(t, err, "alice ec2:DescribeInstances (Describe*)") _, err = aliceCli.EC2.DescribeKeyPairs(&ec2.DescribeKeyPairsInput{}) require.NoError(t, err, "alice ec2:DescribeKeyPairs (Describe*)") _, err = aliceCli.EC2.CreateKeyPair(&ec2.CreateKeyPairInput{KeyName: aws.String("x")}) requireAWSErrorCode(t, err, "UnauthorizedOperation") _, err = aliceCli.IAM.ListUsers(&iam.ListUsersInput{}) requireAWSErrorCode(t, err, "AccessDenied") _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{UserName: aws.String(iamUserCharlie), PolicyArn: aws.String(fullAdminArn)}) require.NoError(t, err, "attach FullAdmin to charlie") _, err = charlieCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) require.NoError(t, err, "charlie ec2:DescribeInstances after FullAdmin") _, err = charlieCli.IAM.ListUsers(&iam.ListUsersInput{}) require.NoError(t, err, "charlie iam:ListUsers after FullAdmin") } // TestIAMPolicyLifecycle proves that detaching alice's policy revokes her // access, that deleting a still-attached policy surfaces DeleteConflict, and // that after detach + delete the policy is gone (NoSuchEntity). The // DescribeInstances allow-path needs its daemon subjects stubbed. func TestIAMPolicyLifecycle(t *testing.T) { gw := StartGateway(t) iamCli := gw.IAMClient(t) stubEmptyInstanceBuckets(t, gw) gw.StubSubject(t, "ec2.DescribeInstances", mustMarshal(t, &ec2.DescribeInstancesOutput{})) _, err := iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-user alice") _, err = iamCli.CreateUser(&iam.CreateUserInput{UserName: aws.String(iamUserBob)}) require.NoError(t, err, "create-user bob") key, err := iamCli.CreateAccessKey(&iam.CreateAccessKeyInput{UserName: aws.String(iamUserAlice)}) require.NoError(t, err, "create-access-key alice") aliceCli := gw.ClientsWithCreds(t, aws.StringValue(key.AccessKey.AccessKeyId), aws.StringValue(key.AccessKey.SecretAccessKey)) descAllPolicy, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyEC2DescribeAll), PolicyDocument: aws.String(iamDocEC2DescribeAll), }) require.NoError(t, err, "create-policy EC2DescribeAll") descAllArn := aws.StringValue(descAllPolicy.Policy.Arn) _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{UserName: aws.String(iamUserAlice), PolicyArn: aws.String(descAllArn)}) require.NoError(t, err, "attach EC2DescribeAll to alice") denyPolicy, err := iamCli.CreatePolicy(&iam.CreatePolicyInput{ PolicyName: aws.String(iamPolicyDenyTerminate), PolicyDocument: aws.String(iamDocDenyTerminate), }) require.NoError(t, err, "create-policy DenyTerminate") denyArn := aws.StringValue(denyPolicy.Policy.Arn) _, err = iamCli.AttachUserPolicy(&iam.AttachUserPolicyInput{UserName: aws.String(iamUserBob), PolicyArn: aws.String(denyArn)}) require.NoError(t, err, "attach DenyTerminate to bob") _, err = iamCli.DetachUserPolicy(&iam.DetachUserPolicyInput{UserName: aws.String(iamUserAlice), PolicyArn: aws.String(descAllArn)}) require.NoError(t, err, "detach EC2DescribeAll") _, err = aliceCli.EC2.DescribeInstances(&ec2.DescribeInstancesInput{}) requireAWSErrorCode(t, err, "UnauthorizedOperation") _, err = iamCli.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: aws.String(denyArn)}) requireAWSErrorCode(t, err, "DeleteConflict") _, err = iamCli.DetachUserPolicy(&iam.DetachUserPolicyInput{UserName: aws.String(iamUserBob), PolicyArn: aws.String(denyArn)}) require.NoError(t, err, "detach DenyTerminate") _, err = iamCli.DeletePolicy(&iam.DeletePolicyInput{PolicyArn: aws.String(denyArn)}) require.NoError(t, err, "delete-policy DenyTerminate") _, err = iamCli.GetPolicy(&iam.GetPolicyInput{PolicyArn: aws.String(denyArn)}) requireAWSErrorCode(t, err, "NoSuchEntity") }