Browse Source

HLRC: Add "_has_privileges" API to Security Client (#35479)

This adds the "hasPrivileges()" method to SecurityClient, including
request, response & async variant of the method.

Also includes API documentation.
Tim Vernum 7 years ago
parent
commit
87a8b99724

+ 30 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

@@ -42,6 +42,8 @@ import org.elasticsearch.client.security.GetRoleMappingsRequest;
 import org.elasticsearch.client.security.GetRoleMappingsResponse;
 import org.elasticsearch.client.security.GetSslCertificatesRequest;
 import org.elasticsearch.client.security.GetSslCertificatesResponse;
+import org.elasticsearch.client.security.HasPrivilegesRequest;
+import org.elasticsearch.client.security.HasPrivilegesResponse;
 import org.elasticsearch.client.security.InvalidateTokenRequest;
 import org.elasticsearch.client.security.InvalidateTokenResponse;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
@@ -244,6 +246,34 @@ public final class SecurityClient {
                 AuthenticateResponse::fromXContent, listener, emptySet());
     }
 
+    /**
+     * Determine whether the current user has a specified list of privileges
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-has-privileges.html">
+     * the docs</a> for more.
+     *
+     * @param request the request with the privileges to check
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @return the response from the has privileges call
+     */
+    public HasPrivilegesResponse hasPrivileges(HasPrivilegesRequest request, RequestOptions options) throws IOException {
+        return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::hasPrivileges, options,
+            HasPrivilegesResponse::fromXContent, emptySet());
+    }
+
+    /**
+     * Asynchronously determine whether the current user has a specified list of privileges
+     * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-has-privileges.html">
+     * the docs</a> for more.
+     *
+     * @param request the request with the privileges to check
+     * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
+     * @param listener the listener to be notified upon request completion
+     */
+    public void hasPrivilegesAsync(HasPrivilegesRequest request, RequestOptions options, ActionListener<HasPrivilegesResponse> listener) {
+         restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::hasPrivileges, options,
+            HasPrivilegesResponse::fromXContent, listener, emptySet());
+    }
+
     /**
      * Clears the cache in one or more realms.
      * See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-clear-cache.html">

+ 7 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java

@@ -30,6 +30,7 @@ import org.elasticsearch.client.security.CreateTokenRequest;
 import org.elasticsearch.client.security.DeletePrivilegesRequest;
 import org.elasticsearch.client.security.DeleteRoleMappingRequest;
 import org.elasticsearch.client.security.DeleteRoleRequest;
+import org.elasticsearch.client.security.HasPrivilegesRequest;
 import org.elasticsearch.client.security.DisableUserRequest;
 import org.elasticsearch.client.security.EnableUserRequest;
 import org.elasticsearch.client.security.GetRoleMappingsRequest;
@@ -114,6 +115,12 @@ final class SecurityRequestConverters {
         return request;
     }
 
+    static Request hasPrivileges(HasPrivilegesRequest hasPrivilegesRequest) throws IOException {
+        Request request = new Request(HttpGet.METHOD_NAME, "/_xpack/security/user/_has_privileges");
+        request.setEntity(createEntity(hasPrivilegesRequest, REQUEST_BODY_CONTENT_TYPE));
+        return request;
+    }
+
     static Request clearRealmCache(ClearRealmCacheRequest clearRealmCacheRequest) {
         RequestConverters.EndpointBuilder builder = new RequestConverters.EndpointBuilder()
             .addPathPartAsIs("_xpack/security/realm");

+ 96 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/HasPrivilegesRequest.java

@@ -0,0 +1,96 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.Validatable;
+import org.elasticsearch.client.security.user.privileges.ApplicationResourcePrivileges;
+import org.elasticsearch.client.security.user.privileges.IndicesPrivileges;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Set;
+
+import static java.util.Collections.emptySet;
+import static java.util.Collections.unmodifiableSet;
+
+/**
+ * Request to determine whether the current user has a list of privileges.
+ */
+public final class HasPrivilegesRequest implements Validatable, ToXContentObject {
+
+    private final Set<String> clusterPrivileges;
+    private final Set<IndicesPrivileges> indexPrivileges;
+    private final Set<ApplicationResourcePrivileges> applicationPrivileges;
+
+    public HasPrivilegesRequest(@Nullable Set<String> clusterPrivileges,
+                                @Nullable Set<IndicesPrivileges> indexPrivileges,
+                                @Nullable Set<ApplicationResourcePrivileges> applicationPrivileges) {
+        this.clusterPrivileges = clusterPrivileges == null ? emptySet() : unmodifiableSet(clusterPrivileges);
+        this.indexPrivileges = indexPrivileges == null ? emptySet() : unmodifiableSet(indexPrivileges);
+        this.applicationPrivileges = applicationPrivileges == null ? emptySet() : unmodifiableSet(applicationPrivileges);
+
+        if (this.clusterPrivileges.isEmpty() && this.indexPrivileges.isEmpty() && this.applicationPrivileges.isEmpty()) {
+            throw new IllegalArgumentException("At last 1 privilege must be specified");
+        }
+    }
+
+    public Set<String> getClusterPrivileges() {
+        return clusterPrivileges;
+    }
+
+    public Set<IndicesPrivileges> getIndexPrivileges() {
+        return indexPrivileges;
+    }
+
+    public Set<ApplicationResourcePrivileges> getApplicationPrivileges() {
+        return applicationPrivileges;
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        return builder.startObject()
+            .field("cluster", clusterPrivileges)
+            .field("index", indexPrivileges)
+            .field("application", applicationPrivileges)
+            .endObject();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        final HasPrivilegesRequest that = (HasPrivilegesRequest) o;
+        return Objects.equals(clusterPrivileges, that.clusterPrivileges) &&
+            Objects.equals(indexPrivileges, that.indexPrivileges) &&
+            Objects.equals(applicationPrivileges, that.applicationPrivileges);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(clusterPrivileges, indexPrivileges, applicationPrivileges);
+    }
+}

