1
0
Эх сурвалжийг харах

Add more context to index access denied errors (#60357)

Access denied messages for indices were overly brief and missed two
pieces of useful information:

1. The names of the indices for which access was denied
2. The privileges that could be used to grant that access

This change improves the access denied messages for index based
actions by adding the index and privilege names.
Privilege names are listed in order from least-privilege to
most-privileged so that the first recommended path to resolution is
also the lowest privilege change.

Relates: #42166
Tim Vernum 5 жил өмнө
parent
commit
fc19689feb

+ 12 - 0
server/src/main/java/org/elasticsearch/common/util/iterable/Iterables.java

@@ -24,6 +24,7 @@ import java.util.Arrays;
 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 class Iterables {
         }
     }
 
+    public static <T> int indexOf(Iterable<T> iterable, Predicate<T> 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();
     }

+ 16 - 0
server/src/test/java/org/elasticsearch/common/util/iterable/IterablesTests.java

@@ -26,7 +26,10 @@ import java.util.Arrays;
 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 class IterablesTests extends ESTestCase {
         assertEquals(1, count);
     }
 
+    public void testIndexOf() {
+        final List<String> 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<String> iterable) {
         try {
             Iterables.get(iterable, -1);

+ 23 - 0
x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/AuthorizationEngine.java

@@ -8,6 +8,8 @@ package org.elasticsearch.xpack.core.security.authz;
 
 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 interface AuthorizationEngine {
             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 interface AuthorizationEngine {
             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;
         }

+ 9 - 0
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.IndicesAndAliasesResolverFiel
 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 class IndicesAccessControl {
         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.
      */

+ 21 - 7
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.alias.get.GetAliasesAction;
 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.ilm.action.ExplainLifecycleAction;
 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.Map;
 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<String, IndexPrivilege> VALUES = Map.ofEntries(
+    private static final Map<String, IndexPrivilege> 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<String> ACTION_MATCHER = ALL.predicate();
     public static final Predicate<String> CREATE_INDEX_MATCHER = CREATE_INDEX.predicate();
@@ -152,7 +154,7 @@ public final class IndexPrivilege extends Privilege {
             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 final class IndexPrivilege extends Privilege {
         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<String> findPrivilegesThatGrant(String action) {
+        return VALUES.entrySet().stream()
+            .filter(e -> e.getValue().predicate.test(action))
+            .map(e -> e.getKey())
+            .collect(Collectors.toUnmodifiableList());
+    }
 }

+ 23 - 0
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 class Privilege {
     public Automaton getAutomaton() {
         return automaton;
     }
+
+    /**
+     * Sorts the map of privileges from least-privilege to most-privilege
+     */
+    static <T extends Privilege> SortedMap<String, T> sortByAccessLevel(Map<String, T> 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<String, Long> 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<String> compare = Comparator.<String>comparingLong(key -> subsetCount.getOrDefault(key, 0L)).reversed()
+            .thenComparing(Comparator.naturalOrder());
+        final TreeMap<String, T> tree = new TreeMap<>(compare);
+        tree.putAll(privileges);
+        return Collections.unmodifiableSortedMap(tree);
+    }
 }

+ 62 - 0
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<String> 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")));
+    }
+
+}

+ 4 - 1
x-pack/plugin/ilm/qa/with-security/src/test/java/org/elasticsearch/xpack/security/PermissionsIT.java

@@ -143,7 +143,10 @@ public class PermissionsIT extends ESRestTestCase {
                 assertThat(indexExplain.get("failed_step"), equalTo("wait-for-shard-history-leases"));
                 Map<String, String> stepInfo = (Map<String, String>) 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]"));
             }
         });
     }

+ 3 - 2
x-pack/plugin/ml/qa/native-multi-node-tests/src/test/java/org/elasticsearch/xpack/ml/integration/DatafeedJobsRestIT.java

