Repository: IBM/ibm-iam-operator
Pull request #1009:  [FORD SAML LOOP SC2] - Update containers.go to add MASTER_PATH
Author: login: rashmi43
Labels: name: approved
name: size/L
Created: 2025-04-02T10:26:14Z | Merged: 2025-04-28T11:44:00Z

Create MASTER_PATH for platform-auth-service and initialise it with /idauth

This might need changes when custom hostname is updated


Diff:
diff --git a/controllers/operator/configmap.go b/controllers/operator/configmap.go
index 3717a5026..830a20d25 100644
--- a/controllers/operator/configmap.go
+++ b/controllers/operator/configmap.go
@@ -45,6 +45,7 @@ import (
 	"k8s.io/apimachinery/pkg/types"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 	"k8s.io/client-go/discovery"
+	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/config"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -339,6 +340,18 @@ func (r *AuthenticationReconciler) handleConfigMap(instance *operatorv1alpha1.Au
 					currentConfigMap.Data["LDAP_CTX_POOL_PREFERREDSIZE"] = newConfigMap.Data["LDAP_CTX_POOL_PREFERREDSIZE"]
 					cmUpdateRequired = true
 				}
+				// Indicates an upgrade from a previous
+				if _, keyExists := currentConfigMap.Data["MASTER_PATH"]; !keyExists {
+					req := ctrl.Request{NamespacedName: types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}}
+					var path string
+					path, err = r.getMasterPath(context.Background(), req)
+					if err != nil {
+						reqLogger.Error(err, "Failed to determine if a SAML connection already exists")
+						return
+					}
+					currentConfigMap.Data["MASTER_PATH"] = path
+					cmUpdateRequired = true
+				}
 				_, keyExists := currentConfigMap.Data["IS_OPENSHIFT_ENV"]
 				if keyExists {
 					reqLogger.Info("Current configmap", "Current Value", currentConfigMap.Data["IS_OPENSHIFT_ENV"])
@@ -517,6 +530,10 @@ func (r *AuthenticationReconciler) authIdpConfigMap(instance *operatorv1alpha1.A
 	if isPublicCloud {
 		roksUserPrefix = "IAM#"
 	}
+
+	// Set the path for SAML connections
+	masterPath := "/idauth"
+
 	newConfigMap := &corev1.ConfigMap{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      "platform-auth-idp",
@@ -532,6 +549,7 @@ func (r *AuthenticationReconciler) authIdpConfigMap(instance *operatorv1alpha1.A
 			"IDENTITY_PROVIDER_URL":              "https://platform-identity-provider:4300",
 			"IDENTITY_MGMT_URL":                  "https://platform-identity-management:4500",
 			"MASTER_HOST":                        clusterAddress,
+			"MASTER_PATH":                        masterPath,
 			"NODE_ENV":                           "production",
 			"AUDIT_ENABLED_IDPROVIDER":           "false",
 			"AUDIT_ENABLED_IDMGMT":               "false",
@@ -1000,3 +1018,22 @@ func readROKSURL(instance *operatorv1alpha1.Authentication) (string, error) {
 	}
 	return issuer, nil
 }
+
+func (r *AuthenticationReconciler) getMasterPath(ctx context.Context, req ctrl.Request) (path string, err error) {
+	p, err := r.getPostgresDB(ctx, req)
+	if err != nil {
+		return
+	}
+	var has bool
+	if err = p.Connect(ctx); err != nil {
+		return
+	}
+	defer p.Disconnect(ctx)
+
+	if has, err = p.HasSAML(ctx); err != nil {
+		return
+	} else if has {
+		return "", err
+	}
+	return "/idauth", nil
+}
diff --git a/controllers/operator/containers.go b/controllers/operator/containers.go
index 02814364b..f1c5fbbb1 100644
--- a/controllers/operator/containers.go
+++ b/controllers/operator/containers.go
@@ -277,7 +277,7 @@ func buildAuthServiceContainer(instance *operatorv1alpha1.Authentication, authSe
 		},
 	}
 
-	idpEnvVarList := []string{"NODE_ENV", "MASTER_HOST", "IDENTITY_PROVIDER_URL", "HTTP_ONLY", "SESSION_TIMEOUT", "LDAP_RECURSIVE_SEARCH", "LDAP_ATTR_CACHE_SIZE", "LDAP_ATTR_CACHE_TIMEOUT", "LDAP_ATTR_CACHE_ENABLED", "LDAP_ATTR_CACHE_SIZELIMIT",
+	idpEnvVarList := []string{"NODE_ENV", "MASTER_PATH", "MASTER_HOST", "IDENTITY_PROVIDER_URL", "HTTP_ONLY", "SESSION_TIMEOUT", "LDAP_RECURSIVE_SEARCH", "LDAP_ATTR_CACHE_SIZE", "LDAP_ATTR_CACHE_TIMEOUT", "LDAP_ATTR_CACHE_ENABLED", "LDAP_ATTR_CACHE_SIZELIMIT",
 		"LDAP_SEARCH_CACHE_SIZE", "LDAP_SEARCH_CACHE_TIMEOUT", "LDAP_CTX_POOL_INITSIZE", "LDAP_CTX_POOL_MAXSIZE", "LDAP_CTX_POOL_TIMEOUT", "LDAP_CTX_POOL_WAITTIME", "LDAP_CTX_POOL_PREFERREDSIZE", "IDENTITY_PROVIDER_URL", "IDENTITY_MGMT_URL", "LDAP_SEARCH_CACHE_ENABLED", "LDAP_SEARCH_CACHE_SIZELIMIT", "IDTOKEN_LIFETIME", "IBMID_CLIENT_ID", "IBMID_CLIENT_ISSUER",
 		"SAML_NAMEID_FORMAT", "FIPS_ENABLED", "LOGJAM_DHKEYSIZE_2048_BITS_ENABLED", "LOG_LEVEL_AUTHSVC", "LIBERTY_DEBUG_ENABLED", "NONCE_ENABLED", "CLAIMS_SUPPORTED", "CLAIMS_MAP", "SCOPE_CLAIM", "OIDC_ISSUER_URL",
 		"DB_CONNECT_TIMEOUT", "DB_IDLE_TIMEOUT", "DB_CONNECT_MAX_RETRIES", "DB_POOL_MIN_SIZE", "DB_POOL_MAX_SIZE", "DB_SSL_MODE"}
@@ -601,7 +601,7 @@ func buildIdentityProviderContainer(instance *operatorv1alpha1.Authentication, i
 		},
 	}
 
-	idpEnvVarList := []string{"NODE_ENV", "LOG_LEVEL_IDPROVIDER", "LOG_LEVEL_MW", "PROVIDER_ISSUER_URL", "MASTER_HOST",
+	idpEnvVarList := []string{"NODE_ENV", "LOG_LEVEL_IDPROVIDER", "LOG_LEVEL_MW", "PROVIDER_ISSUER_URL", "MASTER_HOST", "MASTER_PATH",
 		"PREFERRED_LOGIN", "IDTOKEN_LIFETIME", "SAAS_CLIENT_REDIRECT_URL", "IBM_CLOUD_SAAS", "ROKS_ENABLED", "ROKS_URL",
 		"ROKS_USER_PREFIX", "OS_TOKEN_LENGTH", "LIBERTY_TOKEN_LENGTH", "IDENTITY_PROVIDER_URL", "BASE_AUTH_URL",
 		"BASE_OIDC_URL", "SCOPE_CLAIM", "OIDC_ISSUER_URL", "HTTP_ONLY", "IGNORE_LDAP_FILTERS_VALIDATION",
diff --git a/controllers/operator/ingress.go b/controllers/operator/ingress.go
index b81e2d3cc..e5db79cff 100644
--- a/controllers/operator/ingress.go
+++ b/controllers/operator/ingress.go
@@ -34,6 +34,8 @@ var ingressList []string = []string{
 	"id-mgmt",
 	"idmgmt-v2-api",
 	"platform-auth",
+	"platform-id-auth-block",
+	"platform-id-auth",
 	"platform-id-provider",
 	"platform-login",
 	"platform-oidc-block",
@@ -94,6 +96,8 @@ func (r *AuthenticationReconciler) handleIngress(instance *operatorv1alpha1.Auth
 		idMgmtIngress,
 		idmgmtV2ApiIngress,
 		platformAuthIngress,
+		platformIdAuthBlockIngress,
+		platformIdAuthIngress,
 		platformIdProviderIngress,
 		platformLoginIngress,
 		platformOidcBlockIngress,
@@ -345,6 +349,112 @@ func platformAuthIngress(instance *operatorv1alpha1.Authentication, scheme *runt
 
 }
 
+func platformIdAuthBlockIngress(instance *operatorv1alpha1.Authentication, scheme *runtime.Scheme) *netv1.Ingress {
+	pathType := netv1.PathType("ImplementationSpecific")
+	reqLogger := log.WithValues("Instance.Namespace", instance.Namespace, "Instance.Name", instance.Name)
+	newIngress := &netv1.Ingress{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "platform-id-auth-block",
+			Namespace: instance.Namespace,
+			Labels:    map[string]string{"app": "platform-auth-service"},
+			Annotations: map[string]string{
+				"kubernetes.io/ingress.class":              "ibm-icp-management",
+				"icp.management.ibm.com/location-modifier": "=",
+				"icp.management.ibm.com/configuration-snippet": `
+					add_header 'X-XSS-Protection' '1' always;
+					`,
+			},
+		},
+		Spec: netv1.IngressSpec{
+			Rules: []netv1.IngressRule{
+				{
+					IngressRuleValue: netv1.IngressRuleValue{
+						HTTP: &netv1.HTTPIngressRuleValue{
+							Paths: []netv1.HTTPIngressPath{
+								{
+									Path:     "/idauth/oidc/endpoint",
+									PathType: &pathType,
+									Backend: netv1.IngressBackend{
+										Service: &netv1.IngressServiceBackend{
+											Name: "default-http-backend",
+											Port: netv1.ServiceBackendPort{
+												Number: 80,
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	// Set Authentication instance as the owner and controller of the Ingress
+	err := controllerutil.SetControllerReference(instance, newIngress, scheme)
+	if err != nil {
+		reqLogger.Error(err, "Failed to set owner for Ingress")
+		return nil
+	}
+	return newIngress
+
+}
+
+func platformIdAuthIngress(instance *operatorv1alpha1.Authentication, scheme *runtime.Scheme) *netv1.Ingress {
+	pathType := netv1.PathType("ImplementationSpecific")
+	reqLogger := log.WithValues("Instance.Namespace", instance.Namespace, "Instance.Name", instance.Name)
+	newIngress := &netv1.Ingress{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "platform-id-auth",
+			Namespace: instance.Namespace,
+			Labels:    map[string]string{"app": "platform-auth-service"},
+			Annotations: map[string]string{
+				"kubernetes.io/ingress.class":            "ibm-icp-management",
+				"icp.management.ibm.com/secure-backends": "true",
+				"icp.management.ibm.com/rewrite-target":  "/",
+				"icp.management.ibm.com/configuration-snippet": `
+					add_header 'X-Frame-Options' 'SAMEORIGIN' always;
+					add_header 'X-Content-Type-Options' 'nosniff';
+					`,
+			},
+		},
+		Spec: netv1.IngressSpec{
+			Rules: []netv1.IngressRule{
+				{
+					IngressRuleValue: netv1.IngressRuleValue{
+						HTTP: &netv1.HTTPIngressRuleValue{
+							Paths: []netv1.HTTPIngressPath{
+								{
+									Path:     "/idauth",
+									PathType: &pathType,
+									Backend: netv1.IngressBackend{
+										Service: &netv1.IngressServiceBackend{
+											Name: "platform-auth-service",
+											Port: netv1.ServiceBackendPort{
+												Number: 9443,
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	// Set Authentication instance as the owner and controller of the Ingress
+	err := controllerutil.SetControllerReference(instance, newIngress, scheme)
+	if err != nil {
+		reqLogger.Error(err, "Failed to set owner for Ingress")
+		return nil
+	}
+	return newIngress
+
+}
+
 func platformIdProviderIngress(instance *operatorv1alpha1.Authentication, scheme *runtime.Scheme) *netv1.Ingress {
 	pathType := netv1.PathType("ImplementationSpecific")
 	reqLogger := log.WithValues("Instance.Namespace", instance.Namespace, "Instance.Name", instance.Name)
diff --git a/controllers/operator/resourcestatus.go b/controllers/operator/resourcestatus.go
index b782b2610..f179af198 100644
--- a/controllers/operator/resourcestatus.go
+++ b/controllers/operator/resourcestatus.go
@@ -230,6 +230,7 @@ func (r *AuthenticationReconciler) getCurrentServiceStatus(ctx context.Context,
 		names: []string{
 			"id-mgmt",
 			"platform-auth",
+			"platform-id-auth",
 			"platform-id-provider",
 			"platform-login",
 			"platform-oidc",
diff --git a/controllers/operator/routes.go b/controllers/operator/routes.go
index d5c793789..18bb2cf88 100644
--- a/controllers/operator/routes.go
+++ b/controllers/operator/routes.go
@@ -184,6 +184,18 @@ func (r *AuthenticationReconciler) handleRoutes(ctx context.Context, instance *o
 			DestinationCAcert: platformIdentityProviderCert,
 			ServiceName:       PlatformIdentityProviderServiceName,
 		},
+		"platform-id-auth": {
+			Annotations: map[string]string{
+				"haproxy.router.openshift.io/balance":        "source",
+				"haproxy.router.openshift.io/rewrite-target": "/",
+			},
+			Name:              "platform-id-auth",
+			RouteHost:         routeHost,
+			RoutePath:         "/idauth",
+			RoutePort:         9443,
+			DestinationCAcert: platformAuthCert,
+			ServiceName:       PlatformAuthServiceName,
+		},
 		"platform-id-provider": {
 			Annotations: map[string]string{
 				"haproxy.router.openshift.io/rewrite-target": "/",
@@ -265,29 +277,6 @@ func (r *AuthenticationReconciler) handleRoutes(ctx context.Context, instance *o
 	return
 }
 
-func (r *AuthenticationReconciler) removeIdauth(ctx context.Context, instance *operatorv1alpha1.Authentication) (err error) {
-	namespace := instance.Namespace
-	reqLogger := log.WithValues("func", "ReconcileRoute", "namespace", namespace)
-	reqLogger.Info("Determined platform-id-auth Route should not exist; removing if present")
-	observedRoute := &routev1.Route{}
-	err = r.Get(ctx, types.NamespacedName{Name: "platform-id-auth", Namespace: namespace}, observedRoute)
-	if errors.IsNotFound(err) {
-		return nil
-	} else if err != nil {
-		reqLogger.Error(err, "Failed to get existing platform-id-auth route for reconciliation")
-		return
-	}
-	err = r.Delete(ctx, observedRoute)
-	if errors.IsNotFound(err) {
-		return nil
-	} else if err != nil {
-		reqLogger.Error(err, "Failed to delete platform-id-auth Route")
-		return
-	}
-	reqLogger.Info("Successfully deleted platform-id-auth Route")
-	return
-}
-
 func (r *AuthenticationReconciler) reconcileRoute(ctx context.Context, instance *operatorv1alpha1.Authentication, fields *reconcileRouteFields, needToRequeue *bool) (err error) {
 
 	namespace := instance.Namespace
@@ -295,13 +284,6 @@ func (r *AuthenticationReconciler) reconcileRoute(ctx context.Context, instance
 
 	reqLogger.Info("Reconciling route", "annotations", fields.Annotations, "routeHost", fields.RouteHost, "routePath", fields.RoutePath)
 
-	err = r.removeIdauth(ctx, instance)
-
-	if err != nil {
-		reqLogger.Error(err, "Error deleting platform-id-auth Route")
-		return
-	}
-
 	calculatedRoute, err := r.newRoute(instance, fields)
 	if err != nil {
 		reqLogger.Error(err, "Error creating desired route for reconcilition")
diff --git a/database/connectors/dbconn.go b/database/connectors/dbconn.go
index 0923129b9..3137564ee 100644
--- a/database/connectors/dbconn.go
+++ b/database/connectors/dbconn.go
@@ -17,6 +17,7 @@ package connectors
 
 import (
 	"context"
+	"errors"
 	"fmt"
 
 	"github.com/jackc/pgx/v5"
@@ -128,6 +129,20 @@ func (p *PostgresDB) HasSchemas(ctx context.Context) (bool, error) {
 
 	return true, nil
 }
+
+func (p *PostgresDB) HasSAML(ctx context.Context) (has bool, err error) {
+	query := "SELECT 1 FROM platformdb.idp_configs WHERE protocol = 'saml'"
+	row := p.Conn.QueryRow(ctx, query)
+	var s any
+	err = row.Scan(&s)
+	if errors.Is(err, pgx.ErrNoRows) {
+		return false, nil
+	} else if err != nil {
+		return false, err
+	}
+	return true, nil
+}
+
 func (p *PostgresDB) HasMetadataSchema(ctx context.Context) (has bool, err error) {
 	query := "SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname = 'metadata'"
 	var table string


Reviews:
user: login: abhishekp03
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T08:25:45Z
user: login: abhishekp03
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T14:08:30Z
user: login: rashmi43
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T16:50:16Z
user: login: rwhundley
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-15T17:16:03Z
user: login: abhishekp03
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-28T06:32:31Z
user: login: abhishekp03
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-28T07:07:22Z

Review comments:
diff_hunk: @@ -277,7 +277,7 @@ func buildAuthServiceContainer(instance *operatorv1alpha1.Authentication, authSe
 		},
 	}
 
-	idpEnvVarList := []string{"NODE_ENV", "MASTER_HOST", "IDENTITY_PROVIDER_URL", "HTTP_ONLY", "SESSION_TIMEOUT", "LDAP_RECURSIVE_SEARCH", "LDAP_ATTR_CACHE_SIZE", "LDAP_ATTR_CACHE_TIMEOUT", "LDAP_ATTR_CACHE_ENABLED", "LDAP_ATTR_CACHE_SIZELIMIT",
+	idpEnvVarList := []string{"NODE_ENV", "MASTER_PATH", "IDENTITY_PROVIDER_URL", "HTTP_ONLY", "SESSION_TIMEOUT", "LDAP_RECURSIVE_SEARCH", "LDAP_ATTR_CACHE_SIZE", "LDAP_ATTR_CACHE_TIMEOUT", "LDAP_ATTR_CACHE_ENABLED", "LDAP_ATTR_CACHE_SIZELIMIT",
path: controllers/operator/containers.go
user: login: abhishekp03
body: We would need both MASTER_HOST and MASTER_PATH in authServiceContainer as well
created_at: 2025-04-03T08:23:40Z
author_association: COLLABORATOR
diff_hunk: @@ -532,6 +532,7 @@ func (r *AuthenticationReconciler) authIdpConfigMap(instance *operatorv1alpha1.A
 			"IDENTITY_PROVIDER_URL":              "https://platform-identity-provider:4300",
 			"IDENTITY_MGMT_URL":                  "https://platform-identity-management:4500",
 			"MASTER_HOST":                        clusterAddress,
+			"MASTER_PATH":                        "/idauth",
path: controllers/operator/configmap.go
user: login: abhishekp03
body: Should we default this to "" unless the logic is implemented? We don't want to begin with "" in case of a hotfix and manually add it later. or do we?
created_at: 2025-04-03T14:08:23Z
author_association: COLLABORATOR
diff_hunk: @@ -277,7 +277,7 @@ func buildAuthServiceContainer(instance *operatorv1alpha1.Authentication, authSe
 		},
 	}
 
-	idpEnvVarList := []string{"NODE_ENV", "MASTER_HOST", "IDENTITY_PROVIDER_URL", "HTTP_ONLY", "SESSION_TIMEOUT", "LDAP_RECURSIVE_SEARCH", "LDAP_ATTR_CACHE_SIZE", "LDAP_ATTR_CACHE_TIMEOUT", "LDAP_ATTR_CACHE_ENABLED", "LDAP_ATTR_CACHE_SIZELIMIT",
+	idpEnvVarList := []string{"NODE_ENV", "MASTER_PATH", "IDENTITY_PROVIDER_URL", "HTTP_ONLY", "SESSION_TIMEOUT", "LDAP_RECURSIVE_SEARCH", "LDAP_ATTR_CACHE_SIZE", "LDAP_ATTR_CACHE_TIMEOUT", "LDAP_ATTR_CACHE_ENABLED", "LDAP_ATTR_CACHE_SIZELIMIT",
path: controllers/operator/containers.go
user: login: rashmi43
body: done
created_at: 2025-04-15T16:50:16Z
author_association: MEMBER
diff_hunk: @@ -517,6 +530,10 @@ func (r *AuthenticationReconciler) authIdpConfigMap(instance *operatorv1alpha1.A
 	if isPublicCloud {
 		roksUserPrefix = "IAM#"
 	}
+
+	// Set the path for SAML connections
+	masterPath := "/idauth"
path: controllers/operator/configmap.go
user: login: abhishekp03
body: Is this assignment correct?
created_at: 2025-04-28T06:32:30Z
author_association: COLLABORATOR

PR comments:
user: login: abhishekp03
created_at: 2025-04-03T08:27:18Z
body: Are we expecting any changes related to upgrade and fresh install detection (with the dependency of existing SAML) to come anytime soon?
author_association: COLLABORATOR
user: login: ibm-ci-bot
created_at: 2025-04-28T07:07:25Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/IBM/ibm-iam-operator/pull/1009#pullrequestreview-2798193623" title="Approved">abhishekp03</a>*, *<a href="https://github.com/IBM/ibm-iam-operator/pull/1009#" title="Author self-approved">rashmi43</a>*, *<a href="https://github.com/IBM/ibm-iam-operator/pull/1009#pullrequestreview-2769203510" title="Approved">rwhundley</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=IBM%2Fibm-iam-operator).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/IBM/ibm-iam-operator/blob/release-sc2/OWNERS)~~ [rashmi43,rwhundley]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE