ApplicationSamlFactory.java

package com.cloudforgeci.api.security;

import com.cloudforgeci.api.core.Slot;
import com.cloudforgeci.api.core.annotation.BaseFactory;
import com.cloudforge.core.annotation.DeploymentContext;
import com.cloudforge.core.annotation.SystemContext;
import com.cloudforge.core.enums.AuthMode;
import software.constructs.Construct;

import java.util.logging.Logger;

/**
 * Application SAML Factory - configures SAML authentication for applications.
 *
 * <p>This factory handles SAML configuration for application-level authentication,
 * parallel to ApplicationOidcFactory for OIDC.</p>
 *
 * <p><strong>Supported SAML Providers:</strong></p>
 * <ul>
 *   <li><b>cognito-saml:</b> Deploys Keycloak as SAML bridge to Cognito (OIDC federation)</li>
 *   <li><b>identity-center:</b> Uses AWS IAM Identity Center as SAML IdP</li>
 * </ul>
 *
 * <p><strong>Architecture:</strong></p>
 * <pre>
 * cognito-saml:
 *   Application ←SAML→ Keycloak ←OIDC→ Cognito User Pool
 *
 * identity-center:
 *   Application ←SAML→ IAM Identity Center
 * </pre>
 *
 * <p><strong>Factory Orchestration:</strong></p>
 * <pre>
 * 1. CognitoAuthenticationFactory (if cognito-saml)
 * 2. IdentityCenterSamlFactory (if identity-center)
 * 3. ApplicationOidcFactory
 * 4. ApplicationSamlFactory ← YOU ARE HERE
 *    └─ KeycloakFactory (if cognito-saml)
 * </pre>
 *
 * <p><strong>Configuration Example:</strong></p>
 * <pre>
 * {
 *   "authMode": "application-oidc",
 *   "oidcProvider": "cognito-saml",  // Triggers Keycloak deployment
 *   "cognitoAutoProvision": true
 * }
 * </pre>
 */
public class ApplicationSamlFactory extends BaseFactory {

    private static final Logger LOG = Logger.getLogger(ApplicationSamlFactory.class.getName());

    @DeploymentContext("oidcProvider")
    private String oidcProvider;

    @DeploymentContext("authMode")
    private AuthMode authMode;

    @DeploymentContext("stackName")
    private String stackName;

    // Application spec to check if it supports SAML
    @SystemContext("applicationSpec")
    private com.cloudforge.core.interfaces.ApplicationSpec applicationSpec;

    // Track if Keycloak has been deployed (shared across all apps in stack)
    @SystemContext("keycloakDeployed")
    private Slot<Boolean> keycloakDeployed;

    // Keycloak factory (lazy-created when needed)
    private KeycloakFactory keycloakFactory;

    public ApplicationSamlFactory(Construct scope, String id) {
        super(scope, id);
    }

    @Override
    public void create() {
        // Only process for application-level authentication
        if (authMode != AuthMode.APPLICATION_OIDC) {
            LOG.info("Application SAML not applicable - authMode is '" + authMode + "' (expected 'application-oidc')");
            return;
        }

        // Check if application supports SAML/OIDC
        if (applicationSpec == null || !applicationSpec.supportsOidcIntegration()) {
            LOG.info("Application does not support OIDC/SAML integration - skipping");
            return;
        }

        var oidcIntegration = applicationSpec.getOidcIntegration();
        if (oidcIntegration == null) {
            LOG.info("Application has no OIDC integration configured - skipping");
            return;
        }

        // Only handle SAML authentication types
        if (!"SAML".equals(oidcIntegration.getAuthenticationType())) {
            LOG.info("Application uses " + oidcIntegration.getAuthenticationType() + " - not SAML, skipping");
            return;
        }

        LOG.info("Configuring Application SAML for: " + applicationSpec.applicationId());
        LOG.info("  OIDC Provider: " + oidcProvider);
        LOG.info("  Auth Mode: " + authMode);

        // Route to appropriate SAML provider
        configureSamlProvider();
    }

    private void configureSamlProvider() {
        if ("cognito-saml".equals(oidcProvider)) {
            configureCognitoSaml();
        } else if ("identity-center".equals(oidcProvider)) {
            configureIdentityCenterSaml();
        } else {
            LOG.warning("Unknown SAML provider: " + oidcProvider);
            LOG.warning("Expected 'cognito-saml' or 'identity-center'");
        }
    }

    /**
     * Configures Cognito SAML by deploying Keycloak as a bridge.
     * Keycloak federates with Cognito via OIDC and provides SAML to the application.
     *
     * <p><b>Singleton Pattern:</b> Keycloak is deployed ONCE per stack and shared by all SAML apps.
     * Multiple applications (Mattermost, GitLab, etc.) will reuse the same Keycloak instance.</p>
     */
    private void configureCognitoSaml() {
        LOG.info("Configuring Cognito SAML via Keycloak bridge for: " + applicationSpec.applicationId());

        // Check if Keycloak has already been deployed by another application in this stack
        if (keycloakDeployed.get().isPresent() && keycloakDeployed.get().orElse(false)) {
            LOG.info("Keycloak already deployed in this stack - reusing existing instance");
            LOG.info("  Application '" + applicationSpec.applicationId() + "' will use shared Keycloak");
            // TODO: Register this application as SAML client in existing Keycloak
            return;
        }

        // Deploy Keycloak for the first time (shared by all SAML apps in this stack)
        LOG.info("Deploying shared Keycloak instance for stack: " + stackName);
        keycloakFactory = new KeycloakFactory(this, "Keycloak");
        keycloakFactory.create();

        // Mark Keycloak as deployed so subsequent SAML apps reuse it
        keycloakDeployed.set(true);

        LOG.info("Keycloak SAML bridge deployed and ready for all SAML applications");
    }

    /**
     * Configures Identity Center SAML.
     * The actual IdP setup is handled by IdentityCenterSamlFactory,
     * this just validates it was configured.
     */
    private void configureIdentityCenterSaml() {
        LOG.info("Identity Center SAML configuration");
        LOG.info("  SAML endpoints should be set by IdentityCenterSamlFactory");
        LOG.info("  Application will use SAML integration from Identity Center");

        // IdentityCenterSamlFactory already ran and configured the SAML app
        // Just validate the configuration is available
        // TODO: Add validation that samlIdpMetadataUrl is set in SystemContext
    }
}