Skip to content

Commit

Permalink
Merge pull request #262 from ekristen/fix-iam-virtual-mfa
Browse files Browse the repository at this point in the history
fix(iam-virtual-mfa-device): handle unassigned mfa devices
  • Loading branch information
ekristen authored Aug 27, 2024
2 parents d6ebbea + 5f59c96 commit e64fc94
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 28 deletions.
61 changes: 39 additions & 22 deletions resources/iam-virtual-mfa-device.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,21 @@ package resources

import (
"context"

"errors"
"fmt"
"strings"

"github.com/gotidy/ptr"
"github.com/sirupsen/logrus"

"github.com/aws/aws-sdk-go/service/iam"
"github.com/aws/aws-sdk-go/service/iam/iamiface"

"github.com/ekristen/aws-nuke/v3/pkg/nuke"
"github.com/ekristen/libnuke/pkg/registry"
"github.com/ekristen/libnuke/pkg/resource"
"github.com/ekristen/libnuke/pkg/types"

"github.com/ekristen/aws-nuke/v3/pkg/nuke"
)

const IAMVirtualMFADeviceResource = "IAMVirtualMFADevice"
Expand All @@ -27,25 +29,32 @@ func init() {
})
}

type IAMVirtualMFADeviceLister struct{}
type IAMVirtualMFADeviceLister struct {
mockSvc iamiface.IAMAPI
}

