Skip to content

Commit

Permalink
Fix issue #4280
Browse files Browse the repository at this point in the history
Signed-off-by: Andrey Pleskach <[email protected]>

ooo

Signed-off-by: Andrey Pleskach <[email protected]>
  • Loading branch information
willyborankin committed Apr 23, 2024
1 parent 0d7af4d commit b5de9cb
Show file tree
Hide file tree
Showing 15 changed files with 675 additions and 324 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* 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.google.common.collect.ImmutableList;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Test;

import org.opensearch.security.dlic.rest.api.Endpoint;
import org.opensearch.security.dlic.rest.api.ssl.SslCertsInfoNodesRequest;
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.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED;

public class SslCertsRestApiIntegrationTest extends AbstractApiIntegrationTest {

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

static {
clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true);
testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions())
.withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION));
}

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

@Test
public void forbiddenForRegularUser() throws Exception {
withUser(NEW_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(
nodesWithOrder(),
Set.of(SslCertsInfoNodesRequest.HTTP_CERT_TYPE, SslCertsInfoNodesRequest.TRANSPORT_CERT_TYPE),
ok(() -> client.get(sslCertsPath()))
);
if (localCluster.nodes().size() > 1) {
final var randomNodes = randomNodes();
final var nodeIds = randomNodes.stream()
.map(Pair::getRight)
.map(n -> n.esNode().getNodeEnvironment().nodeId())
.collect(Collectors.joining(","));
assertSSLCertsInfo(
randomNodes,
Set.of(SslCertsInfoNodesRequest.HTTP_CERT_TYPE, SslCertsInfoNodesRequest.TRANSPORT_CERT_TYPE),
ok(() -> client.get(sslCertsPath(nodeIds)))
);
}
final var randomCertType = randomFrom(
List.of(SslCertsInfoNodesRequest.HTTP_CERT_TYPE, SslCertsInfoNodesRequest.TRANSPORT_CERT_TYPE)
);
assertSSLCertsInfo(
nodesWithOrder(),
Set.of(randomCertType),
ok(() -> client.get(String.format("%s?cert_type=%s", sslCertsPath(), randomCertType)))
);

}

private void assertSSLCertsInfo(
final List<Pair<Integer, LocalOpenSearchCluster.Node>> expectedNode,
final Set<String> 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 nodeWithOrder : expectedNode) {
final var esNode = nodeWithOrder.getRight().esNode();
final var node = nodes.get(esNode.getNodeEnvironment().nodeId());
assertThat(prettyStringBody, node.get("name").asText(), is(nodeWithOrder.getRight().getNodeName()));
if (expectedCertTypes.contains(SslCertsInfoNodesRequest.HTTP_CERT_TYPE)) {
assertThat(prettyStringBody, node.has("http_certificates"));
assertThat(prettyStringBody, node.get("http_certificates").isArray());
assertThat(prettyStringBody, node.get("http_certificates").size(), is(1));
verifyCertsJson(nodeWithOrder.getLeft(), node.get("http_certificates").get(0));
}
if (expectedCertTypes.contains(SslCertsInfoNodesRequest.TRANSPORT_CERT_TYPE)) {
assertThat(prettyStringBody, node.has("transport_certificates"));
assertThat(prettyStringBody, node.get("transport_certificates").isArray());
assertThat(prettyStringBody, node.get("transport_certificates").size(), is(1));
verifyCertsJson(nodeWithOrder.getLeft(), node.get("transport_certificates").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<Pair<Integer, LocalOpenSearchCluster.Node>> randomNodes() {
final var nodesWithOrder = nodesWithOrder();
int leaveElements = randomIntBetween(1, nodesWithOrder.size() - 1);
return randomSubsetOf(leaveElements, nodesWithOrder);
}

private List<Pair<Integer, LocalOpenSearchCluster.Node>> nodesWithOrder() {
final var list = ImmutableList.<Pair<Integer, LocalOpenSearchCluster.Node>>builder();
for (int i = 0; i < localCluster.nodes().size(); i++)
list.add(Pair.of(i, localCluster.nodes().get(i)));
return list.build();
}

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 @@ -55,9 +55,13 @@ public class TestCertificates {

private static final Logger log = LogManager.getLogger(TestCertificates.class);

public static final Integer MAX_NUMBER_OF_NODE_CERTIFICATES = 3;
public static final Integer DEFAULT_NUMBER_OF_NODE_CERTIFICATES = 3;

public static final String CA_SUBJECT = "DC=com,DC=example,O=Example Com Inc.,OU=Example Com Inc. Root CA,CN=Example Com Inc. Root CA";

public static final String LDAP_SUBJECT = "DC=de,L=test,O=node,OU=node,CN=ldap.example.com";
public static final String NODE_SUBJECT_PATTERN = "DC=de,L=test,O=node,OU=node,CN=node-%d.example.com";

private static final String CA_SUBJECT = "DC=com,DC=example,O=Example Com Inc.,OU=Example Com Inc. Root CA,CN=Example Com Inc. Root CA";
private static final String ADMIN_DN = "CN=kirk,OU=client,O=client,L=test,C=de";
private static final int CERTIFICATE_VALIDITY_DAYS = 365;
private static final String CERTIFICATE_FILE_EXT = ".cert";
Expand All @@ -66,13 +70,18 @@ public class TestCertificates {
private final CertificateData adminCertificate;
private final List<CertificateData> nodeCertificates;

private final int numberOfNodes;

private final CertificateData ldapCertificate;

public TestCertificates() {
this(DEFAULT_NUMBER_OF_NODE_CERTIFICATES);
}

public TestCertificates(final int numberOfNodes) {
this.caCertificate = createCaCertificate();
this.nodeCertificates = IntStream.range(0, MAX_NUMBER_OF_NODE_CERTIFICATES)
.mapToObj(this::createNodeCertificate)
.collect(Collectors.toList());
this.numberOfNodes = numberOfNodes;
this.nodeCertificates = IntStream.range(0, this.numberOfNodes).mapToObj(this::createNodeCertificate).collect(Collectors.toList());
this.ldapCertificate = createLdapCertificate();
this.adminCertificate = createAdminCertificate(ADMIN_DN);
log.info("Test certificates successfully generated");
Expand Down Expand Up @@ -109,7 +118,7 @@ public CertificateData getRootCertificateData() {

/**
* Certificate for Open Search node. The certificate is derived from root certificate, returned by method {@link #getRootCertificate()}
* @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES}
* @param node is a node index. It has to be less than {@link #DEFAULT_NUMBER_OF_NODE_CERTIFICATES}
* @return file which contains certificate in PEM format, defined by <a href="https://www.rfc-editor.org/rfc/rfc1421.txt">RFC 1421</a>
*/
public File getNodeCertificate(int node) {
Expand All @@ -123,18 +132,18 @@ public CertificateData getNodeCertificateData(int node) {
}

private void isCorrectNodeNumber(int node) {
if (node >= MAX_NUMBER_OF_NODE_CERTIFICATES) {
if (node >= numberOfNodes) {
String message = String.format(
"Cannot get certificate for node %d, number of created certificates for nodes is %d",
node,
MAX_NUMBER_OF_NODE_CERTIFICATES
numberOfNodes
);
throw new RuntimeException(message);
}
}

private CertificateData createNodeCertificate(Integer node) {
String subject = String.format("DC=de,L=test,O=node,OU=node,CN=node-%d.example.com", node);
final var subject = String.format(NODE_SUBJECT_PATTERN, node);
String domain = String.format("node-%d.example.com", node);
CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS)
.withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH)
Expand All @@ -150,8 +159,7 @@ public CertificateData issueUserCertificate(String organizationUnit, String user
}

private CertificateData createLdapCertificate() {
String subject = "DC=de,L=test,O=node,OU=node,CN=ldap.example.com";
CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS)
CertificateMetadata metadata = CertificateMetadata.basicMetadata(LDAP_SUBJECT, CERTIFICATE_VALIDITY_DAYS)
.withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH)
.withSubjectAlternativeName(null, List.of("localhost"), "127.0.0.1");
return CertificatesIssuerFactory.rsaBaseCertificateIssuer().issueSignedCertificate(metadata, caCertificate);
Expand All @@ -164,7 +172,7 @@ public CertificateData getLdapCertificateData() {
/**
* It returns private key associated with node certificate returned by method {@link #getNodeCertificate(int)}
*
* @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES}
* @param node is a node index. It has to be less than {@link #DEFAULT_NUMBER_OF_NODE_CERTIFICATES}
* @param privateKeyPassword is a password used to encode private key, can be <code>null</code> to retrieve unencrypted key.
* @return file which contains private key encoded in PEM format, defined
* by <a href="https://www.rfc-editor.org/rfc/rfc1421.txt">RFC 1421</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ public void before() {
}

for (Map.Entry<String, LocalCluster> entry : remotes.entrySet()) {
@SuppressWarnings("resource")
InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.clusterManagerNode().getTransportAddress();
String key = "cluster.remote." + entry.getKey() + ".seeds";
String value = transportAddress.getHostString() + ":" + transportAddress.getPort();
Expand Down Expand Up @@ -509,7 +508,7 @@ public Builder defaultConfigurationInitDirectory(String defaultConfigurationInit
public LocalCluster build() {
try {
if (testCertificates == null) {
testCertificates = new TestCertificates();
testCertificates = new TestCertificates(clusterManager.getNodes() + 1);
}
clusterName += "_" + num.incrementAndGet();
Settings settings = nodeOverrideSettingsBuilder.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ public class LocalOpenSearchCluster {

private File snapshotDir;

private int nodeCounter = 0;

public LocalOpenSearchCluster(
String clusterName,
ClusterManager clusterManager,
Expand Down Expand Up @@ -163,7 +165,6 @@ public void start() throws Exception {
this.initialClusterManagerHosts = toHostList(clusterManagerPorts);

started = true;

CompletableFuture<Void> clusterManagerNodeFuture = startNodes(
clusterManager.getClusterManagerNodeSettings(),
clusterManagerNodeTransportPorts,
Expand Down Expand Up @@ -195,7 +196,6 @@ public void start() throws Exception {
log.info("Startup finished. Waiting for GREEN");

waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), nodes.size());

log.info("Started: {}", this);

}
Expand Down Expand Up @@ -303,10 +303,10 @@ private CompletableFuture<Void> startNodes(
List<CompletableFuture<StartStage>> futures = new ArrayList<>();

for (NodeSettings nodeSettings : nodeSettingList) {
Node node = new Node(nodeSettings, transportPortIterator.next(), httpPortIterator.next());
Node node = new Node(nodeCounter, nodeSettings, transportPortIterator.next(), httpPortIterator.next());
futures.add(node.start());
nodeCounter += 1;
}

return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}

Expand Down Expand Up @@ -386,8 +386,10 @@ public class Node implements OpenSearchClientProvider {
private PluginAwareNode node;
private boolean running = false;
private boolean portCollision = false;
private final int nodeNumber;

Node(NodeSettings nodeSettings, int transportPort, int httpPort) {
Node(int nodeNumber, NodeSettings nodeSettings, int transportPort, int httpPort) {
this.nodeNumber = nodeNumber;
this.nodeName = createNextNodeName(requireNonNull(nodeSettings, "Node settings are required."));
this.nodeSettings = nodeSettings;
this.nodeHomeDir = new File(clusterHomeDir, nodeName);
Expand Down Expand Up @@ -517,7 +519,7 @@ private Settings getOpenSearchSettings() {

if (nodeSettingsSupplier != null) {
// TODO node number
return Settings.builder().put(settings).put(nodeSettingsSupplier.get(0)).build();
return Settings.builder().put(settings).put(nodeSettingsSupplier.get(nodeNumber)).build();
}
return settings;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,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.SslCertsInfoActionType;
import org.opensearch.security.dlic.rest.api.ssl.TransportSslCertificatesInfoNodesAction;
import org.opensearch.security.dlic.rest.validation.PasswordValidator;
import org.opensearch.security.filter.SecurityFilter;
import org.opensearch.security.filter.SecurityRestFilter;
Expand All @@ -173,6 +175,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 @@ -656,6 +659,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<>(SslCertsInfoActionType.INSTANCE, TransportSslCertificatesInfoNodesAction.class));
}
actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class));
}
return actions;
Expand Down Expand Up @@ -1179,6 +1186,7 @@ public Collection<Object> createComponents(
components.add(si);
components.add(dcf);
components.add(userService);
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 b5de9cb

Please sign in to comment.