Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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.StringJoiner;

import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.Test;

import org.opensearch.security.DefaultObjectMapper;
import org.opensearch.security.dlic.rest.api.Endpoint;
import org.opensearch.test.framework.cluster.TestRestClient;

import static org.opensearch.security.api.PatchPayloadHelper.patch;
import static org.opensearch.security.api.PatchPayloadHelper.replaceOp;
import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE;
import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED;
import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION;

public class ConfigRestApiIntegrationTest extends AbstractApiIntegrationTest {

final static String REST_API_ADMIN_CONFIG_UPDATE = "rest-api-admin-config-update";

static {
clusterSettings.put(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).put(SECURITY_RESTAPI_ADMIN_ENABLED, true);
testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions())
.withRestAdminUser(REST_API_ADMIN_CONFIG_UPDATE, restAdminPermission(Endpoint.CONFIG, SECURITY_CONFIG_UPDATE));
}

private String securityConfigPath(final String... path) {
final var fullPath = new StringJoiner("/").add(super.apiPath("securityconfig"));
if (path != null) for (final var p : path)
fullPath.add(p);
return fullPath.toString();
}

@Test
public void forbiddenForRegularUsers() throws Exception {
withUser(NEW_USER, client -> {
forbidden(() -> client.get(securityConfigPath()));
forbidden(() -> client.putJson(securityConfigPath("config"), EMPTY_BODY));
forbidden(() -> client.patch(securityConfigPath(), EMPTY_BODY));
verifyNotAllowedMethods(client);
});
}

@Test
public void partiallyAvailableForAdminUser() throws Exception {
withUser(ADMIN_USER_NAME, client -> ok(() -> client.get(securityConfigPath())));
withUser(ADMIN_USER_NAME, client -> {
badRequest(() -> client.putJson(securityConfigPath("xxx"), EMPTY_BODY));
forbidden(() -> client.putJson(securityConfigPath("config"), EMPTY_BODY));
forbidden(() -> client.patch(securityConfigPath(), EMPTY_BODY));
});
withUser(ADMIN_USER_NAME, this::verifyNotAllowedMethods);
}

@Test
public void availableForTlsAdminUser() throws Exception {
withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> ok(() -> client.get(securityConfigPath())));
withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifyUpdate);
}

@Test
public void availableForRestAdminUser() throws Exception {
withUser(REST_ADMIN_USER, client -> ok(() -> client.get(securityConfigPath())));
withUser(REST_ADMIN_USER, this::verifyUpdate);
withUser(REST_API_ADMIN_CONFIG_UPDATE, this::verifyUpdate);
}

void verifyUpdate(final TestRestClient client) throws Exception {
badRequest(() -> client.putJson(securityConfigPath("xxx"), EMPTY_BODY));
verifyNotAllowedMethods(client);

final var configJson = ok(() -> client.get(securityConfigPath())).bodyAsJsonNode();
final var authFailureListeners = DefaultObjectMapper.objectMapper.createObjectNode();
authFailureListeners.set(
"ip_rate_limiting",
DefaultObjectMapper.objectMapper.createObjectNode()
.put("type", "ip")
.put("allowed_tries", 10)
.put("time_window_seconds", 3_600)
.put("block_expiry_seconds", 600)
.put("max_blocked_clients", 100_000)
.put("max_tracked_clients", 100_000)
);
authFailureListeners.set(
"internal_authentication_backend_limiting",
DefaultObjectMapper.objectMapper.createObjectNode()
.put("type", "username")
.put("authentication_backend", "intern")
.put("allowed_tries", 10)
.put("time_window_seconds", 3_600)
.put("block_expiry_seconds", 600)
.put("max_blocked_clients", 100_000)
.put("max_tracked_clients", 100_000)
);
final var dynamicConfigJson = (ObjectNode) configJson.get("config").get("dynamic");
dynamicConfigJson.set("auth_failure_listeners", authFailureListeners);
ok(() -> client.putJson(securityConfigPath("config"), DefaultObjectMapper.writeValueAsString(configJson.get("config"), false)));
ok(() -> client.patch(securityConfigPath(), patch(replaceOp("/config/dynamic/hosts_resolver_mode", "other"))));
}

