Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iam/user: Force destroy with policies #36640

Merged
merged 8 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/36640.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_iam_user: When `force_destroy` is used and there are inline or attached policies, allow resource to be destroyed
```
2 changes: 1 addition & 1 deletion GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ sweep: prereq-go ## Run sweepers

sweeper: prereq-go ## Run sweepers with failures allowed
@echo "WARNING: This will destroy infrastructure. Use only in development accounts."
$(GO_VER) test $(SWEEP_DIR) -v -tags=sweep -sweep=$(SWEEP) -sweep-allow-failures -timeout $(SWEEP_TIMEOUT)
$(GO_VER) test $(SWEEP_DIR) -v -sweep=$(SWEEP) -sweep-allow-failures -timeout $(SWEEP_TIMEOUT)

t: prereq-go fmtcheck
TF_ACC=1 $(GO_VER) test ./$(PKG_NAME)/... -v -count $(TEST_COUNT) -parallel $(ACCTEST_PARALLELISM) $(RUNARGS) $(TESTARGS) -timeout $(ACCTEST_TIMEOUT)
Expand Down
1 change: 1 addition & 0 deletions internal/service/iam/exports.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ var (
DeleteServiceLinkedRole = deleteServiceLinkedRole
FindRoleByName = findRoleByName
ListGroupsForUserPages = listGroupsForUserPages
AttachPolicyToUser = attachPolicyToUser
)
148 changes: 16 additions & 132 deletions internal/service/iam/sweep.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,8 @@ func sweepUsers(region string) error {
"tf-acc",
"tf_acc",
}
users := make([]awstypes.User, 0)

var sweepResources []sweep.Sweepable

pages := iam.NewListUsersPaginator(conn, &iam.ListUsersInput{})
for pages.HasMorePages() {
Expand All @@ -616,145 +617,28 @@ func sweepUsers(region string) error {
for _, user := range page.Users {
for _, prefix := range prefixes {
if strings.HasPrefix(aws.ToString(user.UserName), prefix) {
users = append(users, user)
r := resourceUser()
d := r.Data(nil)
d.SetId(aws.ToString(user.UserName))
d.Set("force_destroy", true)

// In general, sweeping should use the resource's Delete function. If Delete
// is missing something that affects sweeping, fix Delete. Most of the time,
// if something in Delete is causing sweep problems, it's also affecting
// some users when they destroy.
sweepResources = append(sweepResources, sdk.NewSweepResource(r, d, client))
break
}
}
}
}

if len(users) == 0 {
log.Print("[DEBUG] No IAM Users to sweep")
return nil
}

var sweeperErrs *multierror.Error
for _, user := range users {
username := aws.ToString(user.UserName)
log.Printf("[DEBUG] Deleting IAM User: %s", username)

listUserPoliciesInput := &iam.ListUserPoliciesInput{
UserName: user.UserName,
}
listUserPoliciesOutput, err := conn.ListUserPolicies(ctx, listUserPoliciesInput)

if errs.IsA[*awstypes.NoSuchEntityException](err) {
continue
}
if err != nil {
sweeperErr := fmt.Errorf("error listing IAM User (%s) inline policies: %s", username, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}

for _, inlinePolicyName := range listUserPoliciesOutput.PolicyNames {
log.Printf("[DEBUG] Deleting IAM User (%s) inline policy %q", username, inlinePolicyName)

input := &iam.DeleteUserPolicyInput{
PolicyName: aws.String(inlinePolicyName),
UserName: user.UserName,
}

if _, err := conn.DeleteUserPolicy(ctx, input); err != nil {
if errs.IsA[*awstypes.NoSuchEntityException](err) {
continue
}
sweeperErr := fmt.Errorf("error deleting IAM User (%s) inline policy %q: %s", username, inlinePolicyName, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}
}

listAttachedUserPoliciesInput := &iam.ListAttachedUserPoliciesInput{
UserName: user.UserName,
}
listAttachedUserPoliciesOutput, err := conn.ListAttachedUserPolicies(ctx, listAttachedUserPoliciesInput)

if errs.IsA[*awstypes.NoSuchEntityException](err) {
continue
}
if err != nil {
sweeperErr := fmt.Errorf("error listing IAM User (%s) attached policies: %s", username, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}

for _, attachedPolicy := range listAttachedUserPoliciesOutput.AttachedPolicies {
policyARN := aws.ToString(attachedPolicy.PolicyArn)

log.Printf("[DEBUG] Detaching IAM User (%s) attached policy: %s", username, policyARN)

if err := detachPolicyFromUser(ctx, conn, username, policyARN); err != nil {
sweeperErr := fmt.Errorf("error detaching IAM User (%s) attached policy (%s): %s", username, policyARN, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}
}

if err := deleteUserGroupMemberships(ctx, conn, username); err != nil {
sweeperErr := fmt.Errorf("error removing IAM User (%s) group memberships: %s", username, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}

if err := deleteUserAccessKeys(ctx, conn, username); err != nil {
sweeperErr := fmt.Errorf("error removing IAM User (%s) access keys: %s", username, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}

if err := deleteUserSSHKeys(ctx, conn, username); err != nil {
sweeperErr := fmt.Errorf("error removing IAM User (%s) SSH keys: %s", username, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}

if err := deleteUserVirtualMFADevices(ctx, conn, username); err != nil {
sweeperErr := fmt.Errorf("error removing IAM User (%s) virtual MFA devices: %s", username, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}

if err := deactivateUserMFADevices(ctx, conn, username); err != nil {
sweeperErr := fmt.Errorf("error removing IAM User (%s) MFA devices: %s", username, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}

if err := deleteUserLoginProfile(ctx, conn, username); err != nil {
sweeperErr := fmt.Errorf("error removing IAM User (%s) login profile: %s", username, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}

input := &iam.DeleteUserInput{
UserName: aws.String(username),
}

_, err = conn.DeleteUser(ctx, input)

if errs.IsA[*awstypes.NoSuchEntityException](err) {
continue
}
if err != nil {
sweeperErr := fmt.Errorf("error deleting IAM User (%s): %s", username, err)
log.Printf("[ERROR] %s", sweeperErr)
sweeperErrs = multierror.Append(sweeperErrs, sweeperErr)
continue
}
err = sweep.SweepOrchestrator(ctx, sweepResources)
if err != nil {
return fmt.Errorf("sweeping IAM Users (%s): %w", region, err)
}

return sweeperErrs.ErrorOrNil()
return nil
}

func roleNameFilter(name string) bool {
Expand Down
64 changes: 64 additions & 0 deletions internal/service/iam/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,8 @@ func resourceUserDelete(ctx context.Context, d *schema.ResourceData, meta interf
f func(context.Context, *iam.Client, string) error
format string
}{
{deleteUserPolicies, "removing IAM User (%s) policies: %s"},
{detachUserPolicies, "detaching IAM User (%s) policies: %s"},
{deleteUserAccessKeys, "removing IAM User (%s) access keys: %s"},
{deleteUserSSHKeys, "removing IAM User (%s) access keys: %s"},
{deleteUserVirtualMFADevices, "removing IAM User (%s) Virtual MFA devices: %s"},
Expand Down Expand Up @@ -536,6 +538,68 @@ func deleteServiceSpecificCredentials(ctx context.Context, conn *iam.Client, use
return nil
}

func deleteUserPolicies(ctx context.Context, conn *iam.Client, username string) error {
input := &iam.ListUserPoliciesInput{
UserName: aws.String(username),
}

output, err := conn.ListUserPolicies(ctx, input)
if errs.IsA[*awstypes.NoSuchEntityException](err) {
// user not found
return nil
}

if err != nil {
return fmt.Errorf("listing/deleting IAM User (%s) inline policies: %s", username, err)
}

for _, name := range output.PolicyNames {
log.Printf("[DEBUG] Deleting IAM User (%s) inline policy %q", username, name)

input := &iam.DeleteUserPolicyInput{
PolicyName: aws.String(name),
UserName: aws.String(username),
}

if _, err := conn.DeleteUserPolicy(ctx, input); err != nil {
if errs.IsA[*awstypes.NoSuchEntityException](err) {
continue
}
return fmt.Errorf("deleting IAM User (%s) inline policies: %s", username, err)
}
}

return nil
}

func detachUserPolicies(ctx context.Context, conn *iam.Client, username string) error {
input := &iam.ListAttachedUserPoliciesInput{
UserName: aws.String(username),
}

output, err := conn.ListAttachedUserPolicies(ctx, input)
if errs.IsA[*awstypes.NoSuchEntityException](err) {
// user was an entity 2 nanoseconds ago, but now it's not
return nil
}

if err != nil {
return fmt.Errorf("listing/detaching IAM User (%s) attached policy: %s", username, err)
}

for _, policy := range output.AttachedPolicies {
policyARN := aws.ToString(policy.PolicyArn)

log.Printf("[DEBUG] Detaching IAM User (%s) attached policy: %s", username, policyARN)

if err := detachPolicyFromUser(ctx, conn, username, policyARN); err != nil {
return fmt.Errorf("detaching IAM User (%s) attached policy: %s", username, err)
}
}

return nil
}

func userTags(ctx context.Context, conn *iam.Client, identifier string) ([]awstypes.Tag, error) {
output, err := conn.ListUserTags(ctx, &iam.ListUserTagsInput{
UserName: aws.String(identifier),
Expand Down
Loading
Loading