diff --git a/server/src/main/java/org/elasticsearch/common/util/iterable/Iterables.java b/server/src/main/java/org/elasticsearch/common/util/iterable/Iterables.java index 6d1dab4a9d010..e7c002438c576 100644 --- a/server/src/main/java/org/elasticsearch/common/util/iterable/Iterables.java +++ b/server/src/main/java/org/elasticsearch/common/util/iterable/Iterables.java @@ -24,6 +24,7 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.function.Predicate; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -103,6 +104,17 @@ public static T get(Iterable iterable, int position) { } } + public static int indexOf(Iterable iterable, Predicate predicate) { + int i = 0; + for (T element : iterable) { + if (predicate.test(element)) { + return i; + } + i++; + } + return -1; + } + public static long size(Iterable iterable) { return StreamSupport.stream(iterable.spliterator(), true).count(); } diff --git a/server/src/test/java/org/elasticsearch/common/util/iterable/IterablesTests.java b/server/src/test/java/org/elasticsearch/common/util/iterable/IterablesTests.java index 6501c7caa1d64..6668d0e6467e9 100644 --- a/server/src/test/java/org/elasticsearch/common/util/iterable/IterablesTests.java +++ b/server/src/test/java/org/elasticsearch/common/util/iterable/IterablesTests.java @@ -26,7 +26,10 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import static org.hamcrest.Matchers.is; import static org.hamcrest.object.HasToString.hasToString; public class IterablesTests extends ESTestCase { @@ -86,6 +89,19 @@ public void testFlatten() { assertEquals(1, count); } + public void testIndexOf() { + final List list = Stream.generate(() -> randomAlphaOfLengthBetween(3, 9)) + .limit(randomIntBetween(10, 30)) + .distinct() + .collect(Collectors.toUnmodifiableList()); + for (int i = 0; i < list.size(); i++) { + final String val = list.get(i); + assertThat(Iterables.indexOf(list, val::equals), is(i)); + } + assertThat(Iterables.indexOf(list, s -> false), is(-1)); + assertThat(Iterables.indexOf(list, s -> true), is(0)); + } + private void test(Iterable iterable) { try { Iterables.get(iterable, -1); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java index 8b2ae8ec14d7a..275dbdef3ef0a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java @@ -8,6 +8,8 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.cluster.metadata.IndexAbstraction; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; import org.elasticsearch.transport.TransportRequest; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; @@ -292,6 +294,14 @@ public boolean isAuditable() { return auditable; } + /** + * Returns additional context about an authorization failure, if {@link #isGranted()} is false. + */ + @Nullable + public String getFailureContext() { + return null; + } + /** * Returns a new authorization result that is granted and auditable */ @@ -321,6 +331,19 @@ public IndexAuthorizationResult(boolean auditable, IndicesAccessControl indicesA this.indicesAccessControl = indicesAccessControl; } + @Override + public String getFailureContext() { + if (isGranted()) { + return null; + } else { + return getFailureDescription(indicesAccessControl.getDeniedIndices()); + } + } + + public static String getFailureDescription(Collection deniedIndices) { + return "on indices [" + Strings.collectionToCommaDelimitedString(deniedIndices) + "]"; + } + public IndicesAccessControl getIndicesAccessControl() { return indicesAccessControl; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java index 604508f95c5af..327b87dc495e0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java @@ -11,10 +11,12 @@ import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions; import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * Encapsulates the field and document permissions per concrete index based on the current request. @@ -51,6 +53,13 @@ public boolean isGranted() { return granted; } + public Collection getDeniedIndices() { + return this.indexPermissions.entrySet().stream() + .filter(e -> e.getValue().granted == false) + .map(Map.Entry::getKey) + .collect(Collectors.toUnmodifiableSet()); + } + /** * Encapsulates the field and document permissions for an index. */ diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java index c4f9048c17ee2..9902353829a4c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilege.java @@ -13,18 +13,18 @@ import org.elasticsearch.action.admin.indices.close.CloseIndexAction; import org.elasticsearch.action.admin.indices.create.AutoCreateAction; import org.elasticsearch.action.admin.indices.create.CreateIndexAction; -import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction; -import org.elasticsearch.xpack.core.action.CreateDataStreamAction; -import org.elasticsearch.xpack.core.action.DeleteDataStreamAction; -import org.elasticsearch.xpack.core.action.GetDataStreamAction; import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; import org.elasticsearch.action.admin.indices.get.GetIndexAction; import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsAction; import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsAction; import org.elasticsearch.action.admin.indices.mapping.put.AutoPutMappingAction; +import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction; import org.elasticsearch.action.admin.indices.settings.get.GetSettingsAction; import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryAction; import org.elasticsearch.common.Strings; +import org.elasticsearch.xpack.core.action.CreateDataStreamAction; +import org.elasticsearch.xpack.core.action.DeleteDataStreamAction; +import org.elasticsearch.xpack.core.action.GetDataStreamAction; import org.elasticsearch.xpack.core.ccr.action.ForgetFollowerAction; import org.elasticsearch.xpack.core.ccr.action.PutFollowAction; import org.elasticsearch.xpack.core.ccr.action.UnfollowAction; @@ -32,6 +32,7 @@ import org.elasticsearch.xpack.core.security.support.Automatons; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Locale; @@ -39,6 +40,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; +import java.util.stream.Collectors; import static java.util.Map.entry; import static org.elasticsearch.xpack.core.security.support.Automatons.patterns; @@ -95,7 +97,7 @@ public final class IndexPrivilege extends Privilege { public static final IndexPrivilege MAINTENANCE = new IndexPrivilege("maintenance", MAINTENANCE_AUTOMATON); public static final IndexPrivilege AUTO_CONFIGURE = new IndexPrivilege("auto_configure", AUTO_CONFIGURE_AUTOMATON); - private static final Map VALUES = Map.ofEntries( + private static final Map VALUES = sortByAccessLevel(Map.ofEntries( entry("none", NONE), entry("all", ALL), entry("manage", MANAGE), @@ -114,7 +116,7 @@ public final class IndexPrivilege extends Privilege { entry("manage_leader_index", MANAGE_LEADER_INDEX), entry("manage_ilm", MANAGE_ILM), entry("maintenance", MAINTENANCE), - entry("auto_configure", AUTO_CONFIGURE)); + entry("auto_configure", AUTO_CONFIGURE))); public static final Predicate ACTION_MATCHER = ALL.predicate(); public static final Predicate CREATE_INDEX_MATCHER = CREATE_INDEX.predicate(); @@ -152,7 +154,7 @@ private static IndexPrivilege resolve(Set name) { if (ACTION_MATCHER.test(part)) { actions.add(actionToPattern(part)); } else { - IndexPrivilege indexPrivilege = VALUES.get(part); + IndexPrivilege indexPrivilege = part == null ? null : VALUES.get(part); if (indexPrivilege != null && size == 1) { return indexPrivilege; } else if (indexPrivilege != null) { @@ -182,4 +184,16 @@ public static Set names() { return Collections.unmodifiableSet(VALUES.keySet()); } + /** + * Returns the names of privileges that grant the specified action. + * @return A collection of names, ordered (to the extent possible) from least privileged (e.g. {@link #CREATE_DOC}) + * to most privileged (e.g. {@link #ALL}) + * @see Privilege#sortByAccessLevel + */ + public static Collection findPrivilegesThatGrant(String action) { + return VALUES.entrySet().stream() + .filter(e -> e.getValue().predicate.test(action)) + .map(e -> e.getKey()) + .collect(Collectors.toUnmodifiableList()); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/Privilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/Privilege.java index 54db92dacae88..2f1eb0728f129 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/Privilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/Privilege.java @@ -6,10 +6,16 @@ package org.elasticsearch.xpack.core.security.authz.privilege; import org.apache.lucene.util.automaton.Automaton; +import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.xpack.core.security.support.Automatons; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; import java.util.function.Predicate; import static org.elasticsearch.xpack.core.security.support.Automatons.patterns; @@ -74,4 +80,21 @@ public String toString() { public Automaton getAutomaton() { return automaton; } + + /** + * Sorts the map of privileges from least-privilege to most-privilege + */ + static SortedMap sortByAccessLevel(Map privileges) { + // How many other privileges is this privilege a subset of. Those with a higher count are considered to be a lower privilege + final Map subsetCount = new HashMap<>(privileges.size()); + privileges.forEach((name, priv) -> subsetCount.put(name, + privileges.values().stream().filter(p2 -> p2 != priv && Operations.subsetOf(priv.automaton, p2.automaton)).count()) + ); + + final Comparator compare = Comparator.comparingLong(key -> subsetCount.getOrDefault(key, 0L)).reversed() + .thenComparing(Comparator.naturalOrder()); + final TreeMap tree = new TreeMap<>(compare); + tree.putAll(privileges); + return Collections.unmodifiableSortedMap(tree); + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java new file mode 100644 index 0000000000000..8a82825972aa4 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/privilege/IndexPrivilegeTests.java @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.core.security.authz.privilege; + +import org.elasticsearch.action.admin.indices.refresh.RefreshAction; +import org.elasticsearch.action.admin.indices.shrink.ShrinkAction; +import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction; +import org.elasticsearch.action.delete.DeleteAction; +import org.elasticsearch.action.index.IndexAction; +import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.update.UpdateAction; +import org.elasticsearch.common.util.iterable.Iterables; +import org.elasticsearch.test.ESTestCase; + +import java.util.List; +import java.util.Set; + +import static org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege.findPrivilegesThatGrant; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.lessThan; + +public class IndexPrivilegeTests extends ESTestCase { + + /** + * The {@link IndexPrivilege#values()} map is sorted so that privilege names that offer the _least_ access come before those that + * offer _more_ access. There is no guarantee of ordering between privileges that offer non-overlapping privileges. + */ + public void testOrderingOfPrivilegeNames() throws Exception { + final Set names = IndexPrivilege.values().keySet(); + final int all = Iterables.indexOf(names, "all"::equals); + final int manage = Iterables.indexOf(names, "manage"::equals); + final int monitor = Iterables.indexOf(names, "monitor"::equals); + final int read = Iterables.indexOf(names, "read"::equals); + final int write = Iterables.indexOf(names, "write"::equals); + final int index = Iterables.indexOf(names, "index"::equals); + final int create_doc = Iterables.indexOf(names, "create_doc"::equals); + final int delete = Iterables.indexOf(names, "delete"::equals); + + assertThat(read, lessThan(all)); + assertThat(manage, lessThan(all)); + assertThat(monitor, lessThan(manage)); + assertThat(write, lessThan(all)); + assertThat(index, lessThan(write)); + assertThat(create_doc, lessThan(index)); + assertThat(delete, lessThan(write)); + } + + public void testFindPrivilegesThatGrant() { + assertThat(findPrivilegesThatGrant(SearchAction.NAME), equalTo(List.of("read", "all"))); + assertThat(findPrivilegesThatGrant(IndexAction.NAME), equalTo(List.of("create_doc", "create", "index", "write", "all"))); + assertThat(findPrivilegesThatGrant(UpdateAction.NAME), equalTo(List.of("index", "write", "all"))); + assertThat(findPrivilegesThatGrant(DeleteAction.NAME), equalTo(List.of("delete", "write", "all"))); + assertThat(findPrivilegesThatGrant(IndicesStatsAction.NAME), equalTo(List.of("monitor", "manage", "all"))); + assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all"))); + assertThat(findPrivilegesThatGrant(ShrinkAction.NAME), equalTo(List.of("manage", "all"))); + } + +} diff --git a/x-pack/plugin/ilm/qa/with-security/src/test/java/org/elasticsearch/xpack/security/PermissionsIT.java b/x-pack/plugin/ilm/qa/with-security/src/test/java/org/elasticsearch/xpack/security/PermissionsIT.java index c4a42d868e8c5..a66ab86d21cae 100644 --- a/x-pack/plugin/ilm/qa/with-security/src/test/java/org/elasticsearch/xpack/security/PermissionsIT.java +++ b/x-pack/plugin/ilm/qa/with-security/src/test/java/org/elasticsearch/xpack/security/PermissionsIT.java @@ -143,7 +143,10 @@ public void testCanManageIndexWithNoPermissions() throws Exception { assertThat(indexExplain.get("failed_step"), equalTo("wait-for-shard-history-leases")); Map stepInfo = (Map) indexExplain.get("step_info"); assertThat(stepInfo.get("type"), equalTo("security_exception")); - assertThat(stepInfo.get("reason"), equalTo("action [indices:monitor/stats] is unauthorized for user [test_ilm]")); + assertThat(stepInfo.get("reason"), equalTo("action [indices:monitor/stats] is unauthorized" + + " for user [test_ilm]" + + " on indices [not-ilm]," + + " this action is granted by the privileges [monitor,manage,all]")); } }); } diff --git a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java index a3d0cff23d54f..49cdaa958e318 100644 --- a/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java +++ b/x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java @@ -805,7 +805,7 @@ public void testLookbackWithoutPermissions() throws Exception { new Request("GET", NotificationsIndex.NOTIFICATIONS_INDEX + "/_search?size=1000&q=job_id:" + jobId)); String notificationsResponseAsString = EntityUtils.toString(notificationsResponse.getEntity()); assertThat(notificationsResponseAsString, containsString("\"message\":\"Datafeed is encountering errors extracting data: " + - "action [indices:data/read/search] is unauthorized for user [ml_admin_plus_data]\"")); + "action [indices:data/read/search] is unauthorized for user [ml_admin_plus_data] on indices [network-data]")); } public void testLookbackWithPipelineBucketAgg() throws Exception { @@ -953,7 +953,8 @@ public void testLookbackWithoutPermissionsAndRollup() throws Exception { new Request("GET", NotificationsIndex.NOTIFICATIONS_INDEX + "/_search?size=1000&q=job_id:" + jobId)); String notificationsResponseAsString = EntityUtils.toString(notificationsResponse.getEntity()); assertThat(notificationsResponseAsString, containsString("\"message\":\"Datafeed is encountering errors extracting data: " + - "action [indices:data/read/xpack/rollup/search] is unauthorized for user [ml_admin_plus_data]\"")); + "action [indices:data/read/xpack/rollup/search] is unauthorized for user [ml_admin_plus_data] " + + "on indices [airline-data-aggs-rollup]")); } public void testLookbackWithSingleBucketAgg() throws Exception { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java index 62d3aa6fa3ca8..6a4ee16f68839 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java @@ -15,7 +15,6 @@ import org.elasticsearch.action.admin.indices.alias.Alias; import org.elasticsearch.action.admin.indices.alias.IndicesAliasesAction; import org.elasticsearch.action.admin.indices.create.CreateIndexRequest; -import org.elasticsearch.xpack.core.action.CreateDataStreamAction; import org.elasticsearch.action.bulk.BulkItemRequest; import org.elasticsearch.action.bulk.BulkShardRequest; import org.elasticsearch.action.bulk.TransportShardBulkAction; @@ -39,6 +38,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportActionProxy; import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.action.CreateDataStreamAction; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse; import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest; @@ -88,6 +88,7 @@ import java.util.function.Consumer; import static org.elasticsearch.action.support.ContextPreservingActionListener.wrapPreservingContext; +import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString; import static org.elasticsearch.xpack.core.security.SecurityField.setting; import static org.elasticsearch.xpack.core.security.support.Exceptions.authorizationError; import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; @@ -250,7 +251,7 @@ private void authorizeAction(final RequestInfo requestInfo, final String request listener.onResponse(null); }, listener::onFailure, requestInfo, requestId, authzInfo), threadContext); authzEngine.authorizeClusterAction(requestInfo, authzInfo, clusterAuthzListener); - } else if (IndexPrivilege.ACTION_MATCHER.test(action)) { + } else if (isIndexAction(action)) { final Metadata metadata = clusterService.state().metadata(); final AsyncSupplier> authorizedIndicesSupplier = new CachingAsyncSupplier<>(authzIndicesListener -> authzEngine.loadAuthorizedIndices(requestInfo, authzInfo, metadata.getIndicesLookup(), @@ -518,7 +519,8 @@ private void authorizeBulkItems(RequestInfo requestInfo, AuthorizationInfo authz if (indexAccessControl == null || indexAccessControl.isGranted() == false) { auditTrail.explicitIndexAccessEvent(requestId, AuditLevel.ACCESS_DENIED, authentication, itemAction, resolvedIndex, item.getClass().getSimpleName(), request.remoteAddress(), authzInfo); - item.abort(resolvedIndex, denialException(authentication, itemAction, null)); + item.abort(resolvedIndex, denialException(authentication, itemAction, + AuthorizationEngine.IndexAuthorizationResult.getFailureDescription(List.of(resolvedIndex)), null)); } else if (audit.get()) { auditTrail.explicitIndexAccessEvent(requestId, AuditLevel.ACCESS_GRANTED, authentication, itemAction, resolvedIndex, item.getClass().getSimpleName(), request.remoteAddress(), authzInfo); @@ -538,8 +540,8 @@ private void authorizeBulkItems(RequestInfo requestInfo, AuthorizationInfo authz groupedActionListener.onResponse(new Tuple<>(bulkItemAction, indexAuthorizationResult)), groupedActionListener::onFailure)); }); - }, listener::onFailure)); }, listener::onFailure)); + }, listener::onFailure)); } private static IllegalArgumentException illegalArgument(String message) { @@ -547,6 +549,10 @@ private static IllegalArgumentException illegalArgument(String message) { return new IllegalArgumentException(message); } + private static boolean isIndexAction(String action) { + return IndexPrivilege.ACTION_MATCHER.test(action); + } + private static String getAction(BulkItemRequest item) { final DocWriteRequest docWriteRequest = item.request(); switch (docWriteRequest.opType()) { @@ -575,6 +581,11 @@ private void putTransientIfNonExisting(String key, Object value) { } private ElasticsearchSecurityException denialException(Authentication authentication, String action, Exception cause) { + return denialException(authentication, action, null, cause); + } + + private ElasticsearchSecurityException denialException(Authentication authentication, String action, @Nullable String context, + Exception cause) { final User authUser = authentication.getUser().authenticatedUser(); // Special case for anonymous user if (isAnonymousEnabled && anonymousUser.equals(authUser)) { @@ -582,23 +593,33 @@ private ElasticsearchSecurityException denialException(Authentication authentica return authcFailureHandler.authenticationRequired(action, threadContext); } } + + String userText = "user [" + authUser.principal() + "]"; // check for run as if (authentication.getUser().isRunAs()) { - logger.debug("action [{}] is unauthorized for user [{}] run as [{}]", action, authUser.principal(), - authentication.getUser().principal()); - return authorizationError("action [{}] is unauthorized for user [{}] run as [{}]", cause, action, authUser.principal(), - authentication.getUser().principal()); + userText = userText + " run as [" + authentication.getUser().principal() + "]"; } // check for authentication by API key if (AuthenticationType.API_KEY == authentication.getAuthenticationType()) { final String apiKeyId = (String) authentication.getMetadata().get(ApiKeyService.API_KEY_ID_KEY); assert apiKeyId != null : "api key id must be present in the metadata"; - logger.debug("action [{}] is unauthorized for API key id [{}] of user [{}]", action, apiKeyId, authUser.principal()); - return authorizationError("action [{}] is unauthorized for API key id [{}] of user [{}]", cause, action, apiKeyId, - authUser.principal()); + userText = "API key id [" + apiKeyId + "] of " + userText; + } + + String message = "action [" + action + "] is unauthorized for " + userText; + if (context != null) { + message = message + " " + context; } - logger.debug("action [{}] is unauthorized for user [{}]", action, authUser.principal()); - return authorizationError("action [{}] is unauthorized for user [{}]", cause, action, authUser.principal()); + + if(isIndexAction(action)) { + final Collection privileges = IndexPrivilege.findPrivilegesThatGrant(action); + if (privileges != null && privileges.size() > 0) { + message = message + ", this action is granted by the privileges [" + collectionToCommaDelimitedString(privileges) + "]"; + } + } + + logger.debug(message); + return authorizationError(message, cause); } private class AuthorizationResultListener implements ActionListener { @@ -631,21 +652,21 @@ public void onResponse(T result) { failureConsumer.accept(e); } } else { - handleFailure(result.isAuditable(), null); + handleFailure(result.isAuditable(), result.getFailureContext(), null); } } @Override public void onFailure(Exception e) { - handleFailure(true, e); + handleFailure(true, null, e); } - private void handleFailure(boolean audit, @Nullable Exception e) { + private void handleFailure(boolean audit, @Nullable String context, @Nullable Exception e) { if (audit) { auditTrailService.get().accessDenied(requestId, requestInfo.getAuthentication(), requestInfo.getAction(), requestInfo.getRequest(), authzInfo); } - failureConsumer.accept(denialException(requestInfo.getAuthentication(), requestInfo.getAction(), e)); + failureConsumer.accept(denialException(requestInfo.getAuthentication(), requestInfo.getAction(), context, e)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeIntegTests.java similarity index 70% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeIntegTests.java index b81521f91888f..d45a6b5f74b9f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeIntegTests.java @@ -21,77 +21,77 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; -public class IndexPrivilegeTests extends AbstractPrivilegeTestCase { +public class IndexPrivilegeIntegTests extends AbstractPrivilegeTestCase { private String jsonDoc = "{ \"name\" : \"elasticsearch\", \"body\": \"foo bar\" }"; private static final String ROLES = - "all_cluster_role:\n" + - " cluster: [ all ]\n" + - "all_indices_role:\n" + - " indices:\n" + - " - names: '*'\n" + - " privileges: [ all ]\n" + - "all_a_role:\n" + - " indices:\n" + - " - names: 'a'\n" + - " privileges: [ all ]\n" + - "read_a_role:\n" + - " indices:\n" + - " - names: 'a'\n" + - " privileges: [ read ]\n" + - "read_b_role:\n" + - " indices:\n" + - " - names: 'b'\n" + - " privileges: [ read ]\n" + - "write_a_role:\n" + - " indices:\n" + - " - names: 'a'\n" + - " privileges: [ write ]\n" + - "read_ab_role:\n" + - " indices:\n" + - " - names: [ 'a', 'b' ]\n" + - " privileges: [ read ]\n" + - "all_regex_ab_role:\n" + - " indices:\n" + - " - names: '/a|b/'\n" + - " privileges: [ all ]\n" + - "manage_starts_with_a_role:\n" + - " indices:\n" + - " - names: 'a*'\n" + - " privileges: [ manage ]\n" + - "read_write_all_role:\n" + - " indices:\n" + - " - names: '*'\n" + - " privileges: [ read, write ]\n" + - "create_c_role:\n" + - " indices:\n" + - " - names: 'c'\n" + - " privileges: [ create_index ]\n" + - "monitor_b_role:\n" + - " indices:\n" + - " - names: 'b'\n" + - " privileges: [ monitor ]\n" + - "maintenance_a_role:\n" + - " indices:\n" + - " - names: 'a'\n" + - " privileges: [ maintenance ]\n" + - "read_write_a_role:\n" + - " indices:\n" + - " - names: 'a'\n" + - " privileges: [ read, write ]\n" + - "delete_b_role:\n" + - " indices:\n" + - " - names: 'b'\n" + - " privileges: [ delete ]\n" + - "index_a_role:\n" + - " indices:\n" + - " - names: 'a'\n" + - " privileges: [ index ]\n" + - "\n"; + "all_cluster_role:\n" + + " cluster: [ all ]\n" + + "all_indices_role:\n" + + " indices:\n" + + " - names: '*'\n" + + " privileges: [ all ]\n" + + "all_a_role:\n" + + " indices:\n" + + " - names: 'a'\n" + + " privileges: [ all ]\n" + + "read_a_role:\n" + + " indices:\n" + + " - names: 'a'\n" + + " privileges: [ read ]\n" + + "read_b_role:\n" + + " indices:\n" + + " - names: 'b'\n" + + " privileges: [ read ]\n" + + "write_a_role:\n" + + " indices:\n" + + " - names: 'a'\n" + + " privileges: [ write ]\n" + + "read_ab_role:\n" + + " indices:\n" + + " - names: [ 'a', 'b' ]\n" + + " privileges: [ read ]\n" + + "all_regex_ab_role:\n" + + " indices:\n" + + " - names: '/a|b/'\n" + + " privileges: [ all ]\n" + + "manage_starts_with_a_role:\n" + + " indices:\n" + + " - names: 'a*'\n" + + " privileges: [ manage ]\n" + + "read_write_all_role:\n" + + " indices:\n" + + " - names: '*'\n" + + " privileges: [ read, write ]\n" + + "create_c_role:\n" + + " indices:\n" + + " - names: 'c'\n" + + " privileges: [ create_index ]\n" + + "monitor_b_role:\n" + + " indices:\n" + + " - names: 'b'\n" + + " privileges: [ monitor ]\n" + + "maintenance_a_role:\n" + + " indices:\n" + + " - names: 'a'\n" + + " privileges: [ maintenance ]\n" + + "read_write_a_role:\n" + + " indices:\n" + + " - names: 'a'\n" + + " privileges: [ read, write ]\n" + + "delete_b_role:\n" + + " indices:\n" + + " - names: 'b'\n" + + " privileges: [ delete ]\n" + + "index_a_role:\n" + + " indices:\n" + + " - names: 'a'\n" + + " privileges: [ index ]\n" + + "\n"; private static final String USERS_ROLES = - "all_indices_role:admin,u8\n" + + "all_indices_role:admin,u8\n" + "all_cluster_role:admin\n" + "all_a_role:u1,u2,u6\n" + "read_a_role:u1,u5,u14\n" + @@ -138,7 +138,7 @@ protected String configUsers() { "u12:" + usersPasswdHashed + "\n" + "u13:" + usersPasswdHashed + "\n" + "u14:" + usersPasswdHashed + "\n" + - "u15:" + usersPasswdHashed + "\n" ; + "u15:" + usersPasswdHashed + "\n"; } @Override @@ -149,7 +149,7 @@ protected String configUsersRoles() { @Before public void insertBaseDocumentsAsAdmin() throws Exception { // indices: a,b,c,abc - for (String index : new String[] {"a", "b", "c", "abc"}) { + for (String index : new String[]{"a", "b", "c", "abc"}) { Request request = new Request("PUT", "/" + index + "/_doc/1"); request.setJsonEntity(jsonDoc); request.addParameter("refresh", "true"); @@ -167,12 +167,12 @@ public void testUserU1() throws Exception { assertUserIsDenied("u1", "all", "b"); assertUserIsDenied("u1", "all", "c"); assertAccessIsAllowed("u1", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u1", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsAllowed("u1", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u1", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU2() throws Exception { @@ -184,12 +184,12 @@ public void testUserU2() throws Exception { assertUserIsDenied("u2", "create_index", "b"); assertUserIsDenied("u2", "all", "c"); assertAccessIsAllowed("u2", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u2", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsAllowed("u2", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u2", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU3() throws Exception { @@ -198,12 +198,12 @@ public void testUserU3() throws Exception { assertUserIsAllowed("u3", "all", "b"); assertUserIsDenied("u3", "all", "c"); assertAccessIsAllowed("u3", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u3", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsAllowed("u3", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u3", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU4() throws Exception { @@ -222,12 +222,12 @@ public void testUserU4() throws Exception { assertUserIsAllowed("u4", "manage", "an_index"); assertAccessIsAllowed("u4", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u4", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsDenied("u4", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u4", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU5() throws Exception { @@ -241,12 +241,12 @@ public void testUserU5() throws Exception { assertUserIsDenied("u5", "write", "b"); assertAccessIsAllowed("u5", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u5", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsDenied("u5", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u5", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU6() throws Exception { @@ -257,12 +257,12 @@ public void testUserU6() throws Exception { assertUserIsDenied("u6", "write", "b"); assertUserIsDenied("u6", "all", "c"); assertAccessIsAllowed("u6", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u6", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsAllowed("u6", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u6", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU7() throws Exception { @@ -271,12 +271,12 @@ public void testUserU7() throws Exception { assertUserIsDenied("u7", "all", "b"); assertUserIsDenied("u7", "all", "c"); assertAccessIsDenied("u7", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsDenied("u7", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsDenied("u7", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsDenied("u7", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU8() throws Exception { @@ -285,12 +285,12 @@ public void testUserU8() throws Exception { assertUserIsAllowed("u8", "all", "b"); assertUserIsAllowed("u8", "all", "c"); assertAccessIsAllowed("u8", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u8", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsAllowed("u8", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u8", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU9() throws Exception { @@ -302,12 +302,12 @@ public void testUserU9() throws Exception { assertUserIsDenied("u9", "write", "b"); assertUserIsDenied("u9", "all", "c"); assertAccessIsAllowed("u9", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u9", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsAllowed("u9", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u9", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU11() throws Exception { @@ -327,12 +327,12 @@ public void testUserU11() throws Exception { assertUserIsDenied("u11", "maintenance", "c"); assertAccessIsDenied("u11", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsDenied("u11", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertBodyHasAccessIsDenied("u11", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsDenied("u11", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU12() throws Exception { @@ -344,12 +344,12 @@ public void testUserU12() throws Exception { assertUserIsDenied("u12", "manage", "c"); assertUserIsAllowed("u12", "data_access", "c"); assertAccessIsAllowed("u12", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u12", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsAllowed("u12", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u12", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU13() throws Exception { @@ -366,12 +366,12 @@ public void testUserU13() throws Exception { assertUserIsDenied("u13", "all", "c"); assertAccessIsAllowed("u13", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u13", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsAllowed("u13", "PUT", "/a/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertBodyHasAccessIsDenied("u13", "PUT", "/b/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u13", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU14() throws Exception { @@ -388,12 +388,12 @@ public void testUserU14() throws Exception { assertUserIsDenied("u14", "all", "c"); assertAccessIsAllowed("u14", - "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); + "GET", "/" + randomIndex() + "/_msearch", "{}\n{ \"query\" : { \"match_all\" : {} } }\n"); assertAccessIsAllowed("u14", "POST", "/" + randomIndex() + "/_mget", "{ \"ids\" : [ \"1\", \"2\" ] } "); assertAccessIsDenied("u14", "PUT", - "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); + "/" + randomIndex() + "/_bulk", "{ \"index\" : { \"_id\" : \"123\" } }\n{ \"foo\" : \"bar\" }\n"); assertAccessIsAllowed("u14", - "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); + "GET", "/" + randomIndex() + "/_mtermvectors", "{ \"docs\" : [ { \"_id\": \"1\" }, { \"_id\": \"2\" } ] }"); } public void testUserU15() throws Exception { @@ -406,18 +406,18 @@ public void testThatUnknownUserIsRejectedProperly() throws Exception { Request request = new Request("GET", "/"); RequestOptions.Builder options = request.getOptions().toBuilder(); options.addHeader("Authorization", - UsernamePasswordToken.basicAuthHeaderValue("idonotexist", new SecureString("passwd".toCharArray()))); + UsernamePasswordToken.basicAuthHeaderValue("idonotexist", new SecureString("passwd".toCharArray()))); request.setOptions(options); getRestClient().performRequest(request); fail("request should have failed"); - } catch(ResponseException e) { + } catch (ResponseException e) { assertThat(e.getResponse().getStatusLine().getStatusCode(), is(401)); } } private void assertUserExecutes(String user, String action, String index, boolean userIsAllowed) throws Exception { switch (action) { - case "all" : + case "all": if (userIsAllowed) { assertUserIsAllowed(user, "crud", index); assertUserIsAllowed(user, "manage", index); @@ -427,7 +427,7 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "create_index" : + case "create_index": if (userIsAllowed) { assertAccessIsAllowed(user, "PUT", "/" + index); } else { @@ -435,7 +435,7 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "maintenance" : + case "maintenance": if (userIsAllowed) { assertAccessIsAllowed(user, "POST", "/" + index + "/_refresh"); assertAccessIsAllowed(user, "POST", "/" + index + "/_flush"); @@ -449,7 +449,7 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "manage" : + case "manage": if (userIsAllowed) { assertAccessIsAllowed(user, "DELETE", "/" + index); assertUserIsAllowed(user, "create_index", index); @@ -464,7 +464,7 @@ private void assertUserExecutes(String user, String action, String index, boolea assertAccessIsAllowed(user, "POST", "/" + index + "/_open"); assertAccessIsAllowed(user, "POST", "/" + index + "/_cache/clear"); // indexing a document to have the mapping available, and wait for green state to make sure index is created - assertAccessIsAllowed("admin", "PUT", "/" + index + "/_doc/1", jsonDoc); + assertAccessIsAllowed("admin", "PUT", "/" + index + "/_doc/1", jsonDoc); assertNoTimeout(client().admin().cluster().prepareHealth(index).setWaitForGreenStatus().get()); assertAccessIsAllowed(user, "GET", "/" + index + "/_mapping/field/name"); assertAccessIsAllowed(user, "GET", "/" + index + "/_settings"); @@ -484,7 +484,7 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "monitor" : + case "monitor": if (userIsAllowed) { assertAccessIsAllowed(user, "GET", "/" + index + "/_stats"); assertAccessIsAllowed(user, "GET", "/" + index + "/_segments"); @@ -496,7 +496,7 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "data_access" : + case "data_access": if (userIsAllowed) { assertUserIsAllowed(user, "crud", index); } else { @@ -504,7 +504,7 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "crud" : + case "crud": if (userIsAllowed) { assertUserIsAllowed(user, "read", index); assertAccessIsAllowed(user, "PUT", "/" + index + "/_doc/321", "{ \"foo\" : \"bar\" }"); @@ -515,13 +515,13 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "read" : + case "read": if (userIsAllowed) { // admin refresh before executing assertAccessIsAllowed("admin", "GET", "/" + index + "/_refresh"); assertAccessIsAllowed(user, "GET", "/" + index + "/_count"); assertAccessIsAllowed("admin", "GET", "/" + index + "/_search"); - assertAccessIsAllowed("admin", "GET", "/" + index + "/_doc/1"); + assertAccessIsAllowed("admin", "GET", "/" + index + "/_doc/1"); assertAccessIsAllowed(user, "GET", "/" + index + "/_explain/1", "{ \"query\" : { \"match_all\" : {} } }"); assertAccessIsAllowed(user, "GET", "/" + index + "/_termvectors/1"); assertUserIsAllowed(user, "search", index); @@ -534,7 +534,7 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "search" : + case "search": if (userIsAllowed) { assertAccessIsAllowed(user, "GET", "/" + index + "/_search"); } else { @@ -542,31 +542,31 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "get" : + case "get": if (userIsAllowed) { - assertAccessIsAllowed(user, "GET", "/" + index + "/_doc/1"); + assertAccessIsAllowed(user, "GET", "/" + index + "/_doc/1"); } else { - assertAccessIsDenied(user, "GET", "/" + index + "/_doc/1"); + assertAccessIsDenied(user, "GET", "/" + index + "/_doc/1"); } break; - case "index" : + case "index": if (userIsAllowed) { assertAccessIsAllowed(user, "PUT", "/" + index + "/_doc/321", "{ \"foo\" : \"bar\" }"); // test auto mapping update is allowed but deprecated Response response = assertAccessIsAllowed(user, "PUT", "/" + index + "/_doc/4321", "{ \"" + - UUIDs.randomBase64UUID() + "\" : \"foo\" }"); + UUIDs.randomBase64UUID() + "\" : \"foo\" }"); String warningHeader = response.getHeader("Warning"); assertThat(warningHeader, containsString("the index privilege [index] allowed the update mapping action " + - "[indices:admin/mapping/auto_put] on index [" + index + "], this privilege will not permit mapping updates in" + - " the next major release - users who require access to update mappings must be granted explicit privileges")); + "[indices:admin/mapping/auto_put] on index [" + index + "], this privilege will not permit mapping updates in" + + " the next major release - users who require access to update mappings must be granted explicit privileges")); assertAccessIsAllowed(user, "POST", "/" + index + "/_update/321", "{ \"doc\" : { \"foo\" : \"baz\" } }"); response = assertAccessIsAllowed(user, "POST", "/" + index + "/_update/321", - "{ \"doc\" : { \"" + UUIDs.randomBase64UUID() + "\" : \"baz\" } }"); + "{ \"doc\" : { \"" + UUIDs.randomBase64UUID() + "\" : \"baz\" } }"); warningHeader = response.getHeader("Warning"); assertThat(warningHeader, containsString("the index privilege [index] allowed the update mapping action " + - "[indices:admin/mapping/auto_put] on index [" + index + "], this privilege will not permit mapping updates in" + - " the next major release - users who require access to update mappings must be granted explicit privileges")); + "[indices:admin/mapping/auto_put] on index [" + index + "], this privilege will not permit mapping updates in" + + " the next major release - users who require access to update mappings must be granted explicit privileges")); } else { assertAccessIsDenied(user, "PUT", "/" + index + "/_doc/321", "{ \"foo\" : \"bar\" }"); assertAccessIsDenied(user, "PUT", "/" + index + "/_doc/321", "{ \"foo\" : \"bar\" }"); @@ -574,34 +574,34 @@ private void assertUserExecutes(String user, String action, String index, boolea } break; - case "delete" : + case "delete": String jsonDoc = "{ \"name\" : \"docToDelete\"}"; - assertAccessIsAllowed("admin", "PUT", "/" + index + "/_doc/docToDelete", jsonDoc); - assertAccessIsAllowed("admin", "PUT", "/" + index + "/_doc/docToDelete2", jsonDoc); + assertAccessIsAllowed("admin", "PUT", "/" + index + "/_doc/docToDelete", jsonDoc); + assertAccessIsAllowed("admin", "PUT", "/" + index + "/_doc/docToDelete2", jsonDoc); if (userIsAllowed) { - assertAccessIsAllowed(user, "DELETE", "/" + index + "/_doc/docToDelete"); + assertAccessIsAllowed(user, "DELETE", "/" + index + "/_doc/docToDelete"); } else { - assertAccessIsDenied(user, "DELETE", "/" + index + "/_doc/docToDelete"); + assertAccessIsDenied(user, "DELETE", "/" + index + "/_doc/docToDelete"); } break; - case "write" : + case "write": if (userIsAllowed) { assertUserIsAllowed(user, "delete", index); assertAccessIsAllowed(user, "PUT", "/" + index + "/_doc/321", "{ \"foo\" : \"bar\" }"); // test auto mapping update is allowed but deprecated Response response = assertAccessIsAllowed(user, "PUT", "/" + index + "/_doc/4321", "{ \"" + - UUIDs.randomBase64UUID() + "\" : \"foo\" }"); + UUIDs.randomBase64UUID() + "\" : \"foo\" }"); String warningHeader = response.getHeader("Warning"); assertThat(warningHeader, containsString("the index privilege [write] allowed the update mapping action [" + - "indices:admin/mapping/auto_put] on index [" + index + "]")); + "indices:admin/mapping/auto_put] on index [" + index + "]")); assertAccessIsAllowed(user, "POST", "/" + index + "/_update/321", "{ \"doc\" : { \"foo\" : \"baz\" } }"); response = assertAccessIsAllowed(user, "POST", "/" + index + "/_update/321", - "{ \"doc\" : { \"" + UUIDs.randomBase64UUID() + "\" : \"baz\" } }"); + "{ \"doc\" : { \"" + UUIDs.randomBase64UUID() + "\" : \"baz\" } }"); warningHeader = response.getHeader("Warning"); assertThat(warningHeader, containsString("the index privilege [write] allowed the update mapping action [" + - "indices:admin/mapping/auto_put] on index [" + index + "]")); + "indices:admin/mapping/auto_put] on index [" + index + "]")); } else { assertUserIsDenied(user, "index", index); assertUserIsDenied(user, "delete", index); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java index 1ddd6faf70b65..ac69ec5c50d02 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java @@ -42,6 +42,9 @@ import org.elasticsearch.action.bulk.BulkItemRequest; import org.elasticsearch.action.bulk.BulkRequest; import org.elasticsearch.action.bulk.BulkShardRequest; +import org.elasticsearch.action.bulk.BulkShardResponse; +import org.elasticsearch.action.bulk.MappingUpdatePerformer; +import org.elasticsearch.action.bulk.TransportShardBulkAction; import org.elasticsearch.action.delete.DeleteAction; import org.elasticsearch.action.delete.DeleteRequest; import org.elasticsearch.action.get.GetAction; @@ -62,11 +65,13 @@ import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.action.support.replication.TransportReplicationAction; import org.elasticsearch.action.termvectors.MultiTermVectorsAction; import org.elasticsearch.action.termvectors.MultiTermVectorsRequest; import org.elasticsearch.action.termvectors.TermVectorsAction; import org.elasticsearch.action.termvectors.TermVectorsRequest; import org.elasticsearch.action.update.UpdateAction; +import org.elasticsearch.action.update.UpdateHelper; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.cluster.ClusterState; import org.elasticsearch.cluster.metadata.AliasMetadata; @@ -85,9 +90,12 @@ import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.index.IndexNotFoundException; +import org.elasticsearch.index.bulk.stats.BulkOperationListener; +import org.elasticsearch.index.shard.IndexShard; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.license.XPackLicenseState.Feature; +import org.elasticsearch.script.ScriptService; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportActionProxy; @@ -153,21 +161,25 @@ import java.util.UUID; import java.util.concurrent.CountDownLatch; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Predicate; import static java.util.Arrays.asList; import static org.elasticsearch.test.SecurityTestsUtils.assertAuthenticationException; import static org.elasticsearch.test.SecurityTestsUtils.assertThrowsAuthorizationException; import static org.elasticsearch.test.SecurityTestsUtils.assertThrowsAuthorizationExceptionRunAs; +import static org.elasticsearch.test.TestMatchers.throwableWithMessage; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.INTERNAL_SECURITY_MAIN_INDEX_7; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; import static org.elasticsearch.xpack.security.audit.logfile.LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME; import static org.hamcrest.Matchers.arrayContainingInAnyOrder; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.startsWith; import static org.mockito.Matchers.any; @@ -448,6 +460,7 @@ public void testUserWithNoRolesCannotSql() throws IOException { authzInfoRoles(Role.EMPTY.names())); verifyNoMoreInteractions(auditTrail); } + /** * Verifies that the behaviour tested in {@link #testUserWithNoRolesCanPerformRemoteSearch} * does not work for requests that are not remote-index-capable. @@ -678,6 +691,75 @@ public void testCreateIndexWithAlias() throws IOException { verify(state, times(1)).metadata(); } + public void testDenialErrorMessagesForSearchAction() throws IOException { + RoleDescriptor role = new RoleDescriptor("some_indices_" + randomAlphaOfLengthBetween(3, 6), null, new IndicesPrivileges[]{ + IndicesPrivileges.builder().indices("all*").privileges("all").build(), + IndicesPrivileges.builder().indices("read*").privileges("read").build(), + IndicesPrivileges.builder().indices("write*").privileges("write").build() + }, null); + User user = new User(randomAlphaOfLengthBetween(6, 8), role.getName()); + final Authentication authentication = createAuthentication(user); + roleMap.put(role.getName(), role); + + AuditUtil.getOrGenerateRequestId(threadContext); + + TransportRequest request = new SearchRequest("all-1", "read-2", "write-3", "other-4"); + + ElasticsearchSecurityException securityException = expectThrows(ElasticsearchSecurityException.class, + () -> authorize(authentication, SearchAction.NAME, request)); + assertThat(securityException, throwableWithMessage( + containsString("[" + SearchAction.NAME + "] is unauthorized for user [" + user.principal() + "] on indices ["))); + assertThat(securityException, throwableWithMessage(containsString("write-3"))); + assertThat(securityException, throwableWithMessage(containsString("other-4"))); + assertThat(securityException, throwableWithMessage(not(containsString("all-1")))); + assertThat(securityException, throwableWithMessage(not(containsString("read-2")))); + assertThat(securityException, throwableWithMessage(containsString(", this action is granted by the privileges [read,all]"))); + } + + public void testDenialErrorMessagesForBulkIngest() throws Exception { + final String index = randomAlphaOfLengthBetween(5, 12); + RoleDescriptor role = new RoleDescriptor("some_indices_" + randomAlphaOfLengthBetween(3, 6), null, new IndicesPrivileges[]{ + IndicesPrivileges.builder().indices(index).privileges(BulkAction.NAME).build() + }, null); + User user = new User(randomAlphaOfLengthBetween(6, 8), role.getName()); + final Authentication authentication = createAuthentication(user); + roleMap.put(role.getName(), role); + + AuditUtil.getOrGenerateRequestId(threadContext); + + final BulkShardRequest request = new BulkShardRequest( + new ShardId(index, randomAlphaOfLength(24), 1), + WriteRequest.RefreshPolicy.NONE, + new BulkItemRequest[]{ + new BulkItemRequest(0, + new IndexRequest(index).id("doc-1").opType(DocWriteRequest.OpType.CREATE).source(Map.of("field", "value"))), + new BulkItemRequest(1, + new IndexRequest(index).id("doc-2").opType(DocWriteRequest.OpType.INDEX).source(Map.of("field", "value"))), + new BulkItemRequest(2, new DeleteRequest(index, "doc-3")) + }); + + authorize(authentication, TransportShardBulkAction.ACTION_NAME, request); + + MappingUpdatePerformer mappingUpdater = (m, s, l) -> l.onResponse(null); + Consumer> waitForMappingUpdate = l -> l.onResponse(null); + PlainActionFuture> future = new PlainActionFuture<>(); + IndexShard indexShard = mock(IndexShard.class); + when(indexShard.getBulkOperationListener()).thenReturn(new BulkOperationListener() { + }); + TransportShardBulkAction.performOnPrimary(request, indexShard, new UpdateHelper(mock(ScriptService.class)), + System::currentTimeMillis, mappingUpdater, waitForMappingUpdate, future, threadPool); + + TransportReplicationAction.PrimaryResult result = future.get(); + BulkShardResponse response = result.finalResponseIfSuccessful; + assertThat(response, notNullValue()); + assertThat(response.getResponses(), arrayWithSize(3)); + assertThat(response.getResponses()[0].getFailureMessage(), containsString("unauthorized for user [" + user.principal() + "]")); + assertThat(response.getResponses()[0].getFailureMessage(), containsString("on indices [" + index + "]")); + assertThat(response.getResponses()[0].getFailureMessage(), containsString("[create_doc,create,index,write,all]") ); + assertThat(response.getResponses()[1].getFailureMessage(), containsString("[create,index,write,all]") ); + assertThat(response.getResponses()[2].getFailureMessage(), containsString("[delete,write,all]") ); + } + public void testDenialForAnonymousUser() throws IOException { TransportRequest request = new GetIndexRequest().indices("b"); ClusterState state = mockEmptyMetadata();