-
-
Notifications
You must be signed in to change notification settings - Fork 356
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
Feature/116 iam groups #364
Merged
ellisonc
merged 9 commits into
gruntwork-io:master
from
ellisonc:feature/116-iam-groups
Oct 31, 2022
Merged
Changes from 5 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
1115c9b
116 - WIP implement scaffolding for iam_groups
ellisonc b894315
116 - WIP - write tests for iam_group
ellisonc b57e837
116 - WIP - implement iam_group.go FNs
ellisonc 7ad78cd
116 - IAM Group removal
ellisonc d25df76
116 - Add IAM Groups to the main aws.go
ellisonc cf5c39f
116 - detach policies before removing iam group
ellisonc c75b348
116 - Add iam policy removal
72862d0
116 - PR fix for broken test config
dc9f687
Merge remote-tracking branch 'upstream/master' into feature/116-iam-g…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package aws | ||
|
||
import ( | ||
"sync" | ||
"time" | ||
|
||
"github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/aws/session" | ||
"github.com/aws/aws-sdk-go/service/iam" | ||
"github.com/gruntwork-io/cloud-nuke/config" | ||
"github.com/gruntwork-io/cloud-nuke/logging" | ||
"github.com/gruntwork-io/gruntwork-cli/errors" | ||
"github.com/hashicorp/go-multierror" | ||
) | ||
|
||
func getAllIamGroups(session *session.Session, excludeAfter time.Time, configObj config.Config) ([]*string, error) { | ||
svc := iam.New(session) | ||
|
||
allIamGroups := []*string{} | ||
err := svc.ListGroupsPages( | ||
&iam.ListGroupsInput{}, | ||
func(page *iam.ListGroupsOutput, lastPage bool) bool { | ||
for _, iamGroup := range page.Groups { | ||
if shouldIncludeIamGroup(iamGroup, excludeAfter, configObj) { | ||
allIamGroups = append(allIamGroups, iamGroup.GroupName) | ||
} | ||
} | ||
return !lastPage | ||
}, | ||
) | ||
if err != nil { | ||
return nil, errors.WithStackTrace(err) | ||
} | ||
return allIamGroups, nil | ||
} | ||
|
||
//nukeAllIamGroups - delete all IAM Roles. Caller is responsible for pagination (no more than 100/request) | ||
func nukeAllIamGroups(session *session.Session, groupNames []*string) error { | ||
region := aws.StringValue(session.Config.Region) //Since this is a global resource this can be any random region | ||
svc := iam.New(session) | ||
|
||
if len(groupNames) == 0 { | ||
logging.Logger.Info("No IAM Groups to nuke") | ||
return nil | ||
} | ||
|
||
//Probably not required since pagination is handled by the caller | ||
if len(groupNames) > 100 { | ||
logging.Logger.Errorf("Nuking too many IAM Groups at once (100): Halting to avoid rate limits") | ||
return TooManyIamGroupErr{} | ||
} | ||
|
||
//No bulk delete exists, do it with goroutines | ||
logging.Logger.Info("Deleting all IAM Groups") | ||
wg := new(sync.WaitGroup) | ||
wg.Add(len(groupNames)) | ||
errChans := make([]chan error, len(groupNames)) | ||
for i, groupName := range groupNames { | ||
errChans[i] = make(chan error, 1) | ||
go deleteIamGroupAsync(wg, errChans[i], svc, groupName) | ||
} | ||
wg.Wait() | ||
|
||
//Collapse the errors down to one | ||
var allErrs *multierror.Error | ||
for _, errChan := range errChans { | ||
if err := <-errChan; err != nil { | ||
allErrs = multierror.Append(allErrs, err) | ||
logging.Logger.Errorf("[Failed] %s", err) | ||
} | ||
} | ||
finalErr := allErrs.ErrorOrNil() | ||
if finalErr != nil { | ||
return errors.WithStackTrace(finalErr) | ||
} | ||
|
||
//Print Successful deletions | ||
for _, groupName := range groupNames { | ||
logging.Logger.Infof("[OK] IAM Group %s was deleted in %s", aws.StringValue(groupName), region) | ||
} | ||
return nil | ||
} | ||
|
||
//deleteIamGroup - removes an IAM group from AWS, designed to run as a goroutine | ||
func deleteIamGroupAsync(wg *sync.WaitGroup, errChan chan error, svc *iam.IAM, groupName *string) { | ||
defer wg.Done() | ||
var multierr *multierror.Error | ||
|
||
//Remove any users from the group | ||
//TODO make this threaded | ||
getGroupInput := &iam.GetGroupInput{ | ||
GroupName: groupName, | ||
} | ||
grp, err := svc.GetGroup(getGroupInput) | ||
for _, user := range grp.Users { | ||
unlinkUserInput := &iam.RemoveUserFromGroupInput{ | ||
UserName: user.UserName, | ||
GroupName: groupName, | ||
} | ||
_, err := svc.RemoveUserFromGroup(unlinkUserInput) | ||
if err != nil { | ||
multierr = multierror.Append(multierr, err) | ||
} | ||
} | ||
|
||
_, err = svc.DeleteGroup(&iam.DeleteGroupInput{ | ||
GroupName: groupName, | ||
}) | ||
if err != nil { | ||
multierr = multierror.Append(multierr, err) | ||
} | ||
errChan <- multierr.ErrorOrNil() | ||
} | ||
|
||
//check if iam group should be included based on config rules (RegExp and Exclude After) | ||
func shouldIncludeIamGroup(iamGroup *iam.Group, excludeAfter time.Time, configObj config.Config) bool { | ||
if iamGroup == nil { | ||
return false | ||
} | ||
|
||
if excludeAfter.Before(*iamGroup.CreateDate) { | ||
return false | ||
} | ||
|
||
return config.ShouldInclude( | ||
aws.StringValue(iamGroup.GroupName), | ||
configObj.IAMGroups.IncludeRule.NamesRegExp, | ||
configObj.IAMGroups.ExcludeRule.NamesRegExp, | ||
) | ||
} | ||
|
||
//Custom Errors | ||
type TooManyIamGroupErr struct{} | ||
|
||
func (err TooManyIamGroupErr) Error() string { | ||
return "Too many IAM Groups requested at once" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package aws | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
"time" | ||
|
||
awsgo "github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/aws/session" | ||
"github.com/aws/aws-sdk-go/service/iam" | ||
"github.com/gruntwork-io/cloud-nuke/config" | ||
"github.com/gruntwork-io/cloud-nuke/util" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
//Test that we can list IAM groups in an AWS account | ||
func TestListIamGroups(t *testing.T) { | ||
t.Parallel() | ||
|
||
region, err := getRandomRegion() | ||
require.NoError(t, err) | ||
|
||
session, err := session.NewSession(&awsgo.Config{ | ||
Region: awsgo.String(region), | ||
}) | ||
require.NoError(t, err) | ||
|
||
groupNames, err := getAllIamGroups(session, time.Now(), config.Config{}) | ||
require.NoError(t, err) | ||
assert.NotEmpty(t, groupNames) | ||
} | ||
|
||
//Creates an empty IAM group for testing | ||
func createEmptyTestGroup(t *testing.T, session *session.Session, name string) error { | ||
svc := iam.New(session) | ||
|
||
groupInput := &iam.CreateGroupInput{ | ||
GroupName: awsgo.String(name), | ||
} | ||
|
||
group, err := svc.CreateGroup(groupInput) | ||
fmt.Println(group.Group.Arn) | ||
require.NoError(t, err) | ||
return nil | ||
} | ||
|
||
func createNonEmptyTestGroup(t *testing.T, session *session.Session, groupName string, userName string) error { | ||
svc := iam.New(session) | ||
|
||
//Create User | ||
userInput := &iam.CreateUserInput{ | ||
UserName: awsgo.String(userName), | ||
} | ||
|
||
_, err := svc.CreateUser(userInput) | ||
require.NoError(t, err) | ||
|
||
//Create Group | ||
groupInput := &iam.CreateGroupInput{ | ||
GroupName: awsgo.String(groupName), | ||
} | ||
|
||
_, err = svc.CreateGroup(groupInput) | ||
require.NoError(t, err) | ||
|
||
//Add user to Group | ||
userGroupLinkInput := &iam.AddUserToGroupInput{ | ||
GroupName: awsgo.String(groupName), | ||
UserName: awsgo.String(userName), | ||
} | ||
_, err = svc.AddUserToGroup(userGroupLinkInput) | ||
require.NoError(t, err) | ||
|
||
return nil | ||
} | ||
|
||
//Test that we can nuke iam groups. | ||
func TestNukeIamGroups(t *testing.T) { | ||
t.Parallel() | ||
|
||
region, err := getRandomRegion() | ||
require.NoError(t, err) | ||
|
||
session, err := session.NewSession(&awsgo.Config{ | ||
Region: awsgo.String(region), | ||
}) | ||
require.NoError(t, err) | ||
|
||
//Create test entities | ||
emptyName := "cloud-nuke-test" + util.UniqueID() | ||
err = createEmptyTestGroup(t, session, emptyName) | ||
require.NoError(t, err) | ||
|
||
nonEmptyName := "cloud-nuke-test" + util.UniqueID() | ||
userName := "cloud-nuke-test" + util.UniqueID() | ||
err = createNonEmptyTestGroup(t, session, nonEmptyName, userName) | ||
require.NoError(t, err) | ||
|
||
//Assert test entities exist | ||
groupNames, err := getAllIamGroups(session, time.Now(), config.Config{}) | ||
require.NoError(t, err) | ||
assert.Contains(t, awsgo.StringValueSlice(groupNames), nonEmptyName) | ||
assert.Contains(t, awsgo.StringValueSlice(groupNames), emptyName) | ||
|
||
//Nuke test entities | ||
err = nukeAllIamGroups(session, []*string{&emptyName, &nonEmptyName}) | ||
require.NoError(t, err) | ||
|
||
err = nukeAllIamUsers(session, []*string{&userName}) | ||
require.NoError(t, err) | ||
|
||
//Assert test entites don't exist anymore | ||
groupNames, err = getAllIamGroups(session, time.Now(), config.Config{}) | ||
require.NoError(t, err) | ||
assert.NotContains(t, awsgo.StringValueSlice(groupNames), nonEmptyName) | ||
assert.NotContains(t, awsgo.StringValueSlice(groupNames), emptyName) | ||
} | ||
|
||
//TODO could test filtered nuke if time |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package aws | ||
|
||
import ( | ||
awsgo "github.com/aws/aws-sdk-go/aws" | ||
"github.com/aws/aws-sdk-go/aws/session" | ||
"github.com/gruntwork-io/go-commons/errors" | ||
) | ||
|
||
//IAMGroups - represents all IAMGroups on the AWS Account | ||
type IAMGroups struct { | ||
GroupNames []string | ||
} | ||
|
||
//ResourceName - the simple name of the AWS resource | ||
func (u IAMGroups) ResourceName() string { | ||
return "iam-group" | ||
} | ||
|
||
// ResourceIdentifiers - The IAM GroupNames | ||
func (g IAMGroups) ResourceIdentifiers() []string { | ||
return g.GroupNames | ||
} | ||
|
||
// Tentative batch size to ensure AWS doesn't throttle | ||
// There's a global max of 500 groups so it shouldn't take long either way | ||
func (g IAMGroups) MaxBatchSize() int { | ||
return 80 | ||
} | ||
|
||
// Nuke - Destroy every group in this collection | ||
func (g IAMGroups) Nuke(session *session.Session, identifiers []string) error { | ||
if err := nukeAllIamGroups(session, awsgo.StringSlice(identifiers)); err != nil { | ||
return errors.WithStackTrace(err) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefer the AWS SDK convenience methods for dereferencing a pointer, i.e,
aws.TimeValue