Skip to content

Commit

Permalink
Add upsert method for certificate and review response format to add c…
Browse files Browse the repository at this point in the history
…ommon name
  • Loading branch information
JBWatenbergScality committed Apr 2, 2024
1 parent b4d5f7c commit 01e18f0
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.scality.keycloak.groupFederationLink;

import java.util.Collections;

import org.keycloak.component.ComponentModel;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.GroupModel;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.scality.keycloak.truststore;

public record CertificateRepresentation(String id, String alias, String certificate, Boolean isRootCA) {
public record CertificateRepresentation(String alias, String certificate, String commonName) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;

import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x500.style.IETFUtils;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.jboss.logging.Logger;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.models.KeycloakSession;
Expand Down Expand Up @@ -45,13 +51,27 @@ public void close() {
}

private CertificateRepresentation toCertificateRepresentation(TruststoreEntity entity) {
CertificateRepresentation certificate = new CertificateRepresentation(
entity.getId(),
entity.getAlias(),
entity.getCertificate(),
entity.isRootCA());
X509Certificate x509Certificate = toX509Certificate(entity.getCertificate());
try {
X500Name x500name = new JcaX509CertificateHolder(x509Certificate).getSubject();
RDN cn = x500name.getRDNs(BCStyle.CN)[0];

CertificateRepresentation certificate = new CertificateRepresentation(
entity.getAlias(),
entity.getCertificate(),
IETFUtils.valueToString(cn.getFirst().getValue()));

return certificate;

} catch (CertificateEncodingException e) {
logger.error("certificate " + entity.getAlias() + " is invalid", e);
CertificateRepresentation certificate = new CertificateRepresentation(
entity.getAlias(),
entity.getCertificate(),
"");
return certificate;
}

return certificate;
}

@Override
Expand Down Expand Up @@ -127,11 +147,14 @@ public CertificateRepresentation addCertificate(String alias, String certificate

@Override
public CertificateRepresentation updateCertificate(String alias, String certificate) {
CertificateRepresentation certificateRepresentation = this.getCertificate(alias);
X509Certificate x509Certificate = toX509Certificate(certificate);
TruststoreEntity storedCertificate = getEntityManager()
.createNamedQuery("findByAlias", TruststoreEntity.class)
.setParameter("alias", alias)
.getSingleResult();
X509Certificate x509Certificate = toX509Certificate(storedCertificate.getCertificate());

TruststoreEntity entity = new TruststoreEntity();
entity.setId(certificateRepresentation.id());
entity.setId(storedCertificate.getId());
entity.setAlias(alias);
entity.setCertificate(certificate);
entity.setRootCA(isSelfSigned(x509Certificate));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;

import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
Expand Down Expand Up @@ -55,7 +58,16 @@ public Stream<CertificateRepresentation> getCertificates() {
public void addCertificate(CertificateRepresentation certificate) throws CertificateException {
auth.realm().requireManageRealm();

session.getProvider(CertificateTruststoreProvider.class).addCertificate(certificate.alias(),
CertificateTruststoreProvider provider = session.getProvider(CertificateTruststoreProvider.class);
try {
if (provider.getCertificate(certificate.alias()) != null) {
throw new ClientErrorException("Certificate already exists", 409);
}
} catch (NotFoundException e) {
// ignore
}

provider.addCertificate(certificate.alias(),
certificate.certificate());
}

Expand All @@ -71,6 +83,26 @@ public CertificateRepresentation getCertificate(String alias) {
return session.getProvider(CertificateTruststoreProvider.class).getCertificate(alias);
}

@Path("{alias}")
@PUT
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = OPEN_API_TAG)
@Operation(summary = "Upsert certificate Returns a trusted certificate.")
public CertificateRepresentation upsertCertificate(String alias, CertificateRepresentation certificate) {
auth.realm().requireManageRealm();

CertificateTruststoreProvider provider = session.getProvider(CertificateTruststoreProvider.class);
try {
provider.getCertificate(alias);
} catch (NotFoundException e) {
return provider.addCertificate(alias, certificate.certificate());
}

return provider.updateCertificate(alias, certificate.certificate());
}

@Path("{alias}")
@PATCH
@NoCache
Expand Down
1 change: 0 additions & 1 deletion src/test/java/com/scality/keycloak/GroupWithLinkTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,6 @@ public void groups_with_links_should_be_returned_when_listing_groups()

try (KeycloakContainer keycloak = FullImageName.createContainer()
.withNetwork(network)
.withEnv("KC_LOG_LEVEL", "DEBUG")
.withStartupTimeout(Duration.ofMinutes(5))
.withLogConsumer(new Slf4jLogConsumer(logger))
.withProviderClassesFrom("target/classes")) {
Expand Down
72 changes: 52 additions & 20 deletions src/test/java/com/scality/keycloak/TrustStoreTest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.scality.keycloak;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.time.Duration;
import java.util.List;

import org.junit.Test;
Expand All @@ -32,6 +34,7 @@ public class TrustStoreTest {
@Test
public void truststore_provider_should_be_registered() throws IOException {
try (KeycloakContainer keycloak = FullImageName.createContainer()
.withStartupTimeout(Duration.ofMinutes(5))
.withProviderClassesFrom("target/classes")) {
keycloak.start();

Expand Down Expand Up @@ -82,24 +85,57 @@ private Boolean isLDAPWithStartTlsConnectionWorking(KeycloakContainer keycloak)
return responseCode == 204;
}

private HttpURLConnection getCertificatesConnection(KeycloakContainer keycloak) throws IOException {
private List<CertificateRepresentation> getCertificates(KeycloakContainer keycloak) throws IOException {
URL url = new URL(keycloak.getAuthServerUrl() + "/admin/realms/master/certificates");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Authorization", "Bearer " + getToken(keycloak));
return conn;
ObjectMapper objectMapper = new ObjectMapper();
List<CertificateRepresentation> certificates = objectMapper.readValue(conn.getInputStream(),
new TypeReference<>() {
});
return certificates;
}

private HttpURLConnection addCertificateConnection(KeycloakContainer keycloak) throws IOException {
private HttpURLConnection addCertificateConnection(KeycloakContainer keycloak, String alias, String certificate)
throws IOException {
URL url = new URL(keycloak.getAuthServerUrl() + "/admin/realms/master/certificates");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Authorization", "Bearer " + getToken(keycloak));
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
conn.getOutputStream().write(("{\n" + //
" \"alias\": \"" + alias + "\",\n" + //
" \"certificate\": \"" + certificate + "\"\n" +
"}").getBytes());
return conn;
}

private CertificateRepresentation upsertCertificateConnection(KeycloakContainer keycloak, String alias,
String certificate)
throws IOException {
URL url = new URL(keycloak.getAuthServerUrl() + "/admin/realms/master/certificates/" + alias);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("PUT");
conn.setRequestProperty("Authorization", "Bearer " + getToken(keycloak));
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
conn.getOutputStream().write(("{\n" + //
" \"certificate\": \"" + certificate + "\"\n" +
"}").getBytes());
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(conn.getInputStream(), CertificateRepresentation.class);
}

private void removeCertificateConnection(KeycloakContainer keycloak, String alias) throws IOException {
URL url = new URL(keycloak.getAuthServerUrl() + "/admin/realms/master/certificates/" + alias);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("DELETE");
conn.setRequestProperty("Authorization", "Bearer " + getToken(keycloak));
conn.getResponseCode();
}

@Test
public void truststore_provider_should_be_taken_in_account_when_setup_ldap()
throws IOException, UnsupportedOperationException, InterruptedException {
Expand All @@ -114,41 +150,37 @@ public void truststore_provider_should_be_taken_in_account_when_setup_ldap()
.withEnv("LDAP_TLS_VERIFY_CLIENT", "try")
.withExposedPorts(389, 636)) {
openldap.start();
Container.ExecResult base64CaResult = openldap.execInContainer("base64", "-w", "0",
"/container/service/slapd/assets/certs/ca.crt");
String base64Ca = base64CaResult.getStdout();

try (KeycloakContainer keycloak = FullImageName.createContainer()
.withNetwork(network)
.withStartupTimeout(Duration.ofMinutes(5))
.withLogConsumer(new Slf4jLogConsumer(logger))
.withProviderClassesFrom("target/classes")) {
keycloak.start();

// V
assertFalse(isLDAPWithStartTlsConnectionWorking(keycloak));

// E
Container.ExecResult base64CaResult = openldap.execInContainer("base64", "-w", "0",
"/container/service/slapd/assets/certs/ca.crt");
String base64Ca = base64CaResult.getStdout();

String alias = "ldap.local";
// Post on certificates endpoint to trust the CA
HttpURLConnection conn = addCertificateConnection(keycloak);
conn.getOutputStream().write(("{\n" + //
" \"alias\": \"ldap.local\",\n" + //
" \"certificate\": \"" + base64Ca + "\"\n" +
"}").getBytes());
HttpURLConnection conn = addCertificateConnection(keycloak, alias, base64Ca);
int responseCode = conn.getResponseCode();

// V
assertTrue(responseCode == 204);
assertTrue(isLDAPWithStartTlsConnectionWorking(keycloak));

// V
conn = getCertificatesConnection(keycloak);
ObjectMapper objectMapper = new ObjectMapper();
List<CertificateRepresentation> certificates = objectMapper.readValue(conn.getInputStream(),
new TypeReference<>() {
});
List<CertificateRepresentation> certificates = getCertificates(keycloak);

assertFalse(certificates.isEmpty());
assertTrue(certificates.stream().anyMatch(it -> it.alias().equals("ldap.local")));

// E
removeCertificateConnection(keycloak, alias);
CertificateRepresentation upsertedCertificate = upsertCertificateConnection(keycloak, alias, base64Ca);
assertEquals("docker-light-baseimage", upsertedCertificate.commonName());
}
}

Expand Down

0 comments on commit 01e18f0

Please sign in to comment.