void verifyNotAllowedMethods(final TestRestClient client) throws Exception {
methodNotAllowed(() -> client.postJson(securityConfigPath(), EMPTY_BODY));
methodNotAllowed(() -> client.delete(securityConfigPath()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.opensearch.security.api.PatchPayloadHelper.patch;
import static org.opensearch.security.api.PatchPayloadHelper.replaceOp;

public class DefaultApiAvailabilityIntegrationTest extends AbstractApiIntegrationTest {

Expand Down Expand Up @@ -49,18 +51,7 @@ private void verifySecurityConfigApi(final TestRestClient client) throws Excepti
methodNotAllowed(() -> client.putJson(apiPath("securityconfig"), EMPTY_BODY));
methodNotAllowed(() -> client.postJson(apiPath("securityconfig"), EMPTY_BODY));
methodNotAllowed(() -> client.delete(apiPath("securityconfig")));
forbidden(
() -> client.patch(
apiPath("securityconfig"),
(builder, params) -> builder.startArray()
.startObject()
.field("op", "replace")
.field("path", "/a/b/c")
.field("value", "other")
.endObject()
.endArray()
)
);
forbidden(() -> client.patch(apiPath("securityconfig"), patch(replaceOp("/a/b/c", "other"))));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* 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.Locale;

import org.opensearch.core.xcontent.ToXContentObject;

interface PatchPayloadHelper extends ToXContentObject {

enum Op {
ADD,
REPLACE,
REMOVE;
}

static <T> ToXContentObject addOp(final String path, final T value) {
return operation(Op.ADD, path, value);
}

static <T> ToXContentObject replaceOp(final String path, final T value) {
return operation(Op.REPLACE, path, value);
}

static ToXContentObject removeOp(final String path) {
return operation(Op.REMOVE, path, null);
}

private static <T> ToXContentObject operation(final Op op, final String path, final T value) {
return (builder, params) -> {
final var opPath = path.startsWith("/") ? path : "/" + path;
builder.startObject().field("op", op.name().toLowerCase(Locale.ROOT)).field("path", opPath);
if (value != null) {
if (value instanceof ToXContentObject) {
builder.field("value", (ToXContentObject) value);
} else if (value instanceof String) {
builder.field("value", (String) value);
} else if (value instanceof Boolean) {
builder.field("value", (Boolean) value);
} else {
throw new IllegalArgumentException("Unsupported java type " + value.getClass());
}
}
return builder.endObject();
};
}

static ToXContentObject patch(final ToXContentObject... operations) {
return (builder, params) -> {
builder.startArray();
for (final var o : operations)
o.toXContent(builder, EMPTY_PARAMS);
return builder.endArray();
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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 com.fasterxml.jackson.databind.JsonNode;
import org.junit.Test;

import org.opensearch.security.dlic.rest.api.Endpoint;
import org.opensearch.test.framework.cluster.TestRestClient;

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() {
return super.apiPath("ssl", "certs");
}

@Test
public void certsInfoForbiddenForRegularUser() throws Exception {
withUser(NEW_USER, client -> forbidden(() -> client.get(sslCertsPath())));
}

@Test
public void certsInfoForbiddenForAdminUser() throws Exception {
withUser(NEW_USER, client -> forbidden(() -> client.get(sslCertsPath())));
}

@Test
public void certsInfoAvailableForTlsAdmin() throws Exception {
withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifySSLCertsInfo);
}

@Test
public void certsInfoAvailableForRestAdmin() throws Exception {
withUser(REST_ADMIN_USER, this::verifySSLCertsInfo);
withUser(REST_API_ADMIN_SSL_INFO, this::verifySSLCertsInfo);
}

private void verifySSLCertsInfo(final TestRestClient client) throws Exception {
final var response = ok(() -> client.get(sslCertsPath()));

final var body = response.bodyAsJsonNode();
assertThat(response.getBody(), body.has("http_certificates_list"));
assertThat(response.getBody(), body.get("http_certificates_list").isArray());
verifyCertsJson(body.get("http_certificates_list").get(0));
assertThat(response.getBody(), body.has("transport_certificates_list"));
assertThat(response.getBody(), body.get("transport_certificates_list").isArray());
verifyCertsJson(body.get("transport_certificates_list").get(0));
}

private void verifyCertsJson(final JsonNode jsonNode) {
assertThat(jsonNode.toPrettyString(), jsonNode.has("issuer_dn"));
assertThat(jsonNode.toPrettyString(), jsonNode.has("subject_dn"));
assertThat(jsonNode.toPrettyString(), jsonNode.get("subject_dn").asText().matches(".*node-\\d.example.com+"));
assertThat(jsonNode.toPrettyString(), jsonNode.get("san").asText().matches(".*node-\\d.example.com.*"));
assertThat(jsonNode.toPrettyString(), jsonNode.has("not_before"));
assertThat(jsonNode.toPrettyString(), jsonNode.has("not_after"));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -66,13 +70,18 @@ public class TestCertificates {
private final CertificateData adminCertificate;
private final List<CertificateData> 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");
Expand Down Expand Up @@ -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 <a href="https://www.rfc-editor.org/rfc/rfc1421.txt">RFC 1421</a>
*/
public File getNodeCertificate(int node) {
Expand All @@ -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)
Expand All @@ -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);
Expand All @@ -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 <code>null</code> to retrieve unencrypted key.
* @return file which contains private key encoded in PEM format, defined
* by <a href="https://www.rfc-editor.org/rfc/rfc1421.txt">RFC 1421</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ public void before() {
}

for (Map.Entry<String, LocalCluster> 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();
Expand Down Expand Up @@ -509,7 +508,7 @@ public Builder defaultConfigurationInitDirectory(String defaultConfigurationInit
public LocalCluster build() {
try {
if (testCertificates == null) {
testCertificates = new TestCertificates();
testCertificates = new TestCertificates(clusterManager.getNodes());
}
clusterName += "_" + num.incrementAndGet();
Settings settings = nodeOverrideSettingsBuilder.build();
Expand Down
Loading