Skip to content

Commit 9ab5e0c

Browse files
committed
Fix issue #4280
Introduce 2 new endpoint: `_plugins/_security/api/certificates` `_plugins/_security/api/certificates/{nodeId}` Query parameters: - cert_type - timeout which provides information about SSL certificates for each node in the cluster Signed-off-by: Andrey Pleskach <ples@aiven.io>
1 parent d19a8ba commit 9ab5e0c

15 files changed

+918
-7
lines changed

src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,9 +245,13 @@ protected void withUser(
245245
}
246246
}
247247

248+
protected String apiPathPrefix() {
249+
return randomFrom(List.of(LEGACY_OPENDISTRO_PREFIX, PLUGINS_PREFIX));
250+
}
251+
248252
protected String securityPath(String... path) {
249253
final var fullPath = new StringJoiner("/");
250-
fullPath.add(randomFrom(List.of(LEGACY_OPENDISTRO_PREFIX, PLUGINS_PREFIX)));
254+
fullPath.add(apiPathPrefix());
251255
if (path != null) {
252256
for (final var p : path)
253257
fullPath.add(p);
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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.fasterxml.jackson.databind.JsonNode;
23+
import org.junit.Test;
24+
25+
import org.opensearch.security.dlic.rest.api.Endpoint;
26+
import org.opensearch.security.dlic.rest.api.ssl.CertificateType;
27+
import org.opensearch.test.framework.TestSecurityConfig;
28+
import org.opensearch.test.framework.certificate.TestCertificates;
29+
import org.opensearch.test.framework.cluster.LocalOpenSearchCluster;
30+
import org.opensearch.test.framework.cluster.TestRestClient;
31+
32+
import static org.hamcrest.CoreMatchers.containsString;
33+
import static org.hamcrest.CoreMatchers.is;
34+
import static org.hamcrest.MatcherAssert.assertThat;
35+
import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX;
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 CertificatesRestApiIntegrationTest extends AbstractApiIntegrationTest {
40+
41+
final static String REST_API_ADMIN_SSL_INFO = "rest-api-admin-ssl-info";
42+
43+
final static String REGULAR_USER = "regular_user";
44+
45+
static {
46+
clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true);
47+
testSecurityConfig.roles(
48+
new TestSecurityConfig.Role("simple_user_role").clusterPermissions("cluster:admin/security/certificates/info")
49+
)
50+
.rolesMapping(new TestSecurityConfig.RoleMapping("simple_user_role").users(REGULAR_USER, ADMIN_USER_NAME))
51+
.user(new TestSecurityConfig.User(REGULAR_USER))
52+
.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions())
53+
.withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION));
54+
}
55+
56+
@Override
57+
protected String apiPathPrefix() {
58+
return PLUGINS_PREFIX;
59+
}
60+
61+
protected String sslCertsPath(String... path) {
62+
final var fullPath = new StringJoiner("/");
63+
fullPath.add(super.apiPath("certificates"));
64+
if (path != null) {
65+
for (final var p : path) {
66+
fullPath.add(p);
67+
}
68+
}
69+
return fullPath.toString();
70+
}
71+
72+
@Test
73+
public void forbiddenForRegularUser() throws Exception {
74+
withUser(REGULAR_USER, client -> forbidden(() -> client.get(sslCertsPath())));
75+
}
76+
77+
@Test
78+
public void forbiddenForAdminUser() throws Exception {
79+
withUser(ADMIN_USER_NAME, client -> forbidden(() -> client.get(sslCertsPath())));
80+
}
81+
82+
@Test
83+
public void availableForTlsAdmin() throws Exception {
84+
withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifySSLCertsInfo);
85+
}
86+
87+
@Test
88+
public void availableForRestAdmin() throws Exception {
89+
withUser(REST_ADMIN_USER, this::verifySSLCertsInfo);
90+
withUser(REST_API_ADMIN_SSL_INFO, this::verifySSLCertsInfo);
91+
}
92+
93+
private void verifySSLCertsInfo(final TestRestClient client) throws Exception {
94+
assertSSLCertsInfo(
95+
localCluster.nodes(),
96+
Set.of(CertificateType.HTTP, CertificateType.TRANSPORT),
97+
ok(() -> client.get(sslCertsPath()))
98+
);
99+
if (localCluster.nodes().size() > 1) {
100+
final var randomNodes = randomNodes();
101+
final var nodeIds = randomNodes.stream().map(n -> n.esNode().getNodeEnvironment().nodeId()).collect(Collectors.joining(","));
102+
assertSSLCertsInfo(
103+
randomNodes,
104+
Set.of(CertificateType.HTTP, CertificateType.TRANSPORT),
105+
ok(() -> client.get(sslCertsPath(nodeIds)))
106+
);
107+
}
108+
final var randomCertType = randomFrom(List.of(CertificateType.HTTP, CertificateType.TRANSPORT));
109+
assertSSLCertsInfo(
110+
localCluster.nodes(),
111+
Set.of(randomCertType),
112+
ok(() -> client.get(String.format("%s?cert_type=%s", sslCertsPath(), randomCertType)))
113+
);
114+
115+
}
116+
117+
private void assertSSLCertsInfo(
118+
final List<LocalOpenSearchCluster.Node> expectedNode,
119+
final Set<CertificateType> expectedCertTypes,
120+
final TestRestClient.HttpResponse response
121+
) {
122+
final var body = response.bodyAsJsonNode();
123+
final var prettyStringBody = body.toPrettyString();
124+
125+
final var _nodes = body.get("_nodes");
126+
assertThat(prettyStringBody, _nodes.get("total").asInt(), is(expectedNode.size()));
127+
assertThat(prettyStringBody, _nodes.get("successful").asInt(), is(expectedNode.size()));
128+
assertThat(prettyStringBody, _nodes.get("failed").asInt(), is(0));
129+
assertThat(prettyStringBody, body.get("cluster_name").asText(), is(localCluster.getClusterName()));
130+
131+
final var nodes = body.get("nodes");
132+
133+
for (final var n : expectedNode) {
134+
final var esNode = n.esNode();
135+
final var node = nodes.get(esNode.getNodeEnvironment().nodeId());
136+
assertThat(prettyStringBody, node.get("name").asText(), is(n.getNodeName()));
137+
assertThat(prettyStringBody, node.has("certificates"));
138+
final var certificates = node.get("certificates");
139+
if (expectedCertTypes.contains(CertificateType.HTTP)) {
140+
final var httpCertificates = certificates.get(CertificateType.HTTP.value());
141+
assertThat(prettyStringBody, httpCertificates.isArray());
142+
assertThat(prettyStringBody, httpCertificates.size(), is(1));
143+
verifyCertsJson(n.nodeNumber(), httpCertificates.get(0));
144+
}
145+
if (expectedCertTypes.contains(CertificateType.TRANSPORT)) {
146+
final var transportCertificates = certificates.get(CertificateType.TRANSPORT.value());
147+
assertThat(prettyStringBody, transportCertificates.isArray());
148+
assertThat(prettyStringBody, transportCertificates.size(), is(1));
149+
verifyCertsJson(n.nodeNumber(), transportCertificates.get(0));
150+
}
151+
}
152+
153+
}
154+
155+
private void verifyCertsJson(final int nodeNumber, final JsonNode jsonNode) {
156+
assertThat(jsonNode.toPrettyString(), jsonNode.get("issuer_dn").asText(), is(TestCertificates.CA_SUBJECT));
157+
assertThat(
158+
jsonNode.toPrettyString(),
159+
jsonNode.get("subject_dn").asText(),
160+
is(String.format(TestCertificates.NODE_SUBJECT_PATTERN, nodeNumber))
161+
);
162+
assertThat(
163+
jsonNode.toPrettyString(),
164+
jsonNode.get("san").asText(),
165+
containsString(String.format("node-%s.example.com", nodeNumber))
166+
);
167+
assertThat(jsonNode.toPrettyString(), jsonNode.has("not_before"));
168+
assertThat(jsonNode.toPrettyString(), jsonNode.has("not_after"));
169+
}
170+
171+
private List<LocalOpenSearchCluster.Node> randomNodes() {
172+
final var nodes = localCluster.nodes();
173+
int leaveElements = randomIntBetween(1, nodes.size() - 1);
174+
return randomSubsetOf(leaveElements, nodes);
175+
}
176+
177+
public <T> List<T> randomSubsetOf(int size, Collection<T> collection) {
178+
if (size > collection.size()) {
179+
throw new IllegalArgumentException(
180+
"Can't pick " + size + " random objects from a collection of " + collection.size() + " objects"
181+
);
182+
}
183+
List<T> tempList = new ArrayList<>(collection);
184+
Collections.shuffle(tempList, RandomizedContext.current().getRandom());
185+
return tempList.subList(0, size);
186+
}
187+
188+
}

