diff --git a/docs/changelog/98259.yaml b/docs/changelog/98259.yaml new file mode 100644 index 0000000000000..359ec0c6c390c --- /dev/null +++ b/docs/changelog/98259.yaml @@ -0,0 +1,6 @@ +pr: 98259 +summary: Support getting active-only API keys via Get API keys API +area: Security +type: enhancement +issues: + - 97995 diff --git a/server/src/main/java/org/elasticsearch/TransportVersion.java b/server/src/main/java/org/elasticsearch/TransportVersion.java index 5f8c2d1f96e60..bb24c06c798e8 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersion.java +++ b/server/src/main/java/org/elasticsearch/TransportVersion.java @@ -176,9 +176,10 @@ private static TransportVersion registerTransportVersion(int id, String uniqueId public static final TransportVersion V_8_500_051 = registerTransportVersion(8_500_051, "a28b43bc-bb5f-4406-afcf-26900aa98a71"); public static final TransportVersion V_8_500_052 = registerTransportVersion(8_500_052, "2d382b3d-9838-4cce-84c8-4142113e5c2b"); public static final TransportVersion V_8_500_053 = registerTransportVersion(8_500_053, "aa603bae-01e2-380a-8950-6604468e8c6d"); + public static final TransportVersion V_8_500_054 = registerTransportVersion(8_500_054, "b76ef950-af03-4dda-85c2-6400ec442e7e"); private static class CurrentHolder { - private static final TransportVersion CURRENT = findCurrent(V_8_500_053); + private static final TransportVersion CURRENT = findCurrent(V_8_500_054); // finds the pluggable current version, or uses the given fallback private static TransportVersion findCurrent(TransportVersion fallback) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyRequest.java index 51ca2060388c6..ebf36bdcdc421 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyRequest.java @@ -25,12 +25,15 @@ */ public final class GetApiKeyRequest extends ActionRequest { + static TransportVersion API_KEY_ACTIVE_ONLY_PARAM_TRANSPORT_VERSION = TransportVersion.V_8_500_054; + private final String realmName; private final String userName; private final String apiKeyId; private final String apiKeyName; private final boolean ownedByAuthenticatedUser; private final boolean withLimitedBy; + private final boolean activeOnly; public GetApiKeyRequest(StreamInput in) throws IOException { super(in); @@ -48,6 +51,11 @@ public GetApiKeyRequest(StreamInput in) throws IOException { } else { withLimitedBy = false; } + if (in.getTransportVersion().onOrAfter(API_KEY_ACTIVE_ONLY_PARAM_TRANSPORT_VERSION)) { + activeOnly = in.readBoolean(); + } else { + activeOnly = false; + } } private GetApiKeyRequest( @@ -56,7 +64,8 @@ private GetApiKeyRequest( @Nullable String apiKeyId, @Nullable String apiKeyName, boolean ownedByAuthenticatedUser, - boolean withLimitedBy + boolean withLimitedBy, + boolean activeOnly ) { this.realmName = textOrNull(realmName); this.userName = textOrNull(userName); @@ -64,6 +73,7 @@ private GetApiKeyRequest( this.apiKeyName = textOrNull(apiKeyName); this.ownedByAuthenticatedUser = ownedByAuthenticatedUser; this.withLimitedBy = withLimitedBy; + this.activeOnly = activeOnly; } private static String textOrNull(@Nullable String arg) { @@ -94,6 +104,10 @@ public boolean withLimitedBy() { return withLimitedBy; } + public boolean activeOnly() { + return activeOnly; + } + @Override public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; @@ -132,6 +146,9 @@ public void writeTo(StreamOutput out) throws IOException { if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_5_0)) { out.writeBoolean(withLimitedBy); } + if (out.getTransportVersion().onOrAfter(API_KEY_ACTIVE_ONLY_PARAM_TRANSPORT_VERSION)) { + out.writeBoolean(activeOnly); + } } @Override @@ -148,12 +165,13 @@ public boolean equals(Object o) { && Objects.equals(userName, that.userName) && Objects.equals(apiKeyId, that.apiKeyId) && Objects.equals(apiKeyName, that.apiKeyName) - && withLimitedBy == that.withLimitedBy; + && withLimitedBy == that.withLimitedBy + && activeOnly == that.activeOnly; } @Override public int hashCode() { - return Objects.hash(realmName, userName, apiKeyId, apiKeyName, ownedByAuthenticatedUser, withLimitedBy); + return Objects.hash(realmName, userName, apiKeyId, apiKeyName, ownedByAuthenticatedUser, withLimitedBy, activeOnly); } public static Builder builder() { @@ -167,6 +185,7 @@ public static class Builder { private String apiKeyName = null; private boolean ownedByAuthenticatedUser = false; private boolean withLimitedBy = false; + private boolean activeOnly = false; public Builder realmName(String realmName) { this.realmName = realmName; @@ -206,8 +225,13 @@ public Builder withLimitedBy(boolean withLimitedBy) { return this; } + public Builder activeOnly(boolean activeOnly) { + this.activeOnly = activeOnly; + return this; + } + public GetApiKeyRequest build() { - return new GetApiKeyRequest(realmName, userName, apiKeyId, apiKeyName, ownedByAuthenticatedUser, withLimitedBy); + return new GetApiKeyRequest(realmName, userName, apiKeyId, apiKeyName, ownedByAuthenticatedUser, withLimitedBy, activeOnly); } } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyRequestTests.java index e88bdf2751778..1001cd4863f5d 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyRequestTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.io.stream.OutputStreamStreamOutput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.TransportVersionUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -21,6 +22,7 @@ import java.util.function.Supplier; import static org.elasticsearch.test.TransportVersionUtils.randomVersionBetween; +import static org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyRequest.API_KEY_ACTIVE_ONLY_PARAM_TRANSPORT_VERSION; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -38,13 +40,17 @@ public void testRequestValidation() { request = GetApiKeyRequest.builder().apiKeyName(randomAlphaOfLength(5)).ownedByAuthenticatedUser(randomBoolean()).build(); ve = request.validate(); assertNull(ve); - request = GetApiKeyRequest.builder().realmName(randomAlphaOfLength(5)).build(); + request = GetApiKeyRequest.builder().realmName(randomAlphaOfLength(5)).activeOnly(randomBoolean()).build(); ve = request.validate(); assertNull(ve); - request = GetApiKeyRequest.builder().userName(randomAlphaOfLength(5)).build(); + request = GetApiKeyRequest.builder().userName(randomAlphaOfLength(5)).activeOnly(randomBoolean()).build(); ve = request.validate(); assertNull(ve); - request = GetApiKeyRequest.builder().realmName(randomAlphaOfLength(5)).userName(randomAlphaOfLength(7)).build(); + request = GetApiKeyRequest.builder() + .realmName(randomAlphaOfLength(5)) + .userName(randomAlphaOfLength(7)) + .activeOnly(randomBoolean()) + .build(); ve = request.validate(); assertNull(ve); } @@ -79,6 +85,7 @@ public void writeTo(StreamOutput out) throws IOException { out.writeOptionalString(apiKeyName); out.writeOptionalBoolean(ownedByAuthenticatedUser); out.writeBoolean(randomBoolean()); + out.writeBoolean(randomBoolean()); } } @@ -143,6 +150,7 @@ public void testSerialization() throws IOException { .apiKeyId(apiKeyId) .ownedByAuthenticatedUser(true) .withLimitedBy(randomBoolean()) + .activeOnly(randomBoolean()) .build(); ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); OutputStreamStreamOutput out = new OutputStreamStreamOutput(outBuffer); @@ -157,17 +165,48 @@ public void testSerialization() throws IOException { assertThat(requestFromInputStream.ownedByAuthenticatedUser(), is(true)); // old version so the default for `withLimitedBy` is false assertThat(requestFromInputStream.withLimitedBy(), is(false)); + // old version so the default for `activeOnly` is false + assertThat(requestFromInputStream.activeOnly(), is(false)); } { - final GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.builder().apiKeyId(apiKeyId).withLimitedBy(randomBoolean()).build(); + final GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.builder() + .apiKeyId(apiKeyId) + .ownedByAuthenticatedUser(randomBoolean()) + .withLimitedBy(randomBoolean()) + .activeOnly(randomBoolean()) + .build(); ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); OutputStreamStreamOutput out = new OutputStreamStreamOutput(outBuffer); - out.setTransportVersion(randomVersionBetween(random(), TransportVersion.V_8_5_0, TransportVersion.current())); + TransportVersion beforeActiveOnly = TransportVersionUtils.getPreviousVersion(API_KEY_ACTIVE_ONLY_PARAM_TRANSPORT_VERSION); + out.setTransportVersion(randomVersionBetween(random(), TransportVersion.V_8_5_0, beforeActiveOnly)); + getApiKeyRequest.writeTo(out); + + InputStreamStreamInput inputStreamStreamInput = new InputStreamStreamInput(new ByteArrayInputStream(outBuffer.toByteArray())); + inputStreamStreamInput.setTransportVersion(randomVersionBetween(random(), TransportVersion.V_8_5_0, beforeActiveOnly)); + GetApiKeyRequest requestFromInputStream = new GetApiKeyRequest(inputStreamStreamInput); + + assertThat(requestFromInputStream.getApiKeyId(), equalTo(getApiKeyRequest.getApiKeyId())); + assertThat(requestFromInputStream.ownedByAuthenticatedUser(), is(getApiKeyRequest.ownedByAuthenticatedUser())); + assertThat(requestFromInputStream.withLimitedBy(), is(getApiKeyRequest.withLimitedBy())); + // old version so the default for `activeOnly` is false + assertThat(requestFromInputStream.activeOnly(), is(false)); + } + { + final GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.builder() + .apiKeyId(apiKeyId) + .withLimitedBy(randomBoolean()) + .activeOnly(randomBoolean()) + .build(); + ByteArrayOutputStream outBuffer = new ByteArrayOutputStream(); + OutputStreamStreamOutput out = new OutputStreamStreamOutput(outBuffer); + out.setTransportVersion( + randomVersionBetween(random(), API_KEY_ACTIVE_ONLY_PARAM_TRANSPORT_VERSION, TransportVersion.current()) + ); getApiKeyRequest.writeTo(out); InputStreamStreamInput inputStreamStreamInput = new InputStreamStreamInput(new ByteArrayInputStream(outBuffer.toByteArray())); inputStreamStreamInput.setTransportVersion( - randomVersionBetween(random(), TransportVersion.V_8_5_0, TransportVersion.current()) + randomVersionBetween(random(), API_KEY_ACTIVE_ONLY_PARAM_TRANSPORT_VERSION, TransportVersion.current()) ); GetApiKeyRequest requestFromInputStream = new GetApiKeyRequest(inputStreamStreamInput); diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/GetApiKeysRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/GetApiKeysRestIT.java new file mode 100644 index 0000000000000..a71c4e342a9ce --- /dev/null +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/GetApiKeysRestIT.java @@ -0,0 +1,274 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.apikey; + +import org.apache.http.client.methods.HttpGet; +import org.apache.http.util.EntityUtils; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Response; +import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.test.XContentTestUtils; +import org.elasticsearch.transport.TcpTransport; +import org.elasticsearch.xcontent.XContentParser; +import org.elasticsearch.xcontent.XContentParserConfiguration; +import org.elasticsearch.xcontent.XContentType; +import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; +import org.elasticsearch.xpack.core.security.action.apikey.GetApiKeyResponse; +import org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken; +import org.elasticsearch.xpack.security.SecurityOnTrialLicenseRestTestCase; +import org.junit.Before; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyArray; +import static org.hamcrest.Matchers.equalTo; + +public class GetApiKeysRestIT extends SecurityOnTrialLicenseRestTestCase { + private static final SecureString END_USER_PASSWORD = new SecureString("end-user-password".toCharArray()); + private static final String MANAGE_OWN_API_KEY_USER = "manage_own_api_key_user"; + private static final String MANAGE_SECURITY_USER = "manage_security_user"; + + @Before + public void createUsers() throws IOException { + createUser(MANAGE_OWN_API_KEY_USER, END_USER_PASSWORD, List.of("manage_own_api_key_role")); + createRole("manage_own_api_key_role", Set.of("manage_own_api_key")); + createUser(MANAGE_SECURITY_USER, END_USER_PASSWORD, List.of("manage_security_role")); + createRole("manage_security_role", Set.of("manage_security")); + } + + public void testGetApiKeysWithActiveOnlyFlag() throws Exception { + final String apiKeyId0 = createApiKey(MANAGE_SECURITY_USER, "key-0"); + final String apiKeyId1 = createApiKey(MANAGE_SECURITY_USER, "key-1"); + // Set short enough expiration for the API key to be expired by the time we query for it + final String apiKeyId2 = createApiKey(MANAGE_SECURITY_USER, "key-2", TimeValue.timeValueNanos(1)); + + // All API keys returned when flag false (implicitly or explicitly) + { + final Map parameters = new HashMap<>(); + if (randomBoolean()) { + parameters.put("active_only", "false"); + } + assertResponseContainsApiKeyIds(getApiKeysWithRequestParams(parameters), apiKeyId0, apiKeyId1, apiKeyId2); + } + + // Only active keys returned when flag true + assertResponseContainsApiKeyIds(getApiKeysWithRequestParams(Map.of("active_only", "true")), apiKeyId0, apiKeyId1); + // Also works with `name` filter + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(Map.of("active_only", "true", "name", randomFrom("*", "key-*"))), + apiKeyId0, + apiKeyId1 + ); + // Also works with `realm_name` filter + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(Map.of("active_only", "true", "realm_name", "default_native")), + apiKeyId0, + apiKeyId1 + ); + + // Same applies to invalidated key + getSecurityClient().invalidateApiKeys(apiKeyId0); + { + final Map parameters = new HashMap<>(); + if (randomBoolean()) { + parameters.put("active_only", "false"); + } + assertResponseContainsApiKeyIds(getApiKeysWithRequestParams(parameters), apiKeyId0, apiKeyId1, apiKeyId2); + } + assertResponseContainsApiKeyIds(getApiKeysWithRequestParams(Map.of("active_only", "true")), apiKeyId1); + // also works with name filter + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(Map.of("active_only", "true", "name", randomFrom("*", "key-*", "key-1"))), + apiKeyId1 + ); + + // We get an empty result when no API keys active + getSecurityClient().invalidateApiKeys(apiKeyId1); + assertThat(getApiKeysWithRequestParams(Map.of("active_only", "true")).getApiKeyInfos(), emptyArray()); + + { + // Using together with id parameter, returns 404 for inactive key + var ex = expectThrows( + ResponseException.class, + () -> getApiKeysWithRequestParams(Map.of("active_only", "true", "id", randomFrom(apiKeyId0, apiKeyId1, apiKeyId2))) + ); + assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(404)); + } + + { + // manage_own_api_key prohibits owner=false, even if active_only is set + var ex = expectThrows( + ResponseException.class, + () -> getApiKeysWithRequestParams(MANAGE_OWN_API_KEY_USER, Map.of("active_only", "true", "owner", "false")) + ); + assertThat(ex.getResponse().getStatusLine().getStatusCode(), equalTo(403)); + } + } + + public void testGetApiKeysWithActiveOnlyFlagAndMultipleUsers() throws Exception { + final String manageOwnApiKeyUserApiKeyId = createApiKey(MANAGE_OWN_API_KEY_USER, "key-0"); + final String manageApiKeyUserApiKeyId = createApiKey(MANAGE_SECURITY_USER, "key-1"); + + // Both users' API keys are returned + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(Map.of("active_only", Boolean.toString(randomBoolean()))), + manageOwnApiKeyUserApiKeyId, + manageApiKeyUserApiKeyId + ); + // Filtering by username works (also via owner flag) + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(Map.of("active_only", Boolean.toString(randomBoolean()), "username", MANAGE_SECURITY_USER)), + manageApiKeyUserApiKeyId + ); + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(Map.of("active_only", Boolean.toString(randomBoolean()), "username", MANAGE_OWN_API_KEY_USER)), + manageOwnApiKeyUserApiKeyId + ); + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(MANAGE_SECURITY_USER, Map.of("active_only", Boolean.toString(randomBoolean()), "owner", "true")), + manageApiKeyUserApiKeyId + ); + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(MANAGE_OWN_API_KEY_USER, Map.of("active_only", Boolean.toString(randomBoolean()), "owner", "true")), + manageOwnApiKeyUserApiKeyId + ); + + // One user's API key is active + invalidateApiKeysForUser(MANAGE_OWN_API_KEY_USER); + + // Filtering by username still works (also via owner flag) + assertResponseContainsApiKeyIds(getApiKeysWithRequestParams(Map.of("active_only", "true")), manageApiKeyUserApiKeyId); + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(Map.of("active_only", "true", "username", MANAGE_SECURITY_USER)), + manageApiKeyUserApiKeyId + ); + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(MANAGE_SECURITY_USER, Map.of("active_only", "true", "owner", "true")), + manageApiKeyUserApiKeyId + ); + assertThat( + getApiKeysWithRequestParams(Map.of("active_only", "true", "username", MANAGE_OWN_API_KEY_USER)).getApiKeyInfos(), + emptyArray() + ); + assertThat( + getApiKeysWithRequestParams(MANAGE_OWN_API_KEY_USER, Map.of("active_only", "true", "owner", "true")).getApiKeyInfos(), + emptyArray() + ); + + // No more active API keys + invalidateApiKeysForUser(MANAGE_SECURITY_USER); + + assertThat( + getApiKeysWithRequestParams( + Map.of("active_only", "true", "username", randomFrom(MANAGE_SECURITY_USER, MANAGE_OWN_API_KEY_USER)) + ).getApiKeyInfos(), + emptyArray() + ); + assertThat( + getApiKeysWithRequestParams( + randomFrom(MANAGE_SECURITY_USER, MANAGE_OWN_API_KEY_USER), + Map.of("active_only", "true", "owner", "true") + ).getApiKeyInfos(), + emptyArray() + ); + // With flag set to false, we get both inactive keys + assertResponseContainsApiKeyIds( + getApiKeysWithRequestParams(randomBoolean() ? Map.of() : Map.of("active_only", "false")), + manageOwnApiKeyUserApiKeyId, + manageApiKeyUserApiKeyId + ); + } + + private GetApiKeyResponse getApiKeysWithRequestParams(Map requestParams) throws IOException { + return getApiKeysWithRequestParams(MANAGE_SECURITY_USER, requestParams); + } + + private GetApiKeyResponse getApiKeysWithRequestParams(String userOnRequest, Map requestParams) throws IOException { + final var request = new Request(HttpGet.METHOD_NAME, "/_security/api_key/"); + request.addParameters(requestParams); + setUserForRequest(request, userOnRequest); + return GetApiKeyResponse.fromXContent(getParser(client().performRequest(request))); + } + + private static void assertResponseContainsApiKeyIds(GetApiKeyResponse response, String... ids) { + assertThat(Arrays.stream(response.getApiKeyInfos()).map(ApiKey::getId).collect(Collectors.toList()), containsInAnyOrder(ids)); + } + + private static XContentParser getParser(Response response) throws IOException { + final byte[] responseBody = EntityUtils.toByteArray(response.getEntity()); + return XContentType.JSON.xContent().createParser(XContentParserConfiguration.EMPTY, responseBody); + } + + private String createApiKey(String creatorUser, String apiKeyName) throws IOException { + return createApiKey(creatorUser, apiKeyName, null); + } + + /** + * Returns id of created API key. + */ + private String createApiKey(String creatorUser, String apiKeyName, @Nullable TimeValue expiration) throws IOException { + // Sanity check to ensure API key name and creator name aren't flipped + assert creatorUser.equals(MANAGE_OWN_API_KEY_USER) || creatorUser.equals(MANAGE_SECURITY_USER); + + // Exercise cross cluster keys, if viable (i.e., creator has enough privileges and feature flag is enabled) + final boolean createCrossClusterKey = creatorUser.equals(MANAGE_SECURITY_USER) + && TcpTransport.isUntrustedRemoteClusterEnabled() + && randomBoolean(); + if (createCrossClusterKey) { + final Map createApiKeyRequestBody = expiration == null + ? Map.of("name", apiKeyName, "access", Map.of("search", List.of(Map.of("names", List.of("*"))))) + : Map.of("name", apiKeyName, "expiration", expiration, "access", Map.of("search", List.of(Map.of("names", List.of("*"))))); + final var createApiKeyRequest = new Request("POST", "/_security/cross_cluster/api_key"); + createApiKeyRequest.setJsonEntity( + XContentTestUtils.convertToXContent(createApiKeyRequestBody, XContentType.JSON).utf8ToString() + ); + setUserForRequest(createApiKeyRequest, creatorUser); + + final Response createApiKeyResponse = client().performRequest(createApiKeyRequest); + + assertOK(createApiKeyResponse); + final Map createApiKeyResponseMap = responseAsMap(createApiKeyResponse); + return (String) createApiKeyResponseMap.get("id"); + } else { + final Map createApiKeyRequestBody = expiration == null + ? Map.of("name", apiKeyName) + : Map.of("name", apiKeyName, "expiration", expiration); + final var createApiKeyRequest = new Request("POST", "/_security/api_key"); + createApiKeyRequest.setJsonEntity( + XContentTestUtils.convertToXContent(createApiKeyRequestBody, XContentType.JSON).utf8ToString() + ); + setUserForRequest(createApiKeyRequest, creatorUser); + + final Response createApiKeyResponse = client().performRequest(createApiKeyRequest); + + assertOK(createApiKeyResponse); + final Map createApiKeyResponseMap = responseAsMap(createApiKeyResponse); + return (String) createApiKeyResponseMap.get("id"); + } + } + + private void setUserForRequest(Request request, String username) { + request.setOptions( + request.getOptions() + .toBuilder() + .removeHeader("Authorization") + .addHeader("Authorization", UsernamePasswordToken.basicAuthHeaderValue(username, END_USER_PASSWORD)) + ); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportGetApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportGetApiKeyAction.java index 1bd0562593cc1..b627693ebda3d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportGetApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/apikey/TransportGetApiKeyAction.java @@ -57,7 +57,7 @@ protected void doExecute(Task task, GetApiKeyRequest request, ActionListener listener ) { ensureEnabled(); @@ -1901,17 +1902,18 @@ public void getApiKeys( username, apiKeyName, apiKeyIds, - false, - false, + activeOnly, + activeOnly, hit -> convertSearchHitToApiKeyInfo(hit, withLimitedBy), ActionListener.wrap(apiKeyInfos -> { if (apiKeyInfos.isEmpty()) { logger.debug( - "No active api keys found for realms {}, user [{}], api key name [{}] and api key ids {}", + "No API keys found for realms {}, user [{}], API key name [{}], API key IDs {}, and active_only flag [{}]", Arrays.toString(realmNames), username, apiKeyName, - Arrays.toString(apiKeyIds) + Arrays.toString(apiKeyIds), + activeOnly ); listener.onResponse(GetApiKeyResponse.emptyResponse()); } else { 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 2e2b4fb950391..cd751740dd0fb 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 @@ -50,6 +50,7 @@ protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClien final String realmName = request.param("realm_name"); final boolean myApiKeysOnly = request.paramAsBoolean("owner", false); final boolean withLimitedBy = request.paramAsBoolean("with_limited_by", false); + final boolean activeOnly = request.paramAsBoolean("active_only", false); final GetApiKeyRequest getApiKeyRequest = GetApiKeyRequest.builder() .realmName(realmName) .userName(userName) @@ -57,6 +58,7 @@ protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClien .apiKeyName(apiKeyName) .ownedByAuthenticatedUser(myApiKeysOnly) .withLimitedBy(withLimitedBy) + .activeOnly(activeOnly) .build(); return channel -> client.execute(GetApiKeyAction.INSTANCE, getApiKeyRequest, new RestBuilderListener<>(channel) { @Override diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 81829b0c94dee..a7bc36efbc92e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -266,6 +266,8 @@ public void testCreateApiKeyUsesBulkIndexAction() throws Exception { @SuppressWarnings("unchecked") public void testGetApiKeys() throws Exception { + final long now = randomMillisUpToYear9999(); + when(clock.instant()).thenReturn(Instant.ofEpochMilli(now)); final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build(); when(client.threadPool()).thenReturn(threadPool); SearchRequestBuilder searchRequestBuilder = Mockito.spy(new SearchRequestBuilder(client, SearchAction.INSTANCE)); @@ -283,7 +285,8 @@ public void testGetApiKeys() throws Exception { String apiKeyName = randomFrom(randomAlphaOfLengthBetween(3, 8), null); String[] apiKeyIds = generateRandomStringArray(4, 4, true, true); PlainActionFuture getApiKeyResponsePlainActionFuture = new PlainActionFuture<>(); - service.getApiKeys(realmNames, username, apiKeyName, apiKeyIds, randomBoolean(), getApiKeyResponsePlainActionFuture); + final boolean activeOnly = randomBoolean(); + service.getApiKeys(realmNames, username, apiKeyName, apiKeyIds, randomBoolean(), activeOnly, getApiKeyResponsePlainActionFuture); final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().filter(QueryBuilders.termQuery("doc_type", "api_key")); if (realmNames != null && realmNames.length > 0) { if (realmNames.length == 1) { @@ -310,6 +313,13 @@ public void testGetApiKeys() throws Exception { if (apiKeyIds != null && apiKeyIds.length > 0) { boolQuery.filter(QueryBuilders.idsQuery().addIds(apiKeyIds)); } + if (activeOnly) { + boolQuery.filter(QueryBuilders.termQuery("api_key_invalidated", false)); + final BoolQueryBuilder expiredQuery = QueryBuilders.boolQuery(); + expiredQuery.should(QueryBuilders.rangeQuery("expiration_time").gt(now)); + expiredQuery.should(QueryBuilders.boolQuery().mustNot(QueryBuilders.existsQuery("expiration_time"))); + boolQuery.filter(expiredQuery); + } verify(searchRequestBuilder).setQuery(eq(boolQuery)); verify(searchRequestBuilder).setFetchSource(eq(true)); assertThat(searchRequest.get().source().query(), is(boolQuery)); @@ -1157,7 +1167,7 @@ public void testParseRoleDescriptors() { assertThat(roleWithoutRestriction.getRestriction().getWorkflows(), nullValue()); } - public void testApiKeyServiceDisabled() throws Exception { + public void testApiKeyServiceDisabled() { final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), false).build(); final ApiKeyService service = createApiKeyService(settings); @@ -1169,6 +1179,7 @@ public void testApiKeyServiceDisabled() throws Exception { null, null, randomBoolean(), + randomBoolean(), new PlainActionFuture<>() ) );