Skip to content

Commit 00a0356

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 00a0356

14 files changed

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

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/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) {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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+
12+
package org.opensearch.security.dlic.rest.api;
13+
14+
import java.util.List;
15+
16+
import com.google.common.collect.ImmutableList;
17+
import org.apache.logging.log4j.LogManager;
18+
import org.apache.logging.log4j.Logger;
19+
20+
import org.opensearch.cluster.service.ClusterService;
21+
import org.opensearch.core.action.ActionListener;
22+
import org.opensearch.rest.RestRequest;
23+
import org.opensearch.rest.action.RestActions;
24+
import org.opensearch.security.dlic.rest.api.ssl.CertificateType;
25+
import org.opensearch.security.dlic.rest.api.ssl.CertificatesActionType;
26+
import org.opensearch.security.dlic.rest.api.ssl.CertificatesInfoNodesRequest;
27+
import org.opensearch.security.dlic.rest.api.ssl.CertificatesNodesResponse;
28+
import org.opensearch.security.securityconf.impl.CType;
29+
import org.opensearch.threadpool.ThreadPool;
30+
31+
import static org.opensearch.security.dlic.rest.api.Responses.internalServerError;
32+
import static org.opensearch.security.dlic.rest.api.Responses.ok;
33+
import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION;
34+
import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_API_ROUTE_PREFIX;
35+
import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix;
36+
37+
public class CertificatesApiAction extends AbstractApiAction {
38+
39+
private final static Logger LOGGER = LogManager.getLogger(CertificatesApiAction.class);
40+
41+
private static final List<Route> ROUTES = addRoutesPrefix(
42+
ImmutableList.of(new Route(RestRequest.Method.GET, "/certificates"), new Route(RestRequest.Method.GET, "/certificates/{nodeId}")),
43+
PLUGIN_API_ROUTE_PREFIX
44+
);
45+
46+
public CertificatesApiAction(
47+
final ClusterService clusterService,
48+
final ThreadPool threadPool,
49+
final SecurityApiDependencies securityApiDependencies
50+
) {
51+
super(Endpoint.SSL, clusterService, threadPool, securityApiDependencies);
52+
this.requestHandlersBuilder.configureRequestHandlers(this::securitySSLCertsRequestHandlers);
53+
}
54+
55+
@Override
56+
public List<Route> routes() {
57+
return ROUTES;
58+
}
59+
60+
@Override
61+
public String getName() {
62+
return "HTTP and Transport Certificates Actions";
63+
}
64+
65+
@Override
66+
protected CType getConfigType() {
67+
return null;
68+
}
69+
70+
@Override
71+
protected void consumeParameters(RestRequest request) {
72+
request.param("nodeId");
73+
request.param("cert_type");
74+
}
75+
76+
private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) {
77+
requestHandlersBuilder.withAccessHandler(this::accessHandler)
78+
.allMethodsNotImplemented()
79+
.verifyAccessForAllMethods()
80+
.override(
81+
RestRequest.Method.GET,
82+
(channel, request, client) -> client.execute(
83+
CertificatesActionType.INSTANCE,
84+
new CertificatesInfoNodesRequest(
85+
CertificateType.from(request.param("cert_type")),
86+
true,
87+
request.paramAsStringArrayOrEmptyIfAll("nodeId")
88+
).timeout(request.param("timeout")),
89+
new ActionListener<>() {
90+
@Override
91+
public void onResponse(final CertificatesNodesResponse response) {
92+
ok(channel, (builder, params) -> {
93+
builder.startObject();
94+
RestActions.buildNodesHeader(builder, channel.request(), response);
95+
builder.field("cluster_name", response.getClusterName().value());
96+
response.toXContent(builder, channel.request());
97+
builder.endObject();
98+
return builder;
99+
});
100+
}
101+
102+
@Override
103+
public void onFailure(Exception e) {
104+
LOGGER.error("Cannot load SSL certificates info due to", e);
105+
internalServerError(channel, "Cannot load SSL certificates info " + e.getMessage() + ".");
106+
}
107+
}
108+
)
109+
);
110+
}
111+
112+
boolean accessHandler(final RestRequest request) {
113+
if (request.method() == RestRequest.Method.GET) {
114+
return securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint, CERTS_INFO_ACTION);
115+
} else {
116+
return false;
117+
}
118+
}
119+
120+
}

src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ public static Collection<RestHandler> getHandler(
9595
new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies),
9696
new AuditApiAction(clusterService, threadPool, securityApiDependencies),
9797
new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies),
98+
new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies),
9899
new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies),
99-
new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies)
100+
new CertificatesApiAction(clusterService, threadPool, securityApiDependencies)
100101
);
101102
}
102103

0 commit comments

Comments
 (0)