From b5de9cb209c7fdc008c04e3f8dbca4931cd5ecd5 Mon Sep 17 00:00:00 2001 From: Andrey Pleskach Date: Mon, 15 Apr 2024 19:49:24 +0200 Subject: [PATCH] Fix issue #4280 Signed-off-by: Andrey Pleskach ooo Signed-off-by: Andrey Pleskach --- .../api/SslCertsRestApiIntegrationTest.java | 186 ++++++++++++++++++ .../certificate/TestCertificates.java | 32 +-- .../test/framework/cluster/LocalCluster.java | 3 +- .../cluster/LocalOpenSearchCluster.java | 14 +- .../security/OpenSearchSecurityPlugin.java | 8 + .../dlic/rest/api/SecurityRestApiActions.java | 88 +++++---- .../rest/api/SecuritySSLCertsApiAction.java | 91 ++++----- .../rest/api/ssl/SslCertsInfoActionType.java | 14 ++ .../api/ssl/SslCertsInfoNodesRequest.java | 47 +++++ .../api/ssl/SslCertsInfoNodesResponse.java | 139 +++++++++++++ ...ansportSslCertificatesInfoNodesAction.java | 143 ++++++++++++++ .../ssl/OpenSearchSecuritySSLPlugin.java | 4 - .../security/ssl/SecurityKeyStore.java | 22 +-- .../dlic/rest/api/SslCertsApiTest.java | 179 ----------------- .../api/legacy/LegacySslCertsApiTest.java | 29 --- 15 files changed, 675 insertions(+), 324 deletions(-) create mode 100644 src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java create mode 100644 src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoActionType.java create mode 100644 src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoNodesRequest.java create mode 100644 src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoNodesResponse.java create mode 100644 src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportSslCertificatesInfoNodesAction.java delete mode 100644 src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java delete mode 100644 src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java diff --git a/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java new file mode 100644 index 0000000000..eb132a678f --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java @@ -0,0 +1,186 @@ +/* + * 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.google.common.collect.ImmutableList; +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.Test; + +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.dlic.rest.api.ssl.SslCertsInfoNodesRequest; +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.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; + +public class SslCertsRestApiIntegrationTest extends AbstractApiIntegrationTest { + + final static String REST_API_ADMIN_SSL_INFO = "rest-api-admin-ssl-info"; + + static { + clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); + testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()) + .withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)); + } + + protected String sslCertsPath(String... path) { + final var fullPath = new StringJoiner("/"); + fullPath.add(super.apiPath("ssl", "certs")); + if (path != null) { + for (final var p : path) { + fullPath.add(p); + } + } + return fullPath.toString(); + } + + @Test + public void forbiddenForRegularUser() throws Exception { + withUser(NEW_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( + nodesWithOrder(), + Set.of(SslCertsInfoNodesRequest.HTTP_CERT_TYPE, SslCertsInfoNodesRequest.TRANSPORT_CERT_TYPE), + ok(() -> client.get(sslCertsPath())) + ); + if (localCluster.nodes().size() > 1) { + final var randomNodes = randomNodes(); + final var nodeIds = randomNodes.stream() + .map(Pair::getRight) + .map(n -> n.esNode().getNodeEnvironment().nodeId()) + .collect(Collectors.joining(",")); + assertSSLCertsInfo( + randomNodes, + Set.of(SslCertsInfoNodesRequest.HTTP_CERT_TYPE, SslCertsInfoNodesRequest.TRANSPORT_CERT_TYPE), + ok(() -> client.get(sslCertsPath(nodeIds))) + ); + } + final var randomCertType = randomFrom( + List.of(SslCertsInfoNodesRequest.HTTP_CERT_TYPE, SslCertsInfoNodesRequest.TRANSPORT_CERT_TYPE) + ); + assertSSLCertsInfo( + nodesWithOrder(), + Set.of(randomCertType), + ok(() -> client.get(String.format("%s?cert_type=%s", sslCertsPath(), randomCertType))) + ); + + } + + private void assertSSLCertsInfo( + final List> expectedNode, + final Set 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 nodeWithOrder : expectedNode) { + final var esNode = nodeWithOrder.getRight().esNode(); + final var node = nodes.get(esNode.getNodeEnvironment().nodeId()); + assertThat(prettyStringBody, node.get("name").asText(), is(nodeWithOrder.getRight().getNodeName())); + if (expectedCertTypes.contains(SslCertsInfoNodesRequest.HTTP_CERT_TYPE)) { + assertThat(prettyStringBody, node.has("http_certificates")); + assertThat(prettyStringBody, node.get("http_certificates").isArray()); + assertThat(prettyStringBody, node.get("http_certificates").size(), is(1)); + verifyCertsJson(nodeWithOrder.getLeft(), node.get("http_certificates").get(0)); + } + if (expectedCertTypes.contains(SslCertsInfoNodesRequest.TRANSPORT_CERT_TYPE)) { + assertThat(prettyStringBody, node.has("transport_certificates")); + assertThat(prettyStringBody, node.get("transport_certificates").isArray()); + assertThat(prettyStringBody, node.get("transport_certificates").size(), is(1)); + verifyCertsJson(nodeWithOrder.getLeft(), node.get("transport_certificates").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> randomNodes() { + final var nodesWithOrder = nodesWithOrder(); + int leaveElements = randomIntBetween(1, nodesWithOrder.size() - 1); + return randomSubsetOf(leaveElements, nodesWithOrder); + } + + private List> nodesWithOrder() { + final var list = ImmutableList.>builder(); + for (int i = 0; i < localCluster.nodes().size(); i++) + list.add(Pair.of(i, localCluster.nodes().get(i))); + return list.build(); + } + + public List randomSubsetOf(int size, Collection collection) { + if (size > collection.size()) { + throw new IllegalArgumentException( + "Can't pick " + size + " random objects from a collection of " + collection.size() + " objects" + ); + } + List tempList = new ArrayList<>(collection); + Collections.shuffle(tempList, RandomizedContext.current().getRandom()); + return tempList.subList(0, size); + } + +} diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java index 2dd1dd5eea..f5a936ce7b 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java @@ -55,9 +55,13 @@ public class TestCertificates { private static final Logger log = LogManager.getLogger(TestCertificates.class); - public static final Integer MAX_NUMBER_OF_NODE_CERTIFICATES = 3; + public static final Integer DEFAULT_NUMBER_OF_NODE_CERTIFICATES = 3; + + 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"; + + public static final String LDAP_SUBJECT = "DC=de,L=test,O=node,OU=node,CN=ldap.example.com"; + public static final String NODE_SUBJECT_PATTERN = "DC=de,L=test,O=node,OU=node,CN=node-%d.example.com"; - 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"; private static final String ADMIN_DN = "CN=kirk,OU=client,O=client,L=test,C=de"; private static final int CERTIFICATE_VALIDITY_DAYS = 365; private static final String CERTIFICATE_FILE_EXT = ".cert"; @@ -66,13 +70,18 @@ public class TestCertificates { private final CertificateData adminCertificate; private final List nodeCertificates; + private final int numberOfNodes; + private final CertificateData ldapCertificate; public TestCertificates() { + this(DEFAULT_NUMBER_OF_NODE_CERTIFICATES); + } + + public TestCertificates(final int numberOfNodes) { this.caCertificate = createCaCertificate(); - this.nodeCertificates = IntStream.range(0, MAX_NUMBER_OF_NODE_CERTIFICATES) - .mapToObj(this::createNodeCertificate) - .collect(Collectors.toList()); + this.numberOfNodes = numberOfNodes; + this.nodeCertificates = IntStream.range(0, this.numberOfNodes).mapToObj(this::createNodeCertificate).collect(Collectors.toList()); this.ldapCertificate = createLdapCertificate(); this.adminCertificate = createAdminCertificate(ADMIN_DN); log.info("Test certificates successfully generated"); @@ -109,7 +118,7 @@ public CertificateData getRootCertificateData() { /** * Certificate for Open Search node. The certificate is derived from root certificate, returned by method {@link #getRootCertificate()} - * @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES} + * @param node is a node index. It has to be less than {@link #DEFAULT_NUMBER_OF_NODE_CERTIFICATES} * @return file which contains certificate in PEM format, defined by RFC 1421 */ public File getNodeCertificate(int node) { @@ -123,18 +132,18 @@ public CertificateData getNodeCertificateData(int node) { } private void isCorrectNodeNumber(int node) { - if (node >= MAX_NUMBER_OF_NODE_CERTIFICATES) { + if (node >= numberOfNodes) { String message = String.format( "Cannot get certificate for node %d, number of created certificates for nodes is %d", node, - MAX_NUMBER_OF_NODE_CERTIFICATES + numberOfNodes ); throw new RuntimeException(message); } } private CertificateData createNodeCertificate(Integer node) { - String subject = String.format("DC=de,L=test,O=node,OU=node,CN=node-%d.example.com", node); + final var subject = String.format(NODE_SUBJECT_PATTERN, node); String domain = String.format("node-%d.example.com", node); CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS) .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH) @@ -150,8 +159,7 @@ public CertificateData issueUserCertificate(String organizationUnit, String user } private CertificateData createLdapCertificate() { - String subject = "DC=de,L=test,O=node,OU=node,CN=ldap.example.com"; - CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS) + CertificateMetadata metadata = CertificateMetadata.basicMetadata(LDAP_SUBJECT, CERTIFICATE_VALIDITY_DAYS) .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH) .withSubjectAlternativeName(null, List.of("localhost"), "127.0.0.1"); return CertificatesIssuerFactory.rsaBaseCertificateIssuer().issueSignedCertificate(metadata, caCertificate); @@ -164,7 +172,7 @@ public CertificateData getLdapCertificateData() { /** * It returns private key associated with node certificate returned by method {@link #getNodeCertificate(int)} * - * @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES} + * @param node is a node index. It has to be less than {@link #DEFAULT_NUMBER_OF_NODE_CERTIFICATES} * @param privateKeyPassword is a password used to encode private key, can be null to retrieve unencrypted key. * @return file which contains private key encoded in PEM format, defined * by RFC 1421 diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 135f1fb481..762270ffa5 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -141,7 +141,6 @@ public void before() { } for (Map.Entry entry : remotes.entrySet()) { - @SuppressWarnings("resource") InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.clusterManagerNode().getTransportAddress(); String key = "cluster.remote." + entry.getKey() + ".seeds"; String value = transportAddress.getHostString() + ":" + transportAddress.getPort(); @@ -509,7 +508,7 @@ public Builder defaultConfigurationInitDirectory(String defaultConfigurationInit public LocalCluster build() { try { if (testCertificates == null) { - testCertificates = new TestCertificates(); + testCertificates = new TestCertificates(clusterManager.getNodes() + 1); } clusterName += "_" + num.incrementAndGet(); Settings settings = nodeOverrideSettingsBuilder.build(); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java index 5e9fd75326..8a14daeb2d 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -105,6 +105,8 @@ public class LocalOpenSearchCluster { private File snapshotDir; + private int nodeCounter = 0; + public LocalOpenSearchCluster( String clusterName, ClusterManager clusterManager, @@ -163,7 +165,6 @@ public void start() throws Exception { this.initialClusterManagerHosts = toHostList(clusterManagerPorts); started = true; - CompletableFuture clusterManagerNodeFuture = startNodes( clusterManager.getClusterManagerNodeSettings(), clusterManagerNodeTransportPorts, @@ -195,7 +196,6 @@ public void start() throws Exception { log.info("Startup finished. Waiting for GREEN"); waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), nodes.size()); - log.info("Started: {}", this); } @@ -303,10 +303,10 @@ private CompletableFuture startNodes( List> futures = new ArrayList<>(); for (NodeSettings nodeSettings : nodeSettingList) { - Node node = new Node(nodeSettings, transportPortIterator.next(), httpPortIterator.next()); + Node node = new Node(nodeCounter, nodeSettings, transportPortIterator.next(), httpPortIterator.next()); futures.add(node.start()); + nodeCounter += 1; } - return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); } @@ -386,8 +386,10 @@ public class Node implements OpenSearchClientProvider { private PluginAwareNode node; private boolean running = false; private boolean portCollision = false; + private final int nodeNumber; - Node(NodeSettings nodeSettings, int transportPort, int httpPort) { + Node(int nodeNumber, NodeSettings nodeSettings, int transportPort, int httpPort) { + this.nodeNumber = nodeNumber; this.nodeName = createNextNodeName(requireNonNull(nodeSettings, "Node settings are required.")); this.nodeSettings = nodeSettings; this.nodeHomeDir = new File(clusterHomeDir, nodeName); @@ -517,7 +519,7 @@ private Settings getOpenSearchSettings() { if (nodeSettingsSupplier != null) { // TODO node number - return Settings.builder().put(settings).put(nodeSettingsSupplier.get(0)).build(); + return Settings.builder().put(settings).put(nodeSettingsSupplier.get(nodeNumber)).build(); } return settings; } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 6e3c22e695..71be612661 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -154,6 +154,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.SslCertsInfoActionType; +import org.opensearch.security.dlic.rest.api.ssl.TransportSslCertificatesInfoNodesAction; import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.filter.SecurityFilter; import org.opensearch.security.filter.SecurityRestFilter; @@ -173,6 +175,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; @@ -656,6 +659,10 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre List> 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<>(SslCertsInfoActionType.INSTANCE, TransportSslCertificatesInfoNodesAction.class)); + } actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); } return actions; @@ -1179,6 +1186,7 @@ public Collection createComponents( components.add(si); components.add(dcf); components.add(userService); + 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) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index f38cf0580d..faa50ac6e1 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -13,7 +13,8 @@ import java.nio.file.Path; import java.util.Collection; -import java.util.List; + +import com.google.common.collect.ImmutableList; import org.opensearch.client.Client; import org.opensearch.cluster.service.ClusterService; @@ -24,6 +25,7 @@ import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.ssl.ExternalSecurityKeyStore; import org.opensearch.security.ssl.SecurityKeyStore; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.user.UserService; @@ -63,41 +65,55 @@ public static Collection getHandler( auditLog, settings ); - return List.of( - new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies), - new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies), - new RolesApiAction(clusterService, threadPool, securityApiDependencies), - new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies), - new FlushCacheApiAction(clusterService, threadPool, securityApiDependencies), - new SecurityConfigApiAction(clusterService, threadPool, securityApiDependencies), - // FIXME Change inheritance for PermissionsInfoAction - new PermissionsInfoAction( - settings, - configPath, - controller, - client, - adminDns, - configurationRepository, - clusterService, - principalExtractor, - evaluator, - threadPool, - auditLog - ), - new AuthTokenProcessorAction(clusterService, threadPool, securityApiDependencies), - new TenantsApiAction(clusterService, threadPool, securityApiDependencies), - new MigrateApiAction(clusterService, threadPool, securityApiDependencies), - new ValidateApiAction(clusterService, threadPool, securityApiDependencies), - new AccountApiAction(clusterService, threadPool, securityApiDependencies), - new NodesDnApiAction(clusterService, threadPool, securityApiDependencies), - new WhitelistApiAction(clusterService, threadPool, securityApiDependencies), - // FIXME change it as soon as WhitelistApiAction will be removed - new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies), - new AuditApiAction(clusterService, threadPool, securityApiDependencies), - new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), - new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies), - new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies) - ); + final var restHandlers = ImmutableList.builder(); + restHandlers.add(new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies)) + .add(new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new RolesApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new ActionGroupsApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new FlushCacheApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new SecurityConfigApiAction(clusterService, threadPool, securityApiDependencies)) + .add( + // FIXME Change inheritance for PermissionsInfoAction + new PermissionsInfoAction( + settings, + configPath, + controller, + client, + adminDns, + configurationRepository, + clusterService, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ) + .add(new AuthTokenProcessorAction(clusterService, threadPool, securityApiDependencies)) + .add(new TenantsApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new MigrateApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new ValidateApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new AccountApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new NodesDnApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new WhitelistApiAction(clusterService, threadPool, securityApiDependencies)) + .add( + // FIXME change it as soon as WhitelistApiAction will be removed + new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies) + ) + .add(new AuditApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies)) + .add(new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies)); + if (!ExternalSecurityKeyStore.hasExternalSslContext(settings)) { + restHandlers.add( + new SecuritySSLCertsApiAction( + clusterService, + threadPool, + securityKeyStore, + certificatesReloadEnabled, + securityApiDependencies + ) + ); + } + return restHandlers.build(); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java index e60070288e..cb4b1ebf0e 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java @@ -12,21 +12,23 @@ package org.opensearch.security.dlic.rest.api; import java.io.IOException; -import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.OpenSearchSecurityException; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestChannel; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; +import org.opensearch.rest.action.RestActions; +import org.opensearch.security.dlic.rest.api.ssl.SslCertsInfoActionType; +import org.opensearch.security.dlic.rest.api.ssl.SslCertsInfoNodesRequest; +import org.opensearch.security.dlic.rest.api.ssl.SslCertsInfoNodesResponse; import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.ssl.SecurityKeyStore; @@ -36,6 +38,7 @@ import static org.opensearch.security.dlic.rest.api.Responses.badRequest; import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; +import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError; import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.api.Responses.response; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; @@ -49,8 +52,15 @@ * PUT _plugins/_security/api/ssl/{certType}/reloadcerts */ public class SecuritySSLCertsApiAction extends AbstractApiAction { + + private final static Logger LOGGER = LogManager.getLogger(FlushCacheApiAction.class); + private static final List ROUTES = addRoutesPrefix( - ImmutableList.of(new Route(Method.GET, "/ssl/certs"), new Route(Method.PUT, "/ssl/{certType}/reloadcerts")) + ImmutableList.of( + new Route(Method.GET, "/ssl/certs"), + new Route(Method.GET, "/ssl/certs/{nodeId}"), + new Route(Method.PUT, "/ssl/{certType}/reloadcerts") + ) ); private final SecurityKeyStore securityKeyStore; @@ -85,7 +95,10 @@ public String getName() { @Override protected void consumeParameters(RestRequest request) { + request.param("nodeId"); + request.param("cert_type"); request.param("certType"); + request.param("in_memory"); } @Override @@ -99,8 +112,33 @@ private void securitySSLCertsRequestHandlers(RequestHandler.RequestHandlersBuild .verifyAccessForAllMethods() .override( Method.GET, - (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> loadCertificates(channel, keyStore)) - .error((status, toXContent) -> response(channel, status, toXContent)) + (channel, request, client) -> client.execute( + SslCertsInfoActionType.INSTANCE, + new SslCertsInfoNodesRequest( + request.param("cert_type", SslCertsInfoNodesRequest.ALL_CERT_TYPE), + request.paramAsBoolean("in_memory", true), + request.paramAsStringArrayOrEmptyIfAll("nodeId") + ), + new ActionListener<>() { + @Override + public void onResponse(final SslCertsInfoNodesResponse response) { + ok(channel, (builder, params) -> { + builder.startObject(); + RestActions.buildNodesHeader(builder, channel.request(), response); + builder.field("cluster_name", response.getClusterName().value()); + response.toXContent(builder, channel.request()); + builder.endObject(); + return builder; + }); + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Cannot load SSL certificates info due to", e); + internalSeverError(channel, "Cannot load SSL certificates info " + e.getMessage() + "."); + } + } + ) ) .override(Method.PUT, (channel, request, client) -> withSecurityKeyStore().valid(keyStore -> { if (!certificatesReloadEnabled) { @@ -136,43 +174,6 @@ ValidationResult withSecurityKeyStore() { return ValidationResult.success(securityKeyStore); } - protected void loadCertificates(final RestChannel channel, final SecurityKeyStore keyStore) throws IOException { - ok( - channel, - (builder, params) -> builder.startObject() - .field("http_certificates_list", httpsEnabled ? generateCertDetailList(keyStore.getHttpCerts()) : null) - .field("transport_certificates_list", generateCertDetailList(keyStore.getTransportCerts())) - .endObject() - ); - } - - private List> generateCertDetailList(final X509Certificate[] certs) { - if (certs == null) { - return null; - } - return Arrays.stream(certs).map(cert -> { - final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName() : ""; - final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName() : ""; - - final String san = securityKeyStore.getSubjectAlternativeNames(cert); - - final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString() : ""; - final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString() : ""; - return ImmutableMap.of( - "issuer_dn", - issuerDn, - "subject_dn", - subjectDn, - "san", - san, - "not_before", - notBefore, - "not_after", - notAfter - ); - }).collect(Collectors.toList()); - } - protected void reloadCertificates(final RestChannel channel, final RestRequest request, final SecurityKeyStore keyStore) throws IOException { final String certType = request.param("certType").toLowerCase().trim(); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoActionType.java b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoActionType.java new file mode 100644 index 0000000000..c74ca9bb8a --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoActionType.java @@ -0,0 +1,14 @@ +package org.opensearch.security.dlic.rest.api.ssl; + +import org.opensearch.action.ActionType; + +public class SslCertsInfoActionType extends ActionType { + + public static final SslCertsInfoActionType INSTANCE = new SslCertsInfoActionType(); + + public static final String NAME = "cluster:admin/opendistro_security/ssl/certs/info"; + + public SslCertsInfoActionType() { + super(NAME, SslCertsInfoNodesResponse::new); + } +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoNodesRequest.java b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoNodesRequest.java new file mode 100644 index 0000000000..911537470c --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoNodesRequest.java @@ -0,0 +1,47 @@ +package org.opensearch.security.dlic.rest.api.ssl; + +import java.io.IOException; + +import org.opensearch.action.support.nodes.BaseNodesRequest; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class SslCertsInfoNodesRequest extends BaseNodesRequest { + + public final static String HTTP_CERT_TYPE = "http"; + + public final static String TRANSPORT_CERT_TYPE = "transport"; + + public final static String ALL_CERT_TYPE = "all"; + + private final String certType; + + private final boolean inMemory; + + public SslCertsInfoNodesRequest(String certType, boolean inMemory, String... nodesIds) { + super(nodesIds); + this.certType = certType; + this.inMemory = inMemory; + } + + public SslCertsInfoNodesRequest(final StreamInput in) throws IOException { + super(in); + certType = in.readString(); + inMemory = in.readBoolean(); + } + + public String certType() { + return certType; + } + + public boolean inMemory() { + return inMemory; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(certType); + out.writeBoolean(inMemory); + } +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoNodesResponse.java b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoNodesResponse.java new file mode 100644 index 0000000000..4bd35026ca --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/SslCertsInfoNodesResponse.java @@ -0,0 +1,139 @@ +package org.opensearch.security.dlic.rest.api.ssl; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.opensearch.OpenSearchException; +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.support.nodes.BaseNodeResponse; +import org.opensearch.action.support.nodes.BaseNodesResponse; +import org.opensearch.cluster.ClusterName; +import org.opensearch.cluster.node.DiscoveryNode; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; + +public class SslCertsInfoNodesResponse extends BaseNodesResponse implements ToXContentFragment { + + public SslCertsInfoNodesResponse(StreamInput in) throws IOException { + super(in); + } + + public SslCertsInfoNodesResponse( + ClusterName clusterName, + List nodes, + List failures + ) { + super(clusterName, nodes, failures); + } + + @Override + protected List readNodesFrom(StreamInput in) throws IOException { + return in.readList(SslCertsInfoNodesResponse.NodeResponse::new); + } + + @Override + protected void writeNodesTo(StreamOutput out, List nodes) throws IOException { + out.writeList(nodes); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject("nodes"); + for (final SslCertsInfoNodesResponse.NodeResponse node : getNodes()) { + builder.startObject(node.getNode().getId()); + builder.field("name", node.getNode().getName()); + if (node.exception() != null) { + builder.startObject("load_exception"); + OpenSearchException.generateThrowableXContent(builder, params, node.exception); + builder.endObject(); + } + if (node.certificates() != null) { + for (final var e : node.certificates().entrySet()) { + builder.field(e.getKey(), e.getValue()); + } + } + builder.endObject(); + } + builder.endObject(); + return builder; + } + + public static class NodeResponse extends BaseNodeResponse { + + private final Exception exception; + + private final Map>> certificates; + + public NodeResponse(final DiscoveryNode node, final Exception exception) { + super(node); + this.exception = exception; + this.certificates = null; + } + + public NodeResponse(final DiscoveryNode node, final Map>> certificates) { + super(node); + this.exception = null; + this.certificates = certificates; + } + + public NodeResponse(StreamInput in) throws IOException { + super(in); + if (in.readBoolean()) { + this.exception = in.readException(); + this.certificates = null; + } else { + this.exception = null; + this.certificates = in.readMap( + StreamInput::readString, + listIn -> listIn.readList(mapIn -> mapIn.readMap(StreamInput::readString, StreamInput::readOptionalString)) + ); + } + } + + public Map>> certificates() { + return certificates; + } + + public Exception exception() { + return exception; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + if (exception != null) { + out.writeBoolean(true); + out.writeException(exception); + } + if (certificates != null) { + out.writeBoolean(false); + out.writeMap( + certificates, + StreamOutput::writeString, + (listOut, list) -> listOut.writeCollection( + list, + (mapOut, map) -> mapOut.writeMap(map, StreamOutput::writeString, StreamOutput::writeOptionalString) + ) + ); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NodeResponse that = (NodeResponse) o; + return Objects.equals(exception, that.exception) && Objects.equals(certificates, that.certificates); + } + + @Override + public int hashCode() { + return Objects.hash(exception, certificates); + } + } + +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportSslCertificatesInfoNodesAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportSslCertificatesInfoNodesAction.java new file mode 100644 index 0000000000..6eba775758 --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ssl/TransportSslCertificatesInfoNodesAction.java @@ -0,0 +1,143 @@ +package org.opensearch.security.dlic.rest.api.ssl; + +import java.io.IOException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; + +import org.opensearch.action.FailedNodeException; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.nodes.TransportNodesAction; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.security.ssl.DefaultSecurityKeyStore; +import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.TransportRequest; +import org.opensearch.transport.TransportService; + +public class TransportSslCertificatesInfoNodesAction extends TransportNodesAction< + SslCertsInfoNodesRequest, + SslCertsInfoNodesResponse, + TransportSslCertificatesInfoNodesAction.NodeRequest, + SslCertsInfoNodesResponse.NodeResponse> { + + private final DefaultSecurityKeyStore securityKeyStore; + + private final boolean httpsEnabled; + + @Inject + public TransportSslCertificatesInfoNodesAction( + final Settings settings, + final ThreadPool threadPool, + final ClusterService clusterService, + final TransportService transportService, + final ActionFilters actionFilters, + final DefaultSecurityKeyStore securityKeyStore + ) { + super( + SslCertsInfoActionType.NAME, + threadPool, + clusterService, + transportService, + actionFilters, + SslCertsInfoNodesRequest::new, + NodeRequest::new, + ThreadPool.Names.GENERIC, + SslCertsInfoNodesResponse.NodeResponse.class + ); + this.httpsEnabled = settings.getAsBoolean(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true); + this.securityKeyStore = securityKeyStore; + } + + @Override + protected SslCertsInfoNodesResponse newResponse( + SslCertsInfoNodesRequest request, + List nodeResponses, + List failures + ) { + return new SslCertsInfoNodesResponse(clusterService.getClusterName(), nodeResponses, failures); + } + + @Override + protected NodeRequest newNodeRequest(final SslCertsInfoNodesRequest request) { + return new NodeRequest(request); + } + + @Override + protected SslCertsInfoNodesResponse.NodeResponse newNodeResponse(final StreamInput in) throws IOException { + return new SslCertsInfoNodesResponse.NodeResponse(in); + } + + @Override + protected SslCertsInfoNodesResponse.NodeResponse nodeOperation(final NodeRequest request) { + final var sslCertRequest = request.sslCertsInfoNodesRequest; + + if (securityKeyStore == null) { + return new SslCertsInfoNodesResponse.NodeResponse( + clusterService.localNode(), + new IllegalArgumentException("keystore is not initialized") + ); + } + try { + return new SslCertsInfoNodesResponse.NodeResponse(clusterService.localNode(), loadCertificates(sslCertRequest.certType())); + } catch (final Exception e) { + return new SslCertsInfoNodesResponse.NodeResponse(clusterService.localNode(), e); + } + } + + protected Map>> loadCertificates(final String certInfo) { + final var certs = ImmutableMap.>>builder(); + if (SslCertsInfoNodesRequest.HTTP_CERT_TYPE.equals(certInfo) || SslCertsInfoNodesRequest.ALL_CERT_TYPE.equals(certInfo)) { + certs.put("http_certificates", httpsEnabled ? generateCertDetailList(securityKeyStore.getHttpCerts()) : List.of()); + } + if (SslCertsInfoNodesRequest.TRANSPORT_CERT_TYPE.equals(certInfo) || SslCertsInfoNodesRequest.ALL_CERT_TYPE.equals(certInfo)) { + certs.put("transport_certificates", generateCertDetailList(securityKeyStore.getTransportCerts())); + } + return certs.build(); + } + + private List> generateCertDetailList(final X509Certificate[] certs) { + if (certs == null) { + return null; + } + return Arrays.stream(certs).map(cert -> { + final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName() : ""; + final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName() : ""; + + final String san = securityKeyStore.getSubjectAlternativeNames(cert); + + final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString() : ""; + final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString() : ""; + return Map.of("issuer_dn", issuerDn, "subject_dn", subjectDn, "san", san, "not_before", notBefore, "not_after", notAfter); + }).collect(Collectors.toList()); + } + + public static class NodeRequest extends TransportRequest { + + SslCertsInfoNodesRequest sslCertsInfoNodesRequest; + + public NodeRequest(StreamInput in) throws IOException { + super(in); + sslCertsInfoNodesRequest = new SslCertsInfoNodesRequest(in); + } + + NodeRequest(SslCertsInfoNodesRequest request) { + this.sslCertsInfoNodesRequest = request; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + sslCertsInfoNodesRequest.writeTo(out); + } + } + +} diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index 073193e9d4..e6a1b47888 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -134,10 +134,6 @@ public class OpenSearchSecuritySSLPlugin extends Plugin implements SystemIndexPl protected final SSLConfig SSLConfig; protected volatile ThreadPool threadPool; - // public OpenSearchSecuritySSLPlugin(final Settings settings, final Path configPath) { - // this(settings, configPath, false); - // } - @SuppressWarnings("removal") protected OpenSearchSecuritySSLPlugin(final Settings settings, final Path configPath, boolean disabled) { diff --git a/src/main/java/org/opensearch/security/ssl/SecurityKeyStore.java b/src/main/java/org/opensearch/security/ssl/SecurityKeyStore.java index 03b5df2100..29083d6d6b 100644 --- a/src/main/java/org/opensearch/security/ssl/SecurityKeyStore.java +++ b/src/main/java/org/opensearch/security/ssl/SecurityKeyStore.java @@ -23,25 +23,25 @@ public interface SecurityKeyStore { - public SSLEngine createHTTPSSLEngine() throws SSLException; + SSLEngine createHTTPSSLEngine() throws SSLException; - public SSLEngine createServerTransportSSLEngine() throws SSLException; + SSLEngine createServerTransportSSLEngine() throws SSLException; - public SSLEngine createClientTransportSSLEngine(String peerHost, int peerPort) throws SSLException; + SSLEngine createClientTransportSSLEngine(String peerHost, int peerPort) throws SSLException; - public String getHTTPProviderName(); + String getHTTPProviderName(); - public String getTransportServerProviderName(); + String getTransportServerProviderName(); - public String getTransportClientProviderName(); + String getTransportClientProviderName(); - public String getSubjectAlternativeNames(X509Certificate cert); + String getSubjectAlternativeNames(X509Certificate cert); - public void initHttpSSLConfig(); + void initHttpSSLConfig(); - public void initTransportSSLConfig(); + void initTransportSSLConfig(); - public X509Certificate[] getTransportCerts(); + X509Certificate[] getTransportCerts(); - public X509Certificate[] getHttpCerts(); + X509Certificate[] getHttpCerts(); } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java deleted file mode 100644 index 8617555925..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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.dlic.rest.api; - -import java.util.List; -import java.util.Map; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.apache.hc.core5.http.Header; -import org.apache.http.HttpStatus; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; - -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; - -public class SslCertsApiTest extends AbstractRestApiUnitTest { - - static final String HTTP_CERTS = "http"; - - static final String TRANSPORT_CERTS = "transport"; - - private final static List> EXPECTED_CERTIFICATES = ImmutableList.of( - ImmutableMap.of( - "issuer_dn", - "CN=Example Com Inc. Signing CA,OU=Example Com Inc. Signing CA,O=Example Com Inc.,DC=example,DC=com", - "subject_dn", - "CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE", - "san", - "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", - "not_before", - "2018-05-05T14:37:09Z", - "not_after", - "2028-05-02T14:37:09Z" - ), - ImmutableMap.of( - "issuer_dn", - "CN=Example Com Inc. Root CA,OU=Example Com Inc. Root CA,O=Example Com Inc.,DC=example,DC=com", - "subject_dn", - "CN=Example Com Inc. Signing CA,OU=Example Com Inc. Signing CA,O=Example Com Inc.,DC=example,DC=com", - "san", - "", - "not_before", - "2018-05-05T14:37:08Z", - "not_after", - "2028-05-04T14:37:08Z" - ) - ); - - private final static String EXPECTED_CERTIFICATES_BY_TYPE; - static { - try { - EXPECTED_CERTIFICATES_BY_TYPE = DefaultObjectMapper.objectMapper.writeValueAsString( - ImmutableMap.of("http_certificates_list", EXPECTED_CERTIFICATES, "transport_certificates_list", EXPECTED_CERTIFICATES) - ); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - private final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - private final Header restApiCertsInfoAdminHeader = encodeBasicHeader("rest_api_admin_ssl_info", "rest_api_admin_ssl_info"); - - private final Header restApiReloadCertsAdminHeader = encodeBasicHeader( - "rest_api_admin_ssl_reloadcerts", - "rest_api_admin_ssl_reloadcerts" - ); - - private final Header restApiHeader = encodeBasicHeader("test", "test"); - - public String certsInfoEndpoint() { - return PLUGINS_PREFIX + "/api/ssl/certs"; - } - - public String certsReloadEndpoint(final String certType) { - return String.format("%s/api/ssl/%s/reloadcerts", PLUGINS_PREFIX, certType); - } - - private void verifyHasNoAccess() throws Exception { - final Header adminCredsHeader = encodeBasicHeader("admin", "admin"); - // No creds, no admin certificate - UNAUTHORIZED - rh.sendAdminCertificate = false; - HttpResponse response = rh.executeGetRequest(certsInfoEndpoint()); - Assert.assertEquals(response.getBody(), HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - rh.sendAdminCertificate = false; - response = rh.executeGetRequest(certsInfoEndpoint(), adminCredsHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - response = rh.executeGetRequest(certsInfoEndpoint(), restApiHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - } - - @Test - public void testCertsInfo() throws Exception { - setup(); - verifyHasNoAccess(); - sendAdminCert(); - HttpResponse response = rh.executeGetRequest(certsInfoEndpoint()); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - Assert.assertEquals(EXPECTED_CERTIFICATES_BY_TYPE, response.getBody()); - - } - - @Test - public void testCertsInfoRestAdmin() throws Exception { - setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); - verifyHasNoAccess(); - rh.sendAdminCertificate = false; - Assert.assertEquals(EXPECTED_CERTIFICATES_BY_TYPE, loadCerts(restApiAdminHeader)); - Assert.assertEquals(EXPECTED_CERTIFICATES_BY_TYPE, loadCerts(restApiCertsInfoAdminHeader)); - } - - private String loadCerts(final Header... header) throws Exception { - HttpResponse response = rh.executeGetRequest(certsInfoEndpoint(), restApiAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - return response.getBody(); - } - - @Test - public void testReloadCertsNotAvailableByDefault() throws Exception { - setupWithRestRoles(); - - sendAdminCert(); - verifyReloadCertsNotAvailable(HttpStatus.SC_BAD_REQUEST); - - rh.sendAdminCertificate = false; - verifyReloadCertsNotAvailable(HttpStatus.SC_FORBIDDEN, restApiAdminHeader); - verifyReloadCertsNotAvailable(HttpStatus.SC_FORBIDDEN, restApiReloadCertsAdminHeader); - } - - private void verifyReloadCertsNotAvailable(final int expectedStatus, final Header... header) { - HttpResponse response = rh.executePutRequest(certsReloadEndpoint(HTTP_CERTS), "{}", header); - Assert.assertEquals(response.getBody(), expectedStatus, response.getStatusCode()); - response = rh.executePutRequest(certsReloadEndpoint(TRANSPORT_CERTS), "{}", header); - Assert.assertEquals(response.getBody(), expectedStatus, response.getStatusCode()); - } - - @Test - public void testReloadCertsWrongCertsType() throws Exception { - setupWithRestRoles(reloadEnabled()); - sendAdminCert(); - HttpResponse response = rh.executePutRequest(certsReloadEndpoint("aaaaa"), "{}"); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - rh.sendAdminCertificate = false; - response = rh.executePutRequest(certsReloadEndpoint("bbbb"), "{}", restApiAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePutRequest(certsReloadEndpoint("cccc"), "{}", restApiReloadCertsAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - } - - private void sendAdminCert() { - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - } - - Settings reloadEnabled() { - return Settings.builder().put(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, true).build(); - } - -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java deleted file mode 100644 index 5d1c3ae538..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.dlic.rest.api.legacy; - -import org.opensearch.security.dlic.rest.api.SslCertsApiTest; - -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; - -public class LegacySslCertsApiTest extends SslCertsApiTest { - - @Override - public String certsInfoEndpoint() { - return LEGACY_OPENDISTRO_PREFIX + "/api/ssl/certs"; - } - - @Override - public String certsReloadEndpoint(String certType) { - return String.format("%s/api/ssl/%s/reloadcerts", LEGACY_OPENDISTRO_PREFIX, certType); - } -}