From 7625930c8975f76fe252206d259189d9e084a476 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 15 Mar 2019 12:24:25 +1100 Subject: [PATCH 1/4] Add granular API key privileges In the current implementation of API keys, to create/get/invalidate API keys one needs to be super user which limits the usage of API keys. We would want to have fine grained privileges rather than system wide privileges for using API keys. This commit adds: - `manage_api_key` cluster privilege which allows users to create, retrieve and invalidate **_any_** API keys in the system. This allows for limited access than `manage_security` or `all`. - `owner_manage_api_key` cluster privilege which allows user to create, retrieve and invalidate API keys owned by this user only. - `create_api_key` is a sub privilege which allows for user to create but not invalidate API keys. - an API key with no api key manage privilege can retrieve its own information Also introduces following rest APIs to manage owned API keys for a user: GET /_security/api_key/my DELETE /_security/api_key/my --- .../elasticsearch/client/SecurityClient.java | 63 ++++ .../client/SecurityRequestConverters.java | 20 +- .../client/security/GetMyApiKeyRequest.java | 73 +++++ .../security/InvalidateMyApiKeyRequest.java | 79 +++++ .../client/security/user/privileges/Role.java | 6 +- .../SecurityRequestConvertersTests.java | 22 ++ .../SecurityDocumentationIT.java | 216 ++++++++++++ .../security/get-my-api-key.asciidoc | 51 +++ .../security/invalidate-my-api-key.asciidoc | 59 ++++ .../high-level/supported-apis.asciidoc | 2 + x-pack/docs/en/rest-api/security.asciidoc | 4 + .../security/get-my-api-keys.asciidoc | 125 +++++++ .../security/invalidate-my-api-keys.asciidoc | 140 ++++++++ .../xpack/core/XPackClientPlugin.java | 4 + .../security/action/GetApiKeyRequest.java | 6 + .../security/action/GetMyApiKeyAction.java | 33 ++ .../security/action/GetMyApiKeyRequest.java | 88 +++++ .../action/InvalidateApiKeyRequest.java | 5 + .../action/InvalidateMyApiKeyAction.java | 33 ++ .../action/InvalidateMyApiKeyRequest.java | 88 +++++ .../authz/privilege/ClusterPrivilege.java | 12 + .../core/security/client/SecurityClient.java | 12 + .../xpack/security/Security.java | 14 +- .../action/TransportGetApiKeyAction.java | 11 +- .../action/TransportGetMyApiKeyAction.java | 36 ++ .../TransportInvalidateApiKeyAction.java | 9 +- .../TransportInvalidateMyApiKeyAction.java | 37 +++ .../xpack/security/authc/ApiKeyService.java | 307 +++++++++--------- .../xpack/security/authz/RBACEngine.java | 45 +-- .../rest/action/RestCreateApiKeyAction.java | 2 +- .../rest/action/RestGetApiKeyAction.java | 2 +- .../rest/action/RestGetMyApiKeyAction.java | 60 ++++ .../action/RestInvalidateApiKeyAction.java | 2 +- .../action/RestInvalidateMyApiKeyAction.java | 68 ++++ .../security/authc/ApiKeyIntegTests.java | 237 +++++++++++++- .../xpack/security/authz/RBACEngineTests.java | 19 ++ .../api/security.get_my_api_key.json | 22 ++ .../api/security.invalidate_my_api_key.json | 15 + 38 files changed, 1822 insertions(+), 205 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetMyApiKeyRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateMyApiKeyRequest.java create mode 100644 docs/java-rest/high-level/security/get-my-api-key.asciidoc create mode 100644 docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/get-my-api-keys.asciidoc create mode 100644 x-pack/docs/en/rest-api/security/invalidate-my-api-keys.asciidoc create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetMyApiKeyAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetMyApiKeyRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateMyApiKeyAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateMyApiKeyRequest.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetMyApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateMyApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetMyApiKeyAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateMyApiKeyAction.java create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/security.get_my_api_key.json create mode 100644 x-pack/plugin/src/test/resources/rest-api-spec/api/security.invalidate_my_api_key.json diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java index fefb5771dc801..67793d4225261 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java @@ -43,6 +43,7 @@ import org.elasticsearch.client.security.EnableUserRequest; import org.elasticsearch.client.security.GetApiKeyRequest; import org.elasticsearch.client.security.GetApiKeyResponse; +import org.elasticsearch.client.security.GetMyApiKeyRequest; import org.elasticsearch.client.security.GetPrivilegesRequest; import org.elasticsearch.client.security.GetPrivilegesResponse; import org.elasticsearch.client.security.GetRoleMappingsRequest; @@ -59,6 +60,7 @@ import org.elasticsearch.client.security.HasPrivilegesResponse; import org.elasticsearch.client.security.InvalidateApiKeyRequest; import org.elasticsearch.client.security.InvalidateApiKeyResponse; +import org.elasticsearch.client.security.InvalidateMyApiKeyRequest; import org.elasticsearch.client.security.InvalidateTokenRequest; import org.elasticsearch.client.security.InvalidateTokenResponse; import org.elasticsearch.client.security.PutPrivilegesRequest; @@ -909,6 +911,36 @@ public void getApiKeyAsync(final GetApiKeyRequest request, final RequestOptions GetApiKeyResponse::fromXContent, listener, emptySet()); } + /** + * Retrieve information for API key(s) owned by authenticated user.
+ * See + * the docs for more. + * + * @param request the request to retrieve API key(s) + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response from the create API key call + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public GetApiKeyResponse getMyApiKey(final GetMyApiKeyRequest request, final RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::getMyApiKey, options, + GetApiKeyResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously retrieve information for API key(s) owned by authenticated user.
+ * See + * the docs for more. + * + * @param request the request to retrieve API key(s) + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void getMyApiKeyAsync(final GetMyApiKeyRequest request, final RequestOptions options, + final ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::getMyApiKey, options, + GetApiKeyResponse::fromXContent, listener, emptySet()); + } + /** * Invalidate API Key(s).
* See @@ -939,4 +971,35 @@ public void invalidateApiKeyAsync(final InvalidateApiKeyRequest request, final R restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::invalidateApiKey, options, InvalidateApiKeyResponse::fromXContent, listener, emptySet()); } + + /** + * Invalidate API key(s) owned by authenticated user.
+ * See
+ * the docs for more. + * + * @param request the request to invalidate API key(s) + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response from the invalidate API key call + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public InvalidateApiKeyResponse invalidateMyApiKey(final InvalidateMyApiKeyRequest request, final RequestOptions options) + throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::invalidateMyApiKey, options, + InvalidateApiKeyResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously invalidates API key(s) owned by authenticated user.
+ * See + * the docs for more. + * + * @param request the request to invalidate API key(s) + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public void invalidateMyApiKeyAsync(final InvalidateMyApiKeyRequest request, final RequestOptions options, + final ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::invalidateMyApiKey, options, + InvalidateApiKeyResponse::fromXContent, listener, emptySet()); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java index f686167e211bb..5c8790af65f7e 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java @@ -35,12 +35,14 @@ import org.elasticsearch.client.security.DisableUserRequest; import org.elasticsearch.client.security.EnableUserRequest; import org.elasticsearch.client.security.GetApiKeyRequest; +import org.elasticsearch.client.security.GetMyApiKeyRequest; import org.elasticsearch.client.security.GetPrivilegesRequest; import org.elasticsearch.client.security.GetRoleMappingsRequest; import org.elasticsearch.client.security.GetRolesRequest; import org.elasticsearch.client.security.GetUsersRequest; import org.elasticsearch.client.security.HasPrivilegesRequest; import org.elasticsearch.client.security.InvalidateApiKeyRequest; +import org.elasticsearch.client.security.InvalidateMyApiKeyRequest; import org.elasticsearch.client.security.InvalidateTokenRequest; import org.elasticsearch.client.security.PutPrivilegesRequest; import org.elasticsearch.client.security.PutRoleMappingRequest; @@ -285,10 +287,26 @@ static Request getApiKey(final GetApiKeyRequest getApiKeyRequest) throws IOExcep return request; } + static Request getMyApiKey(final GetMyApiKeyRequest getMyApiKeyRequest) throws IOException { + final Request request = new Request(HttpGet.METHOD_NAME, "/_security/api_key/my"); + if (Strings.hasText(getMyApiKeyRequest.getId())) { + request.addParameter("id", getMyApiKeyRequest.getId()); + } + if (Strings.hasText(getMyApiKeyRequest.getName())) { + request.addParameter("name", getMyApiKeyRequest.getName()); + } + return request; + } + static Request invalidateApiKey(final InvalidateApiKeyRequest invalidateApiKeyRequest) throws IOException { final Request request = new Request(HttpDelete.METHOD_NAME, "/_security/api_key"); request.setEntity(createEntity(invalidateApiKeyRequest, REQUEST_BODY_CONTENT_TYPE)); - final RequestConverters.Params params = new RequestConverters.Params(request); + return request; + } + + static Request invalidateMyApiKey(final InvalidateMyApiKeyRequest invalidateMyApiKeyRequest) throws IOException { + final Request request = new Request(HttpDelete.METHOD_NAME, "/_security/api_key/my"); + request.setEntity(createEntity(invalidateMyApiKeyRequest, REQUEST_BODY_CONTENT_TYPE)); return request; } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetMyApiKeyRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetMyApiKeyRequest.java new file mode 100644 index 0000000000000..800b01262d48f --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/GetMyApiKeyRequest.java @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security; + +import org.elasticsearch.client.Validatable; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Request for retrieving information for API key(s) owned by the authenticated user. + */ +public final class GetMyApiKeyRequest implements Validatable, ToXContentObject { + + private final String id; + private final String name; + + public GetMyApiKeyRequest(@Nullable String apiKeyId, @Nullable String apiKeyName) { + this.id = apiKeyId; + this.name = apiKeyName; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + /** + * Creates request for given api key id + * @param apiKeyId api key id + * @return {@link GetMyApiKeyRequest} + */ + public static GetMyApiKeyRequest usingApiKeyId(String apiKeyId) { + return new GetMyApiKeyRequest(apiKeyId, null); + } + + /** + * Creates request for given api key name + * @param apiKeyName api key name + * @return {@link GetMyApiKeyRequest} + */ + public static GetMyApiKeyRequest usingApiKeyName(String apiKeyName) { + return new GetMyApiKeyRequest(null, apiKeyName); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder; + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateMyApiKeyRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateMyApiKeyRequest.java new file mode 100644 index 0000000000000..2f276caceb297 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/InvalidateMyApiKeyRequest.java @@ -0,0 +1,79 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.elasticsearch.client.security; + +import org.elasticsearch.client.Validatable; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; + +/** + * Request for invalidating API key(s) for the authenticated user so that it can no longer be used. + */ +public final class InvalidateMyApiKeyRequest implements Validatable, ToXContentObject { + + private final String id; + private final String name; + + public InvalidateMyApiKeyRequest(@Nullable String apiKeyId, @Nullable String apiKeyName) { + this.id = apiKeyId; + this.name = apiKeyName; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + /** + * Creates invalidate API key request for given api key id + * @param apiKeyId api key id + * @return {@link InvalidateMyApiKeyRequest} + */ + public static InvalidateMyApiKeyRequest usingApiKeyId(String apiKeyId) { + return new InvalidateMyApiKeyRequest(apiKeyId, null); + } + + /** + * Creates invalidate API key request for given api key name + * @param apiKeyName api key name + * @return {@link InvalidateMyApiKeyRequest} + */ + public static InvalidateMyApiKeyRequest usingApiKeyName(String apiKeyName) { + return new InvalidateMyApiKeyRequest(null, apiKeyName); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (id != null) { + builder.field("id", id); + } + if (name != null) { + builder.field("name", name); + } + return builder.endObject(); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java index c6dc6910d97b0..cb5778954eaff 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java @@ -311,6 +311,9 @@ public static class ClusterPrivilegeName { public static final String TRANSPORT_CLIENT = "transport_client"; public static final String MANAGE_SECURITY = "manage_security"; public static final String MANAGE_SAML = "manage_saml"; + public static final String MANAGE_API_KEY = "manage_api_key"; + public static final String OWNER_MANAGE_API_KEY = "owner_manage_api_key"; + public static final String CREATE_API_KEY = "create_api_key"; public static final String MANAGE_TOKEN = "manage_token"; public static final String MANAGE_PIPELINE = "manage_pipeline"; public static final String MANAGE_CCR = "manage_ccr"; @@ -319,7 +322,8 @@ public static class ClusterPrivilegeName { public static final String READ_ILM = "read_ilm"; public static final String[] ALL_ARRAY = new String[] { NONE, ALL, MONITOR, MONITOR_ML, MONITOR_WATCHER, MONITOR_ROLLUP, MANAGE, MANAGE_ML, MANAGE_WATCHER, MANAGE_ROLLUP, MANAGE_INDEX_TEMPLATES, MANAGE_INGEST_PIPELINES, TRANSPORT_CLIENT, - MANAGE_SECURITY, MANAGE_SAML, MANAGE_TOKEN, MANAGE_PIPELINE, MANAGE_CCR, READ_CCR, MANAGE_ILM, READ_ILM }; + MANAGE_SECURITY, MANAGE_SAML, MANAGE_API_KEY, OWNER_MANAGE_API_KEY, CREATE_API_KEY, MANAGE_TOKEN, MANAGE_PIPELINE, + MANAGE_CCR, READ_CCR, MANAGE_ILM, READ_ILM }; } /** diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java index 1176cabcc3d9c..1415e986e70f1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java @@ -33,11 +33,13 @@ import org.elasticsearch.client.security.DisableUserRequest; import org.elasticsearch.client.security.EnableUserRequest; import org.elasticsearch.client.security.GetApiKeyRequest; +import org.elasticsearch.client.security.GetMyApiKeyRequest; import org.elasticsearch.client.security.GetPrivilegesRequest; import org.elasticsearch.client.security.GetRoleMappingsRequest; import org.elasticsearch.client.security.GetRolesRequest; import org.elasticsearch.client.security.GetUsersRequest; import org.elasticsearch.client.security.InvalidateApiKeyRequest; +import org.elasticsearch.client.security.InvalidateMyApiKeyRequest; import org.elasticsearch.client.security.PutPrivilegesRequest; import org.elasticsearch.client.security.PutRoleMappingRequest; import org.elasticsearch.client.security.PutRoleRequest; @@ -461,4 +463,24 @@ public void testInvalidateApiKey() throws IOException { assertEquals("/_security/api_key", request.getEndpoint()); assertToXContentBody(invalidateApiKeyRequest, request.getEntity()); } + + public void testGetMyApiKey() throws IOException { + String apiKeyId = randomAlphaOfLength(5); + final GetMyApiKeyRequest getApiKeyRequest = GetMyApiKeyRequest.usingApiKeyId(apiKeyId); + final Request request = SecurityRequestConverters.getMyApiKey(getApiKeyRequest); + assertEquals(HttpGet.METHOD_NAME, request.getMethod()); + assertEquals("/_security/api_key/my", request.getEndpoint()); + Map mapOfParameters = new HashMap<>(); + mapOfParameters.put("id", apiKeyId); + assertThat(request.getParameters(), equalTo(mapOfParameters)); + } + + public void testInvalidateMyApiKey() throws IOException { + String apiKeyId = randomAlphaOfLength(5); + final InvalidateMyApiKeyRequest invalidateApiKeyRequest = new InvalidateMyApiKeyRequest(apiKeyId, null); + final Request request = SecurityRequestConverters.invalidateMyApiKey(invalidateApiKeyRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_security/api_key/my", request.getEndpoint()); + assertToXContentBody(invalidateApiKeyRequest, request.getEntity()); + } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index b095ca5a9a0db..b00e249166414 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -50,6 +50,7 @@ import org.elasticsearch.client.security.ExpressionRoleMapping; import org.elasticsearch.client.security.GetApiKeyRequest; import org.elasticsearch.client.security.GetApiKeyResponse; +import org.elasticsearch.client.security.GetMyApiKeyRequest; import org.elasticsearch.client.security.GetPrivilegesRequest; import org.elasticsearch.client.security.GetPrivilegesResponse; import org.elasticsearch.client.security.GetRoleMappingsRequest; @@ -64,6 +65,7 @@ import org.elasticsearch.client.security.HasPrivilegesResponse; import org.elasticsearch.client.security.InvalidateApiKeyRequest; import org.elasticsearch.client.security.InvalidateApiKeyResponse; +import org.elasticsearch.client.security.InvalidateMyApiKeyRequest; import org.elasticsearch.client.security.InvalidateTokenRequest; import org.elasticsearch.client.security.InvalidateTokenResponse; import org.elasticsearch.client.security.PutPrivilegesRequest; @@ -1959,6 +1961,97 @@ public void onFailure(Exception e) { } } + public void testGetMyApiKey() throws Exception { + RestHighLevelClient client = highLevelClient(); + + List roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build()); + final TimeValue expiration = TimeValue.timeValueHours(24); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + // Create API Keys + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("k1", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse1 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse1.getName(), equalTo("k1")); + assertNotNull(createApiKeyResponse1.getKey()); + + final ApiKey expectedApiKeyInfo = new ApiKey(createApiKeyResponse1.getName(), createApiKeyResponse1.getId(), Instant.now(), + Instant.now().plusMillis(expiration.getMillis()), false, "test_user", "default_file"); + { + // tag::get-my-api-key-id-request + GetMyApiKeyRequest getApiKeyRequest = GetMyApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId()); + // end::get-my-api-key-id-request + + // tag::get-my-api-key-execute + GetApiKeyResponse getApiKeyResponse = client.security().getMyApiKey(getApiKeyRequest, RequestOptions.DEFAULT); + // end::get-my-api-key-execute + + assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue())); + assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1)); + verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + + { + // tag::get-my-api-key-name-request + GetMyApiKeyRequest getApiKeyRequest = GetMyApiKeyRequest.usingApiKeyName(createApiKeyResponse1.getName()); + // end::get-my-api-key-name-request + + GetApiKeyResponse getApiKeyResponse = client.security().getMyApiKey(getApiKeyRequest, RequestOptions.DEFAULT); + + assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue())); + assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1)); + verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + + { + // tag::get-my-api-key-all-request + GetMyApiKeyRequest getApiKeyRequest = new GetMyApiKeyRequest(null, null); + // end::get-my-api-key-all-request + + GetApiKeyResponse getApiKeyResponse = client.security().getMyApiKey(getApiKeyRequest, RequestOptions.DEFAULT); + + assertThat(getApiKeyResponse.getApiKeyInfos(), is(notNullValue())); + assertThat(getApiKeyResponse.getApiKeyInfos().size(), is(1)); + verifyApiKey(getApiKeyResponse.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + + { + GetMyApiKeyRequest getApiKeyRequest = GetMyApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId()); + + ActionListener listener; + // tag::get-my-api-key-execute-listener + listener = new ActionListener() { + @Override + public void onResponse(GetApiKeyResponse getApiKeyResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::get-my-api-key-execute-listener + + // Avoid unused variable warning + assertNotNull(listener); + + // Replace the empty listener by a blocking listener in test + final PlainActionFuture future = new PlainActionFuture<>(); + listener = future; + + // tag::get-my-api-key-execute-async + client.security().getMyApiKeyAsync(getApiKeyRequest, RequestOptions.DEFAULT, listener); // <1> + // end::get-my-api-key-execute-async + + final GetApiKeyResponse response = future.get(30, TimeUnit.SECONDS); + assertNotNull(response); + + assertThat(response.getApiKeyInfos(), is(notNullValue())); + assertThat(response.getApiKeyInfos().size(), is(1)); + verifyApiKey(response.getApiKeyInfos().get(0), expectedApiKeyInfo); + } + } + private void verifyApiKey(final ApiKey actual, final ApiKey expected) { assertThat(actual.getId(), is(expected.getId())); assertThat(actual.getName(), is(expected.getName())); @@ -2138,4 +2231,127 @@ public void onFailure(Exception e) { assertThat(response.getPreviouslyInvalidatedApiKeys().size(), equalTo(0)); } } + + public void testInvalidateMyApiKey() throws Exception { + RestHighLevelClient client = highLevelClient(); + + List roles = Collections.singletonList(Role.builder().name("r1").clusterPrivileges(ClusterPrivilegeName.ALL) + .indicesPrivileges(IndicesPrivileges.builder().indices("ind-x").privileges(IndexPrivilegeName.ALL).build()).build()); + final TimeValue expiration = TimeValue.timeValueHours(24); + final RefreshPolicy refreshPolicy = randomFrom(RefreshPolicy.values()); + // Create API Keys + CreateApiKeyRequest createApiKeyRequest = new CreateApiKeyRequest("k1", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse1 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse1.getName(), equalTo("k1")); + assertNotNull(createApiKeyResponse1.getKey()); + + { + // tag::invalidate-my-api-key-id-request + InvalidateMyApiKeyRequest invalidateApiKeyRequest = InvalidateMyApiKeyRequest.usingApiKeyId(createApiKeyResponse1.getId()); + // end::invalidate-my-api-key-id-request + + // tag::invalidate-my-api-key-execute + InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateMyApiKey(invalidateApiKeyRequest, + RequestOptions.DEFAULT); + // end::invalidate-my-api-key-execute + + final List errors = invalidateApiKeyResponse.getErrors(); + final List invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys(); + final List previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys(); + + assertTrue(errors.isEmpty()); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse1.getId()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0)); + } + + { + createApiKeyRequest = new CreateApiKeyRequest("k2", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse2 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse2.getName(), equalTo("k2")); + assertNotNull(createApiKeyResponse2.getKey()); + + // tag::invalidate-my-api-key-name-request + InvalidateMyApiKeyRequest invalidateApiKeyRequest = InvalidateMyApiKeyRequest.usingApiKeyName(createApiKeyResponse2.getName()); + // end::invalidate-my-api-key-name-request + + InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateMyApiKey(invalidateApiKeyRequest, + RequestOptions.DEFAULT); + + final List errors = invalidateApiKeyResponse.getErrors(); + final List invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys(); + final List previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys(); + + assertTrue(errors.isEmpty()); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse2.getId()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0)); + } + + { + createApiKeyRequest = new CreateApiKeyRequest("k3", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse3 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse3.getName(), equalTo("k3")); + assertNotNull(createApiKeyResponse3.getKey()); + + // tag::invalidate-my-api-key-all-request + InvalidateMyApiKeyRequest invalidateApiKeyRequest = new InvalidateMyApiKeyRequest(null, null); + // end::invalidate-my-api-key-all-request + + InvalidateApiKeyResponse invalidateApiKeyResponse = client.security().invalidateMyApiKey(invalidateApiKeyRequest, + RequestOptions.DEFAULT); + + final List errors = invalidateApiKeyResponse.getErrors(); + final List invalidatedApiKeyIds = invalidateApiKeyResponse.getInvalidatedApiKeys(); + final List previouslyInvalidatedApiKeyIds = invalidateApiKeyResponse.getPreviouslyInvalidatedApiKeys(); + + assertTrue(errors.isEmpty()); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse3.getId()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(previouslyInvalidatedApiKeyIds.size(), equalTo(0)); + } + + { + createApiKeyRequest = new CreateApiKeyRequest("k6", roles, expiration, refreshPolicy); + CreateApiKeyResponse createApiKeyResponse6 = client.security().createApiKey(createApiKeyRequest, RequestOptions.DEFAULT); + assertThat(createApiKeyResponse6.getName(), equalTo("k6")); + assertNotNull(createApiKeyResponse6.getKey()); + + InvalidateMyApiKeyRequest invalidateApiKeyRequest = InvalidateMyApiKeyRequest.usingApiKeyId(createApiKeyResponse6.getId()); + + ActionListener listener; + // tag::invalidate-my-api-key-execute-listener + listener = new ActionListener() { + @Override + public void onResponse(InvalidateApiKeyResponse invalidateApiKeyResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::invalidate-my-api-key-execute-listener + + // Avoid unused variable warning + assertNotNull(listener); + + // Replace the empty listener by a blocking listener in test + final PlainActionFuture future = new PlainActionFuture<>(); + listener = future; + + // tag::invalidate-my-api-key-execute-async + client.security().invalidateMyApiKeyAsync(invalidateApiKeyRequest, RequestOptions.DEFAULT, listener); // <1> + // end::invalidate-my-api-key-execute-async + + final InvalidateApiKeyResponse response = future.get(30, TimeUnit.SECONDS); + assertNotNull(response); + final List invalidatedApiKeyIds = response.getInvalidatedApiKeys(); + List expectedInvalidatedApiKeyIds = Arrays.asList(createApiKeyResponse6.getId()); + assertTrue(response.getErrors().isEmpty()); + assertThat(invalidatedApiKeyIds, containsInAnyOrder(expectedInvalidatedApiKeyIds.toArray(Strings.EMPTY_ARRAY))); + assertThat(response.getPreviouslyInvalidatedApiKeys().size(), equalTo(0)); + } + } } diff --git a/docs/java-rest/high-level/security/get-my-api-key.asciidoc b/docs/java-rest/high-level/security/get-my-api-key.asciidoc new file mode 100644 index 0000000000000..9c5d80e34fd68 --- /dev/null +++ b/docs/java-rest/high-level/security/get-my-api-key.asciidoc @@ -0,0 +1,51 @@ +-- +:api: get-my-api-key +:request: GetApiKeyRequest +:response: GetApiKeyResponse +-- + +[id="{upid}-{api}"] +=== Get information for API key(s) owned by authenticated user API + +API Key(s) information owned by authenticated user can be retrieved using this API. + +[id="{upid}-{api}-request"] +==== Get My API Key Request +The +{request}+ supports retrieving API key information for + +. A specific API key by api key id or by name + +. All API keys of current logged in user + +===== Retrieve information for a specific API key by its id +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[get-my-api-key-id-request] +-------------------------------------------------- + +===== Retrieve information for a specific API key by its name +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[get-my-api-key-name-request] +-------------------------------------------------- + +===== Retrieve information for all API key(s) owned by authenticated user +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[get-my-api-key-all-request] +-------------------------------------------------- + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Get My API Key information API Response + +The returned +{response}+ contains the information regarding the API keys that were +requested. + +`api_keys`:: Available using `getApiKeyInfos`, contains list of API keys that were retrieved for this request. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- diff --git a/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc b/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc new file mode 100644 index 0000000000000..18fffb43cb7a5 --- /dev/null +++ b/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc @@ -0,0 +1,59 @@ +-- +:api: invalidate-my-api-key +:request: InvalidateApiKeyRequest +:response: InvalidateApiKeyResponse +-- + +[id="{upid}-{api}"] +=== Invalidate API key(s) owned by authenticated user API + +API Key(s) owned by authenticated user can be invalidated using this API. + +[id="{upid}-{api}-request"] +==== Invalidate My API Key Request +The +{request}+ supports invalidating + +. A specific API key by api key id or by name + +. All API key(s) for current logged in user + +===== Specific API key by API key id +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[invalidate-my-api-key-id-request] +-------------------------------------------------- + +===== Specific API key by API key name +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[invalidate-my-api-key-name-request] +-------------------------------------------------- + +===== Invalidate all API key(s) owned by authenticated user +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[invalidate-my-api-key-all-request] +-------------------------------------------------- + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Invalidate API Key Response + +The returned +{response}+ contains the information regarding the API keys that the request +invalidated. + +`invalidatedApiKeys`:: Available using `getInvalidatedApiKeys` lists the API keys + that this request invalidated. + +`previouslyInvalidatedApiKeys`:: Available using `getPreviouslyInvalidatedApiKeys` lists the API keys + that this request attempted to invalidate + but were already invalid. + +`errors`:: Available using `getErrors` contains possible errors that were encountered while + attempting to invalidate API keys. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- \ No newline at end of file diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 4e28efc2941db..d4190c21f22ba 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -409,7 +409,9 @@ The Java High Level REST Client supports the following Security APIs: * <<{upid}-delete-privileges>> * <<{upid}-create-api-key>> * <<{upid}-get-api-key>> +* <<{upid}-get-my-api-key>> * <<{upid}-invalidate-api-key>> +* <<{upid}-invalidate-my-api-key>> include::security/put-user.asciidoc[] include::security/get-users.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index c04bae90801ee..efc8a5cdcbbc7 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -60,7 +60,9 @@ without requiring basic authentication: * <> * <> +* <> * <> +* <> [float] [[security-user-apis]] @@ -102,6 +104,7 @@ include::security/delete-users.asciidoc[] include::security/disable-users.asciidoc[] include::security/enable-users.asciidoc[] include::security/get-api-keys.asciidoc[] +include::security/get-my-api-keys.asciidoc[] include::security/get-app-privileges.asciidoc[] include::security/get-role-mappings.asciidoc[] include::security/get-roles.asciidoc[] @@ -109,6 +112,7 @@ include::security/get-tokens.asciidoc[] include::security/get-users.asciidoc[] include::security/has-privileges.asciidoc[] include::security/invalidate-api-keys.asciidoc[] +include::security/invalidate-my-api-keys.asciidoc[] include::security/invalidate-tokens.asciidoc[] include::security/ssl.asciidoc[] include::security/oidc-prepare-authentication-api.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security/get-my-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/get-my-api-keys.asciidoc new file mode 100644 index 0000000000000..732df78fda10a --- /dev/null +++ b/x-pack/docs/en/rest-api/security/get-my-api-keys.asciidoc @@ -0,0 +1,125 @@ +[role="xpack"] +[[security-api-get-my-api-key]] +=== Get information for API key(s) owned by the authenticated user API +++++ +Get information for API key(s) owned by the authenticated user API +++++ + +Retrieves information for one or more API keys owned by the authenticated user + +==== Request + +`GET /_security/api_key/my` + +==== Description + +The information for the API keys created by <> can be retrieved +using this API. This API can only retrieve API key information for the API keys owned by the logged-in user. + +==== 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. + +`name` (optional):: +(string) An API key name. + +NOTE: If none of the parameters are set, it will get information for all API keys owned by the authenticated user. + +==== Examples + +If you create an API key as follows: + +[source, js] +------------------------------------------------------------ +POST /_security/api_key +{ + "name": "my-api-key", + "role_descriptors": {} +} +------------------------------------------------------------ +// CONSOLE +// TEST + +A successful call returns a JSON structure that provides +API key information. For example: + +[source,js] +-------------------------------------------------- +{ + "id":"VuaCfGcBCdbkQm-e5aOx", + "name":"my-api-key", + "api_key":"ui2lp2axTNmsyakw9tvNnw" +} +-------------------------------------------------- +// TESTRESPONSE[s/VuaCfGcBCdbkQm-e5aOx/$body.id/] +// TESTRESPONSE[s/ui2lp2axTNmsyakw9tvNnw/$body.api_key/] + +You can use the following example to retrieve the API key by ID: + +[source,js] +-------------------------------------------------- +GET /_security/api_key/my?id=VuaCfGcBCdbkQm-e5aOx +-------------------------------------------------- +// CONSOLE +// TEST[s/VuaCfGcBCdbkQm-e5aOx/$body.id/] +// TEST[continued] + +You can use the following example to retrieve the API key by name: + +[source,js] +-------------------------------------------------- +GET /_security/api_key/my?name=my-api-key +-------------------------------------------------- +// CONSOLE +// TEST[continued] + +The following example retrieves all API keys owned by the logged in user: + +[source,js] +-------------------------------------------------- +GET /_security/api_key/my +-------------------------------------------------- +// CONSOLE +// TEST[continued] + +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. If the key has been invalidated, it has +a value of `true`. Otherwise, it is `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-my-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/invalidate-my-api-keys.asciidoc new file mode 100644 index 0000000000000..5f208fe43804e --- /dev/null +++ b/x-pack/docs/en/rest-api/security/invalidate-my-api-keys.asciidoc @@ -0,0 +1,140 @@ +[role="xpack"] +[[security-api-invalidate-my-api-key]] +=== Invalidate API key(s) owned by the authenticated user API +++++ +Invalidate API key(s) owned by the authenticated user API +++++ + +Invalidates one or more API keys owned by the authenticated user + +==== Request + +`DELETE /_security/api_key/my` + +==== 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. + +NOTE: If none of the parameters are set, it will invalidate API keys owned by the authenticated user. + +==== Examples + +If you create an API key as follows: + +[source, js] +------------------------------------------------------------ +POST /_security/api_key +{ + "name": "my-api-key", + "role_descriptors": {} +} +------------------------------------------------------------ +// CONSOLE +// TEST + +A successful call returns a JSON structure that provides +API key information. For example: + +[source,js] +-------------------------------------------------- +{ + "id":"VuaCfGcBCdbkQm-e5aOx", + "name":"my-api-key", + "api_key":"ui2lp2axTNmsyakw9tvNnw" +} +-------------------------------------------------- +// TESTRESPONSE[s/VuaCfGcBCdbkQm-e5aOx/$body.id/] +// TESTRESPONSE[s/ui2lp2axTNmsyakw9tvNnw/$body.api_key/] + +The following example invalidates the API key identified by specified `id` immediately: + +[source,js] +-------------------------------------------------- +DELETE /_security/api_key/my +{ + "id" : "VuaCfGcBCdbkQm-e5aOx" +} +-------------------------------------------------- +// CONSOLE +// TEST[s/VuaCfGcBCdbkQm-e5aOx/$body.id/] +// TEST[continued] + +The following example invalidates the API key identified by specified `name` immediately: + +[source,js] +-------------------------------------------------- +DELETE /_security/api_key/my +{ + "name" : "my-api-key" +} +-------------------------------------------------- +// CONSOLE +// TEST + +The following example invalidates all API keys owned by the authenticated user immediately: + +[source,js] +-------------------------------------------------- +DELETE /_security/api_key/my +{ +} +-------------------------------------------------- +// CONSOLE +// TEST + +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/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 a145569898ee6..0cbf2c4090c29 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 @@ -151,7 +151,9 @@ 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.GetMyApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyAction; 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; @@ -332,6 +334,8 @@ public List> getClientActions() { CreateApiKeyAction.INSTANCE, InvalidateApiKeyAction.INSTANCE, GetApiKeyAction.INSTANCE, + InvalidateMyApiKeyAction.INSTANCE, + GetMyApiKeyAction.INSTANCE, // upgrade IndexUpgradeInfoAction.INSTANCE, IndexUpgradeAction.INSTANCE, 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 index 287ebcee4b6f2..f5a5e58e3a1c8 100644 --- 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 @@ -143,4 +143,10 @@ public void writeTo(StreamOutput out) throws IOException { public void readFrom(StreamInput in) throws IOException { throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); } + + @Override + public String toString() { + return "GetApiKeyRequest [realmName=" + realmName + ", userName=" + userName + ", apiKeyId=" + apiKeyId + ", apiKeyName=" + + apiKeyName + "]"; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetMyApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetMyApiKeyAction.java new file mode 100644 index 0000000000000..77e979c67e8d3 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetMyApiKeyAction.java @@ -0,0 +1,33 @@ +/* + * 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.common.io.stream.Writeable; + +/** + * Action for retrieving API key(s) owned by the authenticated user + */ +public final class GetMyApiKeyAction extends Action { + + public static final String NAME = "cluster:admin/xpack/security/api_key/get/my"; + public static final GetMyApiKeyAction INSTANCE = new GetMyApiKeyAction(); + + private GetMyApiKeyAction() { + 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; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetMyApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetMyApiKeyRequest.java new file mode 100644 index 0000000000000..5f63a80fc1e69 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/GetMyApiKeyRequest.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.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +/** + * Request for retrieving information for API key(s) owned by the authenticated user. + */ +public final class GetMyApiKeyRequest extends ActionRequest { + + private final String apiKeyId; + private final String apiKeyName; + + public GetMyApiKeyRequest() { + this(null, null); + } + + public GetMyApiKeyRequest(StreamInput in) throws IOException { + super(in); + apiKeyId = in.readOptionalString(); + apiKeyName = in.readOptionalString(); + } + + public GetMyApiKeyRequest(@Nullable String apiKeyId, @Nullable String apiKeyName) { + this.apiKeyId = apiKeyId; + this.apiKeyName = apiKeyName; + } + + public String getApiKeyId() { + return apiKeyId; + } + + public String getApiKeyName() { + return apiKeyName; + } + + /** + * Creates request for given api key id + * @param apiKeyId api key id + * @return {@link GetMyApiKeyRequest} + */ + public static GetMyApiKeyRequest usingApiKeyId(String apiKeyId) { + return new GetMyApiKeyRequest(apiKeyId, null); + } + + /** + * Creates request for given api key name + * @param apiKeyName api key name + * @return {@link GetMyApiKeyRequest} + */ + public static GetMyApiKeyRequest usingApiKeyName(String apiKeyName) { + return new GetMyApiKeyRequest(null, apiKeyName); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(apiKeyId); + out.writeOptionalString(apiKeyName); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public String toString() { + return "GetMyApiKeyRequest [apiKeyId=" + apiKeyId + ", apiKeyName=" + apiKeyName + "]"; + } + +} 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 index f8815785d53d8..50a63c502a1e5 100644 --- 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 @@ -143,4 +143,9 @@ public void writeTo(StreamOutput out) throws IOException { public void readFrom(StreamInput in) throws IOException { throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); } + + @Override + public String toString() { + return "InvalidateApiKeyRequest [realmName=" + realmName + ", userName=" + userName + ", id=" + id + ", name=" + name + "]"; + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateMyApiKeyAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateMyApiKeyAction.java new file mode 100644 index 0000000000000..0b63abe5cbaf7 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateMyApiKeyAction.java @@ -0,0 +1,33 @@ +/* + * 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.common.io.stream.Writeable; + +/** + * Action for invalidating API key for the authenticated user + */ +public final class InvalidateMyApiKeyAction extends Action { + + public static final String NAME = "cluster:admin/xpack/security/api_key/invalidate/my"; + public static final InvalidateMyApiKeyAction INSTANCE = new InvalidateMyApiKeyAction(); + + private InvalidateMyApiKeyAction() { + 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; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateMyApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateMyApiKeyRequest.java new file mode 100644 index 0000000000000..a4ee7233fa739 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/InvalidateMyApiKeyRequest.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.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +/** + * Request for invalidating API key(s) for the authenticated user so that it can no longer be used. + */ +public final class InvalidateMyApiKeyRequest extends ActionRequest { + + private final String id; + private final String name; + + public InvalidateMyApiKeyRequest() { + this(null, null); + } + + public InvalidateMyApiKeyRequest(StreamInput in) throws IOException { + super(in); + id = in.readOptionalString(); + name = in.readOptionalString(); + } + + public InvalidateMyApiKeyRequest(@Nullable String id, @Nullable String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + /** + * Creates invalidate API key request for given api key id + * @param id api key id + * @return {@link InvalidateMyApiKeyRequest} + */ + public static InvalidateMyApiKeyRequest usingApiKeyId(String id) { + return new InvalidateMyApiKeyRequest(id, null); + } + + /** + * Creates invalidate api key request for given api key name + * @param name api key name + * @return {@link InvalidateMyApiKeyRequest} + */ + public static InvalidateMyApiKeyRequest usingApiKeyName(String name) { + return new InvalidateMyApiKeyRequest(null, name); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(id); + out.writeOptionalString(name); + } + + @Override + public void readFrom(StreamInput in) throws IOException { + throw new UnsupportedOperationException("usage of Streamable is to be replaced by Writeable"); + } + + @Override + public String toString() { + return "InvalidateMyApiKeyRequest [id=" + id + ", name=" + name + "]"; + } + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java index c929fb3bfd348..01480ae803509 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilege.java @@ -37,6 +37,11 @@ public final class ClusterPrivilege extends Privilege { private static final Automaton MANAGE_SECURITY_AUTOMATON = patterns("cluster:admin/xpack/security/*"); private static final Automaton MANAGE_SAML_AUTOMATON = patterns("cluster:admin/xpack/security/saml/*", InvalidateTokenAction.NAME, RefreshTokenAction.NAME); + private static final Automaton MANAGE_API_KEY_AUTOMATON = patterns("cluster:admin/xpack/security/api_key/*"); + private static final Automaton OWNER_MANAGE_API_KEY_AUTOMATON = patterns("cluster:admin/xpack/security/api_key/create", + "cluster:admin/xpack/security/api_key/invalidate/my", "cluster:admin/xpack/security/api_key/get/my"); + private static final Automaton CREATE_API_KEY_AUTOMATON = patterns("cluster:admin/xpack/security/api_key/create", + "cluster:admin/xpack/security/api_key/get/my"); private static final Automaton MANAGE_OIDC_AUTOMATON = patterns("cluster:admin/xpack/security/oidc/*"); private static final Automaton MANAGE_TOKEN_AUTOMATON = patterns("cluster:admin/xpack/security/token/*"); private static final Automaton MONITOR_AUTOMATON = patterns("cluster:monitor/*"); @@ -73,6 +78,10 @@ public final class ClusterPrivilege extends Privilege { public static final ClusterPrivilege MANAGE_ML = new ClusterPrivilege("manage_ml", MANAGE_ML_AUTOMATON); public static final ClusterPrivilege MANAGE_DATA_FRAME = new ClusterPrivilege("manage_data_frame_transforms", MANAGE_DATA_FRAME_AUTOMATON); + public static final ClusterPrivilege MANAGE_API_KEY = new ClusterPrivilege("manage_api_key", MANAGE_API_KEY_AUTOMATON); + public static final ClusterPrivilege OWNER_MANAGE_API_KEY = new ClusterPrivilege("owner_manage_api_key", + OWNER_MANAGE_API_KEY_AUTOMATON); + public static final ClusterPrivilege CREATE_API_KEY = new ClusterPrivilege("create_api_key", CREATE_API_KEY_AUTOMATON); public static final ClusterPrivilege MANAGE_TOKEN = new ClusterPrivilege("manage_token", MANAGE_TOKEN_AUTOMATON); public static final ClusterPrivilege MANAGE_WATCHER = new ClusterPrivilege("manage_watcher", MANAGE_WATCHER_AUTOMATON); public static final ClusterPrivilege MANAGE_ROLLUP = new ClusterPrivilege("manage_rollup", MANAGE_ROLLUP_AUTOMATON); @@ -104,6 +113,9 @@ public final class ClusterPrivilege extends Privilege { .put("manage", MANAGE) .put("manage_ml", MANAGE_ML) .put("manage_data_frame_transforms", MANAGE_DATA_FRAME) + .put("manage_api_key", MANAGE_API_KEY) + .put("owner_manage_api_key", OWNER_MANAGE_API_KEY) + .put("create_api_key", CREATE_API_KEY) .put("manage_token", MANAGE_TOKEN) .put("manage_watcher", MANAGE_WATCHER) .put("manage_index_templates", MANAGE_IDX_TEMPLATES) 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 4619035d0daaf..c99d1ba034c4f 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 @@ -17,9 +17,13 @@ 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.GetMyApiKeyAction; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyRequest; 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.InvalidateMyApiKeyAction; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyRequest; 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; @@ -361,10 +365,18 @@ public void invalidateApiKey(InvalidateApiKeyRequest request, ActionListener listener) { + client.execute(InvalidateMyApiKeyAction.INSTANCE, request, listener); + } + public void getApiKey(GetApiKeyRequest request, ActionListener listener) { client.execute(GetApiKeyAction.INSTANCE, request, listener); } + public void getMyApiKey(GetMyApiKeyRequest request, ActionListener listener) { + client.execute(GetMyApiKeyAction.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/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 8cc970ca77e4f..473c9531579df 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -79,7 +79,9 @@ 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.GetMyApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; @@ -136,7 +138,9 @@ import org.elasticsearch.xpack.core.ssl.rest.RestGetCertificateInfoAction; import org.elasticsearch.xpack.security.action.TransportCreateApiKeyAction; import org.elasticsearch.xpack.security.action.TransportGetApiKeyAction; +import org.elasticsearch.xpack.security.action.TransportGetMyApiKeyAction; import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction; +import org.elasticsearch.xpack.security.action.TransportInvalidateMyApiKeyAction; import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter; import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectLogoutAction; @@ -197,7 +201,9 @@ import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction; import org.elasticsearch.xpack.security.rest.action.RestCreateApiKeyAction; import org.elasticsearch.xpack.security.rest.action.RestGetApiKeyAction; +import org.elasticsearch.xpack.security.rest.action.RestGetMyApiKeyAction; import org.elasticsearch.xpack.security.rest.action.RestInvalidateApiKeyAction; +import org.elasticsearch.xpack.security.rest.action.RestInvalidateMyApiKeyAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction; import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectLogoutAction; @@ -763,7 +769,9 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(DeletePrivilegesAction.INSTANCE, TransportDeletePrivilegesAction.class), new ActionHandler<>(CreateApiKeyAction.INSTANCE, TransportCreateApiKeyAction.class), new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class), - new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class) + new ActionHandler<>(InvalidateMyApiKeyAction.INSTANCE, TransportInvalidateMyApiKeyAction.class), + new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class), + new ActionHandler<>(GetMyApiKeyAction.INSTANCE, TransportGetMyApiKeyAction.class) ); } @@ -818,7 +826,9 @@ public List getRestHandlers(Settings settings, RestController restC new RestDeletePrivilegesAction(settings, restController, getLicenseState()), new RestCreateApiKeyAction(settings, restController, getLicenseState()), new RestInvalidateApiKeyAction(settings, restController, getLicenseState()), - new RestGetApiKeyAction(settings, restController, getLicenseState()) + new RestInvalidateMyApiKeyAction(settings, restController, getLicenseState()), + new RestGetApiKeyAction(settings, restController, getLicenseState()), + new RestGetMyApiKeyAction(settings, restController, getLicenseState()) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java index 403ce482805a2..7c010111eaec4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetApiKeyAction.java @@ -9,7 +9,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.tasks.Task; @@ -32,15 +31,7 @@ public TransportGetApiKeyAction(TransportService transportService, ActionFilters @Override protected void doExecute(Task task, GetApiKeyRequest request, ActionListener listener) { - if (Strings.hasText(request.getRealmName()) || Strings.hasText(request.getUserName())) { - apiKeyService.getApiKeysForRealmAndUser(request.getRealmName(), request.getUserName(), listener); - } else if (Strings.hasText(request.getApiKeyId())) { - apiKeyService.getApiKeyForApiKeyId(request.getApiKeyId(), listener); - } else if (Strings.hasText(request.getApiKeyName())) { - apiKeyService.getApiKeyForApiKeyName(request.getApiKeyName(), listener); - } else { - listener.onFailure(new IllegalArgumentException("One of [api key id, api key name, username, realm name] must be specified")); - } + apiKeyService.getApiKeys(request, listener); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetMyApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetMyApiKeyAction.java new file mode 100644 index 0000000000000..5f11cae062151 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportGetMyApiKeyAction.java @@ -0,0 +1,36 @@ +/* + * 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.security.action; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyAction; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyRequest; +import org.elasticsearch.xpack.security.authc.ApiKeyService; + +public final class TransportGetMyApiKeyAction extends HandledTransportAction { + + private final ApiKeyService apiKeyService; + + @Inject + public TransportGetMyApiKeyAction(TransportService transportService, ActionFilters actionFilters, ApiKeyService apiKeyService) { + super(GetMyApiKeyAction.NAME, transportService, actionFilters, (Writeable.Reader) GetMyApiKeyRequest::new); + this.apiKeyService = apiKeyService; + } + + @Override + protected void doExecute(Task task, GetMyApiKeyRequest request, ActionListener listener) { + apiKeyService.getApiKeysForCurrentUser(request, listener); + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateApiKeyAction.java index 886d15b1f257d..5b32e5b0e81c5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateApiKeyAction.java @@ -9,7 +9,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.tasks.Task; @@ -32,13 +31,7 @@ public TransportInvalidateApiKeyAction(TransportService transportService, Action @Override protected void doExecute(Task task, InvalidateApiKeyRequest request, ActionListener listener) { - if (Strings.hasText(request.getRealmName()) || Strings.hasText(request.getUserName())) { - apiKeyService.invalidateApiKeysForRealmAndUser(request.getRealmName(), request.getUserName(), listener); - } else if (Strings.hasText(request.getId())) { - apiKeyService.invalidateApiKeyForApiKeyId(request.getId(), listener); - } else { - apiKeyService.invalidateApiKeyForApiKeyName(request.getName(), listener); - } + apiKeyService.invalidateApiKeys(request, listener); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateMyApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateMyApiKeyAction.java new file mode 100644 index 0000000000000..84944fa0138e2 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportInvalidateMyApiKeyAction.java @@ -0,0 +1,37 @@ +/* + * 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.security.action; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyAction; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyRequest; +import org.elasticsearch.xpack.security.authc.ApiKeyService; + +public final class TransportInvalidateMyApiKeyAction extends HandledTransportAction { + + private final ApiKeyService apiKeyService; + + @Inject + public TransportInvalidateMyApiKeyAction(TransportService transportService, ActionFilters actionFilters, ApiKeyService apiKeyService) { + super(InvalidateMyApiKeyAction.NAME, transportService, actionFilters, + (Writeable.Reader) InvalidateMyApiKeyRequest::new); + this.apiKeyService = apiKeyService; + } + + @Override + protected void doExecute(Task task, InvalidateMyApiKeyRequest request, ActionListener listener) { + apiKeyService.invalidateApiKeysForCurrentUser(request, listener); + } + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 980a39a186637..1769153e2e990 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -60,8 +60,12 @@ import org.elasticsearch.xpack.core.security.action.ApiKey; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest; import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.support.Hasher; @@ -623,97 +627,76 @@ public void usedDeprecatedField(String usedName, String replacedWith) { } } + /** - * Invalidate API keys for given realm and user name. - * @param realmName realm name - * @param userName user name - * @param invalidateListener listener for {@link InvalidateApiKeyResponse} + * Invalidate API key(s) owned by the authenticated user.
+ * If no API key id or name is specified in the request then invalidates all API keys for current + * logged-in user. + * + * @param invalidateMyApiKeyRequest {@link InvalidateMyApiKeyRequest} + * @param listener listener for {@link InvalidateApiKeyResponse} */ - public void invalidateApiKeysForRealmAndUser(String realmName, String userName, - ActionListener invalidateListener) { + public void invalidateApiKeysForCurrentUser(final InvalidateMyApiKeyRequest invalidateMyApiKeyRequest, + final ActionListener listener) { ensureEnabled(); - if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false) { - logger.trace("No realm name or username provided"); - invalidateListener.onFailure(new IllegalArgumentException("realm name or username must be provided")); - } else { - findApiKeysForUserAndRealm(userName, realmName, true, false, ActionListener.wrap(apiKeyIds -> { - if (apiKeyIds.isEmpty()) { - logger.warn("No active api keys to invalidate for realm [{}] and username [{}]", realmName, userName); - invalidateListener.onResponse(InvalidateApiKeyResponse.emptyResponse()); - } else { - invalidateAllApiKeys(apiKeyIds.stream().map(apiKey -> apiKey.getId()).collect(Collectors.toSet()), invalidateListener); - } - }, invalidateListener::onFailure)); - } - } - private void invalidateAllApiKeys(Collection apiKeyIds, ActionListener invalidateListener) { - indexInvalidation(apiKeyIds, invalidateListener, null); - } + final Authentication authentication = Authentication.getAuthentication(threadPool.getThreadContext()); + final String userName = authentication.getUser().principal(); + final String realmName = authentication.getLookedUpBy() == null ? + authentication.getAuthenticatedBy().getName() : authentication.getLookedUpBy().getName(); - /** - * Invalidate API key for given API key id - * @param apiKeyId API key id - * @param invalidateListener listener for {@link InvalidateApiKeyResponse} - */ - public void invalidateApiKeyForApiKeyId(String apiKeyId, ActionListener invalidateListener) { - ensureEnabled(); - if (Strings.hasText(apiKeyId) == false) { - logger.trace("No api key id provided"); - invalidateListener.onFailure(new IllegalArgumentException("api key id must be provided")); - } else { - findApiKeysForApiKeyId(apiKeyId, true, false, ActionListener.wrap(apiKeyIds -> { - if (apiKeyIds.isEmpty()) { - logger.warn("No api key to invalidate for api key id [{}]", apiKeyId); - invalidateListener.onResponse(InvalidateApiKeyResponse.emptyResponse()); - } else { - invalidateAllApiKeys(apiKeyIds.stream().map(apiKey -> apiKey.getId()).collect(Collectors.toSet()), invalidateListener); - } - }, invalidateListener::onFailure)); - } + findApiKeysForUserRealmApiKeyIdAndNameCombination(userName, realmName, invalidateMyApiKeyRequest.getName(), + invalidateMyApiKeyRequest.getId(), true, false, ActionListener.wrap(apiKeyIds -> { + if (apiKeyIds.isEmpty()) { + logger.warn("No api key to invalidate for {}", invalidateMyApiKeyRequest); + listener.onResponse(InvalidateApiKeyResponse.emptyResponse()); + } else { + invalidateAllApiKeys(apiKeyIds.stream().map(apiKey -> apiKey.getId()).collect(Collectors.toSet()), listener); + } + }, listener::onFailure)); } /** - * Invalidate API key for given API key name - * @param apiKeyName API key name - * @param invalidateListener listener for {@link InvalidateApiKeyResponse} + * Invalidate API key(s). + * @param invalidateApiKeyRequest {@link InvalidateApiKeyRequest} + * @param listener listener for {@link InvalidateApiKeyResponse} */ - public void invalidateApiKeyForApiKeyName(String apiKeyName, ActionListener invalidateListener) { + public void invalidateApiKeys(final InvalidateApiKeyRequest invalidateApiKeyRequest, + final ActionListener listener) { ensureEnabled(); - if (Strings.hasText(apiKeyName) == false) { - logger.trace("No api key name provided"); - invalidateListener.onFailure(new IllegalArgumentException("api key name must be provided")); - } else { - findApiKeyForApiKeyName(apiKeyName, true, false, ActionListener.wrap(apiKeyIds -> { - if (apiKeyIds.isEmpty()) { - logger.warn("No api key to invalidate for api key name [{}]", apiKeyName); - invalidateListener.onResponse(InvalidateApiKeyResponse.emptyResponse()); - } else { - invalidateAllApiKeys(apiKeyIds.stream().map(apiKey -> apiKey.getId()).collect(Collectors.toSet()), invalidateListener); - } - }, invalidateListener::onFailure)); + final String realmName = invalidateApiKeyRequest.getRealmName(); + final String userName = invalidateApiKeyRequest.getUserName(); + final String apiKeyId = invalidateApiKeyRequest.getId(); + final String apiKeyName = invalidateApiKeyRequest.getName(); + + if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false + && Strings.hasText(apiKeyName) == false) { + listener.onFailure(new IllegalArgumentException("one of [api key id, api key name, username, realm name] must be specified")); + } + if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) { + if (Strings.hasText(realmName) || Strings.hasText(userName)) { + listener.onFailure(new IllegalArgumentException( + "username or realm name must not be specified when the api key id or api key name is specified")); + } + } + if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) { + listener.onFailure(new IllegalArgumentException("only one of [api key id, api key name] can be specified")); } - } - private void findApiKeysForUserAndRealm(String userName, String realmName, boolean filterOutInvalidatedKeys, - boolean filterOutExpiredKeys, ActionListener> listener) { - final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze(); - if (frozenSecurityIndex.indexExists() == false) { - listener.onResponse(Collections.emptyList()); - } else if (frozenSecurityIndex.isAvailable() == false) { - listener.onFailure(frozenSecurityIndex.getUnavailableReason()); - } else { - final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() - .filter(QueryBuilders.termQuery("doc_type", "api_key")); - if (Strings.hasText(userName)) { - boolQuery.filter(QueryBuilders.termQuery("creator.principal", userName)); - } - if (Strings.hasText(realmName)) { - boolQuery.filter(QueryBuilders.termQuery("creator.realm", realmName)); - } + findApiKeysForUserRealmApiKeyIdAndNameCombination(userName, realmName, apiKeyName, apiKeyId, true, false, + ActionListener.wrap(apiKeyIds -> { + if (apiKeyIds.isEmpty()) { + logger.warn("No api key to invalidate for {}", invalidateApiKeyRequest); + listener.onResponse(InvalidateApiKeyResponse.emptyResponse()); + } else { + invalidateAllApiKeys(apiKeyIds.stream().map(apiKey -> apiKey.getId()).collect(Collectors.toSet()), + listener); + } + }, listener::onFailure)); + } - findApiKeys(boolQuery, filterOutInvalidatedKeys, filterOutExpiredKeys, listener); - } + private void invalidateAllApiKeys(Collection apiKeyIds, ActionListener invalidateListener) { + indexInvalidation(apiKeyIds, invalidateListener, null); } private void findApiKeys(final BoolQueryBuilder boolQuery, boolean filterOutInvalidatedKeys, boolean filterOutExpiredKeys, @@ -727,31 +710,34 @@ private void findApiKeys(final BoolQueryBuilder boolQuery, boolean filterOutInva expiredQuery.should(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("expiration_time"))); boolQuery.filter(expiredQuery); } - final SearchRequest request = client.prepareSearch(SECURITY_INDEX_NAME) - .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) - .setQuery(boolQuery) - .setVersion(false) - .setSize(1000) - .setFetchSource(true) - .request(); - securityIndex.checkIndexVersionThenExecute(listener::onFailure, - () -> ScrollHelper.fetchAllByEntity(client, request, listener, - (SearchHit hit) -> { - Map source = hit.getSourceAsMap(); - String name = (String) source.get("name"); - String id = hit.getId(); - Long creation = (Long) source.get("creation_time"); - Long expiration = (Long) source.get("expiration_time"); - Boolean invalidated = (Boolean) source.get("api_key_invalidated"); - String username = (String) ((Map) source.get("creator")).get("principal"); - String realm = (String) ((Map) source.get("creator")).get("realm"); - return new ApiKey(name, id, Instant.ofEpochMilli(creation), - (expiration != null) ? Instant.ofEpochMilli(expiration) : null, invalidated, username, realm); - })); - } - - private void findApiKeyForApiKeyName(String apiKeyName, boolean filterOutInvalidatedKeys, boolean filterOutExpiredKeys, - ActionListener> listener) { + try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashWithOrigin(SECURITY_ORIGIN)) { + final SearchRequest request = client.prepareSearch(SECURITY_INDEX_NAME) + .setScroll(DEFAULT_KEEPALIVE_SETTING.get(settings)) + .setQuery(boolQuery) + .setVersion(false) + .setSize(1000) + .setFetchSource(true) + .request(); + securityIndex.checkIndexVersionThenExecute(listener::onFailure, + () -> ScrollHelper.fetchAllByEntity(client, request, listener, + (SearchHit hit) -> { + Map source = hit.getSourceAsMap(); + String name = (String) source.get("name"); + String id = hit.getId(); + Long creation = (Long) source.get("creation_time"); + Long expiration = (Long) source.get("expiration_time"); + Boolean invalidated = (Boolean) source.get("api_key_invalidated"); + String username = (String) ((Map) source.get("creator")).get("principal"); + String realm = (String) ((Map) source.get("creator")).get("realm"); + return new ApiKey(name, id, Instant.ofEpochMilli(creation), + (expiration != null) ? Instant.ofEpochMilli(expiration) : null, invalidated, username, realm); + })); + } + } + + private void findApiKeysForUserRealmApiKeyIdAndNameCombination(String userName, String realmName, String apiKeyName, String apiKeyId, + boolean filterOutInvalidatedKeys, boolean filterOutExpiredKeys, + ActionListener> listener) { final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze(); if (frozenSecurityIndex.indexExists() == false) { listener.onResponse(Collections.emptyList()); @@ -760,25 +746,18 @@ private void findApiKeyForApiKeyName(String apiKeyName, boolean filterOutInvalid } else { final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() .filter(QueryBuilders.termQuery("doc_type", "api_key")); + if (Strings.hasText(userName)) { + boolQuery.filter(QueryBuilders.termQuery("creator.principal", userName)); + } + if (Strings.hasText(realmName)) { + boolQuery.filter(QueryBuilders.termQuery("creator.realm", realmName)); + } if (Strings.hasText(apiKeyName)) { boolQuery.filter(QueryBuilders.termQuery("name", apiKeyName)); } - - findApiKeys(boolQuery, filterOutInvalidatedKeys, filterOutExpiredKeys, listener); - } - } - - private void findApiKeysForApiKeyId(String apiKeyId, boolean filterOutInvalidatedKeys, boolean filterOutExpiredKeys, - ActionListener> listener) { - final SecurityIndexManager frozenSecurityIndex = securityIndex.freeze(); - if (frozenSecurityIndex.indexExists() == false) { - listener.onResponse(Collections.emptyList()); - } else if (frozenSecurityIndex.isAvailable() == false) { - listener.onFailure(frozenSecurityIndex.getUnavailableReason()); - } else { - final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() - .filter(QueryBuilders.termQuery("doc_type", "api_key")) - .filter(QueryBuilders.termQuery("_id", apiKeyId)); + if (Strings.hasText(apiKeyId)) { + boolQuery.filter(QueryBuilders.termQuery("_id", apiKeyId)); + } findApiKeys(boolQuery, filterOutInvalidatedKeys, filterOutExpiredKeys, listener); } @@ -907,70 +886,80 @@ private void maybeStartApiKeyRemover() { } /** - * Get API keys for given realm and user name. - * @param realmName realm name - * @param userName user name + * Get API key(s) owned by the authenticateed user.
+ * If no API key id or name is specified in the request then returns all API keys for current + * logged-in user. + * + * @param getMyApiKeyRequest {@link GetMyApiKeyRequest} * @param listener listener for {@link GetApiKeyResponse} */ - public void getApiKeysForRealmAndUser(String realmName, String userName, ActionListener listener) { + public void getApiKeysForCurrentUser(final GetMyApiKeyRequest getMyApiKeyRequest, ActionListener listener) { ensureEnabled(); - if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false) { - logger.trace("No realm name or username provided"); - listener.onFailure(new IllegalArgumentException("realm name or username must be provided")); + + final Authentication authentication = Authentication.getAuthentication(threadPool.getThreadContext()); + final String userName; + final String realmName; + final String apiKeyId; + final String apiKeyName; + if (authentication.getAuthenticatedBy().getType().equals("_es_api_key")) { + // in case of authenticated by API key, fetch key by API key id from authentication metadata + realmName = null; + userName = null; + apiKeyId = (String) authentication.getMetadata().get(API_KEY_ID_KEY); + apiKeyName = null; } else { - findApiKeysForUserAndRealm(userName, realmName, false, false, ActionListener.wrap(apiKeyInfos -> { - if (apiKeyInfos.isEmpty()) { - logger.warn("No active api keys found for realm [{}] and username [{}]", realmName, userName); - listener.onResponse(GetApiKeyResponse.emptyResponse()); - } else { - listener.onResponse(new GetApiKeyResponse(apiKeyInfos)); - } - }, listener::onFailure)); + realmName = authentication.getLookedUpBy() == null ? authentication.getAuthenticatedBy().getName() + : authentication.getLookedUpBy().getName(); + userName = authentication.getUser().principal(); + apiKeyId = getMyApiKeyRequest.getApiKeyId(); + apiKeyName = getMyApiKeyRequest.getApiKeyName(); } - } - /** - * Get API key for given API key id - * @param apiKeyId API key id - * @param listener listener for {@link GetApiKeyResponse} - */ - public void getApiKeyForApiKeyId(String apiKeyId, ActionListener listener) { - ensureEnabled(); - if (Strings.hasText(apiKeyId) == false) { - logger.trace("No api key id provided"); - listener.onFailure(new IllegalArgumentException("api key id must be provided")); - } else { - findApiKeysForApiKeyId(apiKeyId, false, false, ActionListener.wrap(apiKeyInfos -> { + findApiKeysForUserRealmApiKeyIdAndNameCombination(userName, realmName, apiKeyName, apiKeyId, false, false, + ActionListener.wrap(apiKeyInfos -> { if (apiKeyInfos.isEmpty()) { - logger.warn("No api key found for api key id [{}]", apiKeyId); + logger.warn("No active api keys found for {}", getMyApiKeyRequest); listener.onResponse(GetApiKeyResponse.emptyResponse()); } else { listener.onResponse(new GetApiKeyResponse(apiKeyInfos)); } }, listener::onFailure)); - } } /** - * Get API key for given API key name - * @param apiKeyName API key name + * Get API keys.
+ * @param getApiKeyRequest {@link GetApiKeyRequest} * @param listener listener for {@link GetApiKeyResponse} */ - public void getApiKeyForApiKeyName(String apiKeyName, ActionListener listener) { + public void getApiKeys(final GetApiKeyRequest getApiKeyRequest, ActionListener listener) { ensureEnabled(); - if (Strings.hasText(apiKeyName) == false) { - logger.trace("No api key name provided"); - listener.onFailure(new IllegalArgumentException("api key name must be provided")); - } else { - findApiKeyForApiKeyName(apiKeyName, false, false, ActionListener.wrap(apiKeyInfos -> { + final String realmName = getApiKeyRequest.getRealmName(); + final String userName = getApiKeyRequest.getUserName(); + final String apiKeyId = getApiKeyRequest.getApiKeyId(); + final String apiKeyName = getApiKeyRequest.getApiKeyName(); + + if (Strings.hasText(realmName) == false && Strings.hasText(userName) == false && Strings.hasText(apiKeyId) == false + && Strings.hasText(apiKeyName) == false) { + listener.onFailure(new IllegalArgumentException("one of [api key id, api key name, username, realm name] must be specified")); + } + if (Strings.hasText(apiKeyId) || Strings.hasText(apiKeyName)) { + if (Strings.hasText(realmName) || Strings.hasText(userName)) { + listener.onFailure(new IllegalArgumentException( + "username or realm name must not be specified when the api key id or api key name is specified")); + } + } + if (Strings.hasText(apiKeyId) && Strings.hasText(apiKeyName)) { + listener.onFailure(new IllegalArgumentException("only one of [api key id, api key name] can be specified")); + } + findApiKeysForUserRealmApiKeyIdAndNameCombination(userName, realmName, + apiKeyName, apiKeyId, false, false, ActionListener.wrap(apiKeyInfos -> { if (apiKeyInfos.isEmpty()) { - logger.warn("No api key found for api key name [{}]", apiKeyName); + logger.warn("No active api keys found for {}", getApiKeyRequest); listener.onResponse(GetApiKeyResponse.emptyResponse()); } else { listener.onResponse(new GetApiKeyResponse(apiKeyInfos)); } }, listener::onFailure)); - } } final class CachedApiKeyHashResult { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java index e2824e74ecafe..b977148b4c396 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/RBACEngine.java @@ -33,6 +33,8 @@ import org.elasticsearch.common.util.set.Sets; import org.elasticsearch.transport.TransportActionProxy; import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyAction; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction; @@ -85,7 +87,7 @@ public class RBACEngine implements AuthorizationEngine { private static final Predicate SAME_USER_PRIVILEGE = Automatons.predicate( - ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME, GetUserPrivilegesAction.NAME); + ChangePasswordAction.NAME, AuthenticateAction.NAME, HasPrivilegesAction.NAME, GetUserPrivilegesAction.NAME, GetMyApiKeyAction.NAME); private static final String INDEX_SUB_REQUEST_PRIMARY = IndexAction.NAME + "[p]"; private static final String INDEX_SUB_REQUEST_REPLICA = IndexAction.NAME + "[r]"; private static final String DELETE_SUB_REQUEST_PRIMARY = DeleteAction.NAME + "[p]"; @@ -153,26 +155,31 @@ public void authorizeClusterAction(RequestInfo requestInfo, AuthorizationInfo au boolean checkSameUserPermissions(String action, TransportRequest request, Authentication authentication) { final boolean actionAllowed = SAME_USER_PRIVILEGE.test(action); if (actionAllowed) { - if (request instanceof UserRequest == false) { - assert false : "right now only a user request should be allowed"; - return false; - } - UserRequest userRequest = (UserRequest) request; - String[] usernames = userRequest.usernames(); - if (usernames == null || usernames.length != 1 || usernames[0] == null) { - assert false : "this role should only be used for actions to apply to a single user"; + if (request instanceof UserRequest) { + UserRequest userRequest = (UserRequest) request; + String[] usernames = userRequest.usernames(); + if (usernames == null || usernames.length != 1 || usernames[0] == null) { + assert false : "this role should only be used for actions to apply to a single user"; + return false; + } + final String username = usernames[0]; + final boolean sameUsername = authentication.getUser().principal().equals(username); + if (sameUsername && ChangePasswordAction.NAME.equals(action)) { + return checkChangePasswordAction(authentication); + } + + assert AuthenticateAction.NAME.equals(action) || HasPrivilegesAction.NAME.equals(action) + || GetUserPrivilegesAction.NAME.equals(action) + || sameUsername == false : "Action '" + action + "' should not be possible when sameUsername=" + sameUsername; + return sameUsername; + } else if (request instanceof GetMyApiKeyRequest) { + if (authentication.getAuthenticatedBy().getType().equals("_es_api_key")) { + return true; + } + } else { + assert false : "only a user request or get my API key request should be allowed"; return false; } - final String username = usernames[0]; - final boolean sameUsername = authentication.getUser().principal().equals(username); - if (sameUsername && ChangePasswordAction.NAME.equals(action)) { - return checkChangePasswordAction(authentication); - } - - assert AuthenticateAction.NAME.equals(action) || HasPrivilegesAction.NAME.equals(action) - || GetUserPrivilegesAction.NAME.equals(action) || sameUsername == false - : "Action '" + action + "' should not be possible when sameUsername=" + sameUsername; - return sameUsername; } return false; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyAction.java index 2e3ced0d8933f..6b08e374c5743 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyAction.java @@ -39,7 +39,7 @@ public RestCreateApiKeyAction(Settings settings, RestController controller, XPac @Override public String getName() { - return "xpack_security_create_api_key"; + return "security_create_api_key"; } @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyAction.java index ec0bd7bd9fd31..119d5e86bc205 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyAction.java @@ -57,7 +57,7 @@ public RestResponse buildResponse(GetApiKeyResponse getApiKeyResponse, XContentB @Override public String getName() { - return "xpack_security_get_api_key"; + return "security_get_api_key"; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetMyApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetMyApiKeyAction.java new file mode 100644 index 0000000000000..9283837bc926b --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetMyApiKeyAction.java @@ -0,0 +1,60 @@ +/* + * 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.security.rest.action; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyAction; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyRequest; + +import java.io.IOException; + +/** + * Rest action to get information for one or more API keys owned by the authenticated user. + */ +public class RestGetMyApiKeyAction extends SecurityBaseRestHandler { + + public RestGetMyApiKeyAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(RestRequest.Method.GET, "/_security/api_key/my", this); + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + final String apiKeyId = request.param("id"); + final String apiKeyName = request.param("name"); + final GetMyApiKeyRequest getApiKeyRequest = new GetMyApiKeyRequest(apiKeyId, apiKeyName); + return channel -> client.execute(GetMyApiKeyAction.INSTANCE, getApiKeyRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(GetApiKeyResponse getApiKeyResponse, XContentBuilder builder) throws Exception { + getApiKeyResponse.toXContent(builder, channel.request()); + + // return HTTP status 404 if no API key found for API key id + if (Strings.hasText(apiKeyId) && getApiKeyResponse.getApiKeyInfos().length == 0) { + return new BytesRestResponse(RestStatus.NOT_FOUND, builder); + } + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + + @Override + public String getName() { + return "security_get_my_api_key"; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyAction.java index eb10ec6669e32..6ea1da3dd25e0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyAction.java @@ -64,7 +64,7 @@ public RestResponse buildResponse(InvalidateApiKeyResponse invalidateResp, @Override public String getName() { - return "xpack_security_invalidate_api_key"; + return "security_invalidate_api_key"; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateMyApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateMyApiKeyAction.java new file mode 100644 index 0000000000000..71ff2e54619e7 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateMyApiKeyAction.java @@ -0,0 +1,68 @@ +/* + * 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.security.rest.action; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyAction; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyRequest; + +import java.io.IOException; + +/** + * Rest action to invalidate one or more API keys owned by the authenticated user. + */ +public final class RestInvalidateMyApiKeyAction extends SecurityBaseRestHandler { + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("invalidate_my_api_key", + a -> { + return new InvalidateMyApiKeyRequest((String) a[0], (String) a[1]); + }); + + static { + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("id")); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("name")); + } + + public RestInvalidateMyApiKeyAction(Settings settings, RestController controller, XPackLicenseState licenseState) { + super(settings, licenseState); + controller.registerHandler(RestRequest.Method.DELETE, "/_security/api_key/my", this); + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final InvalidateMyApiKeyRequest invalidateApiKeyRequest = PARSER.parse(parser, null); + return channel -> client.execute(InvalidateMyApiKeyAction.INSTANCE, invalidateApiKeyRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(InvalidateApiKeyResponse invalidateResp, + XContentBuilder builder) throws Exception { + invalidateResp.toXContent(builder, channel.request()); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + } + + @Override + public String getName() { + return "security_invalidate_my_api_key"; + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index 256bf6d9df532..b2a57714025ce 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -29,8 +29,10 @@ import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest; import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyRequest; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyRequest; import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.client.SecurityClient; @@ -61,11 +63,49 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isIn; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; public class ApiKeyIntegTests extends SecurityIntegTestCase { private static final long DELETE_INTERVAL_MILLIS = 100L; + + @Override + public String configRoles() { + return super.configRoles() + "\n" + + "manage_api_key_role:\n" + + " cluster: [\"manage_api_key\"]\n" + + "create_api_key_role:\n" + + " cluster: [\"create_api_key\"]\n" + + "owner_manage_api_key_role:\n" + + " cluster: [\"owner_manage_api_key\"]\n" + + "no_manage_api_key_role:\n" + + " indices:\n" + + " - names: '*'\n" + + " privileges:\n" + + " - all\n"; + } + + @Override + public String configUsers() { + final String usersPasswdHashed = new String( + getFastStoredHashAlgoForTests().hash(SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING)); + return super.configUsers() + + "user_with_manage_api_key_role:" + usersPasswdHashed + "\n" + + "user_with_create_api_key_role:" + usersPasswdHashed + "\n" + + "user_with_owner_manage_api_key_role:" + usersPasswdHashed + "\n" + + "user_with_no_manage_api_key_role:" + usersPasswdHashed + "\n"; + } + + @Override + public String configUsersRoles() { + return super.configUsersRoles() + + "manage_api_key_role:user_with_manage_api_key_role\n" + + "create_api_key_role:user_with_create_api_key_role\n" + + "owner_manage_api_key_role:user_with_owner_manage_api_key_role\n" + + "no_manage_api_key_role:user_with_no_manage_api_key_role"; + } + @Override public Settings nodeSettings(int nodeOrdinal) { return Settings.builder() @@ -145,6 +185,38 @@ public void testCreateApiKey() { assertThat(e.status(), is(RestStatus.FORBIDDEN)); } + public void testApiKeyWithMinimalRoleCanGetApiKeyInformation() { + final RoleDescriptor descriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null); + Client client = client().filterWithHeader(Collections.singletonMap("Authorization", + UsernamePasswordToken.basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, + SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + SecurityClient securityClient = new SecurityClient(client); + final CreateApiKeyResponse response = securityClient.prepareCreateApiKey() + .setName("test key") + .setExpiration(TimeValue.timeValueHours(TimeUnit.DAYS.toHours(7L))) + .setRoleDescriptors(Collections.singletonList(descriptor)) + .get(); + + assertEquals("test key", response.getName()); + assertNotNull(response.getId()); + assertNotNull(response.getKey()); + + // use the first ApiKey for authorized action + final String base64ApiKeyKeyValue = Base64.getEncoder().encodeToString( + (response.getId() + ":" + response.getKey().toString()).getBytes(StandardCharsets.UTF_8)); + client = client().filterWithHeader(Collections.singletonMap("Authorization", "ApiKey " + base64ApiKeyKeyValue)); + securityClient = new SecurityClient(client); + PlainActionFuture listener = new PlainActionFuture<>(); + GetMyApiKeyRequest request = randomFrom(GetMyApiKeyRequest.usingApiKeyId(response.getId()), + GetMyApiKeyRequest.usingApiKeyName(response.getName()), new GetMyApiKeyRequest()); + securityClient.getMyApiKey(request, listener); + GetApiKeyResponse apiKeyResponse = listener.actionGet(); + assertThat(apiKeyResponse.getApiKeyInfos().length, is(1)); + assertThat(apiKeyResponse.getApiKeyInfos()[0].getId(), is(response.getId())); + assertThat(apiKeyResponse.getApiKeyInfos()[0].getName(), is(response.getName())); + assertThat(apiKeyResponse.getApiKeyInfos()[0].getExpiration(), is(response.getExpiration())); + } + public void testCreateApiKeyFailsWhenApiKeyWithSameNameAlreadyExists() throws InterruptedException, ExecutionException { String keyName = randomAlphaOfLength(5); List responses = new ArrayList<>(); @@ -481,6 +553,143 @@ public void testGetApiKeysForApiKeyId() throws InterruptedException, ExecutionEx verifyGetResponse(1, responses, response, Collections.singleton(responses.get(0).getId()), null); } + public void testCreateApiKeyAuthorization() { + // user_with_manage_api_key_role should be able to create API key + List responses = createApiKeys("user_with_manage_api_key_role", 1, null); + assertThat(responses.get(0).getKey(), is(notNullValue())); + + // user_with_create_api_key_role should be able to create API key + responses = createApiKeys("user_with_create_api_key_role", 1, null); + assertThat(responses.get(0).getKey(), is(notNullValue())); + + // user_with_no_manage_api_key_role should not be able to create API key + Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken + .basicAuthHeaderValue("user_with_no_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + SecurityClient securityClient = new SecurityClient(client); + RoleDescriptor roleDescriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null); + ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, + () -> securityClient.prepareCreateApiKey() + .setName("test-key-" + randomAlphaOfLengthBetween(5, 9)) + .setRoleDescriptors(Collections.singletonList(roleDescriptor)) + .get()); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/create", "user_with_no_manage_api_key_role"); + } + + public void testGetApiKeyAuthorization() throws InterruptedException, ExecutionException { + List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role", 2, null); + List userWithOwnerManageApiKeyRoleApiKeys = createApiKeys("user_with_owner_manage_api_key_role", 2, null); + List userWithCreateApiKeyRoleApiKeys = createApiKeys("user_with_create_api_key_role", 1, null); + + // user_with_manage_api_key_role should be able to get any user's API Key + { + final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken + .basicAuthHeaderValue("user_with_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final SecurityClient securityClient = new SecurityClient(client); + PlainActionFuture listener = new PlainActionFuture<>(); + securityClient.getApiKey(GetApiKeyRequest.usingApiKeyId(userWithManageApiKeyRoleApiKeys.get(0).getId()), listener); + GetApiKeyResponse response = listener.actionGet(); + assertThat(response.getApiKeyInfos().length, is(1)); + assertThat(response.getApiKeyInfos()[0].getId(), is(userWithManageApiKeyRoleApiKeys.get(0).getId())); + + listener = new PlainActionFuture<>(); + securityClient.getApiKey(GetApiKeyRequest.usingApiKeyId(userWithOwnerManageApiKeyRoleApiKeys.get(0).getId()), listener); + response = listener.actionGet(); + assertThat(response.getApiKeyInfos().length, is(1)); + assertThat(response.getApiKeyInfos()[0].getId(), is(userWithOwnerManageApiKeyRoleApiKeys.get(0).getId())); + } + + // user_with_owner_manage_api_key_role or user_with_create_api_key_role should be able to get its own API key but not any other + // user's API key + { + final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken + .basicAuthHeaderValue("user_with_owner_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final SecurityClient securityClient = new SecurityClient(client); + PlainActionFuture listener = new PlainActionFuture<>(); + securityClient.getMyApiKey(new GetMyApiKeyRequest(), listener); + GetApiKeyResponse response = listener.actionGet(); + assertThat(response.getApiKeyInfos().length, is(2)); + + final PlainActionFuture getApiKeyOfOtherUserListener = new PlainActionFuture<>(); + securityClient.getApiKey(GetApiKeyRequest.usingApiKeyId(userWithManageApiKeyRoleApiKeys.get(0).getId()), + getApiKeyOfOtherUserListener); + final ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, + () -> getApiKeyOfOtherUserListener.actionGet()); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", "user_with_owner_manage_api_key_role"); + } + + // user_with_create_api_key_role should be allowed to get it's own API key but not any other user's API key + { + final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken + .basicAuthHeaderValue("user_with_create_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final SecurityClient securityClient = new SecurityClient(client); + PlainActionFuture listener = new PlainActionFuture<>(); + securityClient.getMyApiKey(GetMyApiKeyRequest.usingApiKeyName(userWithCreateApiKeyRoleApiKeys.get(0).getName()), listener); + GetApiKeyResponse response = listener.actionGet(); + assertThat(response.getApiKeyInfos().length, is(1)); + assertThat(response.getApiKeyInfos()[0].getId(), is(userWithCreateApiKeyRoleApiKeys.get(0).getId())); + + final PlainActionFuture getApiKeyOfOtherUserListener = new PlainActionFuture<>(); + securityClient.getApiKey(GetApiKeyRequest.usingApiKeyId(userWithManageApiKeyRoleApiKeys.get(0).getId()), + getApiKeyOfOtherUserListener); + final ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, + () -> getApiKeyOfOtherUserListener.actionGet()); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/get", "user_with_create_api_key_role"); + } + } + + public void testInvalidateApiKeyAuthorization() throws InterruptedException, ExecutionException { + List userWithManageApiKeyRoleApiKeys = createApiKeys("user_with_manage_api_key_role", 2, null); + List userWithOwnerManageApiKeyRoleApiKeys = createApiKeys("user_with_owner_manage_api_key_role", 2, null); + List userWithCreateApiKeyRoleApiKeys = createApiKeys("user_with_create_api_key_role", 1, null); + + // user_with_manage_api_key_role should be able to invalidate any user's API Key + InvalidateApiKeyResponse invalidateApiKeyResponse = invalidateApiKey("user_with_manage_api_key_role", null, null, + userWithManageApiKeyRoleApiKeys.get(0).getName(), null); + verifyInvalidateResponse(1, Collections.singletonList(userWithManageApiKeyRoleApiKeys.get(0)), invalidateApiKeyResponse); + invalidateApiKeyResponse = invalidateApiKey("user_with_manage_api_key_role", null, null, + userWithOwnerManageApiKeyRoleApiKeys.get(0).getName(), null); + verifyInvalidateResponse(1, Collections.singletonList(userWithOwnerManageApiKeyRoleApiKeys.get(0)), invalidateApiKeyResponse); + + // user_with_owner_manage_api_key_role should be able to invalidate its own API key but not any other user's API key + { + final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken + .basicAuthHeaderValue("user_with_owner_manage_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final SecurityClient securityClient = new SecurityClient(client); + final PlainActionFuture listener = new PlainActionFuture<>(); + securityClient.invalidateMyApiKey( + InvalidateMyApiKeyRequest.usingApiKeyName(userWithOwnerManageApiKeyRoleApiKeys.get(1).getName()), listener); + invalidateApiKeyResponse = listener.actionGet(); + verifyInvalidateResponse(1, Collections.singletonList(userWithOwnerManageApiKeyRoleApiKeys.get(1)), invalidateApiKeyResponse); + + final ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, + () -> invalidateApiKey("user_with_owner_manage_api_key_role", null, null, + userWithManageApiKeyRoleApiKeys.get(1).getName(), null)); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/invalidate", "user_with_owner_manage_api_key_role"); + } + + // user_with_create_api_key_role should not be allowed to invalidate it's own API keys or any other users API keys + { + final Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken + .basicAuthHeaderValue("user_with_create_api_key_role", SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + final SecurityClient securityClient = new SecurityClient(client); + + ElasticsearchSecurityException ese = expectThrows(ElasticsearchSecurityException.class, + () -> invalidateApiKey("user_with_create_api_key_role", null, null, userWithManageApiKeyRoleApiKeys.get(1).getName(), + null)); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/invalidate", "user_with_create_api_key_role"); + + final PlainActionFuture listener = new PlainActionFuture<>(); + securityClient.invalidateMyApiKey(InvalidateMyApiKeyRequest.usingApiKeyName(userWithCreateApiKeyRoleApiKeys.get(0).getName()), + listener); + ese = expectThrows(ElasticsearchSecurityException.class, () -> listener.actionGet()); + assertErrorMessage(ese, "cluster:admin/xpack/security/api_key/invalidate/my", "user_with_create_api_key_role"); + } + } + + private void assertErrorMessage(final ElasticsearchSecurityException ese, String action, String userName) { + assertThat(ese.getMessage(), is("action [" + action + "] is unauthorized for user [" + userName + "]")); + } + public void testGetApiKeysForApiKeyName() throws InterruptedException, ExecutionException { List responses = createApiKeys(1, null); Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken @@ -519,12 +728,12 @@ private void verifyGetResponse(int noOfApiKeys, List respo } - private List createApiKeys(int noOfApiKeys, TimeValue expiration) { + private List createApiKeys(String user, int noOfApiKeys, TimeValue expiration) { List responses = new ArrayList<>(); for (int i = 0; i < noOfApiKeys; i++) { final RoleDescriptor descriptor = new RoleDescriptor("role", new String[] { "monitor" }, null, null); Client client = client().filterWithHeader(Collections.singletonMap("Authorization", UsernamePasswordToken - .basicAuthHeaderValue(SecuritySettingsSource.TEST_SUPERUSER, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + .basicAuthHeaderValue(user, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); SecurityClient securityClient = new SecurityClient(client); final CreateApiKeyResponse response = securityClient.prepareCreateApiKey() .setName("test-key-" + randomAlphaOfLengthBetween(5, 9) + i).setExpiration(expiration) @@ -536,4 +745,28 @@ private List createApiKeys(int noOfApiKeys, TimeValue expi assertThat(responses.size(), is(noOfApiKeys)); return responses; } + + private List createApiKeys(int noOfApiKeys, TimeValue expiration) { + return createApiKeys(SecuritySettingsSource.TEST_SUPERUSER, noOfApiKeys, expiration); + } + + private InvalidateApiKeyResponse invalidateApiKey(String executeActionAsUser, String realmName, String userName, String apiKeyName, + String apiKeyId) { + Client client = client().filterWithHeader(Collections.singletonMap("Authorization", + UsernamePasswordToken.basicAuthHeaderValue(executeActionAsUser, SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING))); + SecurityClient securityClient = new SecurityClient(client); + PlainActionFuture listener = new PlainActionFuture<>(); + if (Strings.hasText(realmName) && Strings.hasText(userName)) { + securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingRealmAndUserName(realmName, userName), listener); + } else if (Strings.hasText(realmName)) { + securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingRealmName(realmName), listener); + } else if (Strings.hasText(userName)) { + securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingUserName(userName), listener); + } else if (Strings.hasText(apiKeyName)) { + securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingApiKeyName(apiKeyName), listener); + } else if (Strings.hasText(apiKeyId)) { + securityClient.invalidateApiKey(InvalidateApiKeyRequest.usingApiKeyId(apiKeyId), listener); + } + return listener.actionGet(); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java index 5c2e964c743c6..6139985963fcf 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RBACEngineTests.java @@ -21,6 +21,10 @@ import org.elasticsearch.license.GetLicenseAction; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyAction; +import org.elasticsearch.xpack.core.security.action.GetMyApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyAction; +import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest; import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequestBuilder; @@ -110,6 +114,21 @@ public void testSameUserPermission() { assertTrue(engine.checkSameUserPermissions(action, request, authentication)); } + public void testSamUserPermissionForGetMyApiKeyAction() { + final User user = new User("api-key-principal"); + final Authentication authentication = mock(Authentication.class); + final Authentication.RealmRef authenticatedBy = mock(Authentication.RealmRef.class); + when(authentication.getUser()).thenReturn(user); + when(authentication.getAuthenticatedBy()).thenReturn(authenticatedBy); + when(authenticatedBy.getType()).thenReturn("_es_api_key"); + + TransportRequest request = new GetMyApiKeyRequest(null, null); + assertTrue(engine.checkSameUserPermissions(GetMyApiKeyAction.NAME, request, authentication)); + + request = new InvalidateMyApiKeyRequest(); + assertFalse(engine.checkSameUserPermissions(InvalidateMyApiKeyAction.NAME, request, authentication)); + } + public void testSameUserPermissionDoesNotAllowNonMatchingUsername() { final User authUser = new User("admin", new String[]{"bar"}); final User user = new User("joe", null, authUser); diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/security.get_my_api_key.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/security.get_my_api_key.json new file mode 100644 index 0000000000000..6f5b40fb97ce8 --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/security.get_my_api_key.json @@ -0,0 +1,22 @@ +{ + "security.get_my_api_key": { + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-my-api-key.html", + "methods": [ "GET" ], + "url": { + "path": "/_security/api_key/my", + "paths": [ "/_security/api_key/my" ], + "parts": {}, + "params": { + "id": { + "type": "string", + "description": "API key id of the API key to be retrieved" + }, + "name": { + "type": "string", + "description": "API key name of the API key to be retrieved" + } + } + }, + "body": null + } +} diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/api/security.invalidate_my_api_key.json b/x-pack/plugin/src/test/resources/rest-api-spec/api/security.invalidate_my_api_key.json new file mode 100644 index 0000000000000..bbbc43cb9929b --- /dev/null +++ b/x-pack/plugin/src/test/resources/rest-api-spec/api/security.invalidate_my_api_key.json @@ -0,0 +1,15 @@ +{ + "security.invalidate_my_api_key": { + "documentation": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-invalidate-my-api-key.html", + "methods": [ "DELETE" ], + "url": { + "path": "/_security/api_key/my", + "paths": [ "/_security/api_key/my" ], + "parts": {} + }, + "body": { + "description" : "The api key request to invalidate API key(s) owned by authenticated user", + "required" : true + } + } +} From 54d22e7fa24c3394b520eebb3d7c631e4f79c5d8 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Thu, 2 May 2019 16:18:41 +1000 Subject: [PATCH 2/4] move rest actions to apikey package --- .../main/java/org/elasticsearch/xpack/security/Security.java | 4 ++-- .../rest/action/{ => apikey}/RestGetMyApiKeyAction.java | 3 ++- .../action/{ => apikey}/RestInvalidateMyApiKeyAction.java | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/{ => apikey}/RestGetMyApiKeyAction.java (95%) rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/{ => apikey}/RestInvalidateMyApiKeyAction.java (95%) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 116569b0c851e..2366df964618d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -201,9 +201,9 @@ import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestCreateApiKeyAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyAction; -import org.elasticsearch.xpack.security.rest.action.RestGetMyApiKeyAction; +import org.elasticsearch.xpack.security.rest.action.apikey.RestGetMyApiKeyAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateApiKeyAction; -import org.elasticsearch.xpack.security.rest.action.RestInvalidateMyApiKeyAction; +import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateMyApiKeyAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction; import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectAuthenticateAction; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetMyApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetMyApiKeyAction.java similarity index 95% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetMyApiKeyAction.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetMyApiKeyAction.java index 9283837bc926b..d562e9b893f8e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetMyApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetMyApiKeyAction.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.rest.action; +package org.elasticsearch.xpack.security.rest.action.apikey; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.Strings; @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; import org.elasticsearch.xpack.core.security.action.GetMyApiKeyAction; import org.elasticsearch.xpack.core.security.action.GetMyApiKeyRequest; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateMyApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateMyApiKeyAction.java similarity index 95% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateMyApiKeyAction.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateMyApiKeyAction.java index 71ff2e54619e7..a4ab483ea6295 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateMyApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateMyApiKeyAction.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.rest.action; +package org.elasticsearch.xpack.security.rest.action.apikey; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.ParseField; @@ -22,6 +22,7 @@ import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateMyApiKeyRequest; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; From 54d9af662c91c80dc246204b184a2f926151d116 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 3 May 2019 12:15:49 +1000 Subject: [PATCH 3/4] Address review comments --- .../security/get-my-api-key.asciidoc | 14 +++++------ .../security/invalidate-my-api-key.asciidoc | 12 +++++----- .../security/get-my-api-keys.asciidoc | 6 ++--- .../xpack/security/authc/ApiKeyService.java | 24 +++++++++++++++---- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/docs/java-rest/high-level/security/get-my-api-key.asciidoc b/docs/java-rest/high-level/security/get-my-api-key.asciidoc index 9c5d80e34fd68..1f21c576f60bd 100644 --- a/docs/java-rest/high-level/security/get-my-api-key.asciidoc +++ b/docs/java-rest/high-level/security/get-my-api-key.asciidoc @@ -1,21 +1,21 @@ -- :api: get-my-api-key -:request: GetApiKeyRequest -:response: GetApiKeyResponse +:request: GetMyApiKeyRequest +:response: GetMyApiKeyRequest -- [id="{upid}-{api}"] -=== Get information for API key(s) owned by authenticated user API +=== Get information for API key(s) owned by the authenticated user API -API Key(s) information owned by authenticated user can be retrieved using this API. +API Key(s) information owned by the authenticated user can be retrieved using this API. [id="{upid}-{api}-request"] ==== Get My API Key Request The +{request}+ supports retrieving API key information for -. A specific API key by api key id or by name +. A specific API key by id or by name -. All API keys of current logged in user +. All API key(s) of the current authenticated user ===== Retrieve information for a specific API key by its id ["source","java",subs="attributes,callouts,macros"] @@ -29,7 +29,7 @@ include-tagged::{doc-tests-file}[get-my-api-key-id-request] include-tagged::{doc-tests-file}[get-my-api-key-name-request] -------------------------------------------------- -===== Retrieve information for all API key(s) owned by authenticated user +===== Retrieve information for all API key(s) owned by the authenticated user ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- include-tagged::{doc-tests-file}[get-my-api-key-all-request] diff --git a/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc b/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc index 18fffb43cb7a5..8281c59181748 100644 --- a/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc +++ b/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc @@ -1,21 +1,21 @@ -- :api: invalidate-my-api-key -:request: InvalidateApiKeyRequest -:response: InvalidateApiKeyResponse +:request: InvalidateMyApiKeyRequest +:response: InvalidateMyApiKeyRequest -- [id="{upid}-{api}"] -=== Invalidate API key(s) owned by authenticated user API +=== Invalidate API key(s) owned by the authenticated user API -API Key(s) owned by authenticated user can be invalidated using this API. +API Key(s) owned by the authenticated user can be invalidated using this API. [id="{upid}-{api}-request"] ==== Invalidate My API Key Request The +{request}+ supports invalidating -. A specific API key by api key id or by name +. A specific API key by id or by name -. All API key(s) for current logged in user +. All API key(s) of the current authenticated user ===== Specific API key by API key id ["source","java",subs="attributes,callouts,macros"] diff --git a/x-pack/docs/en/rest-api/security/get-my-api-keys.asciidoc b/x-pack/docs/en/rest-api/security/get-my-api-keys.asciidoc index 732df78fda10a..821e0ec4fb4e0 100644 --- a/x-pack/docs/en/rest-api/security/get-my-api-keys.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-my-api-keys.asciidoc @@ -14,7 +14,7 @@ Retrieves information for one or more API keys owned by the authenticated user ==== Description The information for the API keys created by <> can be retrieved -using this API. This API can only retrieve API key information for the API keys owned by the logged-in user. +using this API. This API can only retrieve API key information for the API keys owned by the current authenticated user. ==== Request Body @@ -27,7 +27,7 @@ pertain to retrieving api keys: `name` (optional):: (string) An API key name. -NOTE: If none of the parameters are set, it will get information for all API keys owned by the authenticated user. +NOTE: If none of the parameters are set, it will get information for all API keys owned by the current authenticated user. ==== Examples @@ -77,7 +77,7 @@ GET /_security/api_key/my?name=my-api-key // CONSOLE // TEST[continued] -The following example retrieves all API keys owned by the logged in user: +The following example retrieves all API keys owned by the current authenticated user: [source,js] -------------------------------------------------- diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 3a07d9b53a081..15170b4e99e6b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -657,12 +657,26 @@ public void invalidateApiKeysForCurrentUser(final InvalidateMyApiKeyRequest inva ensureEnabled(); final Authentication authentication = Authentication.getAuthentication(threadPool.getThreadContext()); - final String userName = authentication.getUser().principal(); - final String realmName = authentication.getLookedUpBy() == null ? - authentication.getAuthenticatedBy().getName() : authentication.getLookedUpBy().getName(); + final String userName; + final String realmName; + final String apiKeyId; + final String apiKeyName; + if (authentication.getAuthenticatedBy().getType().equals("_es_api_key")) { + // in case of authenticated by API key, fetch key by API key id from authentication metadata + realmName = null; + userName = null; + apiKeyId = (String) authentication.getMetadata().get(API_KEY_ID_KEY); + apiKeyName = null; + } else { + realmName = authentication.getLookedUpBy() == null ? authentication.getAuthenticatedBy().getName() + : authentication.getLookedUpBy().getName(); + userName = authentication.getUser().principal(); + apiKeyId = invalidateMyApiKeyRequest.getId(); + apiKeyName = invalidateMyApiKeyRequest.getName(); + } - findApiKeysForUserRealmApiKeyIdAndNameCombination(userName, realmName, invalidateMyApiKeyRequest.getName(), - invalidateMyApiKeyRequest.getId(), true, false, ActionListener.wrap(apiKeyIds -> { + findApiKeysForUserRealmApiKeyIdAndNameCombination(userName, realmName, apiKeyName, apiKeyId, true, false, + ActionListener.wrap(apiKeyIds -> { if (apiKeyIds.isEmpty()) { logger.warn("No api key to invalidate for {}", invalidateMyApiKeyRequest); listener.onResponse(InvalidateApiKeyResponse.emptyResponse()); From f3e3c7bcde183cff2355529cc162db606986627e Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad Date: Fri, 3 May 2019 12:18:08 +1000 Subject: [PATCH 4/4] typo --- docs/java-rest/high-level/security/get-my-api-key.asciidoc | 2 +- .../high-level/security/invalidate-my-api-key.asciidoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/java-rest/high-level/security/get-my-api-key.asciidoc b/docs/java-rest/high-level/security/get-my-api-key.asciidoc index 1f21c576f60bd..9cb07dbdccb9b 100644 --- a/docs/java-rest/high-level/security/get-my-api-key.asciidoc +++ b/docs/java-rest/high-level/security/get-my-api-key.asciidoc @@ -1,7 +1,7 @@ -- :api: get-my-api-key :request: GetMyApiKeyRequest -:response: GetMyApiKeyRequest +:response: GetMyApiKeyResponse -- [id="{upid}-{api}"] diff --git a/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc b/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc index 8281c59181748..cbe95a4b7e3d8 100644 --- a/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc +++ b/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc @@ -1,7 +1,7 @@ -- :api: invalidate-my-api-key :request: InvalidateMyApiKeyRequest -:response: InvalidateMyApiKeyRequest +:response: InvalidateMyApiKeyResponse -- [id="{upid}-{api}"]