Skip to content

Commit 40dc0fb

Browse files
authored
Handle role descriptor retrieval for internal users (#85049)
Internal users have hard-coded role descriptors which are not registered with any role store. This means they cannot simply be retrieved by names. This PR adds logic to check for internal users and return their role descriptor accordingly. This change also makes it possible to finally correct the role name used by the _xpack_security user. A test for enrollment token is also added to ensure the change to _xpack_security user do not break the enrollment flow. Relates: #83627, #84096
1 parent 1e6b30e commit 40dc0fb

File tree

7 files changed

+198
-16
lines changed

7 files changed

+198
-16
lines changed

docs/changelog/85049.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 85049
2+
summary: Handle role descriptor retrieval for internal users
3+
area: Authorization
4+
type: bug
5+
issues: []

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/user/UsernamesField.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public final class UsernamesField {
1515
public static final String SYSTEM_NAME = "_system";
1616
public static final String SYSTEM_ROLE = "_system";
1717
public static final String XPACK_SECURITY_NAME = "_xpack_security";
18-
public static final String XPACK_SECURITY_ROLE = "superuser";
18+
public static final String XPACK_SECURITY_ROLE = "_xpack_security";
1919
public static final String XPACK_NAME = "_xpack";
2020
public static final String XPACK_ROLE = "_xpack";
2121
public static final String LOGSTASH_NAME = "logstash_system";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.security.enrollment;
9+
10+
import org.apache.lucene.util.SetOnce;
11+
import org.elasticsearch.client.internal.Client;
12+
import org.elasticsearch.common.settings.Settings;
13+
import org.elasticsearch.common.util.concurrent.EsExecutors;
14+
import org.elasticsearch.core.TimeValue;
15+
import org.elasticsearch.test.SecuritySingleNodeTestCase;
16+
import org.elasticsearch.xpack.core.security.EnrollmentToken;
17+
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction;
18+
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentRequest;
19+
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentResponse;
20+
import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction;
21+
import org.elasticsearch.xpack.core.security.action.user.AuthenticateRequest;
22+
import org.elasticsearch.xpack.core.security.action.user.AuthenticateResponse;
23+
import org.elasticsearch.xpack.core.ssl.SSLService;
24+
import org.junit.BeforeClass;
25+
import org.mockito.Mockito;
26+
27+
import java.nio.charset.StandardCharsets;
28+
import java.util.Base64;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.concurrent.CountDownLatch;
32+
import java.util.concurrent.TimeUnit;
33+
34+
import static org.elasticsearch.test.SecuritySettingsSource.addSSLSettingsForStore;
35+
import static org.elasticsearch.xpack.core.XPackSettings.ENROLLMENT_ENABLED;
36+
import static org.hamcrest.Matchers.equalTo;
37+
import static org.mockito.Mockito.spy;
38+
39+
public class EnrollmentSingleNodeTests extends SecuritySingleNodeTestCase {
40+
41+
@BeforeClass
42+
public static void muteInFips() {
43+
assumeFalse("Enrollment is not supported in FIPS 140-2 as we are using PKCS#12 keystores", inFipsJvm());
44+
}
45+
46+
@Override
47+
protected Settings nodeSettings() {
48+
Settings.Builder builder = Settings.builder().put(super.nodeSettings());
49+
addSSLSettingsForStore(
50+
builder,
51+
"xpack.security.http.",
52+
"/org/elasticsearch/xpack/security/transport/ssl/certs/simple/httpCa.p12",
53+
"password",
54+
false
55+
);
56+
builder.put("xpack.security.http.ssl.enabled", true).put(ENROLLMENT_ENABLED.getKey(), "true");
57+
// Need at least 2 threads because enrollment token creator internally uses a client
58+
builder.put(EsExecutors.NODE_PROCESSORS_SETTING.getKey(), 2);
59+
return builder.build();
60+
}
61+
62+
@Override
63+
protected boolean addMockHttpTransport() {
64+
return false; // enable http
65+
}
66+
67+
@Override
68+
protected boolean transportSSLEnabled() {
69+
return true;
70+
}
71+
72+
public void testKibanaEnrollmentTokenCreation() throws Exception {
73+
final SSLService sslService = getInstanceFromNode(SSLService.class);
74+
75+
final InternalEnrollmentTokenGenerator internalEnrollmentTokenGenerator = spy(
76+
new InternalEnrollmentTokenGenerator(
77+
newEnvironment(Settings.builder().put(ENROLLMENT_ENABLED.getKey(), "true").build()),
78+
sslService,
79+
node().client()
80+
)
81+
);
82+
// Mock the getHttpsCaFingerprint method because the real method requires createClassLoader permission
83+
Mockito.doReturn("fingerprint").when(internalEnrollmentTokenGenerator).getHttpsCaFingerprint();
84+
85+
final SetOnce<EnrollmentToken> enrollmentTokenSetOnce = new SetOnce<>();
86+
final CountDownLatch latch = new CountDownLatch(1);
87+
88+
// Create the kibana enrollment token and wait for the process to complete
89+
internalEnrollmentTokenGenerator.createKibanaEnrollmentToken(enrollmentToken -> {
90+
enrollmentTokenSetOnce.set(enrollmentToken);
91+
latch.countDown();
92+
}, List.of(TimeValue.timeValueMillis(500)).iterator());
93+
latch.await(20, TimeUnit.SECONDS);
94+
95+
// The API key is created by the right user and should work
96+
final Client apiKeyClient = client().filterWithHeader(
97+
Map.of(
98+
"Authorization",
99+
"ApiKey " + Base64.getEncoder().encodeToString(enrollmentTokenSetOnce.get().getApiKey().getBytes(StandardCharsets.UTF_8))
100+
)
101+
);
102+
final AuthenticateResponse authenticateResponse1 = apiKeyClient.execute(
103+
AuthenticateAction.INSTANCE,
104+
new AuthenticateRequest("_xpack_security")
105+
).actionGet();
106+
assertThat(authenticateResponse1.authentication().getUser().principal(), equalTo("_xpack_security"));
107+
108+
final KibanaEnrollmentResponse kibanaEnrollmentResponse = apiKeyClient.execute(
109+
KibanaEnrollmentAction.INSTANCE,
110+
new KibanaEnrollmentRequest()
111+
).actionGet();
112+
113+
// The service token should work
114+
final Client kibanaClient = client().filterWithHeader(
115+
Map.of("Authorization", "Bearer " + kibanaEnrollmentResponse.getTokenValue())
116+
);
117+
118+
final AuthenticateResponse authenticateResponse2 = kibanaClient.execute(
119+
AuthenticateAction.INSTANCE,
120+
new AuthenticateRequest("elastic/kibana")
121+
).actionGet();
122+
assertThat(authenticateResponse2.authentication().getUser().principal(), equalTo("elastic/kibana"));
123+
}
124+
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java

Lines changed: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import java.util.List;
6666
import java.util.Map;
6767
import java.util.Objects;
68+
import java.util.Optional;
6869
import java.util.Set;
6970
import java.util.concurrent.atomic.AtomicLong;
7071
import java.util.function.Consumer;
@@ -347,21 +348,45 @@ private void buildThenMaybeCacheRole(
347348
}
348349

349350
public void getRoleDescriptorsList(Subject subject, ActionListener<Collection<Set<RoleDescriptor>>> listener) {
350-
final List<RoleReference> roleReferences = subject.getRoleReferenceIntersection(anonymousUser).getRoleReferences();
351-
final GroupedActionListener<Set<RoleDescriptor>> groupedActionListener = new GroupedActionListener<>(
352-
listener,
353-
roleReferences.size()
351+
tryGetRoleDescriptorForInternalUser(subject).ifPresentOrElse(
352+
roleDescriptor -> listener.onResponse(List.of(Set.of(roleDescriptor))),
353+
() -> {
354+
final List<RoleReference> roleReferences = subject.getRoleReferenceIntersection(anonymousUser).getRoleReferences();
355+
final GroupedActionListener<Set<RoleDescriptor>> groupedActionListener = new GroupedActionListener<>(
356+
listener,
357+
roleReferences.size()
358+
);
359+
360+
roleReferences.forEach(roleReference -> {
361+
roleReference.resolve(roleReferenceResolver, ActionListener.wrap(rolesRetrievalResult -> {
362+
if (rolesRetrievalResult.isSuccess()) {
363+
groupedActionListener.onResponse(rolesRetrievalResult.getRoleDescriptors());
364+
} else {
365+
groupedActionListener.onFailure(new ElasticsearchException("role retrieval had one or more failures"));
366+
}
367+
}, groupedActionListener::onFailure));
368+
});
369+
}
354370
);
371+
}
355372

356-
roleReferences.forEach(roleReference -> {
357-
roleReference.resolve(roleReferenceResolver, ActionListener.wrap(rolesRetrievalResult -> {
358-
if (rolesRetrievalResult.isSuccess()) {
359-
groupedActionListener.onResponse(rolesRetrievalResult.getRoleDescriptors());
360-
} else {
361-
groupedActionListener.onFailure(new ElasticsearchException("role retrieval had one or more failures"));
362-
}
363-
}, groupedActionListener::onFailure));
364-
});
373+
private Optional<RoleDescriptor> tryGetRoleDescriptorForInternalUser(Subject subject) {
374+
final User user = subject.getUser();
375+
if (SystemUser.is(user)) {
376+
throw new IllegalArgumentException(
377+
"the user [" + user.principal() + "] is the system user and we should never try to get its role descriptors"
378+
);
379+
}
380+
if (XPackUser.is(user)) {
381+
return Optional.of(XPackUser.ROLE_DESCRIPTOR);
382+
}
383+
if (XPackSecurityUser.is(user)) {
384+
return Optional.of(XPackSecurityUser.ROLE_DESCRIPTOR);
385+
}
386+
if (AsyncSearchUser.is(user)) {
387+
return Optional.of(AsyncSearchUser.ROLE_DESCRIPTOR);
388+
}
389+
return Optional.empty();
365390
}
366391

367392
public static void buildRoleFromDescriptors(

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/InternalEnrollmentTokenGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public class InternalEnrollmentTokenGenerator extends BaseEnrollmentTokenGenerat
5151
public InternalEnrollmentTokenGenerator(Environment environment, SSLService sslService, Client client) {
5252
this.environment = environment;
5353
this.sslService = sslService;
54-
// enrollment tokens API keys will be owned by the "_xpack_security" system user ("superuser" role)
54+
// enrollment tokens API keys will be owned by the "_xpack_security" system user ("_xpack_security" role)
5555
this.client = new OriginSettingClient(client, SECURITY_ORIGIN);
5656
}
5757

x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ private void addNodeSSLSettings(Settings.Builder builder) {
257257
}
258258
}
259259

260-
private static void addSSLSettingsForStore(
260+
public static void addSSLSettingsForStore(
261261
Settings.Builder builder,
262262
String prefix,
263263
String resourcePathToStore,

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,6 +1893,34 @@ public void testXpackUserHasClusterPrivileges() {
18931893
}
18941894
}
18951895

1896+
public void testGetRoleDescriptorsListForInternalUsers() {
1897+
final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore(
1898+
SECURITY_ENABLED_SETTINGS,
1899+
null,
1900+
null,
1901+
null,
1902+
null,
1903+
null,
1904+
null,
1905+
mock(ServiceAccountService.class),
1906+
null,
1907+
null
1908+
);
1909+
1910+
final Subject subject = mock(Subject.class);
1911+
when(subject.getUser()).thenReturn(SystemUser.INSTANCE);
1912+
final IllegalArgumentException e1 = expectThrows(
1913+
IllegalArgumentException.class,
1914+
() -> compositeRolesStore.getRoleDescriptorsList(subject, new PlainActionFuture<>())
1915+
);
1916+
assertThat(e1.getMessage(), containsString("system user and we should never try to get its role descriptors"));
1917+
1918+
when(subject.getUser()).thenReturn(XPackSecurityUser.INSTANCE);
1919+
final PlainActionFuture<Collection<Set<RoleDescriptor>>> future2 = new PlainActionFuture<>();
1920+
compositeRolesStore.getRoleDescriptorsList(subject, future2);
1921+
assertThat(future2.actionGet(), equalTo(List.of(Set.of(XPackSecurityUser.ROLE_DESCRIPTOR))));
1922+
}
1923+
18961924
private void getRoleForRoleNames(CompositeRolesStore rolesStore, Collection<String> roleNames, ActionListener<Role> listener) {
18971925
final Subject subject = mock(Subject.class);
18981926
when(subject.getRoleReferenceIntersection(any())).thenReturn(

0 commit comments

Comments
 (0)