diff --git a/build.gradle b/build.gradle index 0bc7cfaeaa..7eb4db25b0 100644 --- a/build.gradle +++ b/build.gradle @@ -751,6 +751,7 @@ dependencies { integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.14" integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16" integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5" + integrationTestImplementation "org.mockito:mockito-core:5.14.2" //spotless implementation('com.google.googlejavaformat:google-java-format:1.25.2') { diff --git a/src/integrationTest/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorTest.java b/src/integrationTest/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorTest.java new file mode 100644 index 0000000000..9f0004c1a4 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorTest.java @@ -0,0 +1,155 @@ +/* + * 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.legacy; + +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.script.mustache.MustachePlugin; +import org.opensearch.script.mustache.RenderSearchTemplateAction; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class PrivilegesEvaluatorTest { + + protected final static TestSecurityConfig.User NEGATIVE_LOOKAHEAD = new TestSecurityConfig.User("negative_lookahead_user").roles( + new Role("negative_lookahead_role").indexPermissions("read").on("/^(?!t.*).*/").clusterPermissions("cluster_composite_ops") + ); + + protected final static TestSecurityConfig.User NEGATED_REGEX = new TestSecurityConfig.User("negated_regex_user").roles( + new Role("negated_regex_role").indexPermissions("read").on("/^[a-z].*/").clusterPermissions("cluster_composite_ops") + ); + + protected final static TestSecurityConfig.User SEARCH_TEMPLATE = new TestSecurityConfig.User("search_template_user").roles( + new Role("search_template_role").indexPermissions("read").on("services").clusterPermissions("cluster_composite_ops") + ); + + protected final static TestSecurityConfig.User RENDER_SEARCH_TEMPLATE = new TestSecurityConfig.User("render_search_template_user") + .roles( + new Role("render_search_template_role").indexPermissions("read") + .on("services") + .clusterPermissions(RenderSearchTemplateAction.NAME) + ); + + private String TEST_QUERY = + "{\"source\":{\"query\":{\"match\":{\"service\":\"{{service_name}}\"}}},\"params\":{\"service_name\":\"Oracle\"}}"; + + private String TEST_DOC = "{\"source\": {\"title\": \"Spirited Away\"}}"; + + private String TEST_RENDER_SEARCH_TEMPLATE_QUERY = + "{\"params\":{\"status\":[\"pending\",\"published\"]},\"source\":\"{\\\"query\\\": {\\\"terms\\\": {\\\"status\\\": [\\\"{{#status}}\\\",\\\"{{.}}\\\",\\\"{{/status}}\\\"]}}}\"}"; + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX, SEARCH_TEMPLATE, RENDER_SEARCH_TEMPLATE, TestSecurityConfig.User.USER_ADMIN) + .plugin(MustachePlugin.class) + .nodeSettings(Map.of(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.getKey(), true)) + .build(); + + @Test + public void testNegativeLookaheadPattern() throws Exception { + + try (TestRestClient client = cluster.getRestClient(NEGATIVE_LOOKAHEAD)) { + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } + + @Test + public void testRegexPattern() throws Exception { + + try (TestRestClient client = cluster.getRestClient(NEGATED_REGEX)) { + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + + } + + @Test + public void testSearchTemplateRequestSuccess() { + // Insert doc into services index with admin user + try (TestRestClient client = cluster.getRestClient(TestSecurityConfig.User.USER_ADMIN)) { + TestRestClient.HttpResponse response = client.postJson("services/_doc", TEST_DOC); + assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); + } + + try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { + final String searchTemplateOnServicesIndex = "services/_search/template"; + final TestRestClient.HttpResponse searchTemplateOnAuthorizedIndexResponse = client.getWithJsonBody( + searchTemplateOnServicesIndex, + TEST_QUERY + ); + assertThat(searchTemplateOnAuthorizedIndexResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } + + @Test + public void testSearchTemplateRequestUnauthorizedIndex() { + try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { + final String searchTemplateOnMoviesIndex = "movies/_search/template"; + final TestRestClient.HttpResponse searchTemplateOnUnauthorizedIndexResponse = client.getWithJsonBody( + searchTemplateOnMoviesIndex, + TEST_QUERY + ); + assertThat(searchTemplateOnUnauthorizedIndexResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + } + + @Test + public void testSearchTemplateRequestUnauthorizedAllIndices() { + try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { + final String searchTemplateOnAllIndices = "_search/template"; + final TestRestClient.HttpResponse searchOnAllIndicesResponse = client.getWithJsonBody(searchTemplateOnAllIndices, TEST_QUERY); + assertThat(searchOnAllIndicesResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + } + + @Test + public void testRenderSearchTemplateRequestFailure() { + try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { + final String renderSearchTemplate = "_render/template"; + final TestRestClient.HttpResponse renderSearchTemplateResponse = client.postJson( + renderSearchTemplate, + TEST_RENDER_SEARCH_TEMPLATE_QUERY + ); + assertThat(renderSearchTemplateResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + } + + @Test + public void testRenderSearchTemplateRequestSuccess() { + try (TestRestClient client = cluster.getRestClient(RENDER_SEARCH_TEMPLATE)) { + final String renderSearchTemplate = "_render/template"; + final TestRestClient.HttpResponse renderSearchTemplateResponse = client.postJson( + renderSearchTemplate, + TEST_RENDER_SEARCH_TEMPLATE_QUERY + ); + assertThat(renderSearchTemplateResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 488ed69364..8db76eabfd 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -1121,20 +1121,43 @@ public Collection createComponents( final CompatConfig compatConfig = new CompatConfig(environment, transportPassiveAuthSetting); - evaluator = new PrivilegesEvaluator( - clusterService, - clusterService::state, - threadPool, - threadPool.getThreadContext(), - cr, - resolver, - auditLog, - settings, - privilegesInterceptor, - cih, - irr, - namedXContentRegistry.get() - ); + if (PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR.get(settings)) { + // Use legacy implementation (non-default) + evaluator = new org.opensearch.security.privileges.legacy.PrivilegesEvaluatorImpl( + clusterService, + threadPool, + cr, + resolver, + auditLog, + settings, + privilegesInterceptor, + cih, + irr, + namedXContentRegistry.get() + ); + + restLayerEvaluator = new org.opensearch.security.privileges.legacy.RestLayerPrivilegesEvaluatorImpl(clusterService, threadPool); + } else { + // Use new implementation (default) + evaluator = new org.opensearch.security.privileges.PrivilegesEvaluatorImpl( + clusterService, + clusterService::state, + threadPool, + threadPool.getThreadContext(), + cr, + resolver, + auditLog, + settings, + privilegesInterceptor, + cih, + irr, + namedXContentRegistry.get() + ); + + restLayerEvaluator = new org.opensearch.security.privileges.RestLayerPrivilegesEvaluatorImpl( + (org.opensearch.security.privileges.PrivilegesEvaluatorImpl) evaluator + ); + } sf = new SecurityFilter(settings, evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, compatConfig, irr, xffResolver); @@ -1146,8 +1169,6 @@ public Collection createComponents( principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass); } - restLayerEvaluator = new RestLayerPrivilegesEvaluator(evaluator); - securityRestHandler = new SecurityRestFilter( backendRegistry, restLayerEvaluator, @@ -2062,6 +2083,7 @@ public List> getSettings() { // Privileges evaluation settings.add(ActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE); + settings.add(PrivilegesEvaluator.USE_LEGACY_PRIVILEGE_EVALUATOR); } return settings; diff --git a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java index b87c92c356..a5db15f2d0 100644 --- a/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SystemIndexSearcherWrapper.java @@ -43,7 +43,6 @@ import org.opensearch.security.privileges.PrivilegesEvaluationContext; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; -import org.opensearch.security.resolver.IndexResolverReplacer; import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; @@ -170,8 +169,7 @@ protected final boolean isBlockedSystemIndexRequest() { String permission = ConfigConstants.SYSTEM_INDEX_PERMISSION; PrivilegesEvaluationContext context = evaluator.createContext(user, permission); - PrivilegesEvaluatorResponse result = evaluator.getActionPrivileges() - .hasExplicitIndexPrivilege(context, Set.of(permission), IndexResolverReplacer.Resolved.ofIndex(index.getName())); + PrivilegesEvaluatorResponse result = evaluator.hasExplicitIndexPrivilege(context, Set.of(permission), index.getName()); return !result.isAllowed(); } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java index cc61905f04..23771caa27 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluationContext.java @@ -51,7 +51,7 @@ public class PrivilegesEvaluationContext { */ private final Map renderedPatternTemplateCache = new HashMap<>(); - PrivilegesEvaluationContext( + public PrivilegesEvaluationContext( User user, ImmutableSet mappedRoles, String action, @@ -135,7 +135,7 @@ public ImmutableSet getMappedRoles() { * However, this method should be only used for this one particular phase. Normally, all roles should be determined * upfront and stay constant during the whole privilege evaluation process. */ - void setMappedRoles(ImmutableSet mappedRoles) { + public void setMappedRoles(ImmutableSet mappedRoles) { this.mappedRoles = mappedRoles; } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 0483123b6e..757a48866c 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -1,842 +1,54 @@ -/* - * 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.Arrays; -import java.util.Collections; -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.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.common.settings.Setting; import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.core.xcontent.NamedXContentRegistry; -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.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.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.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 org.greenrobot.eventbus.Subscribe; -import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; -import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; - -public class 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" - ) +public interface PrivilegesEvaluator { + static Setting USE_LEGACY_PRIVILEGE_EVALUATOR = Setting.boolSetting( + "plugins.security.privileges_evaluation.use_legacy_impl", + false, + Setting.Property.NodeScope ); - 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 final ClusterInfoHolder clusterInfoHolder; - 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 NamedXContentRegistry namedXContentRegistry; - private final Settings settings; - private final AtomicReference actionPrivileges = new AtomicReference<>(); - - 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, - NamedXContentRegistry namedXContentRegistry - ) { - - 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.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.namedXContentRegistry = namedXContentRegistry; - - if (configurationRepository != null) { - configurationRepository.subscribeOnChange(configMap -> { - try { - SecurityDynamicConfiguration actionGroupsConfiguration = configurationRepository.getConfiguration( - CType.ACTIONGROUPS - ); - SecurityDynamicConfiguration rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES); - - this.updateConfiguration(actionGroupsConfiguration, rolesConfiguration); - } catch (Exception e) { - log.error("Error while updating ActionPrivileges object with {}", configMap, e); - } - }); - } - - if (clusterService != null) { - clusterService.addListener(event -> { - ActionPrivileges actionPrivileges = PrivilegesEvaluator.this.actionPrivileges.get(); - if (actionPrivileges != null) { - actionPrivileges.updateClusterStateMetadataAsync(clusterService, threadPool); - } - }); - } - - } - - void updateConfiguration( - SecurityDynamicConfiguration actionGroupsConfiguration, - SecurityDynamicConfiguration rolesConfiguration - ) { - if (rolesConfiguration != null) { - SecurityDynamicConfiguration actionGroupsWithStatics = actionGroupsConfiguration != null - ? DynamicConfigFactory.addStatics(actionGroupsConfiguration.clone()) - : DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS)); - FlattenedActionGroups flattenedActionGroups = new FlattenedActionGroups(actionGroupsWithStatics); - ActionPrivileges actionPrivileges = new ActionPrivileges( - DynamicConfigFactory.addStatics(rolesConfiguration.clone()), - flattenedActionGroups, - () -> clusterStateSupplier.get().metadata().getIndicesLookup(), - settings - ); - Metadata metadata = clusterStateSupplier.get().metadata(); - actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); - ActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); - - if (oldInstance != null) { - oldInstance.shutdown(); - } - } - } - - @Subscribe - public void onConfigModelChanged(ConfigModel configModel) { - this.configModel = configModel; - } - - @Subscribe - public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - this.dcm = dcm; - } - - public ActionPrivileges getActionPrivileges() { - return this.actionPrivileges.get(); - } - - public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permission) { - PrivilegesEvaluationContext context = createContext(user, permission); - return this.actionPrivileges.get().hasExplicitClusterPrivilege(context, permission).isAllowed(); - } - - public boolean isInitialized() { - return configModel != null && dcm != null && actionPrivileges.get() != null; - } - - private void setUserInfoInThreadContext(User user) { - if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { - StringJoiner joiner = new StringJoiner("|"); - joiner.add(user.getName()); - joiner.add(String.join(",", user.getRoles())); - joiner.add(String.join(",", user.getSecurityRoles())); - String requestedTenant = user.getRequestedTenant(); - if (!Strings.isNullOrEmpty(requestedTenant)) { - joiner.add(requestedTenant); - } - threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); - } - } - - public PrivilegesEvaluationContext createContext(User user, String action) { - return createContext(user, action, null, null, null); - } - - public PrivilegesEvaluationContext createContext( - User user, - String action0, - ActionRequest request, - Task task, - Set injectedRoles - ) { - if (!isInitialized()) { - throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); - } - - TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); - - return new PrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr, resolver, clusterStateSupplier); - } - - public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { - - if (!isInitialized()) { - throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); - } - - 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); - } - - // Add the security roles for this user so that they can be used for DLS parameter substitution. - user.addSecurityRoles(mappedRoles); - setUserInfoInThreadContext(user); - - 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 = this.actionPrivileges.get(); - 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 - if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { - return presponse; - } - - // Security 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, - mapTenants(user, mappedRoles) - ); - - 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, - mapTenants(user, mappedRoles) - ); - - 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 Map mapTenants(final User user, Set roles) { - return this.configModel.mapTenants(user, roles); - } - - public Set getAllConfiguredTenantNames() { - - return configModel.getAllConfiguredTenantNames(); - } - - public boolean multitenancyEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsMultitenancyEnabled(); - } - - public boolean privateTenantEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsPrivateTenantEnabled(); - } - - public String dashboardsDefaultTenant() { - return dcm.getDashboardsDefaultTenant(); - } - - public boolean notFailOnForbiddenEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); - } - - public String dashboardsIndex() { - return dcm.getDashboardsIndexname(); - } - - public String dashboardsServerUsername() { - return dcm.getDashboardsServerUsername(); - } - - public String dashboardsOpenSearchRole() { - return dcm.getDashboardsOpenSearchRole(); - } - - public List getSignInOptions() { - return dcm.getSignInOptions(); - } - - 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); - } - - if (log.isDebugEnabled() && result.size() > 1) { - log.debug("Additional permissions required: {}", result); - } - - 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)) { - 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); - } - - indexMetaDataCollection = indexMetaDataSet; - } - // check filtered aliases - for (IndexMetadata indexMetaData : indexMetaDataCollection) { - - final List filteredAliases = new ArrayList(); - - final Map aliases = indexMetaData.getAliases(); + boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permission); - if (aliases != null && aliases.size() > 0) { - if (isDebugEnabled) { - log.debug("Aliases for {}: {}", indexMetaData.getIndex().getName(), aliases); - } + boolean isInitialized(); - final Iterator it = aliases.keySet().iterator(); - while (it.hasNext()) { - final String alias = it.next(); - final AliasMetadata aliasMetadata = aliases.get(alias); + PrivilegesEvaluationContext createContext(User user, String action); - 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); - } - } - } - } + PrivilegesEvaluationContext createContext(User user, String action0, ActionRequest request, Task task, Set injectedRoles); - 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 + PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context); - return false; - } + Set mapRoles(final User user, final TransportAddress caller); - private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { - String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); + Map mapTenants(final User user, Set roles); - if (docAllowListHeader == null) { - return false; - } + Set getAllConfiguredTenantNames(); - if (!(request instanceof GetRequest)) { - return false; - } + boolean multitenancyEnabled(); - try { - DocumentAllowList documentAllowList = DocumentAllowList.parse(docAllowListHeader); - GetRequest getRequest = (GetRequest) request; + boolean privateTenantEnabled(); - if (documentAllowList.isAllowed(getRequest.index(), getRequest.id())) { - if (log.isDebugEnabled()) { - log.debug("Request " + request + " is allowed by " + documentAllowList); - } + String dashboardsDefaultTenant(); - return true; - } else { - return false; - } + boolean notFailOnForbiddenEnabled(); - } catch (Exception e) { - log.error("Error while handling document allow list: " + docAllowListHeader, e); - return false; - } - } + String dashboardsIndex(); - private List toString(List aliases) { - if (aliases == null || aliases.size() == 0) { - return Collections.emptyList(); - } + String dashboardsServerUsername(); - final List ret = new ArrayList<>(aliases.size()); + String dashboardsOpenSearchRole(); - for (final AliasMetadata amd : aliases) { - if (amd != null) { - ret.add(amd.alias()); - } - } + List getSignInOptions(); - return Collections.unmodifiableList(ret); - } + PrivilegesEvaluatorResponse hasExplicitIndexPrivilege(PrivilegesEvaluationContext context, Set actions, String index); } 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..c04119d415 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorImpl.java @@ -0,0 +1,846 @@ +/* + * 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.Arrays; +import java.util.Collections; +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.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.core.xcontent.NamedXContentRegistry; +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.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.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.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 org.greenrobot.eventbus.Subscribe; + +import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; + +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 ThreadContext threadContext; + + private PrivilegesInterceptor privilegesInterceptor; + + private final boolean checkSnapshotRestoreWritePrivileges; + + private final ClusterInfoHolder clusterInfoHolder; + 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 NamedXContentRegistry namedXContentRegistry; + private final Settings settings; + private final AtomicReference actionPrivileges = new AtomicReference<>(); + + public PrivilegesEvaluatorImpl( + 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, + NamedXContentRegistry namedXContentRegistry + ) { + + 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.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.namedXContentRegistry = namedXContentRegistry; + + if (configurationRepository != null) { + configurationRepository.subscribeOnChange(configMap -> { + try { + SecurityDynamicConfiguration actionGroupsConfiguration = configurationRepository.getConfiguration( + CType.ACTIONGROUPS + ); + SecurityDynamicConfiguration rolesConfiguration = configurationRepository.getConfiguration(CType.ROLES); + + this.updateConfiguration(actionGroupsConfiguration, rolesConfiguration); + } catch (Exception e) { + log.error("Error while updating ActionPrivileges object with {}", configMap, e); + } + }); + } + + if (clusterService != null) { + clusterService.addListener(event -> { + ActionPrivileges actionPrivileges = PrivilegesEvaluatorImpl.this.actionPrivileges.get(); + if (actionPrivileges != null) { + actionPrivileges.updateClusterStateMetadataAsync(clusterService, threadPool); + } + }); + } + + } + + void updateConfiguration( + SecurityDynamicConfiguration actionGroupsConfiguration, + SecurityDynamicConfiguration rolesConfiguration + ) { + if (rolesConfiguration != null) { + SecurityDynamicConfiguration actionGroupsWithStatics = actionGroupsConfiguration != null + ? DynamicConfigFactory.addStatics(actionGroupsConfiguration.clone()) + : DynamicConfigFactory.addStatics(SecurityDynamicConfiguration.empty(CType.ACTIONGROUPS)); + FlattenedActionGroups flattenedActionGroups = new FlattenedActionGroups(actionGroupsWithStatics); + ActionPrivileges actionPrivileges = new ActionPrivileges( + DynamicConfigFactory.addStatics(rolesConfiguration.clone()), + flattenedActionGroups, + () -> clusterStateSupplier.get().metadata().getIndicesLookup(), + settings + ); + Metadata metadata = clusterStateSupplier.get().metadata(); + actionPrivileges.updateStatefulIndexPrivileges(metadata.getIndicesLookup(), metadata.version()); + ActionPrivileges oldInstance = this.actionPrivileges.getAndSet(actionPrivileges); + + if (oldInstance != null) { + oldInstance.shutdown(); + } + } + } + + @Subscribe + public void onConfigModelChanged(ConfigModel configModel) { + this.configModel = configModel; + } + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + } + + public ActionPrivileges getActionPrivileges() { + return this.actionPrivileges.get(); + } + + public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permission) { + PrivilegesEvaluationContext context = createContext(user, permission); + return this.actionPrivileges.get().hasExplicitClusterPrivilege(context, permission).isAllowed(); + } + + public boolean isInitialized() { + return configModel != null && dcm != null && actionPrivileges.get() != null; + } + + private void setUserInfoInThreadContext(User user) { + if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { + StringJoiner joiner = new StringJoiner("|"); + joiner.add(user.getName()); + joiner.add(String.join(",", user.getRoles())); + joiner.add(String.join(",", user.getSecurityRoles())); + String requestedTenant = user.getRequestedTenant(); + if (!Strings.isNullOrEmpty(requestedTenant)) { + joiner.add(requestedTenant); + } + threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); + } + } + + public PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, null, null); + } + + public PrivilegesEvaluationContext createContext( + User user, + String action0, + ActionRequest request, + Task task, + Set injectedRoles + ) { + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); + + return new PrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr, resolver, clusterStateSupplier); + } + + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + 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); + } + + // Add the security roles for this user so that they can be used for DLS parameter substitution. + user.addSecurityRoles(mappedRoles); + setUserInfoInThreadContext(user); + + 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 = this.actionPrivileges.get(); + 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 + if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { + return presponse; + } + + // Security 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, + mapTenants(user, mappedRoles) + ); + + 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, + mapTenants(user, mappedRoles) + ); + + 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 Map mapTenants(final User user, Set roles) { + return this.configModel.mapTenants(user, roles); + } + + public Set getAllConfiguredTenantNames() { + + return configModel.getAllConfiguredTenantNames(); + } + + public boolean multitenancyEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsMultitenancyEnabled(); + } + + public boolean privateTenantEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsPrivateTenantEnabled(); + } + + public String dashboardsDefaultTenant() { + return dcm.getDashboardsDefaultTenant(); + } + + public boolean notFailOnForbiddenEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); + } + + public String dashboardsIndex() { + return dcm.getDashboardsIndexname(); + } + + public String dashboardsServerUsername() { + return dcm.getDashboardsServerUsername(); + } + + public String dashboardsOpenSearchRole() { + return dcm.getDashboardsOpenSearchRole(); + } + + public List getSignInOptions() { + return dcm.getSignInOptions(); + } + + public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege(PrivilegesEvaluationContext context, Set actions, String index) { + return getActionPrivileges().hasExplicitIndexPrivilege(context, actions, Resolved.ofIndex(index)); + } + + 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); + } + + if (log.isDebugEnabled() && result.size() > 1) { + log.debug("Additional permissions required: {}", result); + } + + 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)) { + 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); + } + + 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 + + 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())) { + if (log.isDebugEnabled()) { + log.debug("Request " + request + " is allowed by " + 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); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index d072ec301c..79e4c84751 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -40,10 +40,10 @@ import com.selectivem.collections.CheckTable; public class PrivilegesEvaluatorResponse { - boolean allowed = false; - Set missingSecurityRoles = new HashSet<>(); + public boolean allowed = false; + public Set missingSecurityRoles = new HashSet<>(); PrivilegesEvaluatorResponseState state = PrivilegesEvaluatorResponseState.PENDING; - CreateIndexRequestBuilder createIndexRequestBuilder; + public CreateIndexRequestBuilder createIndexRequestBuilder; private Set onlyAllowedForIndices = ImmutableSet.of(); private CheckTable indexToActionCheckTable; private String privilegeMatrix; @@ -192,6 +192,12 @@ public static PrivilegesEvaluatorResponse insufficient(String missingPrivilege) return response; } + public static PrivilegesEvaluatorResponse insufficient(Collection missingPrivileges) { + PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); + response.indexToActionCheckTable = CheckTable.create(ImmutableSet.of("_"), ImmutableSet.copyOf(missingPrivileges)); + return response; + } + public static PrivilegesEvaluatorResponse insufficient(CheckTable indexToActionCheckTable) { PrivilegesEvaluatorResponse response = new PrivilegesEvaluatorResponse(); response.indexToActionCheckTable = indexToActionCheckTable; diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java index f177596573..0e11babc11 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -42,9 +42,9 @@ public class PrivilegesInterceptor { public static class ReplaceResult { - final boolean continueEvaluation; - final boolean accessDenied; - final CreateIndexRequestBuilder createIndexRequestBuilder; + public final boolean continueEvaluation; + public final boolean accessDenied; + public final CreateIndexRequestBuilder createIndexRequestBuilder; private ReplaceResult(boolean continueEvaluation, boolean accessDenied, CreateIndexRequestBuilder createIndexRequestBuilder) { this.continueEvaluation = continueEvaluation; diff --git a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java index b1f994163c..8f2788b907 100644 --- a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluator.java @@ -13,41 +13,8 @@ import java.util.Set; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - import org.opensearch.security.user.User; -public class RestLayerPrivilegesEvaluator { - protected final Logger log = LogManager.getLogger(this.getClass()); - private final PrivilegesEvaluator privilegesEvaluator; - - public RestLayerPrivilegesEvaluator(PrivilegesEvaluator privilegesEvaluator) { - this.privilegesEvaluator = privilegesEvaluator; - } - - public PrivilegesEvaluatorResponse evaluate(final User user, final String routeName, final Set actions) { - PrivilegesEvaluationContext context = privilegesEvaluator.createContext(user, routeName); - - final boolean isDebugEnabled = log.isDebugEnabled(); - if (isDebugEnabled) { - log.debug("Evaluate permissions for {}", user); - log.debug("Action: {}", actions); - log.debug("Mapped roles: {}", context.getMappedRoles().toString()); - } - - PrivilegesEvaluatorResponse result = privilegesEvaluator.getActionPrivileges().hasAnyClusterPrivilege(context, actions); - - if (!result.allowed) { - log.info( - "No permission match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", - user, - routeName, - context.getMappedRoles(), - result.getMissingPrivileges() - ); - } - - return result; - } +public interface RestLayerPrivilegesEvaluator { + PrivilegesEvaluatorResponse evaluate(final User user, final String routeName, final Set actions); } diff --git a/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorImpl.java new file mode 100644 index 0000000000..0668eab917 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorImpl.java @@ -0,0 +1,42 @@ +package org.opensearch.security.privileges; + +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.security.user.User; + +public class RestLayerPrivilegesEvaluatorImpl implements RestLayerPrivilegesEvaluator { + protected final Logger log = LogManager.getLogger(this.getClass()); + private final PrivilegesEvaluatorImpl privilegesEvaluator; + + public RestLayerPrivilegesEvaluatorImpl(PrivilegesEvaluatorImpl privilegesEvaluator) { + this.privilegesEvaluator = privilegesEvaluator; + } + + public PrivilegesEvaluatorResponse evaluate(final User user, final String routeName, final Set actions) { + PrivilegesEvaluationContext context = privilegesEvaluator.createContext(user, routeName); + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {}", user); + log.debug("Action: {}", actions); + log.debug("Mapped roles: {}", context.getMappedRoles().toString()); + } + + PrivilegesEvaluatorResponse result = privilegesEvaluator.getActionPrivileges().hasAnyClusterPrivilege(context, actions); + + if (!result.allowed) { + log.info( + "No permission match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + routeName, + context.getMappedRoles(), + result.getMissingPrivileges() + ); + } + + return result; + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/PitPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/PitPrivilegesEvaluator.java new file mode 100644 index 0000000000..dde32b4b19 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/PitPrivilegesEvaluator.java @@ -0,0 +1,117 @@ +/* + * 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.legacy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.admin.indices.segments.PitSegmentsRequest; +import org.opensearch.action.search.CreatePitRequest; +import org.opensearch.action.search.DeletePitRequest; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.security.OpenSearchSecurityPlugin; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.user.User; + +/** + * This class evaluates privileges for point in time (Delete and List all) operations. + * For aliases - users must have either alias permission or backing index permissions + * For data streams - users must have access to backing indices permission + data streams permission. + */ +public class PitPrivilegesEvaluator { + + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final ClusterService clusterService, + final User user, + final SecurityRoles securityRoles, + final String action, + final IndexNameExpressionResolver resolver, + final PrivilegesEvaluatorResponse presponse, + final IndexResolverReplacer irr + ) { + + if (!(request instanceof DeletePitRequest || request instanceof PitSegmentsRequest)) { + return presponse; + } + List pitIds = new ArrayList<>(); + + if (request instanceof DeletePitRequest) { + DeletePitRequest deletePitRequest = (DeletePitRequest) request; + pitIds = deletePitRequest.getPitIds(); + } else if (request instanceof PitSegmentsRequest) { + PitSegmentsRequest pitSegmentsRequest = (PitSegmentsRequest) request; + pitIds = pitSegmentsRequest.getPitIds(); + } + // if request is for all PIT IDs, skip custom pit ids evaluation + if (pitIds.size() == 1 && "_all".equals(pitIds.get(0))) { + return presponse; + } else { + return handlePitsAccess(pitIds, clusterService, user, securityRoles, action, resolver, presponse, irr); + } + } + + /** + * Handle access for delete operation / pit segments operation where PIT IDs are explicitly passed + */ + private PrivilegesEvaluatorResponse handlePitsAccess( + List pitIds, + ClusterService clusterService, + User user, + SecurityRoles securityRoles, + final String action, + IndexNameExpressionResolver resolver, + PrivilegesEvaluatorResponse presponse, + final IndexResolverReplacer irr + ) { + Map pitToIndicesMap = OpenSearchSecurityPlugin.GuiceHolder.getPitService().getIndicesForPits(pitIds); + Set pitIndices = new HashSet<>(); + // add indices across all PITs to a set and evaluate if user has access to all indices + for (String[] indices : pitToIndicesMap.values()) { + pitIndices.addAll(Arrays.asList(indices)); + } + Set allPermittedIndices = getPermittedIndices(pitIndices, clusterService, user, securityRoles, action, resolver, irr); + // Only if user has access to all PIT's indices, allow operation, otherwise continue evaluation in PrivilegesEvaluator. + if (allPermittedIndices.containsAll(pitIndices)) { + presponse.allowed = true; + presponse.markComplete(); + } + return presponse; + } + + /** + * This method returns list of permitted indices for the PIT indices passed + */ + private Set getPermittedIndices( + Set pitIndices, + ClusterService clusterService, + User user, + SecurityRoles securityRoles, + final String action, + IndexNameExpressionResolver resolver, + final IndexResolverReplacer irr + ) { + String[] indicesArr = new String[pitIndices.size()]; + CreatePitRequest req = new CreatePitRequest(new TimeValue(1, TimeUnit.DAYS), true, pitIndices.toArray(indicesArr)); + final IndexResolverReplacer.Resolved pitResolved = irr.resolveRequest(req); + return securityRoles.reduce(pitResolved, user, new String[] { action }, resolver, clusterService); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorImpl.java new file mode 100644 index 0000000000..75f191eba7 --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/PrivilegesEvaluatorImpl.java @@ -0,0 +1,821 @@ +/* + * 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.legacy; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +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 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.metadata.AliasMetadata; +import org.opensearch.cluster.metadata.IndexMetadata; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +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.core.xcontent.NamedXContentRegistry; +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.DocumentAllowList; +import org.opensearch.security.privileges.PrivilegesEvaluationContext; +import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.privileges.PrivilegesInterceptor; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.DynamicConfigModel; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; +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 org.greenrobot.eventbus.Subscribe; + +import static org.opensearch.security.OpenSearchSecurityPlugin.traceAction; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT; + +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 ClusterService clusterService; + + private final IndexNameExpressionResolver resolver; + + private final AuditLog auditLog; + private ThreadContext threadContext; + + private PrivilegesInterceptor privilegesInterceptor; + + private final boolean checkSnapshotRestoreWritePrivileges; + + private final ClusterInfoHolder clusterInfoHolder; + 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 NamedXContentRegistry namedXContentRegistry; + + public PrivilegesEvaluatorImpl( + final ClusterService clusterService, + final ThreadPool threadPool, + final ConfigurationRepository configurationRepository, + final IndexNameExpressionResolver resolver, + AuditLog auditLog, + final Settings settings, + final PrivilegesInterceptor privilegesInterceptor, + final ClusterInfoHolder clusterInfoHolder, + final IndexResolverReplacer irr, + NamedXContentRegistry namedXContentRegistry + ) { + + super(); + this.clusterService = clusterService; + this.resolver = resolver; + this.auditLog = auditLog; + + this.threadContext = threadPool.getThreadContext(); + this.privilegesInterceptor = privilegesInterceptor; + + this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( + ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES + ); + + 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.namedXContentRegistry = namedXContentRegistry; + } + + @Subscribe + public void onConfigModelChanged(ConfigModel configModel) { + this.configModel = configModel; + } + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + this.dcm = dcm; + } + + public SecurityRoles getSecurityRoles(Set roles) { + return configModel.getSecurityRoles().filter(roles); + } + + public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permissions) { + final Set userRoles = mapRoles(user, remoteAddress); + return hasRestAdminPermissions(userRoles, permissions); + } + + private boolean hasRestAdminPermissions(final Set roles, String permission) { + final SecurityRoles securityRoles = getSecurityRoles(roles); + return securityRoles.hasExplicitClusterPermissionPermission(permission); + } + + public boolean isInitialized() { + return configModel != null && configModel.getSecurityRoles() != null && dcm != null; + } + + private void setUserInfoInThreadContext(User user) { + if (threadContext.getTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT) == null) { + StringJoiner joiner = new StringJoiner("|"); + joiner.add(user.getName()); + joiner.add(String.join(",", user.getRoles())); + joiner.add(String.join(",", user.getSecurityRoles())); + String requestedTenant = user.getRequestedTenant(); + if (!Strings.isNullOrEmpty(requestedTenant)) { + joiner.add(requestedTenant); + } + threadContext.putTransient(OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, joiner.toString()); + } + } + + public PrivilegesEvaluationContext createContext(User user, String action) { + return createContext(user, action, null, null, null); + } + + public PrivilegesEvaluationContext createContext( + User user, + String action0, + ActionRequest request, + Task task, + Set injectedRoles + ) { + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + ImmutableSet mappedRoles = ImmutableSet.copyOf((injectedRoles == null) ? mapRoles(user, caller) : injectedRoles); + + return new PrivilegesEvaluationContext(user, mappedRoles, action0, request, task, irr, resolver, () -> clusterService.state()); + } + + public PrivilegesEvaluatorResponse evaluate(PrivilegesEvaluationContext context) { + + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + 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; + } + + final 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); + } + final SecurityRoles securityRoles = getSecurityRoles(mappedRoles); + + // Add the security roles for this user so that they can be used for DLS parameter substitution. + user.addSecurityRoles(mappedRoles); + setUserInfoInThreadContext(user); + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {} on {}", user, clusterService.localNode().getName()); + log.debug("Action: {} ({})", action0, request.getClass().getSimpleName()); + log.debug("Mapped roles: {}", mappedRoles.toString()); + } + + 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. + + if (!securityRoles.impliesClusterPermissionPermission(action0)) { + log.info( + "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + action0, + mappedRoles, + action0 + ); + return PrivilegesEvaluatorResponse.insufficient(action0); + } else { + presponse.allowed = true; + } + return presponse; + } + + final Resolved requestedResolved = context.getResolvedRequest(); + + if (isDebugEnabled) { + log.debug("RequestedResolved : {}", requestedResolved); + } + + // check snapshot/restore requests + if (snapshotRestoreEvaluator.evaluate(request, task, action0, clusterInfoHolder, presponse).isComplete()) { + return presponse; + } + + // Security index access + if (systemIndexAccessEvaluator.evaluate( + request, + task, + action0, + requestedResolved, + presponse, + securityRoles, + user, + resolver, + clusterService + ).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, clusterService, user, securityRoles, action0, resolver, 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); + } + + if (!securityRoles.impliesClusterPermissionPermission(action0)) { + log.info( + "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + requestedResolved, + action0, + mappedRoles, + action0 + ); + return PrivilegesEvaluatorResponse.insufficient(action0); + } 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, + mapTenants(user, mappedRoles) + ); + + 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, clusterService, user, securityRoles, resolver, presponse) + .isComplete()) { + return presponse; + } + + final Set allIndexPermsRequired = evaluateAdditionalIndexPermissions(request, action0); + final String[] allIndexPermsRequiredA = allIndexPermsRequired.toArray(new String[0]); + + 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, + mapTenants(user, mappedRoles) + ); + + if (isDebugEnabled) { + log.debug("Result from privileges interceptor: {}", replaceResult); + } + + if (!replaceResult.continueEvaluation) { + if (replaceResult.accessDenied) { + auditLog.logMissingPrivileges(action0, request, task); + } else { + presponse.allowed = true; + presponse.createIndexRequestBuilder = replaceResult.createIndexRequestBuilder; + } + return presponse; + } + } + + if (dnfofEnabled && DNFOF_MATCHER.test(action0)) { + + if (requestedResolved.getAllIndices().isEmpty()) { + return PrivilegesEvaluatorResponse.ok(); + } + + Set reduced = securityRoles.reduce(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); + + if (reduced.isEmpty()) { + if (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(); + } + return PrivilegesEvaluatorResponse.insufficient(allIndexPermsRequired); + } + + if (irr.replace(request, true, reduced.toArray(new String[0]))) { + return PrivilegesEvaluatorResponse.ok(); + } + } + + // not bulk, mget, etc request here + boolean permGiven = false; + + if (isDebugEnabled) { + log.debug("Security roles: {}", securityRoles.getRoleNames()); + } + + if (dcm.isMultiRolespanEnabled()) { + permGiven = securityRoles.impliesTypePermGlobal(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); + } else { + permGiven = securityRoles.get(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); + + } + + if (!permGiven) { + log.info( + "No {}-level perm match for {} {} [Action [{}]] [RolesChecked {}]", + "index", + user, + requestedResolved, + action0, + mappedRoles + ); + log.info("No permissions for {}", allIndexPermsRequired); + } else { + + if (checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { + presponse.allowed = false; + return presponse; + } + + if (isDebugEnabled) { + log.debug("Allowed because we have all indices permissions for {}", action0); + } + } + + if (permGiven) { + return PrivilegesEvaluatorResponse.ok(); + } else { + return PrivilegesEvaluatorResponse.insufficient(allIndexPermsRequired); + } + } + + public Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } + + public Map mapTenants(final User user, Set roles) { + return this.configModel.mapTenants(user, roles); + } + + public Set getAllConfiguredTenantNames() { + + return configModel.getAllConfiguredTenantNames(); + } + + public boolean multitenancyEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsMultitenancyEnabled(); + } + + public boolean privateTenantEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsPrivateTenantEnabled(); + } + + public String dashboardsDefaultTenant() { + return dcm.getDashboardsDefaultTenant(); + } + + public boolean notFailOnForbiddenEnabled() { + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); + } + + public String dashboardsIndex() { + return dcm.getDashboardsIndexname(); + } + + public String dashboardsServerUsername() { + return dcm.getDashboardsServerUsername(); + } + + public String dashboardsOpenSearchRole() { + return dcm.getDashboardsOpenSearchRole(); + } + + public List getSignInOptions() { + return dcm.getSignInOptions(); + } + + public PrivilegesEvaluatorResponse hasExplicitIndexPrivilege(PrivilegesEvaluationContext context, Set actions, String index) { + SecurityRoles securityRoles = getSecurityRoles(context.getMappedRoles()); + if (securityRoles.isPermittedOnSystemIndex(index)) { + return PrivilegesEvaluatorResponse.ok(); + } else { + return PrivilegesEvaluatorResponse.insufficient(ConfigConstants.SYSTEM_INDEX_PERMISSION); + } + } + + private Set evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { + // --- check inner bulk requests + final Set additionalPermissionsRequired = new HashSet<>(); + + 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); + } + + if (additionalPermissionsRequired.size() > 1) { + traceAction("Additional permissions required: {}", additionalPermissionsRequired); + } + + if (log.isDebugEnabled() && additionalPermissionsRequired.size() > 1) { + log.debug("Additional permissions required: {}", additionalPermissionsRequired); + } + + return Collections.unmodifiableSet(additionalPermissionsRequired); + } + + 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)) { + return false; + } + + Iterable indexMetaDataCollection; + + if (requestedResolved.isLocalAll()) { + indexMetaDataCollection = new Iterable() { + @Override + public Iterator iterator() { + return clusterService.state().getMetadata().getIndices().values().iterator(); + } + }; + } else { + Set indexMetaDataSet = new HashSet<>(requestedResolved.getAllIndices().size()); + + for (String requestAliasOrIndex : requestedResolved.getAllIndices()) { + IndexMetadata indexMetaData = clusterService.state().getMetadata().getIndices().get(requestAliasOrIndex); + if (indexMetaData == null) { + if (isDebugEnabled) { + 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) { + 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 + + 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())) { + if (log.isDebugEnabled()) { + log.debug("Request " + request + " is allowed by " + 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); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/ProtectedIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/ProtectedIndexAccessEvaluator.java new file mode 100644 index 0000000000..b09e817ccc --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/ProtectedIndexAccessEvaluator.java @@ -0,0 +1,118 @@ +/* + * 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.legacy; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.RealtimeRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.common.settings.Settings; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.tasks.Task; + +public class ProtectedIndexAccessEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private final AuditLog auditLog; + private final WildcardMatcher indexMatcher; + private final WildcardMatcher allowedRolesMatcher; + private final Boolean protectedIndexEnabled; + private final WildcardMatcher deniedActionMatcher; + + public ProtectedIndexAccessEvaluator(final Settings settings, AuditLog auditLog) { + this.indexMatcher = WildcardMatcher.from( + settings.getAsList(ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, ConfigConstants.SECURITY_PROTECTED_INDICES_DEFAULT) + ); + this.allowedRolesMatcher = WildcardMatcher.from( + settings.getAsList( + ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, + ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_DEFAULT + ) + ); + this.protectedIndexEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, + ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT + ); + this.auditLog = auditLog; + + final List indexDeniedActionPatterns = new ArrayList(); + indexDeniedActionPatterns.add("indices:data/write*"); + indexDeniedActionPatterns.add("indices:admin/delete*"); + indexDeniedActionPatterns.add("indices:admin/mapping/delete*"); + indexDeniedActionPatterns.add("indices:admin/mapping/put*"); + indexDeniedActionPatterns.add("indices:admin/freeze*"); + indexDeniedActionPatterns.add("indices:admin/settings/update*"); + indexDeniedActionPatterns.add("indices:admin/aliases"); + indexDeniedActionPatterns.add("indices:admin/close*"); + indexDeniedActionPatterns.add("cluster:admin/snapshot/restore*"); + this.deniedActionMatcher = WildcardMatcher.from(indexDeniedActionPatterns); + } + + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final Task task, + final String action, + final IndexResolverReplacer.Resolved requestedResolved, + final PrivilegesEvaluatorResponse presponse, + final Set mappedRoles + ) { + if (!protectedIndexEnabled) { + return presponse; + } + if (!requestedResolved.isLocalAll() + && indexMatcher.matchAny(requestedResolved.getAllIndices()) + && deniedActionMatcher.test(action) + && !allowedRolesMatcher.matchAny(mappedRoles)) { + auditLog.logMissingPrivileges(action, request, task); + log.warn("{} for '{}' index/indices is not allowed for a regular user", action, indexMatcher); + presponse.allowed = false; + return presponse.markComplete(); + } + + if (requestedResolved.isLocalAll() && deniedActionMatcher.test(action) && !allowedRolesMatcher.matchAny(mappedRoles)) { + auditLog.logMissingPrivileges(action, request, task); + log.warn("{} for '_all' indices is not allowed for a regular user", action); + presponse.allowed = false; + return presponse.markComplete(); + } + if ((requestedResolved.isLocalAll() || indexMatcher.matchAny(requestedResolved.getAllIndices())) + && !allowedRolesMatcher.matchAny(mappedRoles)) { + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (request instanceof SearchRequest) { + ((SearchRequest) request).requestCache(Boolean.FALSE); + if (isDebugEnabled) { + log.debug("Disable search request cache for this request"); + } + } + + if (request instanceof RealtimeRequest) { + ((RealtimeRequest) request).realtime(Boolean.FALSE); + if (isDebugEnabled) { + log.debug("Disable realtime for this request"); + } + } + } + return presponse; + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorImpl.java b/src/main/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorImpl.java new file mode 100644 index 0000000000..4ae80a208c --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorImpl.java @@ -0,0 +1,104 @@ +/* + * 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.legacy; + +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +import org.greenrobot.eventbus.Subscribe; + +public class RestLayerPrivilegesEvaluatorImpl implements RestLayerPrivilegesEvaluator { + protected final Logger log = LogManager.getLogger(this.getClass()); + private final ClusterService clusterService; + private ThreadContext threadContext; + private ConfigModel configModel; + + public RestLayerPrivilegesEvaluatorImpl(final ClusterService clusterService, final ThreadPool threadPool) { + this.clusterService = clusterService; + this.threadContext = threadPool.getThreadContext(); + } + + @Subscribe + public void onConfigModelChanged(final ConfigModel configModel) { + this.configModel = configModel; + } + + SecurityRoles getSecurityRoles(final Set roles) { + return configModel.getSecurityRoles().filter(roles); + } + + boolean isInitialized() { + return configModel != null && configModel.getSecurityRoles() != null; + } + + public PrivilegesEvaluatorResponse evaluate(final User user, String routeName, final Set actions) { + if (!isInitialized()) { + throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); + } + + final PrivilegesEvaluatorResponse presponse = new PrivilegesEvaluatorResponse(); + + final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + + final Set mappedRoles = mapRoles(user, caller); + + final SecurityRoles securityRoles = getSecurityRoles(mappedRoles); + + final boolean isDebugEnabled = log.isDebugEnabled(); + if (isDebugEnabled) { + log.debug("Evaluate permissions for {} on {}", user, clusterService.localNode().getName()); + log.debug("Action: {}", actions); + log.debug("Mapped roles: {}", mappedRoles.toString()); + } + + for (final String action : actions) { + if (!securityRoles.impliesClusterPermissionPermission(action)) { + presponse.allowed = false; + log.info( + "No permission match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + action, + securityRoles.getRoleNames(), + action + ); + } else { + if (isDebugEnabled) { + log.debug("Allowed because we have permissions for {}", actions); + } + presponse.allowed = true; + + // break the loop as we found the matching permission + break; + } + } + + return presponse; + } + + Set mapRoles(final User user, final TransportAddress caller) { + return this.configModel.mapSecurityRoles(user, caller); + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/SnapshotRestoreEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/SnapshotRestoreEvaluator.java new file mode 100644 index 0000000000..00485fc97a --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/SnapshotRestoreEvaluator.java @@ -0,0 +1,117 @@ +/* + * 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.legacy; + +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +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.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.SnapshotRestoreHelper; +import org.opensearch.tasks.Task; + +public class SnapshotRestoreEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + private final boolean enableSnapshotRestorePrivilege; + private final String securityIndex; + private final AuditLog auditLog; + private final boolean restoreSecurityIndexEnabled; + + public SnapshotRestoreEvaluator(final Settings settings, AuditLog auditLog) { + this.enableSnapshotRestorePrivilege = settings.getAsBoolean( + ConfigConstants.SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, + ConfigConstants.SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE + ); + this.restoreSecurityIndexEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, false); + + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); + this.auditLog = auditLog; + } + + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final Task task, + final String action, + final ClusterInfoHolder clusterInfoHolder, + final PrivilegesEvaluatorResponse presponse + ) { + + if (!(request instanceof RestoreSnapshotRequest)) { + return presponse; + } + + // snapshot restore for regular users not enabled + if (!enableSnapshotRestorePrivilege) { + log.warn("{} is not allowed for a regular user", action); + presponse.allowed = false; + return presponse.markComplete(); + } + + // if this feature is enabled, users can also snapshot and restore + // the Security index and the global state + if (restoreSecurityIndexEnabled) { + presponse.allowed = true; + return presponse; + } + + if (clusterInfoHolder.isLocalNodeElectedClusterManager() == Boolean.FALSE) { + presponse.allowed = true; + return presponse.markComplete(); + } + + final RestoreSnapshotRequest restoreRequest = (RestoreSnapshotRequest) request; + + // Do not allow restore of global state + if (restoreRequest.includeGlobalState()) { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn("{} with 'include_global_state' enabled is not allowed", action); + presponse.allowed = false; + return presponse.markComplete(); + } + + final List rs = SnapshotRestoreHelper.resolveOriginalIndices(restoreRequest); + + if (rs != null && (rs.contains(securityIndex) || rs.contains("_all") || rs.contains("*"))) { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn("{} for '{}' as source index is not allowed", action, securityIndex); + presponse.allowed = false; + return presponse.markComplete(); + } + return presponse; + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluator.java new file mode 100644 index 0000000000..2502d239ce --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluator.java @@ -0,0 +1,364 @@ +/* + * 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.legacy; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.RealtimeRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.indices.SystemIndexRegistry; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; + +/** + * This class performs authorization on requests targeting system indices + * NOTE: + * - The term `protected system indices` used here translates to system indices + * which have an added layer of security and cannot be accessed by anyone except Super Admin + */ +public class SystemIndexAccessEvaluator { + + Logger log = LogManager.getLogger(this.getClass()); + + private final String securityIndex; + private final AuditLog auditLog; + private final IndexResolverReplacer irr; + private final boolean filterSecurityIndex; + // for system-indices configuration + private final WildcardMatcher systemIndexMatcher; + private final WildcardMatcher superAdminAccessOnlyIndexMatcher; + private final WildcardMatcher deniedActionsMatcher; + + private final boolean isSystemIndexEnabled; + private final boolean isSystemIndexPermissionEnabled; + + public SystemIndexAccessEvaluator(final Settings settings, AuditLog auditLog, IndexResolverReplacer irr) { + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); + this.auditLog = auditLog; + this.irr = irr; + this.filterSecurityIndex = settings.getAsBoolean(ConfigConstants.SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS, false); + this.systemIndexMatcher = WildcardMatcher.from( + settings.getAsList(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT) + ); + + this.superAdminAccessOnlyIndexMatcher = WildcardMatcher.from(this.securityIndex); + this.isSystemIndexEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, + ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT + ); + final boolean restoreSecurityIndexEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, + false + ); + + final List deniedActionPatternsList = deniedActionPatterns(); + + final List deniedActionPatternsListNoSnapshot = new ArrayList<>(deniedActionPatternsList); + deniedActionPatternsListNoSnapshot.add("indices:admin/close*"); + deniedActionPatternsListNoSnapshot.add("cluster:admin/snapshot/restore*"); + + deniedActionsMatcher = WildcardMatcher.from( + restoreSecurityIndexEnabled ? deniedActionPatternsList : deniedActionPatternsListNoSnapshot + ); + isSystemIndexPermissionEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY, + ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_DEFAULT + ); + } + + private static List deniedActionPatterns() { + final List securityIndexDeniedActionPatternsList = new ArrayList<>(); + securityIndexDeniedActionPatternsList.add("indices:data/write*"); + securityIndexDeniedActionPatternsList.add("indices:admin/delete*"); + securityIndexDeniedActionPatternsList.add("indices:admin/mapping/delete*"); + securityIndexDeniedActionPatternsList.add("indices:admin/mapping/put*"); + securityIndexDeniedActionPatternsList.add("indices:admin/freeze*"); + securityIndexDeniedActionPatternsList.add("indices:admin/settings/update*"); + securityIndexDeniedActionPatternsList.add("indices:admin/aliases"); + return securityIndexDeniedActionPatternsList; + } + + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final Task task, + final String action, + final Resolved requestedResolved, + final PrivilegesEvaluatorResponse presponse, + final SecurityRoles securityRoles, + final User user, + final IndexNameExpressionResolver resolver, + final ClusterService clusterService + ) { + evaluateSystemIndicesAccess(action, requestedResolved, request, task, presponse, securityRoles, user, resolver, clusterService); + + if (requestedResolved.isLocalAll() + || requestedResolved.getAllIndices().contains(securityIndex) + || requestContainsAnySystemIndices(requestedResolved)) { + + if (request instanceof SearchRequest) { + ((SearchRequest) request).requestCache(Boolean.FALSE); + if (log.isDebugEnabled()) { + log.debug("Disable search request cache for this request"); + } + } + + if (request instanceof RealtimeRequest) { + ((RealtimeRequest) request).realtime(Boolean.FALSE); + if (log.isDebugEnabled()) { + log.debug("Disable realtime for this request"); + } + } + } + return presponse; + } + + /** + * Checks if request is for any system index + * @param requestedResolved request which contains indices to be matched against system indices + * @return true if a match is found, false otherwise + */ + private boolean requestContainsAnySystemIndices(final Resolved requestedResolved) { + return !getAllSystemIndices(requestedResolved).isEmpty(); + } + + /** + * Gets all indices requested in the original request. + * It will always return security index if it is present in the request, as security index is protected regardless + * of feature being enabled or disabled + * @param requestedResolved request which contains indices to be matched against system indices + * @return the set of protected system indices present in the request + */ + private Set getAllSystemIndices(final Resolved requestedResolved) { + final Set systemIndices = requestedResolved.getAllIndices() + .stream() + .filter(securityIndex::equals) + .collect(Collectors.toSet()); + if (isSystemIndexEnabled) { + systemIndices.addAll(systemIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList())); + systemIndices.addAll(SystemIndexRegistry.matchesSystemIndexPattern(requestedResolved.getAllIndices())); + } + return systemIndices; + } + + /** + * Checks if request contains any system index that is non-permission-able + * NOTE: Security index is currently non-permission-able + * @param requestedResolved request which contains indices to be matched against non-permission-able system indices + * @return true if the request contains any non-permission-able index,false otherwise + */ + private boolean requestContainsAnyProtectedSystemIndices(final Resolved requestedResolved) { + return !getAllProtectedSystemIndices(requestedResolved).isEmpty(); + } + + /** + * Filters the request to get all system indices that are protected and are non-permission-able + * @param requestedResolved request which contains indices to be matched against non-permission-able system indices + * @return the list of protected system indices present in the request + */ + private List getAllProtectedSystemIndices(final Resolved requestedResolved) { + return new ArrayList<>(superAdminAccessOnlyIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList())); + } + + /** + * Checks if the request contains any regular (non-system and non-protected) indices. + * Regular indices are those that are not categorized as system indices or protected system indices. + * This method helps in identifying requests that might be accessing regular indices alongside system indices. + * @param requestedResolved The resolved object of the request, which contains the list of indices from the original request. + * @return true if the request contains any regular indices, false otherwise. + */ + private boolean requestContainsAnyRegularIndices(final Resolved requestedResolved) { + Set allIndices = requestedResolved.getAllIndices(); + + Set allSystemIndices = getAllSystemIndices(requestedResolved); + List allProtectedSystemIndices = getAllProtectedSystemIndices(requestedResolved); + + return allIndices.stream().anyMatch(index -> !allSystemIndices.contains(index) && !allProtectedSystemIndices.contains(index)); + } + + /** + * Is the current action allowed to be performed on security index + * @param action request action on security index + * @return true if action is allowed, false otherwise + */ + private boolean isActionAllowed(String action) { + return deniedActionsMatcher.test(action); + } + + /** + * Perform access check on requested indices and actions for those indices + * @param action action to be performed on request indices + * @param requestedResolved this object contains all indices this request is resolved to + * @param request the action request to be used for audit logging + * @param task task in which this access check will be performed + * @param presponse the pre-response object that will eventually become a response and returned to the requester + * @param securityRoles user's roles which will be used for access evaluation + * @param user this user's permissions will be looked up + * @param resolver the index expression resolver + * @param clusterService required to fetch cluster state metadata + */ + private void evaluateSystemIndicesAccess( + final String action, + final Resolved requestedResolved, + final ActionRequest request, + final Task task, + final PrivilegesEvaluatorResponse presponse, + SecurityRoles securityRoles, + final User user, + final IndexNameExpressionResolver resolver, + final ClusterService clusterService + ) { + // Perform access check is system index permissions are enabled + boolean containsSystemIndex = requestContainsAnySystemIndices(requestedResolved); + boolean containsRegularIndex = requestContainsAnyRegularIndices(requestedResolved); + boolean serviceAccountUser = user.isServiceAccount(); + + if (isSystemIndexPermissionEnabled) { + if (serviceAccountUser && containsRegularIndex) { + auditLog.logSecurityIndexAttempt(request, action, task); + if (!containsSystemIndex && log.isInfoEnabled()) { + log.info("{} not permitted for a service account {} on non-system indices.", action, securityRoles); + } else if (containsSystemIndex && log.isDebugEnabled()) { + List regularIndices = requestedResolved.getAllIndices() + .stream() + .filter( + index -> !getAllSystemIndices(requestedResolved).contains(index) + && !getAllProtectedSystemIndices(requestedResolved).contains(index) + ) + .collect(Collectors.toList()); + log.debug("Service account cannot access regular indices: {}", regularIndices); + } + presponse.allowed = false; + presponse.markComplete(); + return; + } + boolean containsProtectedIndex = requestContainsAnyProtectedSystemIndices(requestedResolved); + if (containsProtectedIndex) { + auditLog.logSecurityIndexAttempt(request, action, task); + if (log.isInfoEnabled()) { + log.info( + "{} not permitted for a regular user {} on protected system indices {}", + action, + securityRoles, + String.join(", ", getAllProtectedSystemIndices(requestedResolved)) + ); + } + presponse.allowed = false; + presponse.markComplete(); + return; + } else if (containsSystemIndex + && !securityRoles.hasExplicitIndexPermission( + requestedResolved, + user, + new String[] { ConfigConstants.SYSTEM_INDEX_PERMISSION }, + resolver, + clusterService + )) { + auditLog.logSecurityIndexAttempt(request, action, task); + if (log.isInfoEnabled()) { + log.info( + "No {} permission for user roles {} to System Indices {}", + action, + securityRoles, + String.join(", ", getAllSystemIndices(requestedResolved)) + ); + } + presponse.allowed = false; + presponse.markComplete(); + return; + } + } + + if (isActionAllowed(action)) { + if (requestedResolved.isLocalAll()) { + if (filterSecurityIndex) { + irr.replace(request, false, "*", "-" + securityIndex); + if (log.isDebugEnabled()) { + log.debug( + "Filtered '{}' from {}, resulting list with *,-{} is {}", + securityIndex, + requestedResolved, + securityIndex, + irr.resolveRequest(request) + ); + } + } else { + auditLog.logSecurityIndexAttempt(request, action, task); + log.warn("{} for '_all' indices is not allowed for a regular user", action); + presponse.allowed = false; + presponse.markComplete(); + } + } + // if system index is enabled and system index permissions are enabled we don't need to perform any further + // checks as it has already been performed via hasExplicitIndexPermission + else if (containsSystemIndex && !isSystemIndexPermissionEnabled) { + if (filterSecurityIndex) { + Set allWithoutSecurity = new HashSet<>(requestedResolved.getAllIndices()); + allWithoutSecurity.remove(securityIndex); + if (allWithoutSecurity.isEmpty()) { + if (log.isDebugEnabled()) { + log.debug("Filtered '{}' but resulting list is empty", securityIndex); + } + presponse.allowed = false; + presponse.markComplete(); + return; + } + irr.replace(request, false, allWithoutSecurity.toArray(new String[0])); + if (log.isDebugEnabled()) { + log.debug("Filtered '{}', resulting list is {}", securityIndex, allWithoutSecurity); + } + } else { + auditLog.logSecurityIndexAttempt(request, action, task); + final String foundSystemIndexes = String.join(", ", getAllSystemIndices(requestedResolved)); + log.warn("{} for '{}' index is not allowed for a regular user", action, foundSystemIndexes); + presponse.allowed = false; + presponse.markComplete(); + } + } + } + } +} diff --git a/src/main/java/org/opensearch/security/privileges/legacy/TermsAggregationEvaluator.java b/src/main/java/org/opensearch/security/privileges/legacy/TermsAggregationEvaluator.java new file mode 100644 index 0000000000..2a750ef69b --- /dev/null +++ b/src/main/java/org/opensearch/security/privileges/legacy/TermsAggregationEvaluator.java @@ -0,0 +1,119 @@ +/* + * 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.legacy; + +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.fieldcaps.FieldCapabilitiesAction; +import org.opensearch.action.get.GetAction; +import org.opensearch.action.get.MultiGetAction; +import org.opensearch.action.search.MultiSearchAction; +import org.opensearch.action.search.SearchAction; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.index.query.MatchNoneQueryBuilder; +import org.opensearch.index.query.QueryBuilder; +import org.opensearch.index.query.TermsQueryBuilder; +import org.opensearch.search.aggregations.AggregationBuilder; +import org.opensearch.search.aggregations.bucket.terms.TermsAggregationBuilder; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.user.User; + +public class TermsAggregationEvaluator { + + protected final Logger log = LogManager.getLogger(this.getClass()); + + private static final String[] READ_ACTIONS = new String[] { + MultiSearchAction.NAME, + MultiGetAction.NAME, + GetAction.NAME, + SearchAction.NAME, + FieldCapabilitiesAction.NAME }; + + private static final QueryBuilder NONE_QUERY = new MatchNoneQueryBuilder(); + + public TermsAggregationEvaluator() {} + + public PrivilegesEvaluatorResponse evaluate( + final Resolved resolved, + final ActionRequest request, + ClusterService clusterService, + User user, + SecurityRoles securityRoles, + IndexNameExpressionResolver resolver, + PrivilegesEvaluatorResponse presponse + ) { + try { + if (request instanceof SearchRequest) { + SearchRequest sr = (SearchRequest) request; + + if (sr.source() != null + && sr.source().query() == null + && sr.source().aggregations() != null + && sr.source().aggregations().getAggregatorFactories() != null + && sr.source().aggregations().getAggregatorFactories().size() == 1 + && sr.source().size() == 0) { + AggregationBuilder ab = sr.source().aggregations().getAggregatorFactories().iterator().next(); + if (ab instanceof TermsAggregationBuilder && "terms".equals(ab.getType()) && "indices".equals(ab.getName())) { + if ("_index".equals(((TermsAggregationBuilder) ab).field()) + && ab.getPipelineAggregations().isEmpty() + && ab.getSubAggregations().isEmpty()) { + + final Set allPermittedIndices = securityRoles.getAllPermittedIndicesForDashboards( + resolved, + user, + READ_ACTIONS, + resolver, + clusterService + ); + if (allPermittedIndices == null || allPermittedIndices.isEmpty()) { + sr.source().query(NONE_QUERY); + } else { + sr.source().query(new TermsQueryBuilder("_index", allPermittedIndices)); + } + + presponse.allowed = true; + return presponse.markComplete(); + } + } + } + } + } catch (Exception e) { + log.warn("Unable to evaluate terms aggregation", e); + return presponse; + } + + return presponse; + } +} diff --git a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java index 2dfe02115c..1237c917bf 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java @@ -457,6 +457,7 @@ public SecurityDynamicConfiguration deepClone() { return result; } else { // We are on a pre-v7 config version. This can be only if we skipped auto conversion. So, we do here the same. + @SuppressWarnings("unchecked") SecurityDynamicConfiguration result = (SecurityDynamicConfiguration) fromJsonWithoutAutoConversion( DefaultObjectMapper.writeValueAsString(this, false), ctypeUnsafe, diff --git a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java index 37686ca3c4..3c88023bb0 100644 --- a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java +++ b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java @@ -15,8 +15,8 @@ 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.privileges.PrivilegesEvaluatorImpl.DNFOF_MATCHER; +import static org.opensearch.security.privileges.PrivilegesEvaluatorImpl.isClusterPerm; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java index da35226d62..f9e650f734 100644 --- a/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/RestLayerPrivilegesEvaluatorTest.java @@ -91,8 +91,8 @@ public void testEvaluate_Initialized_Success() throws Exception { " cluster_permissions:\n" + // " - any", CType.ROLES); - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(roles); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); @@ -102,8 +102,8 @@ public void testEvaluate_Initialized_Success() throws Exception { @Test public void testEvaluate_NotInitialized_NullModel_ExceptionThrown() { - PrivilegesEvaluator privilegesEvaluator = createPrivilegesEvaluator(null); - RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluator(privilegesEvaluator); + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(null); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); final OpenSearchSecurityException exception = assertThrows( OpenSearchSecurityException.class, () -> restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", null) @@ -117,8 +117,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); + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(true)); } @@ -129,8 +129,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); + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(true)); } @@ -141,14 +141,14 @@ 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); + PrivilegesEvaluatorImpl privilegesEvaluator = createPrivilegesEvaluator(roles); + RestLayerPrivilegesEvaluator restPrivilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl(privilegesEvaluator); PrivilegesEvaluatorResponse response = restPrivilegesEvaluator.evaluate(TEST_USER, "route_name", Set.of(action)); assertThat(response.allowed, equalTo(false)); } - PrivilegesEvaluator createPrivilegesEvaluator(SecurityDynamicConfiguration roles) { - PrivilegesEvaluator privilegesEvaluator = new PrivilegesEvaluator( + PrivilegesEvaluatorImpl createPrivilegesEvaluator(SecurityDynamicConfiguration roles) { + PrivilegesEvaluatorImpl privilegesEvaluator = new PrivilegesEvaluatorImpl( clusterService, () -> clusterService.state(), null, diff --git a/src/test/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorTest.java new file mode 100644 index 0000000000..053643daeb --- /dev/null +++ b/src/test/java/org/opensearch/security/privileges/legacy/RestLayerPrivilegesEvaluatorTest.java @@ -0,0 +1,183 @@ +/* + * 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.legacy; + +import java.util.Collections; +import java.util.Set; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.config.Configurator; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.OpenSearchSecurityException; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +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.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +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 ThreadPool threadPool; + @Mock + private ConfigModel configModel; + + private RestLayerPrivilegesEvaluatorImpl privilegesEvaluator; + + private static final User TEST_USER = new User("test_user"); + + private void setLoggingLevel(final Level level) { + final Logger restLayerPrivilegesEvaluatorLogger = LogManager.getLogger(RestLayerPrivilegesEvaluatorImpl.class); + Configurator.setLevel(restLayerPrivilegesEvaluatorLogger, level); + } + + @Before + public void setUp() { + when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + + when(clusterService.localNode()).thenReturn(mock(DiscoveryNode.class, withSettings().strictness(Strictness.LENIENT))); + privilegesEvaluator = new RestLayerPrivilegesEvaluatorImpl( + clusterService, + threadPool + ); + privilegesEvaluator.onConfigModelChanged(configModel); // Defaults to the mocked config model + verify(threadPool).getThreadContext(); // Called during construction of RestLayerPrivilegesEvaluator + setLoggingLevel(Level.DEBUG); // Enable debug logging scenarios for verification + } + + @After + public void after() { + setLoggingLevel(Level.INFO); + } + + @Test + public void testEvaluate_Initialized_Success() { + String action = "action"; + SecurityRoles securityRoles = mock(SecurityRoles.class); + when(configModel.getSecurityRoles()).thenReturn(securityRoles); + when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); + when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(false); + + PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, "", Set.of(action)); + + assertThat(response.isAllowed(), equalTo(false)); + assertThat(response.getMissingPrivileges(), equalTo(Set.of(action))); + verify(configModel, times(3)).getSecurityRoles(); + } + + @Test + public void testEvaluate_NotInitialized_NullModel_ExceptionThrown() { + // Null out the config model + privilegesEvaluator.onConfigModelChanged(null); + final OpenSearchSecurityException exception = assertThrows( + OpenSearchSecurityException.class, + () -> privilegesEvaluator.evaluate(TEST_USER, null, Set.of("")) + ); + assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); + verify(configModel, never()).getSecurityRoles(); + } + + @Test + public void testEvaluate_NotInitialized_NoSecurityRoles_ExceptionThrown() { + final OpenSearchSecurityException exception = assertThrows( + OpenSearchSecurityException.class, + () -> privilegesEvaluator.evaluate(TEST_USER, null, Set.of("")) + ); + assertThat(exception.getMessage(), equalTo("OpenSearch Security is not initialized.")); + verify(configModel).getSecurityRoles(); + } + + @Test + public void testMapRoles_ReturnsMappedRoles() { + final User user = mock(User.class); + final Set mappedRoles = Collections.singleton("role1"); + when(configModel.mapSecurityRoles(any(), any())).thenReturn(mappedRoles); + + final Set result = privilegesEvaluator.mapRoles(user, null); + + assertThat(result, equalTo(mappedRoles)); + verifyNoInteractions(user); + verify(configModel).mapSecurityRoles(user, null); + } + + @Test + public void testEvaluate_Successful_NewPermission() { + String action = "hw:greet"; + SecurityRoles securityRoles = mock(SecurityRoles.class); + when(configModel.getSecurityRoles()).thenReturn(securityRoles); + when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); + when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(true); + + PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, "", Set.of(action)); + + assertThat(response.allowed, equalTo(true)); + verify(securityRoles).impliesClusterPermissionPermission(action); + } + + @Test + public void testEvaluate_Successful_LegacyPermission() { + String action = "cluster:admin/opensearch/hw/greet"; + SecurityRoles securityRoles = mock(SecurityRoles.class); + when(configModel.getSecurityRoles()).thenReturn(securityRoles); + when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); + when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(true); + + PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, "", Set.of(action)); + + assertThat(response.allowed, equalTo(true)); + verify(securityRoles).impliesClusterPermissionPermission(action); + verify(configModel, times(3)).getSecurityRoles(); + } + + @Test + public void testEvaluate_Unsuccessful() { + String action = "action"; + SecurityRoles securityRoles = mock(SecurityRoles.class); + when(configModel.getSecurityRoles()).thenReturn(securityRoles); + when(configModel.getSecurityRoles().filter(Collections.emptySet())).thenReturn(securityRoles); + when(securityRoles.impliesClusterPermissionPermission(action)).thenReturn(false); + + PrivilegesEvaluatorResponse response = privilegesEvaluator.evaluate(TEST_USER, "", Set.of(action)); + + assertThat(response.allowed, equalTo(false)); + verify(securityRoles).impliesClusterPermissionPermission(action); + } +} diff --git a/src/test/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluatorTest.java new file mode 100644 index 0000000000..e9e7d65e9f --- /dev/null +++ b/src/test/java/org/opensearch/security/privileges/legacy/SystemIndexAccessEvaluatorTest.java @@ -0,0 +1,691 @@ +/* + * 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.legacy; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.Logger; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.support.IndicesOptions; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.privileges.PrivilegesEvaluatorResponse; +import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resolver.IndexResolverReplacer.Resolved; +import org.opensearch.security.securityconf.ConfigModelV7; +import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.tasks.Task; + +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.opensearch.security.support.ConfigConstants.SYSTEM_INDEX_PERMISSION; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SystemIndexAccessEvaluatorTest { + + @Mock + private AuditLog auditLog; + @Mock + private IndexResolverReplacer irr; + @Mock + private ActionRequest request; + @Mock + private Task task; + @Mock + private PrivilegesEvaluatorResponse presponse; + @Mock + private Logger log; + @Mock + ClusterService cs; + + private SystemIndexAccessEvaluator evaluator; + private static final String UNPROTECTED_ACTION = "indices:data/read"; + private static final String PROTECTED_ACTION = "indices:data/write"; + + private static final String TEST_SYSTEM_INDEX = ".test_system_index"; + private static final String TEST_INDEX = ".test"; + private static final String SECURITY_INDEX = ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX; + + @Mock + SecurityRoles securityRoles; + + User user; + + IndexNameExpressionResolver indexNameExpressionResolver; + + private ThreadContext createThreadContext() { + return new ThreadContext(Settings.EMPTY); + } + + protected IndexNameExpressionResolver createIndexNameExpressionResolver(ThreadContext threadContext) { + return new IndexNameExpressionResolver(threadContext); + } + + public void setup( + boolean isSystemIndexEnabled, + boolean isSystemIndexPermissionsEnabled, + String index, + boolean createIndexPatternWithSystemIndexPermission + ) { + ThreadContext threadContext = createThreadContext(); + indexNameExpressionResolver = createIndexNameExpressionResolver(threadContext); + + // create a security role + ConfigModelV7.IndexPattern ip = spy(new ConfigModelV7.IndexPattern(index)); + ConfigModelV7.SecurityRole.Builder _securityRole = new ConfigModelV7.SecurityRole.Builder("role_a"); + ip.addPerm(createIndexPatternWithSystemIndexPermission ? Set.of("*", SYSTEM_INDEX_PERMISSION) : Set.of("*")); + _securityRole.addIndexPattern(ip); + _securityRole.addClusterPerms(List.of("*")); + ConfigModelV7.SecurityRole secRole = _securityRole.build(); + + try { + // create an instance of Security Role + Constructor constructor = ConfigModelV7.SecurityRoles.class.getDeclaredConstructor(int.class); + constructor.setAccessible(true); + securityRoles = constructor.newInstance(1); + + // add security role to Security Roles + Method addSecurityRoleMethod = ConfigModelV7.SecurityRoles.class.getDeclaredMethod( + "addSecurityRole", + ConfigModelV7.SecurityRole.class + ); + addSecurityRoleMethod.setAccessible(true); + addSecurityRoleMethod.invoke(securityRoles, secRole); + + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new RuntimeException(e); + } + + // create a user and associate them with the role + user = new User("user_a"); + user.addSecurityRoles(List.of("role_a")); + + // when trying to resolve Index Names + + evaluator = new SystemIndexAccessEvaluator( + Settings.builder() + .put(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, TEST_SYSTEM_INDEX) + .put(ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, isSystemIndexEnabled) + .put(ConfigConstants.SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY, isSystemIndexPermissionsEnabled) + .build(), + auditLog, + irr + ); + evaluator.log = log; + + when(log.isDebugEnabled()).thenReturn(true); + when(log.isInfoEnabled()).thenReturn(true); + + doReturn(ImmutableSet.of(index)).when(ip).getResolvedIndexPattern(user, indexNameExpressionResolver, cs, true, false); + } + + @After + public void after() { + verifyNoMoreInteractions(auditLog, irr, request, task, presponse, log); + } + + @Test + public void testUnprotectedActionOnRegularIndex_systemIndexDisabled() { + setup(false, false, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + + } + + @Test + public void testUnprotectedActionOnRegularIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + } + + @Test + public void testUnprotectedActionOnRegularIndex_systemIndexPermissionEnabled() { + setup(true, true, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + } + + @Test + public void testUnprotectedActionOnSystemIndex_systemIndexDisabled() { + setup(false, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + } + + @Test + public void testUnprotectedActionOnSystemIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verifyNoInteractions(presponse); + assertThat(response, is(presponse)); + } + + @Test + public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_WithoutSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + verify(presponse).markComplete(); + assertThat(response, is(presponse)); + + verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); + verify(log).isInfoEnabled(); + verify(log).info("No {} permission for user roles {} to System Indices {}", UNPROTECTED_ACTION, securityRoles, TEST_SYSTEM_INDEX); + } + + @Test + public void testUnprotectedActionOnSystemIndex_systemIndexPermissionEnabled_WithSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, true); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + final PrivilegesEvaluatorResponse response = evaluator.evaluate( + request, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + assertThat(response, is(presponse)); + // unprotected action is not allowed on a system index + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexDisabled() { + setup(false, false, TEST_SYSTEM_INDEX, false); + + final SearchRequest searchRequest = mock(SearchRequest.class); + final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate( + searchRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + evaluator.evaluate( + realtimeRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + + verifyNoInteractions(presponse); + } + + @Test + public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_SYSTEM_INDEX, false); + + final SearchRequest searchRequest = mock(SearchRequest.class); + final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate( + searchRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + evaluator.evaluate( + realtimeRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + + verify(searchRequest).requestCache(Boolean.FALSE); + verify(realtimeRequest).realtime(Boolean.FALSE); + + verify(log, times(2)).isDebugEnabled(); + verify(log).debug("Disable search request cache for this request"); + verify(log).debug("Disable realtime for this request"); + } + + @Test + public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, false); + + final SearchRequest searchRequest = mock(SearchRequest.class); + final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate( + searchRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + evaluator.evaluate( + realtimeRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + + verify(searchRequest).requestCache(Boolean.FALSE); + verify(realtimeRequest).realtime(Boolean.FALSE); + + verify(log, times(2)).isDebugEnabled(); + verify(log).debug("Disable search request cache for this request"); + verify(log).debug("Disable realtime for this request"); + verify(auditLog).logSecurityIndexAttempt(request, UNPROTECTED_ACTION, null); + verify(auditLog).logSecurityIndexAttempt(searchRequest, UNPROTECTED_ACTION, null); + verify(auditLog).logSecurityIndexAttempt(realtimeRequest, UNPROTECTED_ACTION, null); + verify(presponse, times(3)).markComplete(); + verify(log, times(2)).isDebugEnabled(); + verify(log, times(3)).isInfoEnabled(); + verify(log, times(3)).info( + "No {} permission for user roles {} to System Indices {}", + UNPROTECTED_ACTION, + securityRoles, + TEST_SYSTEM_INDEX + ); + verify(log).debug("Disable search request cache for this request"); + verify(log).debug("Disable realtime for this request"); + } + + @Test + public void testDisableCacheOrRealtimeOnSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, true); + + final SearchRequest searchRequest = mock(SearchRequest.class); + final MultiGetRequest realtimeRequest = mock(MultiGetRequest.class); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, null, UNPROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + evaluator.evaluate( + searchRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + evaluator.evaluate( + realtimeRequest, + null, + UNPROTECTED_ACTION, + resolved, + presponse, + securityRoles, + user, + indexNameExpressionResolver, + cs + ); + + verify(searchRequest).requestCache(Boolean.FALSE); + verify(realtimeRequest).realtime(Boolean.FALSE); + + verify(log, times(2)).isDebugEnabled(); + verify(log).debug("Disable search request cache for this request"); + verify(log).debug("Disable realtime for this request"); + } + + @Test + public void testProtectedActionLocalAll_systemIndexDisabled() { + setup(false, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = Resolved._LOCAL_ALL; + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).warn("{} for '_all' indices is not allowed for a regular user", "indices:data/write"); + } + + @Test + public void testProtectedActionLocalAll_systemIndexPermissionDisabled() { + setup(true, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = Resolved._LOCAL_ALL; + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); + } + + @Test + public void testProtectedActionLocalAll_systemIndexPermissionEnabled() { + setup(true, true, TEST_SYSTEM_INDEX, false); + final Resolved resolved = Resolved._LOCAL_ALL; + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).warn("{} for '_all' indices is not allowed for a regular user", PROTECTED_ACTION); + } + + @Test + public void testProtectedActionOnRegularIndex_systemIndexDisabled() { + setup(false, false, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnRegularIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnRegularIndex_systemIndexPermissionEnabled() { + setup(true, true, TEST_INDEX, false); + final Resolved resolved = createResolved(TEST_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnSystemIndex_systemIndexDisabled() { + setup(false, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnSystemIndex_systemIndexPermissionDisabled() { + setup(true, false, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, TEST_SYSTEM_INDEX); + } + + @Test + public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { + setup(true, true, TEST_SYSTEM_INDEX, false); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + verify(log).isInfoEnabled(); + verify(log).info("No {} permission for user roles {} to System Indices {}", PROTECTED_ACTION, securityRoles, TEST_SYSTEM_INDEX); + } + + @Test + public void testProtectedActionOnSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { + + setup(true, true, TEST_SYSTEM_INDEX, true); + final Resolved resolved = createResolved(TEST_SYSTEM_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + assertThat(presponse.allowed, is(false)); + } + + @Test + public void testProtectedActionOnProtectedSystemIndex_systemIndexDisabled() { + setup(false, false, SECURITY_INDEX, false); + final Resolved resolved = createResolved(SECURITY_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + + verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); + } + + @Test + public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionDisabled() { + setup(true, false, SECURITY_INDEX, false); + final Resolved resolved = createResolved(SECURITY_INDEX); + + // Action + evaluator.evaluate(request, task, PROTECTED_ACTION, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, PROTECTED_ACTION, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + + verify(log).warn("{} for '{}' index is not allowed for a regular user", PROTECTED_ACTION, SECURITY_INDEX); + } + + @Test + public void testUnprotectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { + testSecurityIndexAccess(UNPROTECTED_ACTION); + } + + @Test + public void testUnprotectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { + testSecurityIndexAccess(UNPROTECTED_ACTION); + } + + @Test + public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withoutSystemIndexPermission() { + testSecurityIndexAccess(PROTECTED_ACTION); + } + + @Test + public void testProtectedActionOnProtectedSystemIndex_systemIndexPermissionEnabled_withSystemIndexPermission() { + testSecurityIndexAccess(PROTECTED_ACTION); + } + + private void testSecurityIndexAccess(String action) { + setup(true, true, SECURITY_INDEX, true); + + final Resolved resolved = createResolved(SECURITY_INDEX); + + // Action + evaluator.evaluate(request, task, action, resolved, presponse, securityRoles, user, indexNameExpressionResolver, cs); + + verify(auditLog).logSecurityIndexAttempt(request, action, task); + assertThat(presponse.allowed, is(false)); + verify(presponse).markComplete(); + + verify(log).isInfoEnabled(); + verify(log).info("{} not permitted for a regular user {} on protected system indices {}", action, securityRoles, SECURITY_INDEX); + } + + private Resolved createResolved(final String... indexes) { + return new Resolved( + ImmutableSet.of(), + ImmutableSet.copyOf(indexes), + ImmutableSet.copyOf(indexes), + ImmutableSet.of(), + IndicesOptions.STRICT_EXPAND_OPEN + ); + } +}