Skip to content

Commit 1d88ef7

Browse files
committed
Fix issue #4280
Signed-off-by: Andrey Pleskach <ples@aiven.io>
1 parent 0d7af4d commit 1d88ef7

File tree

15 files changed

+741
-324
lines changed

15 files changed

+741
-324
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*
8+
* Modifications Copyright OpenSearch Contributors. See
9+
* GitHub history for details.
10+
*/
11+
package org.opensearch.security.api;
12+
13+
import java.util.ArrayList;
14+
import java.util.Collection;
15+
import java.util.Collections;
16+
import java.util.List;
17+
import java.util.Set;
18+
import java.util.StringJoiner;
19+
import java.util.stream.Collectors;
20+
21+
import com.carrotsearch.randomizedtesting.RandomizedContext;
22+
import com.google.common.collect.ImmutableList;
23+
import com.fasterxml.jackson.databind.JsonNode;
24+
import org.apache.commons.lang3.tuple.Pair;
25+
import org.junit.Test;
26+
27+
import org.opensearch.security.dlic.rest.api.Endpoint;
28+
import org.opensearch.security.dlic.rest.api.ssl.SslCertificatesInfoNodesRequest;
29+
import org.opensearch.test.framework.certificate.TestCertificates;
30+
import org.opensearch.test.framework.cluster.LocalOpenSearchCluster;
31+
import org.opensearch.test.framework.cluster.TestRestClient;
32+
33+
import static org.hamcrest.CoreMatchers.containsString;
34+
import static org.hamcrest.CoreMatchers.is;
35+
import static org.hamcrest.MatcherAssert.assertThat;
36+
import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION;
37+
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED;
38+
39+
public class SslCertsRestApiIntegrationTest extends AbstractApiIntegrationTest {
40+
41+
final static String REST_API_ADMIN_SSL_INFO = "rest-api-admin-ssl-info";
42+
43+
static {
44+
clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true);
45+
testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions())
46+
.withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION));
47+
}
48+
49+
protected String sslCertsPath(String... path) {
50+
final var fullPath = new StringJoiner("/");
51+
fullPath.add(super.apiPath("ssl", "certs"));
52+
if (path != null) {
53+
for (final var p : path) {
54+
fullPath.add(p);
55+
}
56+
}
57+
return fullPath.toString();
58+
}
59+
60+
@Test
61+
public void forbiddenForRegularUser() throws Exception {
62+
withUser(NEW_USER, client -> forbidden(() -> client.get(sslCertsPath())));
63+
}
64+
65+
@Test
66+
public void forbiddenForAdminUser() throws Exception {
67+
withUser(ADMIN_USER_NAME, client -> forbidden(() -> client.get(sslCertsPath())));
68+
}
69+
70+
@Test
71+
public void availableForTlsAdmin() throws Exception {
72+
withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifySSLCertsInfo);
73+
}
74+
75+
@Test
76+
public void availableForRestAdmin() throws Exception {
77+
withUser(REST_ADMIN_USER, this::verifySSLCertsInfo);
78+
withUser(REST_API_ADMIN_SSL_INFO, this::verifySSLCertsInfo);
79+
}
80+
81+
private void verifySSLCertsInfo(final TestRestClient client) throws Exception {
82+
assertSSLCertsInfo(
83+
nodesWithOrder(),
84+
Set.of(SslCertificatesInfoNodesRequest.HTTP_CERT_TYPE, SslCertificatesInfoNodesRequest.TRANSPORT_CERT_TYPE),
85+
ok(() -> client.get(sslCertsPath()))
86+
);
87+
if (localCluster.nodes().size() > 1) {
88+
final var randomNodes = randomNodes();
89+
final var nodeIds = randomNodes.stream()
90+
.map(Pair::getRight)
91+
.map(n -> n.esNode().getNodeEnvironment().nodeId())
92+
.collect(Collectors.joining(","));
93+
assertSSLCertsInfo(
94+
randomNodes,
95+
Set.of(SslCertificatesInfoNodesRequest.HTTP_CERT_TYPE, SslCertificatesInfoNodesRequest.TRANSPORT_CERT_TYPE),
96+
ok(() -> client.get(sslCertsPath(nodeIds)))
97+
);
98+
}
99+
final var randomCertType = randomFrom(
100+
List.of(SslCertificatesInfoNodesRequest.HTTP_CERT_TYPE, SslCertificatesInfoNodesRequest.TRANSPORT_CERT_TYPE)
101+
);
102+
assertSSLCertsInfo(
103+
nodesWithOrder(),
104+
Set.of(randomCertType),
105+
ok(() -> client.get(String.format("%s?cert_type=%s", sslCertsPath(), randomCertType)))
106+
);
107+
108+
}
109+
110+
private void assertSSLCertsInfo(
111+
final List<Pair<Integer, LocalOpenSearchCluster.Node>> expectedNode,
112+
final Set<String> expectedCertTypes,
113+
final TestRestClient.HttpResponse response
114+
) {
115+
final var body = response.bodyAsJsonNode();
116+
final var prettyStringBody = body.toPrettyString();
117+
118+
final var _nodes = body.get("_nodes");
119+
assertThat(prettyStringBody, _nodes.get("total").asInt(), is(expectedNode.size()));
120+
assertThat(prettyStringBody, _nodes.get("successful").asInt(), is(expectedNode.size()));
121+
assertThat(prettyStringBody, _nodes.get("failed").asInt(), is(0));
122+
assertThat(prettyStringBody, body.get("cluster_name").asText(), is(localCluster.getClusterName()));
123+
124+
final var nodes = body.get("nodes");
125+
126+
for (final var nodeWithOrder : expectedNode) {
127+
final var esNode = nodeWithOrder.getRight().esNode();
128+
final var node = nodes.get(esNode.getNodeEnvironment().nodeId());
129+
assertThat(prettyStringBody, node.get("name").asText(), is(nodeWithOrder.getRight().getNodeName()));
130+
assertThat(prettyStringBody, node.has("certificates"));
131+
final var certificates = node.get("certificates");
132+
if (expectedCertTypes.contains(SslCertificatesInfoNodesRequest.HTTP_CERT_TYPE)) {
133+
assertThat(prettyStringBody, certificates.get(SslCertificatesInfoNodesRequest.HTTP_CERT_TYPE).isArray());
134+
assertThat(prettyStringBody, certificates.get(SslCertificatesInfoNodesRequest.HTTP_CERT_TYPE).size(), is(1));
135+
verifyCertsJson(nodeWithOrder.getLeft(), certificates.get(SslCertificatesInfoNodesRequest.HTTP_CERT_TYPE).get(0));
136+
}
137+
if (expectedCertTypes.contains(SslCertificatesInfoNodesRequest.TRANSPORT_CERT_TYPE)) {
138+
assertThat(prettyStringBody, certificates.get(SslCertificatesInfoNodesRequest.TRANSPORT_CERT_TYPE).isArray());
139+
assertThat(prettyStringBody, certificates.get(SslCertificatesInfoNodesRequest.TRANSPORT_CERT_TYPE).size(), is(1));
140+
verifyCertsJson(nodeWithOrder.getLeft(), certificates.get(SslCertificatesInfoNodesRequest.TRANSPORT_CERT_TYPE).get(0));
141+
}
142+
}
143+
144+
}
145+
146+
private void verifyCertsJson(final int nodeNumber, final JsonNode jsonNode) {
147+
assertThat(jsonNode.toPrettyString(), jsonNode.get("issuer_dn").asText(), is(TestCertificates.CA_SUBJECT));
148+
assertThat(
149+
jsonNode.toPrettyString(),
150+
jsonNode.get("subject_dn").asText(),
151+
is(String.format(TestCertificates.NODE_SUBJECT_PATTERN, nodeNumber))
152+
);
153+
assertThat(
154+
jsonNode.toPrettyString(),
155+
jsonNode.get("san").asText(),
156+
containsString(String.format("node-%s.example.com", nodeNumber))
157+
);
158+
assertThat(jsonNode.toPrettyString(), jsonNode.has("not_before"));
159+
assertThat(jsonNode.toPrettyString(), jsonNode.has("not_after"));
160+
}
161+
162+
private List<Pair<Integer, LocalOpenSearchCluster.Node>> randomNodes() {
163+
final var nodesWithOrder = nodesWithOrder();
164+
int leaveElements = randomIntBetween(1, nodesWithOrder.size() - 1);
165+
return randomSubsetOf(leaveElements, nodesWithOrder);
166+
}
167+
168+
private List<Pair<Integer, LocalOpenSearchCluster.Node>> nodesWithOrder() {
169+
final var list = ImmutableList.<Pair<Integer, LocalOpenSearchCluster.Node>>builder();
170+
for (int i = 0; i < localCluster.nodes().size(); i++)
171+
list.add(Pair.of(i, localCluster.nodes().get(i)));
172+
return list.build();
173+
}
174+
175+
public <T> List<T> randomSubsetOf(int size, Collection<T> collection) {
176+
if (size > collection.size()) {
177+
throw new IllegalArgumentException(
178+
"Can't pick " + size + " random objects from a collection of " + collection.size() + " objects"
179+
);
180+
}
181+
List<T> tempList = new ArrayList<>(collection);
182+
Collections.shuffle(tempList, RandomizedContext.current().getRandom());
183+
return tempList.subList(0, size);
184+
}
185+
186+
}

