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
This commit is contained in:
parent
5dde208e72
commit
8a843dc300
7 changed files with 190 additions and 11 deletions
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,18 @@
|
||||||
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"})})
|
||||||
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue