From a4a1f4cc6ceb60926c6263c5087fb9fe4f4657a0 Mon Sep 17 00:00:00 2001 From: Guillaume Duval <117720964+g-duval@users.noreply.github.com> Date: Fri, 1 Dec 2023 10:56:26 +0100 Subject: [PATCH] [RHCLOUD-29487] Record gateway certificate details on database (#2366) * [RHCLOUD-29487] Record gateway certificate details on database --------- Co-authored-by: Gwenneg Lepage --- .../X509CertificateRepository.java | 66 ++++++++ .../notifications/models/X509Certificate.java | 95 +++++++++++ .../routers/internal/ValidationResource.java | 25 +++ .../internal/X509CertificateResource.java | 56 +++++++ .../X509CertificatesResourceTest.java | 154 ++++++++++++++++++ ...UD-29487_add_gateway_certificate_table.sql | 11 ++ 6 files changed, 407 insertions(+) create mode 100644 backend/src/main/java/com/redhat/cloud/notifications/db/repositories/X509CertificateRepository.java create mode 100644 backend/src/main/java/com/redhat/cloud/notifications/models/X509Certificate.java create mode 100644 backend/src/main/java/com/redhat/cloud/notifications/routers/internal/X509CertificateResource.java create mode 100644 backend/src/test/java/com/redhat/cloud/notifications/routers/internal/X509CertificatesResourceTest.java create mode 100644 database/src/main/resources/db/migration/V1.89.0__RHCLOUD-29487_add_gateway_certificate_table.sql diff --git a/backend/src/main/java/com/redhat/cloud/notifications/db/repositories/X509CertificateRepository.java b/backend/src/main/java/com/redhat/cloud/notifications/db/repositories/X509CertificateRepository.java new file mode 100644 index 0000000000..83e706e044 --- /dev/null +++ b/backend/src/main/java/com/redhat/cloud/notifications/db/repositories/X509CertificateRepository.java @@ -0,0 +1,66 @@ +package com.redhat.cloud.notifications.db.repositories; + +import com.redhat.cloud.notifications.models.Application; +import com.redhat.cloud.notifications.models.X509Certificate; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.NoResultException; +import jakarta.transaction.Transactional; +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class X509CertificateRepository { + + @Inject + EntityManager entityManager; + + @Inject + ApplicationRepository applicationRepository; + + @Transactional + public X509Certificate createCertificate(X509Certificate gatewayCertificate) { + Application application = applicationRepository.getApplication(gatewayCertificate.getBundle(), gatewayCertificate.getApplication()); + gatewayCertificate.setCertificateApplication(application); + entityManager.persist(gatewayCertificate); + return gatewayCertificate; + } + + + public Optional findCertificate(String bundle, String application, String subjectDn) { + final String query = "SELECT gc FROM X509Certificate gc where gc.certificateApplication.bundle.name = :bundle " + + "AND gc.certificateApplication.name = :application " + + "AND gc.subjectDn = :subjectDn"; + try { + return Optional.of(this.entityManager + .createQuery(query, X509Certificate.class) + .setParameter("bundle", bundle) + .setParameter("application", application) + .setParameter("subjectDn", subjectDn) + .getSingleResult()); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + @Transactional + public boolean updateCertificate(UUID id, X509Certificate gatewayCertificate) { + String hql = "UPDATE X509Certificate SET subjectDn = :subjectDn, sourceEnvironment = :sourceEnvironment WHERE id = :id"; + int rowCount = entityManager.createQuery(hql) + .setParameter("subjectDn", gatewayCertificate.getSubjectDn()) + .setParameter("sourceEnvironment", gatewayCertificate.getSourceEnvironment()) + .setParameter("id", id) + .executeUpdate(); + return rowCount > 0; + } + + @Transactional + public boolean deleteCertificate(UUID id) { + String deleteHql = "DELETE FROM X509Certificate WHERE id = :id"; + int rowCount = entityManager.createQuery(deleteHql) + .setParameter("id", id) + .executeUpdate(); + return rowCount > 0; + } +} diff --git a/backend/src/main/java/com/redhat/cloud/notifications/models/X509Certificate.java b/backend/src/main/java/com/redhat/cloud/notifications/models/X509Certificate.java new file mode 100644 index 0000000000..04b7358def --- /dev/null +++ b/backend/src/main/java/com/redhat/cloud/notifications/models/X509Certificate.java @@ -0,0 +1,95 @@ +package com.redhat.cloud.notifications.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import jakarta.validation.constraints.NotNull; +import java.util.UUID; + +import static com.fasterxml.jackson.annotation.JsonProperty.Access.READ_ONLY; + +@Entity +@Table(name = "x509_certificate") +@JsonNaming(SnakeCaseStrategy.class) +public class X509Certificate { + @Id + @GeneratedValue + @JsonProperty(access = READ_ONLY) + private UUID id; + + @NotNull + private String subjectDn; + + @NotNull + private String sourceEnvironment; + + @NotNull + @Transient + private String bundle; + + @NotNull + @Transient + private String application; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "application_id") + @JsonIgnore + private Application certificateApplication; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getSubjectDn() { + return subjectDn; + } + + public void setSubjectDn(String subjectDn) { + this.subjectDn = subjectDn; + } + + public String getSourceEnvironment() { + return sourceEnvironment; + } + + public void setSourceEnvironment(String environment) { + this.sourceEnvironment = environment; + } + + public String getBundle() { + return bundle; + } + + public void setBundle(String bundle) { + this.bundle = bundle; + } + + public String getApplication() { + return application; + } + + public void setApplication(String application) { + this.application = application; + } + + public Application getCertificateApplication() { + return certificateApplication; + } + + public void setCertificateApplication(Application certificateApplication) { + this.certificateApplication = certificateApplication; + } +} diff --git a/backend/src/main/java/com/redhat/cloud/notifications/routers/internal/ValidationResource.java b/backend/src/main/java/com/redhat/cloud/notifications/routers/internal/ValidationResource.java index 64b6cd0af7..f438c33a5d 100644 --- a/backend/src/main/java/com/redhat/cloud/notifications/routers/internal/ValidationResource.java +++ b/backend/src/main/java/com/redhat/cloud/notifications/routers/internal/ValidationResource.java @@ -4,12 +4,15 @@ import com.redhat.cloud.event.parser.ConsoleCloudEventParser; import com.redhat.cloud.event.parser.exceptions.ConsoleCloudEventValidationException; import com.redhat.cloud.notifications.db.repositories.ApplicationRepository; +import com.redhat.cloud.notifications.db.repositories.X509CertificateRepository; import com.redhat.cloud.notifications.ingress.Parser; import com.redhat.cloud.notifications.ingress.ParsingException; import com.redhat.cloud.notifications.models.EventType; +import com.redhat.cloud.notifications.models.X509Certificate; import com.redhat.cloud.notifications.routers.internal.models.MessageValidationResponse; import jakarta.inject.Inject; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.ForbiddenException; import jakarta.ws.rs.GET; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -20,6 +23,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.jboss.resteasy.reactive.RestQuery; +import java.util.Optional; import static com.redhat.cloud.notifications.Constants.API_INTERNAL; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; @@ -30,10 +34,14 @@ public class ValidationResource { private static final String EVENT_TYPE_NOT_FOUND_MSG = "No event type found for [bundle=%s, application=%s, eventType=%s]"; + private static final String CERTIFICATE_NOT_AUTHORIZED_MSG = "This certificate is not authorized for [bundle=%s, application=%s]"; @Inject ApplicationRepository applicationRepository; + @Inject + X509CertificateRepository x509CertificateRepository; + ConsoleCloudEventParser consoleCloudEventParser = new ConsoleCloudEventParser(); @GET @@ -112,4 +120,21 @@ public Response validateConsoleCloudEvent(String action) { return Response.ok().build(); } + + @GET + @Path("/certificate") + @Produces(APPLICATION_JSON) + @APIResponses({ + @APIResponse(responseCode = "200", description = "This certificate is valid for this bundle and application"), + @APIResponse(responseCode = "403", description = "This certificate is not valid for this bundle and application") + }) + public X509Certificate validateCertificateAccordingBundleAndApp(@RestQuery String bundle, @RestQuery String application, @RestQuery String certificateSubjectDn) { + Optional gatewayCertificate = x509CertificateRepository.findCertificate(bundle, application, certificateSubjectDn); + if (gatewayCertificate.isEmpty()) { + String message = String.format(CERTIFICATE_NOT_AUTHORIZED_MSG, bundle, application); + throw new ForbiddenException(message); + } else { + return gatewayCertificate.get(); + } + } } diff --git a/backend/src/main/java/com/redhat/cloud/notifications/routers/internal/X509CertificateResource.java b/backend/src/main/java/com/redhat/cloud/notifications/routers/internal/X509CertificateResource.java new file mode 100644 index 0000000000..59042355bd --- /dev/null +++ b/backend/src/main/java/com/redhat/cloud/notifications/routers/internal/X509CertificateResource.java @@ -0,0 +1,56 @@ +package com.redhat.cloud.notifications.routers.internal; + +import com.redhat.cloud.notifications.db.repositories.X509CertificateRepository; +import com.redhat.cloud.notifications.models.X509Certificate; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +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.Response; +import org.jboss.resteasy.reactive.RestPath; +import java.util.UUID; + +import static com.redhat.cloud.notifications.Constants.API_INTERNAL; +import static com.redhat.cloud.notifications.auth.ConsoleIdentityProvider.RBAC_INTERNAL_ADMIN; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static jakarta.ws.rs.core.MediaType.TEXT_PLAIN; + +@Path(API_INTERNAL + "/x509Certificates") +@RolesAllowed(RBAC_INTERNAL_ADMIN) +public class X509CertificateResource { + + @Inject + X509CertificateRepository x509CertificateRepository; + + @POST + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON) + public X509Certificate createCertificate(@NotNull @Valid X509Certificate certificate) { + return x509CertificateRepository.createCertificate(certificate); + } + + @PUT + @Path("/{certificateId}") + @Consumes(APPLICATION_JSON) + @Produces(TEXT_PLAIN) + public Response updateCertificate(@RestPath UUID certificateId, @NotNull X509Certificate certificate) { + boolean updated = x509CertificateRepository.updateCertificate(certificateId, certificate); + if (updated) { + return Response.ok().build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + + @DELETE + @Path("/{certificateId}") + public boolean deleteCertificate(@RestPath UUID certificateId) { + return x509CertificateRepository.deleteCertificate(certificateId); + } +} diff --git a/backend/src/test/java/com/redhat/cloud/notifications/routers/internal/X509CertificatesResourceTest.java b/backend/src/test/java/com/redhat/cloud/notifications/routers/internal/X509CertificatesResourceTest.java new file mode 100644 index 0000000000..dd48d66a7b --- /dev/null +++ b/backend/src/test/java/com/redhat/cloud/notifications/routers/internal/X509CertificatesResourceTest.java @@ -0,0 +1,154 @@ +package com.redhat.cloud.notifications.routers.internal; + +import com.redhat.cloud.notifications.Json; +import com.redhat.cloud.notifications.TestHelpers; +import com.redhat.cloud.notifications.TestLifecycleManager; +import com.redhat.cloud.notifications.db.DbIsolatedTest; +import com.redhat.cloud.notifications.models.X509Certificate; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.Header; +import io.vertx.core.json.JsonObject; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; +import java.util.UUID; + +import static com.redhat.cloud.notifications.CrudTestHelpers.createApp; +import static com.redhat.cloud.notifications.CrudTestHelpers.createBundle; +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static jakarta.ws.rs.core.Response.Status.FORBIDDEN; +import static jakarta.ws.rs.core.Response.Status.OK; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +@QuarkusTest +@QuarkusTestResource(TestLifecycleManager.class) +public class X509CertificatesResourceTest extends DbIsolatedTest { + + + @ConfigProperty(name = "internal.admin-role") + String adminRole; + + @Test + void testCrudX509Certificate() { + Header adminIdentity = TestHelpers.createTurnpikeIdentityHeader("user", adminRole); + Header intenralUserIdentity = TestHelpers.createTurnpikeIdentityHeader("internal_user", "not_use"); + + final String bundleName = "bundle-name-x509-certificate"; + final String applicationName = "application-name-x509-certificate"; + String bundleId = createBundle(adminIdentity, bundleName, "Certificate Bundle", OK.getStatusCode()).get(); + createApp(adminIdentity, bundleId, applicationName, "Certificate Application", null, OK.getStatusCode()); + + checkGatewayCertificate(intenralUserIdentity, bundleName, applicationName, "certificate data", FORBIDDEN); + + X509Certificate x509Certificate = new X509Certificate(); + x509Certificate.setSubjectDn("certificate data"); + x509Certificate.setBundle(bundleName); + x509Certificate.setApplication(applicationName); + x509Certificate.setSourceEnvironment("stage"); + JsonObject createdCertificateId = new JsonObject(given() + .header(adminIdentity) + .contentType(JSON) + .body(Json.encode(x509Certificate)) + .when() + .post("/internal/x509Certificates") + .then() + .statusCode(OK.getStatusCode()) + .extract().asString()); + + String certificateId = createdCertificateId.getString("id"); + assertNotNull(certificateId); + + x509Certificate = checkGatewayCertificate(intenralUserIdentity, bundleName, applicationName, "certificate data", OK); + assertEquals("stage", x509Certificate.getSourceEnvironment()); + + X509Certificate certificateUpdated = new X509Certificate(); + certificateUpdated.setSubjectDn("certificate data updated"); + certificateUpdated.setSourceEnvironment("prod"); + + given() + .header(adminIdentity) + .contentType(JSON) + .pathParam("certificateId", certificateId) + .body(Json.encode(certificateUpdated)) + .when() + .put("/internal/x509Certificates/{certificateId}") + .then() + .statusCode(OK.getStatusCode()); + + checkGatewayCertificate(intenralUserIdentity, bundleName, applicationName, "certificate data", FORBIDDEN); + x509Certificate = checkGatewayCertificate(intenralUserIdentity, bundleName, applicationName, "certificate data updated", OK); + assertEquals("prod", x509Certificate.getSourceEnvironment()); + assertEquals("certificate data updated", x509Certificate.getSubjectDn()); + + given() + .header(adminIdentity) + .contentType(JSON) + .pathParam("certificateId", certificateId) + .body(Json.encode(certificateUpdated)) + .when() + .delete("/internal/x509Certificates/{certificateId}") + .then() + .statusCode(OK.getStatusCode()); + + checkGatewayCertificate(intenralUserIdentity, bundleName, applicationName, "certificate data updated", FORBIDDEN); + } + + @Test + void testX509CertificateAPIRestrictions() { + Header intenralUserIdentity = TestHelpers.createTurnpikeIdentityHeader("internal_user", "not_use"); + + given() + .header(intenralUserIdentity) + .contentType(JSON) + .body(Json.encode(new X509Certificate())) + .when() + .post("/internal/x509Certificates") + .then() + .statusCode(FORBIDDEN.getStatusCode()); + + given() + .header(intenralUserIdentity) + .contentType(JSON) + .pathParam("certificateId", UUID.randomUUID().toString()) + .body(Json.encode(new X509Certificate())) + .when() + .put("/internal/x509Certificates/{certificateId}") + .then() + .statusCode(FORBIDDEN.getStatusCode()); + + given() + .header(intenralUserIdentity) + .contentType(JSON) + .pathParam("certificateId", UUID.randomUUID().toString()) + .body(Json.encode(new X509Certificate())) + .when() + .delete("/internal/x509Certificates/{certificateId}") + .then() + .statusCode(FORBIDDEN.getStatusCode()); + + } + + private static X509Certificate checkGatewayCertificate(Header adminIdentity, String bundleName, String applicationName, String certificateData, Response.Status status) { + String responseBody = given() + .header(adminIdentity) + .contentType(JSON) + .param("bundle", bundleName) + .param("application", applicationName) + .param("certificateSubjectDn", certificateData) + .when() + .get("/internal/validation/certificate") + .then() + .statusCode(status.getStatusCode()).extract().asString(); + + if (Response.Status.OK == status) { + JsonObject jsonApp = new JsonObject(responseBody); + return jsonApp.mapTo(X509Certificate.class); + } + return null; + } + +} diff --git a/database/src/main/resources/db/migration/V1.89.0__RHCLOUD-29487_add_gateway_certificate_table.sql b/database/src/main/resources/db/migration/V1.89.0__RHCLOUD-29487_add_gateway_certificate_table.sql new file mode 100644 index 0000000000..a2935abf3d --- /dev/null +++ b/database/src/main/resources/db/migration/V1.89.0__RHCLOUD-29487_add_gateway_certificate_table.sql @@ -0,0 +1,11 @@ +CREATE TABLE x509_certificate ( + id UUID NOT NULL, + application_id UUID NOT NULL, + subject_dn text NOT NULL, + source_environment text NOT NULL, + CONSTRAINT pk_x509_certificate PRIMARY KEY (id), + CONSTRAINT uq_x509_certificate UNIQUE (application_id, subject_dn) +); + +-- gateway_certificate foreign key +ALTER TABLE x509_certificate ADD CONSTRAINT fk_x509_certificate_application_id FOREIGN KEY (application_id) REFERENCES applications(id) ON DELETE CASCADE;