+ 252 - 0
client/rest-high-level/src/main/java/org/elasticsearch/client/security/HasPrivilegesResponse.java

@@ -0,0 +1,252 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ObjectParser;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+
+/**
+ * Response when checking whether the current user has a defined set of privileges.
+ */
+public final class HasPrivilegesResponse {
+
+    private static final ConstructingObjectParser<HasPrivilegesResponse, Void> PARSER = new ConstructingObjectParser<>(
+        "has_privileges_response", true, args -> new HasPrivilegesResponse(
+        (String) args[0], (Boolean) args[1], checkMap(args[2], 0), checkMap(args[3], 1), checkMap(args[4], 2)));
+
+    static {
+        PARSER.declareString(constructorArg(), new ParseField("username"));
+        PARSER.declareBoolean(constructorArg(), new ParseField("has_all_requested"));
+        declareMap(constructorArg(), "cluster");
+        declareMap(constructorArg(), "index");
+        declareMap(constructorArg(), "application");
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> Map<String, T> checkMap(Object argument, int depth) {
+        if (argument instanceof Map) {
+            Map<String, T> map = (Map<String, T>) argument;
+            if (depth == 0) {
+                map.values().stream()
+                    .filter(val -> (val instanceof Boolean) == false)
+                    .forEach(val -> {
+                        throw new IllegalArgumentException("Map value [" + val + "] in [" + map + "] is not a Boolean");
+                    });
+            } else {
+                map.values().stream().forEach(val -> checkMap(val, depth - 1));
+            }
+            return map;
+        }
+        throw new IllegalArgumentException("Value [" + argument + "] is not an Object");
+    }
+
+    private static void declareMap(BiConsumer<HasPrivilegesResponse, Map<String, Object>> arg, String name) {
+        PARSER.declareField(arg, XContentParser::map, new ParseField(name), ObjectParser.ValueType.OBJECT);
+    }
+
+    private final String username;
+    private final boolean hasAllRequested;
+    private final Map<String, Boolean> clusterPrivileges;
+    private final Map<String, Map<String, Boolean>> indexPrivileges;
+    private final Map<String, Map<String, Map<String, Boolean>>> applicationPrivileges;
+
+    public HasPrivilegesResponse(String username, boolean hasAllRequested,
+                                 Map<String, Boolean> clusterPrivileges,
+                                 Map<String, Map<String, Boolean>> indexPrivileges,
+                                 Map<String, Map<String, Map<String, Boolean>>> applicationPrivileges) {
+        this.username = username;
+        this.hasAllRequested = hasAllRequested;
+        this.clusterPrivileges = Collections.unmodifiableMap(clusterPrivileges);
+        this.indexPrivileges = unmodifiableMap2(indexPrivileges);
+        this.applicationPrivileges = unmodifiableMap3(applicationPrivileges);
+    }
+
+    private static Map<String, Map<String, Boolean>> unmodifiableMap2(final Map<String, Map<String, Boolean>> map) {
+        final Map<String, Map<String, Boolean>> copy = new HashMap<>(map);
+        copy.replaceAll((k, v) -> Collections.unmodifiableMap(v));
+        return Collections.unmodifiableMap(copy);
+    }
+
+    private static Map<String, Map<String, Map<String, Boolean>>> unmodifiableMap3(
+        final Map<String, Map<String, Map<String, Boolean>>> map) {
+        final Map<String, Map<String, Map<String, Boolean>>> copy = new HashMap<>(map);
+        copy.replaceAll((k, v) -> unmodifiableMap2(v));
+        return Collections.unmodifiableMap(copy);
+    }
+
+    public static HasPrivilegesResponse fromXContent(XContentParser parser) throws IOException {
+        return PARSER.parse(parser, null);
+    }
+
+    /**
+     * The username (principal) of the user for which the privileges check was executed.
+     */
+    public String getUsername() {
+        return username;
+    }
+
+    /**
+     * {@code true} if the user has every privilege that was checked. Otherwise {@code false}.
+     */
+    public boolean hasAllRequested() {
+        return hasAllRequested;
+    }
+
+    /**
+     * @param clusterPrivilegeName The name of a cluster privilege. This privilege must have been specified (verbatim) in the
+     *                             {@link HasPrivilegesRequest#getClusterPrivileges() cluster privileges of the request}.
+     * @return {@code true} if the user has the specified cluster privilege. {@code false} if the privilege was checked
+     * but it has not been granted to the user.
+     * @throws IllegalArgumentException if the response did not include a value for the specified privilege name.
+     *                                  The response only includes values for privileges that were
+     *                                  {@link HasPrivilegesRequest#getClusterPrivileges() included in the request}.
+     */
+    public boolean hasClusterPrivilege(String clusterPrivilegeName) throws IllegalArgumentException {
+        Boolean has = clusterPrivileges.get(clusterPrivilegeName);
+        if (has == null) {
+            throw new IllegalArgumentException("Cluster privilege [" + clusterPrivilegeName + "] was not included in this response");
+        }
+        return has;
+    }
+
+    /**
+     * @param indexName     The name of the index to check. This index must have been specified (verbatim) in the
+     *                      {@link HasPrivilegesRequest#getIndexPrivileges() requested index privileges}.
+     * @param privilegeName The name of the index privilege to check. This privilege must have been specified (verbatim), for the
+     *                      given index, in the {@link HasPrivilegesRequest#getIndexPrivileges() requested index privileges}.
+     * @return {@code true} if the user has the specified privilege on the specified index. {@code false} if the privilege was checked
+     * for that index and was not granted to the user.
+     * @throws IllegalArgumentException if the response did not include a value for the specified index and privilege name pair.
+     *                                  The response only includes values for indices and privileges that were
+     *                                  {@link HasPrivilegesRequest#getIndexPrivileges() included in the request}.
+     */
+    public boolean hasIndexPrivilege(String indexName, String privilegeName) {
+        Map<String, Boolean> indexPrivileges = this.indexPrivileges.get(indexName);
+        if (indexPrivileges == null) {
+            throw new IllegalArgumentException("No privileges for index [" + indexName + "] were included in this response");
+        }
+        Boolean has = indexPrivileges.get(privilegeName);
+        if (has == null) {
+            throw new IllegalArgumentException("Privilege [" + privilegeName + "] was not included in the response for index ["
+                + indexName + "]");
+        }
+        return has;
+    }
+
+    /**
+     * @param applicationName The name of the application to check. This application must have been specified (verbatim) in the
+     *                        {@link HasPrivilegesRequest#getApplicationPrivileges() requested application privileges}.
+     * @param resourceName    The name of the resource to check. This resource must have been specified (verbatim), for the given
+     *                        application in the {@link HasPrivilegesRequest#getApplicationPrivileges() requested application privileges}.
+     * @param privilegeName   The name of the privilege to check. This privilege must have been specified (verbatim), for the given
+     *                        application and resource, in the
+     *                        {@link HasPrivilegesRequest#getApplicationPrivileges() requested application privileges}.
+     * @return {@code true} if the user has the specified privilege on the specified resource in the specified application.
+     * {@code false} if the privilege was checked for that application and resource, but was not granted to the user.
+     * @throws IllegalArgumentException if the response did not include a value for the specified application, resource and privilege
+     *                                  triplet. The response only includes values for applications, resources and privileges that were
+     *                                  {@link HasPrivilegesRequest#getApplicationPrivileges() included in the request}.
+     */
+    public boolean hasApplicationPrivilege(String applicationName, String resourceName, String privilegeName) {
+        final Map<String, Map<String, Boolean>> appPrivileges = this.applicationPrivileges.get(applicationName);
+        if (appPrivileges == null) {
+            throw new IllegalArgumentException("No privileges for application [" + applicationName + "] were included in this response");
+        }
+        final Map<String, Boolean> resourcePrivileges = appPrivileges.get(resourceName);
+        if (resourcePrivileges == null) {
+            throw new IllegalArgumentException("No privileges for resource [" + resourceName +
+                "] were included in the response for application [" + applicationName + "]");
+        }
+        Boolean has = resourcePrivileges.get(privilegeName);
+        if (has == null) {
+            throw new IllegalArgumentException("Privilege [" + privilegeName + "] was not included in the response for application [" +
+                applicationName + "] and resource [" + resourceName + "]");
+        }
+        return has;
+    }
+
+    /**
+     * A {@code Map} from cluster-privilege-name to access. Each requested privilege is included as a key in the map, and the
+     * associated value indicates whether the user was granted that privilege.
+     * <p>
+     * The {@link #hasClusterPrivilege} method should be used in preference to directly accessing this map.
+     * </p>
+     */
+    public Map<String, Boolean> getClusterPrivileges() {
+        return clusterPrivileges;
+    }
+
+    /**
+     * A {@code Map} from index-name + privilege-name to access. Each requested index is a key in the outer map.
+     * Each requested privilege is a key in the inner map. The inner most {@code Boolean} value indicates whether
+     * the user was granted that privilege on that index.
+     * <p>
+     * The {@link #hasIndexPrivilege} method should be used in preference to directly accessing this map.
+     * </p>
+     */
+    public Map<String, Map<String, Boolean>> getIndexPrivileges() {
+        return indexPrivileges;
+    }
+
+    /**
+     * A {@code Map} from application-name + resource-name + privilege-name to access. Each requested application is a key in the
+     * outer-most map. Each requested resource is a key in the next-level map. The requested privileges form the keys in the inner-most map.
+     * The {@code Boolean} value indicates whether the user was granted that privilege on that resource within that application.
+     * <p>
+     * The {@link #hasApplicationPrivilege} method should be used in preference to directly accessing this map.
+     * </p>
+     */
+    public Map<String, Map<String, Map<String, Boolean>>> getApplicationPrivileges() {
+        return applicationPrivileges;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || this.getClass() != o.getClass()) {
+            return false;
+        }
+        final HasPrivilegesResponse that = (HasPrivilegesResponse) o;
+        return this.hasAllRequested == that.hasAllRequested &&
+            Objects.equals(this.username, that.username) &&
+            Objects.equals(this.clusterPrivileges, that.clusterPrivileges) &&
+            Objects.equals(this.indexPrivileges, that.indexPrivileges) &&
+            Objects.equals(this.applicationPrivileges, that.applicationPrivileges);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(username, hasAllRequested, clusterPrivileges, indexPrivileges, applicationPrivileges);
+    }
+}
+

