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

Feature/116 iam groups #364

Merged
merged 9 commits into from
Oct 31, 2022
Merged
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The currently supported functionality includes:
- Deleting VPCs in an AWS Account (except for default VPCs which is handled by the dedicated `defaults-aws` subcommand)
- Inspecting and deleting all IAM users in an AWS account
- Inspecting and deleting all IAM roles in an AWS account
- Inspecting and deleting all IAM groups in an AWS account
- Inspecting and deleting all Secrets Manager Secrets in an AWS account
- Inspecting and deleting all NAT Gateways in an AWS account
- Inspecting and deleting all IAM Access Analyzers in an AWS account
Expand Down
15 changes: 15 additions & 0 deletions aws/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,20 @@ func GetAllResources(targetRegions []string, excludeAfter time.Time, resourceTyp
}
// End IAM Users

//IAM Groups
iamGroups := IAMGroups{}
if IsNukeable(iamGroups.ResourceName(), resourceTypes) {
groupNames, err := getAllIamGroups(session, excludeAfter, configObj)
if err != nil {
return nil, errors.WithStackTrace(err)
}
if len(groupNames) > 0 {
iamGroups.GroupNames = awsgo.StringValueSlice(groupNames)
globalResources.Resources = append(globalResources.Resources, iamGroups)
}
}
//END IAM Groups

// IAM OpenID Connect Providers
oidcProviders := OIDCProviders{}
if IsNukeable(oidcProviders.ResourceName(), resourceTypes) {
Expand Down Expand Up @@ -913,6 +927,7 @@ func ListResourceTypes() []string {
S3Buckets{}.ResourceName(),
IAMUsers{}.ResourceName(),
IAMRoles{}.ResourceName(),
IAMGroups{}.ResourceName(),
SecretsManagerSecrets{}.ResourceName(),
NatGateways{}.ResourceName(),
OpenSearchDomains{}.ResourceName(),
Expand Down
137 changes: 137 additions & 0 deletions aws/iam_group.go
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) {
Copy link
Contributor

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

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"
}
120 changes: 120 additions & 0 deletions aws/iam_group_test.go
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
37 changes: 37 additions & 0 deletions aws/iam_group_types.go
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
}
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
type Config struct {
S3 ResourceType `yaml:"s3"`
IAMUsers ResourceType `yaml:"IAMUsers"`
IAMGroups ResourceType `yaml:"IAMGroups"` //TODO update the test
IAMRoles ResourceType `yaml:"IAMRoles"`
SecretsManagerSecrets ResourceType `yaml:"SecretsManager"`
NatGateway ResourceType `yaml:"NatGateway"`
Expand Down