@@ -805,7 +805,7 @@ public class DatafeedJobsRestIT extends ESRestTestCase {
                 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 class DatafeedJobsRestIT extends ESRestTestCase {
             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 {

+ 38 - 17
x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/AuthorizationService.java

@@ -15,7 +15,6 @@ import org.elasticsearch.action.StepListener;
 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.license.XPackLicenseState.Feature;
 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.concurrent.atomic.AtomicBoolean;
 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 @@ public class AuthorizationService {
                         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<List<String>> authorizedIndicesSupplier = new CachingAsyncSupplier<>(authzIndicesListener ->
                 authzEngine.loadAuthorizedIndices(requestInfo, authzInfo, metadata.getIndicesLookup(),
@@ -518,7 +519,8 @@ public class AuthorizationService {
                             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 @@ public class AuthorizationService {
                                 groupedActionListener.onResponse(new Tuple<>(bulkItemAction, indexAuthorizationResult)),
                             groupedActionListener::onFailure));
                 });
-                }, listener::onFailure));
             }, listener::onFailure));
+        }, listener::onFailure));
     }
 
     private static IllegalArgumentException illegalArgument(String message) {
@@ -547,6 +549,10 @@ public class AuthorizationService {
         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 @@ public class AuthorizationService {
     }
 
     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 @@ public class AuthorizationService {
                 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<String> 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<T extends AuthorizationResult> implements ActionListener<T> {
@@ -631,21 +652,21 @@ public class AuthorizationService {
                     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));
         }
     }
 

+ 138 - 138
x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeTests.java → x-pack/plugin/security/src/test/java/org/elasticsearch/integration/IndexPrivilegeIntegTests.java

@@ -21,77 +21,77 @@ import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoTi
 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 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
             "u12:" + usersPasswdHashed + "\n" +
             "u13:" + usersPasswdHashed + "\n" +
             "u14:" + usersPasswdHashed + "\n" +
-            "u15:" + usersPasswdHashed + "\n" ;
+            "u15:" + usersPasswdHashed + "\n";
     }
 
     @Override
@@ -149,7 +149,7 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
     @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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
         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 class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
             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 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 break;
 
-            case "create_index" :
+            case "create_index":
                 if (userIsAllowed) {
                     assertAccessIsAllowed(user, "PUT", "/" + index);
                 } else {
@@ -435,7 +435,7 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 break;
 
-            case "maintenance" :
+            case "maintenance":
                 if (userIsAllowed) {
                     assertAccessIsAllowed(user, "POST", "/" + index + "/_refresh");
                     assertAccessIsAllowed(user, "POST", "/" + index + "/_flush");
@@ -449,7 +449,7 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 break;
 
-            case "manage" :
+            case "manage":
                 if (userIsAllowed) {
                     assertAccessIsAllowed(user, "DELETE", "/" + index);
                     assertUserIsAllowed(user, "create_index", index);
@@ -464,7 +464,7 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                     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 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 break;
 
-            case "monitor" :
+            case "monitor":
                 if (userIsAllowed) {
                     assertAccessIsAllowed(user, "GET", "/" + index + "/_stats");
                     assertAccessIsAllowed(user, "GET", "/" + index + "/_segments");
@@ -496,7 +496,7 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 break;
 
-            case "data_access" :
+            case "data_access":
                 if (userIsAllowed) {
                     assertUserIsAllowed(user, "crud", index);
                 } else {
@@ -504,7 +504,7 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 break;
 
-            case "crud" :
+            case "crud":
                 if (userIsAllowed) {
                     assertUserIsAllowed(user, "read", index);
                     assertAccessIsAllowed(user, "PUT", "/" + index + "/_doc/321", "{ \"foo\" : \"bar\" }");
@@ -515,13 +515,13 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 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 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 break;
 
-            case "search" :
+            case "search":
                 if (userIsAllowed) {
                     assertAccessIsAllowed(user, "GET", "/" + index + "/_search");
                 } else {
@@ -542,31 +542,31 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 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 @@ public class IndexPrivilegeTests extends AbstractPrivilegeTestCase {
                 }
                 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);

+ 82 - 0
x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/AuthorizationServiceTests.java

@@ -42,6 +42,9 @@ import org.elasticsearch.action.bulk.BulkAction;
 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.search.SearchTransportService;
 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;
 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.Set;
 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 class AuthorizationServiceTests extends ESTestCase {
             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 class AuthorizationServiceTests extends ESTestCase {
         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<ActionListener<Void>> waitForMappingUpdate = l -> l.onResponse(null);
+        PlainActionFuture<TransportReplicationAction.PrimaryResult<BulkShardRequest, BulkShardResponse>> 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<BulkShardRequest, BulkShardResponse> 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();