diff --git a/apis/elbv2/v1beta1/ingressclassparams_types.go b/apis/elbv2/v1beta1/ingressclassparams_types.go index b9d7aa596a..3f8381848d 100644 --- a/apis/elbv2/v1beta1/ingressclassparams_types.go +++ b/apis/elbv2/v1beta1/ingressclassparams_types.go @@ -114,6 +114,100 @@ type IPAMConfiguration struct { IPv4IPAMPoolId *string `json:"ipv4IPAMPoolId,omitempty"` } +type AuthType string + +const ( + AuthTypeNone AuthType = "none" + AuthTypeCognito AuthType = "cognito" + AuthTypeOIDC AuthType = "oidc" +) + +// Amazon Cognito user pools configuration +type AuthIDPConfigCognito struct { + // The Amazon Resource Name (ARN) of the Amazon Cognito user pool. + UserPoolARN string `json:"userPoolARN"` + + // The ID of the Amazon Cognito user pool client. + UserPoolClientID string `json:"userPoolClientID"` + + // The domain prefix or fully-qualified domain name of the Amazon Cognito user pool. + // If you are using Amazon Cognito Domain, the userPoolDomain should be set to the domain prefix (my-domain) instead of full domain (https://my-domain.auth.us-west-2.amazoncognito.com). + UserPoolDomain string `json:"userPoolDomain"` + + // The query parameters (up to 10) to include in the redirect request to the authorization endpoint. + // +kubebuilder:validation:MinProperties=1 + // +kubebuilder:validation:MaxProperties=10 + // +optional + AuthenticationRequestExtraParams map[string]string `json:"authenticationRequestExtraParams,omitempty"` +} + +// OpenID Connect (OIDC) identity provider (IdP) configuration +type AuthIDPConfigOIDC struct { + // The OIDC issuer identifier of the IdP. + Issuer string `json:"issuer"` + + // The authorization endpoint of the IdP. + AuthorizationEndpoint string `json:"authorizationEndpoint"` + + // The token endpoint of the IdP. + TokenEndpoint string `json:"tokenEndpoint"` + + // The user info endpoint of the IdP. + UserInfoEndpoint string `json:"userInfoEndpoint"` + + // The k8s secret name. The secret must be in the 'default' namespace. + // Example format: + // apiVersion: v1 + // kind: Secret + // metadata: + // namespace: default + // name: my-k8s-secret + // data: + // clientID: base64 of your plain text clientId + // clientSecret: base64 of your plain text clientSecret + SecretName string `json:"secretName"` + + // The query parameters (up to 10) to include in the redirect request to the authorization endpoint. + // +kubebuilder:validation:MinProperties=1 + // +kubebuilder:validation:MaxProperties=10 + // +optional + AuthenticationRequestExtraParams map[string]string `json:"authenticationRequestExtraParams,omitempty"` +} + +// Authentication configuration for Ingress +type AuthConfig struct { + // The authentication type on targets. + // +kubebuilder:validation:Enum=none;oidc;cognito + Type AuthType `json:"type"` + + // The Cognito IdP configuration. + // +optional + IDPConfigCognito *AuthIDPConfigCognito `json:"idpCognitoConfiguration,omitempty"` + + // The OIDC IdP configuration. + // +optional + IDPConfigOIDC *AuthIDPConfigOIDC `json:"idpOidcConfiguration,omitempty"` + + // The behavior if the user is not authenticated. + // +kubebuilder:validation:Enum=authenticate;deny;allow + // +optional + OnUnauthenticatedRequest string `json:"onUnauthenticatedRequest,omitempty"` + + // The set of user claims to be requested from the Cognito IdP or OIDC IdP, in a space-separated list. + // * Options: phone, email, profile, openid, aws.cognito.signin.user.admin + // * Ex. 'email openid' + // +optional + Scope string `json:"scope,omitempty"` + + // The name of the cookie used to maintain session information. + // +optional + SessionCookieName string `json:"sessionCookie,omitempty"` + + // The maximum duration of the authentication session, in seconds. + // +optional + SessionTimeout *int64 `json:"sessionTimeout,omitempty"` +} + // IngressClassParamsSpec defines the desired state of IngressClassParams type IngressClassParamsSpec struct { // CertificateArn specifies the ARN of the certificates for all Ingresses that belong to IngressClass with this IngressClassParams. @@ -121,7 +215,7 @@ type IngressClassParamsSpec struct { CertificateArn []string `json:"certificateArn,omitempty"` // NamespaceSelector restrict the namespaces of Ingresses that are allowed to specify the IngressClass with this IngressClassParams. - // * if absent or present but empty, it selects all namespaces. + // * If absent or present but empty, it selects all namespaces. // +optional NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty"` @@ -145,11 +239,12 @@ type IngressClassParamsSpec struct { // +optional Subnets *SubnetSelector `json:"subnets,omitempty"` - // IPAddressType defines the ip address type for all Ingresses that belong to IngressClass with this IngressClassParams. + // IPAddressType defines the IP address type for all Ingresses that belong to IngressClass with this IngressClassParams. // +optional IPAddressType *IPAddressType `json:"ipAddressType,omitempty"` // Tags defines list of Tags on AWS resources provisioned for Ingresses that belong to IngressClass with this IngressClassParams. + // +optional Tags []Tag `json:"tags,omitempty"` // LoadBalancerAttributes define the custom attributes to LoadBalancers for all Ingress that that belong to IngressClass with this IngressClassParams. @@ -169,7 +264,12 @@ type IngressClassParamsSpec struct { IPAMConfiguration *IPAMConfiguration `json:"ipamConfiguration,omitempty"` // PrefixListsIDs defines the security group prefix lists for all Ingresses that belong to IngressClass with this IngressClassParams. + // +optional PrefixListsIDs []string `json:"PrefixListsIDs,omitempty"` + + // AuthenticationConfiguration defines the authentication configuration for a Load Balancer. Application Load Balancer (ALB) supports authentication with Cognito or OIDC. + // +optional + AuthConfig *AuthConfig `json:"authenticationConfiguration,omitempty"` } // +kubebuilder:object:root=true diff --git a/apis/elbv2/v1beta1/zz_generated.deepcopy.go b/apis/elbv2/v1beta1/zz_generated.deepcopy.go index f94b84d778..67f3565746 100644 --- a/apis/elbv2/v1beta1/zz_generated.deepcopy.go +++ b/apis/elbv2/v1beta1/zz_generated.deepcopy.go @@ -41,6 +41,80 @@ func (in *Attribute) DeepCopy() *Attribute { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthConfig) DeepCopyInto(out *AuthConfig) { + *out = *in + if in.IDPConfigCognito != nil { + in, out := &in.IDPConfigCognito, &out.IDPConfigCognito + *out = new(AuthIDPConfigCognito) + (*in).DeepCopyInto(*out) + } + if in.IDPConfigOIDC != nil { + in, out := &in.IDPConfigOIDC, &out.IDPConfigOIDC + *out = new(AuthIDPConfigOIDC) + (*in).DeepCopyInto(*out) + } + if in.SessionTimeout != nil { + in, out := &in.SessionTimeout, &out.SessionTimeout + *out = new(int64) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthConfig. +func (in *AuthConfig) DeepCopy() *AuthConfig { + if in == nil { + return nil + } + out := new(AuthConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthIDPConfigCognito) DeepCopyInto(out *AuthIDPConfigCognito) { + *out = *in + if in.AuthenticationRequestExtraParams != nil { + in, out := &in.AuthenticationRequestExtraParams, &out.AuthenticationRequestExtraParams + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthIDPConfigCognito. +func (in *AuthIDPConfigCognito) DeepCopy() *AuthIDPConfigCognito { + if in == nil { + return nil + } + out := new(AuthIDPConfigCognito) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthIDPConfigOIDC) DeepCopyInto(out *AuthIDPConfigOIDC) { + *out = *in + if in.AuthenticationRequestExtraParams != nil { + in, out := &in.AuthenticationRequestExtraParams, &out.AuthenticationRequestExtraParams + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthIDPConfigOIDC. +func (in *AuthIDPConfigOIDC) DeepCopy() *AuthIDPConfigOIDC { + if in == nil { + return nil + } + out := new(AuthIDPConfigOIDC) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IPAMConfiguration) DeepCopyInto(out *IPAMConfiguration) { *out = *in @@ -204,6 +278,11 @@ func (in *IngressClassParamsSpec) DeepCopyInto(out *IngressClassParamsSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AuthConfig != nil { + in, out := &in.AuthConfig, &out.AuthConfig + *out = new(AuthConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressClassParamsSpec. diff --git a/config/crd/bases/elbv2.k8s.aws_ingressclassparams.yaml b/config/crd/bases/elbv2.k8s.aws_ingressclassparams.yaml index 326147649a..5dc35f0007 100644 --- a/config/crd/bases/elbv2.k8s.aws_ingressclassparams.yaml +++ b/config/crd/bases/elbv2.k8s.aws_ingressclassparams.yaml @@ -61,6 +61,113 @@ spec: items: type: string type: array + authenticationConfiguration: + description: AuthenticationConfiguration defines the authentication + configuration for a Load Balancer. Application Load Balancer (ALB) + supports authentication with Cognito or OIDC. + properties: + idpCognitoConfiguration: + description: The Cognito IdP configuration. + properties: + authenticationRequestExtraParams: + additionalProperties: + type: string + description: The query parameters (up to 10) to include in + the redirect request to the authorization endpoint. + maxProperties: 10 + minProperties: 1 + type: object + userPoolARN: + description: The Amazon Resource Name (ARN) of the Amazon + Cognito user pool. + type: string + userPoolClientID: + description: The ID of the Amazon Cognito user pool client. + type: string + userPoolDomain: + description: |- + The domain prefix or fully-qualified domain name of the Amazon Cognito user pool. + If you are using Amazon Cognito Domain, the userPoolDomain should be set to the domain prefix (my-domain) instead of full domain (https://my-domain.auth.us-west-2.amazoncognito.com). + type: string + required: + - userPoolARN + - userPoolClientID + - userPoolDomain + type: object + idpOidcConfiguration: + description: The OIDC IdP configuration. + properties: + authenticationRequestExtraParams: + additionalProperties: + type: string + description: The query parameters (up to 10) to include in + the redirect request to the authorization endpoint. + maxProperties: 10 + minProperties: 1 + type: object + authorizationEndpoint: + description: The authorization endpoint of the IdP. + type: string + issuer: + description: The OIDC issuer identifier of the IdP. + type: string + secretName: + description: |- + The k8s secret name. The secret must be in the 'default' namespace. + Example format: + apiVersion: v1 + kind: Secret + metadata: + namespace: default + name: my-k8s-secret + data: + clientID: base64 of your plain text clientId + clientSecret: base64 of your plain text clientSecret + type: string + tokenEndpoint: + description: The token endpoint of the IdP. + type: string + userInfoEndpoint: + description: The user info endpoint of the IdP. + type: string + required: + - authorizationEndpoint + - issuer + - secretName + - tokenEndpoint + - userInfoEndpoint + type: object + onUnauthenticatedRequest: + description: The behavior if the user is not authenticated. + enum: + - authenticate + - deny + - allow + type: string + scope: + description: |- + The set of user claims to be requested from the Cognito IdP or OIDC IdP, in a space-separated list. + * Options: phone, email, profile, openid, aws.cognito.signin.user.admin + * Ex. 'email openid' + type: string + sessionCookie: + description: The name of the cookie used to maintain session information. + type: string + sessionTimeout: + description: The maximum duration of the authentication session, + in seconds. + format: int64 + type: integer + type: + description: The authentication type on targets. + enum: + - none + - oidc + - cognito + type: string + required: + - type + type: object certificateArn: description: CertificateArn specifies the ARN of the certificates for all Ingresses that belong to IngressClass with this IngressClassParams. @@ -84,7 +191,7 @@ spec: type: string type: array ipAddressType: - description: IPAddressType defines the ip address type for all Ingresses + description: IPAddressType defines the IP address type for all Ingresses that belong to IngressClass with this IngressClassParams. enum: - ipv4 @@ -163,7 +270,7 @@ spec: namespaceSelector: description: |- NamespaceSelector restrict the namespaces of Ingresses that are allowed to specify the IngressClass with this IngressClassParams. - * if absent or present but empty, it selects all namespaces. + * If absent or present but empty, it selects all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. diff --git a/controllers/ingress/eventhandlers/ingress_class_params_events.go b/controllers/ingress/eventhandlers/ingress_class_params_events.go index d574197caa..1259b95a3d 100644 --- a/controllers/ingress/eventhandlers/ingress_class_params_events.go +++ b/controllers/ingress/eventhandlers/ingress_class_params_events.go @@ -61,8 +61,8 @@ func (h *enqueueRequestsForIngressClassParamsEvent) Delete(ctx context.Context, h.enqueueImpactedIngressClasses(ctx, ingClassParamsOld) } -func (h *enqueueRequestsForIngressClassParamsEvent) Generic(context.Context, event.TypedGenericEvent[*elbv2api.IngressClassParams], workqueue.TypedRateLimitingInterface[reconcile.Request]) { - // we don't have any generic event for secrets. +func (h *enqueueRequestsForIngressClassParamsEvent) Generic(ctx context.Context, e event.TypedGenericEvent[*elbv2api.IngressClassParams], _ workqueue.TypedRateLimitingInterface[reconcile.Request]) { + h.enqueueImpactedIngressClasses(ctx, e.Object) } func (h *enqueueRequestsForIngressClassParamsEvent) enqueueImpactedIngressClasses(ctx context.Context, ingClassParams *elbv2api.IngressClassParams) { diff --git a/controllers/ingress/eventhandlers/secret_events.go b/controllers/ingress/eventhandlers/secret_events.go index 2b949e01f2..ba5479da54 100644 --- a/controllers/ingress/eventhandlers/secret_events.go +++ b/controllers/ingress/eventhandlers/secret_events.go @@ -2,6 +2,7 @@ package eventhandlers import ( "context" + elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1" "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/go-logr/logr" @@ -18,11 +19,12 @@ import ( ) // NewEnqueueRequestsForSecretEvent constructs new enqueueRequestsForSecretEvent. -func NewEnqueueRequestsForSecretEvent(ingEventChan chan<- event.TypedGenericEvent[*networking.Ingress], svcEventChan chan<- event.TypedGenericEvent[*corev1.Service], +func NewEnqueueRequestsForSecretEvent(ingEventChan chan<- event.TypedGenericEvent[*networking.Ingress], svcEventChan chan<- event.TypedGenericEvent[*corev1.Service], icpEventChan chan<- event.TypedGenericEvent[*elbv2api.IngressClassParams], k8sClient client.Client, eventRecorder record.EventRecorder, logger logr.Logger) handler.TypedEventHandler[*corev1.Secret, reconcile.Request] { return &enqueueRequestsForSecretEvent{ ingEventChan: ingEventChan, svcEventChan: svcEventChan, + icpEventChan: icpEventChan, k8sClient: k8sClient, eventRecorder: eventRecorder, logger: logger, @@ -34,6 +36,7 @@ var _ handler.TypedEventHandler[*corev1.Secret, reconcile.Request] = (*enqueueRe type enqueueRequestsForSecretEvent struct { ingEventChan chan<- event.TypedGenericEvent[*networking.Ingress] svcEventChan chan<- event.TypedGenericEvent[*corev1.Service] + icpEventChan chan<- event.TypedGenericEvent[*elbv2api.IngressClassParams] k8sClient client.Client eventRecorder record.EventRecorder logger logr.Logger @@ -69,7 +72,7 @@ func (h *enqueueRequestsForSecretEvent) Generic(ctx context.Context, e event.Typ h.enqueueImpactedObjects(ctx, secretObj) } -func (h *enqueueRequestsForSecretEvent) enqueueImpactedObjects(ctx context.Context, secret *corev1.Secret) { +func (h *enqueueRequestsForSecretEvent) enqueueImpactedObjects(_ context.Context, secret *corev1.Secret) { secretKey := k8s.NamespacedName(secret) ingList := &networking.IngressList{} @@ -79,6 +82,7 @@ func (h *enqueueRequestsForSecretEvent) enqueueImpactedObjects(ctx context.Conte h.logger.Error(err, "failed to fetch ingresses") return } + for index := range ingList.Items { ing := &ingList.Items[index] @@ -107,4 +111,27 @@ func (h *enqueueRequestsForSecretEvent) enqueueImpactedObjects(ctx context.Conte Object: svc, } } + + if h.icpEventChan == nil { + return + } + + ingressClassParamsList := &elbv2api.IngressClassParamsList{} + if err := h.k8sClient.List(context.Background(), ingressClassParamsList, + client.InNamespace(""), + client.MatchingFields{ingress.IndexKeyIngressClassParamsSecretRefName: secret.GetName()}); err != nil { + h.logger.Error(err, "failed to fetch ingress class params") + return + } + + for index := range ingressClassParamsList.Items { + icp := &ingressClassParamsList.Items[index] + + h.logger.Info("enqueue ingress class for secret event", + "secret", secretKey, + "icp", k8s.NamespacedName(icp)) + h.icpEventChan <- event.TypedGenericEvent[*elbv2api.IngressClassParams]{ + Object: icp, + } + } } diff --git a/controllers/ingress/group_controller.go b/controllers/ingress/group_controller.go index 13cd9234dc..b9ee54f9f7 100644 --- a/controllers/ingress/group_controller.go +++ b/controllers/ingress/group_controller.go @@ -3,9 +3,6 @@ package ingress import ( "context" "fmt" - - "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/go-logr/logr" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" @@ -35,6 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" ) @@ -45,6 +43,10 @@ const ( // the groupVersion of used Ingress & IngressClass resource. ingressResourcesGroupVersion = "networking.k8s.io/v1" ingressClassKind = "IngressClass" + + // the groupVersion of used by IngressClassParams resource. + ingressClassParamsResourcesGroupVersion = "elbv2.k8s.aws/v1beta1" + ingressClassParamsKind = "IngressClassParams" ) // NewGroupReconciler constructs new GroupReconciler @@ -278,12 +280,20 @@ func (r *groupReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager return err } - resList, err := clientSet.ServerResourcesForGroupVersion(ingressResourcesGroupVersion) + ingressClassResourceList, err := clientSet.ServerResourcesForGroupVersion(ingressResourcesGroupVersion) if err != nil { return err } - ingressClassResourceAvailable := isResourceKindAvailable(resList, ingressClassKind) - if err := r.setupIndexes(ctx, mgr.GetFieldIndexer(), ingressClassResourceAvailable); err != nil { + + ingressClassParamsResourceList, err := clientSet.ServerResourcesForGroupVersion(ingressClassParamsResourcesGroupVersion) + if err != nil { + return err + } + + ingressClassResourceAvailable := isResourceKindAvailable(ingressClassResourceList, ingressClassKind) + + ingressClassParamsResourceAvailable := isResourceKindAvailable(ingressClassParamsResourceList, ingressClassParamsKind) + if err := r.setupIndexes(ctx, mgr.GetFieldIndexer(), ingressClassResourceAvailable, ingressClassParamsResourceAvailable); err != nil { return err } if err := r.setupWatches(ctx, c, mgr, ingressClassResourceAvailable, clientSet); err != nil { @@ -292,7 +302,8 @@ func (r *groupReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager return nil } -func (r *groupReconciler) setupIndexes(ctx context.Context, fieldIndexer client.FieldIndexer, ingressClassResourceAvailable bool) error { +func (r *groupReconciler) setupIndexes(ctx context.Context, fieldIndexer client.FieldIndexer, ingressClassResourceAvailable bool, ingressClassParamsResourceAvailable bool) error { + // Service indexes if err := fieldIndexer.IndexField(ctx, &networking.Ingress{}, ingress.IndexKeyServiceRefName, func(obj client.Object) []string { return r.referenceIndexer.BuildServiceRefIndexes(context.Background(), obj.(*networking.Ingress)) @@ -300,20 +311,23 @@ func (r *groupReconciler) setupIndexes(ctx context.Context, fieldIndexer client. ); err != nil { return err } + if err := fieldIndexer.IndexField(ctx, &networking.Ingress{}, ingress.IndexKeySecretRefName, func(obj client.Object) []string { - return r.referenceIndexer.BuildSecretRefIndexes(context.Background(), obj.(*networking.Ingress)) + return r.referenceIndexer.BuildSecretRefIndexes(context.Background(), &elbv2api.IngressClassParams{}, obj.(*networking.Ingress)) }, ); err != nil { return err } + if err := fieldIndexer.IndexField(ctx, &corev1.Service{}, ingress.IndexKeySecretRefName, func(obj client.Object) []string { - return r.referenceIndexer.BuildSecretRefIndexes(context.Background(), obj.(*corev1.Service)) + return r.referenceIndexer.BuildSecretRefIndexes(context.Background(), &elbv2api.IngressClassParams{}, obj.(*corev1.Service)) }, ); err != nil { return err } + if ingressClassResourceAvailable { if err := fieldIndexer.IndexField(ctx, &networking.IngressClass{}, ingress.IndexKeyIngressClassParamsRefName, func(obj client.Object) []string { @@ -330,6 +344,16 @@ func (r *groupReconciler) setupIndexes(ctx context.Context, fieldIndexer client. return err } } + + if ingressClassParamsResourceAvailable { + if err := fieldIndexer.IndexField(ctx, &elbv2api.IngressClassParams{}, ingress.IndexKeyIngressClassParamsSecretRefName, + func(obj client.Object) []string { + return r.referenceIndexer.BuildIngressClassParamsSecretIndexes(context.Background(), obj.(*elbv2api.IngressClassParams)) + }, + ); err != nil { + return err + } + } return nil } @@ -341,8 +365,7 @@ func (r *groupReconciler) setupWatches(_ context.Context, c controller.Controlle r.logger.WithName("eventHandlers").WithName("ingress")) svcEventHandler := eventhandlers.NewEnqueueRequestsForServiceEvent(ingEventChan, r.k8sClient, r.eventRecorder, r.logger.WithName("eventHandlers").WithName("service")) - secretEventHandler := eventhandlers.NewEnqueueRequestsForSecretEvent(ingEventChan, svcEventChan, r.k8sClient, r.eventRecorder, - r.logger.WithName("eventHandlers").WithName("secret")) + if err := c.Watch(source.Channel(ingEventChan, ingEventHandler)); err != nil { return err } @@ -355,11 +378,11 @@ func (r *groupReconciler) setupWatches(_ context.Context, c controller.Controlle if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.Service{}, svcEventHandler)); err != nil { return err } - if err := c.Watch(source.Channel(secretEventsChan, secretEventHandler)); err != nil { - return err - } + + var ingressClassParamsEventChan chan event.TypedGenericEvent[*elbv2api.IngressClassParams] if ingressClassResourceAvailable { ingClassEventChan := make(chan event.TypedGenericEvent[*networking.IngressClass]) + ingressClassParamsEventChan = make(chan event.TypedGenericEvent[*elbv2api.IngressClassParams]) ingClassParamsEventHandler := eventhandlers.NewEnqueueRequestsForIngressClassParamsEvent(ingClassEventChan, r.k8sClient, r.eventRecorder, r.logger.WithName("eventHandlers").WithName("ingressClassParams")) ingClassEventHandler := eventhandlers.NewEnqueueRequestsForIngressClassEvent(ingEventChan, r.k8sClient, r.eventRecorder, @@ -373,7 +396,21 @@ func (r *groupReconciler) setupWatches(_ context.Context, c controller.Controlle if err := c.Watch(source.Kind(mgr.GetCache(), &networking.IngressClass{}, ingClassEventHandler)); err != nil { return err } + if err := c.Watch(source.Channel(ingressClassParamsEventChan, ingClassParamsEventHandler)); err != nil { + return err + } + } + + secretEventHandler := eventhandlers.NewEnqueueRequestsForSecretEvent(ingEventChan, svcEventChan, ingressClassParamsEventChan, r.k8sClient, r.eventRecorder, + r.logger.WithName("eventHandlers").WithName("secret")) + // Add this watch for Secrets + if err := c.Watch(source.Kind(mgr.GetCache(), &corev1.Secret{}, secretEventHandler)); err != nil { + return err } + if err := c.Watch(source.Channel(secretEventsChan, secretEventHandler)); err != nil { + return err + } + r.secretsManager = k8s.NewSecretsManager(clientSet, secretEventsChan, ctrl.Log.WithName("secrets-manager")) return nil } diff --git a/docs/guide/ingress/ingress_class.md b/docs/guide/ingress/ingress_class.md index aa01329e60..f9d47c6f1f 100644 --- a/docs/guide/ingress/ingress_class.md +++ b/docs/guide/ingress/ingress_class.md @@ -149,11 +149,48 @@ You can use IngressClassParams to enforce settings for a set of Ingresses. spec: minimumLoadBalancerCapacity: capacityUnits: 1000 + ``` + - with authenticationConfiguration type cognito ``` + apiVersion: elbv2.k8s.aws/v1beta1 + kind: IngressClassParams + metadata: + name: my-ingress-class-params + spec: + authenticationConfiguration: + type: cognito + idpCognitoConfiguration: + userPoolARN: arn:aws:cognito-idp:us-east-x:xxxx + userPoolClientID: my-client-id + userPoolDomain: us-east-1xxxx + onUnauthenticatedRequest: deny + sessionTimeout: 12345 + ``` + - with authenticationConfiguration type oidc + ``` + apiVersion: elbv2.k8s.aws/v1beta1 + kind: IngressClassParams + metadata: + name: my-ingress-class-params + spec: + authenticationConfiguration: + type: oidc + idpOidcConfiguration: + issuer: https://my-site.com + authorizationEndpoint: https://super-strong-auth.my-site.com + tokenEndpoint: https://token.my-site.com + userInfoEndpoint: https://user.my-site.com + secretName: top-secret + authenticationRequestExtraParams: + key: "value" + onUnauthenticatedRequest: deny + sessionTimeout: 12345 + scope: email openid ### IngressClassParams specification #### spec.namespaceSelector + `namespaceSelector` is an optional setting that follows general Kubernetes [label selector](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors) semantics. @@ -165,7 +202,7 @@ Cluster administrators can use the `namespaceSelector` field to restrict the nam #### spec.group -`group` is an optional setting. The only available sub-field is `group.name`. +`group` is an optional setting. The only available sub-field is `group.name`. Cluster administrators can use `group.name` field to denote the groupName for all Ingresses belong to this IngressClass. @@ -187,8 +224,9 @@ Cluster administrators can use the optional `inboundCIDRs` field to specify the If the field is specified, LBC will ignore the `alb.ingress.kubernetes.io/inbound-cidrs` annotation. #### spec.certificateArn + Cluster administrators can use the optional `certificateARN` field to specify the ARN of the certificates for all Ingresses that belong to IngressClass with this IngressClassParams. - + If the field is specified, LBC will ignore the `alb.ingress.kubernetes.io/certificate-arn` annotation. #### spec.sslPolicy @@ -267,3 +305,109 @@ Cluster administrators can use `prefixListIDs` field to specify the managed pref 1. If `prefixListIDs` is set, the prefix lists defined will be applied to the load balancer that belong to this IngressClass. If you specify invalid prefix list IDs, the controller will fail to reconcile ingresses belonging to the particular ingress class. 2. If `prefixListIDs` un-specified, Ingresses with this IngressClass can continue to use `alb.ingress.kubernetes.io/security-group-prefix-lists` annotation to specify the load balancer prefix lists. + +#### spec.authenticationConfiguration + +Cluster administrators can use the optional `authenticationConfiguration` field to specify the authentication configuration for all load balancers that belong to this IngressClass. Application Load Balancer (ALB) supports authentication with Cognito or OIDC for HTTPS listeners. + +For all authentication types, the following specifications are available: + +- `type` + - The authentication type on targets. + - Value: `none`, `oidc`, `cognito` + - Type: `string` + - Required +- `idpCognitoConfiguration` + - The Cognito IdP configuration. + - Type: `object` +- `idpOidcConfiguration` + - The OIDC IdP configuration. + - Type: `object` +- `onUnauthenticatedRequest` + - The behavior if the user is not authenticated. + - Value: `authenticate`, `deny`, `allow` + - Type: `string` +- `scope` + - The set of user claims to be requested from the Cognito IDP or OIDC IDP, in a space-separated list. + - Options: `phone`, `email`, `profile`, `openid`, `aws.cognito.signin.user.admin` + - Ex. `'email openid'` + - Type: `string` +- `sessionCookie` + - The name of the cookie used to maintain session information. + - Type: `string` +- `sessionTimeout` + - The maximum duration of the authentication session, in seconds. + - Type: `integer` + +If the `authenticationConfiguration` type is `oidc`, then set the `idpOidcConfiguration` field with the following properties + +- `authorizationEndpoint` + - The authorization endpoint of the IdP. + - Type: `string` + - Required +- `issuer` + - The OIDC issuer identifier of the IdP. + - Type: `string` + - Required +- `secretName` + - The k8s secretName. + - The secret must be created in the `default` namespace. It holds the OIDC `clientID` and `clientSecret`. + - Example format: + ``` + apiVersion: v1 + kind: Secret + metadata: + namespace: default + name: my-k8s-secret + data: + clientID: base64 of your plain text clientId + clientSecret: base64 of your plain text clientSecret + ``` + - Type: `string` + - Required +- `tokenEndpoint` + - The token endpoint of the IdP. + - Type: `string` + - Required +- `userInfoEndpoint` + - The user info endpoint of the IdP. + - Type: `string` + - Required +- `additionalProperties` + - The query parameters (up to 10) to include in the redirect request to the authorization endpoint. + - Type: `object` + +If the `authenticationConfiguration` type is `cognito`, then set the `idpCognitoConfiguration` field with the following properties + +- `authenticationRequestExtraParams` + - The query parameters (up to 10) to include in the redirect request to the authorization endpoint. + - Type: `object` + - Required +- `userPoolARN` + - The Amazon Resource Name (ARN) of the Amazon Cognito user pool. + - Type: `string` + - Required +- `userPoolClientID` + - The ID of the Amazon Cognito user pool client. + - Type: `string` + - Required +- `userPoolDomain` + - The domain prefix or fully-qualified domain name of the Amazon Cognito user pool. + - If you are using Amazon Cognito Domain, the userPoolDomain should be set to the domain prefix (ex. my-domain) instead of full domain (ex. https://my-domain.auth.us-west-2.amazoncognito.com). + - Type: `string` + - Required +- `additionalProperties` + - The query parameters (up to 10) to include in the redirect request to the authorization endpoint. + - Type: `object` + +To remove the IngressClass authentication configuration from your ALB, remove `spec.authenticationConfiguration` from the IngressClass definition. + +When `spec.authenticationConfiguration` is specified, LBC will ignore the following Ingress annotations: + +- `alb.ingress.kubernetes.io/auth-type` +- `alb.ingress.kubernetes.io/auth-idp-cognito` +- `alb.ingress.kubernetes.io/auth-idp-oidc` +- `alb.ingress.kubernetes.io/auth-on-unauthenticated-request` +- `alb.ingress.kubernetes.io/auth-scope` +- `alb.ingress.kubernetes.io/auth-session-cookie` +- `alb.ingress.kubernetes.io/auth-session-timeout` diff --git a/helm/aws-load-balancer-controller/crds/crds.yaml b/helm/aws-load-balancer-controller/crds/crds.yaml index 61bf667573..ce39150157 100644 --- a/helm/aws-load-balancer-controller/crds/crds.yaml +++ b/helm/aws-load-balancer-controller/crds/crds.yaml @@ -60,6 +60,113 @@ spec: items: type: string type: array + authenticationConfiguration: + description: AuthenticationConfiguration defines the authentication + configuration for a Load Balancer. Application Load Balancer (ALB) + supports authentication with Cognito or OIDC. + properties: + idpCognitoConfiguration: + description: The Cognito IdP configuration. + properties: + authenticationRequestExtraParams: + additionalProperties: + type: string + description: The query parameters (up to 10) to include in + the redirect request to the authorization endpoint. + maxProperties: 10 + minProperties: 1 + type: object + userPoolARN: + description: The Amazon Resource Name (ARN) of the Amazon + Cognito user pool. + type: string + userPoolClientID: + description: The ID of the Amazon Cognito user pool client. + type: string + userPoolDomain: + description: |- + The domain prefix or fully-qualified domain name of the Amazon Cognito user pool. + If you are using Amazon Cognito Domain, the userPoolDomain should be set to the domain prefix (my-domain) instead of full domain (https://my-domain.auth.us-west-2.amazoncognito.com). + type: string + required: + - userPoolARN + - userPoolClientID + - userPoolDomain + type: object + idpOidcConfiguration: + description: The OIDC IdP configuration. + properties: + authenticationRequestExtraParams: + additionalProperties: + type: string + description: The query parameters (up to 10) to include in + the redirect request to the authorization endpoint. + maxProperties: 10 + minProperties: 1 + type: object + authorizationEndpoint: + description: The authorization endpoint of the IdP. + type: string + issuer: + description: The OIDC issuer identifier of the IdP. + type: string + secretName: + description: |- + The k8s secret name. The secret must be in the 'default' namespace. + Example format: + apiVersion: v1 + kind: Secret + metadata: + namespace: default + name: my-k8s-secret + data: + clientID: base64 of your plain text clientId + clientSecret: base64 of your plain text clientSecret + type: string + tokenEndpoint: + description: The token endpoint of the IdP. + type: string + userInfoEndpoint: + description: The user info endpoint of the IdP. + type: string + required: + - authorizationEndpoint + - issuer + - secretName + - tokenEndpoint + - userInfoEndpoint + type: object + onUnauthenticatedRequest: + description: The behavior if the user is not authenticated. + enum: + - authenticate + - deny + - allow + type: string + scope: + description: |- + The set of user claims to be requested from the Cognito IdP or OIDC IdP, in a space-separated list. + * Options: phone, email, profile, openid, aws.cognito.signin.user.admin + * Ex. 'email openid' + type: string + sessionCookie: + description: The name of the cookie used to maintain session information. + type: string + sessionTimeout: + description: The maximum duration of the authentication session, + in seconds. + format: int64 + type: integer + type: + description: The authentication type on targets. + enum: + - none + - oidc + - cognito + type: string + required: + - type + type: object certificateArn: description: CertificateArn specifies the ARN of the certificates for all Ingresses that belong to IngressClass with this IngressClassParams. @@ -83,7 +190,7 @@ spec: type: string type: array ipAddressType: - description: IPAddressType defines the ip address type for all Ingresses + description: IPAddressType defines the IP address type for all Ingresses that belong to IngressClass with this IngressClassParams. enum: - ipv4 @@ -162,7 +269,7 @@ spec: namespaceSelector: description: |- NamespaceSelector restrict the namespaces of Ingresses that are allowed to specify the IngressClass with this IngressClassParams. - * if absent or present but empty, it selects all namespaces. + * If absent or present but empty, it selects all namespaces. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. diff --git a/pkg/ingress/auth_config_builder.go b/pkg/ingress/auth_config_builder.go index 1f4fc1359e..d8d47f7780 100644 --- a/pkg/ingress/auth_config_builder.go +++ b/pkg/ingress/auth_config_builder.go @@ -2,7 +2,9 @@ package ingress import ( "context" + "github.com/pkg/errors" + elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1" "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations" ) @@ -27,7 +29,7 @@ type AuthConfig struct { // AuthConfig builder can build auth configuration for service or ingresses. type AuthConfigBuilder interface { - Build(ctx context.Context, svcAndIngAnnotations map[string]string) (AuthConfig, error) + Build(ctx context.Context, ingressClassParams *elbv2api.IngressClassParams, svcAndIngAnnotations map[string]string) (AuthConfig, error) } // NewDefaultAuthConfigBuilder constructs new defaultAuthConfigBuilder. @@ -39,28 +41,33 @@ func NewDefaultAuthConfigBuilder(annotationParser annotations.Parser) *defaultAu var _ AuthConfigBuilder = &defaultAuthConfigBuilder{} -// default implementation for AuthConfigBuilder +// Default implementation for AuthConfigBuilder type defaultAuthConfigBuilder struct { annotationParser annotations.Parser } -func (b *defaultAuthConfigBuilder) Build(ctx context.Context, svcAndIngAnnotations map[string]string) (AuthConfig, error) { - authType, err := b.buildAuthType(ctx, svcAndIngAnnotations) +func (b *defaultAuthConfigBuilder) Build(ctx context.Context, ingressClassParams *elbv2api.IngressClassParams, svcAndIngAnnotations map[string]string) (AuthConfig, error) { + err := b.validateIngressClassParamsAuthConfig(ingressClassParams) + if err != nil { + return AuthConfig{}, err + } + + authType, err := b.buildAuthType(ctx, ingressClassParams, svcAndIngAnnotations) if err != nil { return AuthConfig{}, err } - authOnUnauthenticatedRequest := b.buildAuthOnUnauthenticatedRequest(ctx, svcAndIngAnnotations) - authScope := b.buildAuthScope(ctx, svcAndIngAnnotations) - authSessionCookieName := b.buildAuthSessionCookieName(ctx, svcAndIngAnnotations) - authSessionTimeout, err := b.buildAuthSessionTimeout(ctx, svcAndIngAnnotations) + authOnUnauthenticatedRequest := b.buildAuthOnUnauthenticatedRequest(ctx, ingressClassParams, svcAndIngAnnotations) + authScope := b.buildAuthScope(ctx, ingressClassParams, svcAndIngAnnotations) + authSessionCookieName := b.buildAuthSessionCookieName(ctx, ingressClassParams, svcAndIngAnnotations) + authSessionTimeout, err := b.buildAuthSessionTimeout(ctx, ingressClassParams, svcAndIngAnnotations) if err != nil { return AuthConfig{}, err } - authIDPCognito, err := b.buildAuthIDPConfigCognito(ctx, svcAndIngAnnotations) + authIDPCognito, err := b.buildAuthIDPConfigCognito(ctx, ingressClassParams, svcAndIngAnnotations) if err != nil { return AuthConfig{}, err } - authIDPOIDC, err := b.buildAuthIDPConfigOIDC(ctx, svcAndIngAnnotations) + authIDPOIDC, err := b.buildAuthIDPConfigOIDC(ctx, ingressClassParams, svcAndIngAnnotations) if err != nil { return AuthConfig{}, err } @@ -78,9 +85,37 @@ func (b *defaultAuthConfigBuilder) Build(ctx context.Context, svcAndIngAnnotatio return authConfig, nil } -func (b *defaultAuthConfigBuilder) buildAuthType(_ context.Context, svcAndIngAnnotations map[string]string) (AuthType, error) { +func (b *defaultAuthConfigBuilder) validateIngressClassParamsAuthConfig(ingressClassParams *elbv2api.IngressClassParams) error { + if ingressClassParams == nil || ingressClassParams.Spec.AuthConfig == nil { + return nil + } + + // Verify that idpCognitoConfiguration exists when auth type is "cognito" + if string(ingressClassParams.Spec.AuthConfig.Type) == string(AuthTypeCognito) && ingressClassParams.Spec.AuthConfig.IDPConfigCognito == nil { + return errors.Errorf("idpCognitoConfiguration is required when authenticationConfiguration type is %s", string(AuthTypeCognito)) + } + + // Verify that idpOidcConfiguration exists when auth type is "oidc" + if string(ingressClassParams.Spec.AuthConfig.Type) == string(AuthTypeOIDC) && ingressClassParams.Spec.AuthConfig.IDPConfigOIDC == nil { + return errors.Errorf("idpOidcConfiguration is required when authenticationConfiguration type is %s", string(AuthTypeOIDC)) + } + + return nil +} + +// AuthConfig precedence rules: +// In the following functions, the AuthConfig in IngressClassParams takes precedence if specified. +// Otherwise, the Ingress annotations are used. + +func (b *defaultAuthConfigBuilder) buildAuthType(_ context.Context, ingressClassParams *elbv2api.IngressClassParams, svcAndIngAnnotations map[string]string) (AuthType, error) { rawAuthType := string(defaultAuthType) - _ = b.annotationParser.ParseStringAnnotation(annotations.IngressSuffixAuthType, &rawAuthType, svcAndIngAnnotations) + + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil { + rawAuthType = string(ingressClassParams.Spec.AuthConfig.Type) + } else { + _ = b.annotationParser.ParseStringAnnotation(annotations.IngressSuffixAuthType, &rawAuthType, svcAndIngAnnotations) + } + switch rawAuthType { case string(AuthTypeCognito): return AuthTypeCognito, nil @@ -93,8 +128,27 @@ func (b *defaultAuthConfigBuilder) buildAuthType(_ context.Context, svcAndIngAnn } } -func (b *defaultAuthConfigBuilder) buildAuthIDPConfigCognito(_ context.Context, svcAndIngAnnotations map[string]string) (*AuthIDPConfigCognito, error) { +func (b *defaultAuthConfigBuilder) buildAuthIDPConfigCognito(_ context.Context, ingressClassParams *elbv2api.IngressClassParams, svcAndIngAnnotations map[string]string) (*AuthIDPConfigCognito, error) { + // If using ingressClassParams authenticationConfiguration, only build if AuthType is "cognito" + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil && ingressClassParams.Spec.AuthConfig.Type != elbv2api.AuthTypeCognito { + return nil, nil + } + authIDP := AuthIDPConfigCognito{} + + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil && ingressClassParams.Spec.AuthConfig.IDPConfigCognito != nil { + config := ingressClassParams.Spec.AuthConfig.IDPConfigCognito + + authIDP = AuthIDPConfigCognito{ + UserPoolARN: config.UserPoolARN, + UserPoolClientID: config.UserPoolClientID, + UserPoolDomain: config.UserPoolDomain, + AuthenticationRequestExtraParams: config.AuthenticationRequestExtraParams, + } + + return &authIDP, nil + } + exists, err := b.annotationParser.ParseJSONAnnotation(annotations.IngressSuffixAuthIDPCognito, &authIDP, svcAndIngAnnotations) if err != nil { return nil, err @@ -105,7 +159,25 @@ func (b *defaultAuthConfigBuilder) buildAuthIDPConfigCognito(_ context.Context, return &authIDP, nil } -func (b *defaultAuthConfigBuilder) buildAuthIDPConfigOIDC(_ context.Context, svcAndIngAnnotations map[string]string) (*AuthIDPConfigOIDC, error) { +func (b *defaultAuthConfigBuilder) buildAuthIDPConfigOIDC(_ context.Context, ingressClassParams *elbv2api.IngressClassParams, svcAndIngAnnotations map[string]string) (*AuthIDPConfigOIDC, error) { + // When using ingressClassParams authenticationConfiguration, only build if AuthType is "oidc" + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil && ingressClassParams.Spec.AuthConfig.Type != elbv2api.AuthTypeOIDC { + return nil, nil + } + + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil && ingressClassParams.Spec.AuthConfig.IDPConfigOIDC != nil { + config := ingressClassParams.Spec.AuthConfig.IDPConfigOIDC + authIDP := AuthIDPConfigOIDC{ + Issuer: config.Issuer, + AuthorizationEndpoint: config.AuthorizationEndpoint, + TokenEndpoint: config.TokenEndpoint, + UserInfoEndpoint: config.UserInfoEndpoint, + SecretName: config.SecretName, + AuthenticationRequestExtraParams: config.AuthenticationRequestExtraParams, + } + return &authIDP, nil + } + authIDP := AuthIDPConfigOIDC{} exists, err := b.annotationParser.ParseJSONAnnotation(annotations.IngressSuffixAuthIDPOIDC, &authIDP, svcAndIngAnnotations) if err != nil { @@ -117,25 +189,57 @@ func (b *defaultAuthConfigBuilder) buildAuthIDPConfigOIDC(_ context.Context, svc return &authIDP, nil } -func (b *defaultAuthConfigBuilder) buildAuthOnUnauthenticatedRequest(_ context.Context, svcAndIngAnnotations map[string]string) string { +func (b *defaultAuthConfigBuilder) buildAuthOnUnauthenticatedRequest(_ context.Context, ingressClassParams *elbv2api.IngressClassParams, svcAndIngAnnotations map[string]string) string { + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil { + if ingressClassParams.Spec.AuthConfig.Type == elbv2api.AuthTypeNone || ingressClassParams.Spec.AuthConfig.OnUnauthenticatedRequest == "" { + return defaultAuthOnUnauthenticatedRequest + } + + return ingressClassParams.Spec.AuthConfig.OnUnauthenticatedRequest + } + rawOnUnauthenticatedRequest := defaultAuthOnUnauthenticatedRequest _ = b.annotationParser.ParseStringAnnotation(annotations.IngressSuffixAuthOnUnauthenticatedRequest, &rawOnUnauthenticatedRequest, svcAndIngAnnotations) return rawOnUnauthenticatedRequest } -func (b *defaultAuthConfigBuilder) buildAuthScope(_ context.Context, svcAndIngAnnotations map[string]string) string { +func (b *defaultAuthConfigBuilder) buildAuthScope(_ context.Context, ingressClassParams *elbv2api.IngressClassParams, svcAndIngAnnotations map[string]string) string { + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil { + if ingressClassParams.Spec.AuthConfig.Type == elbv2api.AuthTypeNone || ingressClassParams.Spec.AuthConfig.Scope == "" { + return defaultAuthScope + } + + return ingressClassParams.Spec.AuthConfig.Scope + } + rawAuthScope := defaultAuthScope _ = b.annotationParser.ParseStringAnnotation(annotations.IngressSuffixAuthScope, &rawAuthScope, svcAndIngAnnotations) return rawAuthScope } -func (b *defaultAuthConfigBuilder) buildAuthSessionCookieName(_ context.Context, svcAndIngAnnotations map[string]string) string { +func (b *defaultAuthConfigBuilder) buildAuthSessionCookieName(_ context.Context, ingressClassParams *elbv2api.IngressClassParams, svcAndIngAnnotations map[string]string) string { + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil { + if ingressClassParams.Spec.AuthConfig.Type == elbv2api.AuthTypeNone || ingressClassParams.Spec.AuthConfig.SessionCookieName == "" { + return defaultAuthSessionCookieName + } + + return ingressClassParams.Spec.AuthConfig.SessionCookieName + } + rawAuthSessionCookieName := defaultAuthSessionCookieName _ = b.annotationParser.ParseStringAnnotation(annotations.IngressSuffixAuthSessionCookie, &rawAuthSessionCookieName, svcAndIngAnnotations) return rawAuthSessionCookieName } -func (b *defaultAuthConfigBuilder) buildAuthSessionTimeout(_ context.Context, svcAndIngAnnotations map[string]string) (int64, error) { +func (b *defaultAuthConfigBuilder) buildAuthSessionTimeout(_ context.Context, ingressClassParams *elbv2api.IngressClassParams, svcAndIngAnnotations map[string]string) (int64, error) { + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil { + if ingressClassParams.Spec.AuthConfig.Type == elbv2api.AuthTypeNone || ingressClassParams.Spec.AuthConfig.SessionTimeout == nil { + return int64(defaultAuthSessionTimeout), nil + } + + return int64(*ingressClassParams.Spec.AuthConfig.SessionTimeout), nil + } + rawAuthSessionTimeout := int64(defaultAuthSessionTimeout) if _, err := b.annotationParser.ParseInt64Annotation(annotations.IngressSuffixAuthSessionTimeout, &rawAuthSessionTimeout, svcAndIngAnnotations); err != nil { return 0, err diff --git a/pkg/ingress/auth_config_builder_test.go b/pkg/ingress/auth_config_builder_test.go index 6cf3b04260..d68a602065 100644 --- a/pkg/ingress/auth_config_builder_test.go +++ b/pkg/ingress/auth_config_builder_test.go @@ -2,13 +2,17 @@ package ingress import ( "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" "github.com/stretchr/testify/assert" + elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1" "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations" - "testing" ) func Test_defaultAuthConfigBuilder_Build(t *testing.T) { type args struct { + ingressClassParams elbv2api.IngressClassParams svcAndIngAnnotations map[string]string } tests := []struct { @@ -179,14 +183,224 @@ func Test_defaultAuthConfigBuilder_Build(t *testing.T) { SessionTimeout: 86400, }, }, + { + name: "cognito authentication configuration via ingress class params", + args: args{ + ingressClassParams: elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + AuthConfig: &elbv2api.AuthConfig{ + Type: "cognito", + IDPConfigCognito: &elbv2api.AuthIDPConfigCognito{ + UserPoolARN: "arn:aws:cognito-idp:us-east-1:xxx:userpool/xxx", + UserPoolClientID: "client1234", + UserPoolDomain: "https://us-east-1xxx.auth.us-east-1.amazoncognito.com", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "aws.cognito.signin.user.admin email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: aws.Int64(1234), + }, + }, + }, + }, + want: AuthConfig{ + Type: AuthTypeCognito, + IDPConfigCognito: &AuthIDPConfigCognito{ + UserPoolARN: "arn:aws:cognito-idp:us-east-1:xxx:userpool/xxx", + UserPoolClientID: "client1234", + UserPoolDomain: "https://us-east-1xxx.auth.us-east-1.amazoncognito.com", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "aws.cognito.signin.user.admin email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: 1234, + }, + }, + { + name: "cognito authentication configuration via ingress class params - should take precendence over annotations", + args: args{ + ingressClassParams: elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + AuthConfig: &elbv2api.AuthConfig{ + Type: "cognito", + IDPConfigCognito: &elbv2api.AuthIDPConfigCognito{ + UserPoolARN: "arn:aws:cognito-idp:us-east-1:xxx:userpool/xxx", + UserPoolClientID: "client1234", + UserPoolDomain: "https://us-east-1xxx.auth.us-east-1.amazoncognito.com", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "aws.cognito.signin.user.admin email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: aws.Int64(1234), + }, + }, + }, + svcAndIngAnnotations: map[string]string{ + "alb.ingress.kubernetes.io/auth-type": "cognito", + "alb.ingress.kubernetes.io/auth-idp-cognito": `{"userPoolARN":"arn:aws:cognito-idp:us-west-2:xxx:userpool/xxx","userPoolClientID":"my-clientID","userPoolDomain":"my-domain","authenticationRequestExtraParams":{"key":"value"}}`, + "alb.ingress.kubernetes.io/auth-on-unauthenticated-request": "deny", + "alb.ingress.kubernetes.io/auth-scope": "email", + "alb.ingress.kubernetes.io/auth-session-cookie": "my-cookie", + "alb.ingress.kubernetes.io/auth-session-timeout": "86400", + }, + }, + want: AuthConfig{ + Type: AuthTypeCognito, + IDPConfigCognito: &AuthIDPConfigCognito{ + UserPoolARN: "arn:aws:cognito-idp:us-east-1:xxx:userpool/xxx", + UserPoolClientID: "client1234", + UserPoolDomain: "https://us-east-1xxx.auth.us-east-1.amazoncognito.com", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "aws.cognito.signin.user.admin email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: 1234, + }, + }, + { + name: "oidc authentication configuration via ingress class params", + args: args{ + ingressClassParams: elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + AuthConfig: &elbv2api.AuthConfig{ + Type: "oidc", + IDPConfigOIDC: &elbv2api.AuthIDPConfigOIDC{ + Issuer: "https://my-site.com", + AuthorizationEndpoint: "https://super-strong-auth.my-site.com", + TokenEndpoint: "https://token.my-site.com", + UserInfoEndpoint: "https://user.my-site.com", + SecretName: "top-secret", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: aws.Int64(1234), + }, + }, + }, + }, + want: AuthConfig{ + Type: AuthTypeOIDC, + IDPConfigOIDC: &AuthIDPConfigOIDC{ + Issuer: "https://my-site.com", + AuthorizationEndpoint: "https://super-strong-auth.my-site.com", + TokenEndpoint: "https://token.my-site.com", + UserInfoEndpoint: "https://user.my-site.com", + SecretName: "top-secret", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: 1234, + }, + }, + { + name: "oidc authentication configuration via ingress class params - should take precedence over annotations", + args: args{ + ingressClassParams: elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + AuthConfig: &elbv2api.AuthConfig{ + Type: "oidc", + IDPConfigOIDC: &elbv2api.AuthIDPConfigOIDC{ + Issuer: "https://my-site.com", + AuthorizationEndpoint: "https://super-strong-auth.my-site.com", + TokenEndpoint: "https://token.my-site.com", + UserInfoEndpoint: "https://user.my-site.com", + SecretName: "top-secret", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: aws.Int64(1234), + }, + }, + }, + svcAndIngAnnotations: map[string]string{ + "alb.ingress.kubernetes.io/auth-type": "oidc", + "alb.ingress.kubernetes.io/auth-idp-oidc": `{"Issuer":"https://example.com","AuthorizationEndpoint":"https://authorization.example.com","TokenEndpoint":"https://token.example.com","UserInfoEndpoint":"https://userinfo.example.com","SecretName":"my-k8s-secret","AuthenticationRequestExtraParams":{"key":"value"}}`, + "alb.ingress.kubernetes.io/auth-on-unauthenticated-request": "deny", + "alb.ingress.kubernetes.io/auth-scope": "email", + "alb.ingress.kubernetes.io/auth-session-cookie": "my-cookie", + "alb.ingress.kubernetes.io/auth-session-timeout": "86400", + }, + }, + want: AuthConfig{ + Type: AuthTypeOIDC, + IDPConfigOIDC: &AuthIDPConfigOIDC{ + Issuer: "https://my-site.com", + AuthorizationEndpoint: "https://super-strong-auth.my-site.com", + TokenEndpoint: "https://token.my-site.com", + UserInfoEndpoint: "https://user.my-site.com", + SecretName: "top-secret", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: 1234, + }, + }, + { + name: "authentication configuration set to 'none' in ingress class params, should take precedence over annotations", + args: args{ + ingressClassParams: elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + AuthConfig: &elbv2api.AuthConfig{ + Type: "none", + }, + }, + }, + svcAndIngAnnotations: map[string]string{ + "alb.ingress.kubernetes.io/auth-type": "oidc", + "alb.ingress.kubernetes.io/auth-idp-oidc": `{"Issuer":"https://example.com","AuthorizationEndpoint":"https://authorization.example.com","TokenEndpoint":"https://token.example.com","UserInfoEndpoint":"https://userinfo.example.com","SecretName":"my-k8s-secret","AuthenticationRequestExtraParams":{"key":"value"}}`, + "alb.ingress.kubernetes.io/auth-on-unauthenticated-request": "deny", + "alb.ingress.kubernetes.io/auth-scope": "email", + "alb.ingress.kubernetes.io/auth-session-cookie": "my-cookie", + "alb.ingress.kubernetes.io/auth-session-timeout": "86400", + }, + }, + want: AuthConfig{ + Type: AuthTypeNone, + IDPConfigCognito: nil, + IDPConfigOIDC: nil, + OnUnauthenticatedRequest: defaultAuthOnUnauthenticatedRequest, + Scope: defaultAuthScope, + SessionCookieName: defaultAuthSessionCookieName, + SessionTimeout: defaultAuthSessionTimeout, + }, + }, } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { annotationParser := annotations.NewSuffixAnnotationParser("alb.ingress.kubernetes.io") b := &defaultAuthConfigBuilder{ annotationParser: annotationParser, } - got, err := b.Build(context.Background(), tt.args.svcAndIngAnnotations) + got, err := b.Build(context.Background(), &tt.args.ingressClassParams, tt.args.svcAndIngAnnotations) if tt.wantErr != nil { assert.EqualError(t, err, tt.wantErr.Error()) } else { diff --git a/pkg/ingress/enhanced_backend_builder.go b/pkg/ingress/enhanced_backend_builder.go index 2bf87f61ef..8392ab60a7 100644 --- a/pkg/ingress/enhanced_backend_builder.go +++ b/pkg/ingress/enhanced_backend_builder.go @@ -12,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/sets" + elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1" "sigs.k8s.io/aws-load-balancer-controller/pkg/algorithm" "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations" "sigs.k8s.io/controller-runtime/pkg/client" @@ -72,7 +73,7 @@ func WithLoadAuthConfig(loadAuthConfig bool) EnhancedBackendBuildOption { // EnhancedBackendBuilder is capable of build EnhancedBackend for Ingress backend. type EnhancedBackendBuilder interface { - Build(ctx context.Context, ing *networking.Ingress, backend networking.IngressBackend, opts ...EnhancedBackendBuildOption) (EnhancedBackend, error) + Build(ctx context.Context, ing *networking.Ingress, backend networking.IngressBackend, ingressClassParams *elbv2api.IngressClassParams, opts ...EnhancedBackendBuildOption) (EnhancedBackend, error) } // NewDefaultEnhancedBackendBuilder constructs new defaultEnhancedBackendBuilder. @@ -102,7 +103,7 @@ type defaultEnhancedBackendBuilder struct { tolerateNonExistentBackendAction bool } -func (b *defaultEnhancedBackendBuilder) Build(ctx context.Context, ing *networking.Ingress, backend networking.IngressBackend, opts ...EnhancedBackendBuildOption) (EnhancedBackend, error) { +func (b *defaultEnhancedBackendBuilder) Build(ctx context.Context, ing *networking.Ingress, backend networking.IngressBackend, ingressClassParams *elbv2api.IngressClassParams, opts ...EnhancedBackendBuildOption) (EnhancedBackend, error) { buildOpts := EnhancedBackendBuildOptions{ LoadBackendServices: true, LoadAuthConfig: true, @@ -138,7 +139,7 @@ func (b *defaultEnhancedBackendBuilder) Build(ctx context.Context, ing *networki } if buildOpts.LoadAuthConfig { - authCfg, err = b.buildAuthConfig(ctx, action, ing.Namespace, ing.Annotations, buildOpts.BackendServices) + authCfg, err = b.buildAuthConfig(ctx, action, ing.Namespace, ing.Annotations, buildOpts.BackendServices, ingressClassParams) if err != nil { return EnhancedBackend{}, err } @@ -268,7 +269,7 @@ func (b *defaultEnhancedBackendBuilder) loadBackendServices(ctx context.Context, return nil } -func (b *defaultEnhancedBackendBuilder) buildAuthConfig(ctx context.Context, action Action, namespace string, ingAnnotation map[string]string, backendServices map[types.NamespacedName]*corev1.Service) (AuthConfig, error) { +func (b *defaultEnhancedBackendBuilder) buildAuthConfig(ctx context.Context, action Action, namespace string, ingAnnotation map[string]string, backendServices map[types.NamespacedName]*corev1.Service, ingressClassParams *elbv2api.IngressClassParams) (AuthConfig, error) { svcAndIngAnnotations := ingAnnotation // when forward to a single Service, the auth annotations on that Service will be merged in. if action.Type == ActionTypeForward && @@ -281,7 +282,7 @@ func (b *defaultEnhancedBackendBuilder) buildAuthConfig(ctx context.Context, act svcAndIngAnnotations = algorithm.MergeStringMap(svc.Annotations, svcAndIngAnnotations) } - return b.authConfigBuilder.Build(ctx, svcAndIngAnnotations) + return b.authConfigBuilder.Build(ctx, ingressClassParams, svcAndIngAnnotations) } // build503ResponseAction generates a 503 fixed response action when forward to a single non-existent Kubernetes Service. diff --git a/pkg/ingress/enhanced_backend_builder_test.go b/pkg/ingress/enhanced_backend_builder_test.go index 9d8530f74b..527eaf0f58 100644 --- a/pkg/ingress/enhanced_backend_builder_test.go +++ b/pkg/ingress/enhanced_backend_builder_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/aws/aws-sdk-go-v2/aws" awssdk "github.com/aws/aws-sdk-go-v2/aws" "github.com/google/go-cmp/cmp" "github.com/pkg/errors" @@ -15,6 +16,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" clientgoscheme "k8s.io/client-go/kubernetes/scheme" + elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1" "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations" "sigs.k8s.io/aws-load-balancer-controller/pkg/equality" testclient "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -35,6 +37,8 @@ func Test_defaultEnhancedBackendBuilder_Build(t *testing.T) { loadBackendServices bool loadAuthConfig bool backendServices map[types.NamespacedName]*corev1.Service + + ingressClassParams elbv2api.IngressClassParams } svc1 := &corev1.Service{ @@ -622,6 +626,94 @@ func Test_defaultEnhancedBackendBuilder_Build(t *testing.T) { }, wantErr: errors.New("missing required \"service\" field"), }, + { + name: "ingress class params based serviceBackend", + env: env{ + svcs: []*corev1.Service{svc1}, + }, + fields: fields{ + tolerateNonExistentBackendService: true, + tolerateNonExistentBackendAction: true, + }, + args: args{ + ing: &networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "awesome-ns", + Annotations: map[string]string{ + "alb.ingress.kubernetes.io/actions.fake-my-svc": `{"type":"forward","forwardConfig":{"targetGroups":[{"serviceName":"svc-1","servicePort":"http"}]}}`, + "alb.ingress.kubernetes.io/auth-type": "cognito", + "alb.ingress.kubernetes.io/auth-idp-cognito": "{\"userPoolARN\":\"arn:aws:cognito-idp:us-west-2:xxx:userpool/xxx\",\"userPoolClientID\":\"my-clientID\",\"userPoolDomain\":\"my-domain\"}", + }, + }, + }, + backend: networking.IngressBackend{ + Service: &networking.IngressServiceBackend{ + Name: "fake-my-svc", + Port: networking.ServiceBackendPort{ + Name: "use-annotation", + }, + }, + }, + loadBackendServices: true, + loadAuthConfig: true, + backendServices: map[types.NamespacedName]*corev1.Service{}, + ingressClassParams: elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + AuthConfig: &elbv2api.AuthConfig{ + Type: "oidc", + IDPConfigOIDC: &elbv2api.AuthIDPConfigOIDC{ + Issuer: "https://my-site.com", + AuthorizationEndpoint: "https://super-strong-auth.my-site.com", + TokenEndpoint: "https://token.my-site.com", + UserInfoEndpoint: "https://user.my-site.com", + SecretName: "top-secret", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: aws.Int64(1234), + }, + }, + }, + }, + want: EnhancedBackend{ + Action: Action{ + Type: ActionTypeForward, + ForwardConfig: &ForwardActionConfig{ + TargetGroups: []TargetGroupTuple{ + { + ServiceName: awssdk.String("svc-1"), + ServicePort: &portHTTP, + }, + }, + }, + }, + AuthConfig: AuthConfig{ + Type: AuthTypeOIDC, + IDPConfigCognito: nil, + IDPConfigOIDC: &AuthIDPConfigOIDC{ + Issuer: "https://my-site.com", + AuthorizationEndpoint: "https://super-strong-auth.my-site.com", + TokenEndpoint: "https://token.my-site.com", + UserInfoEndpoint: "https://user.my-site.com", + SecretName: "top-secret", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: 1234, + }, + }, + wantBackendServices: map[types.NamespacedName]*corev1.Service{ + {Namespace: "awesome-ns", Name: "svc-1"}: svc1, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -643,7 +735,7 @@ func Test_defaultEnhancedBackendBuilder_Build(t *testing.T) { tolerateNonExistentBackendAction: tt.fields.tolerateNonExistentBackendAction, } - got, err := b.Build(context.Background(), tt.args.ing, tt.args.backend, + got, err := b.Build(context.Background(), tt.args.ing, tt.args.backend, &tt.args.ingressClassParams, WithLoadBackendServices(tt.args.loadBackendServices, tt.args.backendServices), WithLoadAuthConfig(tt.args.loadAuthConfig)) if tt.wantErr != nil { @@ -1624,10 +1716,11 @@ func Test_defaultEnhancedBackendBuilder_loadBackendServices(t *testing.T) { func Test_defaultEnhancedBackendBuilder_buildAuthConfig(t *testing.T) { port80 := intstr.FromInt(80) type args struct { - action Action - namespace string - ingAnnotation map[string]string - backendServices map[types.NamespacedName]*corev1.Service + action Action + namespace string + ingAnnotation map[string]string + backendServices map[types.NamespacedName]*corev1.Service + ingressClassParams *elbv2api.IngressClassParams } tests := []struct { name string @@ -1752,6 +1845,58 @@ func Test_defaultEnhancedBackendBuilder_buildAuthConfig(t *testing.T) { SessionTimeout: 604800, }, }, + { + name: "build authConfig type cognito from IngressClassParams, ignore Ingress annotations", + args: args{ + action: Action{ + Type: ActionTypeFixedResponse, + FixedResponseConfig: &FixedResponseActionConfig{ + ContentType: awssdk.String("text/plain"), + StatusCode: "404", + }, + }, + namespace: "awesome-ns", + ingAnnotation: map[string]string{ + "alb.ingress.kubernetes.io/auth-type": "cognito", + "alb.ingress.kubernetes.io/auth-idp-cognito": "{\"userPoolARN\":\"arn:aws:cognito-idp:us-west-2:xxx:userpool/xxx\",\"userPoolClientID\":\"my-clientID\",\"userPoolDomain\":\"my-domain\"}", + }, + backendServices: map[types.NamespacedName]*corev1.Service{}, + ingressClassParams: &elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + AuthConfig: &elbv2api.AuthConfig{ + Type: "cognito", + IDPConfigCognito: &elbv2api.AuthIDPConfigCognito{ + UserPoolARN: "arn:aws:cognito-idp:us-east-1:xxx:userpool/xxx", + UserPoolClientID: "client1234", + UserPoolDomain: "https://us-east-1xxx.auth.us-east-1.amazoncognito.com", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "aws.cognito.signin.user.admin email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: aws.Int64(1234), + }, + }, + }, + }, + want: AuthConfig{ + Type: AuthTypeCognito, + IDPConfigCognito: &AuthIDPConfigCognito{ + UserPoolARN: "arn:aws:cognito-idp:us-east-1:xxx:userpool/xxx", + UserPoolClientID: "client1234", + UserPoolDomain: "https://us-east-1xxx.auth.us-east-1.amazoncognito.com", + AuthenticationRequestExtraParams: map[string]string{ + "key": "value", + }, + }, + OnUnauthenticatedRequest: "deny", + Scope: "aws.cognito.signin.user.admin email phone", + SessionCookieName: "my-session-cookie", + SessionTimeout: 1234, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1761,7 +1906,7 @@ func Test_defaultEnhancedBackendBuilder_buildAuthConfig(t *testing.T) { annotationParser: annotationParser, authConfigBuilder: authConfigBuilder, } - got, err := b.buildAuthConfig(context.Background(), tt.args.action, tt.args.namespace, tt.args.ingAnnotation, tt.args.backendServices) + got, err := b.buildAuthConfig(context.Background(), tt.args.action, tt.args.namespace, tt.args.ingAnnotation, tt.args.backendServices, tt.args.ingressClassParams) if tt.wantErr != nil { assert.EqualError(t, err, tt.wantErr.Error()) } else { diff --git a/pkg/ingress/model_build_actions.go b/pkg/ingress/model_build_actions.go index 1562290744..7bcbbee4a1 100644 --- a/pkg/ingress/model_build_actions.go +++ b/pkg/ingress/model_build_actions.go @@ -3,20 +3,29 @@ package ingress import ( "context" "fmt" + "strings" + "unicode" + awssdk "github.com/aws/aws-sdk-go-v2/aws" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1" "sigs.k8s.io/aws-load-balancer-controller/pkg/model/core" elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2" - "strings" - "unicode" ) -func (t *defaultModelBuildTask) buildActions(ctx context.Context, protocol elbv2model.Protocol, ing ClassifiedIngress, backend EnhancedBackend) ([]elbv2model.Action, error) { +func (t *defaultModelBuildTask) buildActions(ctx context.Context, protocol elbv2model.Protocol, ing ClassifiedIngress, backend EnhancedBackend, ingClassParams *elbv2api.IngressClassParams) ([]elbv2model.Action, error) { var actions []elbv2model.Action if protocol == elbv2model.ProtocolHTTPS { - authAction, err := t.buildAuthAction(ctx, ing.Ing.Namespace, backend) + var namespace string + if ingClassParams != nil && ingClassParams.Spec.AuthConfig != nil { + namespace = metav1.NamespaceDefault + } else { + namespace = ing.Ing.Namespace + } + authAction, err := t.buildAuthAction(ctx, namespace, backend) if err != nil { return nil, err } diff --git a/pkg/ingress/model_build_listener.go b/pkg/ingress/model_build_listener.go index 264c45b10e..4c3d6f1cd9 100644 --- a/pkg/ingress/model_build_listener.go +++ b/pkg/ingress/model_build_listener.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "fmt" - elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" "net" "strings" + elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" + elbv2sdk "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2" "k8s.io/utils/strings/slices" @@ -89,12 +90,13 @@ func (t *defaultModelBuildTask) buildListenerDefaultActions(ctx context.Context, } ing := ingsWithDefaultBackend[0] enhancedBackend, err := t.enhancedBackendBuilder.Build(ctx, ing.Ing, *ing.Ing.Spec.DefaultBackend, + ing.IngClassConfig.IngClassParams, WithLoadBackendServices(true, t.backendServices), WithLoadAuthConfig(true)) if err != nil { return nil, err } - return t.buildActions(ctx, protocol, ing, enhancedBackend) + return t.buildActions(ctx, protocol, ing, enhancedBackend, ing.IngClassConfig.IngClassParams) } func (t *defaultModelBuildTask) buildListenerTags(_ context.Context, ingList []ClassifiedIngress) (map[string]string, error) { diff --git a/pkg/ingress/model_build_listener_rules.go b/pkg/ingress/model_build_listener_rules.go index dcdc0e1011..0899e58f15 100644 --- a/pkg/ingress/model_build_listener_rules.go +++ b/pkg/ingress/model_build_listener_rules.go @@ -31,6 +31,7 @@ func (t *defaultModelBuildTask) buildListenerRules(ctx context.Context, lsARN co } for _, path := range paths { enhancedBackend, err := t.enhancedBackendBuilder.Build(ctx, ing.Ing, path.Backend, + ing.IngClassConfig.IngClassParams, WithLoadBackendServices(true, t.backendServices), WithLoadAuthConfig(true)) if err != nil { @@ -40,7 +41,7 @@ func (t *defaultModelBuildTask) buildListenerRules(ctx context.Context, lsARN co if err != nil { return errors.Wrapf(err, "ingress: %v", k8s.NamespacedName(ing.Ing)) } - actions, err := t.buildActions(ctx, protocol, ing, enhancedBackend) + actions, err := t.buildActions(ctx, protocol, ing, enhancedBackend, ing.IngClassConfig.IngClassParams) if err != nil { return errors.Wrapf(err, "ingress: %v", k8s.NamespacedName(ing.Ing)) } diff --git a/pkg/ingress/reference_indexer.go b/pkg/ingress/reference_indexer.go index 9cfb4ca73b..2cdbf62cae 100644 --- a/pkg/ingress/reference_indexer.go +++ b/pkg/ingress/reference_indexer.go @@ -12,29 +12,33 @@ import ( ) const ( - // IndexKeyServiceRefName is index key for services referenced by Ingress. + // IndexKeyServiceRefName is index key for services referenced by Ingress IndexKeyServiceRefName = "ingress.serviceRef.name" - // IndexKeySecretRefName is index key for secrets referenced by Ingress or Service. + // IndexKeySecretRefName is index key for secrets referenced by Ingress or Service IndexKeySecretRefName = "ingress.secretRef.name" - // IndexKeyIngressClassRefName is index key for ingressClass referenced by Ingress. + // IndexKeyIngressClassRefName is index key for ingressClass referenced by Ingress IndexKeyIngressClassRefName = "ingress.ingressClassRef.name" - // IndexKeyIngressClassParamsRefName is index key for ingressClassParams referenced by IngressClass. + // IndexKeyIngressClassParamsRefName is index key for ingressClassParams referenced by IngressClass IndexKeyIngressClassParamsRefName = "ingressClass.ingressClassParamsRef.name" + // IndexKeyIngressClassParamsSecretRefName is index key for secrets referenced by IngressClassParams + IndexKeyIngressClassParamsSecretRefName = "ingressClass.ingressClassParamsRef.secret" ) -// ReferenceIndexer has the ability to index Ingresses with referenced objects. +// ReferenceIndexer has the ability to index Ingresses with referenced objects type ReferenceIndexer interface { - // BuildServiceRefIndexes returns the name of related Service objects. + // BuildServiceRefIndexes returns the name of related Service objects BuildServiceRefIndexes(ctx context.Context, ing *networking.Ingress) []string - // BuildSecretRefIndexes returns the name of related Secret objects. - BuildSecretRefIndexes(ctx context.Context, ingOrSvc client.Object) []string - // BuildIngressClassRefIndexes returns the name of related IngressClass objects. + // BuildSecretRefIndexes returns the name of related Secret objects + BuildSecretRefIndexes(ctx context.Context, ingressClassParams *elbv2api.IngressClassParams, ingOrSvc client.Object) []string + // BuildIngressClassRefIndexes returns the name of related IngressClass objects BuildIngressClassRefIndexes(ctx context.Context, ing *networking.Ingress) []string - // BuildIngressClassParamsRefIndexes returns the name of related IngressClassParams objects. + // BuildIngressClassParamsRefIndexes returns the name of related IngressClassParams objects BuildIngressClassParamsRefIndexes(ctx context.Context, ingClass *networking.IngressClass) []string + // BuildIngressClassParamsSecretIndexes returns the secret names that the ingress class param references + BuildIngressClassParamsSecretIndexes(ctx context.Context, ingressClassParams *elbv2api.IngressClassParams) []string } -// NewDefaultReferenceIndexer constructs new defaultReferenceIndexer. +// NewDefaultReferenceIndexer constructs new defaultReferenceIndexer func NewDefaultReferenceIndexer(enhancedBackendBuilder EnhancedBackendBuilder, authConfigBuilder AuthConfigBuilder, logger logr.Logger) *defaultReferenceIndexer { return &defaultReferenceIndexer{ enhancedBackendBuilder: enhancedBackendBuilder, @@ -45,7 +49,7 @@ func NewDefaultReferenceIndexer(enhancedBackendBuilder EnhancedBackendBuilder, a var _ ReferenceIndexer = &defaultReferenceIndexer{} -// default implementation for ReferenceIndexer +// Default implementation for ReferenceIndexer type defaultReferenceIndexer struct { enhancedBackendBuilder EnhancedBackendBuilder authConfigBuilder AuthConfigBuilder @@ -68,7 +72,7 @@ func (i *defaultReferenceIndexer) BuildServiceRefIndexes(ctx context.Context, in serviceNames := sets.NewString() for _, backend := range backends { - enhancedBackend, err := i.enhancedBackendBuilder.Build(ctx, ing, backend, + enhancedBackend, err := i.enhancedBackendBuilder.Build(ctx, ing, backend, nil, WithLoadBackendServices(false, nil), WithLoadAuthConfig(false), ) @@ -83,8 +87,14 @@ func (i *defaultReferenceIndexer) BuildServiceRefIndexes(ctx context.Context, in return serviceNames.List() } -func (i *defaultReferenceIndexer) BuildSecretRefIndexes(ctx context.Context, ingOrSvc client.Object) []string { - authCfg, err := i.authConfigBuilder.Build(ctx, ingOrSvc.GetAnnotations()) +func (i *defaultReferenceIndexer) BuildSecretRefIndexes(ctx context.Context, ingressClassParams *elbv2api.IngressClassParams, ingOrSvc client.Object) []string { + // If AuthConfig exists in Ingress class params, extract the secret from it + if ingressClassParams != nil && ingressClassParams.Spec.AuthConfig != nil { + return extractSecretNamesFromIngressClassParams(ingressClassParams) + } + + // Otherwise, build the authConfig and extract the secret from the annotations + authCfg, err := i.authConfigBuilder.Build(ctx, nil, ingOrSvc.GetAnnotations()) if err != nil { i.logger.Error(err, "failed to build Ingress indexes", "indexKey", IndexKeySecretRefName) @@ -115,6 +125,16 @@ func (i *defaultReferenceIndexer) BuildIngressClassParamsRefIndexes(_ context.Co return []string{ingClassParamsName} } +func (i *defaultReferenceIndexer) BuildIngressClassParamsSecretIndexes(_ context.Context, ingressClassParams *elbv2api.IngressClassParams) []string { + if ingressClassParams == nil { + return nil + } + if ingressClassParams.Spec.AuthConfig != nil && ingressClassParams.Spec.AuthConfig.IDPConfigOIDC != nil && ingressClassParams.Spec.AuthConfig.IDPConfigOIDC.SecretName != "" { + return []string{ingressClassParams.Spec.AuthConfig.IDPConfigOIDC.SecretName} + } + return nil +} + func extractServiceNamesFromAction(action Action) []string { if action.Type != ActionTypeForward || action.ForwardConfig == nil { return nil @@ -134,6 +154,13 @@ func extractServiceNamesFromTargetGroupTuple(tgt TargetGroupTuple) []string { return []string{*tgt.ServiceName} } +func extractSecretNamesFromIngressClassParams(ingressClassParams *elbv2api.IngressClassParams) []string { + if ingressClassParams.Spec.AuthConfig.IDPConfigOIDC == nil { + return nil + } + return []string{ingressClassParams.Spec.AuthConfig.IDPConfigOIDC.SecretName} +} + func extractSecretNamesFromAuthConfig(authCfg AuthConfig) []string { if authCfg.IDPConfigOIDC == nil { return nil diff --git a/pkg/ingress/reference_indexer_test.go b/pkg/ingress/reference_indexer_test.go index 0c463c5ae0..6132e27efd 100644 --- a/pkg/ingress/reference_indexer_test.go +++ b/pkg/ingress/reference_indexer_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" networking "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + elbv2api "sigs.k8s.io/aws-load-balancer-controller/apis/elbv2/v1beta1" "sigs.k8s.io/aws-load-balancer-controller/pkg/annotations" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" @@ -328,7 +329,8 @@ func Test_defaultReferenceIndexer_BuildServiceRefIndexes(t *testing.T) { func Test_defaultReferenceIndexer_BuildSecretRefIndexes(t *testing.T) { type args struct { - ingOrSvc client.Object + ingOrSvc client.Object + ingressClassParams *elbv2api.IngressClassParams } tests := []struct { name string @@ -360,6 +362,26 @@ func Test_defaultReferenceIndexer_BuildSecretRefIndexes(t *testing.T) { }, want: nil, }, + { + name: "ingress class params with auth config type oidc", + args: args{ + ingressClassParams: &elbv2api.IngressClassParams{ + Spec: elbv2api.IngressClassParamsSpec{ + AuthConfig: &elbv2api.AuthConfig{ + Type: "oidc", + IDPConfigOIDC: &elbv2api.AuthIDPConfigOIDC{ + Issuer: "https://my-site.com", + AuthorizationEndpoint: "https://super-strong-auth.my-site.com", + TokenEndpoint: "https://token.my-site.com", + UserInfoEndpoint: "https://user.my-site.com", + SecretName: "top-secret", + }, + }, + }, + }, + }, + want: []string{"top-secret"}, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -371,7 +393,7 @@ func Test_defaultReferenceIndexer_BuildSecretRefIndexes(t *testing.T) { authConfigBuilder: authConfigBuilder, logger: logr.New(&log.NullLogSink{}), } - got := i.BuildSecretRefIndexes(context.Background(), tt.args.ingOrSvc) + got := i.BuildSecretRefIndexes(context.Background(), tt.args.ingressClassParams, tt.args.ingOrSvc) assert.Equal(t, tt.want, got) }) }