src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION;
2121
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED;
2222

23+
@Deprecated
2324
public class SslCertsRestApiIntegrationTest extends AbstractApiIntegrationTest {
2425

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

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import java.util.SortedSet;
4646
import java.util.concurrent.CompletableFuture;
4747
import java.util.concurrent.TimeUnit;
48+
import java.util.concurrent.atomic.AtomicInteger;
4849
import java.util.stream.Collectors;
4950

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

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

179182
CompletableFuture<Void> nonClusterManagerNodeFuture = startNodes(
183+
nodeCounter,
180184
clusterManager.getNonClusterManagerNodeSettings(),
181185
nonClusterManagerNodeTransportPorts,
182186
nonClusterManagerNodeHttpPorts
@@ -292,6 +296,7 @@ private final Node findRunningNode(List<Node> nodes, List<Node>... moreNodes) {
292296
}
293297

294298
private CompletableFuture<Void> startNodes(
299+
AtomicInteger nodeCounter,
295300
List<NodeSettings> nodeSettingList,
296301
SortedSet<Integer> transportPorts,
297302
SortedSet<Integer> httpPorts
@@ -300,8 +305,8 @@ private CompletableFuture<Void> startNodes(
300305
Iterator<Integer> httpPortIterator = httpPorts.iterator();
301306
List<CompletableFuture<StartStage>> futures = new ArrayList<>();
302307

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

393+
boolean hasAssignedType(NodeType type) {
394+
return requireNonNull(type, "Node type is required.").equals(this.nodeType);
395+
}
396+
388397
Node(int nodeNumber, NodeSettings nodeSettings, int transportPort, int httpPort) {
389398
this.nodeNumber = nodeNumber;
390399
this.nodeName = createNextNodeName(requireNonNull(nodeSettings, "Node settings are required."));
@@ -402,8 +411,8 @@ public class Node implements OpenSearchClientProvider {
402411
nodes.add(this);
403412
}
404413

405-
boolean hasAssignedType(NodeType type) {
406-
return requireNonNull(type, "Node type is required.").equals(this.nodeType);
414+
public int nodeNumber() {
415+
return nodeNumber;
407416
}
408417

409418
CompletableFuture<StartStage> start() {

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@
155155
import org.opensearch.security.configuration.SecurityFlsDlsIndexSearcherWrapper;
156156
import org.opensearch.security.dlic.rest.api.Endpoint;
157157
import org.opensearch.security.dlic.rest.api.SecurityRestApiActions;
158+
import org.opensearch.security.dlic.rest.api.ssl.CertificatesActionType;
159+
import org.opensearch.security.dlic.rest.api.ssl.TransportCertificatesInfoNodesAction;
158160
import org.opensearch.security.dlic.rest.validation.PasswordValidator;
159161
import org.opensearch.security.filter.SecurityFilter;
160162
import org.opensearch.security.filter.SecurityRestFilter;
@@ -174,6 +176,7 @@
174176
import org.opensearch.security.securityconf.DynamicConfigFactory;
175177
import org.opensearch.security.setting.OpensearchDynamicSetting;
176178
import org.opensearch.security.setting.TransportPassiveAuthSetting;
179+
import org.opensearch.security.ssl.ExternalSecurityKeyStore;
177180
import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory;
178181
import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin;
179182
import org.opensearch.security.ssl.SslExceptionHandler;
@@ -670,6 +673,10 @@ public UnaryOperator<RestHandler> getRestHandlerWrapper(final ThreadContext thre
670673
List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> actions = new ArrayList<>(1);
671674
if (!disabled && !SSLConfig.isSslOnlyMode()) {
672675
actions.add(new ActionHandler<>(ConfigUpdateAction.INSTANCE, TransportConfigUpdateAction.class));
676+
// external storage does not support reload and does not provide SSL certs info
677+
if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) {
678+
actions.add(new ActionHandler<>(CertificatesActionType.INSTANCE, TransportCertificatesInfoNodesAction.class));
679+
}
673680
actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class));
674681
}
675682
return actions;
@@ -1193,6 +1200,9 @@ public Collection<Object> createComponents(
11931200
components.add(si);
11941201
components.add(dcf);
11951202
components.add(userService);
1203+
if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) {
1204+
components.add(sks);
1205+
}
11961206
final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false);
11971207
final var useClusterState = useClusterStateToInitSecurityConfig(settings);
11981208
if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterState) {

0 commit comments

Comments
 (0)