func (l *IAMVirtualMFADeviceLister) List(_ context.Context, o interface{}) ([]resource.Resource, error) {
opts := o.(*nuke.ListerOpts)
resources := make([]resource.Resource, 0)

var svc iamiface.IAMAPI
if l.mockSvc != nil {
svc = l.mockSvc
} else {
svc = iam.New(opts.Session)
}

svc := iam.New(opts.Session)
resp, err := svc.ListVirtualMFADevices(&iam.ListVirtualMFADevicesInput{})
if err != nil {
return nil, err
}

resources := make([]resource.Resource, 0)
for _, out := range resp.VirtualMFADevices {
resources = append(resources, &IAMVirtualMFADevice{
svc: svc,
userID: out.User.UserId,
userARN: out.User.Arn,
userName: out.User.UserName,
serialNumber: out.SerialNumber,
user: out.User,
SerialNumber: out.SerialNumber,
Assigned: ptr.Bool(out.User != nil),
})
}

Expand All @@ -54,18 +63,19 @@ func (l *IAMVirtualMFADeviceLister) List(_ context.Context, o interface{}) ([]re

type IAMVirtualMFADevice struct {
svc iamiface.IAMAPI
userID *string
userARN *string
userName *string
serialNumber *string
user *iam.User
Assigned *bool
SerialNumber *string
}

func (r *IAMVirtualMFADevice) Filter() error {
isRoot := false
if ptr.ToString(r.userARN) == fmt.Sprintf("arn:aws:iam::%s:root", ptr.ToString(r.userID)) {
if r.user != nil && ptr.ToString(r.user.Arn) == fmt.Sprintf("arn:aws:iam::%s:root", ptr.ToString(r.user.UserId)) {
logrus.Debug("user is not nil, arn is root, assuming root")
isRoot = true
}
if strings.HasSuffix(ptr.ToString(r.serialNumber), "/root-account-mfa-device") {
if !isRoot && strings.HasSuffix(ptr.ToString(r.SerialNumber), "/root-account-mfa-device") {
logrus.Debug("serial number is root, assuming root")
isRoot = true
}

Expand All @@ -77,15 +87,18 @@ func (r *IAMVirtualMFADevice) Filter() error {
}

func (r *IAMVirtualMFADevice) Remove(_ context.Context) error {
if _, err := r.svc.DeactivateMFADevice(&iam.DeactivateMFADeviceInput{
UserName: r.userName,
SerialNumber: r.serialNumber,
}); err != nil {
return err
// Note: if the user is not nil, we need to deactivate the MFA device first
if r.user != nil {
if _, err := r.svc.DeactivateMFADevice(&iam.DeactivateMFADeviceInput{
UserName: r.user.UserName,
SerialNumber: r.SerialNumber,
}); err != nil {
return err
}
}

if _, err := r.svc.DeleteVirtualMFADevice(&iam.DeleteVirtualMFADeviceInput{
SerialNumber: r.serialNumber,
SerialNumber: r.SerialNumber,
}); err != nil {
return err
}
Expand All @@ -94,5 +107,9 @@ func (r *IAMVirtualMFADevice) Remove(_ context.Context) error {
}

func (r *IAMVirtualMFADevice) String() string {
return ptr.ToString(r.serialNumber)
return ptr.ToString(r.SerialNumber)
}

func (r *IAMVirtualMFADevice) Properties() types.Properties {
return types.NewPropertiesFromStruct(r)
}
107 changes: 101 additions & 6 deletions resources/iam-virtual-mfa-device_mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,103 @@ import (
"github.com/gotidy/ptr"
"github.com/stretchr/testify/assert"

"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/iam"

"github.com/ekristen/aws-nuke/v3/mocks/mock_iamiface"
"github.com/ekristen/aws-nuke/v3/pkg/nuke"
)

func Test_Mock_IAMVirtualMFADevice_List(t *testing.T) {
a := assert.New(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()

mockIAM := mock_iamiface.NewMockIAMAPI(ctrl)

mockIAM.EXPECT().ListVirtualMFADevices(gomock.Any()).Return(&iam.ListVirtualMFADevicesOutput{
VirtualMFADevices: []*iam.VirtualMFADevice{
{
SerialNumber: ptr.String("serial:device1"),
User: &iam.User{
UserName: ptr.String("user1"),
Arn: ptr.String("arn:aws:iam::123456789012:user/user1"),
},
},
{
SerialNumber: ptr.String("arn:aws:iam::077097111583:mfa/Authenticator"),
User: &iam.User{
UserName: ptr.String("user1"),
UserId: ptr.String("0000000000000"),
Arn: ptr.String("arn:aws:iam::123456789012:user/user1"),
},
},
{
SerialNumber: ptr.String("serial:device2"),
User: nil,
},
},
}, nil)

lister := &IAMVirtualMFADeviceLister{
mockSvc: mockIAM,
}

resources, err := lister.List(context.TODO(), &nuke.ListerOpts{
Region: &nuke.Region{
Name: "us-east-2",
},
Session: session.Must(session.NewSession()),
})
a.Nil(err)
a.Len(resources, 3)
}

func Test_IAMVirtualMFADevice_Properties(t *testing.T) {
a := assert.New(t)

iamVirtualMFADevice := IAMVirtualMFADevice{
user: &iam.User{
UserName: ptr.String("foobar"),
Arn: ptr.String("arn:aws:iam::123456789012:user/foobar"),
},
SerialNumber: ptr.String("serial:foobar"),
Assigned: ptr.Bool(true),
}

properties := iamVirtualMFADevice.Properties()
a.Equal("serial:foobar", properties.Get("SerialNumber"))
a.Equal("true", properties.Get("Assigned"))
a.Equal("serial:foobar", iamVirtualMFADevice.String())
}

func Test_IAMVirtualMFADevice_Filter(t *testing.T) {
a := assert.New(t)

rootMFADevice := &IAMVirtualMFADevice{
user: &iam.User{
UserId: ptr.String("0000000000000"),
Arn: ptr.String("arn:aws:iam::0000000000000:root"),
},
SerialNumber: ptr.String("arn:aws:iam::0000000000000:mfa/root-account-mfa-device"),
}

err := rootMFADevice.Filter()
a.NotNil(err)
a.EqualError(err, "cannot delete root mfa device")

nonRootMFADevice := &IAMVirtualMFADevice{
user: &iam.User{
UserId: ptr.String("123456789012"),
Arn: ptr.String("arn:aws:iam::123456789012:user/user1"),
},
SerialNumber: ptr.String("arn:aws:iam::123456789012:mfa/user1"),
}

err = nonRootMFADevice.Filter()
a.Nil(err)
}

func Test_Mock_IAMVirtualMFADevice_Remove(t *testing.T) {
a := assert.New(t)
ctrl := gomock.NewController(t)
Expand All @@ -21,18 +113,21 @@ func Test_Mock_IAMVirtualMFADevice_Remove(t *testing.T) {
mockIAM := mock_iamiface.NewMockIAMAPI(ctrl)

iamVirtualMFADevice := IAMVirtualMFADevice{
svc: mockIAM,
userName: ptr.String("user:foobar"),
serialNumber: ptr.String("serial:foobar"),
svc: mockIAM,
user: &iam.User{
UserName: ptr.String("foobar"),
Arn: ptr.String("arn:aws:iam::123456789012:user/foobar"),
},
SerialNumber: ptr.String("serial:foobar"),
}

mockIAM.EXPECT().DeactivateMFADevice(gomock.Eq(&iam.DeactivateMFADeviceInput{
UserName: iamVirtualMFADevice.userName,
SerialNumber: iamVirtualMFADevice.serialNumber,
UserName: iamVirtualMFADevice.user.UserName,
SerialNumber: iamVirtualMFADevice.SerialNumber,
})).Return(&iam.DeactivateMFADeviceOutput{}, nil)

mockIAM.EXPECT().DeleteVirtualMFADevice(gomock.Eq(&iam.DeleteVirtualMFADeviceInput{
SerialNumber: iamVirtualMFADevice.serialNumber,
SerialNumber: iamVirtualMFADevice.SerialNumber,
})).Return(&iam.DeleteVirtualMFADeviceOutput{}, nil)

err := iamVirtualMFADevice.Remove(context.TODO())
Expand Down

0 comments on commit e64fc94

Please sign in to comment.