+ 66 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java

@@ -51,6 +51,8 @@ import org.elasticsearch.client.security.ExpressionRoleMapping;
 import org.elasticsearch.client.security.GetRoleMappingsRequest;
 import org.elasticsearch.client.security.GetRoleMappingsResponse;
 import org.elasticsearch.client.security.GetSslCertificatesResponse;
+import org.elasticsearch.client.security.HasPrivilegesRequest;
+import org.elasticsearch.client.security.HasPrivilegesResponse;
 import org.elasticsearch.client.security.InvalidateTokenRequest;
 import org.elasticsearch.client.security.InvalidateTokenResponse;
 import org.elasticsearch.client.security.PutRoleMappingRequest;
@@ -63,7 +65,9 @@ import org.elasticsearch.client.security.support.expressiondsl.RoleMapperExpress
 import org.elasticsearch.client.security.support.expressiondsl.expressions.AnyRoleMapperExpression;
 import org.elasticsearch.client.security.support.expressiondsl.fields.FieldRoleMapperExpression;
 import org.elasticsearch.client.security.user.User;
+import org.elasticsearch.client.security.user.privileges.IndicesPrivileges;
 import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.util.set.Sets;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.rest.RestStatus;
 import org.hamcrest.Matchers;
@@ -80,6 +84,7 @@ import java.util.concurrent.TimeUnit;
 import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.emptyIterable;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.isIn;
