From 65375fd992c7711efee8abf2c468b90cac719ea0 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Sun, 16 Nov 2025 09:41:35 +0100 Subject: [PATCH 1/6] Modularized PrivilegeEvaluator Signed-off-by: Nils Bandener --- CHANGELOG.md | 1 + .../ConfigurableRoleMapperTest.java | 227 +++++ .../security/OpenSearchSecurityPlugin.java | 65 +- .../security/auth/RolesInjector.java | 52 + .../configuration/ClusterInfoHolder.java | 8 + .../configuration/DlsFlsValveImpl.java | 2 +- .../PrivilegesInterceptorImpl.java | 27 +- .../SecurityFlsDlsIndexSearcherWrapper.java | 8 +- .../SystemIndexSearcherWrapper.java | 25 +- .../dlic/rest/api/AccountApiAction.java | 6 +- .../dlic/rest/api/PermissionsInfoAction.java | 12 +- .../api/RestApiAdminPrivilegesEvaluator.java | 18 +- .../rest/api/RestApiPrivilegesEvaluator.java | 10 +- .../rest/api/SecurityApiDependencies.java | 12 +- .../dlic/rest/api/SecurityRestApiActions.java | 14 +- .../security/filter/SecurityFilter.java | 37 +- .../privileges/ConfigurableRoleMapper.java | 249 +++++ .../DashboardsMultiTenancyConfiguration.java | 77 ++ .../privileges/PrivilegesConfiguration.java | 218 ++++ .../privileges/PrivilegesEvaluator.java | 931 ++---------------- .../privileges/PrivilegesEvaluatorImpl.java | 758 ++++++++++++++ .../privileges/PrivilegesInterceptor.java | 5 +- .../RestLayerPrivilegesEvaluator.java | 8 +- .../security/privileges/RoleMapper.java | 25 + .../privileges/SnapshotRestoreEvaluator.java | 13 +- .../SystemIndexAccessEvaluator.java | 2 +- .../privileges/dlsfls/DlsFlsBaseContext.java | 10 +- .../resources/ResourceAccessHandler.java | 4 - .../security/rest/DashboardsInfoAction.java | 48 +- .../security/rest/SecurityHealthAction.java | 12 +- .../security/rest/SecurityInfoAction.java | 12 +- .../security/rest/TenantInfoAction.java | 30 +- .../security/support/HostResolverMode.java | 13 +- .../security/user/ThreadContextUserInfo.java | 101 ++ .../api/RestApiPrivilegesEvaluatorTest.java | 3 +- .../security/filter/SecurityFilterTests.java | 6 +- .../PrivilegesEvaluatorUnitTest.java | 101 +- .../RestLayerPrivilegesEvaluatorTest.java | 166 ++-- .../resources/ResourceAccessHandlerTest.java | 5 +- 39 files changed, 2123 insertions(+), 1198 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/privileges/ConfigurableRoleMapperTest.java create mode 100644 src/main/java/org/opensearch/security/privileges/ConfigurableRoleMapper.java create mode 100644 src/main/java/org/opensearch/security/privileges/DashboardsMultiTenancyConfiguration.java create mode 100644 src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java create mode 100644 src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java create mode 100644 src/main/java/org/opensearch/security/privileges/RoleMapper.java create mode 100644 src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ef54cd0a89..45bf56afdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Replace AccessController and remove restriction on word Extension ([#5750](https://github.com/opensearch-project/security/pull/5750)) - Add security provider earlier in bootstrap process ([#5749](https://github.com/opensearch-project/security/pull/5749)) - [GRPC] Fix compilation errors from core protobuf version bump to 0.23.0 ([#5763](https://github.com/opensearch-project/security/pull/5763)) +- Modularized PrivilegesEvaluator ([#5791](https://github.com/opensearch-project/security/pull/5791)) ### Maintenance - Bump `org.junit.jupiter:junit-jupiter` from 5.13.4 to 5.14.1 ([#5678](https://github.com/opensearch-project/security/pull/5678), [#5764](https://github.com/opensearch-project/security/pull/5764)) diff --git a/src/integrationTest/java/org/opensearch/security/privileges/ConfigurableRoleMapperTest.java b/src/integrationTest/java/org/opensearch/security/privileges/ConfigurableRoleMapperTest.java new file mode 100644 index 0000000000..3266972d9a --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/ConfigurableRoleMapperTest.java @@ -0,0 +1,227 @@ +/* + * 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.privileges; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Suite; + +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.HostResolverMode; +import org.opensearch.security.user.User; + +import static org.junit.Assert.assertEquals; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ ConfigurableRoleMapperTest.ResolutionModeTest.class, ConfigurableRoleMapperTest.CompiledConfigurationTest.class, }) +public class ConfigurableRoleMapperTest { + + public static class ResolutionModeTest { + @Test + public void fromSettings_valid() { + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "both").build(); + + assertEquals(ConfigurableRoleMapper.ResolutionMode.BOTH, ConfigurableRoleMapper.ResolutionMode.fromSettings(settings)); + } + + @Test + public void fromSettings_invalid() { + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, "totally_invalid_value").build(); + + // invalid -> fallback to MAPPING_ONLY + assertEquals(ConfigurableRoleMapper.ResolutionMode.MAPPING_ONLY, ConfigurableRoleMapper.ResolutionMode.fromSettings(settings)); + } + } + + @RunWith(Parameterized.class) + public static class CompiledConfigurationTest { + + final static User USER_WITH_NO_ROLES = new User("user_no_roles"); + final static User USER_WITH_BACKEND_ROLES = new User("user_with_backend_roles").withRoles("backend_role_1", "backend_role_2"); + final static User USER_WITH_SECURITY_ROLES = new User("user_with_security_roles").withSecurityRoles( + Arrays.asList("effective_role_1", "effective_role_2") + ); + final static User USER_WITH_BOTH = new User("user_with_both").withRoles("backend_role_1", "backend_role_2") + .withSecurityRoles(Arrays.asList("effective_role_1", "effective_role_2")); + + final ConfigurableRoleMapper.ResolutionMode resolutionMode; + final User user; + final TransportAddress transportAddress; + + @Test + public void map_simple() throws Exception { + SecurityDynamicConfiguration roleMapping = SecurityDynamicConfiguration.fromYaml(""" + backend_to_effective: + backend_roles: + - backend_role_1 + """, CType.ROLESMAPPING); + + ConfigurableRoleMapper.CompiledConfiguration compiled = new ConfigurableRoleMapper.CompiledConfiguration( + roleMapping, + HostResolverMode.IP_HOSTNAME, + resolutionMode + ); + + ImmutableSet mappedRoles = compiled.map(user, transportAddress); + Set expectedRoles = new HashSet<>(user.getSecurityRoles()); + + if (resolutionMode == ConfigurableRoleMapper.ResolutionMode.MAPPING_ONLY + || resolutionMode == ConfigurableRoleMapper.ResolutionMode.BOTH) { + if (user.getRoles().contains("backend_role_1")) { + expectedRoles.add("backend_to_effective"); + } + } + + if (resolutionMode == ConfigurableRoleMapper.ResolutionMode.BACKENDROLES_ONLY + || resolutionMode == ConfigurableRoleMapper.ResolutionMode.BOTH) { + expectedRoles.addAll(user.getRoles()); + } + + assertEquals(expectedRoles, mappedRoles); + + } + + @Test + public void map_username() throws Exception { + SecurityDynamicConfiguration roleMapping = SecurityDynamicConfiguration.fromYaml(""" + user_to_effective: + users: + - user_no_roles + """, CType.ROLESMAPPING); + + ConfigurableRoleMapper.CompiledConfiguration compiled = new ConfigurableRoleMapper.CompiledConfiguration( + roleMapping, + HostResolverMode.IP_HOSTNAME, + resolutionMode + ); + + ImmutableSet mappedRoles = compiled.map(user, transportAddress); + Set expectedRoles = new HashSet<>(user.getSecurityRoles()); + + if (user == USER_WITH_NO_ROLES && resolutionMode != ConfigurableRoleMapper.ResolutionMode.BACKENDROLES_ONLY) { + expectedRoles.add("user_to_effective"); + } + + if (resolutionMode == ConfigurableRoleMapper.ResolutionMode.BACKENDROLES_ONLY + || resolutionMode == ConfigurableRoleMapper.ResolutionMode.BOTH) { + expectedRoles.addAll(user.getRoles()); + } + + assertEquals(expectedRoles, mappedRoles); + } + + @Test + public void map_host() throws Exception { + SecurityDynamicConfiguration roleMapping = SecurityDynamicConfiguration.fromYaml(""" + host_to_effective: + hosts: + - "127.0.0.1" + """, CType.ROLESMAPPING); + + ConfigurableRoleMapper.CompiledConfiguration compiled = new ConfigurableRoleMapper.CompiledConfiguration( + roleMapping, + HostResolverMode.IP_HOSTNAME_LOOKUP, + resolutionMode + ); + + ImmutableSet mappedRoles = compiled.map(user, transportAddress); + Set expectedRoles = new HashSet<>(user.getSecurityRoles()); + + if (resolutionMode != ConfigurableRoleMapper.ResolutionMode.BACKENDROLES_ONLY) { + expectedRoles.add("host_to_effective"); + } + + if (resolutionMode == ConfigurableRoleMapper.ResolutionMode.BACKENDROLES_ONLY + || resolutionMode == ConfigurableRoleMapper.ResolutionMode.BOTH) { + expectedRoles.addAll(user.getRoles()); + } + + assertEquals(expectedRoles, mappedRoles); + } + + @Test + public void map_and() throws Exception { + SecurityDynamicConfiguration roleMapping = SecurityDynamicConfiguration.fromYaml(""" + backend_to_effective: + and_backend_roles: + - backend_role_1 + - backend_role_2 + """, CType.ROLESMAPPING); + + ConfigurableRoleMapper.CompiledConfiguration compiled = new ConfigurableRoleMapper.CompiledConfiguration( + roleMapping, + HostResolverMode.IP_HOSTNAME, + resolutionMode + ); + + ImmutableSet mappedRoles = compiled.map(user, transportAddress); + Set expectedRoles = new HashSet<>(user.getSecurityRoles()); + + if (resolutionMode == ConfigurableRoleMapper.ResolutionMode.MAPPING_ONLY + || resolutionMode == ConfigurableRoleMapper.ResolutionMode.BOTH) { + if (user.getRoles().contains("backend_role_1") && user.getRoles().contains("backend_role_2")) { + expectedRoles.add("backend_to_effective"); + } + } + + if (resolutionMode == ConfigurableRoleMapper.ResolutionMode.BACKENDROLES_ONLY + || resolutionMode == ConfigurableRoleMapper.ResolutionMode.BOTH) { + expectedRoles.addAll(user.getRoles()); + } + + assertEquals(expectedRoles, mappedRoles); + + } + + public CompiledConfigurationTest( + ConfigurableRoleMapper.ResolutionMode resolutionMode, + User user, + TransportAddress transportAddress + ) { + this.resolutionMode = resolutionMode; + this.user = user; + this.transportAddress = transportAddress; + } + + @Parameterized.Parameters(name = "{0}, {1}") + public static Collection params() throws Exception { + List result = new ArrayList<>(); + + for (ConfigurableRoleMapper.ResolutionMode mode : ConfigurableRoleMapper.ResolutionMode.values()) { + for (User user : Arrays.asList(USER_WITH_NO_ROLES, USER_WITH_BACKEND_ROLES, USER_WITH_SECURITY_ROLES, USER_WITH_BOTH)) { + result.add( + new Object[] { mode, user, new TransportAddress(InetAddress.getByAddress(new byte[] { 127, 0, 0, 1 }), 9300) } + ); + + } + } + + return result; + } + + } + +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index c392d41a8f..904f9a56aa 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -142,6 +142,7 @@ import org.opensearch.security.auditlog.config.AuditConfig.Filter.FilterEntries; import org.opensearch.security.auditlog.impl.AuditLogImpl; import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.auth.RolesInjector; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl; import org.opensearch.security.configuration.AdminDNs; @@ -150,7 +151,6 @@ import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.configuration.DlsFlsRequestValve; import org.opensearch.security.configuration.DlsFlsValveImpl; -import org.opensearch.security.configuration.PrivilegesInterceptorImpl; import org.opensearch.security.configuration.SecurityConfigVersionHandler; import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper; import org.opensearch.security.dlic.rest.api.Endpoint; @@ -166,12 +166,13 @@ import org.opensearch.security.http.XFFResolver; import org.opensearch.security.identity.SecurePluginSubject; import org.opensearch.security.identity.SecurityTokenManager; +import org.opensearch.security.privileges.ConfigurableRoleMapper; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; -import org.opensearch.security.privileges.PrivilegesEvaluator; -import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.ResourceAccessEvaluator; import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; @@ -270,7 +271,8 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private boolean sslCertReloadEnabled; private volatile SecurityInterceptor si; - private volatile PrivilegesEvaluator evaluator; + private volatile PrivilegesConfiguration privilegesConfiguration; + private volatile RoleMapper roleMapper; private volatile UserService userService; private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; private volatile ConfigurationRepository cr; @@ -623,19 +625,25 @@ public List getRestHandlers( // FGAC enabled == not sslOnly if (!SSLConfig.isSslOnlyMode()) { handlers.add( - new SecurityInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool)) + new SecurityInfoAction( + settings, + restController, + Objects.requireNonNull(privilegesConfiguration), + Objects.requireNonNull(threadPool) + ) ); handlers.add( new SecurityHealthAction( settings, restController, Objects.requireNonNull(backendRegistry), - Objects.requireNonNull(evaluator) + Objects.requireNonNull(privilegesConfiguration) ) ); handlers.add( new DashboardsInfoAction( - Objects.requireNonNull(evaluator), + Objects.requireNonNull(privilegesConfiguration), + Objects.requireNonNull(cr), Objects.requireNonNull(threadPool), resourceSharingEnabledSetting ) @@ -644,7 +652,7 @@ public List getRestHandlers( new TenantInfoAction( settings, restController, - Objects.requireNonNull(evaluator), + Objects.requireNonNull(privilegesConfiguration), Objects.requireNonNull(threadPool), Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), @@ -682,7 +690,8 @@ public List getRestHandlers( cr, cs, principalExtractor, - evaluator, + roleMapper, + privilegesConfiguration, threadPool, Objects.requireNonNull(auditLog), sslSettingsManager, @@ -753,7 +762,8 @@ public void onIndexModule(IndexModule indexModule) { cs, auditLog, ciol, - evaluator, + privilegesConfiguration, + roleMapper, dlsFlsValve::getCurrentConfig, dlsFlsBaseContext ) @@ -1139,15 +1149,11 @@ public Collection createComponents( UserFactory userFactory = new UserFactory.Caching(settings); - final PrivilegesInterceptor privilegesInterceptor; - namedXContentRegistry.set(xContentRegistry); if (SSLConfig.isSslOnlyMode()) { auditLog = new NullAuditLog(); - privilegesInterceptor = new PrivilegesInterceptor(resolver, clusterService, localClient, threadPool); } else { auditLog = new AuditLogImpl(settings, configPath, localClient, threadPool, resolver, clusterService, environment, userFactory); - privilegesInterceptor = new PrivilegesInterceptorImpl(resolver, clusterService, localClient, threadPool); } sslExceptionHandler = new AuditLogSslExceptionHandler(auditLog); @@ -1169,21 +1175,29 @@ public Collection createComponents( final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); rsIndexHandler = new ResourceSharingIndexHandler(localClient, threadPool, resourcePluginInfo); - evaluator = new PrivilegesEvaluator( + + RoleMapper roleMapper = new RolesInjector.InjectedRoleMapper( + new ConfigurableRoleMapper(cr, settings), + threadPool.getThreadContext() + ); + this.roleMapper = roleMapper; + + PrivilegesConfiguration privilegesConfiguration = new PrivilegesConfiguration( + cr, clusterService, clusterService::state, + localClient, + roleMapper, threadPool, - threadPool.getThreadContext(), - cr, resolver, auditLog, settings, - privilegesInterceptor, - cih, + cih::getReasonForUnavailability, irr ); + this.privilegesConfiguration = privilegesConfiguration; - dlsFlsBaseContext = new DlsFlsBaseContext(evaluator, threadPool.getThreadContext(), adminDns); + dlsFlsBaseContext = new DlsFlsBaseContext(privilegesConfiguration, threadPool.getThreadContext(), adminDns); if (SSLConfig.isSslOnlyMode()) { dlsFlsValve = new DlsFlsRequestValve.NoopDlsFlsRequestValve(); @@ -1203,7 +1217,7 @@ public Collection createComponents( cr.subscribeOnChange(configMap -> { ((DlsFlsValveImpl) dlsFlsValve).updateConfiguration(cr.getConfiguration(CType.ROLES)); }); } - resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns, evaluator, resourcePluginInfo); + resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns, resourcePluginInfo); // Assign resource sharing client to each extension // Using the non-gated client (i.e. no additional permissions required) @@ -1228,7 +1242,7 @@ public Collection createComponents( sf = new SecurityFilter( settings, - evaluator, + privilegesConfiguration, adminDns, dlsFlsValve, auditLog, @@ -1249,7 +1263,7 @@ public Collection createComponents( principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass); } - restLayerEvaluator = new RestLayerPrivilegesEvaluator(evaluator); + restLayerEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); securityRestHandler = new SecurityRestFilter( backendRegistry, @@ -1266,7 +1280,6 @@ public Collection createComponents( dcf.registerDCFListener(compatConfig); dcf.registerDCFListener(irr); dcf.registerDCFListener(xffResolver); - dcf.registerDCFListener(evaluator); dcf.registerDCFListener(securityRestHandler); dcf.registerDCFListener(tokenManager); if (!(auditLog instanceof NullAuditLog)) { @@ -1306,7 +1319,7 @@ public Collection createComponents( components.add(cr); components.add(xffResolver); components.add(backendRegistry); - components.add(evaluator); + components.add(privilegesConfiguration); components.add(restLayerEvaluator); components.add(si); components.add(dcf); @@ -2408,7 +2421,7 @@ public PluginSubject getPluginSubject(Plugin plugin) { } } pluginPermissions.getCluster_permissions().add(BulkAction.NAME); - evaluator.updatePluginToActionPrivileges(pluginPrincipal, pluginPermissions); + privilegesConfiguration.updatePluginToActionPrivileges(pluginPrincipal, pluginPermissions); } return subject; } diff --git a/src/main/java/org/opensearch/security/auth/RolesInjector.java b/src/main/java/org/opensearch/security/auth/RolesInjector.java index 42afc77ad2..02116a53ad 100644 --- a/src/main/java/org/opensearch/security/auth/RolesInjector.java +++ b/src/main/java/org/opensearch/security/auth/RolesInjector.java @@ -15,6 +15,8 @@ package org.opensearch.security.auth; +import java.util.Arrays; +import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -23,8 +25,12 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; @@ -90,6 +96,52 @@ private void addUser(final User user, final ThreadPool threadPool) { if (ctx.getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER) == null) { ctx.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, new UserSubjectImpl(threadPool, user)); } + } + + /** + * For users injected by this class, no role mapping shall be performed. This RoleMapper checks whether there + * is an injected user (by header) and skips default role mapping (realized by the delegate) if so. + */ + public static class InjectedRoleMapper implements RoleMapper { + + private final ThreadContext threadContext; + private final RoleMapper defaultRoleMapper; + + public InjectedRoleMapper(RoleMapper defaultRoleMapper, ThreadContext threadContext) { + this.threadContext = threadContext; + this.defaultRoleMapper = defaultRoleMapper; + } + @Override + public ImmutableSet map(User user, TransportAddress caller) { + ImmutableSet mappedRoles; + + if (threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES) != null) { + // Just return the security roles, like they were initialized in the injectUserAndRoles() method above + mappedRoles = user.getSecurityRoles(); + } else { + // No injected user => use default role mapping + mappedRoles = defaultRoleMapper.map(user, caller); + } + + String injectedRolesValidationString = threadContext.getTransient( + ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION + ); + if (injectedRolesValidationString != null) { + // Moved from + // https://github.com/opensearch-project/security/blob/d29095f26dba1a26308c69b608dc926bd40c0f52/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java#L406 + // See also https://github.com/opensearch-project/security/pull/1367 + HashSet injectedRolesValidationSet = new HashSet<>(Arrays.asList(injectedRolesValidationString.split(","))); + if (!mappedRoles.containsAll(injectedRolesValidationSet)) { + throw new OpenSearchSecurityException( + String.format("No mapping for %s on roles %s", user, injectedRolesValidationSet), + RestStatus.FORBIDDEN + ); + } + mappedRoles = ImmutableSet.copyOf(injectedRolesValidationSet); + } + + return mappedRoles; + } } } diff --git a/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java b/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java index 7ccbeb6d14..e4ccb026f4 100644 --- a/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java +++ b/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java @@ -101,4 +101,12 @@ public Boolean hasClusterManager() { } return false; } + + public String getReasonForUnavailability() { + if (!hasClusterManager()) { + return CLUSTER_MANAGER_NOT_PRESENT; + } else { + return null; + } + } } diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index bba86ada1e..3a89d8cf8c 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -89,7 +89,7 @@ import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; -import static org.opensearch.security.privileges.PrivilegesEvaluator.isClusterPerm; +import static org.opensearch.security.privileges.PrivilegesEvaluatorImpl.isClusterPerm; public class DlsFlsValveImpl implements DlsFlsRequestValve { diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index 6a64eada3e..e362c7a8eb 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -13,6 +13,7 @@ import java.util.Map; import java.util.Set; +import java.util.function.Supplier; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -45,12 +46,12 @@ import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; import org.opensearch.security.privileges.DocumentAllowList; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesInterceptor; import org.opensearch.security.privileges.TenantPrivileges; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; @@ -77,13 +78,20 @@ public class PrivilegesInterceptorImpl extends PrivilegesInterceptor { protected final Logger log = LogManager.getLogger(this.getClass()); + private final Supplier tenantPrivilegesSupplier; + private final Supplier multiTenancyConfigurationSupplier; + public PrivilegesInterceptorImpl( IndexNameExpressionResolver resolver, ClusterService clusterService, Client client, - ThreadPool threadPool + ThreadPool threadPool, + Supplier tenantPrivilegesSupplier, + Supplier multiTenancyConfigurationSupplier ) { super(resolver, clusterService, client, threadPool); + this.tenantPrivilegesSupplier = tenantPrivilegesSupplier; + this.multiTenancyConfigurationSupplier = multiTenancyConfigurationSupplier; } /** @@ -97,25 +105,26 @@ public ReplaceResult replaceDashboardsIndex( final ActionRequest request, final String action, final User user, - final DynamicConfigModel config, final Resolved requestedResolved, - final PrivilegesEvaluationContext context, - final TenantPrivileges tenantPrivileges + final PrivilegesEvaluationContext context ) { + DashboardsMultiTenancyConfiguration config = this.multiTenancyConfigurationSupplier.get(); - final boolean enabled = config.isDashboardsMultitenancyEnabled();// config.dynamic.kibana.multitenancy_enabled; + final boolean enabled = config.multitenancyEnabled();// config.dynamic.kibana.multitenancy_enabled; if (!enabled) { return CONTINUE_EVALUATION_REPLACE_RESULT; } + TenantPrivileges tenantPrivileges = this.tenantPrivilegesSupplier.get(); + // next two lines needs to be retrieved from configuration - final String dashboardsServerUsername = config.getDashboardsServerUsername();// config.dynamic.kibana.server_username; - final String dashboardsIndexName = config.getDashboardsIndexname();// config.dynamic.kibana.index; + final String dashboardsServerUsername = config.dashboardsServerUsername();// config.dynamic.kibana.server_username; + final String dashboardsIndexName = config.dashboardsIndex();// config.dynamic.kibana.index; String requestedTenant = user.getRequestedTenant(); if (USER_TENANT.equals(requestedTenant)) { - final boolean private_tenant_enabled = config.isDashboardsPrivateTenantEnabled(); + final boolean private_tenant_enabled = config.privateTenantEnabled(); if (!private_tenant_enabled) { return ACCESS_DENIED_REPLACE_RESULT; } diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index 2cabfbd1a4..96c1616183 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -37,9 +37,10 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.privileges.DocumentAllowList; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluationException; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.privileges.dlsfls.DlsFlsProcessedConfig; import org.opensearch.security.privileges.dlsfls.DlsRestriction; @@ -69,11 +70,12 @@ public SecurityFlsDlsIndexSearcherWrapper( final ClusterService clusterService, final AuditLog auditlog, final ComplianceIndexingOperationListener ciol, - final PrivilegesEvaluator evaluator, + final PrivilegesConfiguration privilegesConfiguration, + final RoleMapper roleMapper, final Supplier dlsFlsProcessedConfigSupplier, final DlsFlsBaseContext dlsFlsBaseContext ) { - super(indexService, settings, adminDNs, evaluator); + super(indexService, settings, adminDNs, privilegesConfiguration, roleMapper); Set metadataFieldsCopy; if (indexService.getMetadata().getState() == IndexMetadata.State.CLOSE) { if (log.isDebugEnabled()) { diff --git a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java index 447f134877..cb3e858284 100644 --- a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java @@ -40,18 +40,16 @@ import org.opensearch.core.index.Index; import org.opensearch.index.IndexService; import org.opensearch.indices.SystemIndexRegistry; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; -import org.greenrobot.eventbus.Subscribe; - public class SystemIndexSearcherWrapper implements CheckedFunction { protected final Logger log = LogManager.getLogger(this.getClass()); @@ -59,8 +57,8 @@ public class SystemIndexSearcherWrapper implements CheckedFunction securityRoles = evaluator.mapRoles(user, caller); + final Set securityRoles = roleMapper.map(user, caller); if (allowedRolesMatcher.matchAny(securityRoles)) { return true; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java index c6a01ecad9..a74983733e 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java @@ -128,7 +128,9 @@ private void userAccount( final TransportAddress remoteAddress, final SecurityDynamicConfiguration configuration ) { - PrivilegesEvaluationContext context = securityApiDependencies.privilegesEvaluator().createContext(user, null); + PrivilegesEvaluationContext context = securityApiDependencies.privilegesConfiguration() + .privilegesEvaluator() + .createContext(user, null); ok( channel, (builder, params) -> builder.startObject() @@ -139,7 +141,7 @@ private void userAccount( .field("user_requested_tenant", user.getRequestedTenant()) .field("backend_roles", user.getRoles()) .field("custom_attribute_names", user.getCustomAttributesMap().keySet()) - .field("tenants", securityApiDependencies.privilegesEvaluator().tenantPrivileges().tenantMap(context)) + .field("tenants", securityApiDependencies.privilegesConfiguration().tenantPrivileges().tenantMap(context)) .field("roles", context.getMappedRoles()) .endObject() ); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java index db67e9b979..06f407f715 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java @@ -35,7 +35,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -60,7 +60,7 @@ public class PermissionsInfoAction extends BaseRestHandler { private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator; private final ThreadPool threadPool; - private final PrivilegesEvaluator privilegesEvaluator; + private final RoleMapper roleMapper; private final ConfigurationRepository configurationRepository; protected PermissionsInfoAction( @@ -72,17 +72,17 @@ protected PermissionsInfoAction( final ConfigurationRepository configurationRepository, final ClusterService cs, final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator privilegesEvaluator, + final RoleMapper roleMapper, ThreadPool threadPool, AuditLog auditLog ) { super(); this.threadPool = threadPool; - this.privilegesEvaluator = privilegesEvaluator; + this.roleMapper = roleMapper; this.restApiPrivilegesEvaluator = new RestApiPrivilegesEvaluator( settings, adminDNs, - privilegesEvaluator, + roleMapper, principalExtractor, configPath, threadPool @@ -129,7 +129,7 @@ public void accept(RestChannel channel) throws Exception { final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); final TransportAddress remoteAddress = threadPool.getThreadContext() .getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); + Set userRoles = roleMapper.map(user, remoteAddress); Boolean hasApiAccess = restApiPrivilegesEvaluator.currentUserHasRestApiAccess(userRoles); Map> disabledEndpoints = restApiPrivilegesEvaluator.getDisabledEndpointsForCurrentUser( user.getName(), diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java index 2f66797076..bcff258fa6 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java @@ -23,7 +23,8 @@ import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.dlic.rest.support.Utils; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.support.WildcardMatcher; @@ -77,7 +78,7 @@ default String build() { private final ThreadContext threadContext; - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final AdminDNs adminDNs; @@ -85,12 +86,12 @@ default String build() { public RestApiAdminPrivilegesEvaluator( final ThreadContext threadContext, - final PrivilegesEvaluator privilegesEvaluator, + final PrivilegesConfiguration privilegesConfiguration, final AdminDNs adminDNs, final boolean restapiAdminEnabled ) { this.threadContext = threadContext; - this.privilegesEvaluator = privilegesEvaluator; + this.privilegesConfiguration = privilegesConfiguration; this.adminDNs = adminDNs; this.restapiAdminEnabled = restapiAdminEnabled; } @@ -108,11 +109,10 @@ public boolean isCurrentUserAdminFor(final Endpoint endpoint, final String actio return false; } final String permission = ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(action); - final boolean hasAccess = privilegesEvaluator.hasRestAdminPermissions( - userAndRemoteAddress.getLeft(), - userAndRemoteAddress.getRight(), - permission - ); + PrivilegesEvaluationContext context = privilegesConfiguration.privilegesEvaluator() + .createContext(userAndRemoteAddress.getLeft(), permission); + final boolean hasAccess = context.getActionPrivileges().hasExplicitClusterPrivilege(context, permission).isAllowed(); + if (logger.isDebugEnabled()) { logger.debug( "User {} with permission {} {} access to endpoint {}", diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java index f1a336986b..5a3b66a561 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java @@ -37,7 +37,7 @@ import org.opensearch.security.dlic.rest.support.Utils; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityRequestFactory; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.SSLRequestHelper; import org.opensearch.security.support.ConfigConstants; @@ -50,7 +50,7 @@ public class RestApiPrivilegesEvaluator { protected final Logger logger = LogManager.getLogger(this.getClass()); private final AdminDNs adminDNs; - private final PrivilegesEvaluator privilegesEvaluator; + private final RoleMapper roleMapper; private final PrincipalExtractor principalExtractor; private final Path configPath; private final ThreadPool threadPool; @@ -77,14 +77,14 @@ public class RestApiPrivilegesEvaluator { public RestApiPrivilegesEvaluator( final Settings settings, final AdminDNs adminDNs, - final PrivilegesEvaluator privilegesEvaluator, + final RoleMapper roleMapper, final PrincipalExtractor principalExtractor, final Path configPath, ThreadPool threadPool ) { this.adminDNs = adminDNs; - this.privilegesEvaluator = privilegesEvaluator; + this.roleMapper = roleMapper; this.principalExtractor = principalExtractor; this.configPath = configPath; this.threadPool = threadPool; @@ -376,7 +376,7 @@ private String checkRoleBasedAccessPermissions(RestRequest request, Endpoint end final TransportAddress remoteAddress = userAndRemoteAddress.getRight(); // map the users Security roles - Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); + Set userRoles = roleMapper.map(user, remoteAddress); // check if user has any role that grants access if (currentUserHasRestApiAccess(userRoles)) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java index 498230423f..cb985899b1 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java @@ -15,7 +15,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.support.ConfigConstants; public class SecurityApiDependencies { @@ -26,12 +26,12 @@ public class SecurityApiDependencies { private final AuditLog auditLog; private final Settings settings; - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; public SecurityApiDependencies( final AdminDNs adminDNs, final ConfigurationRepository configurationRepository, - final PrivilegesEvaluator privilegesEvaluator, + final PrivilegesConfiguration privilegesConfiguration, final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator, final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator, final AuditLog auditLog, @@ -39,7 +39,7 @@ public SecurityApiDependencies( ) { this.adminDNs = adminDNs; this.configurationRepository = configurationRepository; - this.privilegesEvaluator = privilegesEvaluator; + this.privilegesConfiguration = privilegesConfiguration; this.restApiPrivilegesEvaluator = restApiPrivilegesEvaluator; this.restApiAdminPrivilegesEvaluator = restApiAdminPrivilegesEvaluator; this.auditLog = auditLog; @@ -50,8 +50,8 @@ public AdminDNs adminDNs() { return adminDNs; } - public PrivilegesEvaluator privilegesEvaluator() { - return privilegesEvaluator; + public PrivilegesConfiguration privilegesConfiguration() { + return privilegesConfiguration; } public ConfigurationRepository configurationRepository() { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index 957e693bc3..9a1314b837 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -26,7 +26,8 @@ import org.opensearch.security.configuration.SecurityConfigVersionHandler; import org.opensearch.security.configuration.SecurityConfigVersionsLoader; import org.opensearch.security.hasher.PasswordHasher; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.resources.ResourcePluginInfo; import org.opensearch.security.resources.ResourceSharingIndexHandler; import org.opensearch.security.resources.api.migrate.MigrateResourceSharingInfoApiAction; @@ -49,7 +50,8 @@ public static Collection getHandler( final ConfigurationRepository configurationRepository, final ClusterService clusterService, final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, + final RoleMapper roleMapper, + final PrivilegesConfiguration privilegesConfiguration, final ThreadPool threadPool, final AuditLog auditLog, final SslSettingsManager sslSettingsManager, @@ -62,11 +64,11 @@ public static Collection getHandler( final var securityApiDependencies = new SecurityApiDependencies( adminDns, configurationRepository, - evaluator, - new RestApiPrivilegesEvaluator(settings, adminDns, evaluator, principalExtractor, configPath, threadPool), + privilegesConfiguration, + new RestApiPrivilegesEvaluator(settings, adminDns, roleMapper, principalExtractor, configPath, threadPool), new RestApiAdminPrivilegesEvaluator( threadPool.getThreadContext(), - evaluator, + privilegesConfiguration, adminDns, settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false) ), @@ -91,7 +93,7 @@ public static Collection getHandler( configurationRepository, clusterService, principalExtractor, - evaluator, + roleMapper, threadPool, auditLog ), diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index a23db341fd..f5424fd2ad 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -86,6 +86,7 @@ import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.configuration.DlsFlsRequestValve; import org.opensearch.security.http.XFFResolver; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; @@ -96,6 +97,7 @@ import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.SourceFieldsContext; import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.ThreadContextUserInfo; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; @@ -107,7 +109,7 @@ public class SecurityFilter implements ActionFilter { protected final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator evalp; + private final PrivilegesConfiguration privilegesConfiguration; private final AdminDNs adminDns; private final DlsFlsRequestValve dlsFlsValve; private final AuditLog auditLog; @@ -120,10 +122,11 @@ public class SecurityFilter implements ActionFilter { private final RolesInjector rolesInjector; private final UserInjector userInjector; private final ResourceAccessEvaluator resourceAccessEvaluator; + private final ThreadContextUserInfo threadContextUserInfo; public SecurityFilter( final Settings settings, - final PrivilegesEvaluator evalp, + PrivilegesConfiguration privilegesConfiguration, final AdminDNs adminDns, DlsFlsRequestValve dlsFlsValve, AuditLog auditLog, @@ -135,7 +138,7 @@ public SecurityFilter( final XFFResolver xffResolver, ResourceAccessEvaluator resourceAccessEvaluator ) { - this.evalp = evalp; + this.privilegesConfiguration = privilegesConfiguration; this.adminDns = adminDns; this.dlsFlsValve = dlsFlsValve; this.auditLog = auditLog; @@ -150,6 +153,13 @@ public SecurityFilter( this.rolesInjector = new RolesInjector(auditLog); this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver); this.resourceAccessEvaluator = resourceAccessEvaluator; + this.threadContextUserInfo = new ThreadContextUserInfo( + threadPool.getThreadContext(), + privilegesConfiguration, + cs.getClusterSettings(), + settings + ); + log.info("{} indices are made immutable.", immutableIndicesMatcher); } @@ -174,7 +184,7 @@ public void app ) { try (StoredContext ctx = threadPool.getThreadContext().newStoredContext(true)) { org.apache.logging.log4j.ThreadContext.clearAll(); - apply0(task, action, request, listener, chain); + apply0(task, action, request, actionRequestMetadata, listener, chain); } } @@ -186,6 +196,7 @@ private void ap Task task, final String action, Request request, + ActionRequestMetadata actionRequestMetadata, ActionListener listener, ActionFilterChain chain ) { @@ -379,25 +390,15 @@ private void ap } } - final PrivilegesEvaluator eval = evalp; - - if (!eval.isInitialized()) { - StringBuilder error = new StringBuilder("OpenSearch Security not initialized for "); - error.append(action); - if (!clusterInfoHolder.hasClusterManager()) { - error.append(String.format(". %s", ClusterInfoHolder.CLUSTER_MANAGER_NOT_PRESENT)); - } - - log.error(error.toString()); - listener.onFailure(new OpenSearchSecurityException(error.toString(), RestStatus.SERVICE_UNAVAILABLE)); - return; - } + final PrivilegesEvaluator eval = this.privilegesConfiguration.privilegesEvaluator(); if (log.isTraceEnabled()) { log.trace("Evaluate permissions for user: {}", user.getName()); } - PrivilegesEvaluationContext context = eval.createContext(user, action, request, task, injectedRoles); + PrivilegesEvaluationContext context = eval.createContext(user, action, request, actionRequestMetadata, task); + this.threadContextUserInfo.setUserInfoInThreadContext(context); + User finalUser = user; Consumer handleUnauthorized = response -> { auditLog.logMissingPrivileges(action, request, task); diff --git a/src/main/java/org/opensearch/security/privileges/ConfigurableRoleMapper.java b/src/main/java/org/opensearch/security/privileges/ConfigurableRoleMapper.java new file mode 100644 index 0000000000..b6787058a2 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/ConfigurableRoleMapper.java @@ -0,0 +1,249 @@ +/* + * 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.privileges; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ListMultimap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.HostResolverMode; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; + +/** + * A RoleMapper implementation that automatically picks up changes from the role mapping configuration in the configuration index. + */ +public class ConfigurableRoleMapper implements RoleMapper { + private final static Logger log = LogManager.getLogger(ConfigurableRoleMapper.class); + + private final AtomicReference activeConfiguration = new AtomicReference<>(); + + public ConfigurableRoleMapper(ConfigurationRepository configurationRepository, ResolutionMode resolutionMode) { + if (configurationRepository != null) { + configurationRepository.subscribeOnChange(configMap -> { + HostResolverMode hostResolverMode = getHostResolverMode(configurationRepository.getConfiguration(CType.CONFIG)); + SecurityDynamicConfiguration rawRoleMappingConfiguration = configurationRepository.getConfiguration( + CType.ROLESMAPPING + ); + if (rawRoleMappingConfiguration == null) { + rawRoleMappingConfiguration = SecurityDynamicConfiguration.empty(CType.ROLESMAPPING); + } + + this.activeConfiguration.set(new CompiledConfiguration(rawRoleMappingConfiguration, hostResolverMode, resolutionMode)); + }); + } + } + + public ConfigurableRoleMapper(ConfigurationRepository configurationRepository, Settings settings) { + this(configurationRepository, ResolutionMode.fromSettings(settings)); + } + + @Override + public ImmutableSet map(User user, TransportAddress caller) { + CompiledConfiguration activeConfiguration = this.activeConfiguration.get(); + + if (activeConfiguration != null) { + return activeConfiguration.map(user, caller); + } else { + return ImmutableSet.of(); + } + } + + /** + * Determines which roles are used in the final set of effective roles returned by the map() method. + * + * The setting is sourced from the plugins.secutiry.roles_mapping_resolution setting. + */ + enum ResolutionMode { + /** + * Include only the target roles from the role mapping configuration. + */ + MAPPING_ONLY, + + /** + * Include only the backend roles. This effectively disables the role mapping process. + */ + BACKENDROLES_ONLY, + + /** + * Include the union of the target roles and the source backend roles. + */ + BOTH; + + static ResolutionMode fromSettings(Settings settings) { + + try { + return ResolutionMode.valueOf( + settings.get(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, ResolutionMode.MAPPING_ONLY.toString()).toUpperCase() + ); + } catch (Exception e) { + log.error("Cannot apply roles mapping resolution", e); + return ResolutionMode.MAPPING_ONLY; + } + } + } + + static HostResolverMode getHostResolverMode(SecurityDynamicConfiguration configConfig) { + final HostResolverMode defaultValue = HostResolverMode.IP_HOSTNAME; + + if (configConfig == null) { + return defaultValue; + } + + ConfigV7 config = configConfig.getCEntry(CType.CONFIG.name()); + if (config == null || config.dynamic == null) { + return defaultValue; + } + return HostResolverMode.fromConfig(config.dynamic.hosts_resolver_mode); + } + + /** + * Moved from https://github.com/opensearch-project/security/blob/d29095f26dba1a26308c69b608dc926bd40c0f52/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java + */ + static class CompiledConfiguration implements RoleMapper { + + private final ResolutionMode resolutionMode; + private final HostResolverMode hostResolverMode; + + private ListMultimap users; + private ListMultimap, String> abars; + private ListMultimap bars; + private ListMultimap hosts; + + private List userMatchers; + private List barMatchers; + private List hostMatchers; + + CompiledConfiguration( + SecurityDynamicConfiguration rolemappings, + HostResolverMode hostResolverMode, + ResolutionMode resolutionMode + ) { + + this.hostResolverMode = hostResolverMode; + this.resolutionMode = resolutionMode; + + users = ArrayListMultimap.create(); + abars = ArrayListMultimap.create(); + bars = ArrayListMultimap.create(); + hosts = ArrayListMultimap.create(); + + for (final Map.Entry roleMap : rolemappings.getCEntries().entrySet()) { + final String roleMapKey = roleMap.getKey(); + final RoleMappingsV7 roleMapValue = roleMap.getValue(); + + for (String u : roleMapValue.getUsers()) { + users.put(u, roleMapKey); + } + + final Set abar = new HashSet<>(roleMapValue.getAnd_backend_roles()); + + if (!abar.isEmpty()) { + abars.put(WildcardMatcher.matchers(abar), roleMapKey); + } + + for (String bar : roleMapValue.getBackend_roles()) { + bars.put(bar, roleMapKey); + } + + for (String host : roleMapValue.getHosts()) { + hosts.put(host, roleMapKey); + } + } + + userMatchers = WildcardMatcher.matchers(users.keySet()); + barMatchers = WildcardMatcher.matchers(bars.keySet()); + hostMatchers = WildcardMatcher.matchers(hosts.keySet()); + + } + + @Override + public ImmutableSet map(final User user, final TransportAddress caller) { + + if (user == null) { + return ImmutableSet.of(); + } + + ImmutableSet.Builder result = ImmutableSet.builderWithExpectedSize( + user.getSecurityRoles().size() + user.getRoles().size() + ); + + result.addAll(user.getSecurityRoles()); + + if (resolutionMode == ResolutionMode.BOTH || resolutionMode == ResolutionMode.BACKENDROLES_ONLY) { + result.addAll(user.getRoles()); + } + + if (((resolutionMode == ResolutionMode.BOTH || resolutionMode == ResolutionMode.MAPPING_ONLY))) { + + for (String p : WildcardMatcher.getAllMatchingPatterns(userMatchers, user.getName())) { + result.addAll(users.get(p)); + } + for (String p : WildcardMatcher.getAllMatchingPatterns(barMatchers, user.getRoles())) { + result.addAll(bars.get(p)); + } + + for (List patterns : abars.keySet()) { + if (patterns.stream().allMatch(p -> p.matchAny(user.getRoles()))) { + result.addAll(abars.get(patterns)); + } + } + + if (caller != null) { + // IPV4 or IPv6 (compressed and without scope identifiers) + final String ipAddress = caller.getAddress(); + + for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, ipAddress)) { + result.addAll(hosts.get(p)); + } + + if (caller.address() != null + && (hostResolverMode == HostResolverMode.IP_HOSTNAME || hostResolverMode == HostResolverMode.IP_HOSTNAME_LOOKUP)) { + final String hostName = caller.address().getHostString(); + + for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, hostName)) { + result.addAll(hosts.get(p)); + } + } + + if (caller.address() != null && hostResolverMode == HostResolverMode.IP_HOSTNAME_LOOKUP) { + + final String resolvedHostName = caller.address().getHostName(); + + for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, resolvedHostName)) { + result.addAll(hosts.get(p)); + } + } + } + } + + return result.build(); + } + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/DashboardsMultiTenancyConfiguration.java b/src/main/java/org/opensearch/security/privileges/DashboardsMultiTenancyConfiguration.java new file mode 100644 index 0000000000..12bbb739f4 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/DashboardsMultiTenancyConfiguration.java @@ -0,0 +1,77 @@ +/* + * 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.privileges; + +import org.opensearch.security.securityconf.impl.v7.ConfigV7; + +/** + * Provides access to the current configuration related to Dashboards multi-tenancy. + *

+ * This replaces methods from PrivilegesEvaluator: https://github.com/opensearch-project/security/blob/062ea716d10240cc50d01735f457523a61393a59/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java#L690-L719 + */ +public class DashboardsMultiTenancyConfiguration { + public static final DashboardsMultiTenancyConfiguration DEFAULT = new DashboardsMultiTenancyConfiguration(new ConfigV7.Kibana()); + + private final boolean multitenancyEnabled; + private final boolean privateTenantEnabled; + private final String defaultTenant; + private final String index; + private final String serverUsername; + private final String role; + + public DashboardsMultiTenancyConfiguration(ConfigV7.Kibana dashboardsConfig) { + this.multitenancyEnabled = dashboardsConfig.multitenancy_enabled; + this.privateTenantEnabled = dashboardsConfig.private_tenant_enabled; + this.defaultTenant = dashboardsConfig.default_tenant; + this.index = dashboardsConfig.index; + this.serverUsername = dashboardsConfig.server_username; + this.role = dashboardsConfig.opendistro_role; + } + + public DashboardsMultiTenancyConfiguration(ConfigV7 generalConfig) { + this(dashboardsConfig(generalConfig)); + } + + public boolean multitenancyEnabled() { + return multitenancyEnabled; + } + + public boolean privateTenantEnabled() { + return privateTenantEnabled; + } + + public String dashboardsDefaultTenant() { + return defaultTenant; + } + + public String dashboardsIndex() { + return index; + } + + public String dashboardsServerUsername() { + return serverUsername; + } + + public String dashboardsOpenSearchRole() { + return role; + } + + private static ConfigV7.Kibana dashboardsConfig(ConfigV7 generalConfig) { + if (generalConfig != null && generalConfig.dynamic != null && generalConfig.dynamic.kibana != null) { + return generalConfig.dynamic.kibana; + } else { + // Fallback to defaults + return new ConfigV7.Kibana(); + } + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java b/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java new file mode 100644 index 0000000000..6a66b226ab --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesConfiguration.java @@ -0,0 +1,218 @@ +/* + * 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.privileges; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.configuration.PrivilegesInterceptorImpl; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.securityconf.DynamicConfigFactory; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.securityconf.impl.v7.TenantV7; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.Client; + +/** + * This class manages and gives access to various additional classes which are derived from privileges related configuration in + * the security plugin. + *

+ * This is especially: + *

    + *
  • The current PrivilegesEvaluator instance
  • + *
  • The current Dashboards multi tenancy configuration
  • + *
  • The current action groups configuration
  • + *
+ * This class also manages updates to the different configuration objects. + *

+ * Historically, most of this information has been located directly in PrivilegesEvaluator instances. To concentrate + * the purpose of PrivilegesEvaluator to just action based privilege evaluation, the information was distributed amongst + * several classes. + */ +public class PrivilegesConfiguration { + private final static Logger log = LogManager.getLogger(PrivilegesConfiguration.class); + + private final AtomicReference tenantPrivileges = new AtomicReference<>(TenantPrivileges.EMPTY); + private final AtomicReference privilegesEvaluator; + private final AtomicReference actionGroups = new AtomicReference<>(FlattenedActionGroups.EMPTY); + private final Map pluginIdToRolePrivileges = new HashMap<>(); + private final AtomicReference multiTenancyConfiguration = new AtomicReference<>( + DashboardsMultiTenancyConfiguration.DEFAULT + ); + private final PrivilegesInterceptorImpl privilegesInterceptor; + + /** + * The pure static action groups should be ONLY used by action privileges for plugins; only those cannot and should + * not have knowledge of any action groups defined in the dynamic configuration. All other functionality should + * use the action groups derived from the dynamic configuration (which is always computed on the fly on + * configuration updates). + */ + private final FlattenedActionGroups staticActionGroups; + + public PrivilegesConfiguration( + ConfigurationRepository configurationRepository, + ClusterService clusterService, + Supplier clusterStateSupplier, + Client client, + RoleMapper roleMapper, + ThreadPool threadPool, + IndexNameExpressionResolver resolver, + AuditLog auditLog, + Settings settings, + Supplier unavailablityReasonSupplier, + IndexResolverReplacer indexResolverReplacer + ) { + + this.privilegesEvaluator = new AtomicReference<>(new PrivilegesEvaluator.NotInitialized(unavailablityReasonSupplier)); + this.privilegesInterceptor = new PrivilegesInterceptorImpl( + resolver, + clusterService, + client, + threadPool, + this.tenantPrivileges::get, + this.multiTenancyConfiguration::get + ); + this.staticActionGroups = buildStaticActionGroups(); + + if (configurationRepository != null) { + configurationRepository.subscribeOnChange(configMap -> { + SecurityDynamicConfiguration actionGroupsConfiguration = configurationRepository.getConfiguration( + CType.ACTIONGROUPS + ); + SecurityDynamicConfiguration rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES) + .withStaticConfig(); + SecurityDynamicConfiguration tenantConfiguration = configurationRepository.getConfiguration(CType.TENANTS) + .withStaticConfig(); + ConfigV7 generalConfiguration = configurationRepository.getConfiguration(CType.CONFIG).getCEntry(CType.CONFIG.name()); + + FlattenedActionGroups flattenedActionGroups = new FlattenedActionGroups(actionGroupsConfiguration.withStaticConfig()); + this.actionGroups.set(flattenedActionGroups); + + PrivilegesEvaluator currentPrivilegesEvaluator = privilegesEvaluator.get(); + + // We are targeting an initialized PrivilegesEvaluator; this might seem a bit redundant, but gives us + // in the future the flexibility to introduce different implementations of PrivilegesEvaluator + PrivilegesEvaluator.PrivilegesEvaluatorType targetType = PrivilegesEvaluator.PrivilegesEvaluatorType.STANDARD; + PrivilegesEvaluator.PrivilegesEvaluatorType currentType = currentPrivilegesEvaluator.type(); + + if (currentType != targetType) { + PrivilegesEvaluator oldInstance = privilegesEvaluator.getAndSet( + new org.opensearch.security.privileges.PrivilegesEvaluatorImpl( + clusterService, + clusterStateSupplier, + roleMapper, + threadPool, + threadPool.getThreadContext(), + resolver, + auditLog, + settings, + privilegesInterceptor, + indexResolverReplacer, + flattenedActionGroups, + staticActionGroups, + rolesConfiguration, + generalConfiguration, + pluginIdToRolePrivileges + ) + ); + if (oldInstance != null) { + oldInstance.shutdown(); + } + } else { + privilegesEvaluator.get().updateConfiguration(flattenedActionGroups, rolesConfiguration, generalConfiguration); + } + + try { + this.multiTenancyConfiguration.set(new DashboardsMultiTenancyConfiguration(generalConfiguration)); + } catch (Exception e) { + log.error("Error while updating DashboardsMultiTenancyConfiguration", e); + } + + try { + this.tenantPrivileges.set(new TenantPrivileges(rolesConfiguration, tenantConfiguration, flattenedActionGroups)); + } catch (Exception e) { + log.error("Error while updating TenantPrivileges", e); + } + }); + } + + if (clusterService != null) { + clusterService.addListener(event -> { this.privilegesEvaluator.get().updateClusterStateMetadata(clusterService); }); + } + } + + /** + * For testing only: Creates a passive PrivilegesConfiguration object with the given PrivilegesEvaluator implementation and otherwise + * just defaults. + */ + public PrivilegesConfiguration(PrivilegesEvaluator privilegesEvaluator) { + this.privilegesEvaluator = new AtomicReference<>(privilegesEvaluator); + this.privilegesInterceptor = null; + this.staticActionGroups = buildStaticActionGroups(); + } + + /** + * Returns the current tenant privileges object. Important: Do not store the references to the instances returned here; these will change + * after configuration updates. + */ + public TenantPrivileges tenantPrivileges() { + return this.tenantPrivileges.get(); + } + + /** + * Returns the current PrivilegesEvaluator implementation. Important: Do not store the references to the instances returned here; these will change + * after configuration updates. + */ + public PrivilegesEvaluator privilegesEvaluator() { + return this.privilegesEvaluator.get(); + } + + /** + * Returns the current action groups configuration. Important: Do not store the references to the instances returned here; these will change + * after configuration updates. + */ + public FlattenedActionGroups actionGroups() { + return this.actionGroups.get(); + } + + /** + * Returns the current Dashboards multi tenancy configuration. Important: Do not store the references to the instances returned here; these will change + * after configuration updates. + */ + public DashboardsMultiTenancyConfiguration multiTenancyConfiguration() { + return this.multiTenancyConfiguration.get(); + } + + public void updatePluginToActionPrivileges(String pluginIdentifier, RoleV7 pluginPermissions) { + pluginIdToRolePrivileges.put(pluginIdentifier, pluginPermissions); + } + + private static FlattenedActionGroups buildStaticActionGroups() { + return new FlattenedActionGroups(DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS))); + } + +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 5747e3bb4f..8779503e39 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -26,911 +26,136 @@ package org.opensearch.security.privileges; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringJoiner; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import org.opensearch.OpenSearchSecurityException; import org.opensearch.action.ActionRequest; -import org.opensearch.action.IndicesRequest; -import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsRequest; -import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; -import org.opensearch.action.admin.indices.alias.IndicesAliasesAction; -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; -import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; -import org.opensearch.action.admin.indices.create.AutoCreateAction; -import org.opensearch.action.admin.indices.create.CreateIndexAction; -import org.opensearch.action.admin.indices.create.CreateIndexRequest; -import org.opensearch.action.admin.indices.delete.DeleteIndexAction; -import org.opensearch.action.admin.indices.mapping.get.GetFieldMappingsRequest; -import org.opensearch.action.admin.indices.mapping.put.AutoPutMappingAction; -import org.opensearch.action.admin.indices.mapping.put.PutMappingAction; -import org.opensearch.action.bulk.BulkAction; -import org.opensearch.action.bulk.BulkItemRequest; -import org.opensearch.action.bulk.BulkRequest; -import org.opensearch.action.bulk.BulkShardRequest; -import org.opensearch.action.delete.DeleteAction; -import org.opensearch.action.get.GetRequest; -import org.opensearch.action.get.MultiGetAction; -import org.opensearch.action.index.IndexAction; -import org.opensearch.action.search.MultiSearchAction; -import org.opensearch.action.search.SearchAction; -import org.opensearch.action.search.SearchRequest; -import org.opensearch.action.search.SearchScrollAction; -import org.opensearch.action.support.IndicesOptions; -import org.opensearch.action.termvectors.MultiTermVectorsAction; -import org.opensearch.action.update.UpdateAction; -import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.AliasMetadata; -import org.opensearch.cluster.metadata.IndexMetadata; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.common.Strings; -import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.index.reindex.ReindexAction; -import org.opensearch.script.mustache.RenderSearchTemplateAction; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; -import org.opensearch.security.privileges.actionlevel.SubjectBasedActionPrivileges; -import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.DynamicConfigFactory; -import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.core.rest.RestStatus; import org.opensearch.security.securityconf.FlattenedActionGroups; -import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.securityconf.impl.v7.ActionGroupsV7; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; -import org.opensearch.security.securityconf.impl.v7.TenantV7; -import org.opensearch.security.support.Base64Helper; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.support.SecuritySettings; -import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; import org.opensearch.tasks.Task; -import org.opensearch.threadpool.ThreadPool; - -import org.greenrobot.eventbus.Subscribe; - -import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; -import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; -import static org.opensearch.security.support.ConfigConstants.USER_ATTRIBUTE_SERIALIZATION_ENABLED; -import static org.opensearch.security.support.ConfigConstants.USER_ATTRIBUTE_SERIALIZATION_ENABLED_DEFAULT; -import static org.opensearch.security.support.SecurityUtils.escapePipe; - -public class PrivilegesEvaluator { - - private static final String USER_TENANT = "__user__"; - private static final String GLOBAL_TENANT = "global_tenant"; - private static final String READ_ACCESS = "READ"; - private static final String WRITE_ACCESS = "WRITE"; - private static final String NO_ACCESS = "NONE"; - - static final WildcardMatcher DNFOF_MATCHER = WildcardMatcher.from( - ImmutableList.of( - "indices:data/read/*", - "indices:admin/mappings/fields/get*", - "indices:admin/shards/search_shards", - "indices:admin/resolve/index", - "indices:monitor/settings/get", - "indices:monitor/stats", - "indices:admin/aliases/get" - ) - ); - - private static final WildcardMatcher ACTION_MATCHER = WildcardMatcher.from("indices:data/read/*search*"); - - private static final IndicesOptions ALLOW_EMPTY = IndicesOptions.fromOptions(true, true, false, false); - - protected final Logger log = LogManager.getLogger(this.getClass()); - private final Supplier clusterStateSupplier; - - private final IndexNameExpressionResolver resolver; - - private final AuditLog auditLog; - private ThreadContext threadContext; - - private PrivilegesInterceptor privilegesInterceptor; - - private final boolean checkSnapshotRestoreWritePrivileges; - private boolean isUserAttributeSerializationEnabled; - - private final ClusterInfoHolder clusterInfoHolder; - private final ConfigurationRepository configurationRepository; - private ConfigModel configModel; - private final IndexResolverReplacer irr; - private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; - private final SystemIndexAccessEvaluator systemIndexAccessEvaluator; - private final ProtectedIndexAccessEvaluator protectedIndexAccessEvaluator; - private final TermsAggregationEvaluator termsAggregationEvaluator; - private final PitPrivilegesEvaluator pitPrivilegesEvaluator; - private DynamicConfigModel dcm; - private final Settings settings; - private final AtomicReference actionPrivileges = new AtomicReference<>(); - private final AtomicReference tenantPrivileges = new AtomicReference<>(); - private final Map pluginIdToActionPrivileges = new HashMap<>(); - - /** - * The pure static action groups should be ONLY used by action privileges for plugins; only those cannot and should - * not have knowledge of any action groups defined in the dynamic configuration. All other functionality should - * use the action groups derived from the dynamic configuration (which is always computed on the fly on - * configuration updates). - */ - private final FlattenedActionGroups staticActionGroups; - - public PrivilegesEvaluator( - final ClusterService clusterService, - Supplier clusterStateSupplier, - ThreadPool threadPool, - final ThreadContext threadContext, - final ConfigurationRepository configurationRepository, - final IndexNameExpressionResolver resolver, - AuditLog auditLog, - final Settings settings, - final PrivilegesInterceptor privilegesInterceptor, - final ClusterInfoHolder clusterInfoHolder, - final IndexResolverReplacer irr - ) { - - super(); - this.resolver = resolver; - this.auditLog = auditLog; - - this.threadContext = threadContext; - this.privilegesInterceptor = privilegesInterceptor; - this.clusterStateSupplier = clusterStateSupplier; - this.settings = settings; - - this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( - ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, - ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES - ); - this.isUserAttributeSerializationEnabled = settings.getAsBoolean( - USER_ATTRIBUTE_SERIALIZATION_ENABLED, - USER_ATTRIBUTE_SERIALIZATION_ENABLED_DEFAULT - ); - - this.clusterInfoHolder = clusterInfoHolder; - this.irr = irr; - snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog); - systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog, irr); - protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); - termsAggregationEvaluator = new TermsAggregationEvaluator(); - pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); - this.configurationRepository = configurationRepository; - this.staticActionGroups = new FlattenedActionGroups( - DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS)) - ); - - if (configurationRepository != null) { - configurationRepository.subscribeOnChange(configMap -> { - SecurityDynamicConfiguration actionGroupsConfiguration = configurationRepository.getConfiguration( - CType.ACTIONGROUPS - ); - SecurityDynamicConfiguration rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES); - SecurityDynamicConfiguration tenantConfiguration = configurationRepository.getConfiguration(CType.TENANTS); - - this.updateConfiguration(actionGroupsConfiguration, rolesConfiguration, tenantConfiguration); - }); - } - - if (clusterService != null) { - clusterService.addListener(event -> { - RoleBasedActionPrivileges actionPrivileges = PrivilegesEvaluator.this.actionPrivileges.get(); - if (actionPrivileges != null) { - actionPrivileges.clusterStateMetadataDependentPrivileges().updateClusterStateMetadataAsync(clusterService, threadPool); - } - }); - - this.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); - } - } - - void updateConfiguration( - SecurityDynamicConfiguration actionGroupsConfiguration, - SecurityDynamicConfiguration rolesConfiguration, - SecurityDynamicConfiguration tenantConfiguration - ) { - FlattenedActionGroups flattenedActionGroups = new FlattenedActionGroups(actionGroupsConfiguration.withStaticConfig()); - rolesConfiguration = rolesConfiguration.withStaticConfig(); - tenantConfiguration = tenantConfiguration.withStaticConfig(); - try { - RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges(rolesConfiguration, flattenedActionGroups, settings); - Metadata metadata = clusterStateSupplier.get().metadata(); - actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); - RoleBasedActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); - - if (oldInstance != null) { - oldInstance.clusterStateMetadataDependentPrivileges().shutdown(); - } - } catch (Exception e) { - log.error("Error while updating ActionPrivileges", e); - } - - try { - this.tenantPrivileges.set(new TenantPrivileges(rolesConfiguration, tenantConfiguration, flattenedActionGroups)); - } catch (Exception e) { - log.error("Error while updating TenantPrivileges", e); - } - } - - @Subscribe - public void onConfigModelChanged(ConfigModel configModel) { - this.configModel = configModel; - } - - @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - this.dcm = dcm; - } - - public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permission) { - PrivilegesEvaluationContext context = createContext(user, permission); - return context.getActionPrivileges().hasExplicitClusterPrivilege(context, permission).isAllowed(); - } - - public boolean isInitialized() { - return configModel != null && dcm != null && actionPrivileges.get() != null; - } - - public void registerClusterSettingsChangeListener(final ClusterSettings clusterSettings) { - clusterSettings.addSettingsUpdateConsumer( - SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING, - newIsUserAttributeSerializationEnabled -> { - isUserAttributeSerializationEnabled = newIsUserAttributeSerializationEnabled; - } - ); - } - - private boolean isUserAttributeSerializationEnabled() { - return isUserAttributeSerializationEnabled; - } - - private void setUserInfoInThreadContext(PrivilegesEvaluationContext context) { - if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { - StringJoiner joiner = new StringJoiner("|"); - // Escape any pipe characters in the values before joining - joiner.add(escapePipe(context.getUser().getName())); - joiner.add(escapePipe(String.join(",", context.getUser().getRoles()))); - joiner.add(escapePipe(String.join(",", context.getMappedRoles()))); - - String requestedTenant = context.getUser().getRequestedTenant(); - joiner.add(requestedTenant); - - String tenantAccessToCheck = getTenancyAccess(context); - joiner.add(tenantAccessToCheck); - log.debug(joiner); - if (this.isUserAttributeSerializationEnabled()) { - joiner.add(Base64Helper.serializeObject(new HashMap<>(context.getUser().getCustomAttributesMap()))); - } - - threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); - } - } - - public PrivilegesEvaluationContext createContext(User user, String action) { - return createContext(user, action, null, null, null); - } +/** + * The basic interface for privilege evaluation. + */ +public interface PrivilegesEvaluator { - private String getTenancyAccess(PrivilegesEvaluationContext context) { - String requestedTenant = context.getUser().getRequestedTenant(); - final String tenant = Strings.isNullOrEmpty(requestedTenant) ? GLOBAL_TENANT : requestedTenant; - if (tenantPrivileges.get().hasTenantPrivilege(context, tenant, TenantPrivileges.ActionType.WRITE)) { - return WRITE_ACCESS; - } else if (tenantPrivileges.get().hasTenantPrivilege(context, tenant, TenantPrivileges.ActionType.READ)) { - return READ_ACCESS; - } else { - return NO_ACCESS; - } + default PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, ActionRequestMetadata.empty(), null); } - public PrivilegesEvaluationContext createContext( + PrivilegesEvaluationContext createContext( User user, - String action0, - ActionRequest request, - Task task, - Set injectedRoles - ) { - if (!isInitialized()) { - StringBuilder error = new StringBuilder("OpenSearch Security is not initialized."); - if (!clusterInfoHolder.hasClusterManager()) { - error.append(String.format(" %s", ClusterInfoHolder.CLUSTER_MANAGER_NOT_PRESENT)); - } - throw new OpenSearchSecurityException(error.toString()); - } - - TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - - ActionPrivileges actionPrivileges; - ImmutableSet mappedRoles; - - if (user.isPluginUser()) { - mappedRoles = ImmutableSet.of(); - actionPrivileges = this.pluginIdToActionPrivileges.get(user.getName()); - if (actionPrivileges == null) { - actionPrivileges = ActionPrivileges.EMPTY; - } - } else { - mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); - actionPrivileges = this.actionPrivileges.get(); - } - - return new PrivilegesEvaluationContext( - user, - mappedRoles, - action0, - request, - task, - irr, - resolver, - clusterStateSupplier, - actionPrivileges - ); - } - - public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { - - if (!isInitialized()) { - StringBuilder error = new StringBuilder("OpenSearch Security is not initialized."); - if (!clusterInfoHolder.hasClusterManager()) { - error.append(String.format(" %s", ClusterInfoHolder.CLUSTER_MANAGER_NOT_PRESENT)); - } - throw new OpenSearchSecurityException(error.toString()); - } - - String action0 = context.getAction(); - ImmutableSet mappedRoles = context.getMappedRoles(); - User user = context.getUser(); - ActionRequest request = context.getRequest(); - Task task = context.getTask(); - - if (action0.startsWith("internal:indices/admin/upgrade")) { - action0 = "indices:admin/upgrade"; - } - - if (AutoCreateAction.NAME.equals(action0)) { - action0 = CreateIndexAction.NAME; - } - - if (AutoPutMappingAction.NAME.equals(action0)) { - action0 = PutMappingAction.NAME; - } - - PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); - - final String injectedRolesValidationString = threadContext.getTransient( - ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION - ); - if (injectedRolesValidationString != null) { - HashSet injectedRolesValidationSet = new HashSet<>(Arrays.asList(injectedRolesValidationString.split(","))); - if (!mappedRoles.containsAll(injectedRolesValidationSet)) { - presponse.allowed = false; - presponse.missingSecurityRoles.addAll(injectedRolesValidationSet); - log.info("Roles {} are not mapped to the user {}", injectedRolesValidationSet, user); - return presponse; - } - mappedRoles = ImmutableSet.copyOf(injectedRolesValidationSet); - context.setMappedRoles(mappedRoles); - } - - setUserInfoInThreadContext(context); - - final boolean isDebugEnabled = log.isDebugEnabled(); - if (isDebugEnabled) { - log.debug("Evaluate permissions for {}", user); - log.debug("Action: {} ({})", action0, request.getClass().getSimpleName()); - log.debug("Mapped roles: {}", mappedRoles.toString()); - } - - ActionPrivileges actionPrivileges = context.getActionPrivileges(); - if (actionPrivileges == null) { - throw new OpenSearchSecurityException("OpenSearch Security is not initialized: roles configuration is missing"); - } - - if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { - // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action - // indices:data/write/bulk[s]). - // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default - // tenants. - // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction - // level. - - presponse = actionPrivileges.hasClusterPrivilege(context, action0); - - if (!presponse.allowed) { - log.info( - "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - action0, - mappedRoles, - presponse.getMissingPrivileges() - ); - } - return presponse; - } - - final Resolved requestedResolved = context.getResolvedRequest(); - - if (isDebugEnabled) { - log.debug("RequestedResolved : {}", requestedResolved); - } - - // check snapshot/restore requests - // NOTE: Has to go first as restore request could be for protected and/or system indices and the request may - // fail with 403 if system index or protected index evaluators are triggered first - if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { - return presponse; - } - - // System index access - if (systemIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, context, actionPrivileges, user) - .isComplete()) { - return presponse; - } - - // Protected index access - if (protectedIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, mappedRoles).isComplete()) { - return presponse; - } - - // check access for point in time requests - if (pitPrivilegesEvaluator.evaluate(request, context, actionPrivileges, action0, presponse, irr).isComplete()) { - return presponse; - } - - final boolean dnfofEnabled = dcm.isDnfofEnabled(); - - final boolean isTraceEnabled = log.isTraceEnabled(); - if (isTraceEnabled) { - log.trace("dnfof enabled? {}", dnfofEnabled); - } - - final boolean serviceAccountUser = user.isServiceAccount(); - if (isClusterPerm(action0)) { - if (serviceAccountUser) { - log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); - return PrivilegesEvaluatorResponse.insufficient(action0); - } - - presponse = actionPrivileges.hasClusterPrivilege(context, action0); - - if (!presponse.allowed) { - log.info( - "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - requestedResolved, - action0, - mappedRoles, - presponse.getMissingPrivileges() - ); - return presponse; - } else { - - if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { - if (isDebugEnabled) { - log.debug("Normally allowed but we need to apply some extra checks for a restore request."); - } - } else { - if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( - request, - action0, - user, - dcm, - requestedResolved, - context, - this.tenantPrivileges.get() - ); - - if (isDebugEnabled) { - log.debug("Result from privileges interceptor for cluster perm: {}", replaceResult); - } - - if (!replaceResult.continueEvaluation) { - if (replaceResult.accessDenied) { - auditLog.logMissingPrivileges(action0, request, task); - } else { - presponse.allowed = true; - presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; - } - return presponse; - } - } - - if (isDebugEnabled) { - log.debug("Allowed because we have cluster permissions for {}", action0); - } - presponse.allowed = true; - return presponse; - } - } - } - - if (checkDocAllowListHeader(user, action0, request)) { - presponse.allowed = true; - return presponse; - } - - // term aggregations - if (termsAggregationEvaluator.evaluate(requestedResolved, request, context, actionPrivileges, presponse).isComplete()) { - return presponse; - } - - ImmutableSet allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); - - if (isDebugEnabled) { - log.debug( - "Requested {} from {}", - allIndexPermsRequired, - threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS) - ); - } - - if (isDebugEnabled) { - log.debug("Requested resolved index types: {}", requestedResolved); - log.debug("Security roles: {}", mappedRoles); - } - - // TODO exclude Security index - - if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( - request, - action0, - user, - dcm, - requestedResolved, - context, - this.tenantPrivileges.get() - ); - - if (isDebugEnabled) { - log.debug("Result from privileges interceptor: {}", replaceResult); - } - - if (!replaceResult.continueEvaluation) { - if (replaceResult.accessDenied) { - auditLog.logMissingPrivileges(action0, request, task); - return PrivilegesEvaluatorResponse.insufficient(action0); - } else { - presponse.allowed = true; - presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; - return presponse; - } - } - } - - boolean dnfofPossible = dnfofEnabled && DNFOF_MATCHER.test(action0); - - presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, requestedResolved); - - if (presponse.isPartiallyOk()) { - if (dnfofPossible) { - if (irr.replace(request, true, presponse.getAvailableIndices())) { - return PrivilegesEvaluatorResponse.ok(); - } - } - } else if (!presponse.isAllowed()) { - if (dnfofPossible && dcm.isDnfofForEmptyResultsEnabled() && request instanceof IndicesRequest.Replaceable) { - ((IndicesRequest.Replaceable) request).indices(new String[0]); - - if (request instanceof SearchRequest) { - ((SearchRequest) request).indicesOptions(ALLOW_EMPTY); - } else if (request instanceof ClusterSearchShardsRequest) { - ((ClusterSearchShardsRequest) request).indicesOptions(ALLOW_EMPTY); - } else if (request instanceof GetFieldMappingsRequest) { - ((GetFieldMappingsRequest) request).indicesOptions(ALLOW_EMPTY); - } - - return PrivilegesEvaluatorResponse.ok(); - } - } - - if (presponse.isAllowed()) { - if (checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { - presponse.allowed = false; - return presponse; - } - - if (isDebugEnabled) { - log.debug("Allowed because we have all indices permissions for {}", action0); - } - } else { - log.info( - "No {}-level perm match for {} {}: {} [Action [{}]] [RolesChecked {}]", - "index", - user, - requestedResolved, - presponse.getReason(), - action0, - mappedRoles - ); - log.info("Index to privilege matrix:\n{}", presponse.getPrivilegeMatrix()); - if (presponse.hasEvaluationExceptions()) { - log.info("Evaluation errors:\n{}", presponse.getEvaluationExceptionInfo()); - } - } - - return presponse; - } - - public Set mapRoles(final User user, final TransportAddress caller) { - return this.configModel.mapSecurityRoles(user, caller); - } - - public TenantPrivileges tenantPrivileges() { - return this.tenantPrivileges.get(); - } + String action, + ActionRequest actionRequest, + ActionRequestMetadata actionRequestMetadata, + Task task + ); - public boolean multitenancyEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsMultitenancyEnabled(); - } + PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context); - public boolean privateTenantEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsPrivateTenantEnabled(); - } + boolean isClusterPermission(String action); - public String dashboardsDefaultTenant() { - return dcm.getDashboardsDefaultTenant(); - } + void updateConfiguration( + FlattenedActionGroups actionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ); - public boolean notFailOnForbiddenEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); - } + void updateClusterStateMetadata(ClusterService clusterService); - public String dashboardsIndex() { - return dcm.getDashboardsIndexname(); - } + /** + * Shuts down any background processes or other resources that need an explicit shut down + */ + void shutdown(); - public String dashboardsServerUsername() { - return dcm.getDashboardsServerUsername(); - } + boolean notFailOnForbiddenEnabled(); - public String dashboardsOpenSearchRole() { - return dcm.getDashboardsOpenSearchRole(); - } + PrivilegesEvaluatorType type(); - public List getSignInOptions() { - return dcm.getSignInOptions(); + enum PrivilegesEvaluatorType { + NOT_INITIALIZED, + STANDARD } - private ImmutableSet evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { - ImmutableSet.Builder additionalPermissionsRequired = ImmutableSet.builder(); - - if (!isClusterPerm(originalAction)) { - additionalPermissionsRequired.add(originalAction); - } - - if (request instanceof ClusterSearchShardsRequest) { - additionalPermissionsRequired.add(SearchAction.NAME); - } - - if (request instanceof BulkShardRequest) { - BulkShardRequest bsr = (BulkShardRequest) request; - for (BulkItemRequest bir : bsr.items()) { - switch (bir.request().opType()) { - case CREATE: - additionalPermissionsRequired.add(IndexAction.NAME); - break; - case INDEX: - additionalPermissionsRequired.add(IndexAction.NAME); - break; - case DELETE: - additionalPermissionsRequired.add(DeleteAction.NAME); - break; - case UPDATE: - additionalPermissionsRequired.add(UpdateAction.NAME); - break; - } - } - } - - if (request instanceof IndicesAliasesRequest) { - IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; - for (AliasActions bir : bsr.getAliasActions()) { - switch (bir.actionType()) { - case REMOVE_INDEX: - additionalPermissionsRequired.add(DeleteIndexAction.NAME); - break; - default: - break; - } - } - } + /** + * A PrivilegesEvaluator implementation that just throws "not initialized" exceptions. + * Used initially by PrivilegesConfiguration. + */ + class NotInitialized implements PrivilegesEvaluator { + private final Supplier unavailablityReasonSupplier; - if (request instanceof CreateIndexRequest) { - CreateIndexRequest cir = (CreateIndexRequest) request; - if (cir.aliases() != null && !cir.aliases().isEmpty()) { - additionalPermissionsRequired.add(IndicesAliasesAction.NAME); - } + NotInitialized(Supplier unavailablityReasonSupplier) { + this.unavailablityReasonSupplier = unavailablityReasonSupplier; } - if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { - additionalPermissionsRequired.addAll(ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES); + @Override + public PrivilegesEvaluatorType type() { + return PrivilegesEvaluatorType.NOT_INITIALIZED; } - ImmutableSet result = additionalPermissionsRequired.build(); - - if (result.size() > 1) { - traceAction("Additional permissions required: {}", result); + @Override + public PrivilegesEvaluationContext createContext( + User user, + String action, + ActionRequest actionRequest, + ActionRequestMetadata actionRequestMetadata, + Task task + ) { + throw exception(); } - if (log.isDebugEnabled() && result.size() > 1) { - log.debug("Additional permissions required: {}", result); + @Override + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + throw exception(); } - return result; - } - - public static boolean isClusterPerm(String action0) { - return (action0.startsWith("cluster:") - || action0.startsWith("indices:admin/template/") - || action0.startsWith("indices:admin/index_template/") - || action0.startsWith(SearchScrollAction.NAME) - || (action0.equals(BulkAction.NAME)) - || (action0.equals(MultiGetAction.NAME)) - || (action0.startsWith(MultiSearchAction.NAME)) - || (action0.equals(MultiTermVectorsAction.NAME)) - || (action0.equals(ReindexAction.NAME)) - || (action0.equals(RenderSearchTemplateAction.NAME))); - } - - @SuppressWarnings("unchecked") - private boolean checkFilteredAliases(Resolved requestedResolved, String action, boolean isDebugEnabled) { - final String faMode = dcm.getFilteredAliasMode();// getConfigSettings().dynamic.filtered_alias_mode; - - if (!"disallow".equals(faMode)) { - return false; - } - - if (!ACTION_MATCHER.test(action)) { + @Override + public boolean isClusterPermission(String action) { return false; } - Iterable indexMetaDataCollection; - - if (requestedResolved.isLocalAll()) { - indexMetaDataCollection = new Iterable() { - @Override - public Iterator iterator() { - return clusterStateSupplier.get().getMetadata().getIndices().values().iterator(); - } - }; - } else { - Set indexMetaDataSet = new HashSet<>(requestedResolved.getAllIndices().size()); - - for (String requestAliasOrIndex : requestedResolved.getAllIndices()) { - IndexMetadata indexMetaData = clusterStateSupplier.get().getMetadata().getIndices().get(requestAliasOrIndex); - if (indexMetaData == null) { - if (isDebugEnabled) { - log.debug("{} does not exist in cluster metadata", requestAliasOrIndex); - } - continue; - } - - indexMetaDataSet.add(indexMetaData); - } + @Override + public void updateConfiguration( + FlattenedActionGroups actionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ) { - indexMetaDataCollection = indexMetaDataSet; } - // check filtered aliases - for (IndexMetadata indexMetaData : indexMetaDataCollection) { - - final List filteredAliases = new ArrayList(); - - final Map aliases = indexMetaData.getAliases(); - - if (aliases != null && aliases.size() > 0) { - if (isDebugEnabled) { - log.debug("Aliases for {}: {}", indexMetaData.getIndex().getName(), aliases); - } - - final Iterator it = aliases.keySet().iterator(); - while (it.hasNext()) { - final String alias = it.next(); - final AliasMetadata aliasMetadata = aliases.get(alias); - - if (aliasMetadata != null && aliasMetadata.filteringRequired()) { - filteredAliases.add(aliasMetadata); - if (isDebugEnabled) { - log.debug("{} is a filtered alias {}", alias, aliasMetadata.getFilter()); - } - } else { - if (isDebugEnabled) { - log.debug("{} is not an alias or does not have a filter", alias); - } - } - } - } - if (filteredAliases.size() > 1 && ACTION_MATCHER.test(action)) { - // TODO add queries as dls queries (works only if dls module is installed) - log.error( - "More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", - filteredAliases.size(), - indexMetaData.getIndex().getName(), - toString(filteredAliases) - ); - return true; - } - } // end-for + @Override + public void updateClusterStateMetadata(ClusterService clusterService) { - return false; - } + } - private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { - String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); + @Override + public void shutdown() { - if (docAllowListHeader == null) { - return false; } - if (!(request instanceof GetRequest)) { + @Override + public boolean notFailOnForbiddenEnabled() { return false; } - try { - DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); - GetRequest getRequest = (GetRequest) request; - - if (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { - if (log.isDebugEnabled()) { - log.debug("Request " + request + " is allowed by " + documentAllowList); - } + private OpenSearchSecurityException exception() { + StringBuilder error = new StringBuilder("OpenSearch Security is not initialized"); + String reason = this.unavailablityReasonSupplier.get(); - return true; + if (reason != null) { + error.append(": ").append(reason); } else { - return false; + error.append("."); } - } catch (Exception e) { - log.error("Error while handling document allow list: " + docAllowListHeader, e); - return false; - } - } - - private List toString(List aliases) { - if (aliases == null || aliases.size() == 0) { - return Collections.emptyList(); + return new OpenSearchSecurityException(error.toString(), RestStatus.SERVICE_UNAVAILABLE); } + }; - final List ret = new ArrayList<>(aliases.size()); - - for (final AliasMetadata amd : aliases) { - if (amd != null) { - ret.add(amd.alias()); - } - } - - return Collections.unmodifiableList(ret); - } - - public void updatePluginToActionPrivileges(String pluginIdentifier, RoleV7 pluginPermissions) { - pluginIdToActionPrivileges.put(pluginIdentifier, new SubjectBasedActionPrivileges(pluginPermissions, staticActionGroups)); - } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java new file mode 100644 index 0000000000..c4b1bffbc0 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java @@ -0,0 +1,758 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * 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.privileges; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.IndicesRequest; +import org.opensearch.action.admin.cluster.shards.ClusterSearchShardsRequest; +import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesAction; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; +import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions; +import org.opensearch.action.admin.indices.create.AutoCreateAction; +import org.opensearch.action.admin.indices.create.CreateIndexAction; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.delete.DeleteIndexAction; +import org.opensearch.action.admin.indices.mapping.get.GetFieldMappingsRequest; +import org.opensearch.action.admin.indices.mapping.put.AutoPutMappingAction; +import org.opensearch.action.admin.indices.mapping.put.PutMappingAction; +import org.opensearch.action.bulk.BulkAction; +import org.opensearch.action.bulk.BulkItemRequest; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkShardRequest; +import org.opensearch.action.delete.DeleteAction; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.index.IndexAction; +import org.opensearch.action.search.MultiSearchAction; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchScrollAction; +import org.opensearch.action.support.ActionRequestMetadata; +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.action.termvectors.MultiTermVectorsAction; +import org.opensearch.action.update.UpdateAction; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.metadata.AliasMetadata; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.Strings; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.index.reindex.ReindexAction; +import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; +import org.opensearch.security.privileges.actionlevel.SubjectBasedActionPrivileges; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.FlattenedActionGroups; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.securityconf.impl.v7.RoleV7; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; + +/** + * The current implementation for action privilege evaluation. + *

+ * Moved from https://github.com/opensearch-project/security/blob/062ea716d10240cc50d01735f457523a61393a59/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java + */ +public class PrivilegesEvaluatorImpl implements PrivilegesEvaluator { + + static final WildcardMatcher DNFOF_MATCHER = WildcardMatcher.from( + ImmutableList.of( + "indices:data/read/*", + "indices:admin/mappings/fields/get*", + "indices:admin/shards/search_shards", + "indices:admin/resolve/index", + "indices:monitor/settings/get", + "indices:monitor/stats", + "indices:admin/aliases/get" + ) + ); + + private static final WildcardMatcher ACTION_MATCHER = WildcardMatcher.from("indices:data/read/*search*"); + + private static final IndicesOptions ALLOW_EMPTY = IndicesOptions.fromOptions(true, true, false, false); + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final Supplier clusterStateSupplier; + + private final IndexNameExpressionResolver resolver; + + private final AuditLog auditLog; + private final ThreadContext threadContext; + private final ThreadPool threadPool; + + private PrivilegesInterceptor privilegesInterceptor; + + private final boolean checkSnapshotRestoreWritePrivileges; + + private final IndexResolverReplacer irr; + private final SnapshotRestoreEvaluator snapshotRestoreEvaluator; + private final SystemIndexAccessEvaluator systemIndexAccessEvaluator; + private final ProtectedIndexAccessEvaluator protectedIndexAccessEvaluator; + private final TermsAggregationEvaluator termsAggregationEvaluator; + private final PitPrivilegesEvaluator pitPrivilegesEvaluator; + private final Settings settings; + private final AtomicReference actionPrivileges = new AtomicReference<>(); + private final Map pluginIdToActionPrivileges = new HashMap<>(); + private final RoleMapper roleMapper; + + private volatile boolean dnfofEnabled = false; + private volatile boolean dnfofForEmptyResultsEnabled = false; + private volatile String filteredAliasMode = null; + + public PrivilegesEvaluatorImpl( + final ClusterService clusterService, + Supplier clusterStateSupplier, + RoleMapper roleMapper, + ThreadPool threadPool, + final ThreadContext threadContext, + final IndexNameExpressionResolver resolver, + AuditLog auditLog, + final Settings settings, + final PrivilegesInterceptor privilegesInterceptor, + final IndexResolverReplacer irr, + FlattenedActionGroups actionGroups, + FlattenedActionGroups staticActionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration, + Map pluginIdToRolePrivileges + ) { + + super(); + this.resolver = resolver; + this.auditLog = auditLog; + this.roleMapper = roleMapper; + + this.threadContext = threadContext; + this.threadPool = threadPool; + this.privilegesInterceptor = privilegesInterceptor; + this.clusterStateSupplier = clusterStateSupplier; + this.settings = settings; + + this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( + ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES + ); + + Supplier isLocalNodeElectedClusterManager = clusterService != null + ? () -> clusterService.state().nodes().isLocalNodeElectedClusterManager() + : () -> false; + + this.irr = irr; + snapshotRestoreEvaluator = new SnapshotRestoreEvaluator(settings, auditLog, isLocalNodeElectedClusterManager); + systemIndexAccessEvaluator = new SystemIndexAccessEvaluator(settings, auditLog, irr); + protectedIndexAccessEvaluator = new ProtectedIndexAccessEvaluator(settings, auditLog); + termsAggregationEvaluator = new TermsAggregationEvaluator(); + pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); + + this.pluginIdToActionPrivileges.putAll(createActionPrivileges(pluginIdToRolePrivileges, staticActionGroups)); + this.updateConfiguration(actionGroups, rolesConfiguration, generalConfiguration); + } + + @Override + public PrivilegesEvaluatorType type() { + return PrivilegesEvaluatorType.STANDARD; + } + + @Override + public void updateConfiguration( + FlattenedActionGroups flattenedActionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ) { + this.dnfofEnabled = isDnfofEnabled(generalConfiguration); + this.dnfofForEmptyResultsEnabled = isDnfofEmptyEnabled(generalConfiguration); + this.filteredAliasMode = getFilteredAliasMode(generalConfiguration); + + try { + RoleBasedActionPrivileges actionPrivileges = new RoleBasedActionPrivileges(rolesConfiguration, flattenedActionGroups, settings); + Metadata metadata = clusterStateSupplier.get().metadata(); + actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); + RoleBasedActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); + + if (oldInstance != null) { + oldInstance.clusterStateMetadataDependentPrivileges().shutdown(); + } + } catch (Exception e) { + log.error("Error while updating ActionPrivileges", e); + } + + } + + @Override + public void updateClusterStateMetadata(ClusterService clusterService) { + RoleBasedActionPrivileges actionPrivileges = this.actionPrivileges.get(); + if (actionPrivileges != null) { + actionPrivileges.clusterStateMetadataDependentPrivileges().updateClusterStateMetadataAsync(clusterService, threadPool); + } + } + + @Override + public void shutdown() { + RoleBasedActionPrivileges roleBasedActionPrivileges = this.actionPrivileges.get(); + if (roleBasedActionPrivileges != null) { + roleBasedActionPrivileges.clusterStateMetadataDependentPrivileges().shutdown(); + } + } + + @Override + public PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, null, null); + } + + @Override + public PrivilegesEvaluationContext createContext( + User user, + String action0, + ActionRequest request, + ActionRequestMetadata actionRequestMetadata, + Task task + ) { + TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + ActionPrivileges actionPrivileges; + ImmutableSet mappedRoles; + + if (user.isPluginUser()) { + mappedRoles = ImmutableSet.of(); + actionPrivileges = this.pluginIdToActionPrivileges.get(user.getName()); + if (actionPrivileges == null) { + actionPrivileges = ActionPrivileges.EMPTY; + } + } else { + mappedRoles = this.roleMapper.map(user, caller); + actionPrivileges = this.actionPrivileges.get(); + } + + return new PrivilegesEvaluationContext( + user, + mappedRoles, + action0, + request, + task, + irr, + resolver, + clusterStateSupplier, + actionPrivileges + ); + } + + @Override + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + String action0 = context.getAction(); + ImmutableSet mappedRoles = context.getMappedRoles(); + User user = context.getUser(); + ActionRequest request = context.getRequest(); + Task task = context.getTask(); + + if (action0.startsWith("internal:indices/admin/upgrade")) { + action0 = "indices:admin/upgrade"; + } + + if (AutoCreateAction.NAME.equals(action0)) { + action0 = CreateIndexAction.NAME; + } + + if (AutoPutMappingAction.NAME.equals(action0)) { + action0 = PutMappingAction.NAME; + } + + PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {}", user); + log.debug("Action: {} ({})", action0, request.getClass().getSimpleName()); + log.debug("Mapped roles: {}", mappedRoles.toString()); + } + + ActionPrivileges actionPrivileges = context.getActionPrivileges(); + + if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { + // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action + // indices:data/write/bulk[s]). + // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default + // tenants. + // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction + // level. + + presponse = actionPrivileges.hasClusterPrivilege(context, action0); + + if (!presponse.allowed) { + log.info( + "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + action0, + mappedRoles, + presponse.getMissingPrivileges() + ); + } + return presponse; + } + + final Resolved requestedResolved = context.getResolvedRequest(); + + log.debug("RequestedResolved : {}", requestedResolved); + + // check snapshot/restore requests + // NOTE: Has to go first as restore request could be for protected and/or system indices and the request may + // fail with 403 if system index or protected index evaluators are triggered first + if (snapshotRestoreEvaluator.evaluate(request, task, action0, presponse).isComplete()) { + return presponse; + } + + // System index access + if (systemIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, context, actionPrivileges, user) + .isComplete()) { + return presponse; + } + + // Protected index access + if (protectedIndexAccessEvaluator.evaluate(request, task, action0, requestedResolved, presponse, mappedRoles).isComplete()) { + return presponse; + } + + // check access for point in time requests + if (pitPrivilegesEvaluator.evaluate(request, context, actionPrivileges, action0, presponse, irr).isComplete()) { + return presponse; + } + + final boolean dnfofEnabled = this.dnfofEnabled; + + log.trace("dnfof enabled? {}", dnfofEnabled); + + final boolean serviceAccountUser = user.isServiceAccount(); + if (isClusterPerm(action0)) { + if (serviceAccountUser) { + log.info("{} is a service account which doesn't have access to cluster level permission: {}", user, action0); + return PrivilegesEvaluatorResponse.insufficient(action0); + } + + presponse = actionPrivileges.hasClusterPrivilege(context, action0); + + if (!presponse.allowed) { + log.info( + "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + requestedResolved, + action0, + mappedRoles, + presponse.getMissingPrivileges() + ); + return presponse; + } else { + + if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + log.debug("Normally allowed but we need to apply some extra checks for a restore request."); + + } else { + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + requestedResolved, + context + ); + + log.debug("Result from privileges interceptor for cluster perm: {}", replaceResult); + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + auditLog.logMissingPrivileges(action0, request, task); + } else { + presponse.allowed = true; + presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; + } + return presponse; + } + } + + log.debug("Allowed because we have cluster permissions for {}", action0); + + presponse.allowed = true; + return presponse; + } + } + } + + if (checkDocAllowListHeader(user, action0, request)) { + presponse.allowed = true; + return presponse; + } + + // term aggregations + if (termsAggregationEvaluator.evaluate(requestedResolved, request, context, actionPrivileges, presponse).isComplete()) { + return presponse; + } + + ImmutableSet allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); + + log.debug("Requested {}", allIndexPermsRequired); + log.debug("Requested resolved index types: {}", requestedResolved); + log.debug("Security roles: {}", mappedRoles); + + // TODO exclude Security index + + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + requestedResolved, + context + ); + + log.debug("Result from privileges interceptor: {}", replaceResult); + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + auditLog.logMissingPrivileges(action0, request, task); + return PrivilegesEvaluatorResponse.insufficient(action0); + } else { + presponse.allowed = true; + presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; + return presponse; + } + } + } + + boolean dnfofPossible = dnfofEnabled && DNFOF_MATCHER.test(action0); + + presponse = actionPrivileges.hasIndexPrivilege(context, allIndexPermsRequired, requestedResolved); + + if (presponse.isPartiallyOk()) { + if (dnfofPossible) { + if (irr.replace(request, true, presponse.getAvailableIndices())) { + return PrivilegesEvaluatorResponse.ok(); + } + } + } else if (!presponse.isAllowed()) { + if (dnfofPossible && this.dnfofForEmptyResultsEnabled && request instanceof IndicesRequest.Replaceable) { + ((IndicesRequest.Replaceable) request).indices(new String[0]); + + if (request instanceof SearchRequest) { + ((SearchRequest) request).indicesOptions(ALLOW_EMPTY); + } else if (request instanceof ClusterSearchShardsRequest) { + ((ClusterSearchShardsRequest) request).indicesOptions(ALLOW_EMPTY); + } else if (request instanceof GetFieldMappingsRequest) { + ((GetFieldMappingsRequest) request).indicesOptions(ALLOW_EMPTY); + } + + return PrivilegesEvaluatorResponse.ok(); + } + } + + if (presponse.isAllowed()) { + if (checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { + presponse.allowed = false; + return presponse; + } + + log.debug("Allowed because we have all indices permissions for {}", action0); + + } else { + log.info( + "No {}-level perm match for {} {}: {} [Action [{}]] [RolesChecked {}]", + "index", + user, + requestedResolved, + presponse.getReason(), + action0, + mappedRoles + ); + log.info("Index to privilege matrix:\n{}", presponse.getPrivilegeMatrix()); + if (presponse.hasEvaluationExceptions()) { + log.info("Evaluation errors:\n{}", presponse.getEvaluationExceptionInfo()); + } + } + + return presponse; + } + + @Override + public boolean notFailOnForbiddenEnabled() { + return dnfofEnabled; + } + + private ImmutableSet evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { + ImmutableSet.Builder additionalPermissionsRequired = ImmutableSet.builder(); + + if (!isClusterPerm(originalAction)) { + additionalPermissionsRequired.add(originalAction); + } + + if (request instanceof ClusterSearchShardsRequest) { + additionalPermissionsRequired.add(SearchAction.NAME); + } + + if (request instanceof BulkShardRequest) { + BulkShardRequest bsr = (BulkShardRequest) request; + for (BulkItemRequest bir : bsr.items()) { + switch (bir.request().opType()) { + case CREATE: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case INDEX: + additionalPermissionsRequired.add(IndexAction.NAME); + break; + case DELETE: + additionalPermissionsRequired.add(DeleteAction.NAME); + break; + case UPDATE: + additionalPermissionsRequired.add(UpdateAction.NAME); + break; + } + } + } + + if (request instanceof IndicesAliasesRequest) { + IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; + for (AliasActions bir : bsr.getAliasActions()) { + switch (bir.actionType()) { + case REMOVE_INDEX: + additionalPermissionsRequired.add(DeleteIndexAction.NAME); + break; + default: + break; + } + } + } + + if (request instanceof CreateIndexRequest) { + CreateIndexRequest cir = (CreateIndexRequest) request; + if (cir.aliases() != null && !cir.aliases().isEmpty()) { + additionalPermissionsRequired.add(IndicesAliasesAction.NAME); + } + } + + if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + additionalPermissionsRequired.addAll(ConfigConstants.SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES); + } + + ImmutableSet result = additionalPermissionsRequired.build(); + + if (result.size() > 1) { + traceAction("Additional permissions required: {}", result); + log.debug("Additional permissions required: {}", result); + } + + return result; + } + + @Override + public boolean isClusterPermission(String action) { + return isClusterPerm(action); + } + + public static boolean isClusterPerm(String action0) { + return (action0.startsWith("cluster:") + || action0.startsWith("indices:admin/template/") + || action0.startsWith("indices:admin/index_template/") + || action0.startsWith(SearchScrollAction.NAME) + || (action0.equals(BulkAction.NAME)) + || (action0.equals(MultiGetAction.NAME)) + || (action0.startsWith(MultiSearchAction.NAME)) + || (action0.equals(MultiTermVectorsAction.NAME)) + || (action0.equals(ReindexAction.NAME)) + || (action0.equals(RenderSearchTemplateAction.NAME))); + } + + private boolean checkFilteredAliases(Resolved requestedResolved, String action, boolean isDebugEnabled) { + final String faMode = this.filteredAliasMode;// getConfigSettings().dynamic.filtered_alias_mode; + + if (!"disallow".equals(faMode)) { + return false; + } + + if (!ACTION_MATCHER.test(action)) { + return false; + } + + Iterable indexMetaDataCollection; + + if (requestedResolved.isLocalAll()) { + indexMetaDataCollection = new Iterable() { + @Override + public Iterator iterator() { + return clusterStateSupplier.get().getMetadata().getIndices().values().iterator(); + } + }; + } else { + Set indexMetaDataSet = new HashSet<>(requestedResolved.getAllIndices().size()); + + for (String requestAliasOrIndex : requestedResolved.getAllIndices()) { + IndexMetadata indexMetaData = clusterStateSupplier.get().getMetadata().getIndices().get(requestAliasOrIndex); + if (indexMetaData == null) { + log.debug("{} does not exist in cluster metadata", requestAliasOrIndex); + continue; + } + + indexMetaDataSet.add(indexMetaData); + } + + indexMetaDataCollection = indexMetaDataSet; + } + // check filtered aliases + for (IndexMetadata indexMetaData : indexMetaDataCollection) { + + final List filteredAliases = new ArrayList(); + + final Map aliases = indexMetaData.getAliases(); + + if (aliases != null && aliases.size() > 0) { + log.debug("Aliases for {}: {}", indexMetaData.getIndex().getName(), aliases); + + final Iterator it = aliases.keySet().iterator(); + while (it.hasNext()) { + final String alias = it.next(); + final AliasMetadata aliasMetadata = aliases.get(alias); + + if (aliasMetadata != null && aliasMetadata.filteringRequired()) { + filteredAliases.add(aliasMetadata); + log.debug("{} is a filtered alias {}", alias, aliasMetadata.getFilter()); + + } else { + log.debug("{} is not an alias or does not have a filter", alias); + + } + } + } + + if (filteredAliases.size() > 1 && ACTION_MATCHER.test(action)) { + log.error( + "More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", + filteredAliases.size(), + indexMetaData.getIndex().getName(), + toString(filteredAliases) + ); + return true; + } + } // end-for + + return false; + } + + private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { + String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); + + if (docAllowListHeader == null) { + return false; + } + + if (!(request instanceof GetRequest)) { + return false; + } + + try { + DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); + GetRequest getRequest = (GetRequest) request; + + if (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { + log.debug("Request {} is allowed by {}", request, documentAllowList); + return true; + } else { + return false; + } + + } catch (Exception e) { + log.error("Error while handling document allow list: {}", docAllowListHeader, e); + return false; + } + } + + private List toString(List aliases) { + if (aliases == null || aliases.size() == 0) { + return Collections.emptyList(); + } + + final List ret = new ArrayList<>(aliases.size()); + + for (final AliasMetadata amd : aliases) { + if (amd != null) { + ret.add(amd.alias()); + } + } + + return Collections.unmodifiableList(ret); + } + + private static Map createActionPrivileges( + Map pluginIdToRolePrivileges, + FlattenedActionGroups staticActionGroups + ) { + Map result = new HashMap<>(pluginIdToRolePrivileges.size()); + + for (Map.Entry entry : pluginIdToRolePrivileges.entrySet()) { + result.put(entry.getKey(), new SubjectBasedActionPrivileges(entry.getValue(), staticActionGroups)); + } + + return result; + } + + private static boolean isDnfofEnabled(ConfigV7 generalConfiguration) { + return generalConfiguration.dynamic != null && generalConfiguration.dynamic.do_not_fail_on_forbidden; + } + + private static boolean isDnfofEmptyEnabled(ConfigV7 generalConfiguration) { + return generalConfiguration.dynamic != null && generalConfiguration.dynamic.do_not_fail_on_forbidden_empty; + } + + private static String getFilteredAliasMode(ConfigV7 generalConfiguration) { + return generalConfiguration.dynamic != null ? generalConfiguration.dynamic.filtered_alias_mode : "none"; + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java index 0ae809bc9d..e82a512f87 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -32,7 +32,6 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; -import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; @@ -80,10 +79,8 @@ public ReplaceResult replaceDashboardsIndex( final ActionRequest request, final String action, final User user, - final DynamicConfigModel config, final Resolved requestedResolved, - final PrivilegesEvaluationContext context, - final TenantPrivileges tenantPrivileges + final PrivilegesEvaluationContext context ) { throw new RuntimeException("not implemented"); } diff --git a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java index 5274ad3456..4890dd5a89 100644 --- a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java @@ -20,14 +20,14 @@ public class RestLayerPrivilegesEvaluator { protected final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; - public RestLayerPrivilegesEvaluator(PrivilegesEvaluator privilegesEvaluator) { - this.privilegesEvaluator = privilegesEvaluator; + public RestLayerPrivilegesEvaluator(PrivilegesConfiguration privilegesConfiguration) { + this.privilegesConfiguration = privilegesConfiguration; } public PrivilegesEvaluatorResponse evaluate(final User user, final String routeName, final Set actions) { - PrivilegesEvaluationContext context = privilegesEvaluator.createContext(user, routeName); + PrivilegesEvaluationContext context = privilegesConfiguration.privilegesEvaluator().createContext(user, routeName); final boolean isDebugEnabled = log.isDebugEnabled(); if (isDebugEnabled) { diff --git a/src/main/java/org/opensearch/security/privileges/RoleMapper.java b/src/main/java/org/opensearch/security/privileges/RoleMapper.java new file mode 100644 index 0000000000..5a5011968f --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/RoleMapper.java @@ -0,0 +1,25 @@ +/* + * 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.privileges; + +import com.google.common.collect.ImmutableSet; + +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.security.user.User; + +/** + * A general interface for components that map users to their effective roles. + */ +@FunctionalInterface +public interface RoleMapper { + ImmutableSet map(User user, TransportAddress caller); +} diff --git a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java index 23612e1a52..ca5cf985d3 100644 --- a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java @@ -27,6 +27,7 @@ package org.opensearch.security.privileges; import java.util.List; +import java.util.function.Supplier; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -35,7 +36,6 @@ import org.opensearch.action.admin.cluster.snapshots.restore.RestoreSnapshotRequest; import org.opensearch.common.settings.Settings; import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.SnapshotRestoreHelper; import org.opensearch.tasks.Task; @@ -47,8 +47,13 @@ public class SnapshotRestoreEvaluator { private final String securityIndex; private final AuditLog auditLog; private final boolean restoreSecurityIndexEnabled; + private final Supplier isLocalNodeElectedClusterManagerSupplier; - public SnapshotRestoreEvaluator(final Settings settings, AuditLog auditLog) { + public SnapshotRestoreEvaluator( + final Settings settings, + AuditLog auditLog, + Supplier isLocalNodeElectedClusterManagerSupplier + ) { this.enableSnapshotRestorePrivilege = settings.getAsBoolean( ConfigConstants.SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, ConfigConstants.SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE @@ -60,13 +65,13 @@ public SnapshotRestoreEvaluator(final Settings settings, AuditLog auditLog) { ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); this.auditLog = auditLog; + this.isLocalNodeElectedClusterManagerSupplier = isLocalNodeElectedClusterManagerSupplier; } public PrivilegesEvaluatorResponse evaluate( final ActionRequest request, final Task task, final String action, - final ClusterInfoHolder clusterInfoHolder, final PrivilegesEvaluatorResponse presponse ) { @@ -88,7 +93,7 @@ public PrivilegesEvaluatorResponse evaluate( return presponse; } - if (clusterInfoHolder.isLocalNodeElectedClusterManager() == Boolean.FALSE) { + if (!isLocalNodeElectedClusterManagerSupplier.get()) { presponse.allowed = true; return presponse.markComplete(); } diff --git a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java index 68cd42a7a8..fb28eab972 100644 --- a/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SystemIndexAccessEvaluator.java @@ -49,7 +49,7 @@ import org.opensearch.security.user.User; import org.opensearch.tasks.Task; -import static org.opensearch.security.privileges.PrivilegesEvaluator.isClusterPerm; +import static org.opensearch.security.privileges.PrivilegesEvaluatorImpl.isClusterPerm; /** * This class performs authorization on requests targeting system indices diff --git a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java index 91b21e6ba6..ae1a9ed0b3 100644 --- a/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java +++ b/src/main/java/org/opensearch/security/privileges/dlsfls/DlsFlsBaseContext.java @@ -12,8 +12,8 @@ import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.configuration.AdminDNs; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.user.User; @@ -22,12 +22,12 @@ * Node global context data for DLS/FLS. The lifecycle of an instance of this class is equal to the lifecycle of a running node. */ public class DlsFlsBaseContext { - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; private final AdminDNs adminDNs; - public DlsFlsBaseContext(PrivilegesEvaluator privilegesEvaluator, ThreadContext threadContext, AdminDNs adminDNs) { - this.privilegesEvaluator = privilegesEvaluator; + public DlsFlsBaseContext(PrivilegesConfiguration privilegesConfiguration, ThreadContext threadContext, AdminDNs adminDNs) { + this.privilegesConfiguration = privilegesConfiguration; this.threadContext = threadContext; this.adminDNs = adminDNs; } @@ -46,7 +46,7 @@ public PrivilegesEvaluationContext getPrivilegesEvaluationContext() { return null; } - PrivilegesEvaluationContext ctx = this.privilegesEvaluator.createContext(user, null); + PrivilegesEvaluationContext ctx = this.privilegesConfiguration.privilegesEvaluator().createContext(user, null); threadContext.putTransient("tmp_dls_fls_ctx", ctx); return ctx; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 179d9814a7..bbb6d46708 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -28,7 +28,6 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -52,7 +51,6 @@ public class ResourceAccessHandler { private final ThreadContext threadContext; private final ResourceSharingIndexHandler resourceSharingIndexHandler; private final AdminDNs adminDNs; - private final PrivilegesEvaluator privilegesEvaluator; private final ResourcePluginInfo resourcePluginInfo; @Inject @@ -60,13 +58,11 @@ public ResourceAccessHandler( final ThreadPool threadPool, final ResourceSharingIndexHandler resourceSharingIndexHandler, AdminDNs adminDns, - PrivilegesEvaluator evaluator, ResourcePluginInfo resourcePluginInfo ) { this.threadContext = threadPool.getThreadContext(); this.resourceSharingIndexHandler = resourceSharingIndexHandler; this.adminDNs = adminDns; - this.privilegesEvaluator = evaluator; this.resourcePluginInfo = resourcePluginInfo; } diff --git a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java index 7353633071..61dcd353da 100644 --- a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java @@ -40,7 +40,12 @@ import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -78,7 +83,9 @@ public class DashboardsInfoAction extends BaseRestHandler { .build(); private final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator evaluator; + private final PrivilegesConfiguration privilegesConfiguration; + private final ConfigurationRepository configurationRepository; + private final ThreadContext threadContext; private final OpensearchDynamicSetting resourceSharingEnabledSetting; @@ -89,14 +96,16 @@ public class DashboardsInfoAction extends BaseRestHandler { public static final String DEFAULT_PASSWORD_REGEX = "(?=.*[A-Z])(?=.*[^a-zA-Z\\d])(?=.*[0-9])(?=.*[a-z]).{8,}"; public DashboardsInfoAction( - final PrivilegesEvaluator evaluator, + final PrivilegesConfiguration privilegesConfiguration, + final ConfigurationRepository configurationRepository, final ThreadPool threadPool, OpensearchDynamicSetting resourceSharingEnabledSetting ) { super(); this.resourceSharingEnabledSetting = resourceSharingEnabledSetting; this.threadContext = threadPool.getThreadContext(); - this.evaluator = evaluator; + this.privilegesConfiguration = privilegesConfiguration; + this.configurationRepository = configurationRepository; } @Override @@ -122,16 +131,22 @@ public void accept(RestChannel channel) throws Exception { final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + DashboardsMultiTenancyConfiguration multiTenancyConfiguration = privilegesConfiguration.multiTenancyConfiguration(); + builder.startObject(); builder.field("user_name", user == null ? null : user.getName()); - builder.field("not_fail_on_forbidden_enabled", evaluator.notFailOnForbiddenEnabled()); - builder.field("opensearch_dashboards_mt_enabled", evaluator.multitenancyEnabled()); - builder.field("opensearch_dashboards_index", evaluator.dashboardsIndex()); - builder.field("opensearch_dashboards_server_user", evaluator.dashboardsServerUsername()); - builder.field("multitenancy_enabled", evaluator.multitenancyEnabled()); - builder.field("private_tenant_enabled", evaluator.privateTenantEnabled()); - builder.field("default_tenant", evaluator.dashboardsDefaultTenant()); - builder.field("sign_in_options", evaluator.getSignInOptions()); + builder.field( + "not_fail_on_forbidden_enabled", + privilegesConfiguration.privilegesEvaluator().notFailOnForbiddenEnabled() + ); + builder.field("opensearch_dashboards_mt_enabled", multiTenancyConfiguration.multitenancyEnabled()); + builder.field("opensearch_dashboards_index", multiTenancyConfiguration.dashboardsIndex()); + builder.field("opensearch_dashboards_server_user", multiTenancyConfiguration.dashboardsServerUsername()); + builder.field("multitenancy_enabled", multiTenancyConfiguration.multitenancyEnabled()); + builder.field("private_tenant_enabled", multiTenancyConfiguration.privateTenantEnabled()); + builder.field("default_tenant", multiTenancyConfiguration.dashboardsDefaultTenant()); + builder.field("sign_in_options", getSignInOptions()); + builder.field( "password_validation_error_message", client.settings().get(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, DEFAULT_PASSWORD_MESSAGE) @@ -167,4 +182,13 @@ public String getName() { return "Kibana Info Action"; } + private List getSignInOptions() { + ConfigV7 generalConfig = configurationRepository.getConfiguration(CType.CONFIG).getCEntry(CType.CONFIG.name()); + if (generalConfig != null && generalConfig.dynamic != null && generalConfig.dynamic.kibana != null) { + return generalConfig.dynamic.kibana.sign_in_options; + } else { + return new ConfigV7.Kibana().sign_in_options; + } + } + } diff --git a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java index 3de69a1a34..24e9732e0c 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java @@ -40,6 +40,7 @@ import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.transport.client.node.NodeClient; @@ -67,17 +68,17 @@ public class SecurityHealthAction extends BaseRestHandler { ); private final BackendRegistry registry; - private final PrivilegesEvaluator privilegesEvaluator; + private final PrivilegesConfiguration privilegesConfiguration; public SecurityHealthAction( final Settings settings, final RestController controller, final BackendRegistry registry, - final PrivilegesEvaluator privilegesEvaluator + final PrivilegesConfiguration privilegesConfiguration ) { super(); this.registry = registry; - this.privilegesEvaluator = privilegesEvaluator; + this.privilegesConfiguration = privilegesConfiguration; } @Override @@ -108,7 +109,10 @@ public void accept(RestChannel channel) throws Exception { builder.startObject(); - if ("strict".equalsIgnoreCase(mode) && !(registry.isInitialized() && privilegesEvaluator.isInitialized())) { + if ("strict".equalsIgnoreCase(mode) + && !(registry.isInitialized() + && privilegesConfiguration.privilegesEvaluator() + .type() != PrivilegesEvaluator.PrivilegesEvaluatorType.NOT_INITIALIZED)) { status = "DOWN"; message = "Not initialized"; restStatus = RestStatus.SERVICE_UNAVAILABLE; diff --git a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java index 41b8cc98be..6932462f48 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java @@ -48,8 +48,8 @@ import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestController; import org.opensearch.rest.RestRequest; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.PrivilegesEvaluationContext; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.support.Base64Helper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -79,18 +79,18 @@ public class SecurityInfoAction extends BaseRestHandler { ); private final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator evaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; public SecurityInfoAction( final Settings settings, final RestController controller, - final PrivilegesEvaluator evaluator, + final PrivilegesConfiguration privilegesConfiguration, final ThreadPool threadPool ) { super(); this.threadContext = threadPool.getThreadContext(); - this.evaluator = evaluator; + this.privilegesConfiguration = privilegesConfiguration; } @Override @@ -122,7 +122,7 @@ public void accept(RestChannel channel) throws Exception { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); final TransportAddress remoteAddress = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - PrivilegesEvaluationContext context = evaluator.createContext(user, null); + PrivilegesEvaluationContext context = privilegesConfiguration.privilegesEvaluator().createContext(user, null); builder.startObject(); builder.field("user", user == null ? null : user.toString()); @@ -132,7 +132,7 @@ public void accept(RestChannel channel) throws Exception { builder.field("backend_roles", user == null ? null : user.getRoles()); builder.field("custom_attribute_names", user == null ? null : user.getCustomAttributesMap().keySet()); builder.field("roles", context.getMappedRoles()); - builder.field("tenants", evaluator.tenantPrivileges().tenantMap(context)); + builder.field("tenants", privilegesConfiguration.tenantPrivileges().tenantMap(context)); builder.field("principal", (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL)); builder.field("peer_certificates", certs != null && certs.length > 0 ? certs.length + "" : "0"); builder.field("sso_logout_url", (String) threadContext.getTransient(ConfigConstants.SSO_LOGOUT_URL)); diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index 47c4b61cc2..ecf1561992 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -49,7 +49,9 @@ import org.opensearch.rest.RestRequest; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.DashboardsMultiTenancyConfiguration; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.TenantPrivileges; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.RoleMappings; import org.opensearch.security.securityconf.impl.CType; @@ -82,7 +84,7 @@ public class TenantInfoAction extends BaseRestHandler { ); private final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator evaluator; + private final PrivilegesConfiguration privilegesConfiguration; private final ThreadContext threadContext; private final ClusterService clusterService; private final AdminDNs adminDns; @@ -91,7 +93,7 @@ public class TenantInfoAction extends BaseRestHandler { public TenantInfoAction( final Settings settings, final RestController controller, - final PrivilegesEvaluator evaluator, + final PrivilegesConfiguration privilegesConfiguration, final ThreadPool threadPool, final ClusterService clusterService, final AdminDNs adminDns, @@ -99,7 +101,7 @@ public TenantInfoAction( ) { super(); this.threadContext = threadPool.getThreadContext(); - this.evaluator = evaluator; + this.privilegesConfiguration = privilegesConfiguration; this.clusterService = clusterService; this.adminDns = adminDns; this.configurationRepository = configurationRepository; @@ -132,12 +134,14 @@ public void accept(RestChannel channel) throws Exception { if (!isAuthorized()) { response = new BytesRestResponse(RestStatus.FORBIDDEN, ""); } else { + DashboardsMultiTenancyConfiguration multiTenancyConfiguration = privilegesConfiguration.multiTenancyConfiguration(); + TenantPrivileges tenantPrivileges = privilegesConfiguration.tenantPrivileges(); builder.startObject(); final SortedMap lookup = clusterService.state().metadata().getIndicesLookup(); for (final String indexOrAlias : lookup.keySet()) { - final String tenant = tenantNameForIndex(indexOrAlias); + final String tenant = tenantNameForIndex(indexOrAlias, multiTenancyConfiguration, tenantPrivileges); if (tenant != null) { builder.field(indexOrAlias, tenant); } @@ -172,8 +176,10 @@ private boolean isAuthorized() { return false; } + DashboardsMultiTenancyConfiguration multiTenancyConfiguration = privilegesConfiguration.multiTenancyConfiguration(); + // check if the user is a kibanauser or super admin - if (user.getName().equals(evaluator.dashboardsServerUsername()) || adminDns.isAdmin(user)) { + if (user.getName().equals(multiTenancyConfiguration.dashboardsServerUsername()) || adminDns.isAdmin(user)) { return true; } @@ -182,7 +188,7 @@ private boolean isAuthorized() { // check if dashboardsOpenSearchRole is present in RolesMapping and if yes, check if user is a part of this role if (rolesMappingConfiguration != null) { - String dashboardsOpenSearchRole = evaluator.dashboardsOpenSearchRole(); + String dashboardsOpenSearchRole = multiTenancyConfiguration.dashboardsOpenSearchRole(); if (Strings.isNullOrEmpty(dashboardsOpenSearchRole)) { return false; } @@ -201,13 +207,17 @@ private final SecurityDynamicConfiguration load(final CType config, boolea return DynamicConfigFactory.addStatics(loaded); } - private String tenantNameForIndex(String index) { + private String tenantNameForIndex( + String index, + DashboardsMultiTenancyConfiguration multiTenancyConfiguration, + TenantPrivileges tenantPrivileges + ) { String[] indexParts; if (index == null || (indexParts = index.split("_")).length != 3) { return null; } - if (!indexParts[0].equals(evaluator.dashboardsIndex())) { + if (!indexParts[0].equals(multiTenancyConfiguration.dashboardsIndex())) { return null; } @@ -215,7 +225,7 @@ private String tenantNameForIndex(String index) { final int expectedHash = Integer.parseInt(indexParts[1]); final String sanitizedName = indexParts[2]; - for (String tenant : evaluator.tenantPrivileges().allTenantNames()) { + for (String tenant : tenantPrivileges.allTenantNames()) { if (tenant.hashCode() == expectedHash && sanitizedName.equals(tenant.toLowerCase().replaceAll("[^a-z0-9]+", ""))) { return tenant; } diff --git a/src/main/java/org/opensearch/security/support/HostResolverMode.java b/src/main/java/org/opensearch/security/support/HostResolverMode.java index 00ce6e9117..ef23381826 100644 --- a/src/main/java/org/opensearch/security/support/HostResolverMode.java +++ b/src/main/java/org/opensearch/security/support/HostResolverMode.java @@ -13,7 +13,8 @@ public enum HostResolverMode { IP_HOSTNAME("ip-hostname"), - IP_HOSTNAME_LOOKUP("ip-hostname-lookup"); + IP_HOSTNAME_LOOKUP("ip-hostname-lookup"), + DISABLED("disabled"); private final String value; @@ -24,4 +25,14 @@ public enum HostResolverMode { public String getValue() { return value; } + + public static HostResolverMode fromConfig(String hostResolverModeConfig) { + if (hostResolverModeConfig == null || hostResolverModeConfig.equalsIgnoreCase(IP_HOSTNAME.value)) { + return HostResolverMode.IP_HOSTNAME; + } else if (hostResolverModeConfig.equalsIgnoreCase(IP_HOSTNAME_LOOKUP.value)) { + return HostResolverMode.IP_HOSTNAME_LOOKUP; + } else { + return HostResolverMode.DISABLED; + } + } } diff --git a/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java new file mode 100644 index 0000000000..35abbf8844 --- /dev/null +++ b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java @@ -0,0 +1,101 @@ +/* + * 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.user; + +import java.util.HashMap; +import java.util.StringJoiner; + +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.Strings; +import org.opensearch.security.privileges.PrivilegesConfiguration; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.TenantPrivileges; +import org.opensearch.security.support.Base64Helper; +import org.opensearch.security.support.SecuritySettings; + +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; +import static org.opensearch.security.support.ConfigConstants.USER_ATTRIBUTE_SERIALIZATION_ENABLED; +import static org.opensearch.security.support.ConfigConstants.USER_ATTRIBUTE_SERIALIZATION_ENABLED_DEFAULT; +import static org.opensearch.security.support.SecurityUtils.escapePipe; + +/** + * Functionality to add parseable information about the current user to the thread context. Usually called + * in the SecurityFilter. + *

+ * Moved from https://github.com/opensearch-project/security/blob/d29095f26dba1a26308c69b608dc926bd40c0f52/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java#L293 + */ +public class ThreadContextUserInfo { + private static final String READ_ACCESS = "READ"; + private static final String WRITE_ACCESS = "WRITE"; + private static final String NO_ACCESS = "NONE"; + private static final String GLOBAL_TENANT = "global_tenant"; + + private volatile boolean userAttributeSerializationEnabled; + private final ThreadContext threadContext; + private final PrivilegesConfiguration privilegesConfiguration; + + public ThreadContextUserInfo( + ThreadContext threadContext, + PrivilegesConfiguration privilegesConfiguration, + ClusterSettings clusterSettings, + Settings settings + ) { + this.threadContext = threadContext; + this.userAttributeSerializationEnabled = settings.getAsBoolean( + USER_ATTRIBUTE_SERIALIZATION_ENABLED, + USER_ATTRIBUTE_SERIALIZATION_ENABLED_DEFAULT + ); + this.privilegesConfiguration = privilegesConfiguration; + clusterSettings.addSettingsUpdateConsumer( + SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING, + newIsUserAttributeSerializationEnabled -> { + userAttributeSerializationEnabled = newIsUserAttributeSerializationEnabled; + } + ); + } + + public void setUserInfoInThreadContext(PrivilegesEvaluationContext context) { + if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { + StringJoiner joiner = new StringJoiner("|"); + // Escape any pipe characters in the values before joining + joiner.add(escapePipe(context.getUser().getName())); + joiner.add(escapePipe(String.join(",", context.getUser().getRoles()))); + joiner.add(escapePipe(String.join(",", context.getMappedRoles()))); + + String requestedTenant = context.getUser().getRequestedTenant(); + joiner.add(requestedTenant); + + String tenantAccessToCheck = getTenancyAccess(context); + joiner.add(tenantAccessToCheck); + + if (userAttributeSerializationEnabled) { + joiner.add(Base64Helper.serializeObject(new HashMap<>(context.getUser().getCustomAttributesMap()))); + } + + threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); + } + } + + private String getTenancyAccess(PrivilegesEvaluationContext context) { + String requestedTenant = context.getUser().getRequestedTenant(); + TenantPrivileges tenantPrivileges = privilegesConfiguration.tenantPrivileges(); + final String tenant = Strings.isNullOrEmpty(requestedTenant) ? GLOBAL_TENANT : requestedTenant; + if (tenantPrivileges.hasTenantPrivilege(context, tenant, TenantPrivileges.ActionType.WRITE)) { + return WRITE_ACCESS; + } else if (tenantPrivileges.hasTenantPrivilege(context, tenant, TenantPrivileges.ActionType.READ)) { + return READ_ACCESS; + } else { + return NO_ACCESS; + } + } +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java index bbe1bf90f8..e8172d7723 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluatorTest.java @@ -20,7 +20,6 @@ import org.opensearch.common.settings.Settings; import org.opensearch.rest.RestRequest; import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.threadpool.ThreadPool; @@ -37,7 +36,7 @@ public void setUp() { this.privilegesEvaluator = new RestApiPrivilegesEvaluator( Settings.EMPTY, mock(AdminDNs.class), - mock(PrivilegesEvaluator.class), + (user, caller) -> user.getSecurityRoles(), mock(PrincipalExtractor.class), mock(Path.class), mock(ThreadPool.class) diff --git a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java index 5a311bec8e..f23929605c 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityFilterTests.java @@ -30,7 +30,7 @@ import org.opensearch.security.configuration.CompatConfig; import org.opensearch.security.configuration.DlsFlsRequestValve; import org.opensearch.security.http.XFFResolver; -import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesConfiguration; import org.opensearch.security.privileges.ResourceAccessEvaluator; import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.support.ConfigConstants; @@ -80,7 +80,7 @@ public static Collection data() { public void testImmutableIndicesWildcardMatcher() { final SecurityFilter filter = new SecurityFilter( settings, - mock(PrivilegesEvaluator.class), + mock(PrivilegesConfiguration.class), mock(AdminDNs.class), mock(DlsFlsRequestValve.class), mock(AuditLog.class), @@ -105,7 +105,7 @@ public void testUnexepectedCausesAreNotSendToCallers() { final SecurityFilter filter = new SecurityFilter( settings, - mock(PrivilegesEvaluator.class), + mock(PrivilegesConfiguration.class), mock(AdminDNs.class), mock(DlsFlsRequestValve.class), auditLog, diff --git a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java index 66aa954de4..e04f659490 100644 --- a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java @@ -9,40 +9,19 @@ package org.opensearch.security.privileges; import java.util.List; -import java.util.Set; -import java.util.function.Supplier; import com.google.common.collect.ImmutableList; -import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.OpenSearchSecurityException; -import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.security.auditlog.AuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; -import org.opensearch.security.configuration.ConfigurationRepository; -import org.opensearch.security.resolver.IndexResolverReplacer; -import org.opensearch.threadpool.ThreadPool; - -import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.security.privileges.PrivilegesEvaluator.DNFOF_MATCHER; -import static org.opensearch.security.privileges.PrivilegesEvaluator.isClusterPerm; -import static org.opensearch.security.support.SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING; +import static org.opensearch.security.privileges.PrivilegesEvaluatorImpl.DNFOF_MATCHER; +import static org.opensearch.security.privileges.PrivilegesEvaluatorImpl.isClusterPerm; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class PrivilegesEvaluatorUnitTest { @@ -122,66 +101,6 @@ public class PrivilegesEvaluatorUnitTest { "indices:monitor/upgrade" ); - @Mock - private ClusterService clusterService; - - @Mock - private ThreadPool threadPool; - - @Mock - private ConfigurationRepository configurationRepository; - - @Mock - private IndexNameExpressionResolver resolver; - - @Mock - private AuditLog auditLog; - - @Mock - private PrivilegesInterceptor privilegesInterceptor; - - @Mock - private ClusterInfoHolder clusterInfoHolder; - - @Mock - private IndexResolverReplacer irr; - - @Mock - private NamedXContentRegistry namedXContentRegistry; - - @Mock - private ClusterState clusterState; - - private Settings settings; - private Supplier clusterStateSupplier; - private ThreadContext threadContext; - private PrivilegesEvaluator privilegesEvaluator; - - @Before - public void setUp() { - settings = Settings.builder().build(); - clusterStateSupplier = () -> clusterState; - threadContext = new ThreadContext(Settings.EMPTY); - - when(clusterService.getClusterSettings()).thenReturn( - new ClusterSettings(Settings.EMPTY, Set.of(USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING)) - ); - - privilegesEvaluator = new PrivilegesEvaluator( - clusterService, - clusterStateSupplier, - threadPool, - threadContext, - configurationRepository, - resolver, - auditLog, - settings, - privilegesInterceptor, - clusterInfoHolder, - irr - ); - } - @Test public void testClusterPerm() { String multiSearchTemplate = "indices:data/read/msearch/template"; @@ -214,20 +133,4 @@ public void testDnfofPermissions_positive() { } } - @Test - public void testEvaluate_NotInitialized_ExceptionThrown() { - when(clusterInfoHolder.hasClusterManager()).thenReturn(true); - OpenSearchSecurityException exception = assertThrows( - OpenSearchSecurityException.class, - () -> privilegesEvaluator.evaluate(null) - ); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); - - when(clusterInfoHolder.hasClusterManager()).thenReturn(false); - exception = assertThrows( - OpenSearchSecurityException.class, - () -> privilegesEvaluator.evaluate(null) - ); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized. Cluster manager not present")); - } } diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index cf52f9ea54..38433e4fd2 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -12,7 +12,6 @@ package org.opensearch.security.privileges; import java.util.Set; -import java.util.TreeMap; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -24,49 +23,29 @@ import org.junit.runner.RunWith; import org.opensearch.OpenSearchSecurityException; -import org.opensearch.cluster.ClusterState; -import org.opensearch.cluster.metadata.IndexNameExpressionResolver; -import org.opensearch.cluster.metadata.Metadata; -import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.action.ActionRequest; +import org.opensearch.action.support.ActionRequestMetadata; import org.opensearch.cluster.service.ClusterService; -import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.security.auditlog.NullAuditLog; -import org.opensearch.security.configuration.ClusterInfoHolder; -import org.opensearch.security.securityconf.ConfigModel; -import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.privileges.actionlevel.RoleBasedActionPrivileges; +import org.opensearch.security.securityconf.FlattenedActionGroups; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.user.User; -import org.opensearch.threadpool.ThreadPool; +import org.opensearch.tasks.Task; -import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.quality.Strictness; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.security.support.SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING; import static org.junit.Assert.assertThrows; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.withSettings; @RunWith(MockitoJUnitRunner.class) public class RestLayerPrivilegesEvaluatorTest { - @Mock(strictness = Mock.Strictness.LENIENT) - private ClusterService clusterService; - @Mock - private ConfigModel configModel; - @Mock - private DynamicConfigModel dynamicConfigModel; - @Mock - private ClusterInfoHolder clusterInfoHolder; - - private static final User TEST_USER = new User("test_user"); + private static final User TEST_USER = new User("test_user").withSecurityRoles(Set.of("test_role")); private void setLoggingLevel(final Level level) { final Logger restLayerPrivilegesEvaluatorLogger = LogManager.getLogger(RestLayerPrivilegesEvaluator.class); @@ -75,15 +54,7 @@ private void setLoggingLevel(final Level level) { @Before public void setUp() { - when(clusterService.localNode()).thenReturn(mock(DiscoveryNode.class, withSettings().strictness(Strictness.LENIENT))); - when(configModel.mapSecurityRoles(TEST_USER, null)).thenReturn(Set.of("test_role")); setLoggingLevel(Level.DEBUG); // Enable debug logging scenarios for verification - ClusterState clusterState = mock(ClusterState.class); - when(clusterService.state()).thenReturn(clusterState); - when(clusterService.getClusterSettings()).thenReturn(new ClusterSettings(Settings.EMPTY, Set.of(USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING))); - Metadata metadata = mock(Metadata.class); - when(clusterState.metadata()).thenReturn(metadata); - when(metadata.getIndicesLookup()).thenReturn(new TreeMap<>()); } @After @@ -98,8 +69,8 @@ public void testEvaluate_Initialized_Success() throws Exception { " cluster_permissions:\n" + // " - any", CType.ROLES); - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(roles); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesConfiguration privilegesConfiguration = createPrivilegesConfiguration(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); @@ -109,18 +80,16 @@ public void testEvaluate_Initialized_Success() throws Exception { @Test public void testEvaluate_NotInitialized_NullModel_ExceptionThrown() { - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(null); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); - when(clusterInfoHolder.hasClusterManager()).thenReturn(true); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator( + new PrivilegesConfiguration(new PrivilegesEvaluator.NotInitialized(() -> { + return "PrivilegesEvaluator not initialized"; + })) + ); OpenSearchSecurityException exception = assertThrows( OpenSearchSecurityException.class, () -> restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", null) ); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); - - when(clusterInfoHolder.hasClusterManager()).thenReturn(false); - exception = assertThrows(OpenSearchSecurityException.class, () -> restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", null)); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized. Cluster manager not present")); + assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized: PrivilegesEvaluator not initialized")); } @Test @@ -129,8 +98,8 @@ public void testEvaluate_Successful_NewPermission() throws Exception { SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // " cluster_permissions:\n" + // " - hw:greet", CType.ROLES); - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(roles); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesConfiguration privilegesConfiguration = createPrivilegesConfiguration(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(true)); } @@ -141,8 +110,8 @@ public void testEvaluate_Successful_LegacyPermission() throws Exception { SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // " cluster_permissions:\n" + // " - cluster:admin/opensearch/hw/greet", CType.ROLES); - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(roles); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesConfiguration privilegesConfiguration = createPrivilegesConfiguration(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(true)); } @@ -153,36 +122,81 @@ public void testEvaluate_Unsuccessful() throws Exception { SecurityDynamicConfiguration roles = SecurityDynamicConfiguration.fromYaml("test_role:\n" + // " cluster_permissions:\n" + // " - other_action", CType.ROLES); - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(roles); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesConfiguration privilegesConfiguration = createPrivilegesConfiguration(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesConfiguration); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(false)); } + PrivilegesConfiguration createPrivilegesConfiguration(SecurityDynamicConfiguration roles) { + return new PrivilegesConfiguration(createPrivilegesEvaluator(roles)); + } + PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration roles) { - PrivilegesEvaluator privilegesEvaluator = new PrivilegesEvaluator( - clusterService, - () -> clusterService.state(), - mock(ThreadPool.class), - new ThreadContext(Settings.EMPTY), - null, - new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), - new NullAuditLog(), - Settings.EMPTY, - null, - clusterInfoHolder, - null - ); - privilegesEvaluator.onConfigModelChanged(configModel); // Defaults to the mocked config model - privilegesEvaluator.onDynamicConfigModelChanged(dynamicConfigModel); - - if (roles != null) { - privilegesEvaluator.updateConfiguration( - SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS), - roles, - SecurityDynamicConfiguration.empty(CType.TENANTS) - ); - } - return privilegesEvaluator; + ActionPrivileges actionPrivileges = new RoleBasedActionPrivileges(roles, FlattenedActionGroups.EMPTY, Settings.EMPTY); + + return new PrivilegesEvaluator() { + + @Override + public PrivilegesEvaluationContext createContext( + User user, + String action, + ActionRequest actionRequest, + ActionRequestMetadata actionRequestMetadata, + Task task + ) { + return new PrivilegesEvaluationContext( + user, + user.getSecurityRoles(), + action, + actionRequest, + task, + null, + null, + null, + actionPrivileges + ); + } + + @Override + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + return null; + } + + @Override + public boolean isClusterPermission(String action) { + return false; + } + + @Override + public void updateConfiguration( + FlattenedActionGroups actionGroups, + SecurityDynamicConfiguration rolesConfiguration, + ConfigV7 generalConfiguration + ) { + + } + + @Override + public void updateClusterStateMetadata(ClusterService clusterService) { + + } + + @Override + public void shutdown() { + + } + + @Override + public boolean notFailOnForbiddenEnabled() { + return false; + } + + @Override + public PrivilegesEvaluatorType type() { + return PrivilegesEvaluatorType.STANDARD; + } + }; + } } diff --git a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java index e7a90a105e..c1b44c31f9 100644 --- a/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java +++ b/src/test/java/org/opensearch/security/resources/ResourceAccessHandlerTest.java @@ -23,7 +23,6 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.resources.sharing.ResourceSharing; import org.opensearch.security.resources.sharing.ShareWith; import org.opensearch.security.securityconf.FlattenedActionGroups; @@ -51,8 +50,6 @@ public class ResourceAccessHandlerTest { private ResourceSharingIndexHandler sharingIndexHandler; @Mock private AdminDNs adminDNs; - @Mock - private PrivilegesEvaluator privilegesEvaluator; @Mock private ResourcePluginInfo resourcePluginInfo; @@ -69,7 +66,7 @@ public class ResourceAccessHandlerTest { public void setup() { threadContext = new ThreadContext(Settings.EMPTY); when(threadPool.getThreadContext()).thenReturn(threadContext); - handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, privilegesEvaluator, resourcePluginInfo); + handler = new ResourceAccessHandler(threadPool, sharingIndexHandler, adminDNs, resourcePluginInfo); // For tests that verify permission with action-group when(resourcePluginInfo.flattenedForType(any())).thenReturn(mock(FlattenedActionGroups.class)); From c9636256e6bb9394de262a5df4c64d7497c7c8dd Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Mon, 17 Nov 2025 16:03:50 +0100 Subject: [PATCH 2/6] Restored debug logging Signed-off-by: Nils Bandener --- .../org/opensearch/security/user/ThreadContextUserInfo.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java index 35abbf8844..84b2ced0a0 100644 --- a/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java +++ b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java @@ -13,6 +13,9 @@ import java.util.HashMap; import java.util.StringJoiner; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; @@ -35,6 +38,8 @@ * Moved from https://github.com/opensearch-project/security/blob/d29095f26dba1a26308c69b608dc926bd40c0f52/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java#L293 */ public class ThreadContextUserInfo { + protected static final Logger log = LogManager.getLogger(ThreadContextUserInfo.class); + private static final String READ_ACCESS = "READ"; private static final String WRITE_ACCESS = "WRITE"; private static final String NO_ACCESS = "NONE"; @@ -77,6 +82,7 @@ public void setUserInfoInThreadContext(PrivilegesEvaluationContext context) { String tenantAccessToCheck = getTenancyAccess(context); joiner.add(tenantAccessToCheck); + log.debug("userInfo: {}", joiner); if (userAttributeSerializationEnabled) { joiner.add(Base64Helper.serializeObject(new HashMap<>(context.getUser().getCustomAttributesMap()))); From ff01e72321bfb3a3a4837fcac49a1108cc64adc5 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 18 Nov 2025 08:33:18 +0100 Subject: [PATCH 3/6] ConfigModel retirement was not completely done. Now fully deleted. Signed-off-by: Nils Bandener --- .../security/OpenSearchSecurityPlugin.java | 2 +- .../identity/SecurityTokenManager.java | 21 +- .../privileges/PrivilegesEvaluatorImpl.java | 14 +- .../security/securityconf/ConfigModel.java | 36 ---- .../security/securityconf/ConfigModelV7.java | 193 ------------------ .../securityconf/DynamicConfigFactory.java | 3 - .../http/saml/HTTPSamlAuthenticatorTest.java | 5 + .../identity/SecurityTokenManagerTest.java | 55 +---- 8 files changed, 24 insertions(+), 305 deletions(-) delete mode 100644 src/main/java/org/opensearch/security/securityconf/ConfigModel.java delete mode 100644 src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 904f9a56aa..9802e95680 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1170,7 +1170,6 @@ public Collection createComponents( backendRegistry = new BackendRegistry(settings, adminDns, xffResolver, auditLog, threadPool, cih); backendRegistry.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); cr.subscribeOnChange(configMap -> { backendRegistry.invalidateCache(); }); - tokenManager = new SecurityTokenManager(cs, threadPool, userService); final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); @@ -1181,6 +1180,7 @@ public Collection createComponents( threadPool.getThreadContext() ); this.roleMapper = roleMapper; + tokenManager = new SecurityTokenManager(cs, threadPool, userService, roleMapper); PrivilegesConfiguration privilegesConfiguration = new PrivilegesConfiguration( cr, diff --git a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java index c9e8f4a78a..932a7c106e 100644 --- a/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java +++ b/src/main/java/org/opensearch/security/identity/SecurityTokenManager.java @@ -32,7 +32,7 @@ import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.authtoken.jwt.JwtVendor; import org.opensearch.security.authtoken.jwt.claims.OBOJwtClaimsBuilder; -import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.privileges.RoleMapper; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -53,21 +53,22 @@ public class SecurityTokenManager implements TokenManager { private final ClusterService cs; private final ThreadPool threadPool; private final UserService userService; + private final RoleMapper roleMapper; private Settings oboSettings = null; - private ConfigModel configModel = null; private final LongSupplier timeProvider = System::currentTimeMillis; private static final Integer OBO_MAX_EXPIRY_SECONDS = 600; - public SecurityTokenManager(final ClusterService cs, final ThreadPool threadPool, final UserService userService) { + public SecurityTokenManager( + final ClusterService cs, + final ThreadPool threadPool, + final UserService userService, + RoleMapper roleMapper + ) { this.cs = cs; this.threadPool = threadPool; this.userService = userService; - } - - @Subscribe - public void onConfigModelChanged(final ConfigModel configModel) { - this.configModel = configModel; + this.roleMapper = roleMapper; } @Subscribe @@ -90,7 +91,7 @@ JwtVendor createJwtVendor(final Settings settings) { } public boolean issueOnBehalfOfTokenAllowed() { - return oboSettings != null && configModel != null; + return oboSettings != null; } @Override @@ -117,7 +118,7 @@ public ExpiringBearerAuthToken issueOnBehalfOfToken(final Subject subject, final } final TransportAddress callerAddress = null; /* OBO tokens must not roles based on location from network address */ - final Set mappedRoles = configModel.mapSecurityRoles(user, callerAddress); + final Set mappedRoles = this.roleMapper.map(user, callerAddress); final long currentTimeMs = timeProvider.getAsLong(); final Date now = new Date(currentTimeMs); diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java index c4b1bffbc0..3880f16dfe 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java @@ -38,6 +38,7 @@ import java.util.function.Supplier; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -145,7 +146,7 @@ public class PrivilegesEvaluatorImpl implements PrivilegesEvaluator { private final PitPrivilegesEvaluator pitPrivilegesEvaluator; private final Settings settings; private final AtomicReference actionPrivileges = new AtomicReference<>(); - private final Map pluginIdToActionPrivileges = new HashMap<>(); + private final ImmutableMap pluginIdToActionPrivileges; private final RoleMapper roleMapper; private volatile boolean dnfofEnabled = false; @@ -197,7 +198,7 @@ public PrivilegesEvaluatorImpl( termsAggregationEvaluator = new TermsAggregationEvaluator(); pitPrivilegesEvaluator = new PitPrivilegesEvaluator(); - this.pluginIdToActionPrivileges.putAll(createActionPrivileges(pluginIdToRolePrivileges, staticActionGroups)); + this.pluginIdToActionPrivileges = createActionPrivileges(pluginIdToRolePrivileges, staticActionGroups); this.updateConfiguration(actionGroups, rolesConfiguration, generalConfiguration); } @@ -267,10 +268,7 @@ public PrivilegesEvaluationContext createContext( if (user.isPluginUser()) { mappedRoles = ImmutableSet.of(); - actionPrivileges = this.pluginIdToActionPrivileges.get(user.getName()); - if (actionPrivileges == null) { - actionPrivileges = ActionPrivileges.EMPTY; - } + actionPrivileges = this.pluginIdToActionPrivileges.getOrDefault(user.getName(), ActionPrivileges.EMPTY); } else { mappedRoles = this.roleMapper.map(user, caller); actionPrivileges = this.actionPrivileges.get(); @@ -731,7 +729,7 @@ private List toString(List aliases) { return Collections.unmodifiableList(ret); } - private static Map createActionPrivileges( + private static ImmutableMap createActionPrivileges( Map pluginIdToRolePrivileges, FlattenedActionGroups staticActionGroups ) { @@ -741,7 +739,7 @@ private static Map createActionPrivileges( result.put(entry.getKey(), new SubjectBasedActionPrivileges(entry.getValue(), staticActionGroups)); } - return result; + return ImmutableMap.copyOf(result); } private static boolean isDnfofEnabled(ConfigV7 generalConfiguration) { diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModel.java b/src/main/java/org/opensearch/security/securityconf/ConfigModel.java deleted file mode 100644 index a1546de0f4..0000000000 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModel.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * 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.securityconf; - -import java.util.Set; - -import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.security.user.User; - -public abstract class ConfigModel { - public abstract Set mapSecurityRoles(User user, TransportAddress caller); -} diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java deleted file mode 100644 index e811a267a8..0000000000 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2015-2018 floragunn GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.opensearch.security.securityconf; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map.Entry; -import java.util.Set; - -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.ListMultimap; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.common.settings.Settings; -import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; -import org.opensearch.security.securityconf.impl.v7.RoleMappingsV7; -import org.opensearch.security.securityconf.impl.v7.RoleV7; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.support.HostResolverMode; -import org.opensearch.security.support.WildcardMatcher; -import org.opensearch.security.user.User; - -public class ConfigModelV7 extends ConfigModel { - - protected final Logger log = LogManager.getLogger(this.getClass()); - private ConfigConstants.RolesMappingResolution rolesMappingResolution; - private RoleMappingHolder roleMappingHolder; - private SecurityDynamicConfiguration roles; - - public ConfigModelV7( - SecurityDynamicConfiguration roles, - SecurityDynamicConfiguration rolemappings, - DynamicConfigModel dcm, - Settings opensearchSettings - ) { - - this.roles = roles; - - try { - rolesMappingResolution = ConfigConstants.RolesMappingResolution.valueOf( - opensearchSettings.get( - ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, - ConfigConstants.RolesMappingResolution.MAPPING_ONLY.toString() - ).toUpperCase() - ); - } catch (Exception e) { - log.error("Cannot apply roles mapping resolution", e); - rolesMappingResolution = ConfigConstants.RolesMappingResolution.MAPPING_ONLY; - } - - roleMappingHolder = new RoleMappingHolder(rolemappings, dcm.getHostsResolverMode()); - } - - private class RoleMappingHolder { - - private ListMultimap users; - private ListMultimap, String> abars; - private ListMultimap bars; - private ListMultimap hosts; - private final String hostResolverMode; - - private List userMatchers; - private List barMatchers; - private List hostMatchers; - - private RoleMappingHolder(final SecurityDynamicConfiguration rolemappings, final String hostResolverMode) { - - this.hostResolverMode = hostResolverMode; - - if (roles != null) { - - users = ArrayListMultimap.create(); - abars = ArrayListMultimap.create(); - bars = ArrayListMultimap.create(); - hosts = ArrayListMultimap.create(); - - for (final Entry roleMap : rolemappings.getCEntries().entrySet()) { - final String roleMapKey = roleMap.getKey(); - final RoleMappingsV7 roleMapValue = roleMap.getValue(); - - for (String u : roleMapValue.getUsers()) { - users.put(u, roleMapKey); - } - - final Set abar = new HashSet<>(roleMapValue.getAnd_backend_roles()); - - if (!abar.isEmpty()) { - abars.put(WildcardMatcher.matchers(abar), roleMapKey); - } - - for (String bar : roleMapValue.getBackend_roles()) { - bars.put(bar, roleMapKey); - } - - for (String host : roleMapValue.getHosts()) { - hosts.put(host, roleMapKey); - } - } - - userMatchers = WildcardMatcher.matchers(users.keySet()); - barMatchers = WildcardMatcher.matchers(bars.keySet()); - hostMatchers = WildcardMatcher.matchers(hosts.keySet()); - } - } - - private Set map(final User user, final TransportAddress caller) { - - if (user == null || users == null || abars == null || bars == null || hosts == null) { - return Collections.emptySet(); - } - - final Set securityRoles = new HashSet<>(user.getSecurityRoles()); - - if (rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH - || rolesMappingResolution == ConfigConstants.RolesMappingResolution.BACKENDROLES_ONLY) { - if (log.isDebugEnabled()) { - log.debug("Pass backendroles from {}", user); - } - securityRoles.addAll(user.getRoles()); - } - - if (((rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH - || rolesMappingResolution == ConfigConstants.RolesMappingResolution.MAPPING_ONLY))) { - - for (String p : WildcardMatcher.getAllMatchingPatterns(userMatchers, user.getName())) { - securityRoles.addAll(users.get(p)); - } - for (String p : WildcardMatcher.getAllMatchingPatterns(barMatchers, user.getRoles())) { - securityRoles.addAll(bars.get(p)); - } - - for (List patterns : abars.keySet()) { - if (patterns.stream().allMatch(p -> p.matchAny(user.getRoles()))) { - securityRoles.addAll(abars.get(patterns)); - } - } - - if (caller != null) { - // IPV4 or IPv6 (compressed and without scope identifiers) - final String ipAddress = caller.getAddress(); - - for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, ipAddress)) { - securityRoles.addAll(hosts.get(p)); - } - - if (caller.address() != null - && (hostResolverMode.equalsIgnoreCase(HostResolverMode.IP_HOSTNAME.getValue()) - || hostResolverMode.equalsIgnoreCase(HostResolverMode.IP_HOSTNAME_LOOKUP.getValue()))) { - final String hostName = caller.address().getHostString(); - - for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, hostName)) { - securityRoles.addAll(hosts.get(p)); - } - } - - if (caller.address() != null && hostResolverMode.equalsIgnoreCase(HostResolverMode.IP_HOSTNAME_LOOKUP.getValue())) { - - final String resolvedHostName = caller.address().getHostName(); - - for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, resolvedHostName)) { - securityRoles.addAll(hosts.get(p)); - } - } - } - } - - return Collections.unmodifiableSet(securityRoles); - - } - } - - public Set mapSecurityRoles(User user, TransportAddress caller) { - return roleMappingHolder.map(user, caller); - } -} diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index 249c1a8a15..cb124d8c51 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -236,7 +236,6 @@ public void onChange(ConfigurationMap typeToConfig) { final DynamicConfigModel dcm; final InternalUsersModel ium; - final ConfigModel cm; final NodesDnModel nm = new NodesDnModelImpl(nodesDn); final AllowlistingSettings allowlist = cr.getConfiguration(CType.ALLOWLIST).getCEntry("config"); final AuditConfig audit = cr.getConfiguration(CType.AUDIT).getCEntry("config"); @@ -278,10 +277,8 @@ public void onChange(ConfigurationMap typeToConfig) { // rebuild v7 Models dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab, this.cih); ium = new InternalUsersModelV7(internalusers, roles, rolesmapping); - cm = new ConfigModelV7(roles, rolesmapping, dcm, opensearchSettings); // notify subscribers - eventBus.post(cm); eventBus.post(dcm); eventBus.post(ium); eventBus.post(nm); 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 97ecbefb98..c69d86f9e2 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 @@ -999,6 +999,11 @@ public boolean detailedErrorsEnabled() { return false; } + @Override + public boolean detailedErrorStackTraceEnabled() { + return false; + } + @Override public void sendResponse(RestResponse response) { this.response = response; diff --git a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java index 7558533656..a805ec88ba 100644 --- a/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java +++ b/src/test/java/org/opensearch/security/identity/SecurityTokenManagerTest.java @@ -13,7 +13,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Set; import com.google.common.io.BaseEncoding; import org.junit.After; @@ -31,7 +30,6 @@ import org.opensearch.identity.tokens.OnBehalfOfClaims; import org.opensearch.security.authtoken.jwt.ExpiringBearerAuthToken; import org.opensearch.security.authtoken.jwt.JwtVendor; -import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -71,7 +69,7 @@ public class SecurityTokenManagerTest { @Before public void setup() { - tokenManager = spy(new SecurityTokenManager(cs, threadPool, userService)); + tokenManager = spy(new SecurityTokenManager(cs, threadPool, userService, (user, caller) -> user.getSecurityRoles())); } @After @@ -83,26 +81,12 @@ public void after() { "This is my super safe signing key that no one will ever be able to guess. It's would take billions of years and the world's most powerful quantum computer to crack"; final static String signingKeyB64Encoded = BaseEncoding.base64().encode(signingKey.getBytes(StandardCharsets.UTF_8)); - @Test - public void onConfigModelChanged_oboNotSupported() { - final ConfigModel configModel = mock(ConfigModel.class); - - tokenManager.onConfigModelChanged(configModel); - - assertThat(tokenManager.issueOnBehalfOfTokenAllowed(), equalTo(false)); - verifyNoMoreInteractions(configModel); - } - @Test public void onDynamicConfigModelChanged_JwtVendorEnabled() { - final ConfigModel configModel = mock(ConfigModel.class); final DynamicConfigModel mockConfigModel = createMockJwtVendorInTokenManager(true); - tokenManager.onConfigModelChanged(configModel); - assertThat(tokenManager.issueOnBehalfOfTokenAllowed(), equalTo(true)); verify(mockConfigModel).getDynamicOnBehalfOfSettings(); - verifyNoMoreInteractions(configModel); } @Test @@ -211,9 +195,6 @@ public void issueOnBehalfOfToken_jwtGenerationFailure() throws Exception { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(true); @@ -235,9 +216,6 @@ public void issueOnBehalfOfToken_success() throws Exception { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(true); @@ -257,9 +235,6 @@ public void testCreateJwtWithNegativeExpiry() { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(true); @@ -280,9 +255,6 @@ public void testCreateJwtWithExceededExpiry() throws Exception { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(true); @@ -300,9 +272,6 @@ public void testCreateJwtWithBadEncryptionKey() { final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(Set.of()); createMockJwtVendorInTokenManager(false); @@ -315,26 +284,4 @@ public void testCreateJwtWithBadEncryptionKey() { }); assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: encryption_key cannot be null")); } - - @Test - public void testCreateJwtWithBadRoles() { - doAnswer(invocation -> true).when(tokenManager).issueOnBehalfOfTokenAllowed(); - final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User("Jon")); - when(threadPool.getThreadContext()).thenReturn(threadContext); - final ConfigModel configModel = mock(ConfigModel.class); - tokenManager.onConfigModelChanged(configModel); - when(configModel.mapSecurityRoles(any(), any())).thenReturn(null); - - createMockJwtVendorInTokenManager(true); - - final Throwable exception = assertThrows(RuntimeException.class, () -> { - try { - tokenManager.issueOnBehalfOfToken(null, new OnBehalfOfClaims("elmo", 90000000L)); - } catch (final Exception e) { - throw new RuntimeException(e); - } - }); - assertThat(exception.getMessage(), is("java.lang.IllegalArgumentException: Roles cannot be null")); - } } From 088dfbbbe87123e653e1b4355f1ed5f0c45a3346 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 18 Nov 2025 17:21:40 +0100 Subject: [PATCH 4/6] Test fixes Signed-off-by: Nils Bandener --- .../security/user/ThreadContextUserInfo.java | 14 ++++++++------ .../security/support/HostResolverModeTest.java | 5 ----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java index 84b2ced0a0..cacfedc062 100644 --- a/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java +++ b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java @@ -61,12 +61,14 @@ public ThreadContextUserInfo( USER_ATTRIBUTE_SERIALIZATION_ENABLED_DEFAULT ); this.privilegesConfiguration = privilegesConfiguration; - clusterSettings.addSettingsUpdateConsumer( - SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING, - newIsUserAttributeSerializationEnabled -> { - userAttributeSerializationEnabled = newIsUserAttributeSerializationEnabled; - } - ); + if (clusterSettings != null) { + clusterSettings.addSettingsUpdateConsumer( + SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING, + newIsUserAttributeSerializationEnabled -> { + userAttributeSerializationEnabled = newIsUserAttributeSerializationEnabled; + } + ); + } } public void setUserInfoInThreadContext(PrivilegesEvaluationContext context) { diff --git a/src/test/java/org/opensearch/security/support/HostResolverModeTest.java b/src/test/java/org/opensearch/security/support/HostResolverModeTest.java index bee18e5242..b74d46a29a 100644 --- a/src/test/java/org/opensearch/security/support/HostResolverModeTest.java +++ b/src/test/java/org/opensearch/security/support/HostResolverModeTest.java @@ -27,9 +27,4 @@ public void testIpHostnameValue() { public void testIpHostnameLookupValue() { assertThat(HostResolverMode.IP_HOSTNAME_LOOKUP.getValue(), is("ip-hostname-lookup")); } - - @Test - public void testEnumCount() { - assertThat(HostResolverMode.values().length, is(2)); - } } From 86f083d931e9691b2e09a319962cce3025a33e1d Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 18 Nov 2025 17:37:25 +0100 Subject: [PATCH 5/6] Test fixes Signed-off-by: Nils Bandener --- .../security/privileges/PrivilegesEvaluator.java | 2 +- .../opensearch/security/user/ThreadContextUserInfo.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 8779503e39..ddbdf7be5c 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -145,7 +145,7 @@ public boolean notFailOnForbiddenEnabled() { } private OpenSearchSecurityException exception() { - StringBuilder error = new StringBuilder("OpenSearch Security is not initialized"); + StringBuilder error = new StringBuilder("OpenSearch Security not initialized"); String reason = this.unavailablityReasonSupplier.get(); if (reason != null) { diff --git a/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java index cacfedc062..a958f8db5c 100644 --- a/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java +++ b/src/main/java/org/opensearch/security/user/ThreadContextUserInfo.java @@ -63,10 +63,10 @@ public ThreadContextUserInfo( this.privilegesConfiguration = privilegesConfiguration; if (clusterSettings != null) { clusterSettings.addSettingsUpdateConsumer( - SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING, - newIsUserAttributeSerializationEnabled -> { - userAttributeSerializationEnabled = newIsUserAttributeSerializationEnabled; - } + SecuritySettings.USER_ATTRIBUTE_SERIALIZATION_ENABLED_SETTING, + newIsUserAttributeSerializationEnabled -> { + userAttributeSerializationEnabled = newIsUserAttributeSerializationEnabled; + } ); } } From 922e8f4e9cd3cac023cf0c406d4d1cef46911b28 Mon Sep 17 00:00:00 2001 From: Nils Bandener Date: Tue, 18 Nov 2025 17:58:56 +0100 Subject: [PATCH 6/6] Test fixes Signed-off-by: Nils Bandener --- .../security/privileges/RestLayerPrivilegesEvaluatorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index 38433e4fd2..bfb5719be7 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -89,7 +89,7 @@ public void testEvaluate_NotInitialized_NullModel_ExceptionThrown() { OpenSearchSecurityException.class, () -> restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", null) ); - assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized: PrivilegesEvaluator not initialized")); + assertThat(exception.getMessage(), equalTo("OpenSearch Security not initialized: PrivilegesEvaluator not initialized")); } @Test