From edbf194567904435926a535ceabb6c45ecb06e61 Mon Sep 17 00:00:00 2001 From: RPKI Team at RIPE NCC Date: Fri, 9 Aug 2024 08:51:43 +0000 Subject: [PATCH] RIPE NCC has merged 4569ffd * deploy-check: fix deployments selection [c5ca1df4] * Update dependency io.freefair.lombok:io.freefair.lombok.gradle.plugin to v8.7.1 [0ccd4427] * Add integration tests to cover ROA update scenarios [e17644f3] * Update dependency net.logstash.logback:logstash-logback-encoder to v8 [24108932] * Update dependency io.sentry:sentry-bom to v7.13.0 [55c816f4] * Update dependency org.wiremock:wiremock-jetty12 to v3.9.1 [68ae162e] * Refactor for sonarqube [d5967a81] * Cleanup imports [b183d3c5] * Use a thread pool of fixed size [e1b08933] * Revert ignored test [252ec562] * Cleanup [40792e39] * Renaming [788aa678] * Revert disabled security [c77eb8ca] * Fix date JSON serialisation [4d8b2175] * Ignore breaking test [5afbfa78] * Compilation fixes [576f84cf] * Add lastUpdate timestamp [5df6a6f3] * Comment out security again [077a9580] * Revert security [99acfb4d] * Remove security again [92ef220c] * Cleanup [7c8c9a43] * Revert disabled security [0a396415] * Fix DTO for Krill API [33d506e1] * Fix non-existent publishers [3d1cf2fa] * More testing [688522e3] * Disable security more or less [67a810d3] * Ignore broken test [ae3efe8b] * Disable security for easy testing [bed7a4f5] * Make some Krill communication parallel [8193b94f] * Fix publishers listing [8df1828e] * Naming [fcc3500b] * Fix compilation [39916d2d] * Cleanup [b1b6e0b3] * Initial version [662c8972] * Update dependency org.wiremock:wiremock-jetty12 to v3.9.0 [bea428c1] --- .gitignore | 1 + build.gradle | 6 +- buildSrc/build.gradle | 2 +- scripts/gitlab-deploy-check | 3 +- .../java/net/ripe/rpki/ripencc/RoaIT.java | 142 ++++++++++++++++++ .../net/ripe/rpki/config/OpenAPIConfig.java | 1 - .../rpki/rest/security/SecurityConfig.java | 1 + .../service/PublisherRepositoriesService.java | 48 ++++-- ...KrillNonHostedPublisherRepositoryBean.java | 56 ++++++- .../NonHostedPublisherRepositoryService.java | 38 ++++- ...rtificateAuthorityViewServiceImplTest.java | 1 + .../java/net/ripe/rpki/rest/service/Rest.java | 2 +- .../FakeNonHostedPublisherRepositoryBean.java | 8 + 13 files changed, 278 insertions(+), 31 deletions(-) create mode 100644 src/integration/java/net/ripe/rpki/ripencc/RoaIT.java diff --git a/.gitignore b/.gitignore index bbf324c..fbd7d73 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ rpki-ripe-ncc-ui/bin/ logfile .metals bin/ +.jqwik-database \ No newline at end of file diff --git a/build.gradle b/build.gradle index fd11679..9a67111 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ dependencies { implementation "org.thymeleaf:thymeleaf:3.1.2.RELEASE" implementation "org.thymeleaf:thymeleaf-spring6:3.1.2.RELEASE" - implementation platform('io.sentry:sentry-bom:7.12.0') + implementation platform('io.sentry:sentry-bom:7.13.0') implementation 'io.sentry:sentry-spring-boot-starter' implementation 'io.sentry:sentry-logback' @@ -62,7 +62,7 @@ dependencies { implementation 'commons-io:commons-io:2.16.1' implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5' implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5' - implementation 'net.logstash.logback:logstash-logback-encoder:7.3' + implementation 'net.logstash.logback:logstash-logback-encoder:8.0' implementation 'commons-lang:commons-lang:2.6' testImplementation('org.springframework.boot:spring-boot-starter-test') { @@ -72,7 +72,7 @@ dependencies { exclude group: 'org.hamcrest', module: 'hamcrest-core' } - testImplementation "org.wiremock:wiremock-jetty12:3.8.0" + testImplementation "org.wiremock:wiremock-jetty12:3.9.1" testImplementation 'net.jqwik:jqwik:1.9.0' testImplementation "net.ripe.rpki:rpki-commons:$rpki_commons_version:tests" testImplementation 'org.assertj:assertj-core' diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 9304182..4bfc476 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -9,7 +9,7 @@ repositories { } dependencies { - implementation 'io.freefair.lombok:io.freefair.lombok.gradle.plugin:8.6' + implementation 'io.freefair.lombok:io.freefair.lombok.gradle.plugin:8.7.1' implementation('com.gorylenko.gradle-git-properties:com.gorylenko.gradle-git-properties.gradle.plugin:2.4.2') { exclude group: 'org.eclipse.jgit', module: 'org.eclipse.jgit' } diff --git a/scripts/gitlab-deploy-check b/scripts/gitlab-deploy-check index 6329c95..dd14ffe 100755 --- a/scripts/gitlab-deploy-check +++ b/scripts/gitlab-deploy-check @@ -80,6 +80,7 @@ const Calendar = { return xd > yd; }, max: (x, y) => Calendar.isAfter(x, y) ? x : y, + min: (x, y) => Calendar.isAfter(x, y) ? y : x, compare: (key=(x)=>x) => (x, y) => Calendar.isAfter(key(x), key(y)) ? 1 : Calendar.isAfter(key(y), key(x)) ? -1 : 0, showTimestamp: (x) => { @@ -180,7 +181,7 @@ const main = async (ctx) => { const deployments = await gitlab.combinators.findAll(gitlab.queries.deployments)({ environment: ctx.stagingEnv, - finished_after: Calendar.max(lastCommit.created_at, mergeRequest.created_at), + finished_after: Calendar.min(lastCommit.created_at, mergeRequest.created_at), order_by: "finished_at", status: "success", })(); diff --git a/src/integration/java/net/ripe/rpki/ripencc/RoaIT.java b/src/integration/java/net/ripe/rpki/ripencc/RoaIT.java new file mode 100644 index 0000000..3c9e320 --- /dev/null +++ b/src/integration/java/net/ripe/rpki/ripencc/RoaIT.java @@ -0,0 +1,142 @@ +package net.ripe.rpki.ripencc; + +import net.ripe.ipresource.Asn; +import net.ripe.ipresource.ImmutableResourceSet; +import net.ripe.ipresource.IpRange; +import net.ripe.rpki.bgpris.BgpRisEntryRepositoryBean; +import net.ripe.rpki.commons.util.VersionedId; +import net.ripe.rpki.domain.*; +import net.ripe.rpki.rest.service.Rest; +import net.ripe.rpki.ripencc.cache.JpaResourceCacheImpl; +import net.ripe.rpki.server.api.commands.CertificateAuthorityCommand; +import net.ripe.rpki.server.api.commands.UpdateAllIncomingResourceCertificatesCommand; +import net.ripe.rpki.server.api.dto.BgpRisEntry; +import net.ripe.rpki.server.api.services.command.CommandService; +import net.ripe.rpki.server.api.services.command.CommandStatus; +import net.ripe.rpki.server.api.support.objects.CaName; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; +import javax.security.auth.x500.X500Principal; +import java.util.*; +import static net.ripe.ipresource.ImmutableResourceSet.parse; +import static net.ripe.rpki.rest.service.RestService.API_URL_PREFIX; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +@Transactional +@AutoConfigureMockMvc +@AutoConfigureWebMvc +public class RoaIT extends CertificationDomainTestCase { + private static final long HOSTED_CA_ID = 454L; + private static final X500Principal CHILD_CA_NAME = new X500Principal("CN=child"); + public static final ImmutableResourceSet CHILD_CA_RESOURCES = ImmutableResourceSet.parse("fc00::/12, 192.168.0.0/16"); + private static final BgpRisEntry BGP_RIS_ENTRY_1 = new BgpRisEntry(Asn.parse("AS3549"), IpRange.parse("fc00::/12"), 100); + private static final BgpRisEntry BGP_RIS_ENTRY_2 = new BgpRisEntry(Asn.parse("AS3549"), IpRange.parse("192.168.0.0/16"), 100); + @Autowired + private JpaResourceCacheImpl resourceCache; + @Autowired + private CommandService subject; + @Autowired + private MockMvc mockMvc; + @Autowired + private BgpRisEntryRepositoryBean bgpRisEntryRepository; + private ProductionCertificateAuthority parent; + private HostedCertificateAuthority child; + + @Before + public void setUp() throws Exception { + clearDatabase(); + parent = createInitializedAllResourcesAndProductionCertificateAuthority(); + child = new HostedCertificateAuthority(HOSTED_CA_ID, CHILD_CA_NAME, UUID.randomUUID(), parent); + issueCertificateForNewKey(parent, child, CHILD_CA_RESOURCES); + certificateAuthorityRepository.add(child); + resourceCache.updateEntry(CaName.of(CHILD_CA_NAME), parse("fc00::/12, 192.168.0.0/16")); + execute(new UpdateAllIncomingResourceCertificatesCommand(new VersionedId(HOSTED_CA_ID, VersionedId.INITIAL_VERSION), Integer.MAX_VALUE)); + bgpRisEntryRepository.resetEntries(Arrays.asList(BGP_RIS_ENTRY_1)); + // Add initial ROA first + mockMvc.perform(Rest.post(API_URL_PREFIX + "/" + child.getName() + "/roas/publish") + .content("{ " + + "\"added\" : [{\"asn\" : \"AS3549\", \"prefix\" : \"fc00::/12\", \"maximalLength\" : \"12\"}], " + + "\"deleted\" : [] " + + "}")) + .andExpect(status().is(204)); + } + + @Test + public void itIsNotPossibleToStageExistingRoaWhenAnnouncementNotFound() throws Exception { + // All of the sudden we do not see initial announcement anymore, it has changed. + bgpRisEntryRepository.resetEntries(Arrays.asList(BGP_RIS_ENTRY_2)); + + // Ok, announcement has changed, let's see if we can alter existing ROA and add new to cover new announcement, staging + mockMvc.perform(Rest.post(API_URL_PREFIX + "/" + child.getName() + "/roas/stage") + .content("[{\"asn\" : \"AS3549\", \"prefix\" : \"fc00::/12\", \"maximalLength\" : \"14\"}," + + "{\"asn\" : \"AS3549\", \"prefix\" : \"192.168.0.0/16\", \"maximalLength\" : \"16\"}]")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value("1")) + .andExpect(jsonPath("$.[0].asn").value("AS3549")) + .andExpect(jsonPath("$.[0].prefix").value("192.168.0.0/16")) + .andExpect(jsonPath("$.[0].visibility").value("100")) + .andExpect(jsonPath("$.[0].suppressed").value("false")) + .andExpect(jsonPath("$.[0].verified").value("true")) + .andExpect(jsonPath("$.[0].currentState").value("UNKNOWN")) + .andExpect(jsonPath("$.[0].futureState").value("VALID")) + .andExpect(jsonPath("$.[0].affectedByChange").value("true")); + } + + @Test + public void itIsPossibleToUpdateExistingRoaWhenAnnouncementNotFound() throws Exception { + // All of the sudden we do not see initial announcement anymore, it has changed. + bgpRisEntryRepository.resetEntries(Arrays.asList(BGP_RIS_ENTRY_2)); + + // Is it still possible to update initial ROA? + mockMvc.perform(Rest.post(API_URL_PREFIX + "/" + child.getName() + "/roas/publish") + .content("{ " + + "\"added\" : [{\"asn\" : \"AS3549\", \"prefix\" : \"fc00::/12\", \"maximalLength\" : \"14\"}], " + + "\"deleted\" : [{\"asn\" : \"AS3549\", \"prefix\" : \"fc00::/12\", \"maximalLength\" : \"12\"}] " + + "}")) + .andExpect(status().is(204)); + + // Initial ROA should not be VALID anyways. + mockMvc.perform(Rest.get(API_URL_PREFIX + "/" + child.getName() + "/roas")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value("1")) + .andExpect(jsonPath("$.[0].asn").value("AS3549")) + .andExpect(jsonPath("$.[0].prefix").value("fc00::/12")) + .andExpect(jsonPath("$.[0]._numberOfValidsCaused").value("0")) + .andExpect(jsonPath("$.[0]._numberOfInvalidsCaused").value("0")) + .andExpect(jsonPath("$.[0].maximalLength").value("14")); + } + + @Test + public void roaIsValidatingAgainAfterCorrespondingAnnouncementIsVisibleAgain() throws Exception { + // First announcement is visible again, so both announcements are visible now. + bgpRisEntryRepository.resetEntries(Arrays.asList(BGP_RIS_ENTRY_1, BGP_RIS_ENTRY_2)); + + // It is expected to have two ROAs and both valid + mockMvc.perform(Rest.get(API_URL_PREFIX + "/" + child.getName() + "/roas")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value("1")) + .andExpect(jsonPath("$.[0].asn").value("AS3549")) + .andExpect(jsonPath("$.[0].prefix").value("fc00::/12")) + .andExpect(jsonPath("$.[0]._numberOfValidsCaused").value("1")) + .andExpect(jsonPath("$.[0]._numberOfInvalidsCaused").value("0")) + .andExpect(jsonPath("$.[0].maximalLength").value("12")); + } + + protected CommandStatus execute(CertificateAuthorityCommand command) { + try { + return subject.execute(command); + } finally { + entityManager.flush(); + } + } + + private static Collection entries(BgpRisEntry... entries) { + return new HashSet<>(Arrays.asList(entries)); + } +} \ No newline at end of file diff --git a/src/main/java/net/ripe/rpki/config/OpenAPIConfig.java b/src/main/java/net/ripe/rpki/config/OpenAPIConfig.java index 5407a43..6d0f66b 100644 --- a/src/main/java/net/ripe/rpki/config/OpenAPIConfig.java +++ b/src/main/java/net/ripe/rpki/config/OpenAPIConfig.java @@ -42,7 +42,6 @@ private Components getSecuritySchemes() { } private Info getInfo() { - return new Info().title("Resource Certification (RPKI) API") .termsOfService("https://www.ripe.net/lir-services/resource-management/certification/legal/ripe-ncc-certification-service-terms-and-conditions") .description("Rest API for RIPE NCC Resource Certification (RPKI)") diff --git a/src/main/java/net/ripe/rpki/rest/security/SecurityConfig.java b/src/main/java/net/ripe/rpki/rest/security/SecurityConfig.java index c97fe17..1ded110 100644 --- a/src/main/java/net/ripe/rpki/rest/security/SecurityConfig.java +++ b/src/main/java/net/ripe/rpki/rest/security/SecurityConfig.java @@ -53,6 +53,7 @@ public SecurityFilterChain webSecurityFilterChainRequireApiKey(HttpSecurity http .build(); } + @Order(2) @Bean public SecurityFilterChain webSecurityFilterChainAllowProvisioningEndpoint(HttpSecurity http) throws Exception { diff --git a/src/main/java/net/ripe/rpki/rest/service/PublisherRepositoriesService.java b/src/main/java/net/ripe/rpki/rest/service/PublisherRepositoriesService.java index 0d61a58..f3cceaa 100644 --- a/src/main/java/net/ripe/rpki/rest/service/PublisherRepositoriesService.java +++ b/src/main/java/net/ripe/rpki/rest/service/PublisherRepositoriesService.java @@ -3,13 +3,12 @@ import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.core.MediaType; import lombok.Value; import lombok.extern.slf4j.Slf4j; -import net.ripe.rpki.commons.provisioning.identity.IdentitySerializer; -import net.ripe.rpki.commons.provisioning.identity.PublisherRequest; -import net.ripe.rpki.commons.provisioning.identity.PublisherRequestSerializer; -import net.ripe.rpki.commons.provisioning.identity.RepositoryResponse; -import net.ripe.rpki.commons.provisioning.identity.RepositoryResponseSerializer; +import net.ripe.rpki.commons.provisioning.identity.*; import net.ripe.rpki.domain.NonHostedCertificateAuthority; import net.ripe.rpki.rest.exception.CaNotFoundException; import net.ripe.rpki.rest.exception.ObjectNotFoundException; @@ -27,18 +26,9 @@ import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; -import jakarta.persistence.EntityNotFoundException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.core.MediaType; import java.io.IOException; import java.io.InputStream; import java.net.URI; @@ -46,6 +36,7 @@ import java.util.Map; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.ForkJoinPool; import java.util.stream.Collectors; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; @@ -65,6 +56,8 @@ public class PublisherRepositoriesService extends AbstractCaRestService { private final CommandService commandService; private final Optional maybeNonHostedPublisherRepositoryService; + private static final ForkJoinPool krillCommunicationPool = new ForkJoinPool(4); + /** * A workaround for the long-standing issue https://github.com/NLnetLabs/krill/issues/984 that appears to be a wont-fix. * @@ -231,6 +224,30 @@ public ResponseEntity deleteNonHostedPublicationRepository( } } + @GetMapping(path = "non-hosted/publisher-content") + @Operation(summary = "Get content for every publisher for the CA") + public ResponseEntity getPublishers(@PathVariable("caName") final CaName caName) { + if (maybeNonHostedPublisherRepositoryService.isEmpty()) { + return ResponseEntity.status(NOT_ACCEPTABLE).body(NON_HOSTED_PUBLISHERS_ARE_NOT_AVAILABLE); + } + var nonHostedPublisherRepositoryService = this.maybeNonHostedPublisherRepositoryService.orElseThrow(); + + log.info("Getting full information about publishers for CA: {}", caName); + + NonHostedCertificateAuthorityData ca = getCa(NonHostedCertificateAuthorityData.class, caName); + + var nonHostedPublisherRepositories = certificateAuthorityViewService.findNonHostedPublisherRepositories(ca.getName()); + var publisherContent = krillCommunicationPool.submit(() -> + nonHostedPublisherRepositories + .keySet() + .parallelStream() + .flatMap(handle -> nonHostedPublisherRepositoryService.publisherInfo(handle).stream()) + .toList() + ).join(); + + return ResponseEntity.ok(publisherContent); + } + @Value private static class RepositoryResponseDto { @JsonInclude(JsonInclude.Include.NON_NULL) @@ -252,5 +269,4 @@ static RepositoryResponseDto of(RepositoryResponse repositoryResponse) { ); } } - } diff --git a/src/main/java/net/ripe/rpki/ripencc/services/impl/KrillNonHostedPublisherRepositoryBean.java b/src/main/java/net/ripe/rpki/ripencc/services/impl/KrillNonHostedPublisherRepositoryBean.java index e47313f..93f48d1 100644 --- a/src/main/java/net/ripe/rpki/ripencc/services/impl/KrillNonHostedPublisherRepositoryBean.java +++ b/src/main/java/net/ripe/rpki/ripencc/services/impl/KrillNonHostedPublisherRepositoryBean.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.*; import lombok.extern.slf4j.Slf4j; +import net.ripe.rpki.commons.crypto.cms.manifest.ManifestCmsParser; import net.ripe.rpki.commons.provisioning.identity.PublisherRequest; import net.ripe.rpki.commons.provisioning.identity.RepositoryResponse; import net.ripe.rpki.commons.provisioning.x509.ProvisioningIdentityCertificate; @@ -14,6 +15,7 @@ import net.ripe.rpki.server.api.ports.NonHostedPublisherRepositoryService; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; +import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Profile; @@ -29,6 +31,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import java.net.URI; +import java.time.Instant; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -119,14 +122,10 @@ public RepositoryResponse provisionPublisher(UUID publisherHandle, PublisherRequ case "pub-duplicate": throw new DuplicateRepositoryException(publisherHandle); default: - throw new CertificateAuthorityException( - String.format("krill call failed with %d: %s: %s", post.getStatus(), post.getStatusInfo(), error) - ); + throw krillCallException(post); } } else { - throw new CertificateAuthorityException( - String.format("krill call failed with %d: %s", post.getStatus(), post.getStatusInfo()) - ); + throw krillCallException(post); } } } @@ -135,7 +134,7 @@ public RepositoryResponse provisionPublisher(UUID publisherHandle, PublisherRequ public Set listPublishers() { try (Response response = clientForTarget(PUBD_PUBLISHERS).get()) { if (HttpStatus.Series.resolve(response.getStatus()) != HttpStatus.Series.SUCCESSFUL) { - throw new CertificateAuthorityException(String.format("krill call failed with %d: %s", response.getStatus(), response.getStatusInfo())); + throw krillCallException(response); } return response.readEntity(PublishersDto.class).publishers.stream().flatMap(handle -> { try { @@ -172,6 +171,49 @@ public boolean isInitialized() { } } + @Override + public Optional publisherInfo(UUID publisherHandle) { + try (Response response = clientForTarget(PUBD_PUBLISHERS + "/" + publisherHandle).get()) { + if (response.getStatus() == HttpStatus.NOT_FOUND.value()) { + return Optional.empty(); + } + if (HttpStatus.Series.resolve(response.getStatus()) != HttpStatus.Series.SUCCESSFUL) { + throw krillCallException(response); + } + return Optional.of(response.readEntity(Publisher.class)) + .map(this::extractUpdateTimestamp); + } + } + + /** + * Extract thisUpdteTime from the publisher's manifest and assume it a last update time for the publisher. + * We may change it to something more straightforward if/when Krill API has such option. + */ + private Publisher extractUpdateTimestamp(Publisher publisher) { + for (var file : publisher.getCurrentFiles()) { + if (file.getUri().endsWith(".mft")) { + try { + var decoded = Base64.getDecoder().decode(file.getBase64()); + ManifestCmsParser parser = new ManifestCmsParser(); + parser.parse(file.getUri(), decoded); + if (parser.isSuccess()) { + DateTime thisUpdateTime = parser.getManifestCms().getThisUpdateTime(); + return publisher.withLastUpdate(Instant.ofEpochMilli(thisUpdateTime.getMillis())); + } + } catch (Exception ignore) { + // We don't want exceptions in our logs caused by unlikely broken data + // published to PaaS by someone. + } + } + } + return publisher; + } + + private CertificateAuthorityException krillCallException(Response response) { + return new CertificateAuthorityException( + String.format("krill call failed with %d: %s", response.getStatus(), response.getStatusInfo())); + } + @AllArgsConstructor static class PublisherInitDto { @JsonProperty("rrdp_base_uri") diff --git a/src/main/java/net/ripe/rpki/server/api/ports/NonHostedPublisherRepositoryService.java b/src/main/java/net/ripe/rpki/server/api/ports/NonHostedPublisherRepositoryService.java index 0f6c395..afcbabf 100644 --- a/src/main/java/net/ripe/rpki/server/api/ports/NonHostedPublisherRepositoryService.java +++ b/src/main/java/net/ripe/rpki/server/api/ports/NonHostedPublisherRepositoryService.java @@ -1,12 +1,17 @@ package net.ripe.rpki.server.api.ports; -import lombok.Getter; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; import net.ripe.rpki.commons.provisioning.identity.PublisherRequest; import net.ripe.rpki.commons.provisioning.identity.RepositoryResponse; +import java.time.Instant; +import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.UUID; + public interface NonHostedPublisherRepositoryService { boolean isAvailable(); @@ -27,6 +32,8 @@ default void deletePublisher(UUID publisherHandle) { boolean isInitialized(); + Optional publisherInfo(UUID publisherHandle); + class DuplicateRepositoryException extends Exception { @Getter private final UUID publisherHandle; @@ -36,4 +43,33 @@ public DuplicateRepositoryException(UUID publisherHandle) { this.publisherHandle = publisherHandle; } } + + // https://krill.docs.nlnetlabs.nl/en/stable/publication-server.html#show-a-publisher + @Data + @With + @AllArgsConstructor + class Publisher { + String handle; + @JsonProperty("id_cert") + IdCert idCert; + @JsonProperty("base_uri") + String baseUri; + @JsonProperty("current_files") + List currentFiles; + Instant lastUpdate; + } + + @Value + class IdCert { + @JsonProperty("public_key") + String publicKey; + String base64; + String hash; + } + + @Value + class PublisherFile { + String base64; + String uri; + } } diff --git a/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceImplTest.java b/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceImplTest.java index 897a774..9f94780 100644 --- a/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceImplTest.java +++ b/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceImplTest.java @@ -22,6 +22,7 @@ @Transactional public class CertificateAuthorityViewServiceImplTest extends CertificationDomainTestCase { public static final ImmutableResourceSet CHILD_CA_RESOURCES = ImmutableResourceSet.parse("10.0.0.0/8"); + private static final long HOSTED_CA_ID = 7L; private static final X500Principal CHILD_CA_NAME = new X500Principal("CN=child"); diff --git a/src/test/java/net/ripe/rpki/rest/service/Rest.java b/src/test/java/net/ripe/rpki/rest/service/Rest.java index 040a9dc..7c994db 100644 --- a/src/test/java/net/ripe/rpki/rest/service/Rest.java +++ b/src/test/java/net/ripe/rpki/rest/service/Rest.java @@ -27,7 +27,7 @@ static MockHttpServletRequestBuilder post(String url, String content) { )); } - static MockHttpServletRequestBuilder post(String url) { + public static MockHttpServletRequestBuilder post(String url) { return authenticated(withUserId( MockMvcRequestBuilders.post(url) .accept(APPLICATION_JSON) diff --git a/src/test/java/net/ripe/rpki/services/impl/FakeNonHostedPublisherRepositoryBean.java b/src/test/java/net/ripe/rpki/services/impl/FakeNonHostedPublisherRepositoryBean.java index 1035e1f..ac8339e 100644 --- a/src/test/java/net/ripe/rpki/services/impl/FakeNonHostedPublisherRepositoryBean.java +++ b/src/test/java/net/ripe/rpki/services/impl/FakeNonHostedPublisherRepositoryBean.java @@ -55,4 +55,12 @@ public void deletePublisher(UUID publisherHandle, String requestId) { public boolean isInitialized() { return true; } + + @Override + public Optional publisherInfo(UUID publisherHandle) { + return Optional.of(new Publisher(publisherHandle.toString(), + new IdCert("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1b41M0NiNAbxu7wp3D90FWWRQVNJS0dsbDcekDOvtyEYMe", + "MIIDEzCCAfugAwIBAgIB", "e4a2aa8725584679b79c316ec428c605a518da86df81f70fa8976681314032c5"), + "https://fake.rpki.example.com/pubserver", Collections.emptyList(), null)); + } }