MetabaseSamlIntegration.java

package com.cloudforge.core.oidc;

import com.cloudforge.core.interfaces.Ec2Context;
import com.cloudforge.core.interfaces.OidcConfiguration;
import com.cloudforge.core.interfaces.OidcIntegration;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * SAML integration for Metabase (Pro/Enterprise editions).
 *
 * <p><strong>Important:</strong> Metabase does NOT support native OpenID Connect.
 * This integration uses SAML 2.0 which is available in Metabase Pro and Enterprise editions.
 * For open-source Metabase, use JWT-based authentication for embedding.</p>
 *
 * <p><strong>AWS Cognito as SAML IdP:</strong></p>
 * <ul>
 *   <li>Cognito User Pools can act as a SAML 2.0 Identity Provider</li>
 *   <li>Configure Cognito to issue SAML assertions to Metabase</li>
 *   <li>Metabase receives SAML assertions at /auth/sso endpoint</li>
 * </ul>
 *
 * <p><strong>IAM Identity Center as SAML IdP:</strong></p>
 * <ul>
 *   <li>Identity Center natively supports SAML 2.0</li>
 *   <li>Create a custom SAML application for Metabase</li>
 *   <li>Map attributes: email, firstName, lastName, groups</li>
 * </ul>
 *
 * <p><strong>Required Metabase Edition:</strong> Pro or Enterprise</p>
 *
 * <p><strong>SAML Attribute Requirements:</strong></p>
 * <ul>
 *   <li>email (required) - User's email address</li>
 *   <li>firstName (required) - User's first name</li>
 *   <li>lastName (required) - User's last name</li>
 *   <li>groups (optional) - For group sync</li>
 * </ul>
 *
 * @see <a href="https://www.metabase.com/docs/latest/people-and-groups/authenticating-with-saml">Metabase SAML Documentation</a>
 */
public class MetabaseSamlIntegration implements OidcIntegration {

    private static final String SAML_MOUNT_PATH = "/metabase/saml";

    @Override
    public boolean isSupported() {
        // SAML is only available in Metabase Pro/Enterprise
        return true;
    }

    @Override
    public String getIntegrationMethod() {
        return "SAML 2.0 authentication (Metabase Pro/Enterprise required). " +
               "Configured via MB_SAML_* environment variables. " +
               "Note: Metabase does not support native OIDC - uses SAML instead.";
    }

    @Override
    public Map<String, String> getEnvironmentVariables(OidcConfiguration config) {
        Map<String, String> env = new HashMap<>();

        // Enable SAML
        env.put("MB_SAML_ENABLED", "true");

        // Identity Provider configuration
        // For Cognito: Need to configure Cognito as SAML IdP
        // For Identity Center: Use the SAML metadata URL
        String samlIdpUri = getSamlIdpUri(config);
        env.put("MB_SAML_IDENTITY_PROVIDER_URI", samlIdpUri);

        // SAML attribute mappings
        // Standard SAML attributes that Cognito/Identity Center provide
        env.put("MB_SAML_ATTRIBUTE_EMAIL", "email");
        env.put("MB_SAML_ATTRIBUTE_FIRSTNAME", "firstName");
        env.put("MB_SAML_ATTRIBUTE_LASTNAME", "lastName");

        // Group sync configuration
        env.put("MB_SAML_GROUP_SYNC", "true");
        env.put("MB_SAML_ATTRIBUTE_GROUP", getGroupAttribute(config));

        // Application URL for SAML redirect
        String siteUrl = config.getApplicationUrl();
        if (siteUrl != null && !siteUrl.isEmpty()) {
            env.put("MB_SITE_URL", siteUrl);
        }

        // IdP certificate - placeholder for runtime injection
        // The actual certificate needs to be retrieved from Secrets Manager
        env.put("MB_SAML_IDENTITY_PROVIDER_CERTIFICATE", "${METABASE_SAML_IDP_CERTIFICATE}");

        // Optional: Configure SLO (Single Logout)
        String logoutUri = getSamlLogoutUri(config);
        if (logoutUri != null) {
            env.put("MB_SAML_IDENTITY_PROVIDER_LOGOUT_URI", logoutUri);
            // Required for SLO to work with cookies
            env.put("MB_SESSION_COOKIE_SAMESITE", "none");
        }

        return env;
    }

