Skip to content

Commit

Permalink
Test related to authentication with x.509 certificates. (#2227)
Browse files Browse the repository at this point in the history
Signed-off-by: Lukasz Soszynski <[email protected]>
  • Loading branch information
lukasz-soszynski-eliatra authored Nov 22, 2022
1 parent 25ea092 commit 7cad5e4
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright OpenSearch Contributors
* 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.
*
*/
package org.opensearch.security.http;

import java.util.List;
import java.util.Map;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;

import org.opensearch.test.framework.RolesMapping;
import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain;
import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator;
import org.opensearch.test.framework.TestSecurityConfig.Role;
import org.opensearch.test.framework.TestSecurityConfig.User;
import org.opensearch.test.framework.certificate.CertificateData;
import org.opensearch.test.framework.certificate.TestCertificates;
import org.opensearch.test.framework.cluster.ClusterManager;
import org.opensearch.test.framework.cluster.LocalCluster;
import org.opensearch.test.framework.cluster.TestRestClient;
import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse;

import static org.apache.hc.core5.http.HttpStatus.SC_OK;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL;
import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS;

@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class)
@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class CertificateAuthenticationTest {

private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS);

public static final String POINTER_BACKEND_ROLES = "/backend_roles";
public static final String POINTER_ROLES = "/roles";

private static final String USER_SPOCK = "spock";
private static final String USER_KIRK = "kirk";

private static final String BACKEND_ROLE_BRIDGE = "bridge";
private static final String BACKEND_ROLE_CAPTAIN = "captain";

private static final Role ROLE_ALL_INDEX_SEARCH = new Role("all-index-search").indexPermissions("indices:data/read/search")
.on("*");

private static final Map<String, Object> CERT_AUTH_CONFIG = Map.of(
"username_attribute", "cn",
"roles_attribute", "ou"
);

@ClassRule
public static final LocalCluster cluster = new LocalCluster.Builder()
.nodeSettings(Map.of("plugins.security.ssl.http.clientauth_mode", "OPTIONAL"))
.clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false)
.authc(new AuthcDomain("clientcert_auth_domain", -1, true)
.httpAuthenticator(new HttpAuthenticator("clientcert").challenge(false)
.config(CERT_AUTH_CONFIG)).backend("noop"))
.authc(AUTHC_HTTPBASIC_INTERNAL).roles(ROLE_ALL_INDEX_SEARCH).users(USER_ADMIN)
.rolesMapping(new RolesMapping(ROLE_ALL_INDEX_SEARCH).backendRoles(BACKEND_ROLE_BRIDGE)).build();

private static final TestCertificates TEST_CERTIFICATES = cluster.getTestCertificates();

@Test
public void shouldAuthenticateUserWithBasicAuthWhenCertificateAuthenticationIsConfigured() {
try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(SC_OK);
}
}

@Test
public void shouldAuthenticateUserWithCertificate_positiveUserSpoke() {
CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_SPOCK);
try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) {

client.assertCorrectCredentials(USER_SPOCK);
}
}

@Test
public void shouldAuthenticateUserWithCertificate_positiveUserKirk() {
CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_KIRK);
try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) {

client.assertCorrectCredentials(USER_KIRK);
}
}

@Test
public void shouldAuthenticateUserWithCertificate_negative() {
CertificateData untrustedUserCertificate = TEST_CERTIFICATES.createSelfSignedCertificate("CN=untrusted");
try (TestRestClient client = cluster.getRestClient(untrustedUserCertificate)) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(401);
}
}

@Test
public void shouldRetrieveBackendRoleFromCertificate_positiveRoleBridge() {
CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_KIRK);
try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(200);
List<String> backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES);
assertThat(backendRoles, hasSize(1));
assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_BRIDGE));
List<String> roles = response.getTextArrayFromJsonBody(POINTER_ROLES);
assertThat(roles, hasSize(1));
assertThat(roles, containsInAnyOrder(ROLE_ALL_INDEX_SEARCH.getName()));
}
}

@Test
public void shouldRetrieveBackendRoleFromCertificate_positiveRoleCaptain() {
CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_CAPTAIN, USER_KIRK);
try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) {

HttpResponse response = client.getAuthInfo();

response.assertStatusCode(200);
List<String> backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES);
assertThat(backendRoles, hasSize(1));
assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_CAPTAIN));
List<String> roles = response.getTextArrayFromJsonBody(POINTER_ROLES);
assertThat(roles, hasSize(0));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright OpenSearch Contributors
* 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.
*
*/
package org.opensearch.test.framework;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.opensearch.common.xcontent.ToXContentObject;
import org.opensearch.common.xcontent.XContentBuilder;
import org.opensearch.test.framework.TestSecurityConfig.Role;

import static java.util.Objects.requireNonNull;