@@ -437,6 +442,67 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
         }
     }
 
+    public void testHasPrivileges() throws Exception {
+        RestHighLevelClient client = highLevelClient();
+        {
+            //tag::has-privileges-request
+            HasPrivilegesRequest request = new HasPrivilegesRequest(
+                Sets.newHashSet("monitor", "manage"),
+                Sets.newHashSet(
+                    IndicesPrivileges.builder().indices("logstash-2018-10-05").privileges("read", "write").build(),
+                    IndicesPrivileges.builder().indices("logstash-2018-*").privileges("read").build()
+                ),
+                null
+            );
+            //end::has-privileges-request
+
+            //tag::has-privileges-execute
+            HasPrivilegesResponse response = client.security().hasPrivileges(request, RequestOptions.DEFAULT);
+            //end::has-privileges-execute
+
+            //tag::has-privileges-response
+            boolean hasMonitor = response.hasClusterPrivilege("monitor"); // <1>
+            boolean hasWrite = response.hasIndexPrivilege("logstash-2018-10-05", "write"); // <2>
+            boolean hasRead = response.hasIndexPrivilege("logstash-2018-*", "read"); // <3>
+            //end::has-privileges-response
+
+            assertThat(response.getUsername(), is("test_user"));
+            assertThat(response.hasAllRequested(), is(true));
+            assertThat(hasMonitor, is(true));
+            assertThat(hasWrite, is(true));
+            assertThat(hasRead, is(true));
+            assertThat(response.getApplicationPrivileges().entrySet(), emptyIterable());
+        }
+
+        {
+            HasPrivilegesRequest request = new HasPrivilegesRequest(Collections.singleton("monitor"),null,null);
+
+            // tag::has-privileges-execute-listener
+            ActionListener<HasPrivilegesResponse> listener = new ActionListener<HasPrivilegesResponse>() {
+                @Override
+                public void onResponse(HasPrivilegesResponse response) {
+                    // <1>
+                }
+
+                @Override
+                public void onFailure(Exception e) {
+                    // <2>
+                }
+            };
+            // end::has-privileges-execute-listener
+
+            // Replace the empty listener by a blocking listener in test
+            final CountDownLatch latch = new CountDownLatch(1);
+            listener = new LatchedActionListener<>(listener, latch);
+
+            // tag::has-privileges-execute-async
+            client.security().hasPrivilegesAsync(request, RequestOptions.DEFAULT, listener); // <1>
+            // end::has-privileges-execute-async
+
+            assertTrue(latch.await(30L, TimeUnit.SECONDS));
+        }
+    }
+
     public void testClearRealmCache() throws Exception {
         RestHighLevelClient client = highLevelClient();
         {

+ 111 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/security/HasPrivilegesRequestTests.java

@@ -0,0 +1,111 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.client.security.user.privileges.ApplicationResourcePrivileges;
+import org.elasticsearch.client.security.user.privileges.IndicesPrivileges;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.util.set.Sets;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.EqualsHashCodeTestUtils;
+import org.elasticsearch.test.XContentTestUtils;
+import org.hamcrest.Matchers;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class HasPrivilegesRequestTests extends ESTestCase {
+
+    public void testToXContent() throws IOException {
+        final HasPrivilegesRequest request = new HasPrivilegesRequest(
+            new LinkedHashSet<>(Arrays.asList("monitor", "manage_watcher", "manage_ml")),
+            new LinkedHashSet<>(Arrays.asList(
+                IndicesPrivileges.builder().indices("index-001", "index-002").privileges("all").build(),
+                IndicesPrivileges.builder().indices("index-003").privileges("read").build()
+            )),
+            new LinkedHashSet<>(Arrays.asList(
+                new ApplicationResourcePrivileges("myapp", Arrays.asList("read", "write"), Arrays.asList("*")),
+                new ApplicationResourcePrivileges("myapp", Arrays.asList("admin"), Arrays.asList("/data/*"))
+            ))
+        );
+        String json = Strings.toString(request);
+        final Map<String, Object> parsed = XContentHelper.convertToMap(XContentType.JSON.xContent(), json, false);
+
+        final Map<String, Object> expected = XContentHelper.convertToMap(XContentType.JSON.xContent(), "{" +
+            " \"cluster\":[\"monitor\",\"manage_watcher\",\"manage_ml\"]," +
+            " \"index\":[{" +
+            "   \"names\":[\"index-001\",\"index-002\"]," +
+            "   \"privileges\":[\"all\"]" +
+            "  },{" +
+            "   \"names\":[\"index-003\"]," +
+            "   \"privileges\":[\"read\"]" +
+            " }]," +
+            " \"application\":[{" +
+            "   \"application\":\"myapp\"," +
+            "   \"privileges\":[\"read\",\"write\"]," +
+            "   \"resources\":[\"*\"]" +
+            "  },{" +
+            "   \"application\":\"myapp\"," +
+            "   \"privileges\":[\"admin\"]," +
+            "   \"resources\":[\"/data/*\"]" +
+            " }]" +
+            "}", false);
+
+        assertThat(XContentTestUtils.differenceBetweenMapsIgnoringArrayOrder(parsed, expected), Matchers.nullValue());
+    }
+
+    public void testEqualsAndHashCode() {
+        final Set<String> cluster = Sets.newHashSet(randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 8)));
+        final Set<IndicesPrivileges> indices = Sets.newHashSet(randomArray(1, 5, IndicesPrivileges[]::new,
+            () -> IndicesPrivileges.builder()
+                .indices(generateRandomStringArray(5, 12, false, false))
+                .privileges(generateRandomStringArray(3, 8, false, false))
+                .build()));
+        final Set<ApplicationResourcePrivileges> application = Sets.newHashSet(randomArray(1, 5, ApplicationResourcePrivileges[]::new,
+            () -> new ApplicationResourcePrivileges(
+                randomAlphaOfLengthBetween(5, 12),
+                Sets.newHashSet(generateRandomStringArray(3, 8, false, false)),
+                Sets.newHashSet(generateRandomStringArray(2, 6, false, false))
+            )));
+        final HasPrivilegesRequest request = new HasPrivilegesRequest(cluster, indices, application);
+        EqualsHashCodeTestUtils.checkEqualsAndHashCode(request, this::copy, this::mutate);
+    }
+
+    private HasPrivilegesRequest copy(HasPrivilegesRequest request) {
+        return new HasPrivilegesRequest(request.getClusterPrivileges(), request.getIndexPrivileges(), request.getApplicationPrivileges());
+    }
+
+    private HasPrivilegesRequest mutate(HasPrivilegesRequest request) {
+        switch (randomIntBetween(1, 3)) {
+            case 1:
+                return new HasPrivilegesRequest(null, request.getIndexPrivileges(), request.getApplicationPrivileges());
+            case 2:
+                return new HasPrivilegesRequest(request.getClusterPrivileges(), null, request.getApplicationPrivileges());
+            case 3:
+                return new HasPrivilegesRequest(request.getClusterPrivileges(), request.getIndexPrivileges(), null);
+        }
+        throw new IllegalStateException("The universe is broken (or the RNG is)");
+    }
+
+}