    @Override
    public List<String> getUserDataCommands(OidcConfiguration config, Ec2Context context) {
        List<String> commands = new ArrayList<>();

        commands.add("# Configure Metabase SAML integration");
        commands.add("# NOTE: Metabase does not support native OIDC - using SAML 2.0 instead");
        commands.add("# Requires Metabase Pro or Enterprise edition");
        commands.add("");

        // Retrieve SAML IdP certificate from Secrets Manager
        commands.add("# Retrieve SAML IdP certificate from AWS Secrets Manager");
        commands.add("echo 'Retrieving SAML IdP certificate...' >> /var/log/userdata.log");
        commands.add("export METABASE_SAML_IDP_CERTIFICATE=$(aws secretsmanager get-secret-value \\");
        commands.add("  --secret-id " + config.getClientSecretArn() + " \\");
        commands.add("  --query SecretString --output text 2>/var/log/userdata.log)");
        commands.add("");
        commands.add("if [ -z \"$METABASE_SAML_IDP_CERTIFICATE\" ]; then");
        commands.add("  echo 'WARNING: Failed to retrieve SAML IdP certificate' >> /var/log/userdata.log");
        commands.add("  echo 'SAML authentication will not be available' >> /var/log/userdata.log");
        commands.add("fi");
        commands.add("");

        // Create environment file for Metabase SAML config
        commands.add("# Create Metabase SAML configuration file");
        commands.add("cat > /opt/metabase/metabase-saml-env.sh <<'EOF'");
        commands.add("export MB_SAML_ENABLED=true");

        String samlIdpUri = getSamlIdpUri(config);
        commands.add("export MB_SAML_IDENTITY_PROVIDER_URI=\"" + samlIdpUri + "\"");

        // SAML attribute mappings
        commands.add("export MB_SAML_ATTRIBUTE_EMAIL=\"email\"");
        commands.add("export MB_SAML_ATTRIBUTE_FIRSTNAME=\"firstName\"");
        commands.add("export MB_SAML_ATTRIBUTE_LASTNAME=\"lastName\"");

        // Group sync
        commands.add("export MB_SAML_GROUP_SYNC=true");
        commands.add("export MB_SAML_ATTRIBUTE_GROUP=\"" + getGroupAttribute(config) + "\"");

        // Site URL for redirects
        String siteUrl = config.getApplicationUrl();
        if (siteUrl != null && !siteUrl.isEmpty()) {
            commands.add("export MB_SITE_URL=\"" + siteUrl + "\"");
        }

        commands.add("EOF");
        commands.add("");

        // Add certificate to env file
        commands.add("# Add IdP certificate to configuration");
        commands.add("echo \"export MB_SAML_IDENTITY_PROVIDER_CERTIFICATE=\\\"$METABASE_SAML_IDP_CERTIFICATE\\\"\" >> /opt/metabase/metabase-saml-env.sh");
        commands.add("");

        // Source the environment and restart Metabase
        commands.add("# Apply SAML configuration");
        commands.add("chmod 600 /opt/metabase/metabase-saml-env.sh");
        commands.add("source /opt/metabase/metabase-saml-env.sh");
        commands.add("");

        // Restart container with SAML config
        commands.add("# Restart Metabase container with SAML configuration");
        commands.add("docker stop metabase 2>/dev/null || true");
        commands.add("docker rm metabase 2>/dev/null || true");
        commands.add("");

        commands.add("echo 'Metabase SAML integration configured' >> /var/log/userdata.log");
        commands.add("echo 'IMPORTANT: Metabase Pro/Enterprise edition required for SAML' >> /var/log/userdata.log");

        return commands;
    }

    @Override
    public String getContainerStartupCommand() {
        return "/app/run_metabase.sh";
    }

    @Override
    public boolean supportsCognito() {
        // Cognito requires manual SAML setup in Cognito console
        return true;
    }

    @Override
    public boolean supportsIdentityCenterSaml() {
        // Full support - auto-provisioned via IdentityCenterSamlFactory
        return true;
    }

    @Override
    public String getAuthenticationType() {
        return "SAML";
    }

