Skip to content

Commit

Permalink
[Fix opensearch-project#4280] Introduce new endpoint _plugins/_securi…
Browse files Browse the repository at this point in the history
…ty/api/certificates (opensearch-project#4299)

Signed-off-by: Andrey Pleskach <[email protected]>
  • Loading branch information
willyborankin authored May 21, 2024
1 parent b8f6ed3 commit 382bc5f
Show file tree
Hide file tree
Showing 15 changed files with 918 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,13 @@ protected void withUser(
}
}

protected String apiPathPrefix() {
return randomFrom(List.of(LEGACY_OPENDISTRO_PREFIX, PLUGINS_PREFIX));
}

protected String securityPath(String... path) {
final var fullPath = new StringJoiner("/");
fullPath.add(randomFrom(List.of(LEGACY_OPENDISTRO_PREFIX, PLUGINS_PREFIX)));
fullPath.add(apiPathPrefix());
if (path != null) {
for (final var p : path)
fullPath.add(p);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
package org.opensearch.security.api;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;

import com.carrotsearch.randomizedtesting.RandomizedContext;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Test;

import org.opensearch.security.dlic.rest.api.Endpoint;
import org.opensearch.security.dlic.rest.api.ssl.CertificateType;
import org.opensearch.test.framework.TestSecurityConfig;
import org.opensearch.test.framework.certificate.TestCertificates;
import org.opensearch.test.framework.cluster.LocalOpenSearchCluster;
import org.opensearch.test.framework.cluster.TestRestClient;

import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX;
import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED;

public class CertificatesRestApiIntegrationTest extends AbstractApiIntegrationTest {

final static String REST_API_ADMIN_SSL_INFO = "rest-api-admin-ssl-info";

final static String REGULAR_USER = "regular_user";

static {
clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true);
testSecurityConfig.roles(
new TestSecurityConfig.Role("simple_user_role").clusterPermissions("cluster:admin/security/certificates/info")
)
.rolesMapping(new TestSecurityConfig.RoleMapping("simple_user_role").users(REGULAR_USER, ADMIN_USER_NAME))
.user(new TestSecurityConfig.User(REGULAR_USER))
.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions())
.withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION));
}

@Override
protected String apiPathPrefix() {
return PLUGINS_PREFIX;
}

protected String sslCertsPath(String... path) {
final var fullPath = new StringJoiner("/");
fullPath.add(super.apiPath("certificates"));
if (path != null) {
for (final var p : path) {
fullPath.add(p);
}
}
return fullPath.toString();
}

@Test
public void forbiddenForRegularUser() throws Exception {
withUser(REGULAR_USER, client -> forbidden(() -> client.get(sslCertsPath())));
}

@Test
public void forbiddenForAdminUser() throws Exception {
withUser(ADMIN_USER_NAME, client -> forbidden(() -> client.get(sslCertsPath())));
}

@Test
public void availableForTlsAdmin() throws Exception {
withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifySSLCertsInfo);
}

@Test
public void availableForRestAdmin() throws Exception {
withUser(REST_ADMIN_USER, this::verifySSLCertsInfo);
withUser(REST_API_ADMIN_SSL_INFO, this::verifySSLCertsInfo);
}

private void verifySSLCertsInfo(final TestRestClient client) throws Exception {
assertSSLCertsInfo(
localCluster.nodes(),
Set.of(CertificateType.HTTP, CertificateType.TRANSPORT),
ok(() -> client.get(sslCertsPath()))
);
if (localCluster.nodes().size() > 1) {
final var randomNodes = randomNodes();
final var nodeIds = randomNodes.stream().map(n -> n.esNode().getNodeEnvironment().nodeId()).collect(Collectors.joining(","));
assertSSLCertsInfo(
randomNodes,
Set.of(CertificateType.HTTP, CertificateType.TRANSPORT),
ok(() -> client.get(sslCertsPath(nodeIds)))
);
}
final var randomCertType = randomFrom(List.of(CertificateType.HTTP, CertificateType.TRANSPORT));
assertSSLCertsInfo(
localCluster.nodes(),
Set.of(randomCertType),
ok(() -> client.get(String.format("%s?cert_type=%s", sslCertsPath(), randomCertType)))
);

}

