diff --git a/server/src/main/java/org/elasticsearch/TransportVersion.java b/server/src/main/java/org/elasticsearch/TransportVersion.java index 76a1a4056bdc2..d0bdced9def86 100644 --- a/server/src/main/java/org/elasticsearch/TransportVersion.java +++ b/server/src/main/java/org/elasticsearch/TransportVersion.java @@ -121,12 +121,13 @@ private static TransportVersion registerTransportVersion(int id, String uniqueId * Detached transport versions added below here. */ public static final TransportVersion V_8_500_000 = registerTransportVersion(8_500_000, "dc3cbf06-3ed5-4e1b-9978-ee1d04d235bc"); + public static final TransportVersion V_8_500_001 = registerTransportVersion(8_500_001, "c943cfe5-c89d-4eae-989f-f5f4537e84e0"); /** * Reference to the most recent transport version. * This should be the transport version with the highest id. */ - public static final TransportVersion CURRENT = V_8_500_000; + public static final TransportVersion CURRENT = V_8_500_001; /** * Reference to the earliest compatible transport version to this version of the codebase. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java index 4e08cafaba9cb..01fb9eaf7737a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKey.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParserUtils; import org.elasticsearch.core.Nullable; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.xcontent.ConstructingObjectParser; import org.elasticsearch.xcontent.ObjectParser; import org.elasticsearch.xcontent.ParseField; @@ -80,6 +81,7 @@ public String value() { private final String name; private final String id; + private final Type type; private final Instant creation; private final Instant expiration; private final boolean invalidated; @@ -94,6 +96,7 @@ public String value() { public ApiKey( String name, String id, + Type type, Instant creation, Instant expiration, boolean invalidated, @@ -106,6 +109,7 @@ public ApiKey( this( name, id, + type, creation, expiration, invalidated, @@ -120,6 +124,7 @@ public ApiKey( private ApiKey( String name, String id, + Type type, Instant creation, Instant expiration, boolean invalidated, @@ -131,6 +136,7 @@ private ApiKey( ) { this.name = name; this.id = id; + this.type = type; // As we do not yet support the nanosecond precision when we serialize to JSON, // here creating the 'Instant' of milliseconds precision. // This Instant can then be used for date comparison. @@ -153,6 +159,14 @@ public ApiKey(StreamInput in) throws IOException { this.name = in.readString(); } this.id = in.readString(); + if (in.getTransportVersion().onOrAfter(TransportVersion.V_8_500_001)) { + this.type = in.readEnum(Type.class); + } else { + // This default is safe because + // 1. ApiKey objects never transfer between nodes + // 2. Creating cross-cluster API keys mandates minimal node version that understands the API key type + this.type = Type.REST; + } this.creation = in.readInstant(); this.expiration = in.readOptionalInstant(); this.invalidated = in.readBoolean(); @@ -181,6 +195,10 @@ public String getName() { return name; } + public Type getType() { + return type; + } + public Instant getCreation() { return creation; } @@ -221,7 +239,11 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } public XContentBuilder innerToXContent(XContentBuilder builder, Params params) throws IOException { - builder.field("id", id).field("name", name).field("creation", creation.toEpochMilli()); + builder.field("id", id).field("name", name); + if (TcpTransport.isUntrustedRemoteClusterEnabled()) { + builder.field("type", type.value()); + } + builder.field("creation", creation.toEpochMilli()); if (expiration != null) { builder.field("expiration", expiration.toEpochMilli()); } @@ -237,6 +259,7 @@ public XContentBuilder innerToXContent(XContentBuilder builder, Params params) t builder.endObject(); } if (limitedBy != null) { + assert type != Type.CROSS_CLUSTER; builder.field("limited_by", limitedBy); } return builder; @@ -250,6 +273,9 @@ public void writeTo(StreamOutput out) throws IOException { out.writeString(name); } out.writeString(id); + if (out.getTransportVersion().onOrAfter(TransportVersion.V_8_500_001)) { + out.writeEnum(type); + } out.writeInstant(creation); out.writeOptionalInstant(expiration); out.writeBoolean(invalidated); @@ -266,7 +292,7 @@ public void writeTo(StreamOutput out) throws IOException { @Override public int hashCode() { - return Objects.hash(name, id, creation, expiration, invalidated, username, realm, metadata, roleDescriptors, limitedBy); + return Objects.hash(name, id, type, creation, expiration, invalidated, username, realm, metadata, roleDescriptors, limitedBy); } @Override @@ -283,6 +309,7 @@ public boolean equals(Object obj) { ApiKey other = (ApiKey) obj; return Objects.equals(name, other.name) && Objects.equals(id, other.id) + && Objects.equals(type, other.type) && Objects.equals(creation, other.creation) && Objects.equals(expiration, other.expiration) && Objects.equals(invalidated, other.invalidated) @@ -298,19 +325,22 @@ public boolean equals(Object obj) { return new ApiKey( (String) args[0], (String) args[1], - Instant.ofEpochMilli((Long) args[2]), - (args[3] == null) ? null : Instant.ofEpochMilli((Long) args[3]), - (Boolean) args[4], - (String) args[5], + // TODO: remove null check once TcpTransport.isUntrustedRemoteClusterEnabled() is removed + args[2] == null ? Type.REST : (Type) args[2], + Instant.ofEpochMilli((Long) args[3]), + (args[4] == null) ? null : Instant.ofEpochMilli((Long) args[4]), + (Boolean) args[5], (String) args[6], - (args[7] == null) ? null : (Map) args[7], - (List) args[8], - (RoleDescriptorsIntersection) args[9] + (String) args[7], + (args[8] == null) ? null : (Map) args[8], + (List) args[9], + (RoleDescriptorsIntersection) args[10] ); }); static { PARSER.declareString(constructorArg(), new ParseField("name")); PARSER.declareString(constructorArg(), new ParseField("id")); + PARSER.declareField(optionalConstructorArg(), Type::fromXContent, new ParseField("type"), ObjectParser.ValueType.STRING); PARSER.declareLong(constructorArg(), new ParseField("creation")); PARSER.declareLong(optionalConstructorArg(), new ParseField("expiration")); PARSER.declareBoolean(constructorArg(), new ParseField("invalidated")); @@ -339,6 +369,8 @@ public String toString() { + name + ", id=" + id + + ", type=" + + type.value() + ", creation=" + creation + ", expiration=" diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java index 21097837a5a95..b2162c63e42f6 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/ApiKeyTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.XContentTestUtils; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -32,6 +33,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; public class ApiKeyTests extends ESTestCase { @@ -39,6 +41,7 @@ public class ApiKeyTests extends ESTestCase { public void testXContent() throws IOException { final String name = randomAlphaOfLengthBetween(4, 10); final String id = randomAlphaOfLength(20); + final ApiKey.Type type = TcpTransport.isUntrustedRemoteClusterEnabled() ? randomFrom(ApiKey.Type.values()) : ApiKey.Type.REST; // between 1970 and 2065 final Instant creation = Instant.ofEpochSecond(randomLongBetween(0, 3000000000L), randomLongBetween(0, 999999999)); final Instant expiration = randomBoolean() @@ -49,11 +52,14 @@ public void testXContent() throws IOException { final String realmName = randomAlphaOfLengthBetween(3, 8); final Map metadata = randomMetadata(); final List roleDescriptors = randomBoolean() ? null : randomUniquelyNamedRoleDescriptors(0, 3); - final List limitedByRoleDescriptors = randomUniquelyNamedRoleDescriptors(0, 3); + final List limitedByRoleDescriptors = type == ApiKey.Type.CROSS_CLUSTER + ? null + : randomUniquelyNamedRoleDescriptors(0, 3); final ApiKey apiKey = new ApiKey( name, id, + type, creation, expiration, invalidated, @@ -77,6 +83,9 @@ public void testXContent() throws IOException { assertThat(map.get("name"), equalTo(name)); assertThat(map.get("id"), equalTo(id)); + if (TcpTransport.isUntrustedRemoteClusterEnabled()) { + assertThat(map.get("type"), equalTo(type.value())); + } assertThat(Long.valueOf(map.get("creation").toString()), equalTo(creation.toEpochMilli())); if (expiration != null) { assertThat(Long.valueOf(map.get("expiration").toString()), equalTo(expiration.toEpochMilli())); @@ -100,12 +109,16 @@ public void testXContent() throws IOException { } final var limitedByList = (List>) map.get("limited_by"); - assertThat(limitedByList.size(), equalTo(1)); - final Map limitedByMap = limitedByList.get(0); - assertThat(limitedByMap.size(), equalTo(limitedByRoleDescriptors.size())); - for (RoleDescriptor roleDescriptor : limitedByRoleDescriptors) { - assertThat(limitedByMap, hasKey(roleDescriptor.getName())); - assertThat(XContentTestUtils.convertToMap(roleDescriptor), equalTo(limitedByMap.get(roleDescriptor.getName()))); + if (type != ApiKey.Type.CROSS_CLUSTER) { + assertThat(limitedByList.size(), equalTo(1)); + final Map limitedByMap = limitedByList.get(0); + assertThat(limitedByMap.size(), equalTo(limitedByRoleDescriptors.size())); + for (RoleDescriptor roleDescriptor : limitedByRoleDescriptors) { + assertThat(limitedByMap, hasKey(roleDescriptor.getName())); + assertThat(XContentTestUtils.convertToMap(roleDescriptor), equalTo(limitedByMap.get(roleDescriptor.getName()))); + } + } else { + assertThat(limitedByList, nullValue()); } } @@ -130,7 +143,6 @@ private ApiKey.Type parseTypeString(String typeString) throws IOException { } } - @SuppressWarnings("unchecked") public static Map randomMetadata() { return randomFrom( Map.of( diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java index eaedc461137bc..fe8a49c0068f9 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/GetApiKeyResponseTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.xcontent.ToXContent; import org.elasticsearch.xcontent.XContentBuilder; import org.elasticsearch.xcontent.XContentFactory; @@ -36,9 +37,11 @@ public class GetApiKeyResponseTests extends ESTestCase { public void testSerialization() throws IOException { boolean withApiKeyName = randomBoolean(); boolean withExpiration = randomBoolean(); + final ApiKey.Type type = randomFrom(ApiKey.Type.values()); ApiKey apiKeyInfo = createApiKeyInfo( (withApiKeyName) ? randomAlphaOfLength(4) : null, randomAlphaOfLength(5), + type, Instant.now(), (withExpiration) ? Instant.now() : null, false, @@ -46,7 +49,7 @@ public void testSerialization() throws IOException { randomAlphaOfLength(5), randomBoolean() ? null : Map.of(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)), randomBoolean() ? null : randomUniquelyNamedRoleDescriptors(0, 3), - randomUniquelyNamedRoleDescriptors(1, 3) + type == ApiKey.Type.CROSS_CLUSTER ? null : randomUniquelyNamedRoleDescriptors(1, 3) ); GetApiKeyResponse response = new GetApiKeyResponse(Collections.singletonList(apiKeyInfo)); @@ -97,6 +100,7 @@ public void testToXContent() throws IOException { ApiKey apiKeyInfo1 = createApiKeyInfo( "name1", "id-1", + ApiKey.Type.REST, Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), false, @@ -109,6 +113,7 @@ public void testToXContent() throws IOException { ApiKey apiKeyInfo2 = createApiKeyInfo( "name2", "id-2", + ApiKey.Type.REST, Instant.ofEpochMilli(100000L), Instant.ofEpochMilli(10000000L), true, @@ -121,6 +126,7 @@ public void testToXContent() throws IOException { ApiKey apiKeyInfo3 = createApiKeyInfo( null, "id-3", + ApiKey.Type.REST, Instant.ofEpochMilli(100000L), null, true, @@ -130,15 +136,29 @@ public void testToXContent() throws IOException { roleDescriptors, limitedByRoleDescriptors ); - GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2, apiKeyInfo3)); + ApiKey apiKeyInfo4 = createApiKeyInfo( + "name4", + "id-4", + ApiKey.Type.CROSS_CLUSTER, + Instant.ofEpochMilli(100000L), + null, + true, + "user-c", + "realm-z", + Map.of("foo", "bar"), + roleDescriptors, + null + ); + GetApiKeyResponse response = new GetApiKeyResponse(Arrays.asList(apiKeyInfo1, apiKeyInfo2, apiKeyInfo3, apiKeyInfo4)); XContentBuilder builder = XContentFactory.jsonBuilder(); response.toXContent(builder, ToXContent.EMPTY_PARAMS); - assertThat(Strings.toString(builder), equalTo(XContentHelper.stripWhitespace(""" + assertThat(Strings.toString(builder), equalTo(XContentHelper.stripWhitespace(Strings.format(""" { "api_keys": [ { "id": "id-1", "name": "name1", + %s "creation": 100000, "expiration": 10000000, "invalidated": false, @@ -152,6 +172,7 @@ public void testToXContent() throws IOException { { "id": "id-2", "name": "name2", + %s "creation": 100000, "expiration": 10000000, "invalidated": true, @@ -191,6 +212,7 @@ public void testToXContent() throws IOException { { "id": "id-3", "name": null, + %s "creation": 100000, "invalidated": true, "username": "user-c", @@ -252,14 +274,53 @@ public void testToXContent() throws IOException { } } ] + }, + { + "id": "id-4", + "name": "name4", + %s + "creation": 100000, + "invalidated": true, + "username": "user-c", + "realm": "realm-z", + "metadata": { + "foo": "bar" + }, + "role_descriptors": { + "rd_42": { + "cluster": [ + "monitor" + ], + "indices": [ + { + "names": [ + "index" + ], + "privileges": [ + "read" + ], + "allow_restricted_indices": false + } + ], + "applications": [], + "run_as": [ + "foo" + ], + "metadata": {}, + "transient_metadata": { + "enabled": true + } + } + } } ] - }"""))); + }""", getType("rest"), getType("rest"), getType("rest"), getType("cross_cluster"))))); } private ApiKey createApiKeyInfo( String name, String id, + ApiKey.Type type, Instant creation, Instant expiration, boolean invalidated, @@ -272,6 +333,7 @@ private ApiKey createApiKeyInfo( return new ApiKey( name, id, + type, creation, expiration, invalidated, @@ -282,4 +344,8 @@ private ApiKey createApiKeyInfo( limitedByRoleDescriptors ); } + + private String getType(String type) { + return TcpTransport.isUntrustedRemoteClusterEnabled() ? "\"type\": \"" + type + "\"," : ""; + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponseTests.java index a1364b4b60efe..f2ff90f867bd6 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/apikey/QueryApiKeyResponseTests.java @@ -88,6 +88,7 @@ private QueryApiKeyResponse.Item randomItem() { private ApiKey randomApiKeyInfo() { final String name = randomAlphaOfLengthBetween(3, 8); final String id = randomAlphaOfLength(22); + final ApiKey.Type type = randomFrom(ApiKey.Type.values()); final String username = randomAlphaOfLengthBetween(3, 8); final String realm_name = randomAlphaOfLengthBetween(3, 8); final Instant creation = Instant.ofEpochMilli(randomMillisUpToYear9999()); @@ -97,6 +98,7 @@ private ApiKey randomApiKeyInfo() { return new ApiKey( name, id, + type, creation, expiration, false, @@ -104,7 +106,7 @@ private ApiKey randomApiKeyInfo() { realm_name, metadata, roleDescriptors, - randomUniquelyNamedRoleDescriptors(1, 3) + type == ApiKey.Type.CROSS_CLUSTER ? null : randomUniquelyNamedRoleDescriptors(1, 3) ); } diff --git a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java index f737ff141e246..e0b981bbff8e9 100644 --- a/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java +++ b/x-pack/plugin/security/qa/security-trial/src/javaRestTest/java/org/elasticsearch/xpack/security/apikey/ApiKeyRestIT.java @@ -54,6 +54,7 @@ import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; /** * Integration Rest Tests relating to API Keys. @@ -746,8 +747,10 @@ public void testCreateCrossClusterApiKey() throws IOException { if (randomBoolean()) { fetchRequest = new Request("GET", "/_security/api_key"); fetchRequest.addParameter("id", apiKeyId); + fetchRequest.addParameter("with_limited_by", String.valueOf(randomBoolean())); } else { fetchRequest = new Request("GET", "/_security/_query/api_key"); + fetchRequest.addParameter("with_limited_by", String.valueOf(randomBoolean())); fetchRequest.setJsonEntity(Strings.format(""" { "query": { "ids": { "values": ["%s"] } } }""", apiKeyId)); } @@ -760,6 +763,7 @@ public void testCreateCrossClusterApiKey() throws IOException { final ObjectPath fetchResponse = assertOKAndCreateObjectPath(client().performRequest(fetchRequest)); assertThat(fetchResponse.evaluate("api_keys.0.id"), equalTo(apiKeyId)); + assertThat(fetchResponse.evaluate("api_keys.0.type"), equalTo("cross_cluster")); assertThat( fetchResponse.evaluate("api_keys.0.role_descriptors"), equalTo( @@ -786,6 +790,7 @@ public void testCreateCrossClusterApiKey() throws IOException { ) ) ); + assertThat(fetchResponse.evaluate("api_keys.0.limited_by"), nullValue()); final Request deleteRequest = new Request("DELETE", "/_security/api_key"); deleteRequest.setJsonEntity(Strings.format(""" diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java index 44c2d9da43b92..2cb955204b3f1 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/ApiKeyIntegTests.java @@ -275,6 +275,7 @@ public void testCreateApiKey() throws Exception { assertThat(daysBetween, is(7L)); assertThat(getApiKeyDocument(response.getId()).get("type"), equalTo("rest")); + assertThat(getApiKeyInfo(client(), response.getId(), randomBoolean(), randomBoolean()).getType(), is(ApiKey.Type.REST)); // create simple api key final CreateApiKeyResponse simple = new CreateApiKeyRequestBuilder(client).setName("simple").get(); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java index 1cce4991979da..d6317ba2cac5d 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/apikey/ApiKeySingleNodeTests.java @@ -35,6 +35,7 @@ import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.action.Grant; +import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyAction; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.apikey.CreateApiKeyResponse; @@ -80,6 +81,7 @@ import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_MAIN_ALIAS; import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyArray; @@ -446,10 +448,10 @@ public void testCreateCrossClusterApiKey() throws IOException { containsString("authentication expected API key type of [rest], but API key [" + apiKeyId + "] has type [cross_cluster]") ); + // Check the API key attributes with raw document final Map document = client().execute(GetAction.INSTANCE, new GetRequest(SECURITY_MAIN_ALIAS, apiKeyId)) .actionGet() .getSource(); - assertThat(document.get("type"), equalTo("cross_cluster")); @SuppressWarnings("unchecked") @@ -463,22 +465,45 @@ public void testCreateCrossClusterApiKey() throws IOException { XContentType.JSON ); - assertThat( - actualRoleDescriptor, - equalTo( - new RoleDescriptor( - "cross_cluster", - new String[] { "cross_cluster_search" }, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices("logs") - .privileges("read", "read_cross_cluster", "view_index_metadata") - .build() }, - null - ) - ) + final RoleDescriptor expectedRoleDescriptor = new RoleDescriptor( + "cross_cluster", + new String[] { "cross_cluster_search" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("logs") + .privileges("read", "read_cross_cluster", "view_index_metadata") + .build() }, + null ); + assertThat(actualRoleDescriptor, equalTo(expectedRoleDescriptor)); assertThat((Map) document.get("limited_by_role_descriptors"), anEmptyMap()); + + // Check the API key attributes with Get API + final GetApiKeyResponse getApiKeyResponse = client().execute( + GetApiKeyAction.INSTANCE, + GetApiKeyRequest.builder().apiKeyId(apiKeyId).withLimitedBy(randomBoolean()).build() + ).actionGet(); + assertThat(getApiKeyResponse.getApiKeyInfos(), arrayWithSize(1)); + final ApiKey getApiKeyInfo = getApiKeyResponse.getApiKeyInfos()[0]; + assertThat(getApiKeyInfo.getType(), is(ApiKey.Type.CROSS_CLUSTER)); + assertThat(getApiKeyInfo.getRoleDescriptors(), contains(expectedRoleDescriptor)); + assertThat(getApiKeyInfo.getLimitedBy(), nullValue()); + + // Check the API key attributes with Query API + final QueryApiKeyRequest queryApiKeyRequest = new QueryApiKeyRequest( + QueryBuilders.boolQuery().filter(QueryBuilders.idsQuery().addIds(apiKeyId)), + null, + null, + null, + null, + randomBoolean() + ); + final QueryApiKeyResponse queryApiKeyResponse = client().execute(QueryApiKeyAction.INSTANCE, queryApiKeyRequest).actionGet(); + assertThat(queryApiKeyResponse.getItems(), arrayWithSize(1)); + final ApiKey queryApiKeyInfo = queryApiKeyResponse.getItems()[0].getApiKey(); + assertThat(queryApiKeyInfo.getType(), is(ApiKey.Type.CROSS_CLUSTER)); + assertThat(queryApiKeyInfo.getRoleDescriptors(), contains(expectedRoleDescriptor)); + assertThat(queryApiKeyInfo.getLimitedBy(), nullValue()); } private GrantApiKeyRequest buildGrantApiKeyRequest(String username, SecureString password, String runAsUsername) throws IOException { 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 1dfe2a6e1a313..36f1445d3075a 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 @@ -1857,13 +1857,14 @@ private ApiKey convertSearchHitToApiKeyInfo(SearchHit hit, boolean withLimitedBy RoleReference.ApiKeyRoleType.ASSIGNED ); - final List limitedByRoleDescriptors = withLimitedBy + final List limitedByRoleDescriptors = (withLimitedBy && apiKeyDoc.type != ApiKey.Type.CROSS_CLUSTER) ? parseRoleDescriptorsBytes(apiKeyId, apiKeyDoc.limitedByRoleDescriptorsBytes, RoleReference.ApiKeyRoleType.LIMITED_BY) : null; return new ApiKey( apiKeyDoc.name, apiKeyId, + apiKeyDoc.type, Instant.ofEpochMilli(apiKeyDoc.creationTime), apiKeyDoc.expirationTime != -1 ? Instant.ofEpochMilli(apiKeyDoc.expirationTime) : null, apiKeyDoc.invalidated, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java index 86c3e8a53bd49..345186009b73f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java @@ -26,6 +26,7 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.TcpTransport; import org.elasticsearch.xcontent.NamedXContentRegistry; import org.elasticsearch.xcontent.XContentType; import org.elasticsearch.xpack.core.security.action.apikey.ApiKey; @@ -93,17 +94,20 @@ public void sendResponse(RestResponse restResponse) { responseSetOnce.set(restResponse); } }; + final ApiKey.Type type = TcpTransport.isUntrustedRemoteClusterEnabled() ? randomFrom(ApiKey.Type.values()) : ApiKey.Type.REST; final Instant creation = Instant.now(); final Instant expiration = randomFrom(Arrays.asList(null, Instant.now().plus(10, ChronoUnit.DAYS))); - @SuppressWarnings("unchecked") final Map metadata = ApiKeyTests.randomMetadata(); final List roleDescriptors = randomUniquelyNamedRoleDescriptors(0, 3); - final List limitedByRoleDescriptors = withLimitedBy ? randomUniquelyNamedRoleDescriptors(1, 3) : null; + final List limitedByRoleDescriptors = withLimitedBy && type != ApiKey.Type.CROSS_CLUSTER + ? randomUniquelyNamedRoleDescriptors(1, 3) + : null; final GetApiKeyResponse getApiKeyResponseExpected = new GetApiKeyResponse( Collections.singletonList( new ApiKey( "api-key-name-1", "api-key-id-1", + type, creation, expiration, false, @@ -166,6 +170,7 @@ public void doE new ApiKey( "api-key-name-1", "api-key-id-1", + type, creation, expiration, false, @@ -209,11 +214,13 @@ public void sendResponse(RestResponse restResponse) { } }; + final ApiKey.Type type = TcpTransport.isUntrustedRemoteClusterEnabled() ? randomFrom(ApiKey.Type.values()) : ApiKey.Type.REST; final Instant creation = Instant.now(); final Instant expiration = randomFrom(Arrays.asList(null, Instant.now().plus(10, ChronoUnit.DAYS))); final ApiKey apiKey1 = new ApiKey( "api-key-name-1", "api-key-id-1", + type, creation, expiration, false, @@ -221,11 +228,12 @@ public void sendResponse(RestResponse restResponse) { "realm-1", ApiKeyTests.randomMetadata(), randomUniquelyNamedRoleDescriptors(0, 3), - withLimitedBy ? randomUniquelyNamedRoleDescriptors(1, 3) : null + withLimitedBy && type != ApiKey.Type.CROSS_CLUSTER ? randomUniquelyNamedRoleDescriptors(1, 3) : null ); final ApiKey apiKey2 = new ApiKey( "api-key-name-2", "api-key-id-2", + type, creation, expiration, false, @@ -233,7 +241,7 @@ public void sendResponse(RestResponse restResponse) { "realm-1", ApiKeyTests.randomMetadata(), randomUniquelyNamedRoleDescriptors(0, 3), - withLimitedBy ? randomUniquelyNamedRoleDescriptors(1, 3) : null + withLimitedBy && type != ApiKey.Type.CROSS_CLUSTER ? randomUniquelyNamedRoleDescriptors(1, 3) : null ); final GetApiKeyResponse getApiKeyResponseExpectedWhenOwnerFlagIsTrue = new GetApiKeyResponse(Collections.singletonList(apiKey1)); final GetApiKeyResponse getApiKeyResponseExpectedWhenOwnerFlagIsFalse = new GetApiKeyResponse(List.of(apiKey1, apiKey2));