diff --git a/pkg/clients/iam/fake/role.go b/pkg/clients/iam/fake/role.go index d51c656909..968baf04f2 100644 --- a/pkg/clients/iam/fake/role.go +++ b/pkg/clients/iam/fake/role.go @@ -29,13 +29,15 @@ var _ clientset.RoleClient = (*MockRoleClient)(nil) // MockRoleClient is a type that implements all the methods for RoleClient interface type MockRoleClient struct { - MockGetRole func(ctx context.Context, input *iam.GetRoleInput, opts []func(*iam.Options)) (*iam.GetRoleOutput, error) - MockCreateRole func(ctx context.Context, input *iam.CreateRoleInput, opts []func(*iam.Options)) (*iam.CreateRoleOutput, error) - MockDeleteRole func(ctx context.Context, input *iam.DeleteRoleInput, opts []func(*iam.Options)) (*iam.DeleteRoleOutput, error) - MockUpdateRole func(ctx context.Context, input *iam.UpdateRoleInput, opts []func(*iam.Options)) (*iam.UpdateRoleOutput, error) - MockUpdateAssumeRolePolicy func(ctx context.Context, input *iam.UpdateAssumeRolePolicyInput, opts []func(*iam.Options)) (*iam.UpdateAssumeRolePolicyOutput, error) - MockTagRole func(ctx context.Context, input *iam.TagRoleInput, opts []func(*iam.Options)) (*iam.TagRoleOutput, error) - MockUntagRole func(ctx context.Context, input *iam.UntagRoleInput, opts []func(*iam.Options)) (*iam.UntagRoleOutput, error) + MockGetRole func(ctx context.Context, input *iam.GetRoleInput, opts []func(*iam.Options)) (*iam.GetRoleOutput, error) + MockCreateRole func(ctx context.Context, input *iam.CreateRoleInput, opts []func(*iam.Options)) (*iam.CreateRoleOutput, error) + MockDeleteRole func(ctx context.Context, input *iam.DeleteRoleInput, opts []func(*iam.Options)) (*iam.DeleteRoleOutput, error) + MockUpdateRole func(ctx context.Context, input *iam.UpdateRoleInput, opts []func(*iam.Options)) (*iam.UpdateRoleOutput, error) + MockPutRolePermissionsBoundary func(ctx context.Context, input *iam.PutRolePermissionsBoundaryInput, opts []func(*iam.Options)) (*iam.PutRolePermissionsBoundaryOutput, error) + MockDeleteRolePermissionsBoundary func(ctx context.Context, input *iam.DeleteRolePermissionsBoundaryInput, opts []func(*iam.Options)) (*iam.DeleteRolePermissionsBoundaryOutput, error) + MockUpdateAssumeRolePolicy func(ctx context.Context, input *iam.UpdateAssumeRolePolicyInput, opts []func(*iam.Options)) (*iam.UpdateAssumeRolePolicyOutput, error) + MockTagRole func(ctx context.Context, input *iam.TagRoleInput, opts []func(*iam.Options)) (*iam.TagRoleOutput, error) + MockUntagRole func(ctx context.Context, input *iam.UntagRoleInput, opts []func(*iam.Options)) (*iam.UntagRoleOutput, error) } // GetRole mocks GetRole method @@ -58,6 +60,16 @@ func (m *MockRoleClient) UpdateRole(ctx context.Context, input *iam.UpdateRoleIn return m.MockUpdateRole(ctx, input, opts) } +// PutRolePermissionsBoundary mocks PutRolePermissionsBoundary method +func (m *MockRoleClient) PutRolePermissionsBoundary(ctx context.Context, input *iam.PutRolePermissionsBoundaryInput, opts ...func(*iam.Options)) (*iam.PutRolePermissionsBoundaryOutput, error) { + return m.MockPutRolePermissionsBoundary(ctx, input, opts) +} + +// DeleteRolePermissionsBoundary mocks DeleteRolePermissionsBoundary method +func (m *MockRoleClient) DeleteRolePermissionsBoundary(ctx context.Context, input *iam.DeleteRolePermissionsBoundaryInput, opts ...func(*iam.Options)) (*iam.DeleteRolePermissionsBoundaryOutput, error) { + return m.MockDeleteRolePermissionsBoundary(ctx, input, opts) +} + // UpdateAssumeRolePolicy mocks UpdateAssumeRolePolicy method func (m *MockRoleClient) UpdateAssumeRolePolicy(ctx context.Context, input *iam.UpdateAssumeRolePolicyInput, opts ...func(*iam.Options)) (*iam.UpdateAssumeRolePolicyOutput, error) { return m.MockUpdateAssumeRolePolicy(ctx, input, opts) diff --git a/pkg/clients/iam/fake/user.go b/pkg/clients/iam/fake/user.go index 299fbcda8a..129ad8f0cf 100644 --- a/pkg/clients/iam/fake/user.go +++ b/pkg/clients/iam/fake/user.go @@ -35,13 +35,15 @@ type MockUserInput struct { // MockUserClient is a type that implements all the methods for RoleClient interface type MockUserClient struct { - MockUserInput MockUserInput - MockGetUser func(ctx context.Context, input *iam.GetUserInput, opts []func(*iam.Options)) (*iam.GetUserOutput, error) - MockCreateUser func(ctx context.Context, input *iam.CreateUserInput, opts []func(*iam.Options)) (*iam.CreateUserOutput, error) - MockDeleteUser func(ctx context.Context, input *iam.DeleteUserInput, opts []func(*iam.Options)) (*iam.DeleteUserOutput, error) - MockUpdateUser func(ctx context.Context, input *iam.UpdateUserInput, opts []func(*iam.Options)) (*iam.UpdateUserOutput, error) - MockTagUser func(ctx context.Context, input *iam.TagUserInput, opt []func(*iam.Options)) (*iam.TagUserOutput, error) - MockUntagUser func(ctx context.Context, input *iam.UntagUserInput, opts []func(*iam.Options)) (*iam.UntagUserOutput, error) + MockUserInput MockUserInput + MockGetUser func(ctx context.Context, input *iam.GetUserInput, opts []func(*iam.Options)) (*iam.GetUserOutput, error) + MockCreateUser func(ctx context.Context, input *iam.CreateUserInput, opts []func(*iam.Options)) (*iam.CreateUserOutput, error) + MockDeleteUser func(ctx context.Context, input *iam.DeleteUserInput, opts []func(*iam.Options)) (*iam.DeleteUserOutput, error) + MockUpdateUser func(ctx context.Context, input *iam.UpdateUserInput, opts []func(*iam.Options)) (*iam.UpdateUserOutput, error) + MockPutUserPermissionsBoundary func(ctx context.Context, input *iam.PutUserPermissionsBoundaryInput, opts []func(*iam.Options)) (*iam.PutUserPermissionsBoundaryOutput, error) + MockDeleteUserPermissionsBoundary func(ctx context.Context, input *iam.DeleteUserPermissionsBoundaryInput, opts []func(*iam.Options)) (*iam.DeleteUserPermissionsBoundaryOutput, error) + MockTagUser func(ctx context.Context, input *iam.TagUserInput, opt []func(*iam.Options)) (*iam.TagUserOutput, error) + MockUntagUser func(ctx context.Context, input *iam.UntagUserInput, opts []func(*iam.Options)) (*iam.UntagUserOutput, error) } // GetUser mocks GetUser method @@ -64,6 +66,16 @@ func (m *MockUserClient) UpdateUser(ctx context.Context, input *iam.UpdateUserIn return m.MockUpdateUser(ctx, input, opts) } +// PutUserPermissionsBoundary mocks PutUserPermissionsBoundary method +func (m *MockUserClient) PutUserPermissionsBoundary(ctx context.Context, input *iam.PutUserPermissionsBoundaryInput, opts ...func(*iam.Options)) (*iam.PutUserPermissionsBoundaryOutput, error) { + return m.MockPutUserPermissionsBoundary(ctx, input, opts) +} + +// DeleteUserPermissionsBoundary mocks DeleteUserPermissionsBoundary method +func (m *MockUserClient) DeleteUserPermissionsBoundary(ctx context.Context, input *iam.DeleteUserPermissionsBoundaryInput, opts ...func(*iam.Options)) (*iam.DeleteUserPermissionsBoundaryOutput, error) { + return m.MockDeleteUserPermissionsBoundary(ctx, input, opts) +} + // TagUser mocks TagUser method func (m *MockUserClient) TagUser(ctx context.Context, input *iam.TagUserInput, opts ...func(*iam.Options)) (*iam.TagUserOutput, error) { m.MockUserInput.TagUserInput = input diff --git a/pkg/clients/iam/role.go b/pkg/clients/iam/role.go index 2943045dbf..d759eabb4d 100644 --- a/pkg/clients/iam/role.go +++ b/pkg/clients/iam/role.go @@ -33,6 +33,8 @@ type RoleClient interface { CreateRole(ctx context.Context, input *iam.CreateRoleInput, opts ...func(*iam.Options)) (*iam.CreateRoleOutput, error) DeleteRole(ctx context.Context, input *iam.DeleteRoleInput, opts ...func(*iam.Options)) (*iam.DeleteRoleOutput, error) UpdateRole(ctx context.Context, input *iam.UpdateRoleInput, opts ...func(*iam.Options)) (*iam.UpdateRoleOutput, error) + PutRolePermissionsBoundary(ctx context.Context, params *iam.PutRolePermissionsBoundaryInput, optFns ...func(*iam.Options)) (*iam.PutRolePermissionsBoundaryOutput, error) + DeleteRolePermissionsBoundary(ctx context.Context, params *iam.DeleteRolePermissionsBoundaryInput, optFns ...func(*iam.Options)) (*iam.DeleteRolePermissionsBoundaryOutput, error) UpdateAssumeRolePolicy(ctx context.Context, input *iam.UpdateAssumeRolePolicyInput, opts ...func(*iam.Options)) (*iam.UpdateAssumeRolePolicyOutput, error) TagRole(ctx context.Context, input *iam.TagRoleInput, opts ...func(*iam.Options)) (*iam.TagRoleOutput, error) UntagRole(ctx context.Context, input *iam.UntagRoleInput, opts ...func(*iam.Options)) (*iam.UntagRoleOutput, error) @@ -89,6 +91,11 @@ func GenerateRole(in v1beta1.RoleParameters, role *iamtypes.Role) error { role.Description = in.Description role.MaxSessionDuration = in.MaxSessionDuration role.Path = in.Path + if in.PermissionsBoundary != nil { + role.PermissionsBoundary = &iamtypes.AttachedPermissionsBoundary{ + PermissionsBoundaryArn: in.PermissionsBoundary, + } + } if len(in.Tags) != 0 { role.Tags = make([]iamtypes.Tag, len(in.Tags)) diff --git a/pkg/clients/iam/user.go b/pkg/clients/iam/user.go index 93eadc46a4..cc0b938a8a 100644 --- a/pkg/clients/iam/user.go +++ b/pkg/clients/iam/user.go @@ -17,6 +17,8 @@ type UserClient interface { CreateUser(ctx context.Context, input *iam.CreateUserInput, opts ...func(*iam.Options)) (*iam.CreateUserOutput, error) DeleteUser(ctx context.Context, input *iam.DeleteUserInput, opts ...func(*iam.Options)) (*iam.DeleteUserOutput, error) UpdateUser(ctx context.Context, input *iam.UpdateUserInput, opts ...func(*iam.Options)) (*iam.UpdateUserOutput, error) + PutUserPermissionsBoundary(ctx context.Context, params *iam.PutUserPermissionsBoundaryInput, optFns ...func(*iam.Options)) (*iam.PutUserPermissionsBoundaryOutput, error) + DeleteUserPermissionsBoundary(ctx context.Context, params *iam.DeleteUserPermissionsBoundaryInput, optFns ...func(*iam.Options)) (*iam.DeleteUserPermissionsBoundaryOutput, error) TagUser(ctx context.Context, params *iam.TagUserInput, opts ...func(*iam.Options)) (*iam.TagUserOutput, error) UntagUser(ctx context.Context, params *iam.UntagUserInput, opts ...func(*iam.Options)) (*iam.UntagUserOutput, error) } diff --git a/pkg/controller/iam/role/controller.go b/pkg/controller/iam/role/controller.go index 2d1e1b21f2..ff870a83be 100644 --- a/pkg/controller/iam/role/controller.go +++ b/pkg/controller/iam/role/controller.go @@ -213,6 +213,27 @@ func (e *external) Update(ctx context.Context, mgd resource.Managed) (managed.Ex } } + boundaryArn := "" + if observed.Role.PermissionsBoundary != nil { + boundaryArn = *observed.Role.PermissionsBoundary.PermissionsBoundaryArn + } + if aws.ToString(&boundaryArn) != aws.ToString(cr.Spec.ForProvider.PermissionsBoundary) { + if aws.ToString(cr.Spec.ForProvider.PermissionsBoundary) == "" { + _, err = e.client.DeleteRolePermissionsBoundary(ctx, &awsiam.DeleteRolePermissionsBoundaryInput{ + RoleName: aws.String(meta.GetExternalName(cr)), + }) + } else { + _, err = e.client.PutRolePermissionsBoundary(ctx, &awsiam.PutRolePermissionsBoundaryInput{ + PermissionsBoundary: cr.Spec.ForProvider.PermissionsBoundary, + RoleName: aws.String(meta.GetExternalName(cr)), + }) + } + + if err != nil { + return managed.ExternalUpdate{}, awsclient.Wrap(err, errUpdate) + } + } + if patch.AssumeRolePolicyDocument != "" { _, err = e.client.UpdateAssumeRolePolicy(ctx, &awsiam.UpdateAssumeRolePolicyInput{ PolicyDocument: &cr.Spec.ForProvider.AssumeRolePolicyDocument, diff --git a/pkg/controller/iam/user/controller.go b/pkg/controller/iam/user/controller.go index f1c5898b72..23c6323402 100644 --- a/pkg/controller/iam/user/controller.go +++ b/pkg/controller/iam/user/controller.go @@ -21,6 +21,7 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" awsiam "github.com/aws/aws-sdk-go-v2/service/iam" + "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" @@ -45,13 +46,15 @@ import ( const ( errUnexpectedObject = "The managed resource is not an IAM User resource" - errGet = "cannot get IAM User" - errCreate = "cannot create the IAM User resource" - errDelete = "cannot delete the IAM User resource" - errUpdate = "cannot update the IAM User resource" - errSDK = "empty IAM User received from IAM API" - errTag = "cannot tag the IAM User resource" - errUntag = "cannot remove tags from the IAM User resource" + errGet = "cannot get IAM User" + errCreate = "cannot create the IAM User resource" + errDelete = "cannot delete the IAM User resource" + errUpdateUser = "cannot update the IAM User resource" + errPutUserPermissionsBoundary = "cannot update the IAM User permission boundary" + errDeleteUserPermissionsBoundary = "cannot delete the IAM User permission boundary" + errSDK = "empty IAM User received from IAM API" + errTag = "cannot tag the IAM User resource" + errUntag = "cannot remove tags from the IAM User resource" errKubeUpdateFailed = "cannot late initialize IAM User" ) @@ -135,16 +138,9 @@ func (e *external) Observe(ctx context.Context, mgd resource.Managed) (managed.E UserID: aws.ToString(user.UserId), } - crTagMap := make(map[string]string, len(cr.Spec.ForProvider.Tags)) - for _, v := range cr.Spec.ForProvider.Tags { - crTagMap[v.Key] = v.Value - } - _, _, areTagsUpdated := iam.DiffIAMTags(crTagMap, observed.User.Tags) - return managed.ExternalObservation{ - ResourceExists: true, - ResourceUpToDate: aws.ToString(cr.Spec.ForProvider.Path) == aws.ToString(user.Path) && - areTagsUpdated, + ResourceExists: true, + ResourceUpToDate: isUpToDate(cr, &user), }, nil } @@ -171,44 +167,29 @@ func (e *external) Update(ctx context.Context, mgd resource.Managed) (managed.Ex return managed.ExternalUpdate{}, errors.New(errUnexpectedObject) } - _, err := e.client.UpdateUser(ctx, &awsiam.UpdateUserInput{ - NewPath: cr.Spec.ForProvider.Path, - UserName: aws.String(meta.GetExternalName(cr)), - }) - - if err != nil { - return managed.ExternalUpdate{}, awsclient.Wrap(err, errUpdate) - } - observed, err := e.client.GetUser(ctx, &awsiam.GetUserInput{ UserName: aws.String(meta.GetExternalName(cr)), }) if err != nil { - return managed.ExternalUpdate{}, awsclient.Wrap(err, errGet) + return managed.ExternalUpdate{}, awsclient.Wrap(resource.Ignore(iam.IsErrorNotFound, err), errGet) } - add, remove, _ := iam.DiffIAMTagsWithUpdates(cr.Spec.ForProvider.Tags, observed.User.Tags) - - if len(add) > 0 { - if _, err := e.client.TagUser(ctx, &awsiam.TagUserInput{ - UserName: aws.String(meta.GetExternalName(cr)), - Tags: add, - }); err != nil { - return managed.ExternalUpdate{}, awsclient.Wrap(err, errTag) - } + // take care of changes to path (only call if necessary) + err = e.updateUser(ctx, observed, cr) + if err != nil { + return managed.ExternalUpdate{}, err } - if len(remove) > 0 { - if _, err := e.client.UntagUser(ctx, &awsiam.UntagUserInput{ - TagKeys: remove, - UserName: aws.String(meta.GetExternalName(cr)), - }); err != nil { - return managed.ExternalUpdate{}, awsclient.Wrap(err, errUntag) - } + // take care of changes to PermissionBoundary (only call if necessary) + err = e.updatePermissionsBoundary(ctx, observed, cr) + if err != nil { + return managed.ExternalUpdate{}, err } - return managed.ExternalUpdate{}, awsclient.Wrap(err, errUpdate) + // take care of changes to Tags + err = e.updateTags(ctx, observed, cr) + return managed.ExternalUpdate{}, err } func (e *external) Delete(ctx context.Context, mgd resource.Managed) error { @@ -258,3 +239,91 @@ func (t *tagger) Initialize(ctx context.Context, mgd resource.Managed) error { } return errors.Wrap(t.kube.Update(ctx, cr), errKubeUpdateFailed) } + +func (e *external) updateUser(ctx context.Context, observed *awsiam.GetUserOutput, cr *v1beta1.User) error { + if aws.ToString(observed.User.Path) != aws.ToString(cr.Spec.ForProvider.Path) { + _, err := e.client.UpdateUser(ctx, &awsiam.UpdateUserInput{ + NewPath: cr.Spec.ForProvider.Path, + UserName: aws.String(meta.GetExternalName(cr)), + }) + + return awsclient.Wrap(err, errUpdateUser) + } + + return nil +} + +func (e *external) updatePermissionsBoundary(ctx context.Context, observed *awsiam.GetUserOutput, cr *v1beta1.User) error { + boundaryArn := "" + var err error + + if observed.User.PermissionsBoundary != nil { + boundaryArn = *observed.User.PermissionsBoundary.PermissionsBoundaryArn + } + if aws.ToString(&boundaryArn) != aws.ToString(cr.Spec.ForProvider.PermissionsBoundary) { + // is this a delete? + if aws.ToString(cr.Spec.ForProvider.PermissionsBoundary) == "" { + _, err = e.client.DeleteUserPermissionsBoundary(ctx, &awsiam.DeleteUserPermissionsBoundaryInput{ + UserName: aws.String(meta.GetExternalName(cr)), + }) + + return awsclient.Wrap(err, errDeleteUserPermissionsBoundary) + } + + // must be an update + _, err = e.client.PutUserPermissionsBoundary(ctx, &awsiam.PutUserPermissionsBoundaryInput{ + PermissionsBoundary: cr.Spec.ForProvider.PermissionsBoundary, + UserName: aws.String(meta.GetExternalName(cr)), + }) + + return awsclient.Wrap(err, errPutUserPermissionsBoundary) + } + + return nil +} + +func (e *external) updateTags(ctx context.Context, observed *awsiam.GetUserOutput, cr *v1beta1.User) error { + add, remove, _ := iam.DiffIAMTagsWithUpdates(cr.Spec.ForProvider.Tags, observed.User.Tags) + + if len(add) > 0 { + if _, err := e.client.TagUser(ctx, &awsiam.TagUserInput{ + UserName: aws.String(meta.GetExternalName(cr)), + Tags: add, + }); err != nil { + return awsclient.Wrap(err, errTag) + } + } + + if len(remove) > 0 { + if _, err := e.client.UntagUser(ctx, &awsiam.UntagUserInput{ + TagKeys: remove, + UserName: aws.String(meta.GetExternalName(cr)), + }); err != nil { + return awsclient.Wrap(err, errUntag) + } + } + + return nil +} + +func isUpToDate(cr *v1beta1.User, user *types.User) bool { + // check path + isPathUpdated := aws.ToString(cr.Spec.ForProvider.Path) == aws.ToString(user.Path) + + // check tags + crTagMap := make(map[string]string, len(cr.Spec.ForProvider.Tags)) + for _, v := range cr.Spec.ForProvider.Tags { + crTagMap[v.Key] = v.Value + } + _, _, areTagsUpdated := iam.DiffIAMTags(crTagMap, user.Tags) + + // check permissions boundary + boundaryArn := "" + if user.PermissionsBoundary != nil { + boundaryArn = *user.PermissionsBoundary.PermissionsBoundaryArn + } + isBoundaryUpdated := + aws.ToString(cr.Spec.ForProvider.PermissionsBoundary) == aws.ToString(&boundaryArn) + + return isPathUpdated && areTagsUpdated && isBoundaryUpdated +} diff --git a/pkg/controller/iam/user/controller_test.go b/pkg/controller/iam/user/controller_test.go index a807360799..6b0b389be7 100644 --- a/pkg/controller/iam/user/controller_test.go +++ b/pkg/controller/iam/user/controller_test.go @@ -108,6 +108,18 @@ func withGroupVersionKind() userModifier { } } +func withPath(path *string) userModifier { + return func(r *v1beta1.User) { + r.Spec.ForProvider.Path = path + } +} + +func withBoundary(boundary *string) userModifier { + return func(r *v1beta1.User) { + r.Spec.ForProvider.PermissionsBoundary = boundary + } +} + func user(m ...userModifier) *v1beta1.User { cr := &v1beta1.User{} for _, f := range m { @@ -196,6 +208,31 @@ func TestObserve(t *testing.T) { }, }, }, + "DifferentBoundary": { + args: args{ + iam: &fake.MockUserClient{ + MockGetUser: func(ctx context.Context, input *awsiam.GetUserInput, opts []func(*awsiam.Options)) (*awsiam.GetUserOutput, error) { + return &awsiam.GetUserOutput{ + User: &awsiamtypes.User{ + PermissionsBoundary: &awsiamtypes.AttachedPermissionsBoundary{ + PermissionsBoundaryArn: aws.String("old"), + }, + }, + }, nil + }, + }, + cr: user(withExternalName(userName), withBoundary(aws.String("new"))), + }, + want: want{ + cr: user(withExternalName(userName), + withConditions(xpv1.Available()), + withBoundary(aws.String("new"))), + result: managed.ExternalObservation{ + ResourceExists: true, + ResourceUpToDate: false, + }, + }, + }, } for name, tc := range cases { @@ -331,12 +368,17 @@ func TestUpdate(t *testing.T) { MockUpdateUser: func(ctx context.Context, input *awsiam.UpdateUserInput, opts []func(*awsiam.Options)) (*awsiam.UpdateUserOutput, error) { return nil, errBoom }, + MockGetUser: func(ctx context.Context, input *awsiam.GetUserInput, opts []func(*awsiam.Options)) (*awsiam.GetUserOutput, error) { + return &awsiam.GetUserOutput{ + User: &awsiamtypes.User{}, + }, nil + }, }, - cr: user(withExternalName(userName)), + cr: user(withExternalName(userName), withPath(aws.String("foo"))), }, want: want{ - cr: user(withExternalName(userName)), - err: awsclient.Wrap(errBoom, errUpdate), + cr: user(withExternalName(userName), withPath(aws.String("foo"))), + err: awsclient.Wrap(errBoom, errUpdateUser), }, }, "GetUserError": {