private void assertSSLCertsInfo(
final List<LocalOpenSearchCluster.Node> expectedNode,
final Set<CertificateType> expectedCertTypes,
final TestRestClient.HttpResponse response
) {
final var body = response.bodyAsJsonNode();
final var prettyStringBody = body.toPrettyString();

final var _nodes = body.get("_nodes");
assertThat(prettyStringBody, _nodes.get("total").asInt(), is(expectedNode.size()));
assertThat(prettyStringBody, _nodes.get("successful").asInt(), is(expectedNode.size()));
assertThat(prettyStringBody, _nodes.get("failed").asInt(), is(0));
assertThat(prettyStringBody, body.get("cluster_name").asText(), is(localCluster.getClusterName()));

final var nodes = body.get("nodes");

for (final var n : expectedNode) {
final var esNode = n.esNode();
final var node = nodes.get(esNode.getNodeEnvironment().nodeId());
assertThat(prettyStringBody, node.get("name").asText(), is(n.getNodeName()));
assertThat(prettyStringBody, node.has("certificates"));
final var certificates = node.get("certificates");
if (expectedCertTypes.contains(CertificateType.HTTP)) {
final var httpCertificates = certificates.get(CertificateType.HTTP.value());
assertThat(prettyStringBody, httpCertificates.isArray());
assertThat(prettyStringBody, httpCertificates.size(), is(1));
verifyCertsJson(n.nodeNumber(), httpCertificates.get(0));
}
if (expectedCertTypes.contains(CertificateType.TRANSPORT)) {
final var transportCertificates = certificates.get(CertificateType.TRANSPORT.value());
assertThat(prettyStringBody, transportCertificates.isArray());
assertThat(prettyStringBody, transportCertificates.size(), is(1));
verifyCertsJson(n.nodeNumber(), transportCertificates.get(0));
}
}

}

private void verifyCertsJson(final int nodeNumber, final JsonNode jsonNode) {
assertThat(jsonNode.toPrettyString(), jsonNode.get("issuer_dn").asText(), is(TestCertificates.CA_SUBJECT));
assertThat(
jsonNode.toPrettyString(),
jsonNode.get("subject_dn").asText(),
is(String.format(TestCertificates.NODE_SUBJECT_PATTERN, nodeNumber))
);
assertThat(
jsonNode.toPrettyString(),
jsonNode.get("san").asText(),
containsString(String.format("node-%s.example.com", nodeNumber))
);
assertThat(jsonNode.toPrettyString(), jsonNode.has("not_before"));
assertThat(jsonNode.toPrettyString(), jsonNode.has("not_after"));
}

private List<LocalOpenSearchCluster.Node> randomNodes() {
final var nodes = localCluster.nodes();
int leaveElements = randomIntBetween(1, nodes.size() - 1);
return randomSubsetOf(leaveElements, nodes);
}