+ 262 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/security/HasPrivilegesResponseTests.java

@@ -0,0 +1,262 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch.client.security;
+
+import org.elasticsearch.common.collect.MapBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.EqualsHashCodeTestUtils;
+import org.hamcrest.Matchers;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import static java.util.Collections.emptyMap;
+
+public class HasPrivilegesResponseTests extends ESTestCase {
+
+    public void testParseValidResponse() throws IOException {
+        String json = "{" +
+            " \"username\": \"namor\"," +
+            " \"has_all_requested\": false," +
+            " \"cluster\" : {" +
+            "   \"manage\" : false," +
+            "   \"monitor\" : true" +
+            " }," +
+            " \"index\" : {" +
+            "   \"index-01\": {" +
+            "     \"read\" : true," +
+            "     \"write\" : false" +
+            "   }," +
+            "   \"index-02\": {" +
+            "     \"read\" : true," +
+            "     \"write\" : true" +
+            "   }," +
+            "   \"index-03\": {" +
+            "     \"read\" : false," +
+            "     \"write\" : false" +
+            "   }" +
+            " }," +
+            " \"application\" : {" +
+            "   \"app01\" : {" +
+            "     \"/object/1\" : {" +
+            "       \"read\" : true," +
+            "       \"write\" : false" +
+            "     }," +
+            "     \"/object/2\" : {" +
+            "       \"read\" : true," +
+            "       \"write\" : true" +
+            "     }" +
+            "   }," +
+            "   \"app02\" : {" +
+            "     \"/object/1\" : {" +
+            "       \"read\" : false," +
+            "       \"write\" : false" +
+            "     }," +
+            "     \"/object/3\" : {" +
+            "       \"read\" : false," +
+            "       \"write\" : true" +
+            "     }" +
+            "   }" +
+            " }" +
+            "}";
+        final XContentParser parser = createParser(XContentType.JSON.xContent(), json);
+        HasPrivilegesResponse response = HasPrivilegesResponse.fromXContent(parser);
+
+        assertThat(response.getUsername(), Matchers.equalTo("namor"));
+        assertThat(response.hasAllRequested(), Matchers.equalTo(false));
+
+        assertThat(response.getClusterPrivileges().keySet(), Matchers.containsInAnyOrder("monitor", "manage"));
+        assertThat(response.hasClusterPrivilege("monitor"), Matchers.equalTo(true));
+        assertThat(response.hasClusterPrivilege("manage"), Matchers.equalTo(false));
+
+        assertThat(response.getIndexPrivileges().keySet(), Matchers.containsInAnyOrder("index-01", "index-02", "index-03"));
+        assertThat(response.hasIndexPrivilege("index-01", "read"), Matchers.equalTo(true));
+        assertThat(response.hasIndexPrivilege("index-01", "write"), Matchers.equalTo(false));
+        assertThat(response.hasIndexPrivilege("index-02", "read"), Matchers.equalTo(true));
+        assertThat(response.hasIndexPrivilege("index-02", "write"), Matchers.equalTo(true));
+        assertThat(response.hasIndexPrivilege("index-03", "read"), Matchers.equalTo(false));
+        assertThat(response.hasIndexPrivilege("index-03", "write"), Matchers.equalTo(false));
+
+        assertThat(response.getApplicationPrivileges().keySet(), Matchers.containsInAnyOrder("app01", "app02"));
+        assertThat(response.hasApplicationPrivilege("app01", "/object/1", "read"), Matchers.equalTo(true));
+        assertThat(response.hasApplicationPrivilege("app01", "/object/1", "write"), Matchers.equalTo(false));
+        assertThat(response.hasApplicationPrivilege("app01", "/object/2", "read"), Matchers.equalTo(true));
+        assertThat(response.hasApplicationPrivilege("app01", "/object/2", "write"), Matchers.equalTo(true));
+        assertThat(response.hasApplicationPrivilege("app02", "/object/1", "read"), Matchers.equalTo(false));
+        assertThat(response.hasApplicationPrivilege("app02", "/object/1", "write"), Matchers.equalTo(false));
+        assertThat(response.hasApplicationPrivilege("app02", "/object/3", "read"), Matchers.equalTo(false));
+        assertThat(response.hasApplicationPrivilege("app02", "/object/3", "write"), Matchers.equalTo(true));
+    }
+
+    public void testHasClusterPrivilege() {
+        final Map<String, Boolean> cluster = MapBuilder.<String, Boolean>newMapBuilder()
+            .put("a", true)
+            .put("b", false)
+            .put("c", false)
+            .put("d", true)
+            .map();
+        final HasPrivilegesResponse response = new HasPrivilegesResponse("x", false, cluster, emptyMap(), emptyMap());
+        assertThat(response.hasClusterPrivilege("a"), Matchers.is(true));
+        assertThat(response.hasClusterPrivilege("b"), Matchers.is(false));
+        assertThat(response.hasClusterPrivilege("c"), Matchers.is(false));
+        assertThat(response.hasClusterPrivilege("d"), Matchers.is(true));
+
+        final IllegalArgumentException iae = expectThrows(IllegalArgumentException.class, () -> response.hasClusterPrivilege("e"));
+        assertThat(iae.getMessage(), Matchers.containsString("[e]"));
+        assertThat(iae.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("cluster privilege"));
+    }
+
+    public void testHasIndexPrivilege() {
+        final Map<String, Map<String, Boolean>> index = MapBuilder.<String, Map<String, Boolean>>newMapBuilder()
+            .put("i1", Collections.singletonMap("read", true))
+            .put("i2", Collections.singletonMap("read", false))
+            .put("i3", MapBuilder.<String, Boolean>newMapBuilder().put("read", true).put("write", true).map())
+            .put("i4", MapBuilder.<String, Boolean>newMapBuilder().put("read", true).put("write", false).map())
+            .put("i*", MapBuilder.<String, Boolean>newMapBuilder().put("read", false).put("write", false).map())
+            .map();
+        final HasPrivilegesResponse response = new HasPrivilegesResponse("x", false, emptyMap(), index, emptyMap());
+        assertThat(response.hasIndexPrivilege("i1", "read"), Matchers.is(true));
+        assertThat(response.hasIndexPrivilege("i2", "read"), Matchers.is(false));
+        assertThat(response.hasIndexPrivilege("i3", "read"), Matchers.is(true));
+        assertThat(response.hasIndexPrivilege("i3", "write"), Matchers.is(true));
+        assertThat(response.hasIndexPrivilege("i4", "read"), Matchers.is(true));
+        assertThat(response.hasIndexPrivilege("i4", "write"), Matchers.is(false));
+        assertThat(response.hasIndexPrivilege("i*", "read"), Matchers.is(false));
+        assertThat(response.hasIndexPrivilege("i*", "write"), Matchers.is(false));
+
+        final IllegalArgumentException iae1 = expectThrows(IllegalArgumentException.class, () -> response.hasIndexPrivilege("i0", "read"));
+        assertThat(iae1.getMessage(), Matchers.containsString("index [i0]"));
+
+        final IllegalArgumentException iae2 = expectThrows(IllegalArgumentException.class, () -> response.hasIndexPrivilege("i1", "write"));
+        assertThat(iae2.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("privilege [write]"));
+        assertThat(iae2.getMessage(), Matchers.containsString("index [i1]"));
+    }
+
+    public void testHasApplicationPrivilege() {
+        final Map<String, Map<String, Boolean>> app1 = MapBuilder.<String, Map<String, Boolean>>newMapBuilder()
+            .put("/data/1", Collections.singletonMap("read", true))
+            .put("/data/2", Collections.singletonMap("read", false))
+            .put("/data/3", MapBuilder.<String, Boolean>newMapBuilder().put("read", true).put("write", true).map())
+            .put("/data/4", MapBuilder.<String, Boolean>newMapBuilder().put("read", true).put("write", false).map())
+            .map();
+        final Map<String, Map<String, Boolean>> app2 = MapBuilder.<String, Map<String, Boolean>>newMapBuilder()
+            .put("/action/1", Collections.singletonMap("execute", true))
+            .put("/action/*", Collections.singletonMap("execute", false))
+            .map();
+        Map<String, Map<String, Map<String, Boolean>>> appPrivileges = new HashMap<>();
+        appPrivileges.put("a1", app1);
+        appPrivileges.put("a2", app2);
+        final HasPrivilegesResponse response = new HasPrivilegesResponse("x", false, emptyMap(), emptyMap(), appPrivileges);
+        assertThat(response.hasApplicationPrivilege("a1", "/data/1", "read"), Matchers.is(true));
+        assertThat(response.hasApplicationPrivilege("a1", "/data/2", "read"), Matchers.is(false));
+        assertThat(response.hasApplicationPrivilege("a1", "/data/3", "read"), Matchers.is(true));
+        assertThat(response.hasApplicationPrivilege("a1", "/data/3", "write"), Matchers.is(true));
+        assertThat(response.hasApplicationPrivilege("a1", "/data/4", "read"), Matchers.is(true));
+        assertThat(response.hasApplicationPrivilege("a1", "/data/4", "write"), Matchers.is(false));
+        assertThat(response.hasApplicationPrivilege("a2", "/action/1", "execute"), Matchers.is(true));
+        assertThat(response.hasApplicationPrivilege("a2", "/action/*", "execute"), Matchers.is(false));
+
+        final IllegalArgumentException iae1 = expectThrows(IllegalArgumentException.class,
+            () -> response.hasApplicationPrivilege("a0", "/data/1", "read"));
+        assertThat(iae1.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("application [a0]"));
+
+        final IllegalArgumentException iae2 = expectThrows(IllegalArgumentException.class,
+            () -> response.hasApplicationPrivilege("a1", "/data/0", "read"));
+        assertThat(iae2.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("application [a1]"));
+        assertThat(iae2.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("resource [/data/0]"));
+
+        final IllegalArgumentException iae3 = expectThrows(IllegalArgumentException.class,
+            () -> response.hasApplicationPrivilege("a1", "/action/1", "execute"));
+        assertThat(iae3.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("application [a1]"));
+        assertThat(iae3.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("resource [/action/1]"));
+
+        final IllegalArgumentException iae4 = expectThrows(IllegalArgumentException.class,
+            () -> response.hasApplicationPrivilege("a1", "/data/1", "write"));
+        assertThat(iae4.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("application [a1]"));
+        assertThat(iae4.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("resource [/data/1]"));
+        assertThat(iae4.getMessage().toLowerCase(Locale.ROOT), Matchers.containsString("privilege [write]"));
+    }
+
+    public void testEqualsAndHashCode() {
+        final HasPrivilegesResponse response = randomResponse();
+        EqualsHashCodeTestUtils.checkEqualsAndHashCode(response, this::copy, this::mutate);
+    }
+
+    private HasPrivilegesResponse copy(HasPrivilegesResponse response) {
+        return new HasPrivilegesResponse(response.getUsername(),
+            response.hasAllRequested(),
+            response.getClusterPrivileges(),
+            response.getIndexPrivileges(),
+            response.getApplicationPrivileges());
+    }
+
+    private HasPrivilegesResponse mutate(HasPrivilegesResponse request) {
+        switch (randomIntBetween(1, 5)) {
+            case 1:
+                return new HasPrivilegesResponse("_" + request.getUsername(), request.hasAllRequested(),
+                    request.getClusterPrivileges(), request.getIndexPrivileges(), request.getApplicationPrivileges());
+            case 2:
+                return new HasPrivilegesResponse(request.getUsername(), request.hasAllRequested() == false,
+                    request.getClusterPrivileges(), request.getIndexPrivileges(), request.getApplicationPrivileges());
+            case 3:
+                return new HasPrivilegesResponse(request.getUsername(), request.hasAllRequested(),
+                    emptyMap(), request.getIndexPrivileges(), request.getApplicationPrivileges());
+            case 4:
+                return new HasPrivilegesResponse(request.getUsername(), request.hasAllRequested(),
+                    request.getClusterPrivileges(), emptyMap(), request.getApplicationPrivileges());
+            case 5:
+                return new HasPrivilegesResponse(request.getUsername(), request.hasAllRequested(),
+                    request.getClusterPrivileges(), request.getIndexPrivileges(), emptyMap());
+        }
+        throw new IllegalStateException("The universe is broken (or the RNG is)");
+    }
+
+    private HasPrivilegesResponse randomResponse() {
+        final Map<String, Boolean> cluster = randomPrivilegeMap();
+        final Map<String, Map<String, Boolean>> index = randomResourceMap();
+
+        final Map<String, Map<String, Map<String, Boolean>>> application = new HashMap<>();
+        for (String app : randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 6).toLowerCase(Locale.ROOT))) {
+            application.put(app, randomResourceMap());
+        }
+        return new HasPrivilegesResponse(randomAlphaOfLengthBetween(3, 8), randomBoolean(), cluster, index, application);
+    }
+
+    private Map<String, Map<String, Boolean>> randomResourceMap() {
+        final Map<String, Map<String, Boolean>> resource = new HashMap<>();
+        for (String res : randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(5, 8))) {
+            resource.put(res, randomPrivilegeMap());
+        }
+        return resource;
+    }
+
+    private Map<String, Boolean> randomPrivilegeMap() {
+        final Map<String, Boolean> map = new HashMap<>();
+        for (String privilege : randomArray(1, 6, String[]::new, () -> randomAlphaOfLengthBetween(3, 12))) {
+            map.put(privilege, randomBoolean());
+        }
+        return map;
+    }
+
+}

