Skip to content

Commit bb574e0

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

File tree

10 files changed

+796
-42
lines changed

10 files changed

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

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

Lines changed: 10 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.CertificatesActionType;
158+
import org.opensearch.security.dlic.rest.api.ssl.TransportCertificatesInfoNodesAction;
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<>(CertificatesActionType.INSTANCE, TransportCertificatesInfoNodesAction.class));
665+
}
659666
actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class));
660667
}
661668
return actions;
@@ -1179,6 +1186,9 @@ public Collection<Object> createComponents(
11791186
components.add(si);
11801187
components.add(dcf);
11811188
components.add(userService);
1189+
if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) {
1190+
components.add(sks);
1191+
}
11821192
final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false);
11831193
final var useClusterState = useClusterStateToInitSecurityConfig(settings);
11841194
if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterState) {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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.CertificatesActionType;
25+
import org.opensearch.security.dlic.rest.api.ssl.CertificatesInfoNodesRequest;
26+
import org.opensearch.security.dlic.rest.api.ssl.CertificatesNodesResponse;
27+
import org.opensearch.security.securityconf.impl.CType;
28+
import org.opensearch.threadpool.ThreadPool;
29+
30+
import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError;
31+
import static org.opensearch.security.dlic.rest.api.Responses.ok;
32+
import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION;
33+
import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_API_ROUTE_PREFIX;
34+
import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix;
35+
36+
public class CertificatesApiAction extends AbstractApiAction {
37+
38+
private final static Logger LOGGER = LogManager.getLogger(CertificatesApiAction.class);
39+
40+
private static final List<Route> ROUTES = addRoutesPrefix(
41+
ImmutableList.of(new Route(RestRequest.Method.GET, "/certificates"), new Route(RestRequest.Method.GET, "/certificates/{nodeId}")),
42+
PLUGIN_API_ROUTE_PREFIX
43+
);
44+
45+
public CertificatesApiAction(
46+
final ClusterService clusterService,
47+
final ThreadPool threadPool,
48+
final SecurityApiDependencies securityApiDependencies
49+
) {
50+
super(Endpoint.SSL, clusterService, threadPool, securityApiDependencies);
51+
this.requestHandlersBuilder.configureRequestHandlers(this::securitySSLCertsRequestHandlers);
52+
}
53+
54+
@Override
55+
public List<Route> routes() {
56+
return ROUTES;
57+
}
58+
59+
@Override
60+
public String getName() {
61+
return "HTTP and Transport Certificates Actions";
62+
}
63+
64+
@Override
65+
protected CType getConfigType() {
66+
return null;
67+
}
68+
69+
@Override
70+
protected void consumeParameters(RestRequest request) {
71+
request.param("nodeId");
72+
request.param("cert_type");
73+
}
74+
75+
private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuilder requestHandlersBuilder) {
76+
requestHandlersBuilder.withAccessHandler(this::accessHandler)
77+
.allMethodsNotImplemented()
78+
.verifyAccessForAllMethods()
79+
.override(
80+
RestRequest.Method.GET,
81+
(channel, request, client) -> client.execute(
82+
CertificatesActionType.INSTANCE,
83+
new CertificatesInfoNodesRequest(
84+
request.param("cert_type", CertificatesInfoNodesRequest.ALL_CERT_TYPE),
85+
true,
86+
request.paramAsStringArrayOrEmptyIfAll("nodeId")
87+
),
88+
new ActionListener<>() {
89+
@Override
90+
public void onResponse(final CertificatesNodesResponse response) {
91+
ok(channel, (builder, params) -> {
92+
builder.startObject();
93+
RestActions.buildNodesHeader(builder, channel.request(), response);
94+
builder.field("cluster_name", response.getClusterName().value());
95+
response.toXContent(builder, channel.request());
96+
builder.endObject();
97+
return builder;
98+
});
99+
}
100+
101+
@Override
102+
public void onFailure(Exception e) {
103+
LOGGER.error("Cannot load SSL certificates info due to", e);
104+
internalSeverError(channel, "Cannot load SSL certificates info " + e.getMessage() + ".");
105+
}
106+
}
107+
)
108+
);
109+
}
110+
111+
boolean accessHandler(final RestRequest request) {
112+
if (request.method() == RestRequest.Method.GET) {
113+
return securityApiDependencies.restApiAdminPrivilegesEvaluator().isCurrentUserAdminFor(endpoint, CERTS_INFO_ACTION);
114+
} else {
115+
return false;
116+
}
117+
}
118+
119+
}

0 commit comments

Comments
 (0)