    @Override
    public String getPostDeploymentInstructions() {
        return """
                Metabase SAML Integration
                =========================

                IMPORTANT: SAML authentication requires Metabase Pro or Enterprise edition.
                Open-source Metabase does not support SAML - use JWT for embedding instead.

                Prerequisites:
                1. Metabase Pro or Enterprise license
                2. AWS Cognito configured as SAML IdP, or
                3. IAM Identity Center with custom SAML application

                Cognito SAML Setup:
                1. In Cognito User Pool, enable "SAML" under "App Integration"
                2. Download the SAML metadata from Cognito
                3. Configure Metabase with the IdP URI and certificate

                Identity Center SAML Setup:
                1. Create a custom SAML 2.0 application in Identity Center
                2. Set ACS URL to: https://{your-metabase}/auth/sso
                3. Map attributes: email, firstName, lastName
                4. Assign users/groups to the application

                Metabase Configuration:
                1. Access Metabase at: https://{your-domain}:3000
                2. Go to Admin > Settings > Authentication
                3. Enable SAML and verify settings
                4. Test SSO login

                SAML Redirect URL: https://{your-metabase}/auth/sso

                Group Mapping:
                - Configure group mappings in Admin > People > Groups
                - Map SAML groups to Metabase groups for permissions
                """;
    }

    /**
     * Gets the SAML Identity Provider URI based on the OIDC configuration.
     *
     * <p>For Cognito: Uses the Cognito SAML IdP endpoint
     * For Identity Center: Uses the Identity Center SAML SSO URL</p>
     */
    private String getSamlIdpUri(OidcConfiguration config) {
        String providerType = config.getProviderType();
        if ("cognito".equals(providerType)) {
            // Cognito SAML IdP endpoint
            // Format: https://cognito-idp.{region}.amazonaws.com/{userPoolId}/saml2/idpresponse
            // But for Metabase, we need the SSO URL from Cognito's SAML metadata
            return config.getIssuerUrl() + "/saml2/idp/SSO";
        } else if ("identity-center".equals(providerType)) {
            // Identity Center: authorizationEndpoint may already contain the SAML SSO URL
            // (set by ApplicationOidcFactory.buildIdentityCenterSamlConfiguration)
            // Or it may be an OIDC endpoint (from IdentityCenterOidcConfiguration in tests)
            String authEndpoint = config.getAuthorizationEndpoint();
            if (authEndpoint != null && authEndpoint.contains("/saml/")) {
                // Already a SAML URL
                return authEndpoint;
            }
            // Derive SAML URL from OIDC endpoint
            return authEndpoint.replace("/oauth2/authorize", "/saml/SSO");
        } else {
            // External OIDC provider - try to derive SAML URL
            return config.getAuthorizationEndpoint().replace("/oauth2/authorize", "/saml/SSO");
        }
    }

    /**
     * Gets the SAML logout URI for Single Logout (SLO).
     */
    private String getSamlLogoutUri(OidcConfiguration config) {
        String providerType = config.getProviderType();
        if ("cognito".equals(providerType)) {
            return config.getIssuerUrl() + "/saml2/logout";
        } else if ("identity-center".equals(providerType)) {
            // Identity Center doesn't have a standard SAML logout endpoint via API
            // Return null to skip SLO configuration
            return null;
        } else {
            return config.getAuthorizationEndpoint().replace("/oauth2/authorize", "/saml/logout");
        }
    }

    /**
     * Gets the SAML group attribute name based on provider type.
     */
    private String getGroupAttribute(OidcConfiguration config) {
        if (config.getProviderType().equals("cognito")) {
            // Cognito uses custom attributes for groups in SAML
            return "custom:groups";
        } else {
            // Identity Center uses standard groups attribute
            return "groups";
        }
    }

    // ==================== SAML Certificate Configuration ====================

    @Override
    public String getSamlCertificateMountPath() {
        return SAML_MOUNT_PATH;
    }

    @Override
    public String getSamlCertificateFilePath() {
        return SAML_MOUNT_PATH + "/idp.crt";
    }

    @Override
    public String getSamlCertificateEnvVar() {
        return "MB_SAML_IDENTITY_PROVIDER_CERTIFICATE";
    }
}