From 01e18f0cece5742739c04a532c43976b47f92267 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste WATENBERG Date: Fri, 29 Mar 2024 16:29:13 +0100 Subject: [PATCH] Add upsert method for certificate and review response format to add common name --- .../GroupWithLinkLDAPStorageMapper.java | 2 - .../truststore/CertificateRepresentation.java | 2 +- .../JpaCertificateTruststoreProvider.java | 41 ++++++++--- .../truststore/TruststoreAdminResource.java | 34 ++++++++- .../scality/keycloak/GroupWithLinkTest.java | 1 - .../com/scality/keycloak/TrustStoreTest.java | 72 +++++++++++++------ 6 files changed, 118 insertions(+), 34 deletions(-) diff --git a/src/main/java/com/scality/keycloak/groupFederationLink/GroupWithLinkLDAPStorageMapper.java b/src/main/java/com/scality/keycloak/groupFederationLink/GroupWithLinkLDAPStorageMapper.java index a4904a5..f3f17ca 100644 --- a/src/main/java/com/scality/keycloak/groupFederationLink/GroupWithLinkLDAPStorageMapper.java +++ b/src/main/java/com/scality/keycloak/groupFederationLink/GroupWithLinkLDAPStorageMapper.java @@ -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; diff --git a/src/main/java/com/scality/keycloak/truststore/CertificateRepresentation.java b/src/main/java/com/scality/keycloak/truststore/CertificateRepresentation.java index 4a8fc84..b6e83bb 100644 --- a/src/main/java/com/scality/keycloak/truststore/CertificateRepresentation.java +++ b/src/main/java/com/scality/keycloak/truststore/CertificateRepresentation.java @@ -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) { } diff --git a/src/main/java/com/scality/keycloak/truststore/JpaCertificateTruststoreProvider.java b/src/main/java/com/scality/keycloak/truststore/JpaCertificateTruststoreProvider.java index 727dbe9..83e7cc1 100644 --- a/src/main/java/com/scality/keycloak/truststore/JpaCertificateTruststoreProvider.java +++ b/src/main/java/com/scality/keycloak/truststore/JpaCertificateTruststoreProvider.java @@ -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; @@ -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 @@ -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)); diff --git a/src/main/java/com/scality/keycloak/truststore/TruststoreAdminResource.java b/src/main/java/com/scality/keycloak/truststore/TruststoreAdminResource.java index 2bc332d..9c57baf 100644 --- a/src/main/java/com/scality/keycloak/truststore/TruststoreAdminResource.java +++ b/src/main/java/com/scality/keycloak/truststore/TruststoreAdminResource.java @@ -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; @@ -55,7 +58,16 @@ public Stream 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()); } @@ -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 diff --git a/src/test/java/com/scality/keycloak/GroupWithLinkTest.java b/src/test/java/com/scality/keycloak/GroupWithLinkTest.java index 6f935c8..2654be8 100644 --- a/src/test/java/com/scality/keycloak/GroupWithLinkTest.java +++ b/src/test/java/com/scality/keycloak/GroupWithLinkTest.java @@ -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")) { diff --git a/src/test/java/com/scality/keycloak/TrustStoreTest.java b/src/test/java/com/scality/keycloak/TrustStoreTest.java index b38aae1..6d4cfa0 100644 --- a/src/test/java/com/scality/keycloak/TrustStoreTest.java +++ b/src/test/java/com/scality/keycloak/TrustStoreTest.java @@ -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; @@ -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(); @@ -82,24 +85,57 @@ private Boolean isLDAPWithStartTlsConnectionWorking(KeycloakContainer keycloak) return responseCode == 204; } - private HttpURLConnection getCertificatesConnection(KeycloakContainer keycloak) throws IOException { + private List 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 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 { @@ -114,27 +150,21 @@ 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 @@ -142,13 +172,15 @@ public void truststore_provider_should_be_taken_in_account_when_setup_ldap() assertTrue(isLDAPWithStartTlsConnectionWorking(keycloak)); // V - conn = getCertificatesConnection(keycloak); - ObjectMapper objectMapper = new ObjectMapper(); - List certificates = objectMapper.readValue(conn.getInputStream(), - new TypeReference<>() { - }); + List 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()); } }