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 99350fc29db8a..5a66024fa7ce2 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 f9a1c5c6571eb..135fd9a2f7099 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; @@ -1962,6 +1964,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())); @@ -2141,4 +2234,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..9cb07dbdccb9b --- /dev/null +++ b/docs/java-rest/high-level/security/get-my-api-key.asciidoc @@ -0,0 +1,51 @@ +-- +:api: get-my-api-key +:request: GetMyApiKeyRequest +:response: GetMyApiKeyResponse +-- + +[id="{upid}-{api}"] +=== Get information for API key(s) owned by the authenticated user 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 id or by name + +. 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"] +-------------------------------------------------- +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 the 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..cbe95a4b7e3d8 --- /dev/null +++ b/docs/java-rest/high-level/security/invalidate-my-api-key.asciidoc @@ -0,0 +1,59 @@ +-- +:api: invalidate-my-api-key +:request: InvalidateMyApiKeyRequest +:response: InvalidateMyApiKeyResponse +-- + +[id="{upid}-{api}"] +=== Invalidate API key(s) owned by the authenticated user 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 id or by name + +. All API key(s) of the current authenticated 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..821e0ec4fb4e0 --- /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 current authenticated 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 current 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 current authenticated 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 f7d03c2356e5b..b85c0cb7ff453 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 { entry("manage", MANAGE), entry("manage_ml", MANAGE_ML), entry("manage_data_frame_transforms", MANAGE_DATA_FRAME), + entry("manage_api_key", MANAGE_API_KEY), + entry("owner_manage_api_key", OWNER_MANAGE_API_KEY), + entry("create_api_key", CREATE_API_KEY), entry("manage_token", MANAGE_TOKEN), entry("manage_watcher", MANAGE_WATCHER), entry("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 a36a004c7f413..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 @@ -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.apikey.RestCreateApiKeyAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyAction; +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.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; @@ -764,7 +770,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) ); } @@ -819,7 +827,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 74d4cce8d02de..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 @@ -62,8 +62,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; @@ -639,97 +643,90 @@ 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} - */ - public void invalidateApiKeysForRealmAndUser(String realmName, String userName, - ActionListener invalidateListener) { - 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); - } /** - * Invalidate API key for given API key id - * @param apiKeyId API key id - * @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 invalidateApiKeyForApiKeyId(String apiKeyId, ActionListener invalidateListener) { + public void invalidateApiKeysForCurrentUser(final InvalidateMyApiKeyRequest invalidateMyApiKeyRequest, + final ActionListener listener) { ensureEnabled(); - if (Strings.hasText(apiKeyId) == false) { - logger.trace("No api key id provided"); - invalidateListener.onFailure(new IllegalArgumentException("api key id 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 { - 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)); - } + realmName = authentication.getLookedUpBy() == null ? authentication.getAuthenticatedBy().getName() + : authentication.getLookedUpBy().getName(); + userName = authentication.getUser().principal(); + apiKeyId = invalidateMyApiKeyRequest.getId(); + apiKeyName = invalidateMyApiKeyRequest.getName(); + } + + 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()); + } 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, @@ -743,31 +740,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_MAIN_ALIAS) - .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_MAIN_ALIAS) + .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()); @@ -776,25 +776,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); } @@ -923,70 +916,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/apikey/RestCreateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java index 14d4726553dff..84bb746808197 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/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/apikey/RestGetApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyAction.java index 71ed5a06efb65..9430124175b94 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/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/apikey/RestGetMyApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetMyApiKeyAction.java new file mode 100644 index 0000000000000..d562e9b893f8e --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetMyApiKeyAction.java @@ -0,0 +1,61 @@ +/* + * 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.apikey; + +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 org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +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/apikey/RestInvalidateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateApiKeyAction.java index b11a0edde42f8..4675e3dac65d7 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/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/apikey/RestInvalidateMyApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateMyApiKeyAction.java new file mode 100644 index 0000000000000..a4ab483ea6295 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateMyApiKeyAction.java @@ -0,0 +1,69 @@ +/* + * 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.apikey; + +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 org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +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 f6849cae4c1cd..62b6cbbe6473f 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; @@ -60,11 +62,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() @@ -144,6 +184,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<>(); @@ -479,6 +551,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 @@ -517,12 +726,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) @@ -534,4 +743,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 + } + } +}