From 8a843dc3001c85c9f0d78418cbca913188a4b7f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20Le=C3=9Fmann=20=28=40Mark=2ETwoFive=29?= Date: Thu, 19 Jun 2025 23:07:27 +0200 Subject: [PATCH] wip: Generate and configure OpenAPI spec - Create two (non-functioning) demo endpoints to check the swagger UI with - Configure Jackson to only serialize specific attributes - Configure SpringDoc so that only attributes known to Jackson are shown - Add some shortcut annotations for Json formatting --- .../config/JacksonConfiguration.java | 35 +++++++++++++ .../config/SpringdocConfiguration.java | 27 ++++++++++ .../certassist/models/Certificate.java | 35 +++++++++---- .../repositories/CertificateRepository.java | 5 +- .../certassist/web/CertificatesEndpoint.java | 51 +++++++++++++++++++ .../certassist/web/DocumentedPage.java | 29 +++++++++++ .../certassist/web/JsonIsoOffsetDate.java | 19 +++++++ 7 files changed, 190 insertions(+), 11 deletions(-) create mode 100644 src/main/java/de/mlessmann/certassist/config/JacksonConfiguration.java create mode 100644 src/main/java/de/mlessmann/certassist/config/SpringdocConfiguration.java create mode 100644 src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java create mode 100644 src/main/java/de/mlessmann/certassist/web/DocumentedPage.java create mode 100644 src/main/java/de/mlessmann/certassist/web/JsonIsoOffsetDate.java diff --git a/src/main/java/de/mlessmann/certassist/config/JacksonConfiguration.java b/src/main/java/de/mlessmann/certassist/config/JacksonConfiguration.java new file mode 100644 index 0000000..a232b8e --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/config/JacksonConfiguration.java @@ -0,0 +1,35 @@ +package de.mlessmann.certassist.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.MapperFeature; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfiguration { + + /** + * Customizes the objectMapper so that ONLY specifically annotated fields are serialized. + * Other fields MUST NOT be serialized since they may contain sensitive information! + */ + @Bean + public Jackson2ObjectMapperBuilderCustomizer customizeObjectMapper() { + return builder -> builder + .featuresToDisable( + MapperFeature.AUTO_DETECT_FIELDS, + MapperFeature.AUTO_DETECT_GETTERS, + MapperFeature.AUTO_DETECT_IS_GETTERS + ) + .serializationInclusion(JsonInclude.Include.NON_EMPTY) + .visibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.IS_GETTER, JsonAutoDetect.Visibility.NONE) + .visibility(PropertyAccessor.SETTER, JsonAutoDetect.Visibility.PUBLIC_ONLY) + .visibility(PropertyAccessor.CREATOR, JsonAutoDetect.Visibility.PUBLIC_ONLY) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/de/mlessmann/certassist/config/SpringdocConfiguration.java b/src/main/java/de/mlessmann/certassist/config/SpringdocConfiguration.java new file mode 100644 index 0000000..bf3523f --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/config/SpringdocConfiguration.java @@ -0,0 +1,27 @@ +package de.mlessmann.certassist.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.swagger.v3.core.jackson.ModelResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Configuration for Springdoc OpenAPI to respect Jackson annotations. + * This ensures that only properties that are visible to Jackson (annotated with @JsonProperty) + * are included in the OpenAPI schema. + */ +@Configuration +public class SpringdocConfiguration { + + /** + * Creates a ModelResolver that uses the same ObjectMapper as the application. + * This ensures that Springdoc respects the same visibility settings as Jackson. + * + * @param objectMapper the configured ObjectMapper from JacksonConfiguration + * @return a ModelResolver that uses the application's ObjectMapper + */ + @Bean + public ModelResolver modelResolver(ObjectMapper objectMapper) { + return new ModelResolver(objectMapper); + } +} \ No newline at end of file diff --git a/src/main/java/de/mlessmann/certassist/models/Certificate.java b/src/main/java/de/mlessmann/certassist/models/Certificate.java index fdf3d16..678facf 100644 --- a/src/main/java/de/mlessmann/certassist/models/Certificate.java +++ b/src/main/java/de/mlessmann/certassist/models/Certificate.java @@ -1,17 +1,21 @@ package de.mlessmann.certassist.models; +import com.fasterxml.jackson.annotation.JsonProperty; +import de.mlessmann.certassist.web.JsonIsoOffsetDate; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.persistence.*; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; +import lombok.*; +import org.hibernate.proxy.HibernateProxy; + import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import lombok.*; -import org.hibernate.proxy.HibernateProxy; @Entity -@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "fingerprint" }) }) +@Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"fingerprint"})}) @Getter @Setter @ToString @@ -25,8 +29,10 @@ public class Certificate { @NotNull @Enumerated(EnumType.STRING) + @JsonProperty private CertificateType type; + @JsonProperty private String trustingAuthority; /** @@ -39,17 +45,26 @@ public class Certificate { @Min(-1) private int requestedKeyLength; + @JsonIsoOffsetDate private OffsetDateTime notBefore; + @JsonIsoOffsetDate private OffsetDateTime notAfter; @NotNull + @JsonProperty private String subjectCommonName; + @JsonProperty private String subjectEmailAddress; + @JsonProperty private String subjectOrganization; + @JsonProperty private String subjectOrganizationalUnit; + @JsonProperty private String subjectCountry; + @JsonProperty private String subjectState; + @JsonProperty private String subjectLocality; @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @@ -69,6 +84,8 @@ public class Certificate { private byte[] fullchain; @Column(nullable = false) + @JsonProperty + @Schema(description = "The certificate fingerprint. The algorithm used to derive the fingerprint is determined by OpenSSL") private String fingerprint; @Override @@ -76,11 +93,11 @@ public class Certificate { if (this == o) return true; if (o == null) return false; Class oEffectiveClass = o instanceof HibernateProxy - ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() - : o.getClass(); + ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() + : o.getClass(); Class thisEffectiveClass = this instanceof HibernateProxy - ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() - : this.getClass(); + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() + : this.getClass(); if (thisEffectiveClass != oEffectiveClass) return false; Certificate that = (Certificate) o; return getId() != null && Objects.equals(getId(), that.getId()); @@ -89,7 +106,7 @@ public class Certificate { @Override public final int hashCode() { return this instanceof HibernateProxy - ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() - : getClass().hashCode(); + ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() + : getClass().hashCode(); } } diff --git a/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java b/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java index 7976856..70a43d0 100644 --- a/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java +++ b/src/main/java/de/mlessmann/certassist/repositories/CertificateRepository.java @@ -1,10 +1,11 @@ package de.mlessmann.certassist.repositories; import de.mlessmann.certassist.models.Certificate; -import org.springframework.data.repository.CrudRepository; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository -public interface CertificateRepository extends CrudRepository { +public interface CertificateRepository extends JpaRepository { + Certificate findByFingerprintIs(String fingerprint); } diff --git a/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java b/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java new file mode 100644 index 0000000..bb1b389 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/web/CertificatesEndpoint.java @@ -0,0 +1,51 @@ +package de.mlessmann.certassist.web; + +import de.mlessmann.certassist.models.Certificate; +import de.mlessmann.certassist.models.CertificateInfo; +import de.mlessmann.certassist.models.CertificateInfoSubject; +import de.mlessmann.certassist.repositories.CertificateRepository; +import de.mlessmann.certassist.service.CertificateCreationService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor(onConstructor_ = @Autowired) +public class CertificatesEndpoint { + + private final CertificateRepository certificateRepository; + private final CertificateCreationService certificateCreationService; + + @GetMapping("/certificates") + @Operation(description = "Fetches certificates", responses = { + @ApiResponse(responseCode = "200", description = "Contains the returned certificates of the requested page.") + }) + public ResponseEntity> getCertificates(@ParameterObject Pageable pageable) { + var certificates = certificateRepository.findAll(pageable); + return ResponseEntity.ok(DocumentedPage.of(certificates)); + } + + @PostMapping("/certificates") + @Operation(description = "Requests a new certificate", responses = { + @ApiResponse(responseCode = "400", description = "One of the provided parameters is invalid."), + @ApiResponse(responseCode = "200", description = "Returns the newly created certificate.") + }) + public ResponseEntity createCertificate(@RequestBody CertificateInfo request) { + var createdCertificate = certificateCreationService.createCertificate( + CertificateInfo.builder() + .type(CertificateInfo.RequestType.STANDALONE_CERTIFICATE) + .issuer(CertificateInfoSubject.builder() + .commonName("Test") + ) + .build() + ); + return ResponseEntity.ok(createdCertificate); + } + +} diff --git a/src/main/java/de/mlessmann/certassist/web/DocumentedPage.java b/src/main/java/de/mlessmann/certassist/web/DocumentedPage.java new file mode 100644 index 0000000..2e73292 --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/web/DocumentedPage.java @@ -0,0 +1,29 @@ +package de.mlessmann.certassist.web; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import org.springframework.data.domain.Page; + +import java.util.List; + +public interface DocumentedPage extends Page { + + @Override + @JsonProperty + @Schema(description = "The content of the paginated response. See nested type for more information.") + List getContent(); + + @Override + @JsonProperty("size") + @Schema(description = "How many items there are in this current page.") + int getNumberOfElements(); + + @Override + @JsonProperty + @Schema(description = "How many pages are currently available in total.") + int getTotalPages(); + + static DocumentedPage of(Page page) { + return (DocumentedPage) page; + } +} diff --git a/src/main/java/de/mlessmann/certassist/web/JsonIsoOffsetDate.java b/src/main/java/de/mlessmann/certassist/web/JsonIsoOffsetDate.java new file mode 100644 index 0000000..115661c --- /dev/null +++ b/src/main/java/de/mlessmann/certassist/web/JsonIsoOffsetDate.java @@ -0,0 +1,19 @@ +package de.mlessmann.certassist.web; + +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.time.format.DateTimeFormatter; + +@JacksonAnnotationsInside +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@JsonProperty +@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") +public @interface JsonIsoOffsetDate { +}