From f19e2d84751edab547d9990298c718a6fd2282c7 Mon Sep 17 00:00:00 2001 From: Praveen Raj V Date: Wed, 1 Jul 2026 16:12:57 +0530 Subject: [PATCH 1/4] Add support for dynamic Dashboards URL via cluster settings Signed-off-by: Praveen Raj V --- .../security/OpenSearchSecurityPlugin.java | 18 ++++++++ .../http/saml/AuthTokenProcessorHandler.java | 17 +++++-- .../auth/http/saml/HTTPSamlAuthenticator.java | 43 ++++++++++++++++- .../auth/http/saml/Saml2SettingsProvider.java | 33 +++++++++++-- .../setting/DashboardsUrlSetting.java | 46 +++++++++++++++++++ .../security/support/ConfigConstants.java | 3 ++ 6 files changed, 152 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/opensearch/security/setting/DashboardsUrlSetting.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index f64516c74e..e2f833589d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -206,6 +206,7 @@ import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.setting.DashboardsUrlSetting; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.setting.TransportPassiveAuthSetting; import org.opensearch.security.spi.SecurityConfigExtension; @@ -304,6 +305,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private final OpensearchDynamicSetting transportPassiveAuthSetting; private final OpensearchDynamicSetting resourceSharingEnabledSetting; private final OpensearchDynamicSetting> resourceSharingProtectedResourceTypesSetting; + private final OpensearchDynamicSetting dashboardsUrlSetting; private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; private ResourceSharingIndexHandler rsIndexHandler; @@ -391,6 +393,7 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) resourceSharingEnabledSetting = new ResourceSharingFeatureFlagSetting(settings, resourcePluginInfo); // not filtered resourceSharingProtectedResourceTypesSetting = new ResourceSharingProtectedResourcesSetting(settings, resourcePluginInfo); // not // filtered + dashboardsUrlSetting = new DashboardsUrlSetting(settings); resourcePluginInfo.setProtectedTypesSetting(resourceSharingProtectedResourceTypesSetting); if (disabled) { @@ -1208,6 +1211,10 @@ public Collection createComponents( transportPassiveAuthSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); resourceSharingEnabledSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); resourceSharingProtectedResourceTypesSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); + dashboardsUrlSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); + + // Make dashboardsUrlSetting available through GuiceHolder for SAML authenticators + GuiceHolder.setDashboardsUrlSetting(dashboardsUrlSetting); final ClusterInfoHolder cih = new ClusterInfoHolder(this.cs.getClusterName().value()); this.cs.addListener(cih); @@ -2227,6 +2234,8 @@ public List> getSettings() { ); settings.add(transportPassiveAuthSetting.getDynamicSetting()); + settings.add(dashboardsUrlSetting.getDynamicSetting()); + settings.add( Setting.boolSetting( ConfigConstants.SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS, @@ -2587,6 +2596,7 @@ public static class GuiceHolder implements LifecycleComponent { private static BackendRegistry backendRegistry; private static AuditLogImpl auditLog; private static PrivilegesConfiguration privilegesConfiguration; + private static OpensearchDynamicSetting dashboardsUrlSetting; private static ExtensionsManager extensionsManager; @@ -2637,6 +2647,14 @@ public static AuditLog getAuditLog() { return auditLog; } + public static OpensearchDynamicSetting getDashboardsUrlSetting() { + return dashboardsUrlSetting; + } + + public static void setDashboardsUrlSetting(OpensearchDynamicSetting setting) { + dashboardsUrlSetting = setting; + } + @Override public void close() {} diff --git a/src/main/java/org/opensearch/security/auth/http/saml/AuthTokenProcessorHandler.java b/src/main/java/org/opensearch/security/auth/http/saml/AuthTokenProcessorHandler.java index e6010aa5d1..17e895f1a5 100644 --- a/src/main/java/org/opensearch/security/auth/http/saml/AuthTokenProcessorHandler.java +++ b/src/main/java/org/opensearch/security/auth/http/saml/AuthTokenProcessorHandler.java @@ -19,6 +19,7 @@ import java.util.Date; import java.util.List; import java.util.Optional; +import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -72,7 +73,7 @@ class AuthTokenProcessorHandler { private String jwtRolesKey; private String samlSubjectKey; private String samlRolesKey; - private String kibanaRootUrl; + private Supplier dashboardsUrlSupplier; private long expiryOffset = 0; private ExpiryBaseValue expiryBaseValue = ExpiryBaseValue.AUTO; @@ -81,7 +82,17 @@ class AuthTokenProcessorHandler { private Pattern samlRolesSeparatorPattern; AuthTokenProcessorHandler(Settings settings, Settings jwtSettings, Saml2SettingsProvider saml2SettingsProvider) throws Exception { + this(settings, jwtSettings, saml2SettingsProvider, () -> settings.get("kibana_url")); + } + + AuthTokenProcessorHandler( + Settings settings, + Settings jwtSettings, + Saml2SettingsProvider saml2SettingsProvider, + Supplier dashboardsUrlSupplier + ) throws Exception { this.saml2SettingsProvider = saml2SettingsProvider; + this.dashboardsUrlSupplier = dashboardsUrlSupplier; this.jwtRolesKey = jwtSettings.get("roles_key", "roles"); this.jwtSubjectKey = jwtSettings.get("subject_key", "sub"); @@ -90,7 +101,6 @@ class AuthTokenProcessorHandler { this.samlSubjectKey = settings.get("subject_key"); // Originally release with a typo, prioritize correct spelling over typo'ed version String samlRolesSeparator = settings.get("roles_separator", settings.get("roles_seperator")); - this.kibanaRootUrl = settings.get("kibana_url"); if (samlRolesSeparator != null) { this.samlRolesSeparatorPattern = Pattern.compile(samlRolesSeparator); } @@ -394,7 +404,8 @@ private String getAbsoluteAcsEndpoint(String acsEndpoint) { if (acsEndpointUri.isAbsolute()) { return acsEndpoint; } else { - return new URI(this.kibanaRootUrl).resolve(acsEndpointUri).toString(); + String dashboardsUrl = dashboardsUrlSupplier.get(); + return new URI(dashboardsUrl).resolve(acsEndpointUri).toString(); } } catch (URISyntaxException e) { log.error("Could not parse URI for acsEndpoint: {}", acsEndpoint); diff --git a/src/main/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticator.java b/src/main/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticator.java index afd632b8ac..cd3ea98259 100644 --- a/src/main/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticator.java @@ -34,6 +34,7 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.rest.RestRequest; import org.opensearch.secure_sm.AccessController; +import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.auth.Destroyable; import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.auth.http.jwt.AbstractHTTPJwtAuthenticator; @@ -45,6 +46,7 @@ import org.opensearch.security.filter.SecurityRequestChannelUnsupported; import org.opensearch.security.filter.SecurityResponse; import org.opensearch.security.opensaml.integration.SecurityXMLObjectProviderInitializer; +import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.PemKeyReader; import org.opensearch.security.user.AuthCredentials; @@ -132,7 +134,12 @@ public HTTPSamlAuthenticator(final Settings settings, final Path configPath) { this.metadataResolver = createMetadataResolver(settings, configPath); - this.saml2SettingsProvider = new Saml2SettingsProvider(settings, this.metadataResolver, spSignaturePrivateKey); + this.saml2SettingsProvider = new Saml2SettingsProvider( + settings, + this.metadataResolver, + spSignaturePrivateKey, + this::getDashboardsUrl + ); try { this.saml2SettingsProvider.getCached(); @@ -145,7 +152,12 @@ public HTTPSamlAuthenticator(final Settings settings, final Path configPath) { this.jwtSettings = this.createJwtAuthenticatorSettings(settings); - this.authTokenProcessorHandler = new AuthTokenProcessorHandler(settings, jwtSettings, this.saml2SettingsProvider); + this.authTokenProcessorHandler = new AuthTokenProcessorHandler( + settings, + jwtSettings, + this.saml2SettingsProvider, + this::getDashboardsUrl + ); this.httpJwtAuthenticator = new HTTPJwtAuthenticator(this.jwtSettings, configPath); @@ -155,6 +167,33 @@ public HTTPSamlAuthenticator(final Settings settings, final Path configPath) { } } + /** + * Get the Dashboards URL with fallback logic: + * 1. Try dynamic cluster setting (if available) + * 2. Fall back to security configuration (kibana_url) + * + * @return the Dashboards URL to use + */ + private String getDashboardsUrl() { + try { + // Try to get the dynamic cluster setting value + OpensearchDynamicSetting dashboardsUrlSetting = OpenSearchSecurityPlugin.GuiceHolder.getDashboardsUrlSetting(); + if (dashboardsUrlSetting != null) { + String dynamicUrl = dashboardsUrlSetting.getDynamicSettingValue(); + if (dynamicUrl != null && !dynamicUrl.trim().isEmpty()) { + log.debug("Using Dashboards URL from dynamic cluster setting: {}", dynamicUrl); + return dynamicUrl; + } + } + } catch (Exception e) { + log.debug("Dynamic dashboards URL setting not available, using security configuration", e); + } + + // Fall back to security configuration + log.debug("Using Dashboards URL from security configuration (kibana_url): {}", kibanaRootUrl); + return kibanaRootUrl; + } + @Override public AuthCredentials extractCredentials(final SecurityRequest request, final ThreadContext threadContext) throws OpenSearchSecurityException { diff --git a/src/main/java/org/opensearch/security/auth/http/saml/Saml2SettingsProvider.java b/src/main/java/org/opensearch/security/auth/http/saml/Saml2SettingsProvider.java index 782d8f1b51..09a298aac1 100644 --- a/src/main/java/org/opensearch/security/auth/http/saml/Saml2SettingsProvider.java +++ b/src/main/java/org/opensearch/security/auth/http/saml/Saml2SettingsProvider.java @@ -19,6 +19,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; @@ -52,14 +53,26 @@ public class Saml2SettingsProvider { private final MetadataResolver metadataResolver; private final String idpEntityId; private final PrivateKey spSignaturePrivateKey; + private final Supplier dashboardsUrlSupplier; private Saml2Settings cachedSaml2Settings; private Instant metadataUpdateTime; + private String lastUsedDashboardsUrl; Saml2SettingsProvider(Settings opensearchSettings, MetadataResolver metadataResolver, PrivateKey spSignaturePrivateKey) { + this(opensearchSettings, metadataResolver, spSignaturePrivateKey, () -> opensearchSettings.get("kibana_url")); + } + + Saml2SettingsProvider( + Settings opensearchSettings, + MetadataResolver metadataResolver, + PrivateKey spSignaturePrivateKey, + Supplier dashboardsUrlSupplier + ) { this.opensearchSettings = opensearchSettings; this.metadataResolver = metadataResolver; this.idpEntityId = opensearchSettings.get("idp.entity_id"); this.spSignaturePrivateKey = spSignaturePrivateKey; + this.dashboardsUrlSupplier = dashboardsUrlSupplier; } Saml2Settings get() throws SamlConfigException { @@ -113,8 +126,10 @@ Saml2Settings getCached() throws SamlConfigException { } if (this.cachedSaml2Settings == null) { + String currentDashboardsUrl = this.dashboardsUrlSupplier.get(); this.cachedSaml2Settings = this.get(); this.metadataUpdateTime = tempLastUpdate; + this.lastUsedDashboardsUrl = currentDashboardsUrl; } return this.cachedSaml2Settings; @@ -128,10 +143,22 @@ private boolean isUpdateRequired() { } if (refreshableMetadataResolver.getLastUpdate().isAfter(this.metadataUpdateTime)) { + log.debug("IdP metadata has been updated; SAML settings cache will be refreshed"); + return true; + } + + // Check if Dashboards URL has changed + String currentDashboardsUrl = this.dashboardsUrlSupplier.get(); + if (this.lastUsedDashboardsUrl != null && !this.lastUsedDashboardsUrl.equals(currentDashboardsUrl)) { + log.debug( + "Dashboards URL has changed from '{}' to '{}'; SAML settings cache will be refreshed", + this.lastUsedDashboardsUrl, + currentDashboardsUrl + ); return true; - } else { - return false; } + + return false; } private void initMisc(HashMap configProperties) { @@ -143,7 +170,7 @@ private void initMisc(HashMap configProperties) { private void initSpEndpoints(HashMap configProperties) { configProperties.put( SettingsBuilder.SP_ASSERTION_CONSUMER_SERVICE_URL_PROPERTY_KEY, - this.buildAssertionConsumerEndpoint(this.opensearchSettings.get("kibana_url")) + this.buildAssertionConsumerEndpoint(this.dashboardsUrlSupplier.get()) ); configProperties.put( SettingsBuilder.SP_ASSERTION_CONSUMER_SERVICE_BINDING_PROPERTY_KEY, diff --git a/src/main/java/org/opensearch/security/setting/DashboardsUrlSetting.java b/src/main/java/org/opensearch/security/setting/DashboardsUrlSetting.java new file mode 100644 index 0000000000..0daff04800 --- /dev/null +++ b/src/main/java/org/opensearch/security/setting/DashboardsUrlSetting.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.setting; + +import org.opensearch.common.settings.Setting; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.support.ConfigConstants; + +/** + * Dynamic cluster setting for OpenSearch Dashboards URL used in SAML authentication flow. + * This setting takes precedence over the kibana_url configured in the security index + * when both are present, allowing runtime updates without modifying security configuration. + */ +public class DashboardsUrlSetting extends OpensearchDynamicSetting { + + private static final String SETTING = ConfigConstants.SECURITY_DASHBOARDS_URL; + + public DashboardsUrlSetting(final Settings settings) { + super(getSetting(), getSettingInitialValue(settings)); + } + + private static Setting getSetting() { + return Setting.simpleString(SETTING, Setting.Property.NodeScope, Setting.Property.Dynamic, Setting.Property.Sensitive); + } + + private static String getSettingInitialValue(final Settings settings) { + return settings.get(SETTING, null); + } + + @Override + protected String getClusterChangeMessage(final String dynamicSettingNewValue) { + if (dynamicSettingNewValue == null || dynamicSettingNewValue.isEmpty()) { + return "Dashboards URL cluster setting has been cleared. Will fall back to security configuration."; + } + return String.format("Detected change in settings, cluster setting for Dashboards URL is now: %s", dynamicSettingNewValue); + } +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index d4cebc362c..ff58181f07 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -402,6 +402,9 @@ public class ConfigConstants { public static final List SECURITY_SYSTEM_INDICES_DEFAULT = Collections.emptyList(); public static final String SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT = SECURITY_SETTINGS_PREFIX + "masked_fields.algorithm.default"; + // Dynamic cluster setting for Dashboards URL used in SAML authentication flow + public static final String SECURITY_DASHBOARDS_URL = SECURITY_SETTINGS_PREFIX + "dashboards.dashboards_url"; + public static final String TENANCY_PRIVATE_TENANT_NAME = "private"; public static final String TENANCY_GLOBAL_TENANT_NAME = "global"; public static final String TENANCY_GLOBAL_TENANT_DEFAULT_NAME = ""; From fa756dbaee8855e49cd7546a38f9bb04b9a7abd4 Mon Sep 17 00:00:00 2001 From: Praveen Raj V Date: Wed, 1 Jul 2026 20:52:37 +0530 Subject: [PATCH 2/4] JUnits for dynamic Dashboards URL implementation Signed-off-by: Praveen Raj V --- .../auth/http/saml/HTTPSamlAuthenticator.java | 25 ++++---- .../http/saml/HTTPSamlAuthenticatorTest.java | 51 ++++++++++++++++ .../setting/DashboardsUrlSettingTest.java | 58 +++++++++++++++++++ 3 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 src/test/java/org/opensearch/security/setting/DashboardsUrlSettingTest.java diff --git a/src/main/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticator.java b/src/main/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticator.java index cd3ea98259..f2dbfcb102 100644 --- a/src/main/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticator.java @@ -97,7 +97,8 @@ public class HTTPSamlAuthenticator implements HTTPAuthenticator, Destroyable { private String spSignatureAlgorithm; private Boolean useForceAuthn; private PrivateKey spSignaturePrivateKey; - private Saml2SettingsProvider saml2SettingsProvider; + @VisibleForTesting + protected Saml2SettingsProvider saml2SettingsProvider; private MetadataResolver metadataResolver; private AuthTokenProcessorHandler authTokenProcessorHandler; @VisibleForTesting @@ -169,27 +170,21 @@ public HTTPSamlAuthenticator(final Settings settings, final Path configPath) { /** * Get the Dashboards URL with fallback logic: - * 1. Try dynamic cluster setting (if available) - * 2. Fall back to security configuration (kibana_url) + * 1. Try dynamic cluster setting + * 2. Fall back to security configuration * * @return the Dashboards URL to use */ private String getDashboardsUrl() { - try { - // Try to get the dynamic cluster setting value - OpensearchDynamicSetting dashboardsUrlSetting = OpenSearchSecurityPlugin.GuiceHolder.getDashboardsUrlSetting(); - if (dashboardsUrlSetting != null) { - String dynamicUrl = dashboardsUrlSetting.getDynamicSettingValue(); - if (dynamicUrl != null && !dynamicUrl.trim().isEmpty()) { - log.debug("Using Dashboards URL from dynamic cluster setting: {}", dynamicUrl); - return dynamicUrl; - } + OpensearchDynamicSetting dashboardsUrlSetting = OpenSearchSecurityPlugin.GuiceHolder.getDashboardsUrlSetting(); + if (dashboardsUrlSetting != null) { + String dynamicUrl = dashboardsUrlSetting.getDynamicSettingValue(); + if (dynamicUrl != null && !dynamicUrl.trim().isEmpty()) { + log.debug("Using Dashboards URL from dynamic cluster setting: {}", dynamicUrl); + return dynamicUrl; } - } catch (Exception e) { - log.debug("Dynamic dashboards URL setting not available, using security configuration", e); } - // Fall back to security configuration log.debug("Using Dashboards URL from security configuration (kibana_url): {}", kibanaRootUrl); return kibanaRootUrl; } diff --git a/src/test/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticatorTest.java b/src/test/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticatorTest.java index f79bcae38a..8256b8d733 100644 --- a/src/test/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticatorTest.java +++ b/src/test/java/org/opensearch/security/auth/http/saml/HTTPSamlAuthenticatorTest.java @@ -50,9 +50,11 @@ import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestResponse; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.OpenSearchSecurityPlugin; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.filter.SecurityResponse; +import org.opensearch.security.setting.DashboardsUrlSetting; import org.opensearch.security.test.helper.file.FileHelper; import org.opensearch.security.user.AuthCredentials; import org.opensearch.security.util.FakeRestRequest; @@ -991,6 +993,55 @@ public XContentBuilder newBuilder(MediaType mediaType, MediaType responseContent } + @Test + public void dynamicClusterSettingUrlTakesPrecedenceOverKibanaUrl() throws Exception { + // Inject a dynamic setting with a URL different from kibana_url + Settings settingsWithDynamicUrl = Settings.builder() + .put(org.opensearch.security.support.ConfigConstants.SECURITY_DASHBOARDS_URL, "http://dynamic-dashboards") + .build(); + DashboardsUrlSetting dynamicSetting = new DashboardsUrlSetting(settingsWithDynamicUrl); + OpenSearchSecurityPlugin.GuiceHolder.setDashboardsUrlSetting(dynamicSetting); + + try { + Settings settings = Settings.builder() + .put(IDP_METADATA_URL, mockSamlIdpServer.getMetadataUri()) + .put("kibana_url", "http://kibana-fallback") + .put("idp.entity_id", mockSamlIdpServer.getIdpEntityId()) + .put("exchange_key", "abc") + .put("roles_key", "roles") + .put("path.home", ".") + .build(); + HTTPSamlAuthenticator samlAuthenticator = new HTTPSamlAuthenticator(settings, null); + String acsUrl = samlAuthenticator.saml2SettingsProvider.getCached().getSpAssertionConsumerServiceUrl().toString(); + assertThat(acsUrl, Matchers.startsWith("http://dynamic-dashboards")); + } finally { + OpenSearchSecurityPlugin.GuiceHolder.setDashboardsUrlSetting(null); + } + } + + @Test + public void fallsBackToKibanaUrlWhenDynamicSettingIsAbsent() throws Exception { + // Dynamic setting is null (not configured) — must fall back to kibana_url + DashboardsUrlSetting dynamicSetting = new DashboardsUrlSetting(Settings.builder().build()); + OpenSearchSecurityPlugin.GuiceHolder.setDashboardsUrlSetting(dynamicSetting); + + try { + Settings settings = Settings.builder() + .put(IDP_METADATA_URL, mockSamlIdpServer.getMetadataUri()) + .put("kibana_url", "http://kibana-fallback") + .put("idp.entity_id", mockSamlIdpServer.getIdpEntityId()) + .put("exchange_key", "abc") + .put("roles_key", "roles") + .put("path.home", ".") + .build(); + HTTPSamlAuthenticator samlAuthenticator = new HTTPSamlAuthenticator(settings, null); + String acsUrl = samlAuthenticator.saml2SettingsProvider.getCached().getSpAssertionConsumerServiceUrl().toString(); + assertThat(acsUrl, Matchers.startsWith("http://kibana-fallback")); + } finally { + OpenSearchSecurityPlugin.GuiceHolder.setDashboardsUrlSetting(null); + } + } + static class AuthenticateHeaders { final String location; final String requestId; diff --git a/src/test/java/org/opensearch/security/setting/DashboardsUrlSettingTest.java b/src/test/java/org/opensearch/security/setting/DashboardsUrlSettingTest.java new file mode 100644 index 0000000000..f78115b4c2 --- /dev/null +++ b/src/test/java/org/opensearch/security/setting/DashboardsUrlSettingTest.java @@ -0,0 +1,58 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.setting; + +import org.junit.Test; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.support.ConfigConstants; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +public class DashboardsUrlSettingTest { + + @Test + public void initialValueIsNullWhenNotConfigured() { + DashboardsUrlSetting setting = new DashboardsUrlSetting(Settings.builder().build()); + assertThat(setting.getDynamicSettingValue(), is(nullValue())); + } + + @Test + public void initialValueIsReadFromSettings() { + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_DASHBOARDS_URL, "https://dashboards.example.com").build(); + DashboardsUrlSetting setting = new DashboardsUrlSetting(settings); + assertThat(setting.getDynamicSettingValue(), equalTo("https://dashboards.example.com")); + } + + @Test + public void dynamicSettingKeyMatchesConstant() { + DashboardsUrlSetting setting = new DashboardsUrlSetting(Settings.builder().build()); + assertThat(setting.getDynamicSetting().getKey(), equalTo(ConfigConstants.SECURITY_DASHBOARDS_URL)); + } + + @Test + public void dynamicSettingIsNodeScopeAndDynamic() { + DashboardsUrlSetting setting = new DashboardsUrlSetting(Settings.builder().build()); + assertThat(setting.getDynamicSetting().hasNodeScope(), is(true)); + assertThat(setting.getDynamicSetting().isDynamic(), is(true)); + } + + @Test + public void valueCanBeUpdatedAtRuntime() { + DashboardsUrlSetting setting = new DashboardsUrlSetting(Settings.builder().build()); + setting.setDynamicSettingValue("https://new-dashboards.example.com"); + assertThat(setting.getDynamicSettingValue(), equalTo("https://new-dashboards.example.com")); + } +} From 410b3dd35f1c981aa10f57503d9bd36744cc85c0 Mon Sep 17 00:00:00 2001 From: Praveen Raj V Date: Wed, 1 Jul 2026 22:17:31 +0530 Subject: [PATCH 3/4] Implement PR code suggestions Signed-off-by: Praveen Raj V --- .../security/OpenSearchSecurityPlugin.java | 2 +- .../auth/http/saml/Saml2SettingsProvider.java | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index e2f833589d..79962d57b5 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -2596,7 +2596,7 @@ public static class GuiceHolder implements LifecycleComponent { private static BackendRegistry backendRegistry; private static AuditLogImpl auditLog; private static PrivilegesConfiguration privilegesConfiguration; - private static OpensearchDynamicSetting dashboardsUrlSetting; + private static volatile OpensearchDynamicSetting dashboardsUrlSetting; private static ExtensionsManager extensionsManager; diff --git a/src/main/java/org/opensearch/security/auth/http/saml/Saml2SettingsProvider.java b/src/main/java/org/opensearch/security/auth/http/saml/Saml2SettingsProvider.java index 09a298aac1..3d45f9922e 100644 --- a/src/main/java/org/opensearch/security/auth/http/saml/Saml2SettingsProvider.java +++ b/src/main/java/org/opensearch/security/auth/http/saml/Saml2SettingsProvider.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -125,6 +126,10 @@ Saml2Settings getCached() throws SamlConfigException { tempLastUpdate = ((RefreshableMetadataResolver) this.metadataResolver).getLastUpdate(); } + if (this.isDashboardsUrlChanged()) { + this.cachedSaml2Settings = null; + } + if (this.cachedSaml2Settings == null) { String currentDashboardsUrl = this.dashboardsUrlSupplier.get(); this.cachedSaml2Settings = this.get(); @@ -143,13 +148,15 @@ private boolean isUpdateRequired() { } if (refreshableMetadataResolver.getLastUpdate().isAfter(this.metadataUpdateTime)) { - log.debug("IdP metadata has been updated; SAML settings cache will be refreshed"); return true; + } else { + return false; } + } - // Check if Dashboards URL has changed + private boolean isDashboardsUrlChanged() { String currentDashboardsUrl = this.dashboardsUrlSupplier.get(); - if (this.lastUsedDashboardsUrl != null && !this.lastUsedDashboardsUrl.equals(currentDashboardsUrl)) { + if (!Objects.equals(this.lastUsedDashboardsUrl, currentDashboardsUrl)) { log.debug( "Dashboards URL has changed from '{}' to '{}'; SAML settings cache will be refreshed", this.lastUsedDashboardsUrl, @@ -157,7 +164,6 @@ private boolean isUpdateRequired() { ); return true; } - return false; } From afbf218b83b18dd246dc0c256484bb09d63604df Mon Sep 17 00:00:00 2001 From: Praveen Raj V Date: Sun, 5 Jul 2026 22:53:42 +0530 Subject: [PATCH 4/4] Update AuthTokenProcessorHandler.java Signed-off-by: Praveen Raj V --- .../security/auth/http/saml/AuthTokenProcessorHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/opensearch/security/auth/http/saml/AuthTokenProcessorHandler.java b/src/main/java/org/opensearch/security/auth/http/saml/AuthTokenProcessorHandler.java index 17e895f1a5..bb9169214a 100644 --- a/src/main/java/org/opensearch/security/auth/http/saml/AuthTokenProcessorHandler.java +++ b/src/main/java/org/opensearch/security/auth/http/saml/AuthTokenProcessorHandler.java @@ -405,6 +405,10 @@ private String getAbsoluteAcsEndpoint(String acsEndpoint) { return acsEndpoint; } else { String dashboardsUrl = dashboardsUrlSupplier.get(); + if (dashboardsUrl == null || dashboardsUrl.isEmpty()) { + log.error("Dashboards URL is not configured; cannot resolve relative acsEndpoint: {}", acsEndpoint); + return acsEndpoint; + } return new URI(dashboardsUrl).resolve(acsEndpointUri).toString(); } } catch (URISyntaxException e) {