Skip to content

Commit 18bd1cd

Browse files
Allow API key authentication subject on the fulfilling side for RCS 2.0 (#93808)
This PR is follow up on #93414, which allows using API keys to authenticate cross cluster requests in the new remote cluster security model. The main change is around removing restrictions and allowing API key authentication subjects on the fulfilling (server) side.
1 parent a4f66d0 commit 18bd1cd

File tree

13 files changed

+891
-87
lines changed

13 files changed

+891
-87
lines changed

build-tools-internal/src/main/groovy/elasticsearch.bwc-test.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ tasks.register("bwcTest") {
3232
}
3333

3434
plugins.withType(ElasticsearchTestBasePlugin) {
35-
tasks.withType(Test).configureEach {
35+
tasks.withType(Test).matching { it.name ==~ /v[0-9\.]+#.*/ }.configureEach {
3636
onlyIf { project.bwc_tests_enabled }
3737
nonInputProperties.systemProperty 'tests.bwc', 'true'
3838
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/Subject.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,9 +275,10 @@ private RoleReferenceIntersection buildRoleReferencesForCrossClusterAccess() {
275275
// restrict access of the overall intersection accordingly
276276
roleReferences.add(new RoleReference.CrossClusterAccessRoleReference(RoleDescriptorsBytes.EMPTY));
277277
} else {
278-
// TODO handle this once we support API keys as querying subjects
279-
assert crossClusterAccessRoleDescriptorsBytes.size() == 1
280-
: "only a singleton list of cross cluster access role descriptors bytes is supported";
278+
// This is just a sanity check, since we should never have more than 2 role descriptors.
279+
// We can have max two role descriptors in case when API key is used for cross cluster access.
280+
assert crossClusterAccessRoleDescriptorsBytes.size() <= 2
281+
: "not expected to have list of cross cluster access role descriptors bytes which have more than 2 elements";
281282
for (RoleDescriptorsBytes roleDescriptorsBytes : crossClusterAccessRoleDescriptorsBytes) {
282283
roleReferences.add(new RoleReference.CrossClusterAccessRoleReference(roleDescriptorsBytes));
283284
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTestHelper.java

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
import java.io.IOException;
4141
import java.io.UncheckedIOException;
42+
import java.util.ArrayList;
4243
import java.util.Arrays;
4344
import java.util.EnumSet;
4445
import java.util.HashMap;
@@ -241,42 +242,58 @@ public static CrossClusterAccessSubjectInfo randomCrossClusterAccessSubjectInfo(
241242
RoleDescriptorsIntersection roleDescriptorsIntersection
242243
) {
243244
try {
244-
// TODO add apikey() once we have querying-cluster-side API key support
245-
final Authentication authentication = ESTestCase.randomFrom(
246-
AuthenticationTestHelper.builder().realm(),
247-
AuthenticationTestHelper.builder().internal(SystemUser.INSTANCE)
248-
).build();
245+
final Authentication authentication = randomCrossClusterAccessSupportedAuthenticationSubject();
249246
return new CrossClusterAccessSubjectInfo(authentication, roleDescriptorsIntersection);
250247
} catch (IOException e) {
251248
throw new UncheckedIOException(e);
252249
}
253250
}
254251

252+
private static Authentication randomCrossClusterAccessSupportedAuthenticationSubject() {
253+
return ESTestCase.randomFrom(
254+
AuthenticationTestHelper.builder().realm(),
255+
AuthenticationTestHelper.builder().internal(SystemUser.INSTANCE),
256+
AuthenticationTestHelper.builder().apiKey()
257+
).build();
258+
}
259+
255260
public static CrossClusterAccessSubjectInfo randomCrossClusterAccessSubjectInfo() {
256-
return randomCrossClusterAccessSubjectInfo(
257-
new RoleDescriptorsIntersection(
258-
List.of(
259-
// TODO randomize to add a second set once we have querying-cluster-side API key support
260-
Set.of(
261-
new RoleDescriptor(
262-
"_remote_user",
263-
null,
264-
new RoleDescriptor.IndicesPrivileges[] {
265-
RoleDescriptor.IndicesPrivileges.builder()
266-
.indices("index1")
267-
.privileges("read", "read_cross_cluster")
268-
.build() },
269-
null,
270-
null,
271-
null,
272-
null,
273-
null,
274-
null
275-
)
261+
final Authentication authentication = randomCrossClusterAccessSupportedAuthenticationSubject();
262+
return randomCrossClusterAccessSubjectInfo(authentication);
263+
}
264+
265+
public static CrossClusterAccessSubjectInfo randomCrossClusterAccessSubjectInfo(final Authentication authentication) {
266+
final int numberOfRoleDescriptors;
267+
if (authentication.isApiKey()) {
268+
// In case of API keys, we can have either 1 (only owner's - aka limited-by) or 2 role descriptors.
269+
numberOfRoleDescriptors = ESTestCase.randomIntBetween(1, 2);
270+
} else {
271+
numberOfRoleDescriptors = 1;
272+
}
273+
final List<Set<RoleDescriptor>> roleDescriptors = new ArrayList<>(numberOfRoleDescriptors);
274+
for (int i = 0; i < numberOfRoleDescriptors; i++) {
275+
roleDescriptors.add(
276+
Set.of(
277+
new RoleDescriptor(
278+
"_remote_user",
279+
null,
280+
new RoleDescriptor.IndicesPrivileges[] {
281+
RoleDescriptor.IndicesPrivileges.builder().indices("index1").privileges("read", "read_cross_cluster").build() },
282+
null,
283+
null,
284+
null,
285+
null,
286+
null,
287+
null
276288
)
277289
)
278-
)
279-
);
290+
);
291+
}
292+
try {
293+
return new CrossClusterAccessSubjectInfo(authentication, new RoleDescriptorsIntersection(roleDescriptors));
294+
} catch (IOException e) {
295+
throw new UncheckedIOException(e);
296+
}
280297
}
281298

282299
public static class AuthenticationTestBuilder {

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authc/AuthenticationTests.java

Lines changed: 110 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import java.util.stream.Collectors;
4141

4242
import static java.util.Map.entry;
43+
import static org.elasticsearch.xpack.core.security.authc.AuthenticationTestHelper.randomCrossClusterAccessSubjectInfo;
4344
import static org.elasticsearch.xpack.core.security.authc.CrossClusterAccessSubjectInfoTests.randomRoleDescriptorsIntersection;
4445
import static org.hamcrest.Matchers.containsString;
4546
import static org.hamcrest.Matchers.equalTo;
@@ -139,9 +140,8 @@ public void testCanAccessResourcesOf() {
139140

140141
public void testCrossClusterAccessCanAccessResourceOf() throws IOException {
141142
final String apiKeyId1 = randomAlphaOfLengthBetween(10, 20);
142-
final CrossClusterAccessSubjectInfo crossClusterAccessSubjectInfo1 = randomValueOtherThanMany(
143-
ra -> User.isInternal(ra.getAuthentication().getEffectiveSubject().getUser()),
144-
AuthenticationTestHelper::randomCrossClusterAccessSubjectInfo
143+
final CrossClusterAccessSubjectInfo crossClusterAccessSubjectInfo1 = randomCrossClusterAccessSubjectInfo(
144+
AuthenticationTestHelper.builder().realm().build()
145145
);
146146
final Authentication authentication = AuthenticationTestHelper.builder()
147147
.crossClusterAccess(apiKeyId1, crossClusterAccessSubjectInfo1)
@@ -260,7 +260,110 @@ public void testCrossClusterAccessCanAccessResourceOf() throws IOException {
260260
assert false : "Case number out of range";
261261
}
262262

263-
// TODO: Add more tests for API keys when they work as QC subject
263+
}
264+
265+
private static Authentication randomCrossClusterAccessAuthentication(
266+
String crossClusterApiKeyId,
267+
User user,
268+
Authentication authentication
269+
) {
270+
return AuthenticationTestHelper.builder()
271+
.crossClusterAccess(crossClusterApiKeyId, randomCrossClusterAccessSubjectInfo(authentication))
272+
.user(user)
273+
.build(false);
274+
}
275+
276+
public void testCrossClusterAccessCanAccessResourceOfWithApiKey() {
277+
final User user1 = randomUser();
278+
final RealmRef realm1 = randomRealmRef(false);
279+
280+
// Different username is different no matter which realm it is from
281+
final User user2 = randomValueOtherThanMany(u -> u.principal().equals(user1.principal()), AuthenticationTests::randomUser);
282+
// user 2 can be from either the same realm or a different realm
283+
final RealmRef realm2 = randomFrom(realm1, randomRealmRef(false));
284+
285+
final String apiKeyId1 = randomAlphaOfLengthBetween(10, 20);
286+
// User is irrelevant
287+
final User crossClusterUser1 = randomFrom(user1, user2, randomUser());
288+
final String crossClusterApiKeyId1 = randomAlphaOfLengthBetween(10, 20);
289+
290+
// Same cross cluster access authentication with the same API key is allowed.
291+
assertCanAccessResources(
292+
randomCrossClusterAccessAuthentication(crossClusterApiKeyId1, crossClusterUser1, randomApiKeyAuthentication(user1, apiKeyId1)),
293+
randomCrossClusterAccessAuthentication(crossClusterApiKeyId1, crossClusterUser1, randomApiKeyAuthentication(user1, apiKeyId1))
294+
);
295+
296+
// Cluster access authentication with different API credentials keys is not allowed.
297+
final String crossClusterApiKeyId2 = randomValueOtherThan(crossClusterApiKeyId1, () -> randomAlphaOfLengthBetween(10, 20));
298+
assertCannotAccessResources(
299+
randomCrossClusterAccessAuthentication(crossClusterApiKeyId1, crossClusterUser1, randomApiKeyAuthentication(user1, apiKeyId1)),
300+
randomCrossClusterAccessAuthentication(crossClusterApiKeyId2, crossClusterUser1, randomApiKeyAuthentication(user1, apiKeyId1))
301+
);
302+
303+
// Cross cluster access with a user and its API key is not the same owner, hence not allowed.
304+
assertCannotAccessResources(
305+
randomCrossClusterAccessAuthentication(crossClusterApiKeyId1, crossClusterUser1, randomAuthentication(user1, realm1)),
306+
randomCrossClusterAccessAuthentication(crossClusterApiKeyId1, crossClusterUser1, randomApiKeyAuthentication(user1, apiKeyId1))
307+
);
308+
309+
// Cross cluster access with two different API keys (regardless if the same user is owner) is not allowed.
310+
final String apiKeyId2 = randomValueOtherThanMany(id -> id.equals(apiKeyId1), () -> randomAlphaOfLengthBetween(10, 20));
311+
assertCannotAccessResources(
312+
randomCrossClusterAccessAuthentication(
313+
crossClusterApiKeyId1,
314+
crossClusterUser1,
315+
randomApiKeyAuthentication(randomFrom(user1, user2), apiKeyId1)
316+
),
317+
randomCrossClusterAccessAuthentication(
318+
crossClusterApiKeyId1,
319+
crossClusterUser1,
320+
randomApiKeyAuthentication(randomFrom(user1, user2), apiKeyId2)
321+
)
322+
);
323+
324+
// Cross cluster access using same API key but run-as different users is not allowed.
325+
final User user3 = randomValueOtherThanMany(
326+
u -> u.principal().equals(user1.principal()) || u.principal().equals(user2.principal()),
327+
AuthenticationTests::randomUser
328+
);
329+
assertCannotAccessResources(
330+
randomCrossClusterAccessAuthentication(
331+
crossClusterApiKeyId1,
332+
crossClusterUser1,
333+
randomApiKeyAuthentication(user1, apiKeyId1).runAs(user2, realm2)
334+
),
335+
randomCrossClusterAccessAuthentication(
336+
crossClusterApiKeyId1,
337+
crossClusterUser1,
338+
randomApiKeyAuthentication(user1, apiKeyId1).runAs(user3, realm2)
339+
)
340+
);
341+
342+
// Cross cluster access using same or different API key which run-as the same user (user3) is allowed.
343+
assertCanAccessResources(
344+
randomCrossClusterAccessAuthentication(
345+
crossClusterApiKeyId1,
346+
crossClusterUser1,
347+
randomApiKeyAuthentication(user1, apiKeyId1).runAs(user3, realm2)
348+
),
349+
randomCrossClusterAccessAuthentication(
350+
crossClusterApiKeyId1,
351+
crossClusterUser1,
352+
randomApiKeyAuthentication(user1, apiKeyId1).runAs(user3, realm2)
353+
)
354+
);
355+
assertCanAccessResources(
356+
randomCrossClusterAccessAuthentication(
357+
crossClusterApiKeyId1,
358+
crossClusterUser1,
359+
randomApiKeyAuthentication(user1, apiKeyId1).runAs(user3, realm2)
360+
),
361+
randomCrossClusterAccessAuthentication(
362+
crossClusterApiKeyId1,
363+
crossClusterUser1,
364+
randomApiKeyAuthentication(user2, apiKeyId2).runAs(user3, realm2)
365+
)
366+
);
264367
}
265368

266369
public void testTokenAccessResourceOf() {
@@ -569,7 +672,7 @@ public void testDomainSerialize() throws Exception {
569672

570673
public void testCrossClusterAccessAuthentication() throws IOException {
571674
final String crossClusterAccessApiKeyId = ESTestCase.randomAlphaOfLength(20);
572-
final CrossClusterAccessSubjectInfo crossClusterAccessSubjectInfo = AuthenticationTestHelper.randomCrossClusterAccessSubjectInfo();
675+
final CrossClusterAccessSubjectInfo crossClusterAccessSubjectInfo = randomCrossClusterAccessSubjectInfo();
573676
final Authentication authentication = AuthenticationTestHelper.builder()
574677
.crossClusterAccess(crossClusterAccessApiKeyId, crossClusterAccessSubjectInfo)
575678
.build(false);
@@ -676,7 +779,7 @@ public void testToXContentWithApiKey() throws IOException {
676779
public void testToXContentWithCrossClusterAccess() throws IOException {
677780
final String apiKeyId = randomAlphaOfLength(20);
678781
final Authentication authentication = AuthenticationTestHelper.builder()
679-
.crossClusterAccess(apiKeyId, AuthenticationTestHelper.randomCrossClusterAccessSubjectInfo())
782+
.crossClusterAccess(apiKeyId, randomCrossClusterAccessSubjectInfo())
680783
.build(false);
681784
final String apiKeyName = (String) authentication.getAuthenticatingSubject()
682785
.getMetadata()
@@ -867,7 +970,7 @@ public void testToCrossClusterAccess() {
867970
final User creator = randomUser();
868971
final String apiKeyId = randomAlphaOfLength(42);
869972
final Authentication apiKeyAuthentication = AuthenticationTestHelper.builder().apiKey(apiKeyId).user(creator).build(false);
870-
final CrossClusterAccessSubjectInfo crossClusterAccessSubjectInfo = AuthenticationTestHelper.randomCrossClusterAccessSubjectInfo();
973+
final CrossClusterAccessSubjectInfo crossClusterAccessSubjectInfo = randomCrossClusterAccessSubjectInfo();
871974

872975
final Authentication actualAuthentication = apiKeyAuthentication.toCrossClusterAccess(crossClusterAccessSubjectInfo);
873976

0 commit comments

Comments
 (0)