public <T> List<T> randomSubsetOf(int size, Collection<T> collection) {
if (size > collection.size()) {
throw new IllegalArgumentException(
"Can't pick " + size + " random objects from a collection of " + collection.size() + " objects"
);
}
List<T> tempList = new ArrayList<>(collection);
Collections.shuffle(tempList, RandomizedContext.current().getRandom());
return tempList.subList(0, size);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED;

@Deprecated
public class SslCertsRestApiIntegrationTest extends AbstractApiIntegrationTest {

final static String REST_API_ADMIN_SSL_INFO = "rest-api-admin-ssl-info";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
import java.util.SortedSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import com.google.common.collect.ImmutableList;
Expand Down Expand Up @@ -163,7 +164,9 @@ public void start() throws Exception {
this.initialClusterManagerHosts = toHostList(clusterManagerPorts);

started = true;
final var nodeCounter = new AtomicInteger(0);
CompletableFuture<Void> clusterManagerNodeFuture = startNodes(
nodeCounter,
clusterManager.getClusterManagerNodeSettings(),
clusterManagerNodeTransportPorts,
clusterManagerNodeHttpPorts
Expand All @@ -177,6 +180,7 @@ public void start() throws Exception {
SortedSet<Integer> nonClusterManagerNodeHttpPorts = TCP.allocate(clusterName, nonClusterManagerNodeCount, 5000 + 42 * 1000 + 210);

CompletableFuture<Void> nonClusterManagerNodeFuture = startNodes(
nodeCounter,
clusterManager.getNonClusterManagerNodeSettings(),
nonClusterManagerNodeTransportPorts,
nonClusterManagerNodeHttpPorts
Expand Down Expand Up @@ -292,6 +296,7 @@ private final Node findRunningNode(List<Node> nodes, List<Node>... moreNodes) {
}

private CompletableFuture<Void> startNodes(
AtomicInteger nodeCounter,
List<NodeSettings> nodeSettingList,
SortedSet<Integer> transportPorts,
SortedSet<Integer> httpPorts
Expand All @@ -300,8 +305,8 @@ private CompletableFuture<Void> startNodes(
Iterator<Integer> httpPortIterator = httpPorts.iterator();
List<CompletableFuture<StartStage>> futures = new ArrayList<>();

for (var i = 0; i < nodeSettingList.size(); i++) {
Node node = new Node(i, nodeSettingList.get(i), transportPortIterator.next(), httpPortIterator.next());
for (final var nodeSettings : nodeSettingList) {
Node node = new Node(nodeCounter.getAndIncrement(), nodeSettings, transportPortIterator.next(), httpPortIterator.next());
futures.add(node.start());
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
Expand Down Expand Up @@ -385,6 +390,10 @@ public class Node implements OpenSearchClientProvider {
private boolean portCollision = false;
private final int nodeNumber;

boolean hasAssignedType(NodeType type) {
return requireNonNull(type, "Node type is required.").equals(this.nodeType);
}

Node(int nodeNumber, NodeSettings nodeSettings, int transportPort, int httpPort) {
this.nodeNumber = nodeNumber;
this.nodeName = createNextNodeName(requireNonNull(nodeSettings, "Node settings are required."));
Expand All @@ -402,8 +411,8 @@ public class Node implements OpenSearchClientProvider {
nodes.add(this);
}

boolean hasAssignedType(NodeType type) {
return requireNonNull(type, "Node type is required.").equals(this.nodeType);
public int nodeNumber() {
return nodeNumber;
}

CompletableFuture<StartStage> start() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@
import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper;
import org.opensearch.security.dlic.rest.api.Endpoint;
import org.opensearch.security.dlic.rest.api.SecurityRestApiActions;
import org.opensearch.security.dlic.rest.api.ssl.CertificatesActionType;
import org.opensearch.security.dlic.rest.api.ssl.TransportCertificatesInfoNodesAction;
import org.opensearch.security.dlic.rest.validation.PasswordValidator;
import org.opensearch.security.filter.SecurityFilter;
import org.opensearch.security.filter.SecurityRestFilter;
Expand All @@ -174,6 +176,7 @@
import org.opensearch.security.securityconf.DynamicConfigFactory;
import org.opensearch.security.setting.OpensearchDynamicSetting;
import org.opensearch.security.setting.TransportPassiveAuthSetting;
import org.opensearch.security.ssl.ExternalSecurityKeyStore;
import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory;
import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin;
import org.opensearch.security.ssl.SslExceptionHandler;
Expand Down Expand Up @@ -670,6 +673,10 @@ public UnaryOperator<RestHandler> getRestHandlerWrapper(final ThreadContext thre
List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> actions = new ArrayList<>(1);
if (!disabled && !SSLConfig.isSslOnlyMode()) {
actions.add(new ActionHandler<>(ConfigUpdateAction.INSTANCE, TransportConfigUpdateAction.class));
// external storage does not support reload and does not provide SSL certs info
if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) {
actions.add(new ActionHandler<>(CertificatesActionType.INSTANCE, TransportCertificatesInfoNodesAction.class));
}
actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class));
}
return actions;
Expand Down Expand Up @@ -1193,6 +1200,9 @@ public Collection<Object> createComponents(
components.add(si);
components.add(dcf);
components.add(userService);
if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) {
components.add(sks);
}
final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false);
final var useClusterState = useClusterStateToInitSecurityConfig(settings);
if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterState) {
Expand Down
Loading

0 comments on commit 382bc5f

Please sign in to comment.