Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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