src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,13 @@ public class TestCertificates {
5555

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

58-
public static final Integer MAX_NUMBER_OF_NODE_CERTIFICATES = 3;
58+
public static final Integer DEFAULT_NUMBER_OF_NODE_CERTIFICATES = 3;
59+
60+
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";
61+
62+
public static final String LDAP_SUBJECT = "DC=de,L=test,O=node,OU=node,CN=ldap.example.com";
63+
public static final String NODE_SUBJECT_PATTERN = "DC=de,L=test,O=node,OU=node,CN=node-%d.example.com";
5964

60-
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";
6165
private static final String ADMIN_DN = "CN=kirk,OU=client,O=client,L=test,C=de";
6266
private static final int CERTIFICATE_VALIDITY_DAYS = 365;
6367
private static final String CERTIFICATE_FILE_EXT = ".cert";
@@ -66,13 +70,18 @@ public class TestCertificates {
6670
private final CertificateData adminCertificate;
6771
private final List<CertificateData> nodeCertificates;
6872

73+
private final int numberOfNodes;
74+
6975
private final CertificateData ldapCertificate;
7076

7177
public TestCertificates() {
78+
this(DEFAULT_NUMBER_OF_NODE_CERTIFICATES);
79+
}
80+
81+
public TestCertificates(final int numberOfNodes) {
7282
this.caCertificate = createCaCertificate();
73-
this.nodeCertificates = IntStream.range(0, MAX_NUMBER_OF_NODE_CERTIFICATES)
74-
.mapToObj(this::createNodeCertificate)
75-
.collect(Collectors.toList());
83+
this.numberOfNodes = numberOfNodes;
84+
this.nodeCertificates = IntStream.range(0, this.numberOfNodes).mapToObj(this::createNodeCertificate).collect(Collectors.toList());
7685
this.ldapCertificate = createLdapCertificate();
7786
this.adminCertificate = createAdminCertificate(ADMIN_DN);
7887
log.info("Test certificates successfully generated");
@@ -109,7 +118,7 @@ public CertificateData getRootCertificateData() {
109118

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

125134
private void isCorrectNodeNumber(int node) {
126-
if (node >= MAX_NUMBER_OF_NODE_CERTIFICATES) {
135+
if (node >= numberOfNodes) {
127136
String message = String.format(
128137
"Cannot get certificate for node %d, number of created certificates for nodes is %d",
129138
node,
130-
MAX_NUMBER_OF_NODE_CERTIFICATES
139+
numberOfNodes
131140
);
132141
throw new RuntimeException(message);
133142
}
134143
}
135144

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

152161
private CertificateData createLdapCertificate() {
153-
String subject = "DC=de,L=test,O=node,OU=node,CN=ldap.example.com";
154-
CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS)
162+
CertificateMetadata metadata = CertificateMetadata.basicMetadata(LDAP_SUBJECT, CERTIFICATE_VALIDITY_DAYS)
155163
.withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH)
156164
.withSubjectAlternativeName(null, List.of("localhost"), "127.0.0.1");
157165
return CertificatesIssuerFactory.rsaBaseCertificateIssuer().issueSignedCertificate(metadata, caCertificate);
@@ -164,7 +172,7 @@ public CertificateData getLdapCertificateData() {
164172
/**
165173
* It returns private key associated with node certificate returned by method {@link #getNodeCertificate(int)}
166174
*
167-
* @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES}
175+
* @param node is a node index. It has to be less than {@link #DEFAULT_NUMBER_OF_NODE_CERTIFICATES}
168176
* @param privateKeyPassword is a password used to encode private key, can be <code>null</code> to retrieve unencrypted key.
169177
* @return file which contains private key encoded in PEM format, defined
170178
* by <a href="https://www.rfc-editor.org/rfc/rfc1421.txt">RFC 1421</a>

src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,6 @@ public void before() {
141141
}
142142

143143
for (Map.Entry<String, LocalCluster> entry : remotes.entrySet()) {
144-
@SuppressWarnings("resource")
145144
InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.clusterManagerNode().getTransportAddress();
146145
String key = "cluster.remote." + entry.getKey() + ".seeds";
147146
String value = transportAddress.getHostString() + ":" + transportAddress.getPort();
@@ -509,7 +508,7 @@ public Builder defaultConfigurationInitDirectory(String defaultConfigurationInit
509508
public LocalCluster build() {
510509
try {
511510
if (testCertificates == null) {
512-
testCertificates = new TestCertificates();
511+
testCertificates = new TestCertificates(clusterManager.getNodes() + 1);
513512
}
514513
clusterName += "_" + num.incrementAndGet();
515514
Settings settings = nodeOverrideSettingsBuilder.build();

src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public class LocalOpenSearchCluster {
105105

106106
private File snapshotDir;
107107

108+
private int nodeCounter = 0;
109+
108110
public LocalOpenSearchCluster(
109111
String clusterName,
110112
ClusterManager clusterManager,
@@ -163,7 +165,6 @@ public void start() throws Exception {
163165
this.initialClusterManagerHosts = toHostList(clusterManagerPorts);
164166

165167
started = true;
166-
167168
CompletableFuture<Void> clusterManagerNodeFuture = startNodes(
168169
clusterManager.getClusterManagerNodeSettings(),
169170
clusterManagerNodeTransportPorts,
@@ -195,7 +196,6 @@ public void start() throws Exception {
195196
log.info("Startup finished. Waiting for GREEN");
196197

197198
waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), nodes.size());
198-
199199
log.info("Started: {}", this);
200200

201201
}
@@ -303,10 +303,10 @@ private CompletableFuture<Void> startNodes(
303303
List<CompletableFuture<StartStage>> futures = new ArrayList<>();
304304

305305
for (NodeSettings nodeSettings : nodeSettingList) {
306-
Node node = new Node(nodeSettings, transportPortIterator.next(), httpPortIterator.next());
306+
Node node = new Node(nodeCounter, nodeSettings, transportPortIterator.next(), httpPortIterator.next());
307307
futures.add(node.start());
308+
nodeCounter += 1;
308309
}
309-
310310
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
311311
}
312312

@@ -386,8 +386,10 @@ public class Node implements OpenSearchClientProvider {
386386
private PluginAwareNode node;
387387
private boolean running = false;
388388
private boolean portCollision = false;
389+
private final int nodeNumber;
389390

390-
Node(NodeSettings nodeSettings, int transportPort, int httpPort) {
391+
Node(int nodeNumber, NodeSettings nodeSettings, int transportPort, int httpPort) {
392+
this.nodeNumber = nodeNumber;
391393
this.nodeName = createNextNodeName(requireNonNull(nodeSettings, "Node settings are required."));
392394
this.nodeSettings = nodeSettings;
393395
this.nodeHomeDir = new File(clusterHomeDir, nodeName);
@@ -517,7 +519,7 @@ private Settings getOpenSearchSettings() {
517519

518520
if (nodeSettingsSupplier != null) {
519521
// TODO node number
520-
return Settings.builder().put(settings).put(nodeSettingsSupplier.get(0)).build();
522+
return Settings.builder().put(settings).put(nodeSettingsSupplier.get(nodeNumber)).build();
521523
}
522524
return settings;
523525
}

src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@
154154
import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper;
155155
import org.opensearch.security.dlic.rest.api.Endpoint;
156156
import org.opensearch.security.dlic.rest.api.SecurityRestApiActions;
157+
import org.opensearch.security.dlic.rest.api.ssl.SslCertificatesActionType;
158+
import org.opensearch.security.dlic.rest.api.ssl.TransportSslCertificatesInfoNodesAction;
157159
import org.opensearch.security.dlic.rest.validation.PasswordValidator;
158160
import org.opensearch.security.filter.SecurityFilter;
159161
import org.opensearch.security.filter.SecurityRestFilter;
@@ -173,6 +175,7 @@
173175
import org.opensearch.security.securityconf.DynamicConfigFactory;
174176
import org.opensearch.security.setting.OpensearchDynamicSetting;
175177
import org.opensearch.security.setting.TransportPassiveAuthSetting;
178+
import org.opensearch.security.ssl.ExternalSecurityKeyStore;
176179
import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory;
177180
import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin;
178181
import org.opensearch.security.ssl.SslExceptionHandler;
@@ -656,6 +659,10 @@ public UnaryOperator<RestHandler> getRestHandlerWrapper(final ThreadContext thre
656659
List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> actions = new ArrayList<>(1);
657660
if (!disabled && !SSLConfig.isSslOnlyMode()) {
658661
actions.add(new ActionHandler<>(ConfigUpdateAction.INSTANCE, TransportConfigUpdateAction.class));
662+
// external storage does not support reload and does not provide SSL certs info
663+
if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) {
664+
actions.add(new ActionHandler<>(SslCertificatesActionType.INSTANCE, TransportSslCertificatesInfoNodesAction.class));
665+
}
659666
actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class));
660667
}
661668
return actions;
@@ -1179,6 +1186,7 @@ public Collection<Object> createComponents(
11791186
components.add(si);
11801187
components.add(dcf);
11811188
components.add(userService);
1189+
components.add(sks);
11821190
final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false);
11831191
final var useClusterState = useClusterStateToInitSecurityConfig(settings);
11841192
if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterState) {

0 commit comments

Comments
 (0)