Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -304,6 +305,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin
private final OpensearchDynamicSetting<Boolean> transportPassiveAuthSetting;
private final OpensearchDynamicSetting<Boolean> resourceSharingEnabledSetting;
private final OpensearchDynamicSetting<List<String>> resourceSharingProtectedResourceTypesSetting;
private final OpensearchDynamicSetting<String> dashboardsUrlSetting;
private volatile PasswordHasher passwordHasher;
private volatile DlsFlsBaseContext dlsFlsBaseContext;
private ResourceSharingIndexHandler rsIndexHandler;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1208,6 +1211,10 @@ public Collection<Object> 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);
Expand Down Expand Up @@ -2227,6 +2234,8 @@ public List<Setting<?>> getSettings() {
);
settings.add(transportPassiveAuthSetting.getDynamicSetting());

settings.add(dashboardsUrlSetting.getDynamicSetting());

settings.add(
Setting.boolSetting(
ConfigConstants.SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS,
Expand Down Expand Up @@ -2587,6 +2596,7 @@ public static class GuiceHolder implements LifecycleComponent {
private static BackendRegistry backendRegistry;
private static AuditLogImpl auditLog;
private static PrivilegesConfiguration privilegesConfiguration;
private static volatile OpensearchDynamicSetting<String> dashboardsUrlSetting;

private static ExtensionsManager extensionsManager;

Expand Down Expand Up @@ -2637,6 +2647,14 @@ public static AuditLog getAuditLog() {
return auditLog;
}

public static OpensearchDynamicSetting<String> getDashboardsUrlSetting() {
return dashboardsUrlSetting;
}

public static void setDashboardsUrlSetting(OpensearchDynamicSetting<String> setting) {
dashboardsUrlSetting = setting;
}

@Override
public void close() {}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -72,7 +73,7 @@ class AuthTokenProcessorHandler {
private String jwtRolesKey;
private String samlSubjectKey;
private String samlRolesKey;
private String kibanaRootUrl;
private Supplier<String> dashboardsUrlSupplier;

private long expiryOffset = 0;
private ExpiryBaseValue expiryBaseValue = ExpiryBaseValue.AUTO;
Expand All @@ -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<String> dashboardsUrlSupplier
) throws Exception {
this.saml2SettingsProvider = saml2SettingsProvider;
this.dashboardsUrlSupplier = dashboardsUrlSupplier;

this.jwtRolesKey = jwtSettings.get("roles_key", "roles");
this.jwtSubjectKey = jwtSettings.get("subject_key", "sub");
Expand All @@ -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);
}
Expand Down Expand Up @@ -394,7 +404,12 @@ private String getAbsoluteAcsEndpoint(String acsEndpoint) {
if (acsEndpointUri.isAbsolute()) {
return acsEndpoint;
} else {
return new URI(this.kibanaRootUrl).resolve(acsEndpointUri).toString();
String dashboardsUrl = dashboardsUrlSupplier.get();
Comment thread
praveenvenkat06 marked this conversation as resolved.
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) {
log.error("Could not parse URI for acsEndpoint: {}", acsEndpoint);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -95,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
Expand Down Expand Up @@ -132,7 +135,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();
Expand All @@ -145,7 +153,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);

Expand All @@ -155,6 +168,27 @@ public HTTPSamlAuthenticator(final Settings settings, final Path configPath) {
}
}

/**
* Get the Dashboards URL with fallback logic:
* 1. Try dynamic cluster setting
* 2. Fall back to security configuration
*
* @return the Dashboards URL to use
*/
private String getDashboardsUrl() {
OpensearchDynamicSetting<String> 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;
}
}

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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
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;

import org.apache.logging.log4j.LogManager;
Expand Down Expand Up @@ -52,14 +54,26 @@ public class Saml2SettingsProvider {
private final MetadataResolver metadataResolver;
private final String idpEntityId;
private final PrivateKey spSignaturePrivateKey;
private final Supplier<String> 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<String> dashboardsUrlSupplier
) {
this.opensearchSettings = opensearchSettings;
this.metadataResolver = metadataResolver;
this.idpEntityId = opensearchSettings.get("idp.entity_id");
this.spSignaturePrivateKey = spSignaturePrivateKey;
this.dashboardsUrlSupplier = dashboardsUrlSupplier;
}

Saml2Settings get() throws SamlConfigException {
Expand Down Expand Up @@ -112,9 +126,15 @@ 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();
this.metadataUpdateTime = tempLastUpdate;
this.lastUsedDashboardsUrl = currentDashboardsUrl;
}

return this.cachedSaml2Settings;
Expand All @@ -134,6 +154,19 @@ private boolean isUpdateRequired() {
}
}

private boolean isDashboardsUrlChanged() {
String currentDashboardsUrl = this.dashboardsUrlSupplier.get();
if (!Objects.equals(this.lastUsedDashboardsUrl, currentDashboardsUrl)) {
log.debug(
"Dashboards URL has changed from '{}' to '{}'; SAML settings cache will be refreshed",
this.lastUsedDashboardsUrl,
currentDashboardsUrl
);
return true;
}
return false;
}

private void initMisc(HashMap<String, Object> configProperties) {
configProperties.put(SettingsBuilder.STRICT_PROPERTY_KEY, true);
configProperties.put(SettingsBuilder.SECURITY_REJECT_UNSOLICITED_RESPONSES_WITH_INRESPONSETO, true);
Expand All @@ -143,7 +176,7 @@ private void initMisc(HashMap<String, Object> configProperties) {
private void initSpEndpoints(HashMap<String, Object> 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> {

private static final String SETTING = ConfigConstants.SECURITY_DASHBOARDS_URL;

public DashboardsUrlSetting(final Settings settings) {
super(getSetting(), getSettingInitialValue(settings));
}

private static Setting<String> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,9 @@ public class ConfigConstants {
public static final List<String> 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 = "";
Expand Down
Loading