Skip to content

Commit

Permalink
Handle role descriptor retrieval for internal users
Browse files Browse the repository at this point in the history
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: elastic#83627, elastic#84096
  • Loading branch information
ywangd committed Mar 17, 2022
1 parent 3d7b277 commit 5fe8eeb
Show file tree
Hide file tree
Showing 6 changed files with 186 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public final class UsernamesField {
public static final String SYSTEM_NAME = "_system";
public static final String SYSTEM_ROLE = "_system";
public static final String XPACK_SECURITY_NAME = "_xpack_security";
public static final String XPACK_SECURITY_ROLE = "superuser";
public static final String XPACK_SECURITY_ROLE = "_xpack_security";
public static final String XPACK_NAME = "_xpack";
public static final String XPACK_ROLE = "_xpack";
public static final String LOGSTASH_NAME = "logstash_system";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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.enrollment;

import org.apache.lucene.util.SetOnce;
import org.elasticsearch.client.internal.Client;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.EsExecutors;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.test.SecuritySingleNodeTestCase;
import org.elasticsearch.xpack.core.security.EnrollmentToken;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentRequest;
import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentResponse;
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.AuthenticateResponse;
import org.elasticsearch.xpack.core.ssl.SSLService;
import org.mockito.Mockito;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static org.elasticsearch.test.SecuritySettingsSource.addSSLSettingsForStore;
import static org.elasticsearch.xpack.core.XPackSettings.ENROLLMENT_ENABLED;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.spy;

public class EnrollmentSingleNodeTests extends SecuritySingleNodeTestCase {

@Override
protected Settings nodeSettings() {
Settings.Builder builder = Settings.builder().put(super.nodeSettings());
addSSLSettingsForStore(
builder,
"xpack.security.http.",
"/org/elasticsearch/xpack/security/transport/ssl/certs/simple/httpCa.p12",
"password",
false
);
builder.put("xpack.security.http.ssl.enabled", true).put(ENROLLMENT_ENABLED.getKey(), "true");
// Need at least 2 threads because enrollment token creator internally uses a client
builder.put(EsExecutors.NODE_PROCESSORS_SETTING.getKey(), 2);
return builder.build();
}

@Override
protected boolean addMockHttpTransport() {
return false; // enable http
}

@Override
protected boolean transportSSLEnabled() {
return true;
}

public void testKibanaEnrollmentTokenCreation() throws Exception {
final SSLService sslService = getInstanceFromNode(SSLService.class);

final InternalEnrollmentTokenGenerator internalEnrollmentTokenGenerator = spy(
new InternalEnrollmentTokenGenerator(
newEnvironment(Settings.builder().put(ENROLLMENT_ENABLED.getKey(), "true").build()),
sslService,
node().client()
)
);
// Mock the getHttpsCaFingerprint method because the real method requires createClassLoader permission
Mockito.doReturn("fingerprint").when(internalEnrollmentTokenGenerator).getHttpsCaFingerprint();

final SetOnce<EnrollmentToken> enrollmentTokenSetOnce = new SetOnce<>();
final CountDownLatch latch = new CountDownLatch(1);

// Create the kibana enrollment token and wait for the process to complete
internalEnrollmentTokenGenerator.createKibanaEnrollmentToken(enrollmentToken -> {
enrollmentTokenSetOnce.set(enrollmentToken);
latch.countDown();
}, List.of(TimeValue.timeValueMillis(500)).iterator());
latch.await(20, TimeUnit.SECONDS);

// The API key is created by the right user and should work
final Client apiKeyClient = client().filterWithHeader(
Map.of(
"Authorization",
"ApiKey " + Base64.getEncoder().encodeToString(enrollmentTokenSetOnce.get().getApiKey().getBytes(StandardCharsets.UTF_8))
)
);
final AuthenticateResponse authenticateResponse1 = apiKeyClient.execute(
AuthenticateAction.INSTANCE,
new AuthenticateRequest("_xpack_security")
).actionGet();
assertThat(authenticateResponse1.authentication().getUser().principal(), equalTo("_xpack_security"));

final KibanaEnrollmentResponse kibanaEnrollmentResponse = apiKeyClient.execute(
KibanaEnrollmentAction.INSTANCE,
new KibanaEnrollmentRequest()
).actionGet();

// The service token should work
final Client kibanaClient = client().filterWithHeader(
Map.of("Authorization", "Bearer " + kibanaEnrollmentResponse.getTokenValue())
);

final AuthenticateResponse authenticateResponse2 = kibanaClient.execute(
AuthenticateAction.INSTANCE,
new AuthenticateRequest("elastic/kibana")
).actionGet();
assertThat(authenticateResponse2.authentication().getUser().principal(), equalTo("elastic/kibana"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
Expand Down Expand Up @@ -347,21 +348,45 @@ private void buildThenMaybeCacheRole(
}

public void getRoleDescriptorsList(Subject subject, ActionListener<Collection<Set<RoleDescriptor>>> listener) {
final List<RoleReference> roleReferences = subject.getRoleReferenceIntersection(anonymousUser).getRoleReferences();
final GroupedActionListener<Set<RoleDescriptor>> groupedActionListener = new GroupedActionListener<>(
listener,
roleReferences.size()
tryGetRoleDescriptorForInternalUser(subject).ifPresentOrElse(
roleDescriptor -> listener.onResponse(List.of(Set.of(roleDescriptor))),
() -> {
final List<RoleReference> roleReferences = subject.getRoleReferenceIntersection(anonymousUser).getRoleReferences();
final GroupedActionListener<Set<RoleDescriptor>> groupedActionListener = new GroupedActionListener<>(
listener,
roleReferences.size()
);

roleReferences.forEach(roleReference -> {
roleReference.resolve(roleReferenceResolver, ActionListener.wrap(rolesRetrievalResult -> {
if (rolesRetrievalResult.isSuccess()) {
groupedActionListener.onResponse(rolesRetrievalResult.getRoleDescriptors());
} else {
groupedActionListener.onFailure(new ElasticsearchException("role retrieval had one or more failures"));
}
}, groupedActionListener::onFailure));
});
}
);
}

roleReferences.forEach(roleReference -> {
roleReference.resolve(roleReferenceResolver, ActionListener.wrap(rolesRetrievalResult -> {
if (rolesRetrievalResult.isSuccess()) {
groupedActionListener.onResponse(rolesRetrievalResult.getRoleDescriptors());
} else {
groupedActionListener.onFailure(new ElasticsearchException("role retrieval had one or more failures"));
}
}, groupedActionListener::onFailure));
});
private Optional<RoleDescriptor> tryGetRoleDescriptorForInternalUser(Subject subject) {
final User user = subject.getUser();
if (SystemUser.is(user)) {
throw new IllegalArgumentException(
"the user [" + user.principal() + "] is the system user and we should never try to get its role descriptors"
);
}
if (XPackUser.is(user)) {
return Optional.of(XPackUser.ROLE_DESCRIPTOR);
}
if (XPackSecurityUser.is(user)) {
return Optional.of(XPackSecurityUser.ROLE_DESCRIPTOR);
}
if (AsyncSearchUser.is(user)) {
return Optional.of(AsyncSearchUser.ROLE_DESCRIPTOR);
}
return Optional.empty();
}

public static void buildRoleFromDescriptors(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class InternalEnrollmentTokenGenerator extends BaseEnrollmentTokenGenerat
public InternalEnrollmentTokenGenerator(Environment environment, SSLService sslService, Client client) {
this.environment = environment;
this.sslService = sslService;
// enrollment tokens API keys will be owned by the "_xpack_security" system user ("superuser" role)
// enrollment tokens API keys will be owned by the "_xpack_security" system user ("_xpack_security" role)
this.client = new OriginSettingClient(client, SECURITY_ORIGIN);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ private void addNodeSSLSettings(Settings.Builder builder) {
}
}

private static void addSSLSettingsForStore(
public static void addSSLSettingsForStore(
Settings.Builder builder,
String prefix,
String resourcePathToStore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1893,6 +1893,33 @@ public void testXpackUserHasClusterPrivileges() {
}
}

public void testGetRoleDescriptorsListForInternalUsers() {
final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore(
SECURITY_ENABLED_SETTINGS,
null,
null,
null,
null,
null,
null,
mock(ServiceAccountService.class),
null,
null
);

final Subject subject = mock(Subject.class);
when(subject.getUser()).thenReturn(SystemUser.INSTANCE);
final PlainActionFuture<Collection<Set<RoleDescriptor>>> future1 = new PlainActionFuture<>();
compositeRolesStore.getRoleDescriptorsList(subject, future1);
final IllegalArgumentException e1 = expectThrows(IllegalArgumentException.class, future1::actionGet);
assertThat(e1.getMessage(), containsString("system user and we should never try to get its role descriptors"));

when(subject.getUser()).thenReturn(XPackSecurityUser.INSTANCE);
final PlainActionFuture<Collection<Set<RoleDescriptor>>> future2 = new PlainActionFuture<>();
compositeRolesStore.getRoleDescriptorsList(subject, future2);
assertThat(future2.actionGet(), equalTo(List.of(Set.of(XPackSecurityUser.ROLE_DESCRIPTOR))));
}

private void getRoleForRoleNames(CompositeRolesStore rolesStore, Collection<String> roleNames, ActionListener<Role> listener) {
final Subject subject = mock(Subject.class);
when(subject.getRoleReferenceIntersection(any())).thenReturn(
Expand Down

0 comments on commit 5fe8eeb

Please sign in to comment.