diff --git a/pkg/cloud/services/iamauth/configmap.go b/pkg/cloud/services/iamauth/configmap.go index 05810afb1e..a25316b63f 100644 --- a/pkg/cloud/services/iamauth/configmap.go +++ b/pkg/cloud/services/iamauth/configmap.go @@ -88,6 +88,44 @@ func (b *configMapBackend) MapUser(mapping ekscontrolplanev1.UserMapping) error return b.saveAuthConfig(authConfig) } +func (b *configMapBackend) MapUsers(mappings []ekscontrolplanev1.UserMapping) error { + for _, mapping := range mappings { + if errs := mapping.Validate(); errs != nil { + return kerrors.NewAggregate(errs) + } + } + + authConfig, err := b.getAuthConfig() + if err != nil { + return fmt.Errorf("getting auth config: %w", err) + } + + authConfig.UserMappings = []ekscontrolplanev1.UserMapping{} + + authConfig.UserMappings = append(authConfig.UserMappings, mappings...) + + return b.saveAuthConfig(authConfig) +} + +func (b *configMapBackend) MapRoles(mappings []ekscontrolplanev1.RoleMapping) error { + for _, mapping := range mappings { + if errs := mapping.Validate(); errs != nil { + return kerrors.NewAggregate(errs) + } + } + + authConfig, err := b.getAuthConfig() + if err != nil { + return fmt.Errorf("getting auth config: %w", err) + } + + authConfig.RoleMappings = []ekscontrolplanev1.RoleMapping{} + + authConfig.RoleMappings = append(authConfig.RoleMappings, mappings...) + + return b.saveAuthConfig(authConfig) +} + func (b *configMapBackend) getAuthConfig() (*ekscontrolplanev1.IAMAuthenticatorConfig, error) { ctx := context.Background() diff --git a/pkg/cloud/services/iamauth/configmap_test.go b/pkg/cloud/services/iamauth/configmap_test.go index 12c38fde40..adbf487c76 100644 --- a/pkg/cloud/services/iamauth/configmap_test.go +++ b/pkg/cloud/services/iamauth/configmap_test.go @@ -315,6 +315,168 @@ func TestAddUserMappingCM(t *testing.T) { } } +func TestAddUserMappingsCM(t *testing.T) { + testCases := []struct { + name string + existingAuthConfigMap *corev1.ConfigMap + usersToMap []ekscontrolplanev1.UserMapping + expectError bool + }{ + { + name: "no existing user mappings, add user mapping", + usersToMap: []ekscontrolplanev1.UserMapping{ + { + UserARN: "arn:aws:iam::000000000000:user/Alice", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "alice", + Groups: []string{"system:masters"}, + }, + }, + { + UserARN: "arn:aws:iam::000000000000:user/John", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "john", + Groups: []string{"system:masters"}, + }, + }, + }, + expectError: false, + }, + { + name: "invalid arn", + usersToMap: []ekscontrolplanev1.UserMapping{ + { + UserARN: "a b c :: 123 --", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "sdfghjk", + Groups: []string{"system:masters"}, + }, + }, + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + var client crclient.Client + if tc.existingAuthConfigMap == nil { + client = fake.NewClientBuilder().Build() + } else { + client = fake.NewClientBuilder().WithObjects(tc.existingAuthConfigMap).Build() + } + backend, err := NewBackend(BackendTypeConfigMap, client) + g.Expect(err).To(BeNil()) + + err = backend.MapUsers(tc.usersToMap) + if tc.expectError { + g.Expect(err).ToNot(BeNil()) + return + } + + g.Expect(err).To(BeNil()) + }) + } +} + +func TestAddRoleMappingsCM(t *testing.T) { + testCases := []struct { + name string + existingAuthConfigMap *corev1.ConfigMap + rolesToMap []ekscontrolplanev1.RoleMapping + expectedRoleMaps []ekscontrolplanev1.RoleMapping + expectError bool + }{ + { + name: "no existing user mappings, add user mapping", + rolesToMap: []ekscontrolplanev1.RoleMapping{ + { + RoleARN: "arn:aws:iam::000000000000:role/KubernetesNode", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "system:node:{{EC2PrivateDNSName}}", + Groups: []string{"system:bootstrappers", "system:nodes"}, + }, + }, + }, + expectedRoleMaps: []ekscontrolplanev1.RoleMapping{ + { + RoleARN: "arn:aws:iam::000000000000:role/KubernetesNode", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "system:node:{{EC2PrivateDNSName}}", + Groups: []string{"system:bootstrappers", "system:nodes"}, + }, + }, + }, + expectError: false, + }, + { + name: "invalid arn", + rolesToMap: []ekscontrolplanev1.RoleMapping{ + { + RoleARN: "a b c :: 123 --", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "sdfghjk", + Groups: []string{"system:masters"}, + }, + }, + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + + var client crclient.Client + if tc.existingAuthConfigMap == nil { + client = fake.NewClientBuilder().Build() + } else { + client = fake.NewClientBuilder().WithObjects(tc.existingAuthConfigMap).Build() + } + backend, err := NewBackend(BackendTypeConfigMap, client) + g.Expect(err).To(BeNil()) + + err = backend.MapRoles(tc.rolesToMap) + if tc.expectError { + g.Expect(err).ToNot(BeNil()) + return + } + + g.Expect(err).To(BeNil()) + + key := types.NamespacedName{ + Name: "aws-auth", + Namespace: "kube-system", + } + + cm := &corev1.ConfigMap{} + + err = client.Get(context.TODO(), key, cm) + g.Expect(err).To(BeNil()) + + g.Expect(cm.Name).To(Equal("aws-auth")) + g.Expect(cm.Namespace).To(Equal("kube-system")) + g.Expect(cm.Data).ToNot(BeNil()) + + actualRoleMappings, roleMappingsFound := cm.Data["mapRoles"] + if len(tc.expectedRoleMaps) == 0 { + g.Expect(roleMappingsFound).To(BeFalse()) + } else { + roles := []ekscontrolplanev1.RoleMapping{} + err := yaml.Unmarshal([]byte(actualRoleMappings), &roles) + g.Expect(err).To(BeNil()) + g.Expect(len(roles)).To(Equal(len(tc.expectedRoleMaps))) + //TODO: we may need to do a better match + bothMatch := cmp.Equal(roles, tc.expectedRoleMaps) + g.Expect(bothMatch).To(BeTrue()) + } + }) + } +} + func createFakeConfigMap(roleMappings string, userMappings string) *corev1.ConfigMap { cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/cloud/services/iamauth/crd.go b/pkg/cloud/services/iamauth/crd.go index cb2b3847f4..609ccc959b 100644 --- a/pkg/cloud/services/iamauth/crd.go +++ b/pkg/cloud/services/iamauth/crd.go @@ -104,6 +104,26 @@ func (b *crdBackend) MapUser(mapping ekscontrolplanev1.UserMapping) error { return b.client.Create(ctx, iamMapping) } +func (b *crdBackend) MapRoles(mappings []ekscontrolplanev1.RoleMapping) error { + for _, mapping := range mappings { + if err := b.MapRole(mapping); err != nil { + return err + } + } + + return nil +} + +func (b *crdBackend) MapUsers(mappings []ekscontrolplanev1.UserMapping) error { + for _, mapping := range mappings { + if err := b.MapUser(mapping); err != nil { + return err + } + } + + return nil +} + func roleMappingMatchesIAMMap(mapping ekscontrolplanev1.RoleMapping, iamMapping *iamauthv1.IAMIdentityMapping) bool { if mapping.RoleARN != iamMapping.Spec.ARN { return false diff --git a/pkg/cloud/services/iamauth/crd_test.go b/pkg/cloud/services/iamauth/crd_test.go index 54bce64274..be1818a719 100644 --- a/pkg/cloud/services/iamauth/crd_test.go +++ b/pkg/cloud/services/iamauth/crd_test.go @@ -288,6 +288,170 @@ func TestAddUserMappingCRD(t *testing.T) { } } +func TestAddUserMappingsCRD(t *testing.T) { + testCases := []struct { + name string + mappings []ekscontrolplanev1.UserMapping + expectedError bool + }{ + { + name: "add single valid user mapping", + mappings: []ekscontrolplanev1.UserMapping{ + { + UserARN: "arn:aws:iam::000000000000:user/Alice", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "alice", + Groups: []string{"system:masters"}, + }, + }, + }, + expectedError: false, + }, + { + name: "add multiple valid user mappings", + mappings: []ekscontrolplanev1.UserMapping{ + { + UserARN: "arn:aws:iam::000000000000:user/Alice", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "alice", + Groups: []string{"system:masters"}, + }, + }, + { + UserARN: "arn:aws:iam::000000000000:user/Bob", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "bob", + Groups: []string{"system:admin"}, + }, + }, + }, + expectedError: false, + }, + { + name: "add invalid user mapping in list", + mappings: []ekscontrolplanev1.UserMapping{ + { + UserARN: "arn:aws:iam::000000000000:user/Alice", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "alice", + Groups: []string{"system:masters"}, + }, + }, + { + UserARN: "arn:aws:iam::000000000000:role/invalid", // Invalid role ARN + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "system:node:{{EC2PrivateDNSName}}", + Groups: []string{"system:masters"}, + }, + }, + }, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + scheme := runtime.NewScheme() + iamauthv1.AddToScheme(scheme) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + backend, err := NewBackend(BackendTypeCRD, client) + g.Expect(err).To(BeNil()) + + err = backend.MapUsers(tc.mappings) + if tc.expectedError { + g.Expect(err).ToNot(BeNil()) + return + } + + g.Expect(err).To(BeNil()) + }) + } +} + +func TestAddRoleMappingsCRD(t *testing.T) { + testCases := []struct { + name string + mappings []ekscontrolplanev1.RoleMapping + expectedError bool + }{ + { + name: "add single valid user mapping", + mappings: []ekscontrolplanev1.RoleMapping{ + { + RoleARN: "arn:aws:iam::000000000000:role/KubernetesNode", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "system:node:{{EC2PrivateDNSName}}", + Groups: []string{"system:bootstrappers", "system:nodes"}, + }, + }, + }, + expectedError: false, + }, + { + name: "add multiple valid user mappings", + mappings: []ekscontrolplanev1.RoleMapping{ + { + RoleARN: "arn:aws:iam::000000000000:role/KubernetesNode", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "system:node:{{EC2PrivateDNSName}}", + Groups: []string{"system:bootstrappers", "system:nodes"}, + }, + }, + { + RoleARN: "arn:aws:iam::000000000000:role/FakeNode", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "system:node:{{FakeNode}}", + Groups: []string{"system:bootstrappers", "system:nodes"}, + }, + }, + }, + expectedError: false, + }, + { + name: "add invalid user mapping in list", + mappings: []ekscontrolplanev1.RoleMapping{ + { + RoleARN: "arn:aws:iam::000000000000:user/Alice", + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "alice", + Groups: []string{"system:masters"}, + }, + }, + { + RoleARN: "arn:aws:iam::000000000000:role/invalid", // Invalid role ARN + KubernetesMapping: ekscontrolplanev1.KubernetesMapping{ + UserName: "system:node:{{EC2PrivateDNSName}}", + Groups: []string{"system:masters"}, + }, + }, + }, + expectedError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + g := NewGomegaWithT(t) + scheme := runtime.NewScheme() + iamauthv1.AddToScheme(scheme) + + client := fake.NewClientBuilder().WithScheme(scheme).Build() + backend, err := NewBackend(BackendTypeCRD, client) + g.Expect(err).To(BeNil()) + + err = backend.MapRoles(tc.mappings) + if tc.expectedError { + g.Expect(err).ToNot(BeNil()) + return + } + + g.Expect(err).To(BeNil()) + }) + } +} + func createIAMAuthMapping(arn string, username string, groups []string) *iamauthv1.IAMIdentityMapping { return &iamauthv1.IAMIdentityMapping{ ObjectMeta: metav1.ObjectMeta{ diff --git a/pkg/cloud/services/iamauth/iamauth.go b/pkg/cloud/services/iamauth/iamauth.go index 9bc0313699..b8d5e3cd9b 100644 --- a/pkg/cloud/services/iamauth/iamauth.go +++ b/pkg/cloud/services/iamauth/iamauth.go @@ -38,6 +38,10 @@ type AuthenticatorBackend interface { MapRole(mapping ekscontrolplanev1.RoleMapping) error // MapUser is used to map a user ARN to a user and set of groups MapUser(mapping ekscontrolplanev1.UserMapping) error + // MapUsers is used to set multiple user ARN to a users and groups + MapUsers(mapping []ekscontrolplanev1.UserMapping) error + // MapRoles is used to set multiple role ARN to a users and groups + MapRoles(mapping []ekscontrolplanev1.RoleMapping) error } // BackendType is a type that represents the different aws-iam-authenticator backends. diff --git a/pkg/cloud/services/iamauth/reconcile.go b/pkg/cloud/services/iamauth/reconcile.go index f33fca1d5c..d73500c836 100644 --- a/pkg/cloud/services/iamauth/reconcile.go +++ b/pkg/cloud/services/iamauth/reconcile.go @@ -73,18 +73,13 @@ func (s *Service) ReconcileIAMAuthenticator(ctx context.Context) error { s.scope.Debug("Mapping additional IAM roles and users") iamCfg := s.scope.IAMAuthConfig() - for _, roleMapping := range iamCfg.RoleMappings { - s.scope.Debug("Mapping IAM role", "iam-role", roleMapping.RoleARN, "user", roleMapping.UserName) - if err := authBackend.MapRole(roleMapping); err != nil { - return fmt.Errorf("mapping iam role: %w", err) - } + + if err := authBackend.MapRoles(iamCfg.RoleMappings); err != nil { + return fmt.Errorf("mapping iam role: %w", err) } - for _, userMapping := range iamCfg.UserMappings { - s.scope.Debug("Mapping IAM user", "iam-user", userMapping.UserARN, "user", userMapping.UserName) - if err := authBackend.MapUser(userMapping); err != nil { - return fmt.Errorf("mapping iam user: %w", err) - } + if err := authBackend.MapUsers(iamCfg.UserMappings); err != nil { + return fmt.Errorf("mapping iam user: %w", err) } s.scope.Info("Reconciled aws-iam-authenticator configuration", "cluster", klog.KRef("", s.scope.Name()))