+ 86 - 0
docs/java-rest/high-level/security/has-privileges.asciidoc

@@ -0,0 +1,86 @@
+--
+:api: has-privileges
+:request: HasPrivilegesRequest
+:response: HasPrivilegesResponse
+--
+
+[id="{upid}-{api}"]
+=== Has Privileges API
+
+[id="{upid}-{api}-request"]
+==== Has Privileges Request
+The +{request}+ supports checking for any or all of the following privilege types:
+
+* Cluster Privileges
+* Index Privileges
+* Application Privileges
+
+Privileges types that you do not wish to check my be passed in as +null+, but as least
+one privilege must be specified.
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-request]
+--------------------------------------------------
+include::../execution.asciidoc[]
+
+[id="{upid}-{api}-response"]
+==== Has Privileges Response
+
+The returned +{response}+ contains the following properties
+
+`username`::
+The username (userid) of the current user (for whom the "has privileges"
+check was executed)
+
+`hasAllRequested`::
+`true` if the user has all of the privileges that were specified in the
++{request}+. Otherwise `false`.
+
+`clusterPrivileges`::
+A `Map<String,Boolean>` where each key is the name of one of the cluster
+privileges specified in the request, and the value is `true` if the user
+has that privilege, and `false` otherwise.
++
+The method `hasClusterPrivilege` can be used to retrieve this information
+in a more fluent manner. This method throws an `IllegalArgumentException`
+if the privilege was not included in the response (which will be the case
+if the privilege was not part of the request).
+
+`indexPrivileges`::
+A `Map<String, Map<String, Boolean>>` where each key is the name of an
+index (as specified in the +{request}+) and the value is a `Map` from
+privilege name to a `Boolean`. The `Boolean` value is `true` if the user
+has that privilege on that index, and `false` otherwise.
++
+The method `hasIndexPrivilege` can be used to retrieve this information
+in a more fluent manner. This method throws an `IllegalArgumentException`
+if the privilege was not included in the response (which will be the case
+if the privilege was not part of the request).
+
+`applicationPrivileges`::
+A `Map<String, Map<String, Map<String, Boolean>>>>` where each key is the
+name of an application (as specified in the +{request}+).
+For each application, the value is a `Map` keyed by resource name, with
+each value  being another `Map` from privilege name to a `Boolean`.
+The `Boolean` value is `true` if the user has that privilege on that 
+resource for that application, and `false` otherwise.
++
+The method `hasApplicationPrivilege` can be used to retrieve this 
+information in a more fluent manner. This method throws an
+`IllegalArgumentException` if the privilege was not included in the
+response (which will be the case if the privilege was not part of the
+request).
+
+["source","java",subs="attributes,callouts,macros"]
+--------------------------------------------------
+include-tagged::{doc-tests-file}[{api}-response]
+--------------------------------------------------
+<1> `hasMonitor` will be `true` if the user has the `"monitor"`
+    cluster privilege.
+<2> `hasWrite` will be `true` if the user has the `"write"`
+    privilege on the `"logstash-2018-10-05"` index.
+<3> `hasRead` will be `true` if the user has the `"read"`
+    privilege on all possible indices that would match
+    the `"logstash-2018-*"` pattern.
+

+ 2 - 0
docs/java-rest/high-level/supported-apis.asciidoc

@@ -351,6 +351,7 @@ The Java High Level REST Client supports the following Security APIs:
 * <<{upid}-clear-roles-cache>>
 * <<{upid}-clear-realm-cache>>
 * <<{upid}-authenticate>>
+* <<{upid}-has-privileges>>
 * <<java-rest-high-security-get-certificates>>
 * <<java-rest-high-security-put-role-mapping>>
 * <<java-rest-high-security-get-role-mappings>>
@@ -368,6 +369,7 @@ include::security/delete-privileges.asciidoc[]
 include::security/clear-roles-cache.asciidoc[]
 include::security/clear-realm-cache.asciidoc[]
 include::security/authenticate.asciidoc[]
+include::security/has-privileges.asciidoc[]
 include::security/get-certificates.asciidoc[]
 include::security/put-role-mapping.asciidoc[]
 include::security/get-role-mappings.asciidoc[]