wip: Generate and configure OpenAPI spec
Some checks failed
Build / build (pull_request) Successful in 1m59s
Check formatting / check-formatting (pull_request) Failing after 19s

- 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
This commit is contained in:
Magnus Leßmann (@Mark.TwoFive) 2025-06-19 23:07:27 +02:00
parent 5dde208e72
commit 8a843dc300
Signed by: Mark.TwoFive
GPG key ID: 58204042FE30B10C
7 changed files with 190 additions and 11 deletions

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -1,17 +1,21 @@
package de.mlessmann.certassist.models; 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.persistence.*;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.*;
import org.hibernate.proxy.HibernateProxy;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import lombok.*;
import org.hibernate.proxy.HibernateProxy;
@Entity @Entity
@Table(uniqueConstraints = { @UniqueConstraint(columnNames = { "fingerprint" }) }) @Table(uniqueConstraints = {@UniqueConstraint(columnNames = {"fingerprint"})})
@Getter @Getter
@Setter @Setter
@ToString @ToString
@ -25,8 +29,10 @@ public class Certificate {
@NotNull @NotNull
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@JsonProperty
private CertificateType type; private CertificateType type;
@JsonProperty
private String trustingAuthority; private String trustingAuthority;
/** /**
@ -39,17 +45,26 @@ public class Certificate {
@Min(-1) @Min(-1)
private int requestedKeyLength; private int requestedKeyLength;
@JsonIsoOffsetDate
private OffsetDateTime notBefore; private OffsetDateTime notBefore;
@JsonIsoOffsetDate
private OffsetDateTime notAfter; private OffsetDateTime notAfter;
@NotNull @NotNull
@JsonProperty
private String subjectCommonName; private String subjectCommonName;
@JsonProperty
private String subjectEmailAddress; private String subjectEmailAddress;
@JsonProperty
private String subjectOrganization; private String subjectOrganization;
@JsonProperty
private String subjectOrganizationalUnit; private String subjectOrganizationalUnit;
@JsonProperty
private String subjectCountry; private String subjectCountry;
@JsonProperty
private String subjectState; private String subjectState;
@JsonProperty
private String subjectLocality; private String subjectLocality;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@ -69,6 +84,8 @@ public class Certificate {
private byte[] fullchain; private byte[] fullchain;
@Column(nullable = false) @Column(nullable = false)
@JsonProperty
@Schema(description = "The certificate fingerprint. The algorithm used to derive the fingerprint is determined by OpenSSL")
private String fingerprint; private String fingerprint;
@Override @Override

View file

@ -1,10 +1,11 @@
package de.mlessmann.certassist.repositories; package de.mlessmann.certassist.repositories;
import de.mlessmann.certassist.models.Certificate; import de.mlessmann.certassist.models.Certificate;
import org.springframework.data.repository.CrudRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@Repository @Repository
public interface CertificateRepository extends CrudRepository<Certificate, String> { public interface CertificateRepository extends JpaRepository<Certificate, String> {
Certificate findByFingerprintIs(String fingerprint); Certificate findByFingerprintIs(String fingerprint);
} }

View file

@ -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<DocumentedPage<Certificate>> 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<Certificate> 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);
}
}

View file

@ -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<T> extends Page<T> {
@Override
@JsonProperty
@Schema(description = "The content of the paginated response. See nested type for more information.")
List<T> 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 <T> DocumentedPage<T> of(Page<T> page) {
return (DocumentedPage<T>) page;
}
}

View file

@ -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 {
}