public class RolesMapping implements ToXContentObject {
private String roleName;
private List<String> backendRoles;
private List<String> hosts;
private List<String> users;

private boolean reserved = false;

public RolesMapping(Role role) {
requireNonNull(role);
this.roleName = requireNonNull(role.getName());
this.backendRoles = new ArrayList<>();
}

public RolesMapping backendRoles(String...backendRoles) {
this.backendRoles.addAll(Arrays.asList(backendRoles));
return this;
}

public RolesMapping hosts(List<String> hosts) {
this.hosts = hosts;
return this;
}

public RolesMapping users(List<String> users) {
this.users = users;
return this;
}

public RolesMapping reserved(boolean reserved) {
this.reserved = reserved;
return this;
}

public String getRoleName() {
return roleName;
}

@Override
public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException {
xContentBuilder.startObject();
xContentBuilder.field("reserved", reserved);
xContentBuilder.field("backend_roles", backendRoles);
xContentBuilder.endObject();
return xContentBuilder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ public class TestSecurityConfig {
private Config config = new Config();
private Map<String, User> internalUsers = new LinkedHashMap<>();
private Map<String, Role> roles = new LinkedHashMap<>();

private AuditConfiguration auditConfiguration;
private Map<String, RolesMapping> rolesMapping = new LinkedHashMap<>();

private String indexName = ".opendistro_security";

Expand Down Expand Up @@ -126,6 +126,9 @@ public TestSecurityConfig user(User user) {

public TestSecurityConfig roles(Role... roles) {
for (Role role : roles) {
if(this.roles.containsKey(role.name)) {
throw new IllegalStateException("Role with name " + role.name + " is already defined");
}
this.roles.put(role.name, role);
}

Expand All @@ -137,6 +140,17 @@ public TestSecurityConfig audit(AuditConfiguration auditConfiguration) {
return this;
}

public TestSecurityConfig rolesMapping(RolesMapping...mappings) {
for (RolesMapping mapping : mappings) {
String roleName = mapping.getRoleName();
if(rolesMapping.containsKey(roleName)) {
throw new IllegalArgumentException("Role mapping " + roleName + " already exists");
}
this.rolesMapping.put(roleName, mapping);
}
return this;
}

public static class Config implements ToXContentObject {
private boolean anonymousAuth;

Expand Down Expand Up @@ -545,7 +559,7 @@ public void initIndex(Client client) {
}
writeConfigToIndex(client, CType.ROLES, roles);
writeConfigToIndex(client, CType.INTERNALUSERS, internalUsers);
writeEmptyConfigToIndex(client, CType.ROLESMAPPING);
writeConfigToIndex(client, CType.ROLESMAPPING, rolesMapping);
writeEmptyConfigToIndex(client, CType.ACTIONGROUPS);
writeEmptyConfigToIndex(client, CType.TENANTS);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@

package org.opensearch.test.framework.certificate;

import java.security.Key;
import java.security.KeyPair;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;

/**
* The class contains all data related to Certificate including private key which is considered to be a secret.
*/
class CertificateData {
public class CertificateData {

private final X509CertificateHolder certificate;
private final KeyPair keyPair;
Expand All @@ -53,6 +57,14 @@ public String certificateInPemFormat() {
return PemConverter.toPem(certificate);
}

public X509Certificate certificate() {
try {
return new JcaX509CertificateConverter().getCertificate(certificate);
} catch (CertificateException e) {
throw new RuntimeException("Cannot retrieve certificate", e);
}
}

/**
* It returns the private key associated with certificate encoded in PEM format. PEM format is defined by
* <a href="https://www.rfc-editor.org/rfc/rfc1421.txt">RFC 1421</a>.
Expand All @@ -70,4 +82,8 @@ X500Name getCertificateSubject() {
KeyPair getKeyPair() {
return keyPair;
}

public Key getKey() {
return keyPair.getPrivate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ private CertificateData createAdminCertificate() {
.issueSelfSignedCertificate(metadata);
}

public CertificateData createSelfSignedCertificate(String distinguishedName) {
CertificateMetadata metadata = CertificateMetadata.basicMetadata(distinguishedName, CERTIFICATE_VALIDITY_DAYS);
return CertificatesIssuerFactory
.rsaBaseCertificateIssuer()
.issueSelfSignedCertificate(metadata);
}

/**
* It returns the most trusted certificate. Certificates for nodes and users are derived from this certificate.
* @return file which contains certificate in PEM format, defined by <a href="https://www.rfc-editor.org/rfc/rfc1421.txt">RFC 1421</a>
Expand Down Expand Up @@ -131,6 +138,15 @@ private CertificateData createNodeCertificate(Integer node) {
.issueSignedCertificate(metadata, caCertificate);
}

public CertificateData issueUserCertificate(String organizationUnit, String username) {
String subject = String.format("DC=de,L=test,O=users,OU=%s,CN=%s", organizationUnit, username);
CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS)
.withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH);
return CertificatesIssuerFactory
.rsaBaseCertificateIssuer()
.issueSignedCertificate(metadata, caCertificate);
}

/**
* It returns private key associated with node certificate returned by method {@link #getNodeCertificate(int)}
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.opensearch.security.support.ConfigConstants;
import org.opensearch.test.framework.AuditConfiguration;
import org.opensearch.test.framework.AuthFailureListeners;
import org.opensearch.test.framework.RolesMapping;
import org.opensearch.test.framework.TestIndex;
import org.opensearch.test.framework.TestSecurityConfig;
import org.opensearch.test.framework.TestSecurityConfig.Role;
Expand Down Expand Up @@ -365,6 +366,11 @@ public Builder roles(Role... roles) {
return this;
}

public Builder rolesMapping(RolesMapping...mappings) {
testSecurityConfig.rolesMapping(mappings);
return this;
}

public Builder authc(TestSecurityConfig.AuthcDomain authc) {
testSecurityConfig.authc(authc);
return this;
Expand Down
Loading

0 comments on commit 7cad5e4

Please sign in to comment.