DEFAULT_EXECUTABLE_CONTEXTS = unmodifiableList(Arrays.asList(
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("do"), DoSection::parse),
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("set"), SetSection::parse),
+ new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("transform_and_set"), TransformAndSetSection::parse),
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("match"), MatchAssertion::parse),
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("is_true"), IsTrueAssertion::parse),
new NamedXContentRegistry.Entry(ExecutableSection.class, new ParseField("is_false"), IsFalseAssertion::parse),
diff --git a/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java
new file mode 100644
index 0000000000000..7b0b915dd97df
--- /dev/null
+++ b/test/framework/src/main/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSection.java
@@ -0,0 +1,106 @@
+/*
+ * 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.test.rest.yaml.section;
+
+import org.elasticsearch.common.ParsingException;
+import org.elasticsearch.common.xcontent.XContentLocation;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.rest.yaml.ClientYamlTestExecutionContext;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Represents a transform_and_set section:
+ *
+ *
+ * In the following example,
+ * - transform_and_set: { login_creds: "#base64EncodeCredentials(user,password)" }
+ * user and password are from the response which are joined by ':' and Base64 encoded and then stashed as 'login_creds'
+ *
+ */
+public class TransformAndSetSection implements ExecutableSection {
+ public static TransformAndSetSection parse(XContentParser parser) throws IOException {
+ String currentFieldName = null;
+ XContentParser.Token token;
+
+ TransformAndSetSection transformAndStashSection = new TransformAndSetSection(parser.getTokenLocation());
+
+ while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
+ if (token == XContentParser.Token.FIELD_NAME) {
+ currentFieldName = parser.currentName();
+ } else if (token.isValue()) {
+ transformAndStashSection.addSet(currentFieldName, parser.text());
+ }
+ }
+
+ parser.nextToken();
+
+ if (transformAndStashSection.getStash().isEmpty()) {
+ throw new ParsingException(transformAndStashSection.location, "transform_and_set section must set at least a value");
+ }
+
+ return transformAndStashSection;
+ }
+
+ private final Map transformStash = new HashMap<>();
+ private final XContentLocation location;
+
+ public TransformAndSetSection(XContentLocation location) {
+ this.location = location;
+ }
+
+ public void addSet(String stashedField, String transformThis) {
+ transformStash.put(stashedField, transformThis);
+ }
+
+ public Map getStash() {
+ return transformStash;
+ }
+
+ @Override
+ public XContentLocation getLocation() {
+ return location;
+ }
+
+ @Override
+ public void execute(ClientYamlTestExecutionContext executionContext) throws IOException {
+ for (Map.Entry entry : transformStash.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+ if (value.startsWith("#base64EncodeCredentials(") && value.endsWith(")")) {
+ value = entry.getValue().substring("#base64EncodeCredentials(".length(), entry.getValue().lastIndexOf(")"));
+ String[] idAndPassword = value.split(",");
+ if (idAndPassword.length == 2) {
+ String credentials = executionContext.response(idAndPassword[0].trim()) + ":"
+ + executionContext.response(idAndPassword[1].trim());
+ value = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8));
+ } else {
+ throw new IllegalArgumentException("base64EncodeCredentials requires a username/id and a password parameters");
+ }
+ }
+ executionContext.stash().stashValue(key, value);
+ }
+ }
+
+}
diff --git a/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.java b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.java
new file mode 100644
index 0000000000000..a61f91de287e7
--- /dev/null
+++ b/test/framework/src/test/java/org/elasticsearch/test/rest/yaml/section/TransformAndSetSectionTests.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.test.rest.yaml.section;
+
+import org.elasticsearch.common.ParsingException;
+import org.elasticsearch.common.xcontent.yaml.YamlXContent;
+import org.elasticsearch.test.rest.yaml.ClientYamlTestExecutionContext;
+import org.elasticsearch.test.rest.yaml.Stash;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class TransformAndSetSectionTests extends AbstractClientYamlTestFragmentParserTestCase {
+
+ public void testParseSingleValue() throws Exception {
+ parser = createParser(YamlXContent.yamlXContent,
+ "{ key: value }"
+ );
+
+ TransformAndSetSection transformAndSet = TransformAndSetSection.parse(parser);
+ assertThat(transformAndSet, notNullValue());
+ assertThat(transformAndSet.getStash(), notNullValue());
+ assertThat(transformAndSet.getStash().size(), equalTo(1));
+ assertThat(transformAndSet.getStash().get("key"), equalTo("value"));
+ }
+
+ public void testParseMultipleValues() throws Exception {
+ parser = createParser(YamlXContent.yamlXContent,
+ "{ key1: value1, key2: value2 }"
+ );
+
+ TransformAndSetSection transformAndSet = TransformAndSetSection.parse(parser);
+ assertThat(transformAndSet, notNullValue());
+ assertThat(transformAndSet.getStash(), notNullValue());
+ assertThat(transformAndSet.getStash().size(), equalTo(2));
+ assertThat(transformAndSet.getStash().get("key1"), equalTo("value1"));
+ assertThat(transformAndSet.getStash().get("key2"), equalTo("value2"));
+ }
+
+ public void testTransformation() throws Exception {
+ parser = createParser(YamlXContent.yamlXContent, "{ login_creds: \"#base64EncodeCredentials(id,api_key)\" }");
+
+ TransformAndSetSection transformAndSet = TransformAndSetSection.parse(parser);
+ assertThat(transformAndSet, notNullValue());
+ assertThat(transformAndSet.getStash(), notNullValue());
+ assertThat(transformAndSet.getStash().size(), equalTo(1));
+ assertThat(transformAndSet.getStash().get("login_creds"), equalTo("#base64EncodeCredentials(id,api_key)"));
+
+ ClientYamlTestExecutionContext executionContext = mock(ClientYamlTestExecutionContext.class);
+ when(executionContext.response("id")).thenReturn("user");
+ when(executionContext.response("api_key")).thenReturn("password");
+ Stash stash = new Stash();
+ when(executionContext.stash()).thenReturn(stash);
+ transformAndSet.execute(executionContext);
+ verify(executionContext).response("id");
+ verify(executionContext).response("api_key");
+ verify(executionContext).stash();
+ assertThat(stash.getValue("$login_creds"),
+ equalTo(Base64.getEncoder().encodeToString("user:password".getBytes(StandardCharsets.UTF_8))));
+ verifyNoMoreInteractions(executionContext);
+ }
+
+ public void testParseSetSectionNoValues() throws Exception {
+ parser = createParser(YamlXContent.yamlXContent,
+ "{ }"
+ );
+
+ Exception e = expectThrows(ParsingException.class, () -> TransformAndSetSection.parse(parser));
+ assertThat(e.getMessage(), is("transform_and_set section must set at least a value"));
+ }
+}
diff --git a/x-pack/docs/build.gradle b/x-pack/docs/build.gradle
index b5bff78045511..713c3c1a5815f 100644
--- a/x-pack/docs/build.gradle
+++ b/x-pack/docs/build.gradle
@@ -73,6 +73,7 @@ project.copyRestSpec.from(xpackResources) {
}
integTestCluster {
setting 'xpack.security.enabled', 'true'
+ setting 'xpack.security.authc.api_key.enabled', 'true'
setting 'xpack.security.authc.token.enabled', 'true'
// Disable monitoring exporters for the docs tests
setting 'xpack.monitoring.exporters._local.type', 'local'
diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc
index 851bd2ba327b2..c59c44312ae60 100644
--- a/x-pack/docs/en/rest-api/security.asciidoc
+++ b/x-pack/docs/en/rest-api/security.asciidoc
@@ -51,6 +51,17 @@ without requiring basic authentication:
* <>
* <>
+[float]
+[[security-api-keys]]
+=== API Keys
+
+You can use the following APIs to create, retrieve and invalidate API keys for access
+without requiring basic authentication:
+
+* <>
+* <>
+* <>
+
[float]
[[security-user-apis]]
=== Users
@@ -88,3 +99,6 @@ include::security/get-users.asciidoc[]
include::security/has-privileges.asciidoc[]
include::security/invalidate-tokens.asciidoc[]
include::security/ssl.asciidoc[]
+include::security/create-api-keys.asciidoc[]
+include::security/invalidate-api-keys.asciidoc[]
+include::security/get-api-keys.asciidoc[]
diff --git a/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc
new file mode 100644
index 0000000000000..e4fa1be71d40e
--- /dev/null
+++ b/x-pack/docs/en/rest-api/security/create-api-keys.asciidoc
@@ -0,0 +1,99 @@
+[role="xpack"]
+[[security-api-create-api-key]]
+=== Create API Key API
+
+Creates an API key for access without requiring basic authentication.
+
+==== Request
+
+`POST /_security/api_key`
+`PUT /_security/api_key`
+
+==== Description
+
+The API keys are created by the {es} API key service, which is automatically enabled
+when you configure TLS on the HTTP interface. See <>. Alternatively,
+you can explicitly enable the `xpack.security.authc.api_key.enabled` setting. When
+you are running in production mode, a bootstrap check prevents you from enabling
+the API key service unless you also enable TLS on the HTTP interface.
+
+A successful create API key API call returns a JSON structure that contains
+the unique id, the name to identify API key, the API key and the expiration if
+applicable for the API key in milliseconds.
+
+NOTE: By default API keys never expire. You can specify expiration at the time of
+creation for the API keys.
+
+==== Request Body
+
+The following parameters can be specified in the body of a POST or PUT request:
+
+`name`::
+(string) Specifies the name for this API key.
+
+`role_descriptors`::
+(array-of-role-descriptor) Optional array of role descriptor for this API key. The role descriptor
+must be a subset of permissions of the authenticated user. The structure of role
+descriptor is same as the request for create role API. For more details on role
+see <>.
+If the role descriptors are not provided then permissions of the authenticated user are applied.
+
+`expiration`::
+(string) Optional expiration time for the API key. By default API keys never expire.
+
+==== Examples
+
+The following example creates an API key:
+
+[source, js]
+------------------------------------------------------------
+POST /_security/api_key
+{
+ "name": "my-api-key",
+ "expiration": "1d", <1>
+ "role_descriptors": { <2>
+ "role-a": {
+ "cluster": ["all"],
+ "index": [
+ {
+ "names": ["index-a*"],
+ "privileges": ["read"]
+ }
+ ]
+ },
+ "role-b": {
+ "cluster": ["all"],
+ "index": [
+ {
+ "names": ["index-b*"],
+ "privileges": ["all"]
+ }
+ ]
+ }
+ }
+}
+------------------------------------------------------------
+// CONSOLE
+<1> optional expiration for the API key being generated. If expiration is not
+ provided then the API keys do not expire.
+<2> optional role descriptors for this API key, if not provided then permissions
+ of authenticated user are applied.
+
+A successful call returns a JSON structure that provides
+API key information.
+
+[source,js]
+--------------------------------------------------
+{
+ "id":"VuaCfGcBCdbkQm-e5aOx", <1>
+ "name":"my-api-key",
+ "expiration":1544068612110, <2>
+ "api_key":"ui2lp2axTNmsyakw9tvNnw" <3>
+}
+--------------------------------------------------
+// TESTRESPONSE[s/VuaCfGcBCdbkQm-e5aOx/$body.id/]
+// TESTRESPONSE[s/1544068612110/$body.expiration/]
+// TESTRESPONSE[s/ui2lp2axTNmsyakw9tvNnw/$body.api_key/]
+<1> unique id for this API key
+<2> optional expiration in milliseconds for this API key
+<3> generated API key
diff --git a/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc
new file mode 100644
index 0000000000000..ab2ef770cb124
--- /dev/null
+++ b/x-pack/docs/en/rest-api/security/get-api-keys.asciidoc
@@ -0,0 +1,118 @@
+[role="xpack"]
+[[security-api-get-api-key]]
+=== Get API Key information API
+++++
+Get API key information
+++++
+
+Retrieves information for one or more API keys.
+
+==== Request
+
+`GET /_security/api_key`
+
+==== Description
+
+The information for the API keys created by <> can be retrieved
+using this API.
+
+==== Request Body
+
+The following parameters can be specified in the query parameters of a GET request and
+pertain to retrieving api keys:
+
+`id` (optional)::
+(string) An API key id. This parameter cannot be used with any of `name`, `realm_name` or
+ `username` are used.
+
+`name` (optional)::
+(string) An API key name. This parameter cannot be used with any of `id`, `realm_name` or
+ `username` are used.
+
+`realm_name` (optional)::
+(string) The name of an authentication realm. This parameter cannot be used with either `id` or `name`.
+
+`username` (optional)::
+(string) The username of a user. This parameter cannot be used with either `id` or `name`.
+
+NOTE: While all parameters are optional, at least one of them is required.
+
+==== Examples
+
+The following example to retrieve the API key identified by specified `id`:
+
+[source,js]
+--------------------------------------------------
+GET /_security/api_key?id=dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==
+--------------------------------------------------
+// NOTCONSOLE
+
+whereas the following example to retrieve the API key identified by specified `name`:
+
+[source,js]
+--------------------------------------------------
+GET /_security/api_key?name=hadoop_myuser_key
+--------------------------------------------------
+// NOTCONSOLE
+
+The following example retrieves all API keys for the `native1` realm:
+
+[source,js]
+--------------------------------------------------
+GET /_xpack/api_key?realm_name=native1
+--------------------------------------------------
+// NOTCONSOLE
+
+The following example retrieves all API keys for the user `myuser` in all realms:
+
+[source,js]
+--------------------------------------------------
+GET /_xpack/api_key?username=myuser
+--------------------------------------------------
+// NOTCONSOLE
+
+Finally, the following example retrieves all API keys for the user `myuser` in
+ the `native1` realm immediately:
+
+[source,js]
+--------------------------------------------------
+GET /_xpack/api_key?username=myuser&realm_name=native1
+--------------------------------------------------
+// NOTCONSOLE
+
+A successful call returns a JSON structure that contains the information of one or more API keys that were retrieved.
+
+[source,js]
+--------------------------------------------------
+{
+ "api_keys": [ <1>
+ {
+ "id": "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==", <2>
+ "name": "hadoop_myuser_key", <3>
+ "creation": 1548550550158, <4>
+ "expiration": 1548551550158, <5>
+ "invalidated": false, <6>
+ "username": "myuser", <7>
+ "realm": "native1" <8>
+ },
+ {
+ "id": "api-key-id-2",
+ "name": "api-key-name-2",
+ "creation": 1548550550158,
+ "invalidated": false,
+ "username": "user-y",
+ "realm": "realm-2"
+ }
+ ]
+}
+--------------------------------------------------
+// NOTCONSOLE
+
+<1> The list of API keys that were retrieved for this request.
+<2> Id for the API key
+<3> Name of the API key
+<4> Creation time for the API key in milliseconds
+<5> optional expiration time for the API key in milliseconds
+<6> invalidation status for the API key, `true` if the key has been invalidated else `false`
+<7> principal for which this API key was created
+<8> realm name of the principal for which this API key was created
diff --git a/x-pack/docs/en/rest-api/security/invalidate-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/invalidate-api-keys.asciidoc
new file mode 100644
index 0000000000000..4809e267ebd80
--- /dev/null
+++ b/x-pack/docs/en/rest-api/security/invalidate-api-keys.asciidoc
@@ -0,0 +1,140 @@
+[role="xpack"]
+[[security-api-invalidate-api-key]]
+=== Invalidate API Key API
+++++
+Invalidate API key
+++++
+
+Invalidates one or more API keys.
+
+==== Request
+
+`DELETE /_security/api_key`
+
+==== Description
+
+The API keys created by <> can be invalidated
+using this API.
+
+==== Request Body
+
+The following parameters can be specified in the body of a DELETE request and
+pertain to invalidating api keys:
+
+`id` (optional)::
+(string) An API key id. This parameter cannot be used with any of `name`, `realm_name` or
+ `username` are used.
+
+`name` (optional)::
+(string) An API key name. This parameter cannot be used with any of `id`, `realm_name` or
+ `username` are used.
+
+`realm_name` (optional)::
+(string) The name of an authentication realm. This parameter cannot be used with either `api_key_id` or `api_key_name`.
+
+`username` (optional)::
+(string) The username of a user. This parameter cannot be used with either `api_key_id` or `api_key_name`.
+
+NOTE: While all parameters are optional, at least one of them is required.
+
+==== Examples
+
+The following example invalidates the API key identified by specified `id` immediately:
+
+[source,js]
+--------------------------------------------------
+DELETE /_security/api_key
+{
+ "id" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ=="
+}
+--------------------------------------------------
+// NOTCONSOLE
+
+whereas the following example invalidates the API key identified by specified `name` immediately:
+
+[source,js]
+--------------------------------------------------
+DELETE /_security/api_key
+{
+ "name" : "hadoop_myuser_key"
+}
+--------------------------------------------------
+// NOTCONSOLE
+
+The following example invalidates all API keys for the `native1` realm immediately:
+
+[source,js]
+--------------------------------------------------
+DELETE /_xpack/api_key
+{
+ "realm_name" : "native1"
+}
+--------------------------------------------------
+// NOTCONSOLE
+
+The following example invalidates all API keys for the user `myuser` in all realms immediately:
+
+[source,js]
+--------------------------------------------------
+DELETE /_xpack/api_key
+{
+ "username" : "myuser"
+}
+--------------------------------------------------
+// NOTCONSOLE
+
+Finally, the following example invalidates all API keys for the user `myuser` in
+ the `native1` realm immediately:
+
+[source,js]
+--------------------------------------------------
+DELETE /_xpack/api_key
+{
+ "username" : "myuser",
+ "realm_name" : "native1"
+}
+--------------------------------------------------
+// NOTCONSOLE
+
+A successful call returns a JSON structure that contains the ids of the API keys that were invalidated, the ids
+of the API keys that had already been invalidated, and potentially a list of errors encountered while invalidating
+specific api keys.
+
+[source,js]
+--------------------------------------------------
+{
+ "invalidated_api_keys": [ <1>
+ "api-key-id-1"
+ ],
+ "previously_invalidated_api_keys": [ <2>
+ "api-key-id-2",
+ "api-key-id-3"
+ ],
+ "error_count": 2, <3>
+ "error_details": [ <4>
+ {
+ "type": "exception",
+ "reason": "error occurred while invalidating api keys",
+ "caused_by": {
+ "type": "illegal_argument_exception",
+ "reason": "invalid api key id"
+ }
+ },
+ {
+ "type": "exception",
+ "reason": "error occurred while invalidating api keys",
+ "caused_by": {
+ "type": "illegal_argument_exception",
+ "reason": "invalid api key id"
+ }
+ }
+ ]
+}
+--------------------------------------------------
+// NOTCONSOLE
+
+<1> The ids of the API keys that were invalidated as part of this request.
+<2> The ids of the API keys that were already invalidated.
+<3> The number of errors that were encountered when invalidating the API keys.
+<4> Details about these errors. This field is not present in the response when
+ `error_count` is 0.
diff --git a/x-pack/plugin/build.gradle b/x-pack/plugin/build.gradle
index dbb4e86dc197b..debd4dca867b6 100644
--- a/x-pack/plugin/build.gradle
+++ b/x-pack/plugin/build.gradle
@@ -133,6 +133,7 @@ integTestCluster {
setting 'xpack.monitoring.exporters._local.type', 'local'
setting 'xpack.monitoring.exporters._local.enabled', 'false'
setting 'xpack.security.authc.token.enabled', 'true'
+ setting 'xpack.security.authc.api_key.enabled', 'true'
setting 'xpack.security.transport.ssl.enabled', 'true'
setting 'xpack.security.transport.ssl.key', nodeKey.name
setting 'xpack.security.transport.ssl.certificate', nodeCert.name
diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java
index 884d12afe6650..06a686541005d 100644
--- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java
+++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/CcrLicenseChecker.java
@@ -14,13 +14,13 @@
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.action.admin.cluster.state.ClusterStateRequest;
import org.elasticsearch.action.admin.cluster.state.ClusterStateResponse;
-import org.elasticsearch.action.support.ContextPreservingActionListener;
import org.elasticsearch.action.admin.indices.stats.IndexShardStats;
import org.elasticsearch.action.admin.indices.stats.IndexStats;
import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction;
import org.elasticsearch.action.admin.indices.stats.IndicesStatsRequest;
import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse;
import org.elasticsearch.action.admin.indices.stats.ShardStats;
+import org.elasticsearch.action.support.ContextPreservingActionListener;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.FilterClient;
import org.elasticsearch.cluster.ClusterState;
@@ -37,14 +37,15 @@
import org.elasticsearch.license.RemoteClusterLicenseChecker;
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.rest.RestStatus;
-import org.elasticsearch.xpack.ccr.action.ShardFollowTask;
import org.elasticsearch.xpack.ccr.action.ShardChangesAction;
+import org.elasticsearch.xpack.ccr.action.ShardFollowTask;
import org.elasticsearch.xpack.core.XPackPlugin;
import org.elasticsearch.xpack.core.security.SecurityContext;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesResponse;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
+import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges;
import org.elasticsearch.xpack.core.security.support.Exceptions;
import java.util.Arrays;
@@ -334,7 +335,7 @@ public void hasPrivilegesToFollowIndices(final Client remoteClient, final String
message.append(indices.length == 1 ? " index " : " indices ");
message.append(Arrays.toString(indices));
- HasPrivilegesResponse.ResourcePrivileges resourcePrivileges = response.getIndexPrivileges().iterator().next();
+ ResourcePrivileges resourcePrivileges = response.getIndexPrivileges().iterator().next();
for (Map.Entry entry : resourcePrivileges.getPrivileges().entrySet()) {
if (entry.getValue() == false) {
message.append(", privilege for action [");
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java
index b16409b6a3ccf..10ea252f5cd5f 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackClientPlugin.java
@@ -136,6 +136,9 @@
import org.elasticsearch.xpack.core.security.SecurityFeatureSetUsage;
import org.elasticsearch.xpack.core.security.SecurityField;
import org.elasticsearch.xpack.core.security.SecuritySettings;
+import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.GetApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction;
import org.elasticsearch.xpack.core.security.action.realm.ClearRealmCacheAction;
import org.elasticsearch.xpack.core.security.action.role.ClearRolesCacheAction;
import org.elasticsearch.xpack.core.security.action.role.DeleteRoleAction;
@@ -314,6 +317,9 @@ public List getClientActions() {
InvalidateTokenAction.INSTANCE,
GetCertificateInfoAction.INSTANCE,
RefreshTokenAction.INSTANCE,
+ CreateApiKeyAction.INSTANCE,
+ InvalidateApiKeyAction.INSTANCE,
+ GetApiKeyAction.INSTANCE,
// upgrade
IndexUpgradeInfoAction.INSTANCE,
IndexUpgradeAction.INSTANCE,
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java
index 288811fc1af9b..89e572bff8237 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java
@@ -100,7 +100,7 @@ private XPackSettings() {
public static final Setting RESERVED_REALM_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.reserved_realm.enabled",
true, Setting.Property.NodeScope);
- /** Setting for enabling or disabling the token service. Defaults to true */
+ /** Setting for enabling or disabling the token service. Defaults to the value of https being enabled */
public static final Setting TOKEN_SERVICE_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.token.enabled", (s) -> {
if (NetworkModule.HTTP_ENABLED.get(s)) {
return XPackSettings.HTTP_SSL_ENABLED.getRaw(s);
@@ -109,6 +109,10 @@ private XPackSettings() {
}
}, Setting.Property.NodeScope);
+ /** Setting for enabling or disabling the api key service. Defaults to the value of https being enabled */
+ public static final Setting API_KEY_SERVICE_ENABLED_SETTING = Setting.boolSetting("xpack.security.authc.api_key.enabled",
+ XPackSettings.HTTP_SSL_ENABLED::getRaw, Setting.Property.NodeScope);
+
/** Setting for enabling or disabling FIPS mode. Defaults to false */
public static final Setting FIPS_MODE_ENABLED =
Setting.boolSetting("xpack.security.fips_mode.enabled", false, Property.NodeScope);
@@ -197,6 +201,7 @@ public static List> getAllSettings() {
settings.add(HTTP_SSL_ENABLED);
settings.add(RESERVED_REALM_ENABLED_SETTING);
settings.add(TOKEN_SERVICE_ENABLED_SETTING);
+ settings.add(API_KEY_SERVICE_ENABLED_SETTING);
settings.add(SQL_ENABLED);
settings.add(USER_SETTING);
settings.add(ROLLUP_ENABLED);
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java
index c737ab75d81aa..0da07a52996ad 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityContext.java
@@ -13,9 +13,11 @@
import org.elasticsearch.common.util.concurrent.ThreadContext.StoredContext;
import org.elasticsearch.node.Node;
import org.elasticsearch.xpack.core.security.authc.Authentication;
+import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
import org.elasticsearch.xpack.core.security.user.User;
import java.io.IOException;
+import java.util.Collections;
import java.util.Objects;
import java.util.function.Consumer;
@@ -71,7 +73,8 @@ public void setUser(User user, Version version) {
} else {
lookedUpBy = null;
}
- setAuthentication(new Authentication(user, authenticatedBy, lookedUpBy, version));
+ setAuthentication(
+ new Authentication(user, authenticatedBy, lookedUpBy, version, AuthenticationType.INTERNAL, Collections.emptyMap()));
}
/** Writes the authentication to the thread context */
@@ -89,7 +92,7 @@ private void setAuthentication(Authentication authentication) {
*/
public void executeAsUser(User user, Consumer consumer, Version version) {
final StoredContext original = threadContext.newStoredContext(true);
- try (ThreadContext.StoredContext ctx = threadContext.stashContext()) {
+ try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
setUser(user, version);
consumer.accept(original);
}
@@ -102,9 +105,9 @@ public void executeAsUser(User user, Consumer consumer, Version v
public void executeAfterRewritingAuthentication(Consumer consumer, Version version) {
final StoredContext original = threadContext.newStoredContext(true);
final Authentication authentication = Objects.requireNonNull(userSettings.getAuthentication());
- try (ThreadContext.StoredContext ctx = threadContext.stashContext()) {
+ try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
setAuthentication(new Authentication(authentication.getUser(), authentication.getAuthenticatedBy(),
- authentication.getLookedUpBy(), version));
+ authentication.getLookedUpBy(), version, authentication.getAuthenticationType(), authentication.getMetadata()));
consumer.accept(original);
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java
new file mode 100644
index 0000000000000..bfe9f523062a0
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ApiKey.java
@@ -0,0 +1,165 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+/**
+ * API key information
+ */
+public final class ApiKey implements ToXContentObject, Writeable {
+
+ private final String name;
+ private final String id;
+ private final Instant creation;
+ private final Instant expiration;
+ private final boolean invalidated;
+ private final String username;
+ private final String realm;
+
+ public ApiKey(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username, String realm) {
+ this.name = name;
+ this.id = id;
+ // As we do not yet support the nanosecond precision when we serialize to JSON,
+ // here creating the 'Instant' of milliseconds precision.
+ // This Instant can then be used for date comparison.
+ this.creation = Instant.ofEpochMilli(creation.toEpochMilli());
+ this.expiration = (expiration != null) ? Instant.ofEpochMilli(expiration.toEpochMilli()): null;
+ this.invalidated = invalidated;
+ this.username = username;
+ this.realm = realm;
+ }
+
+ public ApiKey(StreamInput in) throws IOException {
+ this.name = in.readString();
+ this.id = in.readString();
+ this.creation = in.readInstant();
+ this.expiration = in.readOptionalInstant();
+ this.invalidated = in.readBoolean();
+ this.username = in.readString();
+ this.realm = in.readString();
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Instant getCreation() {
+ return creation;
+ }
+
+ public Instant getExpiration() {
+ return expiration;
+ }
+
+ public boolean isInvalidated() {
+ return invalidated;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public String getRealm() {
+ return realm;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject()
+ .field("id", id)
+ .field("name", name)
+ .field("creation", creation.toEpochMilli());
+ if (expiration != null) {
+ builder.field("expiration", expiration.toEpochMilli());
+ }
+ builder.field("invalidated", invalidated)
+ .field("username", username)
+ .field("realm", realm);
+ return builder.endObject();
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeString(name);
+ out.writeString(id);
+ out.writeInstant(creation);
+ out.writeOptionalInstant(expiration);
+ out.writeBoolean(invalidated);
+ out.writeString(username);
+ out.writeString(realm);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, id, creation, expiration, invalidated, username, realm);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ ApiKey other = (ApiKey) obj;
+ return Objects.equals(name, other.name)
+ && Objects.equals(id, other.id)
+ && Objects.equals(creation, other.creation)
+ && Objects.equals(expiration, other.expiration)
+ && Objects.equals(invalidated, other.invalidated)
+ && Objects.equals(username, other.username)
+ && Objects.equals(realm, other.realm);
+ }
+
+ static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("api_key", args -> {
+ return new ApiKey((String) args[0], (String) args[1], Instant.ofEpochMilli((Long) args[2]),
+ (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), (Boolean) args[4], (String) args[5], (String) args[6]);
+ });
+ static {
+ PARSER.declareString(constructorArg(), new ParseField("name"));
+ PARSER.declareString(constructorArg(), new ParseField("id"));
+ PARSER.declareLong(constructorArg(), new ParseField("creation"));
+ PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration"));
+ PARSER.declareBoolean(constructorArg(), new ParseField("invalidated"));
+ PARSER.declareString(constructorArg(), new ParseField("username"));
+ PARSER.declareString(constructorArg(), new ParseField("realm"));
+ }
+
+ public static ApiKey fromXContent(XContentParser parser) throws IOException {
+ return PARSER.parse(parser, null);
+ }
+
+ @Override
+ public String toString() {
+ return "ApiKey [name=" + name + ", id=" + id + ", creation=" + creation + ", expiration=" + expiration + ", invalidated="
+ + invalidated + ", username=" + username + ", realm=" + realm + "]";
+ }
+
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyAction.java
new file mode 100644
index 0000000000000..52d290e10ca3a
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyAction.java
@@ -0,0 +1,39 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.Action;
+import org.elasticsearch.client.ElasticsearchClient;
+import org.elasticsearch.common.io.stream.Writeable;
+
+/**
+ * Action for the creation of an API key
+ */
+public final class CreateApiKeyAction extends Action {
+
+ public static final String NAME = "cluster:admin/xpack/security/api_key/create";
+ public static final CreateApiKeyAction INSTANCE = new CreateApiKeyAction();
+
+ private CreateApiKeyAction() {
+ super(NAME);
+ }
+
+ @Override
+ public CreateApiKeyResponse newResponse() {
+ throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
+ }
+
+ @Override
+ public Writeable.Reader getResponseReader() {
+ return CreateApiKeyResponse::new;
+ }
+
+ @Override
+ public CreateApiKeyRequestBuilder newRequestBuilder(ElasticsearchClient client) {
+ return new CreateApiKeyRequestBuilder(client);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java
new file mode 100644
index 0000000000000..c3f7ece21fc79
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.support.WriteRequest;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+
+/**
+ * Request class used for the creation of an API key. The request requires a name to be provided
+ * and optionally an expiration time and permission limitation can be provided.
+ */
+public final class CreateApiKeyRequest extends ActionRequest {
+ public static final WriteRequest.RefreshPolicy DEFAULT_REFRESH_POLICY = WriteRequest.RefreshPolicy.WAIT_UNTIL;
+
+ private String name;
+ private TimeValue expiration;
+ private List roleDescriptors = Collections.emptyList();
+ private WriteRequest.RefreshPolicy refreshPolicy = DEFAULT_REFRESH_POLICY;
+
+ public CreateApiKeyRequest() {}
+
+ /**
+ * Create API Key request constructor
+ * @param name name for the API key
+ * @param roleDescriptors list of {@link RoleDescriptor}s
+ * @param expiration to specify expiration for the API key
+ */
+ public CreateApiKeyRequest(String name, List roleDescriptors, @Nullable TimeValue expiration) {
+ if (Strings.hasText(name)) {
+ this.name = name;
+ } else {
+ throw new IllegalArgumentException("name must not be null or empty");
+ }
+ this.roleDescriptors = Objects.requireNonNull(roleDescriptors, "role descriptors may not be null");
+ this.expiration = expiration;
+ }
+
+ public CreateApiKeyRequest(StreamInput in) throws IOException {
+ super(in);
+ this.name = in.readString();
+ this.expiration = in.readOptionalTimeValue();
+ this.roleDescriptors = Collections.unmodifiableList(in.readList(RoleDescriptor::new));
+ this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ if (Strings.hasText(name)) {
+ this.name = name;
+ } else {
+ throw new IllegalArgumentException("name must not be null or empty");
+ }
+ }
+
+ public TimeValue getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(TimeValue expiration) {
+ this.expiration = expiration;
+ }
+
+ public List getRoleDescriptors() {
+ return roleDescriptors;
+ }
+
+ public void setRoleDescriptors(List roleDescriptors) {
+ this.roleDescriptors = Collections.unmodifiableList(Objects.requireNonNull(roleDescriptors, "role descriptors may not be null"));
+ }
+
+ public WriteRequest.RefreshPolicy getRefreshPolicy() {
+ return refreshPolicy;
+ }
+
+ public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) {
+ this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null");
+ }
+
+ @Override
+ public ActionRequestValidationException validate() {
+ ActionRequestValidationException validationException = null;
+ if (Strings.isNullOrEmpty(name)) {
+ validationException = addValidationError("name is required", validationException);
+ } else {
+ if (name.length() > 256) {
+ validationException = addValidationError("name may not be more than 256 characters long", validationException);
+ }
+ if (name.equals(name.trim()) == false) {
+ validationException = addValidationError("name may not begin or end with whitespace", validationException);
+ }
+ if (name.startsWith("_")) {
+ validationException = addValidationError("name may not begin with an underscore", validationException);
+ }
+ }
+ return validationException;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeString(name);
+ out.writeOptionalTimeValue(expiration);
+ out.writeList(roleDescriptors);
+ refreshPolicy.writeTo(out);
+ }
+
+ @Override
+ public void readFrom(StreamInput in) throws IOException {
+ super.readFrom(in);
+ this.name = in.readString();
+ this.expiration = in.readOptionalTimeValue();
+ this.roleDescriptors = Collections.unmodifiableList(in.readList(RoleDescriptor::new));
+ this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java
new file mode 100644
index 0000000000000..e089ec826da17
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilder.java
@@ -0,0 +1,85 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionRequestBuilder;
+import org.elasticsearch.action.support.WriteRequest;
+import org.elasticsearch.client.ElasticsearchClient;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+/**
+ * Request builder for populating a {@link CreateApiKeyRequest}
+ */
+public final class CreateApiKeyRequestBuilder
+ extends ActionRequestBuilder {
+
+ @SuppressWarnings("unchecked")
+ static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(
+ "api_key_request", false, (args, v) -> {
+ return new CreateApiKeyRequest((String) args[0], (List) args[1],
+ TimeValue.parseTimeValue((String) args[2], null, "expiration"));
+ });
+
+ static {
+ PARSER.declareString(constructorArg(), new ParseField("name"));
+ PARSER.declareNamedObjects(constructorArg(), (p, c, n) -> {
+ p.nextToken();
+ return RoleDescriptor.parse(n, p, false);
+ }, new ParseField("role_descriptors"));
+ PARSER.declareString(optionalConstructorArg(), new ParseField("expiration"));
+ }
+
+ public CreateApiKeyRequestBuilder(ElasticsearchClient client) {
+ super(client, CreateApiKeyAction.INSTANCE, new CreateApiKeyRequest());
+ }
+
+ public CreateApiKeyRequestBuilder setName(String name) {
+ request.setName(name);
+ return this;
+ }
+
+ public CreateApiKeyRequestBuilder setExpiration(TimeValue expiration) {
+ request.setExpiration(expiration);
+ return this;
+ }
+
+ public CreateApiKeyRequestBuilder setRoleDescriptors(List roleDescriptors) {
+ request.setRoleDescriptors(roleDescriptors);
+ return this;
+ }
+
+ public CreateApiKeyRequestBuilder setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) {
+ request.setRefreshPolicy(refreshPolicy);
+ return this;
+ }
+
+ public CreateApiKeyRequestBuilder source(BytesReference source, XContentType xContentType) throws IOException {
+ final NamedXContentRegistry registry = NamedXContentRegistry.EMPTY;
+ try (InputStream stream = source.streamInput();
+ XContentParser parser = xContentType.xContent().createParser(registry, LoggingDeprecationHandler.INSTANCE, stream)) {
+ CreateApiKeyRequest createApiKeyRequest = PARSER.parse(parser, null);
+ setName(createApiKeyRequest.getName());
+ setRoleDescriptors(createApiKeyRequest.getRoleDescriptors());
+ setExpiration(createApiKeyRequest.getExpiration());
+ }
+ return this;
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java
new file mode 100644
index 0000000000000..a774413c3c4a2
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponse.java
@@ -0,0 +1,168 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.CharArrays;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+/**
+ * Response for the successful creation of an api key
+ */
+public final class CreateApiKeyResponse extends ActionResponse implements ToXContentObject {
+
+ static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("create_api_key_response",
+ args -> new CreateApiKeyResponse((String) args[0], (String) args[1], new SecureString((String) args[2]),
+ (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3])));
+ static {
+ PARSER.declareString(constructorArg(), new ParseField("name"));
+ PARSER.declareString(constructorArg(), new ParseField("id"));
+ PARSER.declareString(constructorArg(), new ParseField("api_key"));
+ PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration"));
+ }
+
+ private final String name;
+ private final String id;
+ private final SecureString key;
+ private final Instant expiration;
+
+ public CreateApiKeyResponse(String name, String id, SecureString key, Instant expiration) {
+ this.name = name;
+ this.id = id;
+ this.key = key;
+ // As we do not yet support the nanosecond precision when we serialize to JSON,
+ // here creating the 'Instant' of milliseconds precision.
+ // This Instant can then be used for date comparison.
+ this.expiration = (expiration != null) ? Instant.ofEpochMilli(expiration.toEpochMilli()): null;
+ }
+
+ public CreateApiKeyResponse(StreamInput in) throws IOException {
+ super(in);
+ this.name = in.readString();
+ this.id = in.readString();
+ byte[] bytes = null;
+ try {
+ bytes = in.readByteArray();
+ this.key = new SecureString(CharArrays.utf8BytesToChars(bytes));
+ } finally {
+ if (bytes != null) {
+ Arrays.fill(bytes, (byte) 0);
+ }
+ }
+ this.expiration = in.readOptionalInstant();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public SecureString getKey() {
+ return key;
+ }
+
+ @Nullable
+ public Instant getExpiration() {
+ return expiration;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((expiration == null) ? 0 : expiration.hashCode());
+ result = prime * result + Objects.hash(id, name, key);
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ final CreateApiKeyResponse other = (CreateApiKeyResponse) obj;
+ if (expiration == null) {
+ if (other.expiration != null)
+ return false;
+ } else if (!Objects.equals(expiration, other.expiration))
+ return false;
+ return Objects.equals(id, other.id)
+ && Objects.equals(key, other.key)
+ && Objects.equals(name, other.name);
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeString(name);
+ out.writeString(id);
+ byte[] bytes = null;
+ try {
+ bytes = CharArrays.toUtf8Bytes(key.getChars());
+ out.writeByteArray(bytes);
+ } finally {
+ if (bytes != null) {
+ Arrays.fill(bytes, (byte) 0);
+ }
+ }
+ out.writeOptionalInstant(expiration);
+ }
+
+ @Override
+ public void readFrom(StreamInput in) {
+ throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
+ }
+
+ public static CreateApiKeyResponse fromXContent(XContentParser parser) throws IOException {
+ return PARSER.parse(parser, null);
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject()
+ .field("id", id)
+ .field("name", name);
+ if (expiration != null) {
+ builder.field("expiration", expiration.toEpochMilli());
+ }
+ byte[] charBytes = CharArrays.toUtf8Bytes(key.getChars());
+ try {
+ builder.field("api_key").utf8Value(charBytes, 0, charBytes.length);
+ } finally {
+ Arrays.fill(charBytes, (byte) 0);
+ }
+ return builder.endObject();
+ }
+
+ @Override
+ public String toString() {
+ return "CreateApiKeyResponse [name=" + name + ", id=" + id + ", expiration=" + expiration + "]";
+ }
+
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyAction.java
new file mode 100644
index 0000000000000..6729a23618ee6
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyAction.java
@@ -0,0 +1,39 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.Action;
+import org.elasticsearch.client.ElasticsearchClient;
+import org.elasticsearch.common.io.stream.Writeable;
+
+/**
+ * Action for retrieving API key(s)
+ */
+public final class GetApiKeyAction extends Action {
+
+ public static final String NAME = "cluster:admin/xpack/security/api_key/get";
+ public static final GetApiKeyAction INSTANCE = new GetApiKeyAction();
+
+ private GetApiKeyAction() {
+ super(NAME);
+ }
+
+ @Override
+ public GetApiKeyResponse newResponse() {
+ throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
+ }
+
+ @Override
+ public Writeable.Reader getResponseReader() {
+ return GetApiKeyResponse::new;
+ }
+
+ @Override
+ public GetApiKeyRequestBuilder newRequestBuilder(ElasticsearchClient client) {
+ return new GetApiKeyRequestBuilder(client);
+ }
+}
\ No newline at end of file
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequest.java
new file mode 100644
index 0000000000000..819a64151d20a
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+
+import java.io.IOException;
+
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+
+/**
+ * Request for get API key
+ */
+public final class GetApiKeyRequest extends ActionRequest {
+
+ private String realmName;
+ private String userName;
+ private String apiKeyId;
+ private String apiKeyName;
+
+ public GetApiKeyRequest() {
+ this(null, null, null, null);
+ }
+
+ public GetApiKeyRequest(StreamInput in) throws IOException {
+ super(in);
+ realmName = in.readOptionalString();
+ userName = in.readOptionalString();
+ apiKeyId = in.readOptionalString();
+ apiKeyName = in.readOptionalString();
+ }
+
+ public GetApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String apiKeyId,
+ @Nullable String apiKeyName) {
+ this.realmName = realmName;
+ this.userName = userName;
+ this.apiKeyId = apiKeyId;
+ this.apiKeyName = apiKeyName;
+ }
+
+ public String getRealmName() {
+ return realmName;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public String getApiKeyId() {
+ return apiKeyId;
+ }
+
+ public String getApiKeyName() {
+ return apiKeyName;
+ }
+
+ /**
+ * Creates get API key request for given realm name
+ * @param realmName realm name
+ * @return {@link GetApiKeyRequest}
+ */
+ public static GetApiKeyRequest usingRealmName(String realmName) {
+ return new GetApiKeyRequest(realmName, null, null, null);
+ }
+
+ /**
+ * Creates get API key request for given user name
+ * @param userName user name
+ * @return {@link GetApiKeyRequest}
+ */
+ public static GetApiKeyRequest usingUserName(String userName) {
+ return new GetApiKeyRequest(null, userName, null, null);
+ }
+
+ /**
+ * Creates get API key request for given realm and user name
+ * @param realmName realm name
+ * @param userName user name
+ * @return {@link GetApiKeyRequest}
+ */
+ public static GetApiKeyRequest usingRealmAndUserName(String realmName, String userName) {
+ return new GetApiKeyRequest(realmName, userName, null, null);
+ }
+
+ /**
+ * Creates get API key request for given api key id
+ * @param apiKeyId api key id
+ * @return {@link GetApiKeyRequest}
+ */
+ public static GetApiKeyRequest usingApiKeyId(String apiKeyId) {
+ return new GetApiKeyRequest(null, null, apiKeyId, null);
+ }
+
+ /**
+ * Creates get api key request for given api key name
+ * @param apiKeyName api key name
+ * @return {@link GetApiKeyRequest}
+ */
+ public static GetApiKeyRequest usingApiKeyName(String apiKeyName) {
+ return new GetApiKeyRequest(null, null, null, apiKeyName);
+ }
+
+ @Override
+ public ActionRequestValidationException validate() {
+ ActionRequestValidationException validationException = null;
+ if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false
+ && Strings.hasText(apiKeyName) == false) {
+ validationException = addValidationError("One of [api key id, api key name, username, realm name] must be specified",
+ validationException);
+ }
+ if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) {
+ if (Strings.hasText(realmName) || Strings.hasText(userName)) {
+ validationException = addValidationError(
+ "username or realm name must not be specified when the api key id or api key name is specified",
+ validationException);
+ }
+ }
+ if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) {
+ validationException = addValidationError("only one of [api key id, api key name] can be specified", validationException);
+ }
+ return validationException;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeOptionalString(realmName);
+ out.writeOptionalString(userName);
+ out.writeOptionalString(apiKeyId);
+ out.writeOptionalString(apiKeyName);
+ }
+
+ @Override
+ public void readFrom(StreamInput in) throws IOException {
+ super.readFrom(in);
+ realmName = in.readOptionalString();
+ userName = in.readOptionalString();
+ apiKeyId = in.readOptionalString();
+ apiKeyName = in.readOptionalString();
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestBuilder.java
new file mode 100644
index 0000000000000..9a8b0b2910f55
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestBuilder.java
@@ -0,0 +1,21 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionRequestBuilder;
+import org.elasticsearch.client.ElasticsearchClient;
+
+/**
+ * Request builder for populating a {@link GetApiKeyRequest}
+ */
+public class GetApiKeyRequestBuilder extends ActionRequestBuilder {
+
+ protected GetApiKeyRequestBuilder(ElasticsearchClient client) {
+ super(client, GetApiKeyAction.INSTANCE, new GetApiKeyRequest());
+ }
+
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java
new file mode 100644
index 0000000000000..97b8f380f6940
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponse.java
@@ -0,0 +1,88 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+/**
+ * Response for get API keys.
+ * The result contains information about the API keys that were found.
+ */
+public final class GetApiKeyResponse extends ActionResponse implements ToXContentObject, Writeable {
+
+ private final ApiKey[] foundApiKeysInfo;
+
+ public GetApiKeyResponse(StreamInput in) throws IOException {
+ super(in);
+ this.foundApiKeysInfo = in.readArray(ApiKey::new, ApiKey[]::new);
+ }
+
+ public GetApiKeyResponse(Collection foundApiKeysInfo) {
+ Objects.requireNonNull(foundApiKeysInfo, "found_api_keys_info must be provided");
+ this.foundApiKeysInfo = foundApiKeysInfo.toArray(new ApiKey[0]);
+ }
+
+ public static GetApiKeyResponse emptyResponse() {
+ return new GetApiKeyResponse(Collections.emptyList());
+ }
+
+ public ApiKey[] getApiKeyInfos() {
+ return foundApiKeysInfo;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject()
+ .array("api_keys", (Object[]) foundApiKeysInfo);
+ return builder.endObject();
+ }
+
+ @Override
+ public void readFrom(StreamInput in) throws IOException {
+ throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeArray(foundApiKeysInfo);
+ }
+
+ @SuppressWarnings("unchecked")
+ static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_api_key_response", args -> {
+ return (args[0] == null) ? GetApiKeyResponse.emptyResponse() : new GetApiKeyResponse((List) args[0]);
+ });
+ static {
+ PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ApiKey.fromXContent(p), new ParseField("api_keys"));
+ }
+
+ public static GetApiKeyResponse fromXContent(XContentParser parser) throws IOException {
+ return PARSER.parse(parser, null);
+ }
+
+ @Override
+ public String toString() {
+ return "GetApiKeyResponse [foundApiKeysInfo=" + foundApiKeysInfo + "]";
+ }
+
+}
\ No newline at end of file
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyAction.java
new file mode 100644
index 0000000000000..9cac055ed9351
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyAction.java
@@ -0,0 +1,40 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.Action;
+import org.elasticsearch.client.ElasticsearchClient;
+import org.elasticsearch.common.io.stream.Writeable;
+
+/**
+ * Action for invalidating API key
+ */
+public final class InvalidateApiKeyAction
+ extends Action {
+
+ public static final String NAME = "cluster:admin/xpack/security/api_key/invalidate";
+ public static final InvalidateApiKeyAction INSTANCE = new InvalidateApiKeyAction();
+
+ private InvalidateApiKeyAction() {
+ super(NAME);
+ }
+
+ @Override
+ public InvalidateApiKeyResponse newResponse() {
+ throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
+ }
+
+ @Override
+ public Writeable.Reader getResponseReader() {
+ return InvalidateApiKeyResponse::new;
+ }
+
+ @Override
+ public InvalidateApiKeyRequestBuilder newRequestBuilder(ElasticsearchClient client) {
+ return new InvalidateApiKeyRequestBuilder(client);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequest.java
new file mode 100644
index 0000000000000..1f6939fa5a95c
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequest.java
@@ -0,0 +1,150 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+
+import java.io.IOException;
+
+import static org.elasticsearch.action.ValidateActions.addValidationError;
+
+/**
+ * Request for invalidating API key(s) so that it can no longer be used
+ */
+public final class InvalidateApiKeyRequest extends ActionRequest {
+
+ private String realmName;
+ private String userName;
+ private String id;
+ private String name;
+
+ public InvalidateApiKeyRequest() {
+ this(null, null, null, null);
+ }
+
+ public InvalidateApiKeyRequest(StreamInput in) throws IOException {
+ super(in);
+ realmName = in.readOptionalString();
+ userName = in.readOptionalString();
+ id = in.readOptionalString();
+ name = in.readOptionalString();
+ }
+
+ public InvalidateApiKeyRequest(@Nullable String realmName, @Nullable String userName, @Nullable String id,
+ @Nullable String name) {
+ this.realmName = realmName;
+ this.userName = userName;
+ this.id = id;
+ this.name = name;
+ }
+
+ public String getRealmName() {
+ return realmName;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Creates invalidate api key request for given realm name
+ * @param realmName realm name
+ * @return {@link InvalidateApiKeyRequest}
+ */
+ public static InvalidateApiKeyRequest usingRealmName(String realmName) {
+ return new InvalidateApiKeyRequest(realmName, null, null, null);
+ }
+
+ /**
+ * Creates invalidate API key request for given user name
+ * @param userName user name
+ * @return {@link InvalidateApiKeyRequest}
+ */
+ public static InvalidateApiKeyRequest usingUserName(String userName) {
+ return new InvalidateApiKeyRequest(null, userName, null, null);
+ }
+
+ /**
+ * Creates invalidate API key request for given realm and user name
+ * @param realmName realm name
+ * @param userName user name
+ * @return {@link InvalidateApiKeyRequest}
+ */
+ public static InvalidateApiKeyRequest usingRealmAndUserName(String realmName, String userName) {
+ return new InvalidateApiKeyRequest(realmName, userName, null, null);
+ }
+
+ /**
+ * Creates invalidate API key request for given api key id
+ * @param id api key id
+ * @return {@link InvalidateApiKeyRequest}
+ */
+ public static InvalidateApiKeyRequest usingApiKeyId(String id) {
+ return new InvalidateApiKeyRequest(null, null, id, null);
+ }
+
+ /**
+ * Creates invalidate api key request for given api key name
+ * @param name api key name
+ * @return {@link InvalidateApiKeyRequest}
+ */
+ public static InvalidateApiKeyRequest usingApiKeyName(String name) {
+ return new InvalidateApiKeyRequest(null, null, null, name);
+ }
+
+ @Override
+ public ActionRequestValidationException validate() {
+ ActionRequestValidationException validationException = null;
+ if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(id) == false
+ && Strings.hasText(name) == false) {
+ validationException = addValidationError("One of [api key id, api key name, username, realm name] must be specified",
+ validationException);
+ }
+ if (Strings.hasText(id) || Strings.hasText(name)) {
+ if (Strings.hasText(realmName) || Strings.hasText(userName)) {
+ validationException = addValidationError(
+ "username or realm name must not be specified when the api key id or api key name is specified",
+ validationException);
+ }
+ }
+ if (Strings.hasText(id) && Strings.hasText(name)) {
+ validationException = addValidationError("only one of [api key id, api key name] can be specified", validationException);
+ }
+ return validationException;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeOptionalString(realmName);
+ out.writeOptionalString(userName);
+ out.writeOptionalString(id);
+ out.writeOptionalString(name);
+ }
+
+ @Override
+ public void readFrom(StreamInput in) throws IOException {
+ super.readFrom(in);
+ realmName = in.readOptionalString();
+ userName = in.readOptionalString();
+ id = in.readOptionalString();
+ name = in.readOptionalString();
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestBuilder.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestBuilder.java
new file mode 100644
index 0000000000000..cb71c91e5ecef
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestBuilder.java
@@ -0,0 +1,22 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionRequestBuilder;
+import org.elasticsearch.client.ElasticsearchClient;
+
+/**
+ * Request builder for populating a {@link InvalidateApiKeyRequest}
+ */
+public final class InvalidateApiKeyRequestBuilder
+ extends ActionRequestBuilder {
+
+ protected InvalidateApiKeyRequestBuilder(ElasticsearchClient client) {
+ super(client, InvalidateApiKeyAction.INSTANCE, new InvalidateApiKeyRequest());
+ }
+
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java
new file mode 100644
index 0000000000000..e9580c93d9086
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponse.java
@@ -0,0 +1,141 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.common.Nullable;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
+import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
+
+/**
+ * Response for invalidation of one or more API keys result.
+ * The result contains information about:
+ *
+ * - API key ids that were actually invalidated
+ * - API key ids that were not invalidated in this request because they were already invalidated
+ * - how many errors were encountered while invalidating API keys and the error details
+ *
+ */
+public final class InvalidateApiKeyResponse extends ActionResponse implements ToXContentObject, Writeable {
+
+ private final List invalidatedApiKeys;
+ private final List previouslyInvalidatedApiKeys;
+ private final List errors;
+
+ public InvalidateApiKeyResponse(StreamInput in) throws IOException {
+ super(in);
+ this.invalidatedApiKeys = in.readList(StreamInput::readString);
+ this.previouslyInvalidatedApiKeys = in.readList(StreamInput::readString);
+ this.errors = in.readList(StreamInput::readException);
+ }
+
+ /**
+ * Constructor for API keys invalidation response
+ * @param invalidatedApiKeys list of invalidated API key ids
+ * @param previouslyInvalidatedApiKeys list of previously invalidated API key ids
+ * @param errors list of encountered errors while invalidating API keys
+ */
+ public InvalidateApiKeyResponse(List invalidatedApiKeys, List previouslyInvalidatedApiKeys,
+ @Nullable List errors) {
+ this.invalidatedApiKeys = Objects.requireNonNull(invalidatedApiKeys, "invalidated_api_keys must be provided");
+ this.previouslyInvalidatedApiKeys = Objects.requireNonNull(previouslyInvalidatedApiKeys,
+ "previously_invalidated_api_keys must be provided");
+ if (null != errors) {
+ this.errors = errors;
+ } else {
+ this.errors = Collections.emptyList();
+ }
+ }
+
+ public static InvalidateApiKeyResponse emptyResponse() {
+ return new InvalidateApiKeyResponse(Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
+ }
+
+ public List getInvalidatedApiKeys() {
+ return invalidatedApiKeys;
+ }
+
+ public List getPreviouslyInvalidatedApiKeys() {
+ return previouslyInvalidatedApiKeys;
+ }
+
+ public List getErrors() {
+ return errors;
+ }
+
+ @Override
+ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+ builder.startObject()
+ .array("invalidated_api_keys", invalidatedApiKeys.toArray(Strings.EMPTY_ARRAY))
+ .array("previously_invalidated_api_keys", previouslyInvalidatedApiKeys.toArray(Strings.EMPTY_ARRAY))
+ .field("error_count", errors.size());
+ if (errors.isEmpty() == false) {
+ builder.field("error_details");
+ builder.startArray();
+ for (ElasticsearchException e : errors) {
+ builder.startObject();
+ ElasticsearchException.generateThrowableXContent(builder, params, e);
+ builder.endObject();
+ }
+ builder.endArray();
+ }
+ return builder.endObject();
+ }
+
+ @Override
+ public void readFrom(StreamInput in) throws IOException {
+ throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable");
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeStringCollection(invalidatedApiKeys);
+ out.writeStringCollection(previouslyInvalidatedApiKeys);
+ out.writeCollection(errors, StreamOutput::writeException);
+ }
+
+ static ConstructingObjectParser PARSER = new ConstructingObjectParser<>("invalidate_api_key_response",
+ args -> {
+ return new InvalidateApiKeyResponse((List) args[0], (List) args[1], (List) args[3]);
+ });
+ static {
+ PARSER.declareStringArray(constructorArg(), new ParseField("invalidated_api_keys"));
+ PARSER.declareStringArray(constructorArg(), new ParseField("previously_invalidated_api_keys"));
+ // we parse error_count but ignore it while constructing response
+ PARSER.declareInt(constructorArg(), new ParseField("error_count"));
+ PARSER.declareObjectArray(optionalConstructorArg(), (p, c) -> ElasticsearchException.fromXContent(p),
+ new ParseField("error_details"));
+ }
+
+ public static InvalidateApiKeyResponse fromXContent(XContentParser parser) throws IOException {
+ return PARSER.parse(parser, null);
+ }
+
+ @Override
+ public String toString() {
+ return "InvalidateApiKeyResponse [invalidatedApiKeys=" + invalidatedApiKeys + ", previouslyInvalidatedApiKeys="
+ + previouslyInvalidatedApiKeys + ", errors=" + errors + "]";
+ }
+
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java
index 93c9d6bca9b64..27079eebcc36b 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/role/GetRolesResponse.java
@@ -37,7 +37,7 @@ public void readFrom(StreamInput in) throws IOException {
int size = in.readVInt();
roles = new RoleDescriptor[size];
for (int i = 0; i < size; i++) {
- roles[i] = RoleDescriptor.readFrom(in);
+ roles[i] = new RoleDescriptor(in);
}
}
@@ -46,7 +46,7 @@ public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeVInt(roles.length);
for (RoleDescriptor role : roles) {
- RoleDescriptor.writeTo(role, out);
+ role.writeTo(out);
}
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java
index 71aafbb76187a..2885e702944ed 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponse.java
@@ -11,6 +11,7 @@
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges;
import java.io.IOException;
import java.util.Collection;
@@ -49,7 +50,7 @@ public HasPrivilegesResponse(String username, boolean completeMatch, Map sorted(Collection resources) {
- final Set set = new TreeSet<>(Comparator.comparing(o -> o.resource));
+ final Set set = new TreeSet<>(Comparator.comparing(o -> o.getResource()));
set.addAll(resources);
return set;
}
@@ -116,11 +117,11 @@ public void readFrom(StreamInput in) throws IOException {
private static Set readResourcePrivileges(StreamInput in) throws IOException {
final int count = in.readVInt();
- final Set set = new TreeSet<>(Comparator.comparing(o -> o.resource));
+ final Set set = new TreeSet<>(Comparator.comparing(o -> o.getResource()));
for (int i = 0; i < count; i++) {
final String index = in.readString();
final Map privileges = in.readMap(StreamInput::readString, StreamInput::readBoolean);
- set.add(new ResourcePrivileges(index, privileges));
+ set.add(ResourcePrivileges.builder(index).addPrivileges(privileges).build());
}
return set;
}
@@ -144,8 +145,8 @@ public void writeTo(StreamOutput out) throws IOException {
private static void writeResourcePrivileges(StreamOutput out, Set privileges) throws IOException {
out.writeVInt(privileges.size());
for (ResourcePrivileges priv : privileges) {
- out.writeString(priv.resource);
- out.writeMap(priv.privileges, StreamOutput::writeString, StreamOutput::writeBoolean);
+ out.writeString(priv.getResource());
+ out.writeMap(priv.getPrivileges(), StreamOutput::writeString, StreamOutput::writeBoolean);
}
}
@@ -181,60 +182,14 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
return builder;
}
- private void appendResources(XContentBuilder builder, String field, Set privileges)
+ private void appendResources(XContentBuilder builder, String field, Set privileges)
throws IOException {
builder.startObject(field);
- for (HasPrivilegesResponse.ResourcePrivileges privilege : privileges) {
+ for (ResourcePrivileges privilege : privileges) {
builder.field(privilege.getResource());
builder.map(privilege.getPrivileges());
}
builder.endObject();
}
-
- public static class ResourcePrivileges {
- private final String resource;
- private final Map privileges;
-
- public ResourcePrivileges(String resource, Map privileges) {
- this.resource = Objects.requireNonNull(resource);
- this.privileges = Collections.unmodifiableMap(privileges);
- }
-
- public String getResource() {
- return resource;
- }
-
- public Map getPrivileges() {
- return privileges;
- }
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + "{" +
- "resource='" + resource + '\'' +
- ", privileges=" + privileges +
- '}';
- }
-
- @Override
- public int hashCode() {
- int result = resource.hashCode();
- result = 31 * result + privileges.hashCode();
- return result;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
-
- final ResourcePrivileges other = (ResourcePrivileges) o;
- return this.resource.equals(other.resource) && this.privileges.equals(other.privileges);
- }
- }
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java
index b9dbe0a948ff2..a18c35c651e52 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Authentication.java
@@ -18,6 +18,8 @@
import java.io.IOException;
import java.util.Base64;
+import java.util.Collections;
+import java.util.Map;
import java.util.Objects;
// TODO(hub-cap) Clean this up after moving User over - This class can re-inherit its field AUTHENTICATION_KEY in AuthenticationField.
@@ -28,16 +30,25 @@ public class Authentication implements ToXContentObject {
private final RealmRef authenticatedBy;
private final RealmRef lookedUpBy;
private final Version version;
+ private final AuthenticationType type;
+ private final Map metadata;
public Authentication(User user, RealmRef authenticatedBy, RealmRef lookedUpBy) {
this(user, authenticatedBy, lookedUpBy, Version.CURRENT);
}
public Authentication(User user, RealmRef authenticatedBy, RealmRef lookedUpBy, Version version) {
+ this(user, authenticatedBy, lookedUpBy, version, AuthenticationType.REALM, Collections.emptyMap());
+ }
+
+ public Authentication(User user, RealmRef authenticatedBy, RealmRef lookedUpBy, Version version,
+ AuthenticationType type, Map metadata) {
this.user = Objects.requireNonNull(user);
this.authenticatedBy = Objects.requireNonNull(authenticatedBy);
this.lookedUpBy = lookedUpBy;
this.version = version;
+ this.type = type;
+ this.metadata = metadata;
}
public Authentication(StreamInput in) throws IOException {
@@ -49,6 +60,13 @@ public Authentication(StreamInput in) throws IOException {
this.lookedUpBy = null;
}
this.version = in.getVersion();
+ if (in.getVersion().onOrAfter(Version.V_6_6_0)) {
+ type = AuthenticationType.values()[in.readVInt()];
+ metadata = in.readMap();
+ } else {
+ type = AuthenticationType.REALM;
+ metadata = Collections.emptyMap();
+ }
}
public User getUser() {
@@ -67,8 +85,15 @@ public Version getVersion() {
return version;
}
- public static Authentication readFromContext(ThreadContext ctx)
- throws IOException, IllegalArgumentException {
+ public AuthenticationType getAuthenticationType() {
+ return type;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public static Authentication readFromContext(ThreadContext ctx) throws IOException, IllegalArgumentException {
Authentication authentication = ctx.getTransient(AuthenticationField.AUTHENTICATION_KEY);
if (authentication != null) {
assert ctx.getHeader(AuthenticationField.AUTHENTICATION_KEY) != null;
@@ -107,8 +132,7 @@ public static Authentication decode(String header) throws IOException {
* Writes the authentication to the context. There must not be an existing authentication in the context and if there is an
* {@link IllegalStateException} will be thrown
*/
- public void writeToContext(ThreadContext ctx)
- throws IOException, IllegalArgumentException {
+ public void writeToContext(ThreadContext ctx) throws IOException, IllegalArgumentException {
ensureContextDoesNotContainAuthentication(ctx);
String header = encode();
ctx.putTransient(AuthenticationField.AUTHENTICATION_KEY, this);
@@ -141,28 +165,28 @@ public void writeTo(StreamOutput out) throws IOException {
} else {
out.writeBoolean(false);
}
+ if (out.getVersion().onOrAfter(Version.V_6_6_0)) {
+ out.writeVInt(type.ordinal());
+ out.writeMap(metadata);
+ }
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
-
Authentication that = (Authentication) o;
-
- if (!user.equals(that.user)) return false;
- if (!authenticatedBy.equals(that.authenticatedBy)) return false;
- if (lookedUpBy != null ? !lookedUpBy.equals(that.lookedUpBy) : that.lookedUpBy != null) return false;
- return version.equals(that.version);
+ return user.equals(that.user) &&
+ authenticatedBy.equals(that.authenticatedBy) &&
+ Objects.equals(lookedUpBy, that.lookedUpBy) &&
+ version.equals(that.version) &&
+ type == that.type &&
+ metadata.equals(that.metadata);
}
@Override
public int hashCode() {
- int result = user.hashCode();
- result = 31 * result + authenticatedBy.hashCode();
- result = 31 * result + (lookedUpBy != null ? lookedUpBy.hashCode() : 0);
- result = 31 * result + version.hashCode();
- return result;
+ return Objects.hash(user, authenticatedBy, lookedUpBy, version, type, metadata);
}
@Override
@@ -246,5 +270,13 @@ public int hashCode() {
return result;
}
}
+
+ public enum AuthenticationType {
+ REALM,
+ API_KEY,
+ TOKEN,
+ ANONYMOUS,
+ INTERNAL
+ }
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java
index 736b9378e3876..a7c51b206db9c 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java
@@ -77,10 +77,12 @@ private static Integer authSchemePriority(final String headerValue) {
return 0;
} else if (headerValue.regionMatches(true, 0, "bearer", 0, "bearer".length())) {
return 1;
- } else if (headerValue.regionMatches(true, 0, "basic", 0, "basic".length())) {
+ } else if (headerValue.regionMatches(true, 0, "apikey", 0, "apikey".length())) {
return 2;
- } else {
+ } else if (headerValue.regionMatches(true, 0, "basic", 0, "basic".length())) {
return 3;
+ } else {
+ return 4;
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java
index 7472a510c38b2..dc506881b1db1 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java
@@ -43,7 +43,7 @@
* A holder for a Role that contains user-readable information about the Role
* without containing the actual Role object.
*/
-public class RoleDescriptor implements ToXContentObject {
+public class RoleDescriptor implements ToXContentObject, Writeable {
public static final String ROLE_TYPE = "role";
@@ -110,6 +110,27 @@ public RoleDescriptor(String name,
Collections.singletonMap("enabled", true);
}
+ public RoleDescriptor(StreamInput in) throws IOException {
+ this.name = in.readString();
+ this.clusterPrivileges = in.readStringArray();
+ int size = in.readVInt();
+ this.indicesPrivileges = new IndicesPrivileges[size];
+ for (int i = 0; i < size; i++) {
+ indicesPrivileges[i] = new IndicesPrivileges(in);
+ }
+ this.runAs = in.readStringArray();
+ this.metadata = in.readMap();
+ this.transientMetadata = in.readMap();
+
+ if (in.getVersion().onOrAfter(Version.V_6_4_0)) {
+ this.applicationPrivileges = in.readArray(ApplicationResourcePrivileges::new, ApplicationResourcePrivileges[]::new);
+ this.conditionalClusterPrivileges = ConditionalClusterPrivileges.readArray(in);
+ } else {
+ this.applicationPrivileges = ApplicationResourcePrivileges.NONE;
+ this.conditionalClusterPrivileges = ConditionalClusterPrivileges.EMPTY_ARRAY;
+ }
+ }
+
public String getName() {
return this.name;
}
@@ -264,21 +285,20 @@ public static RoleDescriptor readFrom(StreamInput in) throws IOException {
runAs, metadata, transientMetadata);
}
- public static void writeTo(RoleDescriptor descriptor, StreamOutput out) throws IOException {
- out.writeString(descriptor.name);
- out.writeStringArray(descriptor.clusterPrivileges);
- out.writeVInt(descriptor.indicesPrivileges.length);
- for (IndicesPrivileges group : descriptor.indicesPrivileges) {
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ out.writeString(name);
+ out.writeStringArray(clusterPrivileges);
+ out.writeVInt(indicesPrivileges.length);
+ for (IndicesPrivileges group : indicesPrivileges) {
group.writeTo(out);
}
- out.writeStringArray(descriptor.runAs);
- out.writeMap(descriptor.metadata);
- if (out.getVersion().onOrAfter(Version.V_5_2_0)) {
- out.writeMap(descriptor.transientMetadata);
- }
+ out.writeStringArray(runAs);
+ out.writeMap(metadata);
+ out.writeMap(transientMetadata);
if (out.getVersion().onOrAfter(Version.V_6_4_0)) {
- out.writeArray(ApplicationResourcePrivileges::write, descriptor.applicationPrivileges);
- ConditionalClusterPrivileges.writeArray(out, descriptor.getConditionalClusterPrivileges());
+ out.writeArray(ApplicationResourcePrivileges::write, applicationPrivileges);
+ ConditionalClusterPrivileges.writeArray(out, getConditionalClusterPrivileges());
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java
index 6df9ad834c1e5..8cdf099e676d8 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/IndicesAccessControl.java
@@ -6,11 +6,13 @@
package org.elasticsearch.xpack.core.security.authz.accesscontrol;
import org.elasticsearch.common.Nullable;
-import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.xpack.core.security.authz.IndicesAndAliasesResolverField;
+import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions;
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -22,7 +24,7 @@ public class IndicesAccessControl {
public static final IndicesAccessControl ALLOW_ALL = new IndicesAccessControl(true, Collections.emptyMap());
public static final IndicesAccessControl ALLOW_NO_INDICES = new IndicesAccessControl(true,
Collections.singletonMap(IndicesAndAliasesResolverField.NO_INDEX_PLACEHOLDER,
- new IndicesAccessControl.IndexAccessControl(true, new FieldPermissions(), null)));
+ new IndicesAccessControl.IndexAccessControl(true, new FieldPermissions(), DocumentPermissions.allowAll())));
private final boolean granted;
private final Map indexPermissions;
@@ -55,12 +57,12 @@ public static class IndexAccessControl {
private final boolean granted;
private final FieldPermissions fieldPermissions;
- private final Set queries;
+ private final DocumentPermissions documentPermissions;
- public IndexAccessControl(boolean granted, FieldPermissions fieldPermissions, Set queries) {
+ public IndexAccessControl(boolean granted, FieldPermissions fieldPermissions, DocumentPermissions documentPermissions) {
this.granted = granted;
- this.fieldPermissions = fieldPermissions;
- this.queries = queries;
+ this.fieldPermissions = (fieldPermissions == null) ? FieldPermissions.DEFAULT : fieldPermissions;
+ this.documentPermissions = (documentPermissions == null) ? DocumentPermissions.allowAll() : documentPermissions;
}
/**
@@ -82,8 +84,33 @@ public FieldPermissions getFieldPermissions() {
* then this means that there are no document level restrictions
*/
@Nullable
- public Set getQueries() {
- return queries;
+ public DocumentPermissions getDocumentPermissions() {
+ return documentPermissions;
+ }
+
+ /**
+ * Returns a instance of {@link IndexAccessControl}, where the privileges for {@code this} object are constrained by the privileges
+ * contained in the provided parameter.
+ * Allowed fields for this index permission would be an intersection of allowed fields.
+ * Allowed documents for this index permission would be an intersection of allowed documents.
+ *
+ * @param limitedByIndexAccessControl {@link IndexAccessControl}
+ * @return {@link IndexAccessControl}
+ * @see FieldPermissions#limitFieldPermissions(FieldPermissions)
+ * @see DocumentPermissions#limitDocumentPermissions(DocumentPermissions)
+ */
+ public IndexAccessControl limitIndexAccessControl(IndexAccessControl limitedByIndexAccessControl) {
+ final boolean granted;
+ if (this.granted == limitedByIndexAccessControl.granted) {
+ granted = this.granted;
+ } else {
+ granted = false;
+ }
+ FieldPermissions fieldPermissions = getFieldPermissions().limitFieldPermissions(
+ limitedByIndexAccessControl.fieldPermissions);
+ DocumentPermissions documentPermissions = getDocumentPermissions()
+ .limitDocumentPermissions(limitedByIndexAccessControl.getDocumentPermissions());
+ return new IndexAccessControl(granted, fieldPermissions, documentPermissions);
}
@Override
@@ -91,11 +118,38 @@ public String toString() {
return "IndexAccessControl{" +
"granted=" + granted +
", fieldPermissions=" + fieldPermissions +
- ", queries=" + queries +
+ ", documentPermissions=" + documentPermissions +
'}';
}
}
+ /**
+ * Returns a instance of {@link IndicesAccessControl}, where the privileges for {@code this}
+ * object are constrained by the privileges contained in the provided parameter.
+ *
+ * @param limitedByIndicesAccessControl {@link IndicesAccessControl}
+ * @return {@link IndicesAccessControl}
+ */
+ public IndicesAccessControl limitIndicesAccessControl(IndicesAccessControl limitedByIndicesAccessControl) {
+ final boolean granted;
+ if (this.granted == limitedByIndicesAccessControl.granted) {
+ granted = this.granted;
+ } else {
+ granted = false;
+ }
+ Set indexes = indexPermissions.keySet();
+ Set otherIndexes = limitedByIndicesAccessControl.indexPermissions.keySet();
+ Set commonIndexes = Sets.intersection(indexes, otherIndexes);
+
+ Map indexPermissions = new HashMap<>(commonIndexes.size());
+ for (String index : commonIndexes) {
+ IndexAccessControl indexAccessControl = getIndexPermissions(index);
+ IndexAccessControl limitedByIndexAccessControl = limitedByIndicesAccessControl.getIndexPermissions(index);
+ indexPermissions.put(index, indexAccessControl.limitIndexAccessControl(limitedByIndexAccessControl));
+ }
+ return new IndicesAccessControl(granted, indexPermissions);
+ }
+
@Override
public String toString() {
return "IndicesAccessControl{" +
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java
index 997db63c47423..1169cfff5794c 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexSearcherWrapper.java
@@ -5,8 +5,8 @@
*/
package org.elasticsearch.xpack.core.security.authz.accesscontrol;
-import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.search.BooleanQuery;
@@ -18,64 +18,35 @@
import org.apache.lucene.search.DocIdSetIterator;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.LeafCollector;
-import org.apache.lucene.search.Query;
import org.apache.lucene.search.Scorer;
import org.apache.lucene.search.Weight;
-import org.apache.lucene.search.join.BitSetProducer;
-import org.apache.lucene.search.join.ToChildBlockJoinQuery;
import org.apache.lucene.util.BitSet;
import org.apache.lucene.util.BitSetIterator;
import org.apache.lucene.util.Bits;
import org.apache.lucene.util.SparseFixedBitSet;
-import org.elasticsearch.ElasticsearchParseException;
import org.elasticsearch.ExceptionsHelper;
-import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.logging.LoggerMessageFormat;
-import org.elasticsearch.common.lucene.search.Queries;
import org.elasticsearch.common.util.concurrent.ThreadContext;
-import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
-import org.elasticsearch.common.xcontent.NamedXContentRegistry;
-import org.elasticsearch.common.xcontent.XContentFactory;
-import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.index.cache.bitset.BitsetFilterCache;
import org.elasticsearch.index.engine.EngineException;
-import org.elasticsearch.index.query.BoolQueryBuilder;
-import org.elasticsearch.index.query.BoostingQueryBuilder;
-import org.elasticsearch.index.query.ConstantScoreQueryBuilder;
-import org.elasticsearch.index.query.GeoShapeQueryBuilder;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.index.query.QueryRewriteContext;
import org.elasticsearch.index.query.QueryShardContext;
-import org.elasticsearch.index.query.Rewriteable;
-import org.elasticsearch.index.query.TermsQueryBuilder;
-import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
-import org.elasticsearch.index.search.NestedHelper;
import org.elasticsearch.index.shard.IndexSearcherWrapper;
import org.elasticsearch.index.shard.ShardId;
import org.elasticsearch.index.shard.ShardUtils;
import org.elasticsearch.license.XPackLicenseState;
-import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptService;
-import org.elasticsearch.script.ScriptType;
-import org.elasticsearch.script.TemplateScript;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.authz.AuthorizationServiceField;
import org.elasticsearch.xpack.core.security.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader;
+import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions;
import org.elasticsearch.xpack.core.security.support.Exceptions;
import org.elasticsearch.xpack.core.security.user.User;
import java.io.IOException;
-import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
import java.util.List;
-import java.util.Map;
import java.util.function.Function;
-import static org.apache.lucene.search.BooleanClause.Occur.FILTER;
-import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
-
/**
* An {@link IndexSearcherWrapper} implementation that is used for field and document level security.
*
@@ -107,7 +78,7 @@ public SecurityIndexSearcherWrapper(Function querySh
}
@Override
- protected DirectoryReader wrap(DirectoryReader reader) {
+ protected DirectoryReader wrap(final DirectoryReader reader) {
if (licenseState.isDocumentAndFieldLevelSecurityAllowed() == false) {
return reader;
}
@@ -120,47 +91,22 @@ protected DirectoryReader wrap(DirectoryReader reader) {
throw new IllegalStateException(LoggerMessageFormat.format("couldn't extract shardId from reader [{}]", reader));
}
- IndicesAccessControl.IndexAccessControl permissions = indicesAccessControl.getIndexPermissions(shardId.getIndexName());
+ final IndicesAccessControl.IndexAccessControl permissions = indicesAccessControl.getIndexPermissions(shardId.getIndexName());
// No permissions have been defined for an index, so don't intercept the index reader for access control
if (permissions == null) {
return reader;
}
- if (permissions.getQueries() != null) {
- BooleanQuery.Builder filter = new BooleanQuery.Builder();
- for (BytesReference bytesReference : permissions.getQueries()) {
- QueryShardContext queryShardContext = queryShardContextProvider.apply(shardId);
- String templateResult = evaluateTemplate(bytesReference.utf8ToString());
- try (XContentParser parser = XContentFactory.xContent(templateResult)
- .createParser(queryShardContext.getXContentRegistry(), LoggingDeprecationHandler.INSTANCE, templateResult)) {
- QueryBuilder queryBuilder = queryShardContext.parseInnerQueryBuilder(parser);
- verifyRoleQuery(queryBuilder);
- failIfQueryUsesClient(queryBuilder, queryShardContext);
- Query roleQuery = queryShardContext.toFilter(queryBuilder).query();
- filter.add(roleQuery, SHOULD);
- if (queryShardContext.getMapperService().hasNested()) {
- NestedHelper nestedHelper = new NestedHelper(queryShardContext.getMapperService());
- if (nestedHelper.mightMatchNestedDocs(roleQuery)) {
- roleQuery = new BooleanQuery.Builder()
- .add(roleQuery, FILTER)
- .add(Queries.newNonNestedFilter(queryShardContext.indexVersionCreated()), FILTER)
- .build();
- }
- // If access is allowed on root doc then also access is allowed on all nested docs of that root document:
- BitSetProducer rootDocs = queryShardContext.bitsetFilter(
- Queries.newNonNestedFilter(queryShardContext.indexVersionCreated()));
- ToChildBlockJoinQuery includeNestedDocs = new ToChildBlockJoinQuery(roleQuery, rootDocs);
- filter.add(includeNestedDocs, SHOULD);
- }
- }
+ DirectoryReader wrappedReader = reader;
+ DocumentPermissions documentPermissions = permissions.getDocumentPermissions();
+ if (documentPermissions != null && documentPermissions.hasDocumentLevelPermissions()) {
+ BooleanQuery filterQuery = documentPermissions.filter(getUser(), scriptService, shardId, queryShardContextProvider);
+ if (filterQuery != null) {
+ wrappedReader = DocumentSubsetReader.wrap(wrappedReader, bitsetFilterCache, new ConstantScoreQuery(filterQuery));
}
-
- // at least one of the queries should match
- filter.setMinimumNumberShouldMatch(1);
- reader = DocumentSubsetReader.wrap(reader, bitsetFilterCache, new ConstantScoreQuery(filter.build()));
}
- return permissions.getFieldPermissions().filter(reader);
+ return permissions.getFieldPermissions().filter(wrappedReader);
} catch (IOException e) {
logger.error("Unable to apply field level security");
throw ExceptionsHelper.convertToElastic(e);
@@ -255,48 +201,6 @@ static void intersectScorerAndRoleBits(Scorer scorer, SparseFixedBitSet roleBits
}
}
- String evaluateTemplate(String querySource) throws IOException {
- // EMPTY is safe here because we never use namedObject
- try (XContentParser parser = XContentFactory.xContent(querySource).createParser(NamedXContentRegistry.EMPTY,
- LoggingDeprecationHandler.INSTANCE, querySource)) {
- XContentParser.Token token = parser.nextToken();
- if (token != XContentParser.Token.START_OBJECT) {
- throw new ElasticsearchParseException("Unexpected token [" + token + "]");
- }
- token = parser.nextToken();
- if (token != XContentParser.Token.FIELD_NAME) {
- throw new ElasticsearchParseException("Unexpected token [" + token + "]");
- }
- if ("template".equals(parser.currentName())) {
- token = parser.nextToken();
- if (token != XContentParser.Token.START_OBJECT) {
- throw new ElasticsearchParseException("Unexpected token [" + token + "]");
- }
- Script script = Script.parse(parser);
- // Add the user details to the params
- Map params = new HashMap<>();
- if (script.getParams() != null) {
- params.putAll(script.getParams());
- }
- User user = getUser();
- Map userModel = new HashMap<>();
- userModel.put("username", user.principal());
- userModel.put("full_name", user.fullName());
- userModel.put("email", user.email());
- userModel.put("roles", Arrays.asList(user.roles()));
- userModel.put("metadata", Collections.unmodifiableMap(user.metadata()));
- params.put("_user", userModel);
- // Always enforce mustache script lang:
- script = new Script(script.getType(),
- script.getType() == ScriptType.STORED ? null : "mustache", script.getIdOrCode(), script.getOptions(), params);
- TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(script.getParams());
- return compiledTemplate.execute();
- } else {
- return querySource;
- }
- }
- }
-
protected IndicesAccessControl getIndicesAccessControl() {
IndicesAccessControl indicesAccessControl = threadContext.getTransient(AuthorizationServiceField.INDICES_PERMISSIONS_KEY);
if (indicesAccessControl == null) {
@@ -310,65 +214,4 @@ protected User getUser(){
return authentication.getUser();
}
- /**
- * Checks whether the role query contains queries we know can't be used as DLS role query.
- */
- static void verifyRoleQuery(QueryBuilder queryBuilder) throws IOException {
- if (queryBuilder instanceof TermsQueryBuilder) {
- TermsQueryBuilder termsQueryBuilder = (TermsQueryBuilder) queryBuilder;
- if (termsQueryBuilder.termsLookup() != null) {
- throw new IllegalArgumentException("terms query with terms lookup isn't supported as part of a role query");
- }
- } else if (queryBuilder instanceof GeoShapeQueryBuilder) {
- GeoShapeQueryBuilder geoShapeQueryBuilder = (GeoShapeQueryBuilder) queryBuilder;
- if (geoShapeQueryBuilder.shape() == null) {
- throw new IllegalArgumentException("geoshape query referring to indexed shapes isn't support as part of a role query");
- }
- } else if (queryBuilder.getName().equals("percolate")) {
- // actually only if percolate query is referring to an existing document then this is problematic,
- // a normal percolate query does work. However we can't check that here as this query builder is inside
- // another module. So we don't allow the entire percolate query. I don't think users would ever use
- // a percolate query as role query, so this restriction shouldn't prohibit anyone from using dls.
- throw new IllegalArgumentException("percolate query isn't support as part of a role query");
- } else if (queryBuilder.getName().equals("has_child")) {
- throw new IllegalArgumentException("has_child query isn't support as part of a role query");
- } else if (queryBuilder.getName().equals("has_parent")) {
- throw new IllegalArgumentException("has_parent query isn't support as part of a role query");
- } else if (queryBuilder instanceof BoolQueryBuilder) {
- BoolQueryBuilder boolQueryBuilder = (BoolQueryBuilder) queryBuilder;
- List clauses = new ArrayList<>();
- clauses.addAll(boolQueryBuilder.filter());
- clauses.addAll(boolQueryBuilder.must());
- clauses.addAll(boolQueryBuilder.mustNot());
- clauses.addAll(boolQueryBuilder.should());
- for (QueryBuilder clause : clauses) {
- verifyRoleQuery(clause);
- }
- } else if (queryBuilder instanceof ConstantScoreQueryBuilder) {
- verifyRoleQuery(((ConstantScoreQueryBuilder) queryBuilder).innerQuery());
- } else if (queryBuilder instanceof FunctionScoreQueryBuilder) {
- verifyRoleQuery(((FunctionScoreQueryBuilder) queryBuilder).query());
- } else if (queryBuilder instanceof BoostingQueryBuilder) {
- verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).negativeQuery());
- verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).positiveQuery());
- }
- }
-
- /**
- * Fall back validation that verifies that queries during rewrite don't use
- * the client to make remote calls. In the case of DLS this can cause a dead
- * lock if DLS is also applied on these remote calls. For example in the
- * case of terms query with lookup, this can cause recursive execution of
- * the DLS query until the get thread pool has been exhausted:
- * https://github.com/elastic/x-plugins/issues/3145
- */
- static void failIfQueryUsesClient(QueryBuilder queryBuilder, QueryRewriteContext original)
- throws IOException {
- QueryRewriteContext copy = new QueryRewriteContext(
- original.getXContentRegistry(), original.getWriteableRegistry(), null, original::nowInMillis);
- Rewriteable.rewrite(queryBuilder, copy);
- if (copy.hasAsyncActions()) {
- throw new IllegalStateException("role queries are not allowed to execute additional requests");
- }
- }
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java
index 073e92f7faf44..0cd4e8a8b0ddc 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ApplicationPermission.java
@@ -12,10 +12,12 @@
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.util.set.Sets;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege;
+import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
import org.elasticsearch.xpack.core.security.support.Automatons;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -83,6 +85,40 @@ public boolean grants(ApplicationPrivilege other, String resource) {
return matched;
}
+ /**
+ * For a given application, checks for the privileges for resources and returns an instance of {@link ResourcePrivilegesMap} holding a
+ * map of resource to {@link ResourcePrivileges} where the resource is application resource and the map of application privilege to
+ * whether it is allowed or not.
+ *
+ * @param applicationName checks privileges for the provided application name
+ * @param checkForResources check permission grants for the set of resources
+ * @param checkForPrivilegeNames check permission grants for the set of privilege names
+ * @param storedPrivileges stored {@link ApplicationPrivilegeDescriptor} for an application against which the access checks are
+ * performed
+ * @return an instance of {@link ResourcePrivilegesMap}
+ */
+ public ResourcePrivilegesMap checkResourcePrivileges(final String applicationName, Set checkForResources,
+ Set checkForPrivilegeNames,
+ Collection storedPrivileges) {
+ final ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder = ResourcePrivilegesMap.builder();
+ for (String checkResource : checkForResources) {
+ for (String checkPrivilegeName : checkForPrivilegeNames) {
+ final Set nameSet = Collections.singleton(checkPrivilegeName);
+ final ApplicationPrivilege checkPrivilege = ApplicationPrivilege.get(applicationName, nameSet, storedPrivileges);
+ assert checkPrivilege.getApplication().equals(applicationName) : "Privilege " + checkPrivilege + " should have application "
+ + applicationName;
+ assert checkPrivilege.name().equals(nameSet) : "Privilege " + checkPrivilege + " should have name " + nameSet;
+
+ if (grants(checkPrivilege, checkResource)) {
+ resourcePrivilegesMapBuilder.addResourcePrivilege(checkResource, checkPrivilegeName, Boolean.TRUE);
+ } else {
+ resourcePrivilegesMapBuilder.addResourcePrivilege(checkResource, checkPrivilegeName, Boolean.FALSE);
+ }
+ }
+ }
+ return resourcePrivilegesMapBuilder.build();
+ }
+
@Override
public String toString() {
return getClass().getSimpleName() + "{privileges=" + permissions + "}";
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java
index 3af016959d4ed..687798971399f 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermission.java
@@ -5,6 +5,7 @@
*/
package org.elasticsearch.xpack.core.security.authz.permission;
+import org.apache.lucene.util.automaton.Operations;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
@@ -33,6 +34,10 @@ public ClusterPrivilege privilege() {
public abstract boolean check(String action, TransportRequest request);
+ public boolean grants(ClusterPrivilege clusterPrivilege) {
+ return Operations.subsetOf(clusterPrivilege.getAutomaton(), this.privilege().getAutomaton());
+ }
+
public abstract List> privileges();
/**
@@ -111,5 +116,10 @@ public List> privileges() {
public boolean check(String action, TransportRequest request) {
return children.stream().anyMatch(p -> p.check(action, request));
}
+
+ @Override
+ public boolean grants(ClusterPrivilege clusterPrivilege) {
+ return children.stream().anyMatch(p -> p.grants(clusterPrivilege));
+ }
}
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java
new file mode 100644
index 0000000000000..08d754b4e5357
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/DocumentPermissions.java
@@ -0,0 +1,262 @@
+/*
+ * 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.permission;
+
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.join.BitSetProducer;
+import org.apache.lucene.search.join.ToChildBlockJoinQuery;
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.lucene.search.Queries;
+import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.BoostingQueryBuilder;
+import org.elasticsearch.index.query.ConstantScoreQueryBuilder;
+import org.elasticsearch.index.query.GeoShapeQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryRewriteContext;
+import org.elasticsearch.index.query.QueryShardContext;
+import org.elasticsearch.index.query.Rewriteable;
+import org.elasticsearch.index.query.TermsQueryBuilder;
+import org.elasticsearch.index.query.functionscore.FunctionScoreQueryBuilder;
+import org.elasticsearch.index.search.NestedHelper;
+import org.elasticsearch.index.shard.ShardId;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.xpack.core.security.authz.support.SecurityQueryTemplateEvaluator;
+import org.elasticsearch.xpack.core.security.user.User;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Function;
+
+import static org.apache.lucene.search.BooleanClause.Occur.FILTER;
+import static org.apache.lucene.search.BooleanClause.Occur.SHOULD;
+
+/**
+ * Stores document level permissions in the form queries that match all the accessible documents.
+ * The document level permissions may be limited by another set of queries in that case the limited
+ * queries are used as an additional filter.
+ */
+public final class DocumentPermissions {
+ private final Set queries;
+ private final Set limitedByQueries;
+
+ private static DocumentPermissions ALLOW_ALL = new DocumentPermissions();
+
+ DocumentPermissions() {
+ this.queries = null;
+ this.limitedByQueries = null;
+ }
+
+ DocumentPermissions(Set queries) {
+ this(queries, null);
+ }
+
+ DocumentPermissions(Set queries, Set scopedByQueries) {
+ if (queries == null && scopedByQueries == null) {
+ throw new IllegalArgumentException("one of the queries or scoped queries must be provided");
+ }
+ this.queries = (queries != null) ? Collections.unmodifiableSet(queries) : queries;
+ this.limitedByQueries = (scopedByQueries != null) ? Collections.unmodifiableSet(scopedByQueries) : scopedByQueries;
+ }
+
+ public Set getQueries() {
+ return queries;
+ }
+
+ public Set getLimitedByQueries() {
+ return limitedByQueries;
+ }
+
+ /**
+ * @return {@code true} if either queries or scoped queries are present for document level security else returns {@code false}
+ */
+ public boolean hasDocumentLevelPermissions() {
+ return queries != null || limitedByQueries != null;
+ }
+
+ /**
+ * Creates a {@link BooleanQuery} to be used as filter to restrict access to documents.
+ * Document permission queries are used to create an boolean query.
+ * If the document permissions are limited, then there is an additional filter added restricting access to documents only allowed by the
+ * limited queries.
+ *
+ * @param user authenticated {@link User}
+ * @param scriptService {@link ScriptService} for evaluating query templates
+ * @param shardId {@link ShardId}
+ * @param queryShardContextProvider {@link QueryShardContext}
+ * @return {@link BooleanQuery} for the filter
+ * @throws IOException thrown if there is an exception during parsing
+ */
+ public BooleanQuery filter(User user, ScriptService scriptService, ShardId shardId,
+ Function queryShardContextProvider) throws IOException {
+ if (hasDocumentLevelPermissions()) {
+ BooleanQuery.Builder filter;
+ if (queries != null && limitedByQueries != null) {
+ filter = new BooleanQuery.Builder();
+ BooleanQuery.Builder scopedFilter = new BooleanQuery.Builder();
+ buildRoleQuery(user, scriptService, shardId, queryShardContextProvider, limitedByQueries, scopedFilter);
+ filter.add(scopedFilter.build(), FILTER);
+
+ buildRoleQuery(user, scriptService, shardId, queryShardContextProvider, queries, filter);
+ } else if (queries != null) {
+ filter = new BooleanQuery.Builder();
+ buildRoleQuery(user, scriptService, shardId, queryShardContextProvider, queries, filter);
+ } else if (limitedByQueries != null) {
+ filter = new BooleanQuery.Builder();
+ buildRoleQuery(user, scriptService, shardId, queryShardContextProvider, limitedByQueries, filter);
+ } else {
+ return null;
+ }
+ return filter.build();
+ }
+ return null;
+ }
+
+ private static void buildRoleQuery(User user, ScriptService scriptService, ShardId shardId,
+ Function queryShardContextProvider, Set queries,
+ BooleanQuery.Builder filter) throws IOException {
+ for (BytesReference bytesReference : queries) {
+ QueryShardContext queryShardContext = queryShardContextProvider.apply(shardId);
+ String templateResult = SecurityQueryTemplateEvaluator.evaluateTemplate(bytesReference.utf8ToString(), scriptService, user);
+ try (XContentParser parser = XContentFactory.xContent(templateResult).createParser(queryShardContext.getXContentRegistry(),
+ LoggingDeprecationHandler.INSTANCE, templateResult)) {
+ QueryBuilder queryBuilder = queryShardContext.parseInnerQueryBuilder(parser);
+ verifyRoleQuery(queryBuilder);
+ failIfQueryUsesClient(queryBuilder, queryShardContext);
+ Query roleQuery = queryShardContext.toQuery(queryBuilder).query();
+ filter.add(roleQuery, SHOULD);
+ if (queryShardContext.getMapperService().hasNested()) {
+ NestedHelper nestedHelper = new NestedHelper(queryShardContext.getMapperService());
+ if (nestedHelper.mightMatchNestedDocs(roleQuery)) {
+ roleQuery = new BooleanQuery.Builder().add(roleQuery, FILTER)
+ .add(Queries.newNonNestedFilter(queryShardContext.indexVersionCreated()), FILTER).build();
+ }
+ // If access is allowed on root doc then also access is allowed on all nested docs of that root document:
+ BitSetProducer rootDocs = queryShardContext
+ .bitsetFilter(Queries.newNonNestedFilter(queryShardContext.indexVersionCreated()));
+ ToChildBlockJoinQuery includeNestedDocs = new ToChildBlockJoinQuery(roleQuery, rootDocs);
+ filter.add(includeNestedDocs, SHOULD);
+ }
+ }
+ }
+ // at least one of the queries should match
+ filter.setMinimumNumberShouldMatch(1);
+ }
+
+ /**
+ * Checks whether the role query contains queries we know can't be used as DLS role query.
+ */
+ static void verifyRoleQuery(QueryBuilder queryBuilder) throws IOException {
+ if (queryBuilder instanceof TermsQueryBuilder) {
+ TermsQueryBuilder termsQueryBuilder = (TermsQueryBuilder) queryBuilder;
+ if (termsQueryBuilder.termsLookup() != null) {
+ throw new IllegalArgumentException("terms query with terms lookup isn't supported as part of a role query");
+ }
+ } else if (queryBuilder instanceof GeoShapeQueryBuilder) {
+ GeoShapeQueryBuilder geoShapeQueryBuilder = (GeoShapeQueryBuilder) queryBuilder;
+ if (geoShapeQueryBuilder.shape() == null) {
+ throw new IllegalArgumentException("geoshape query referring to indexed shapes isn't support as part of a role query");
+ }
+ } else if (queryBuilder.getName().equals("percolate")) {
+ // actually only if percolate query is referring to an existing document then this is problematic,
+ // a normal percolate query does work. However we can't check that here as this query builder is inside
+ // another module. So we don't allow the entire percolate query. I don't think users would ever use
+ // a percolate query as role query, so this restriction shouldn't prohibit anyone from using dls.
+ throw new IllegalArgumentException("percolate query isn't support as part of a role query");
+ } else if (queryBuilder.getName().equals("has_child")) {
+ throw new IllegalArgumentException("has_child query isn't support as part of a role query");
+ } else if (queryBuilder.getName().equals("has_parent")) {
+ throw new IllegalArgumentException("has_parent query isn't support as part of a role query");
+ } else if (queryBuilder instanceof BoolQueryBuilder) {
+ BoolQueryBuilder boolQueryBuilder = (BoolQueryBuilder) queryBuilder;
+ List clauses = new ArrayList<>();
+ clauses.addAll(boolQueryBuilder.filter());
+ clauses.addAll(boolQueryBuilder.must());
+ clauses.addAll(boolQueryBuilder.mustNot());
+ clauses.addAll(boolQueryBuilder.should());
+ for (QueryBuilder clause : clauses) {
+ verifyRoleQuery(clause);
+ }
+ } else if (queryBuilder instanceof ConstantScoreQueryBuilder) {
+ verifyRoleQuery(((ConstantScoreQueryBuilder) queryBuilder).innerQuery());
+ } else if (queryBuilder instanceof FunctionScoreQueryBuilder) {
+ verifyRoleQuery(((FunctionScoreQueryBuilder) queryBuilder).query());
+ } else if (queryBuilder instanceof BoostingQueryBuilder) {
+ verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).negativeQuery());
+ verifyRoleQuery(((BoostingQueryBuilder) queryBuilder).positiveQuery());
+ }
+ }
+
+ /**
+ * Fall back validation that verifies that queries during rewrite don't use
+ * the client to make remote calls. In the case of DLS this can cause a dead
+ * lock if DLS is also applied on these remote calls. For example in the
+ * case of terms query with lookup, this can cause recursive execution of
+ * the DLS query until the get thread pool has been exhausted:
+ * https://github.com/elastic/x-plugins/issues/3145
+ */
+ static void failIfQueryUsesClient(QueryBuilder queryBuilder, QueryRewriteContext original)
+ throws IOException {
+ QueryRewriteContext copy = new QueryRewriteContext(
+ original.getXContentRegistry(), original.getWriteableRegistry(), null, original::nowInMillis);
+ Rewriteable.rewrite(queryBuilder, copy);
+ if (copy.hasAsyncActions()) {
+ throw new IllegalStateException("role queries are not allowed to execute additional requests");
+ }
+ }
+
+ /**
+ * Create {@link DocumentPermissions} for given set of queries
+ * @param queries set of queries
+ * @return {@link DocumentPermissions}
+ */
+ public static DocumentPermissions filteredBy(Set queries) {
+ if (queries == null || queries.isEmpty()) {
+ throw new IllegalArgumentException("null or empty queries not permitted");
+ }
+ return new DocumentPermissions(queries);
+ }
+
+ /**
+ * Create {@link DocumentPermissions} with no restriction. The {@link #getQueries()}
+ * will return {@code null} in this case and {@link #hasDocumentLevelPermissions()}
+ * will be {@code false}
+ * @return {@link DocumentPermissions}
+ */
+ public static DocumentPermissions allowAll() {
+ return ALLOW_ALL;
+ }
+
+ /**
+ * Create a document permissions, where the permissions for {@code this} are
+ * limited by the queries from other document permissions.
+ *
+ * @param limitedByDocumentPermissions {@link DocumentPermissions} used to limit the document level access
+ * @return instance of {@link DocumentPermissions}
+ */
+ public DocumentPermissions limitDocumentPermissions(
+ DocumentPermissions limitedByDocumentPermissions) {
+ assert limitedByQueries == null
+ && limitedByDocumentPermissions.limitedByQueries == null : "nested scoping for document permissions is not permitted";
+ if (queries == null && limitedByDocumentPermissions.queries == null) {
+ return DocumentPermissions.allowAll();
+ }
+ return new DocumentPermissions(queries, limitedByDocumentPermissions.queries);
+ }
+
+ @Override
+ public String toString() {
+ return "DocumentPermissions [queries=" + queries + ", scopedByQueries=" + limitedByQueries + "]";
+ }
+
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java
index e7dd9d2be4c88..53ea785913557 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java
@@ -93,13 +93,15 @@ public FieldPermissions(FieldPermissionsDefinition fieldPermissionsDefinition) {
long ramBytesUsed = BASE_FIELD_PERM_DEF_BYTES;
- for (FieldGrantExcludeGroup group : fieldPermissionsDefinition.getFieldGrantExcludeGroups()) {
- ramBytesUsed += BASE_FIELD_GROUP_BYTES + BASE_HASHSET_ENTRY_SIZE;
- if (group.getGrantedFields() != null) {
- ramBytesUsed += RamUsageEstimator.shallowSizeOf(group.getGrantedFields());
- }
- if (group.getExcludedFields() != null) {
- ramBytesUsed += RamUsageEstimator.shallowSizeOf(group.getExcludedFields());
+ if (fieldPermissionsDefinition != null) {
+ for (FieldGrantExcludeGroup group : fieldPermissionsDefinition.getFieldGrantExcludeGroups()) {
+ ramBytesUsed += BASE_FIELD_GROUP_BYTES + BASE_HASHSET_ENTRY_SIZE;
+ if (group.getGrantedFields() != null) {
+ ramBytesUsed += RamUsageEstimator.shallowSizeOf(group.getGrantedFields());
+ }
+ if (group.getExcludedFields() != null) {
+ ramBytesUsed += RamUsageEstimator.shallowSizeOf(group.getExcludedFields());
+ }
}
}
ramBytesUsed += permittedFieldsAutomaton.ramBytesUsed();
@@ -170,6 +172,28 @@ private static boolean containsAllField(String[] fields) {
return fields != null && Arrays.stream(fields).anyMatch(AllFieldMapper.NAME::equals);
}
+ /**
+ * Returns a field permissions instance where it is limited by the given field permissions.
+ * If the current and the other field permissions have field level security then it takes
+ * an intersection of permitted fields.
+ * If none of the permissions have field level security enabled, then returns permissions
+ * instance where all fields are allowed.
+ *
+ * @param limitedBy {@link FieldPermissions} used to limit current field permissions
+ * @return {@link FieldPermissions}
+ */
+ public FieldPermissions limitFieldPermissions(FieldPermissions limitedBy) {
+ if (hasFieldLevelSecurity() && limitedBy != null && limitedBy.hasFieldLevelSecurity()) {
+ Automaton permittedFieldsAutomaton = Automatons.intersectAndMinimize(getIncludeAutomaton(), limitedBy.getIncludeAutomaton());
+ return new FieldPermissions(null, permittedFieldsAutomaton);
+ } else if (limitedBy != null && limitedBy.hasFieldLevelSecurity()) {
+ return new FieldPermissions(limitedBy.getFieldPermissionsDefinition(), limitedBy.getIncludeAutomaton());
+ } else if (hasFieldLevelSecurity()) {
+ return new FieldPermissions(getFieldPermissionsDefinition(), getIncludeAutomaton());
+ }
+ return FieldPermissions.DEFAULT;
+ }
+
/**
* Returns true if this field permission policy allows access to the field and false if not.
* fieldName can be a wildcard.
@@ -195,7 +219,6 @@ public DirectoryReader filter(DirectoryReader reader) throws IOException {
return FieldSubsetReader.wrap(reader, permittedFieldsAutomaton);
}
- // for testing only
Automaton getIncludeAutomaton() {
return originalAutomaton;
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java
index 4c2a479721a2a..da8c3701ed300 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java
@@ -7,6 +7,7 @@
import org.apache.logging.log4j.LogManager;
import org.apache.lucene.util.automaton.Automaton;
+import org.apache.lucene.util.automaton.Operations;
import org.apache.lucene.util.automaton.TooComplexToDeterminizeException;
import org.elasticsearch.ElasticsearchSecurityException;
import org.elasticsearch.cluster.metadata.AliasOrIndex;
@@ -23,6 +24,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -123,6 +125,49 @@ public boolean check(String action) {
return false;
}
+ /**
+ * For given index patterns and index privileges determines allowed privileges and creates an instance of {@link ResourcePrivilegesMap}
+ * holding a map of resource to {@link ResourcePrivileges} where resource is index pattern and the map of index privilege to whether it
+ * is allowed or not.
+ *
+ * @param checkForIndexPatterns check permission grants for the set of index patterns
+ * @param allowRestrictedIndices if {@code true} then checks permission grants even for restricted indices by index matching
+ * @param checkForPrivileges check permission grants for the set of index privileges
+ * @return an instance of {@link ResourcePrivilegesMap}
+ */
+ public ResourcePrivilegesMap checkResourcePrivileges(Set checkForIndexPatterns, boolean allowRestrictedIndices,
+ Set checkForPrivileges) {
+ final ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder = ResourcePrivilegesMap.builder();
+ final Map predicateCache = new HashMap<>();
+ for (String forIndexPattern : checkForIndexPatterns) {
+ final Automaton checkIndexAutomaton = IndicesPermission.Group.buildIndexMatcherAutomaton(allowRestrictedIndices,
+ forIndexPattern);
+ Automaton allowedIndexPrivilegesAutomaton = null;
+ for (Group group : groups) {
+ final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group,
+ g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices()));
+ if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) {
+ if (allowedIndexPrivilegesAutomaton != null) {
+ allowedIndexPrivilegesAutomaton = Automatons
+ .unionAndMinimize(Arrays.asList(allowedIndexPrivilegesAutomaton, group.privilege().getAutomaton()));
+ } else {
+ allowedIndexPrivilegesAutomaton = group.privilege().getAutomaton();
+ }
+ }
+ }
+ for (String privilege : checkForPrivileges) {
+ IndexPrivilege indexPrivilege = IndexPrivilege.get(Collections.singleton(privilege));
+ if (allowedIndexPrivilegesAutomaton != null
+ && Operations.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) {
+ resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE);
+ } else {
+ resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.FALSE);
+ }
+ }
+ }
+ return resourcePrivilegesMapBuilder.build();
+ }
+
public Automaton allowedActionsMatcher(String index) {
List automatonList = new ArrayList<>();
for (Group group : groups) {
@@ -207,7 +252,8 @@ public Map authorize(String act
} else {
fieldPermissions = FieldPermissions.DEFAULT;
}
- indexPermissions.put(index, new IndicesAccessControl.IndexAccessControl(entry.getValue(), fieldPermissions, roleQueries));
+ indexPermissions.put(index, new IndicesAccessControl.IndexAccessControl(entry.getValue(), fieldPermissions,
+ (roleQueries != null) ? DocumentPermissions.filteredBy(roleQueries) : DocumentPermissions.allowAll()));
}
return unmodifiableMap(indexPermissions);
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java
new file mode 100644
index 0000000000000..809b95965340e
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/LimitedRole.java
@@ -0,0 +1,152 @@
+/*
+ * 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.permission;
+
+import org.elasticsearch.cluster.metadata.MetaData;
+import org.elasticsearch.transport.TransportRequest;
+import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
+import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
+import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
+
+import java.util.Collection;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/**
+ * A {@link Role} limited by another role.
+ * The effective permissions returned on {@link #authorize(String, Set, MetaData, FieldPermissionsCache)} call would be limited by the
+ * provided role.
+ */
+public final class LimitedRole extends Role {
+ private final Role limitedBy;
+
+ LimitedRole(String[] names, ClusterPermission cluster, IndicesPermission indices, ApplicationPermission application,
+ RunAsPermission runAs, Role limitedBy) {
+ super(names, cluster, indices, application, runAs);
+ assert limitedBy != null : "limiting role is required";
+ this.limitedBy = limitedBy;
+ }
+
+ public Role limitedBy() {
+ return limitedBy;
+ }
+
+ @Override
+ public IndicesAccessControl authorize(String action, Set requestedIndicesOrAliases, MetaData metaData,
+ FieldPermissionsCache fieldPermissionsCache) {
+ IndicesAccessControl indicesAccessControl = super.authorize(action, requestedIndicesOrAliases, metaData, fieldPermissionsCache);
+ IndicesAccessControl limitedByIndicesAccessControl = limitedBy.authorize(action, requestedIndicesOrAliases, metaData,
+ fieldPermissionsCache);
+
+ return indicesAccessControl.limitIndicesAccessControl(limitedByIndicesAccessControl);
+ }
+
+ /**
+ * @return A predicate that will match all the indices that this role and the limited by role has the privilege for executing the given
+ * action on.
+ */
+ @Override
+ public Predicate allowedIndicesMatcher(String action) {
+ Predicate predicate = indices().allowedIndicesMatcher(action);
+ predicate = predicate.and(limitedBy.indices().allowedIndicesMatcher(action));
+ return predicate;
+ }
+
+ /**
+ * Check if indices permissions allow for the given action, also checks whether the limited by role allows the given actions
+ *
+ * @param action indices action
+ * @return {@code true} if action is allowed else returns {@code false}
+ */
+ @Override
+ public boolean checkIndicesAction(String action) {
+ return super.checkIndicesAction(action) && limitedBy.checkIndicesAction(action);
+ }
+
+ /**
+ * For given index patterns and index privileges determines allowed privileges and creates an instance of {@link ResourcePrivilegesMap}
+ * holding a map of resource to {@link ResourcePrivileges} where resource is index pattern and the map of index privilege to whether it
+ * is allowed or not.
+ * This one takes intersection of resource privileges with the resource privileges from the limited-by role.
+ *
+ * @param checkForIndexPatterns check permission grants for the set of index patterns
+ * @param allowRestrictedIndices if {@code true} then checks permission grants even for restricted indices by index matching
+ * @param checkForPrivileges check permission grants for the set of index privileges
+ * @return an instance of {@link ResourcePrivilegesMap}
+ */
+ @Override
+ public ResourcePrivilegesMap checkIndicesPrivileges(Set checkForIndexPatterns, boolean allowRestrictedIndices,
+ Set checkForPrivileges) {
+ ResourcePrivilegesMap resourcePrivilegesMap = super.indices().checkResourcePrivileges(checkForIndexPatterns, allowRestrictedIndices,
+ checkForPrivileges);
+ ResourcePrivilegesMap resourcePrivilegesMapForLimitedRole = limitedBy.indices().checkResourcePrivileges(checkForIndexPatterns,
+ allowRestrictedIndices, checkForPrivileges);
+ return ResourcePrivilegesMap.intersection(resourcePrivilegesMap, resourcePrivilegesMapForLimitedRole);
+ }
+
+ /**
+ * Check if cluster permissions allow for the given action, also checks whether the limited by role allows the given actions
+ *
+ * @param action cluster action
+ * @param request {@link TransportRequest}
+ * @return {@code true} if action is allowed else returns {@code false}
+ */
+ @Override
+ public boolean checkClusterAction(String action, TransportRequest request) {
+ return super.checkClusterAction(action, request) && limitedBy.checkClusterAction(action, request);
+ }
+
+ /**
+ * Check if cluster permissions grants the given cluster privilege, also checks whether the limited by role grants the given cluster
+ * privilege
+ *
+ * @param clusterPrivilege cluster privilege
+ * @return {@code true} if cluster privilege is allowed else returns {@code false}
+ */
+ @Override
+ public boolean grants(ClusterPrivilege clusterPrivilege) {
+ return super.grants(clusterPrivilege) && limitedBy.grants(clusterPrivilege);
+ }
+
+ /**
+ * For a given application, checks for the privileges for resources and returns an instance of {@link ResourcePrivilegesMap} holding a
+ * map of resource to {@link ResourcePrivileges} where the resource is application resource and the map of application privilege to
+ * whether it is allowed or not.
+ * This one takes intersection of resource privileges with the resource privileges from the limited-by role.
+ *
+ * @param applicationName checks privileges for the provided application name
+ * @param checkForResources check permission grants for the set of resources
+ * @param checkForPrivilegeNames check permission grants for the set of privilege names
+ * @param storedPrivileges stored {@link ApplicationPrivilegeDescriptor} for an application against which the access checks are
+ * performed
+ * @return an instance of {@link ResourcePrivilegesMap}
+ */
+ @Override
+ public ResourcePrivilegesMap checkApplicationResourcePrivileges(final String applicationName, Set checkForResources,
+ Set checkForPrivilegeNames,
+ Collection storedPrivileges) {
+ ResourcePrivilegesMap resourcePrivilegesMap = super.application().checkResourcePrivileges(applicationName, checkForResources,
+ checkForPrivilegeNames, storedPrivileges);
+ ResourcePrivilegesMap resourcePrivilegesMapForLimitedRole = limitedBy.application().checkResourcePrivileges(applicationName,
+ checkForResources, checkForPrivilegeNames, storedPrivileges);
+ return ResourcePrivilegesMap.intersection(resourcePrivilegesMap, resourcePrivilegesMapForLimitedRole);
+ }
+
+ /**
+ * Create a new role defined by given role and the limited role.
+ *
+ * @param fromRole existing role {@link Role}
+ * @param limitedByRole restrict the newly formed role to the permissions defined by this limited {@link Role}
+ * @return {@link LimitedRole}
+ */
+ public static LimitedRole createLimitedRole(Role fromRole, Role limitedByRole) {
+ Objects.requireNonNull(limitedByRole, "limited by role is required to create limited role");
+ return new LimitedRole(fromRole.names(), fromRole.cluster(), fromRole.indices(), fromRole.application(), fromRole.runAs(),
+ limitedByRole);
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivileges.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivileges.java
new file mode 100644
index 0000000000000..3c64cc4afa8a1
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivileges.java
@@ -0,0 +1,93 @@
+/*
+ * 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.permission;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+
+/**
+ * A generic structure to encapsulate resource to privileges map.
+ */
+public final class ResourcePrivileges {
+
+ private final String resource;
+ private final Map privileges;
+
+ ResourcePrivileges(String resource, Map privileges) {
+ this.resource = Objects.requireNonNull(resource);
+ this.privileges = Collections.unmodifiableMap(privileges);
+ }
+
+ public String getResource() {
+ return resource;
+ }
+
+ public Map getPrivileges() {
+ return privileges;
+ }
+
+ public boolean isAllowed(String privilege) {
+ return Boolean.TRUE.equals(privileges.get(privilege));
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "{" + "resource='" + resource + '\'' + ", privileges=" + privileges + '}';
+ }
+
+ @Override
+ public int hashCode() {
+ int result = resource.hashCode();
+ result = 31 * result + privileges.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ final ResourcePrivileges other = (ResourcePrivileges) o;
+ return this.resource.equals(other.resource) && this.privileges.equals(other.privileges);
+ }
+
+ public static Builder builder(String resource) {
+ return new Builder(resource);
+ }
+
+ public static final class Builder {
+ private final String resource;
+ private Map privileges = new HashMap<>();
+
+ private Builder(String resource) {
+ this.resource = resource;
+ }
+
+ public Builder addPrivilege(String privilege, Boolean allowed) {
+ this.privileges.compute(privilege, (k, v) -> ((v == null) ? allowed : v && allowed));
+ return this;
+ }
+
+ public Builder addPrivileges(Map privileges) {
+ for (Entry entry : privileges.entrySet()) {
+ addPrivilege(entry.getKey(), entry.getValue());
+ }
+ return this;
+ }
+
+ public ResourcePrivileges build() {
+ return new ResourcePrivileges(resource, privileges);
+ }
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivilegesMap.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivilegesMap.java
new file mode 100644
index 0000000000000..814a6ed29d39f
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/ResourcePrivilegesMap.java
@@ -0,0 +1,121 @@
+/*
+ * 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.permission;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * A generic structure to encapsulate resources to {@link ResourcePrivileges}. Also keeps track of whether the resource privileges allow
+ * permissions to all resources.
+ */
+public final class ResourcePrivilegesMap {
+
+ private final boolean allAllowed;
+ private final Map resourceToResourcePrivileges;
+
+ public ResourcePrivilegesMap(boolean allAllowed, Map resToResPriv) {
+ this.allAllowed = allAllowed;
+ this.resourceToResourcePrivileges = Collections.unmodifiableMap(Objects.requireNonNull(resToResPriv));
+ }
+
+ public boolean allAllowed() {
+ return allAllowed;
+ }
+
+ public Map getResourceToResourcePrivileges() {
+ return resourceToResourcePrivileges;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(allAllowed, resourceToResourcePrivileges);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final ResourcePrivilegesMap other = (ResourcePrivilegesMap) obj;
+ return allAllowed == other.allAllowed && Objects.equals(resourceToResourcePrivileges, other.resourceToResourcePrivileges);
+ }
+
+ @Override
+ public String toString() {
+ return "ResourcePrivilegesMap [allAllowed=" + allAllowed + ", resourceToResourcePrivileges=" + resourceToResourcePrivileges + "]";
+ }
+
+ public static final class Builder {
+ private boolean allowAll = true;
+ private Map resourceToResourcePrivilegesBuilder = new LinkedHashMap<>();
+
+ public Builder addResourcePrivilege(String resource, String privilege, Boolean allowed) {
+ assert resource != null && privilege != null
+ && allowed != null : "resource, privilege and permission(allowed or denied) are required";
+ ResourcePrivileges.Builder builder = resourceToResourcePrivilegesBuilder.computeIfAbsent(resource, ResourcePrivileges::builder);
+ builder.addPrivilege(privilege, allowed);
+ allowAll = allowAll && allowed;
+ return this;
+ }
+
+ public Builder addResourcePrivilege(String resource, Map privilegePermissions) {
+ assert resource != null && privilegePermissions != null : "resource, privilege permissions(allowed or denied) are required";
+ ResourcePrivileges.Builder builder = resourceToResourcePrivilegesBuilder.computeIfAbsent(resource, ResourcePrivileges::builder);
+ builder.addPrivileges(privilegePermissions);
+ allowAll = allowAll && privilegePermissions.values().stream().allMatch(b -> Boolean.TRUE.equals(b));
+ return this;
+ }
+
+ public Builder addResourcePrivilegesMap(ResourcePrivilegesMap resourcePrivilegesMap) {
+ resourcePrivilegesMap.getResourceToResourcePrivileges().entrySet().stream()
+ .forEach(e -> this.addResourcePrivilege(e.getKey(), e.getValue().getPrivileges()));
+ return this;
+ }
+
+ public ResourcePrivilegesMap build() {
+ Map result = resourceToResourcePrivilegesBuilder.entrySet().stream()
+ .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().build()));
+ return new ResourcePrivilegesMap(allowAll, result);
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Takes an intersection of resource privileges and returns a new instance of {@link ResourcePrivilegesMap}. If one of the resource
+ * privileges map does not allow access to a resource then the resulting map would also not allow access.
+ *
+ * @param left an instance of {@link ResourcePrivilegesMap}
+ * @param right an instance of {@link ResourcePrivilegesMap}
+ * @return a new instance of {@link ResourcePrivilegesMap}, an intersection of resource privileges.
+ */
+ public static ResourcePrivilegesMap intersection(final ResourcePrivilegesMap left, final ResourcePrivilegesMap right) {
+ Objects.requireNonNull(left);
+ Objects.requireNonNull(right);
+ final ResourcePrivilegesMap.Builder builder = ResourcePrivilegesMap.builder();
+ for (Entry leftResPrivsEntry : left.getResourceToResourcePrivileges().entrySet()) {
+ final ResourcePrivileges leftResPrivs = leftResPrivsEntry.getValue();
+ final ResourcePrivileges rightResPrivs = right.getResourceToResourcePrivileges().get(leftResPrivsEntry.getKey());
+ builder.addResourcePrivilege(leftResPrivsEntry.getKey(), leftResPrivs.getPrivileges());
+ builder.addResourcePrivilege(leftResPrivsEntry.getKey(), rightResPrivs.getPrivileges());
+ }
+ return builder.build();
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java
index f01869b4ea8dc..c63e8049193af 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/Role.java
@@ -10,9 +10,11 @@
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.Tuple;
import org.elasticsearch.common.util.set.Sets;
+import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.accesscontrol.IndicesAccessControl;
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege;
+import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;
@@ -20,13 +22,15 @@
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.function.Predicate;
-public final class Role {
+public class Role {
public static final Role EMPTY = Role.builder("__empty").build();
@@ -44,6 +48,7 @@ public final class Role {
this.runAs = Objects.requireNonNull(runAs);
}
+
public String[] names() {
return names;
}
@@ -76,6 +81,79 @@ public static Builder builder(RoleDescriptor rd, FieldPermissionsCache fieldPerm
return new Builder(rd, fieldPermissionsCache);
}
+ /**
+ * @return A predicate that will match all the indices that this role
+ * has the privilege for executing the given action on.
+ */
+ public Predicate allowedIndicesMatcher(String action) {
+ return indices().allowedIndicesMatcher(action);
+ }
+
+ /**
+ * Check if indices permissions allow for the given action
+ *
+ * @param action indices action
+ * @return {@code true} if action is allowed else returns {@code false}
+ */
+ public boolean checkIndicesAction(String action) {
+ return indices().check(action);
+ }
+
+
+ /**
+ * For given index patterns and index privileges determines allowed privileges and creates an instance of {@link ResourcePrivilegesMap}
+ * holding a map of resource to {@link ResourcePrivileges} where resource is index pattern and the map of index privilege to whether it
+ * is allowed or not.
+ *
+ * @param checkForIndexPatterns check permission grants for the set of index patterns
+ * @param allowRestrictedIndices if {@code true} then checks permission grants even for restricted indices by index matching
+ * @param checkForPrivileges check permission grants for the set of index privileges
+ * @return an instance of {@link ResourcePrivilegesMap}
+ */
+ public ResourcePrivilegesMap checkIndicesPrivileges(Set checkForIndexPatterns, boolean allowRestrictedIndices,
+ Set checkForPrivileges) {
+ return indices().checkResourcePrivileges(checkForIndexPatterns, allowRestrictedIndices, checkForPrivileges);
+ }
+
+ /**
+ * Check if cluster permissions allow for the given action
+ *
+ * @param action cluster action
+ * @param request {@link TransportRequest}
+ * @return {@code true} if action is allowed else returns {@code false}
+ */
+ public boolean checkClusterAction(String action, TransportRequest request) {
+ return cluster().check(action, request);
+ }
+
+ /**
+ * Check if cluster permissions grants the given cluster privilege
+ *
+ * @param clusterPrivilege cluster privilege
+ * @return {@code true} if cluster privilege is allowed else returns {@code false}
+ */
+ public boolean grants(ClusterPrivilege clusterPrivilege) {
+ return cluster().grants(clusterPrivilege);
+ }
+
+ /**
+ * For a given application, checks for the privileges for resources and returns an instance of {@link ResourcePrivilegesMap} holding a
+ * map of resource to {@link ResourcePrivileges} where the resource is application resource and the map of application privilege to
+ * whether it is allowed or not.
+ *
+ * @param applicationName checks privileges for the provided application name
+ * @param checkForResources check permission grants for the set of resources
+ * @param checkForPrivilegeNames check permission grants for the set of privilege names
+ * @param storedPrivileges stored {@link ApplicationPrivilegeDescriptor} for an application against which the access checks are
+ * performed
+ * @return an instance of {@link ResourcePrivilegesMap}
+ */
+ public ResourcePrivilegesMap checkApplicationResourcePrivileges(final String applicationName, Set checkForResources,
+ Set checkForPrivilegeNames,
+ Collection storedPrivileges) {
+ return application().checkResourcePrivileges(applicationName, checkForResources, checkForPrivilegeNames, storedPrivileges);
+ }
+
/**
* Returns whether at least one group encapsulated by this indices permissions is authorized to execute the
* specified action with the requested indices/aliases. At the same time if field and/or document level security
@@ -211,4 +289,5 @@ static Tuple> convertApplicationPrivilege(Stri
), Sets.newHashSet(arp.getResources()));
}
}
+
}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java
new file mode 100644
index 0000000000000..951c4acf10d0d
--- /dev/null
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/support/SecurityQueryTemplateEvaluator.java
@@ -0,0 +1,92 @@
+/*
+ * 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.support;
+
+import org.elasticsearch.ElasticsearchParseException;
+import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
+import org.elasticsearch.common.xcontent.NamedXContentRegistry;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.script.Script;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.script.ScriptType;
+import org.elasticsearch.script.TemplateScript;
+import org.elasticsearch.xpack.core.security.user.User;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Helper class that helps to evaluate the query source template.
+ */
+public final class SecurityQueryTemplateEvaluator {
+
+ private SecurityQueryTemplateEvaluator() {
+ }
+
+ /**
+ * If the query source is a template, then parses the script, compiles the
+ * script with user details parameters and then executes it to return the
+ * query string.
+ *
+ * Note: This method always enforces "mustache" script language for the
+ * template.
+ *
+ * @param querySource query string template to be evaluated.
+ * @param scriptService {@link ScriptService}
+ * @param user {@link User} details for user defined parameters in the
+ * script.
+ * @return resultant query string after compiling and executing the script.
+ * If the source does not contain template then it will return the query
+ * source without any modifications.
+ * @throws IOException thrown when there is any error parsing the query
+ * string.
+ */
+ public static String evaluateTemplate(final String querySource, final ScriptService scriptService, final User user) throws IOException {
+ // EMPTY is safe here because we never use namedObject
+ try (XContentParser parser = XContentFactory.xContent(querySource).createParser(NamedXContentRegistry.EMPTY,
+ LoggingDeprecationHandler.INSTANCE, querySource)) {
+ XContentParser.Token token = parser.nextToken();
+ if (token != XContentParser.Token.START_OBJECT) {
+ throw new ElasticsearchParseException("Unexpected token [" + token + "]");
+ }
+ token = parser.nextToken();
+ if (token != XContentParser.Token.FIELD_NAME) {
+ throw new ElasticsearchParseException("Unexpected token [" + token + "]");
+ }
+ if ("template".equals(parser.currentName())) {
+ token = parser.nextToken();
+ if (token != XContentParser.Token.START_OBJECT) {
+ throw new ElasticsearchParseException("Unexpected token [" + token + "]");
+ }
+ Script script = Script.parse(parser);
+ // Add the user details to the params
+ Map params = new HashMap<>();
+ if (script.getParams() != null) {
+ params.putAll(script.getParams());
+ }
+ Map userModel = new HashMap<>();
+ userModel.put("username", user.principal());
+ userModel.put("full_name", user.fullName());
+ userModel.put("email", user.email());
+ userModel.put("roles", Arrays.asList(user.roles()));
+ userModel.put("metadata", Collections.unmodifiableMap(user.metadata()));
+ params.put("_user", userModel);
+ // Always enforce mustache script lang:
+ script = new Script(script.getType(), script.getType() == ScriptType.STORED ? null : "mustache", script.getIdOrCode(),
+ script.getOptions(), params);
+ TemplateScript compiledTemplate = scriptService.compile(script, TemplateScript.CONTEXT).newInstance(script.getParams());
+ return compiledTemplate.execute();
+ } else {
+ return querySource;
+ }
+ }
+ }
+}
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java
index 709f12ecf498e..55026c553ccb0 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/client/SecurityClient.java
@@ -10,6 +10,16 @@
import org.elasticsearch.client.ElasticsearchClient;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest;
+import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequestBuilder;
+import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse;
+import org.elasticsearch.xpack.core.security.action.GetApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest;
+import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse;
+import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction;
+import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest;
+import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse;
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction;
import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequestBuilder;
import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction;
@@ -337,6 +347,27 @@ public void invalidateToken(InvalidateTokenRequest request, ActionListener listener) {
+ client.execute(CreateApiKeyAction.INSTANCE, request, listener);
+ }
+
+ public void invalidateApiKey(InvalidateApiKeyRequest request, ActionListener listener) {
+ client.execute(InvalidateApiKeyAction.INSTANCE, request, listener);
+ }
+
+ public void getApiKey(GetApiKeyRequest request, ActionListener listener) {
+ client.execute(GetApiKeyAction.INSTANCE, request, listener);
+ }
+
public SamlAuthenticateRequestBuilder prepareSamlAuthenticate(byte[] xmlContent, List validIds) {
final SamlAuthenticateRequestBuilder builder = new SamlAuthenticateRequestBuilder(client);
builder.saml(xmlContent);
diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java
index 87a0099580b5f..7e6fd7ca46283 100644
--- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java
+++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Automatons.java
@@ -26,6 +26,7 @@
import static org.apache.lucene.util.automaton.MinimizationOperations.minimize;
import static org.apache.lucene.util.automaton.Operations.DEFAULT_MAX_DETERMINIZED_STATES;
import static org.apache.lucene.util.automaton.Operations.concatenate;
+import static org.apache.lucene.util.automaton.Operations.intersection;
import static org.apache.lucene.util.automaton.Operations.minus;
import static org.apache.lucene.util.automaton.Operations.union;
import static org.elasticsearch.common.Strings.collectionToDelimitedString;
@@ -173,6 +174,11 @@ public static Automaton minusAndMinimize(Automaton a1, Automaton a2) {
return minimize(res, maxDeterminizedStates);
}
+ public static Automaton intersectAndMinimize(Automaton a1, Automaton a2) {
+ Automaton res = intersection(a1, a2);
+ return minimize(res, maxDeterminizedStates);
+ }
+
public static Predicate predicate(String... patterns) {
return predicate(Arrays.asList(patterns));
}
diff --git a/x-pack/plugin/core/src/main/resources/security-index-template.json b/x-pack/plugin/core/src/main/resources/security-index-template.json
index 3723aff9054de..183ffff4ea534 100644
--- a/x-pack/plugin/core/src/main/resources/security-index-template.json
+++ b/x-pack/plugin/core/src/main/resources/security-index-template.json
@@ -152,6 +152,40 @@
"type" : "date",
"format" : "epoch_millis"
},
+ "api_key_hash" : {
+ "type" : "keyword",
+ "index": false,
+ "doc_values": false
+ },
+ "api_key_invalidated" : {
+ "type" : "boolean"
+ },
+ "role_descriptors" : {
+ "type" : "object",
+ "enabled": false
+ },
+ "limited_by_role_descriptors" : {
+ "type" : "object",
+ "enabled": false
+ },
+ "version" : {
+ "type" : "integer"
+ },
+ "creator" : {
+ "type" : "object",
+ "properties" : {
+ "principal" : {
+ "type": "keyword"
+ },
+ "metadata" : {
+ "type" : "object",
+ "dynamic" : true
+ },
+ "realm" : {
+ "type" : "keyword"
+ }
+ }
+ },
"rules" : {
"type" : "object",
"dynamic" : true
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilderTests.java
new file mode 100644
index 0000000000000..fb4f87089e8e7
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestBuilderTests.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.action;
+
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.common.xcontent.XContentType;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
+import org.elasticsearch.xpack.core.security.authz.RoleDescriptor.IndicesPrivileges;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.mock;
+
+public class CreateApiKeyRequestBuilderTests extends ESTestCase {
+
+ public void testParserAndCreateApiRequestBuilder() throws IOException {
+ boolean withExpiration = randomBoolean();
+ final String json = "{ \"name\" : \"my-api-key\", "
+ + ((withExpiration) ? " \"expiration\": \"1d\", " : "")
+ +" \"role_descriptors\": { \"role-a\": {\"cluster\":[\"a-1\", \"a-2\"],"
+ + " \"index\": [{\"names\": [\"indx-a\"], \"privileges\": [\"read\"] }] }, "
+ + " \"role-b\": {\"cluster\":[\"b\"],"
+ + " \"index\": [{\"names\": [\"indx-b\"], \"privileges\": [\"read\"] }] } "
+ + "} }";
+ final BytesArray source = new BytesArray(json);
+ final NodeClient mockClient = mock(NodeClient.class);
+ final CreateApiKeyRequest request = new CreateApiKeyRequestBuilder(mockClient).source(source, XContentType.JSON).request();
+ final List actualRoleDescriptors = request.getRoleDescriptors();
+ assertThat(request.getName(), equalTo("my-api-key"));
+ assertThat(actualRoleDescriptors.size(), is(2));
+ for (RoleDescriptor rd : actualRoleDescriptors) {
+ String[] clusters = null;
+ IndicesPrivileges indicesPrivileges = null;
+ if (rd.getName().equals("role-a")) {
+ clusters = new String[] { "a-1", "a-2" };
+ indicesPrivileges = RoleDescriptor.IndicesPrivileges.builder().indices("indx-a").privileges("read").build();
+ } else if (rd.getName().equals("role-b")){
+ clusters = new String[] { "b" };
+ indicesPrivileges = RoleDescriptor.IndicesPrivileges.builder().indices("indx-b").privileges("read").build();
+ } else {
+ fail("unexpected role name");
+ }
+ assertThat(rd.getClusterPrivileges(), arrayContainingInAnyOrder(clusters));
+ assertThat(rd.getIndicesPrivileges(),
+ arrayContainingInAnyOrder(indicesPrivileges));
+ }
+ if (withExpiration) {
+ assertThat(request.getExpiration(), equalTo(TimeValue.parseTimeValue("1d", "expiration")));
+ }
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java
new file mode 100644
index 0000000000000..654d56b42130e
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyRequestTests.java
@@ -0,0 +1,113 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.support.WriteRequest;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.unit.TimeValue;
+import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+
+public class CreateApiKeyRequestTests extends ESTestCase {
+
+ public void testNameValidation() {
+ final String name = randomAlphaOfLengthBetween(1, 256);
+ CreateApiKeyRequest request = new CreateApiKeyRequest();
+
+ ActionRequestValidationException ve = request.validate();
+ assertNotNull(ve);
+ assertThat(ve.validationErrors().size(), is(1));
+ assertThat(ve.validationErrors().get(0), containsString("name is required"));
+
+ request.setName(name);
+ ve = request.validate();
+ assertNull(ve);
+
+ IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> request.setName(""));
+ assertThat(e.getMessage(), containsString("name must not be null or empty"));
+
+ e = expectThrows(IllegalArgumentException.class, () -> request.setName(null));
+ assertThat(e.getMessage(), containsString("name must not be null or empty"));
+
+ request.setName(randomAlphaOfLength(257));
+ ve = request.validate();
+ assertNotNull(ve);
+ assertThat(ve.validationErrors().size(), is(1));
+ assertThat(ve.validationErrors().get(0), containsString("name may not be more than 256 characters long"));
+
+ request.setName(" leading space");
+ ve = request.validate();
+ assertNotNull(ve);
+ assertThat(ve.validationErrors().size(), is(1));
+ assertThat(ve.validationErrors().get(0), containsString("name may not begin or end with whitespace"));
+
+ request.setName("trailing space ");
+ ve = request.validate();
+ assertNotNull(ve);
+ assertThat(ve.validationErrors().size(), is(1));
+ assertThat(ve.validationErrors().get(0), containsString("name may not begin or end with whitespace"));
+
+ request.setName(" leading and trailing space ");
+ ve = request.validate();
+ assertNotNull(ve);
+ assertThat(ve.validationErrors().size(), is(1));
+ assertThat(ve.validationErrors().get(0), containsString("name may not begin or end with whitespace"));
+
+ request.setName("inner space");
+ ve = request.validate();
+ assertNull(ve);
+
+ request.setName("_foo");
+ ve = request.validate();
+ assertNotNull(ve);
+ assertThat(ve.validationErrors().size(), is(1));
+ assertThat(ve.validationErrors().get(0), containsString("name may not begin with an underscore"));
+ }
+
+ public void testSerialization() throws IOException {
+ final String name = randomAlphaOfLengthBetween(1, 256);
+ final TimeValue expiration = randomBoolean() ? null :
+ TimeValue.parseTimeValue(randomTimeValue(), "test serialization of create api key");
+ final WriteRequest.RefreshPolicy refreshPolicy = randomFrom(WriteRequest.RefreshPolicy.values());
+ final int numDescriptors = randomIntBetween(0, 4);
+ final List descriptorList = new ArrayList<>();
+ for (int i = 0; i < numDescriptors; i++) {
+ descriptorList.add(new RoleDescriptor("role_" + i, new String[] { "all" }, null, null));
+ }
+
+ final CreateApiKeyRequest request = new CreateApiKeyRequest();
+ request.setName(name);
+ request.setExpiration(expiration);
+
+ if (refreshPolicy != request.getRefreshPolicy() || randomBoolean()) {
+ request.setRefreshPolicy(refreshPolicy);
+ }
+ if (descriptorList.isEmpty() == false || randomBoolean()) {
+ request.setRoleDescriptors(descriptorList);
+ }
+
+ try (BytesStreamOutput out = new BytesStreamOutput()) {
+ request.writeTo(out);
+ try (StreamInput in = out.bytes().streamInput()) {
+ final CreateApiKeyRequest serialized = new CreateApiKeyRequest(in);
+ assertEquals(name, serialized.getName());
+ assertEquals(expiration, serialized.getExpiration());
+ assertEquals(refreshPolicy, serialized.getRefreshPolicy());
+ assertEquals(descriptorList, serialized.getRoleDescriptors());
+ }
+ }
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponseTests.java
new file mode 100644
index 0000000000000..20ff4bc251d15
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateApiKeyResponseTests.java
@@ -0,0 +1,81 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.common.UUIDs;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.settings.SecureString;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractXContentTestCase;
+import org.elasticsearch.test.EqualsHashCodeTestUtils;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class CreateApiKeyResponseTests extends AbstractXContentTestCase {
+
+ @Override
+ protected CreateApiKeyResponse doParseInstance(XContentParser parser) throws IOException {
+ return CreateApiKeyResponse.fromXContent(parser);
+ }
+
+ @Override
+ protected CreateApiKeyResponse createTestInstance() {
+ final String name = randomAlphaOfLengthBetween(1, 256);
+ final SecureString key = new SecureString(UUIDs.randomBase64UUID().toCharArray());
+ final Instant expiration = randomBoolean() ? Instant.now().plus(7L, ChronoUnit.DAYS) : null;
+ final String id = randomAlphaOfLength(100);
+ return new CreateApiKeyResponse(name, id, key, expiration);
+ }
+
+ @Override
+ protected boolean supportsUnknownFields() {
+ return false;
+ }
+
+ public void testSerialization() throws IOException {
+ final CreateApiKeyResponse response = createTestInstance();
+ try (BytesStreamOutput out = new BytesStreamOutput()) {
+ response.writeTo(out);
+ try (StreamInput in = out.bytes().streamInput()) {
+ CreateApiKeyResponse serialized = new CreateApiKeyResponse(in);
+ assertThat(serialized, equalTo(response));
+ }
+ }
+ }
+
+ public void testEqualsHashCode() {
+ CreateApiKeyResponse createApiKeyResponse = createTestInstance();
+
+ EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyResponse, (original) -> {
+ return new CreateApiKeyResponse(original.getName(), original.getId(), original.getKey(), original.getExpiration());
+ });
+ EqualsHashCodeTestUtils.checkEqualsAndHashCode(createApiKeyResponse, (original) -> {
+ return new CreateApiKeyResponse(original.getName(), original.getId(), original.getKey(), original.getExpiration());
+ }, CreateApiKeyResponseTests::mutateTestItem);
+ }
+
+ private static CreateApiKeyResponse mutateTestItem(CreateApiKeyResponse original) {
+ switch (randomIntBetween(0, 3)) {
+ case 0:
+ return new CreateApiKeyResponse(randomAlphaOfLength(5), original.getId(), original.getKey(), original.getExpiration());
+ case 1:
+ return new CreateApiKeyResponse(original.getName(), randomAlphaOfLength(5), original.getKey(), original.getExpiration());
+ case 2:
+ return new CreateApiKeyResponse(original.getName(), original.getId(), new SecureString(UUIDs.randomBase64UUID().toCharArray()),
+ original.getExpiration());
+ case 3:
+ return new CreateApiKeyResponse(original.getName(), original.getId(), original.getKey(), Instant.now());
+ default:
+ return new CreateApiKeyResponse(randomAlphaOfLength(5), original.getId(), original.getKey(), original.getExpiration());
+ }
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestTests.java
new file mode 100644
index 0000000000000..27be0d88eb82c
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyRequestTests.java
@@ -0,0 +1,103 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.io.stream.InputStreamStreamInput;
+import org.elasticsearch.common.io.stream.OutputStreamStreamOutput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+
+public class GetApiKeyRequestTests extends ESTestCase {
+
+ public void testRequestValidation() {
+ GetApiKeyRequest request = GetApiKeyRequest.usingApiKeyId(randomAlphaOfLength(5));
+ ActionRequestValidationException ve = request.validate();
+ assertNull(ve);
+ request = GetApiKeyRequest.usingApiKeyName(randomAlphaOfLength(5));
+ ve = request.validate();
+ assertNull(ve);
+ request = GetApiKeyRequest.usingRealmName(randomAlphaOfLength(5));
+ ve = request.validate();
+ assertNull(ve);
+ request = GetApiKeyRequest.usingUserName(randomAlphaOfLength(5));
+ ve = request.validate();
+ assertNull(ve);
+ request = GetApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), randomAlphaOfLength(7));
+ ve = request.validate();
+ assertNull(ve);
+ }
+
+ public void testRequestValidationFailureScenarios() throws IOException {
+ class Dummy extends ActionRequest {
+ String realm;
+ String user;
+ String apiKeyId;
+ String apiKeyName;
+
+ Dummy(String[] a) {
+ realm = a[0];
+ user = a[1];
+ apiKeyId = a[2];
+ apiKeyName = a[3];
+ }
+
+ @Override
+ public ActionRequestValidationException validate() {
+ return null;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeOptionalString(realm);
+ out.writeOptionalString(user);
+ out.writeOptionalString(apiKeyId);
+ out.writeOptionalString(apiKeyName);
+ }
+ }
+
+ String[][] inputs = new String[][] {
+ { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }),
+ randomFrom(new String[] { null, "" }) },
+ { randomFrom(new String[] { null, "" }), "user", "api-kid", "api-kname" },
+ { "realm", randomFrom(new String[] { null, "" }), "api-kid", "api-kname" },
+ { "realm", "user", "api-kid", randomFrom(new String[] { null, "" }) },
+ { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), "api-kid", "api-kname" } };
+ String[][] expectedErrorMessages = new String[][] { { "One of [api key id, api key name, username, realm name] must be specified" },
+ { "username or realm name must not be specified when the api key id or api key name is specified",
+ "only one of [api key id, api key name] can be specified" },
+ { "username or realm name must not be specified when the api key id or api key name is specified",
+ "only one of [api key id, api key name] can be specified" },
+ { "username or realm name must not be specified when the api key id or api key name is specified" },
+ { "only one of [api key id, api key name] can be specified" } };
+
+ for (int caseNo = 0; caseNo < inputs.length; caseNo++) {
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ OutputStreamStreamOutput osso = new OutputStreamStreamOutput(bos)) {
+ Dummy d = new Dummy(inputs[caseNo]);
+ d.writeTo(osso);
+
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+ InputStreamStreamInput issi = new InputStreamStreamInput(bis);
+
+ GetApiKeyRequest request = new GetApiKeyRequest(issi);
+ ActionRequestValidationException ve = request.validate();
+ assertNotNull(ve);
+ assertEquals(expectedErrorMessages[caseNo].length, ve.validationErrors().size());
+ assertThat(ve.validationErrors(), containsInAnyOrder(expectedErrorMessages[caseNo]));
+ }
+ }
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java
new file mode 100644
index 0000000000000..c278c135edaf8
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/GetApiKeyResponseTests.java
@@ -0,0 +1,64 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.hamcrest.Matchers.equalTo;
+
+public class GetApiKeyResponseTests extends ESTestCase {
+
+ public void testSerialization() throws IOException {
+ boolean withExpiration = randomBoolean();
+ ApiKey apiKeyInfo = createApiKeyInfo(randomAlphaOfLength(4), randomAlphaOfLength(5), Instant.now(),
+ (withExpiration) ? Instant.now() : null, false, randomAlphaOfLength(4), randomAlphaOfLength(5));
+ GetApiKeyResponse response = new GetApiKeyResponse(Collections.singletonList(apiKeyInfo));
+ try (BytesStreamOutput output = new BytesStreamOutput()) {
+ response.writeTo(output);
+ try (StreamInput input = output.bytes().streamInput()) {
+ GetApiKeyResponse serialized = new GetApiKeyResponse(input);
+ assertThat(serialized.getApiKeyInfos(), equalTo(response.getApiKeyInfos()));
+ }
+ }
+ }
+
+ public void testToXContent() throws IOException {
+ ApiKey apiKeyInfo1 = createApiKeyInfo("name1", "id-1", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), false,
+ "user-a", "realm-x");
+ ApiKey apiKeyInfo2 = createApiKeyInfo("name2", "id-2", Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true,
+ "user-b", "realm-y");
+ GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2));
+ XContentBuilder builder = XContentFactory.jsonBuilder();
+ response.toXContent(builder, ToXContent.EMPTY_PARAMS);
+ assertThat(Strings.toString(builder), equalTo(
+ "{"
+ + "\"api_keys\":["
+ + "{\"id\":\"id-1\",\"name\":\"name1\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":false,"
+ + "\"username\":\"user-a\",\"realm\":\"realm-x\"},"
+ + "{\"id\":\"id-2\",\"name\":\"name2\",\"creation\":100000,\"expiration\":10000000,\"invalidated\":true,"
+ + "\"username\":\"user-b\",\"realm\":\"realm-y\"}"
+ + "]"
+ + "}"));
+ }
+
+ private ApiKey createApiKeyInfo(String name, String id, Instant creation, Instant expiration, boolean invalidated, String username,
+ String realm) {
+ return new ApiKey(name, id, creation, expiration, invalidated, username, realm);
+ }
+}
+
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestTests.java
new file mode 100644
index 0000000000000..3d7fd90234286
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyRequestTests.java
@@ -0,0 +1,104 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.common.io.stream.InputStreamStreamInput;
+import org.elasticsearch.common.io.stream.OutputStreamStreamOutput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+
+public class InvalidateApiKeyRequestTests extends ESTestCase {
+
+ public void testRequestValidation() {
+ InvalidateApiKeyRequest request = InvalidateApiKeyRequest.usingApiKeyId(randomAlphaOfLength(5));
+ ActionRequestValidationException ve = request.validate();
+ assertNull(ve);
+ request = InvalidateApiKeyRequest.usingApiKeyName(randomAlphaOfLength(5));
+ ve = request.validate();
+ assertNull(ve);
+ request = InvalidateApiKeyRequest.usingRealmName(randomAlphaOfLength(5));
+ ve = request.validate();
+ assertNull(ve);
+ request = InvalidateApiKeyRequest.usingUserName(randomAlphaOfLength(5));
+ ve = request.validate();
+ assertNull(ve);
+ request = InvalidateApiKeyRequest.usingRealmAndUserName(randomAlphaOfLength(5), randomAlphaOfLength(7));
+ ve = request.validate();
+ assertNull(ve);
+ }
+
+ public void testRequestValidationFailureScenarios() throws IOException {
+ class Dummy extends ActionRequest {
+ String realm;
+ String user;
+ String apiKeyId;
+ String apiKeyName;
+
+ Dummy(String[] a) {
+ realm = a[0];
+ user = a[1];
+ apiKeyId = a[2];
+ apiKeyName = a[3];
+ }
+
+ @Override
+ public ActionRequestValidationException validate() {
+ return null;
+ }
+
+ @Override
+ public void writeTo(StreamOutput out) throws IOException {
+ super.writeTo(out);
+ out.writeOptionalString(realm);
+ out.writeOptionalString(user);
+ out.writeOptionalString(apiKeyId);
+ out.writeOptionalString(apiKeyName);
+ }
+ }
+
+ String[][] inputs = new String[][] {
+ { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }),
+ randomFrom(new String[] { null, "" }) },
+ { randomFrom(new String[] { null, "" }), "user", "api-kid", "api-kname" },
+ { "realm", randomFrom(new String[] { null, "" }), "api-kid", "api-kname" },
+ { "realm", "user", "api-kid", randomFrom(new String[] { null, "" }) },
+ { randomFrom(new String[] { null, "" }), randomFrom(new String[] { null, "" }), "api-kid", "api-kname" } };
+ String[][] expectedErrorMessages = new String[][] { { "One of [api key id, api key name, username, realm name] must be specified" },
+ { "username or realm name must not be specified when the api key id or api key name is specified",
+ "only one of [api key id, api key name] can be specified" },
+ { "username or realm name must not be specified when the api key id or api key name is specified",
+ "only one of [api key id, api key name] can be specified" },
+ { "username or realm name must not be specified when the api key id or api key name is specified" },
+ { "only one of [api key id, api key name] can be specified" } };
+
+
+ for (int caseNo = 0; caseNo < inputs.length; caseNo++) {
+ try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ OutputStreamStreamOutput osso = new OutputStreamStreamOutput(bos)) {
+ Dummy d = new Dummy(inputs[caseNo]);
+ d.writeTo(osso);
+
+ ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
+ InputStreamStreamInput issi = new InputStreamStreamInput(bis);
+
+ InvalidateApiKeyRequest request = new InvalidateApiKeyRequest(issi);
+ ActionRequestValidationException ve = request.validate();
+ assertNotNull(ve);
+ assertEquals(expectedErrorMessages[caseNo].length, ve.validationErrors().size());
+ assertThat(ve.validationErrors(), containsInAnyOrder(expectedErrorMessages[caseNo]));
+ }
+ }
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponseTests.java
new file mode 100644
index 0000000000000..f4606a4f20f1b
--- /dev/null
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/InvalidateApiKeyResponseTests.java
@@ -0,0 +1,88 @@
+/*
+ * 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.action;
+
+import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.common.Strings;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.test.ESTestCase;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+public class InvalidateApiKeyResponseTests extends ESTestCase {
+
+ public void testSerialization() throws IOException {
+ InvalidateApiKeyResponse response = new InvalidateApiKeyResponse(Arrays.asList("api-key-id-1"),
+ Arrays.asList("api-key-id-2", "api-key-id-3"),
+ Arrays.asList(new ElasticsearchException("error1"),
+ new ElasticsearchException("error2")));
+ try (BytesStreamOutput output = new BytesStreamOutput()) {
+ response.writeTo(output);
+ try (StreamInput input = output.bytes().streamInput()) {
+ InvalidateApiKeyResponse serialized = new InvalidateApiKeyResponse(input);
+ assertThat(serialized.getInvalidatedApiKeys(), equalTo(response.getInvalidatedApiKeys()));
+ assertThat(serialized.getPreviouslyInvalidatedApiKeys(),
+ equalTo(response.getPreviouslyInvalidatedApiKeys()));
+ assertThat(serialized.getErrors().size(), equalTo(response.getErrors().size()));
+ assertThat(serialized.getErrors().get(0).toString(), containsString("error1"));
+ assertThat(serialized.getErrors().get(1).toString(), containsString("error2"));
+ }
+ }
+
+ response = new InvalidateApiKeyResponse(Arrays.asList(generateRandomStringArray(20, 15, false)),
+ Arrays.asList(generateRandomStringArray(20, 15, false)),
+ Collections.emptyList());
+ try (BytesStreamOutput output = new BytesStreamOutput()) {
+ response.writeTo(output);
+ try (StreamInput input = output.bytes().streamInput()) {
+ InvalidateApiKeyResponse serialized = new InvalidateApiKeyResponse(input);
+ assertThat(serialized.getInvalidatedApiKeys(), equalTo(response.getInvalidatedApiKeys()));
+ assertThat(serialized.getPreviouslyInvalidatedApiKeys(),
+ equalTo(response.getPreviouslyInvalidatedApiKeys()));
+ assertThat(serialized.getErrors().size(), equalTo(response.getErrors().size()));
+ }
+ }
+ }
+
+ public void testToXContent() throws IOException {
+ InvalidateApiKeyResponse response = new InvalidateApiKeyResponse(Arrays.asList("api-key-id-1"),
+ Arrays.asList("api-key-id-2", "api-key-id-3"),
+ Arrays.asList(new ElasticsearchException("error1", new IllegalArgumentException("msg - 1")),
+ new ElasticsearchException("error2", new IllegalArgumentException("msg - 2"))));
+ XContentBuilder builder = XContentFactory.jsonBuilder();
+ response.toXContent(builder, ToXContent.EMPTY_PARAMS);
+ assertThat(Strings.toString(builder),
+ equalTo("{" +
+ "\"invalidated_api_keys\":[\"api-key-id-1\"]," +
+ "\"previously_invalidated_api_keys\":[\"api-key-id-2\",\"api-key-id-3\"]," +
+ "\"error_count\":2," +
+ "\"error_details\":[" +
+ "{\"type\":\"exception\"," +
+ "\"reason\":\"error1\"," +
+ "\"caused_by\":{" +
+ "\"type\":\"illegal_argument_exception\"," +
+ "\"reason\":\"msg - 1\"}" +
+ "}," +
+ "{\"type\":\"exception\"," +
+ "\"reason\":\"error2\"," +
+ "\"caused_by\":" +
+ "{\"type\":\"illegal_argument_exception\"," +
+ "\"reason\":\"msg - 2\"}" +
+ "}" +
+ "]" +
+ "}"));
+ }
+}
diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java
index 0481e01e74ac3..a605917f01c2d 100644
--- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java
+++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/user/HasPrivilegesResponseTests.java
@@ -18,6 +18,7 @@
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.protocol.AbstractHlrcStreamableXContentTestCase;
import org.elasticsearch.test.VersionUtils;
+import org.elasticsearch.xpack.core.security.authz.permission.ResourcePrivileges;
import org.hamcrest.Matchers;
import java.io.IOException;
@@ -59,16 +60,17 @@ public void testSerializationV63() throws IOException {
}
public void testToXContent() throws Exception {
- final HasPrivilegesResponse response = new HasPrivilegesResponse("daredevil", false,
- Collections.singletonMap("manage", true),
- Arrays.asList(
- new HasPrivilegesResponse.ResourcePrivileges("staff",
- MapBuilder.newMapBuilder(new LinkedHashMap<>())
- .put("read", true).put("index", true).put("delete", false).put("manage", false).map()),
- new HasPrivilegesResponse.ResourcePrivileges("customers",
- MapBuilder.newMapBuilder(new LinkedHashMap<>())
- .put("read", true).put("index", true).put("delete", true).put("manage", false).map())
- ), Collections.emptyMap());
+ final HasPrivilegesResponse response = new HasPrivilegesResponse("daredevil", false, Collections.singletonMap("manage", true),
+ Arrays.asList(
+ ResourcePrivileges.builder("staff")
+ .addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<>()).put("read", true)
+ .put("index", true).put("delete", false).put("manage", false).map())
+ .build(),
+ ResourcePrivileges.builder("customers")
+ .addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<>()).put("read", true)
+ .put("index", true).put("delete", true).put("manage", false).map())
+ .build()),
+ Collections.emptyMap());
final XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent());
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
@@ -120,9 +122,9 @@ public HasPrivilegesResponse convertHlrcToInternal(org.elasticsearch.client.secu
);
}
- private static List toResourcePrivileges(Map> map) {
+ private static List toResourcePrivileges(Map> map) {
return map.entrySet().stream()
- .map(e -> new HasPrivilegesResponse.ResourcePrivileges(e.getKey(), e.getValue()))
+ .map(e -> ResourcePrivileges.builder(e.getKey()).addPrivileges(e.getValue()).build())
.collect(Collectors.toList());
}
@@ -146,23 +148,23 @@ private HasPrivilegesResponse randomResponse() {
for (String priv : randomArray(1, 6, String[]::new, () -> randomAlphaOfLengthBetween(3, 12))) {
cluster.put(priv, randomBoolean());
}
- final Collection index = randomResourcePrivileges();
- final Map> application = new HashMap<>();
+ final Collection index = randomResourcePrivileges();
+ final Map> application = new HashMap<>();
for (String app : randomArray(1, 3, String[]::new, () -> randomAlphaOfLengthBetween(3, 6).toLowerCase(Locale.ROOT))) {
application.put(app, randomResourcePrivileges());
}
return new HasPrivilegesResponse(username, randomBoolean(), cluster, index, application);
}
- private Collection