From f4db4ad2a5ae74df57828d36d09b004f5688f34d Mon Sep 17 00:00:00 2001 From: RPKI Team at RIPE NCC Date: Tue, 16 Jan 2024 10:39:57 +0000 Subject: [PATCH] RIPE NCC has merged 953ef97 * Pass in exact createdAt value where available [5d4a69c6] * Use rpki-commons implementation of object time parsing [12750355] * add PrivateAsnsUsedException [4d64424c] * Skip stacktrace for two more error situations [79fe45f5] * Do not log stacktrace for EntityTagDoesNotMatch and PreconditionRequired exceptions [eee53915] * Fix sonar nit by using Object::toString [f810f7e8] * Fix broken tests [ec4e8129] * Address some code smells [5e3997ef] * Prevent controller from running certain operations. [5c832754] * This is to change RSNG resource-service monitoring URL [ff2a902d] * Minimal fix to avoid "wrong button" click in the Ustream CA UI [0e92676f] * Don't recalculate current time [03f91b3c] * Update plugin org.springframework.boot to v2.7.18 [1fcdb70d] * Exclude ncipher/thales from public repositories. [f6e1ce9f] * Fix location of thymeleaf templates in local profile [47bee4ca] * Make error message a bit better [9f5eef99] * Fix tests [17eef53d] * Fixing more code smells [e67b420f] * Fix tests [0814b06c] * Try to cater for sonarcube [c0db3caa] * Use existing ROA configuration instead of existing ROA objects to validate [c39fea0c] * Fix server crash when submitting duplicate ROAs. [f6d982bd] * Code smell [b5733066] * Remove some code smells. [0c8ab8b4] * Fix test name [5be56abf] * Add validation to "stage", fix broken test. [8fd13940] * Add a comment [711f3089] * Add validation to "api/publish" REST call [057f3f38] * Add Utils.validateNoIdenticalROAs [c18156df] * rename variable and log ASPA when reissuing [aa795c4e] * Only force configuration check on nonhosted CAs [b9769f23] * Fix sonarqube warning [cfb6933e] * Move some validation of updates to command handler [1d5a7593] * Junit5 API in test case [33aff947] * Ignore a test [f06fa879] * Do not issue for AspaConfigurations without providers [3d6f7dac] * Do not create AspaEntities without providers [8047d022] * Add failing test for AspaEntity with empty provider AS [2e94165f] * ASPAs where the ASN was no longer on the certificate were not correctly deleted. [0259e1a3] * Correctly re-issue ASPAs when they are not parseable [9eadd9e6] * Copy the providers in the migrations [c721b259] * Make riswhois dump work in local development [f65e5283] * Reissue ASPA files when a new profile version is present [8b449b19] * Support "no providers" ASPA in the API [b0063b70] * Update for new profile [24e54137] * With ProviderAS explicitly [ec45854f] * fix two nits in PublisherRepositoriesService [c9077c8b] --- build.gradle | 9 +- .../rpki-ripe-ncc.build-conventions.gradle | 15 ++- dependencies.gradle | 2 +- .../services/command/CommandServiceImpl.java | 9 +- .../rpki/domain/GenericPublishedObject.java | 6 + .../domain/ManagedCertificateAuthority.java | 3 +- .../net/ripe/rpki/domain/PublishedObject.java | 33 ++++-- .../rpki/domain/aspa/AspaConfiguration.java | 57 +++------- .../aspa/AspaConfigurationRepository.java | 2 + .../net/ripe/rpki/domain/aspa/AspaEntity.java | 37 +++---- .../domain/aspa/AspaEntityServiceBean.java | 78 +++++++++---- .../net/ripe/rpki/domain/crl/CrlEntity.java | 2 +- .../rpki/domain/manifest/ManifestEntity.java | 2 +- .../net/ripe/rpki/domain/roa/RoaEntity.java | 2 +- .../service/TrustAnchorResponseProcessor.java | 19 +++- .../RestExceptionControllerAdvice.java | 4 +- .../service/CaRoaConfigurationService.java | 82 ++++++++++---- .../service/PublisherRepositoriesService.java | 12 +- .../net/ripe/rpki/rest/service/Utils.java | 103 ++++++++++++++++++ .../impl/RestResourceServicesClient.java | 4 +- .../UpdateAspaConfigurationCommand.java | 2 +- .../rpki/server/api/dto/AspaAfiLimit.java | 33 ------ .../server/api/dto/AspaConfigurationData.java | 19 ++-- .../rpki/server/api/dto/AspaProviderData.java | 13 --- .../api/dto/RoaConfigurationPrefixData.java | 15 ++- ...ion.java => IllegalResourceException.java} | 4 +- ...UpdateAspaConfigurationCommandHandler.java | 62 +++++++---- .../jpa/JpaAspaConfigurationRepository.java | 13 +++ .../ripe/rpki/util/PublishedObjectUtil.java | 51 --------- .../ripe/rpki/web/UpstreamCaController.java | 58 ++++++---- src/main/resources/application-local.yml | 3 +- .../db/migration/V126__add_aspa_providers.sql | 13 +++ .../V127__add_aspa_profile_version.sql | 11 ++ .../static/riswhois/riswhoisdump.IPv4.gz | Bin .../static/riswhois/riswhoisdump.IPv6.gz | Bin ...nfigurationMaintenanceServiceBeanTest.java | 4 +- .../aspa/AspaEntityServiceBeanTest.java | 72 ++++++++---- .../TrustAnchorResponseProcessorTest.java | 32 ++++++ .../CaAspaConfigurationServiceTest.java | 81 +++++++++----- .../CaRoaConfigurationServiceTest.java | 57 +++++++--- .../net/ripe/rpki/rest/service/UtilsTest.java | 59 +++++++++- .../service/monitoring/AspaServiceTest.java | 25 ++--- .../impl/RestResourceServicesClientTest.java | 7 +- .../UpdateAspaConfigurationCommandTest.java | 7 +- ...datedManifestAndCrlCommandHandlerTest.java | 4 +- ...teAspaConfigurationCommandHandlerTest.java | 94 ++++++++-------- .../JpaAspaConfigurationRepositoryTest.java | 13 +-- .../rpki/web/UpstreamCaControllerTest.java | 23 ++++ 48 files changed, 805 insertions(+), 451 deletions(-) delete mode 100644 src/main/java/net/ripe/rpki/server/api/dto/AspaAfiLimit.java delete mode 100644 src/main/java/net/ripe/rpki/server/api/dto/AspaProviderData.java rename src/main/java/net/ripe/rpki/server/api/services/command/{DuplicateResourceException.java => IllegalResourceException.java} (62%) delete mode 100644 src/main/java/net/ripe/rpki/util/PublishedObjectUtil.java create mode 100644 src/main/resources/db/migration/V126__add_aspa_providers.sql create mode 100644 src/main/resources/db/migration/V127__add_aspa_profile_version.sql rename src/{test => main}/resources/static/riswhois/riswhoisdump.IPv4.gz (100%) rename src/{test => main}/resources/static/riswhois/riswhoisdump.IPv6.gz (100%) diff --git a/build.gradle b/build.gradle index 2c27c3c..460d411 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'rpki-ripe-ncc.build-conventions' - id 'org.springframework.boot' version '2.7.16' + id 'org.springframework.boot' version '2.7.18' id 'distribution' id 'jacoco' id "com.google.cloud.tools.jib" version "3.3.2" @@ -93,13 +93,6 @@ java { } sourceSets { - main { - resources { - srcDir 'public' - // Wicket resources are in src/main/java - srcDir 'src/main/java' - } - } integration { java.srcDir 'src/integration/java' resources.srcDir 'src/integration/resources' diff --git a/buildSrc/src/main/groovy/rpki-ripe-ncc.build-conventions.gradle b/buildSrc/src/main/groovy/rpki-ripe-ncc.build-conventions.gradle index c3c18a6..dd43574 100644 --- a/buildSrc/src/main/groovy/rpki-ripe-ncc.build-conventions.gradle +++ b/buildSrc/src/main/groovy/rpki-ripe-ncc.build-conventions.gradle @@ -10,13 +10,26 @@ group = 'net.ripe.rpki-ripe-ncc' repositories { mavenLocal() - mavenCentral() + mavenCentral() { + content { + excludeGroupByRegex "com\\.thales\\.esecurity\\.*" + excludeGroupByRegex "com\\.ncipher\\.nfast\\.*" + } + } maven { url = uri('https://oss.sonatype.org/content/repositories/releases') + content { + excludeGroupByRegex "com\\.thales\\.esecurity\\.*" + excludeGroupByRegex "com\\.ncipher\\.nfast\\.*" + } } maven { url = uri('https://oss.sonatype.org/content/repositories/snapshots') + content { + excludeGroupByRegex "com\\.thales\\.esecurity\\.*" + excludeGroupByRegex "com\\.ncipher\\.nfast\\.*" + } } maven { diff --git a/dependencies.gradle b/dependencies.gradle index ab855f0..507f781 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -1,4 +1,4 @@ ext { - rpki_commons_version = '1.34' + rpki_commons_version = '1.36' spring_boot_version = '2.7.16' } diff --git a/src/main/java/net/ripe/rpki/core/write/services/command/CommandServiceImpl.java b/src/main/java/net/ripe/rpki/core/write/services/command/CommandServiceImpl.java index 20dcf6e..f2f1831 100644 --- a/src/main/java/net/ripe/rpki/core/write/services/command/CommandServiceImpl.java +++ b/src/main/java/net/ripe/rpki/core/write/services/command/CommandServiceImpl.java @@ -10,13 +10,12 @@ import net.ripe.rpki.core.events.CertificateAuthorityEventVisitor; import net.ripe.rpki.domain.ManagedCertificateAuthority; import net.ripe.rpki.domain.audit.CommandAuditService; +import net.ripe.rpki.rest.exception.PreconditionRequiredException; import net.ripe.rpki.ripencc.support.event.EventDelegateTracker; import net.ripe.rpki.ripencc.support.event.EventSubscription; import net.ripe.rpki.server.api.commands.CertificateAuthorityCommand; import net.ripe.rpki.server.api.commands.CommandContext; -import net.ripe.rpki.server.api.services.command.CommandService; -import net.ripe.rpki.server.api.services.command.CommandStatus; -import net.ripe.rpki.server.api.services.command.CommandWithoutEffectException; +import net.ripe.rpki.server.api.services.command.*; import net.ripe.rpki.services.impl.handlers.CommandHandlerMetrics; import net.ripe.rpki.services.impl.handlers.LockCertificateAuthorityHandler; import net.ripe.rpki.services.impl.handlers.MessageDispatcher; @@ -127,6 +126,10 @@ private CommandStatus executeCommandWithRetries(CertificateAuthorityCommand comm log.info("Command failed with possibly transient locking exception {}, retry {} in {} ms: {}", e.getClass().getName(), retryCount, sleepForMs, command); sleepUninterruptibly(sleepForMs, TimeUnit.MILLISECONDS); } + } catch (EntityTagDoesNotMatchException | PreconditionRequiredException | IllegalResourceException | NotHolderOfResourcesException | PrivateAsnsUsedException e) { + // Do not log these user generated commands: This causes very noisy logs. + log.info("Aborted a (user) command: {} for reason {}", command, e.getMessage()); + throw e; } catch (Exception e) { log.warn("Error processing command: {}", command, e); throw e; diff --git a/src/main/java/net/ripe/rpki/domain/GenericPublishedObject.java b/src/main/java/net/ripe/rpki/domain/GenericPublishedObject.java index c115596..4b91adb 100644 --- a/src/main/java/net/ripe/rpki/domain/GenericPublishedObject.java +++ b/src/main/java/net/ripe/rpki/domain/GenericPublishedObject.java @@ -40,6 +40,12 @@ public abstract class GenericPublishedObject extends EntitySupport { @NonNull protected byte[] content = new byte[0]; + /** + * The time at which this object was created. + * + * Do not parse the object for this value if an approximate value is available since parsing violates + * abstraction layers. + */ @Column(name = "created_at", nullable = false) @Getter private Instant createdAt; diff --git a/src/main/java/net/ripe/rpki/domain/ManagedCertificateAuthority.java b/src/main/java/net/ripe/rpki/domain/ManagedCertificateAuthority.java index 8ccb40f..c44ac57 100644 --- a/src/main/java/net/ripe/rpki/domain/ManagedCertificateAuthority.java +++ b/src/main/java/net/ripe/rpki/domain/ManagedCertificateAuthority.java @@ -338,8 +338,9 @@ private void activatePendingKey(KeyPairEntity newKeyPair) { * @return false if NO key was activated */ public boolean activatePendingKeys(Duration minStagingTime) { + DateTime cutOffTime = new DateTime().minus(minStagingTime); return findPendingKeyPair() - .filter(pkp -> pkp.getStatusChangedAt(KeyPairStatus.PENDING).isBefore(new DateTime().minus(minStagingTime))) + .filter(pkp -> pkp.getStatusChangedAt(KeyPairStatus.PENDING).isBefore(cutOffTime)) .map(pkp -> { activatePendingKey(pkp); return true; diff --git a/src/main/java/net/ripe/rpki/domain/PublishedObject.java b/src/main/java/net/ripe/rpki/domain/PublishedObject.java index 4eb0836..dc3fed7 100644 --- a/src/main/java/net/ripe/rpki/domain/PublishedObject.java +++ b/src/main/java/net/ripe/rpki/domain/PublishedObject.java @@ -5,7 +5,7 @@ import net.ripe.rpki.commons.crypto.ValidityPeriod; import net.ripe.rpki.domain.manifest.ManifestEntity; import org.apache.commons.lang3.Validate; -import org.joda.time.Instant; +import org.joda.time.DateTime; import javax.persistence.*; import java.net.URI; @@ -63,14 +63,15 @@ protected PublishedObject() { } public PublishedObject( - @NonNull KeyPairEntity issuingKeyPair, - @NonNull String filename, - byte[] content, - boolean includedInManifest, - @NonNull URI publicationDirectory, - @NonNull ValidityPeriod validityPeriod + @NonNull KeyPairEntity issuingKeyPair, + @NonNull String filename, + byte[] content, + boolean includedInManifest, + @NonNull URI publicationDirectory, + @NonNull ValidityPeriod validityPeriod, + @NonNull DateTime createdAt ) { - super(content, validityPeriod.getNotValidBefore().toInstant()); + super(content, createdAt.toInstant()); this.issuingKeyPair = issuingKeyPair; this.filename = filename; this.includedInManifest = includedInManifest; @@ -79,6 +80,22 @@ public PublishedObject( this.validityPeriod = new EmbeddedValidityPeriod(validityPeriod); } + /** + * Construct a PublishedObject with implicit createdAt from the validity period. + * + * Do not use for CMS signed objects or CRLs + */ + public PublishedObject( + @NonNull KeyPairEntity issuingKeyPair, + @NonNull String filename, + byte[] content, + boolean includedInManifest, + @NonNull URI publicationDirectory, + @NonNull ValidityPeriod validityPeriod + ) { + this(issuingKeyPair, filename, content, includedInManifest, publicationDirectory, validityPeriod, validityPeriod.getNotValidBefore()); + } + @NonNull public URI getUri() { return URI.create(directory).resolve(filename); diff --git a/src/main/java/net/ripe/rpki/domain/aspa/AspaConfiguration.java b/src/main/java/net/ripe/rpki/domain/aspa/AspaConfiguration.java index 7036ea4..6375b9f 100644 --- a/src/main/java/net/ripe/rpki/domain/aspa/AspaConfiguration.java +++ b/src/main/java/net/ripe/rpki/domain/aspa/AspaConfiguration.java @@ -1,37 +1,19 @@ package net.ripe.rpki.domain.aspa; +import com.google.common.collect.ImmutableSortedSet; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import net.ripe.ipresource.Asn; import net.ripe.rpki.domain.ManagedCertificateAuthority; import net.ripe.rpki.ncc.core.domain.support.EntitySupport; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import net.ripe.rpki.server.api.dto.AspaConfigurationData; -import net.ripe.rpki.server.api.dto.AspaProviderData; -import javax.persistence.CollectionTable; -import javax.persistence.Column; -import javax.persistence.ElementCollection; -import javax.persistence.Entity; -import javax.persistence.EnumType; -import javax.persistence.Enumerated; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.MapKeyColumn; -import javax.persistence.OneToOne; -import javax.persistence.OrderBy; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; +import javax.persistence.*; import javax.validation.constraints.NotEmpty; -import java.util.Collections; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; +import java.util.*; import java.util.stream.Collectors; import static net.ripe.rpki.util.Streams.streamToSortedMap; @@ -58,25 +40,20 @@ public class AspaConfiguration extends EntitySupport { private Asn customerAsn; /** - * Set of provider ASNs with the AFI limit (null allows both IPv4 and IPv6, otherwise only the specified address - * family is allowed). + * Set of provider ASNs. */ - @ElementCollection(fetch = FetchType.EAGER) - @MapKeyColumn(name = "provider_asn") - @Enumerated(value = EnumType.STRING) - @Column(name = "afi_limit") - @CollectionTable(name = "aspaconfiguration_providers", joinColumns = @JoinColumn(name = "aspaconfiguration_id")) - @OrderBy("provider_asn") @NotEmpty - private SortedMap<@NonNull Asn, @NonNull AspaAfiLimit> providers = new TreeMap<>(); + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "aspaconfiguration_providers") + private Set<@NonNull Asn> providers = new TreeSet<>(); - public AspaConfiguration(@NonNull ManagedCertificateAuthority certificateAuthority, @NonNull Asn customerAsn, @NonNull Map providers) { + public AspaConfiguration(@NonNull ManagedCertificateAuthority certificateAuthority, @NonNull Asn customerAsn, @NonNull SortedSet providers) { this.certificateAuthority = certificateAuthority; this.customerAsn = customerAsn; - this.providers.putAll(providers); + this.providers.addAll(providers); } - public static SortedMap> entitiesToMaps(SortedMap entities) { + public static SortedMap> entitiesToMaps(SortedMap entities) { return streamToSortedMap( entities.values().stream(), AspaConfiguration::getCustomerAsn, @@ -84,20 +61,18 @@ public static SortedMap> entitiesToMaps(Sorted ); } - public SortedMap getProviders() { - return Collections.unmodifiableSortedMap(providers); + public SortedSet getProviders() { + return ImmutableSortedSet.copyOf(providers); } - public void setProviders(Map providers) { - this.providers = new TreeMap<>(providers); + public void setProviders(SortedSet providers) { + this.providers = new TreeSet<>(providers); } public AspaConfigurationData toData() { return new AspaConfigurationData( getCustomerAsn(), - providers.entrySet().stream() - .map(entry -> new AspaProviderData(entry.getKey(), entry.getValue())) - .collect(Collectors.toList()) + List.copyOf(getProviders()) ); } } diff --git a/src/main/java/net/ripe/rpki/domain/aspa/AspaConfigurationRepository.java b/src/main/java/net/ripe/rpki/domain/aspa/AspaConfigurationRepository.java index 2bd3fe2..e728d59 100644 --- a/src/main/java/net/ripe/rpki/domain/aspa/AspaConfigurationRepository.java +++ b/src/main/java/net/ripe/rpki/domain/aspa/AspaConfigurationRepository.java @@ -9,6 +9,8 @@ public interface AspaConfigurationRepository { SortedMap findByCertificateAuthority(ManagedCertificateAuthority certificateAuthority); + SortedMap findConfigurationsWithProvidersByCertificateAuthority(ManagedCertificateAuthority certificateAuthority); + Collection findAll(); void add(AspaConfiguration aspaConfiguration); diff --git a/src/main/java/net/ripe/rpki/domain/aspa/AspaEntity.java b/src/main/java/net/ripe/rpki/domain/aspa/AspaEntity.java index d371581..a4e5038 100644 --- a/src/main/java/net/ripe/rpki/domain/aspa/AspaEntity.java +++ b/src/main/java/net/ripe/rpki/domain/aspa/AspaEntity.java @@ -1,33 +1,24 @@ package net.ripe.rpki.domain.aspa; +import com.google.common.collect.ImmutableSortedSet; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import net.ripe.ipresource.Asn; import net.ripe.rpki.commons.crypto.cms.aspa.AspaCms; import net.ripe.rpki.commons.crypto.cms.aspa.AspaCmsParser; -import net.ripe.rpki.commons.crypto.cms.aspa.ProviderAS; import net.ripe.rpki.commons.validation.ValidationResult; import net.ripe.rpki.domain.OutgoingResourceCertificate; import net.ripe.rpki.domain.PublishedObject; import net.ripe.rpki.ncc.core.domain.support.EntitySupport; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import net.ripe.rpki.server.api.services.command.UnparseableRpkiObjectException; import org.apache.commons.lang.Validate; -import javax.persistence.CascadeType; -import javax.persistence.Entity; -import javax.persistence.FetchType; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.JoinColumn; -import javax.persistence.OneToOne; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; -import javax.persistence.Transient; +import javax.persistence.*; import java.net.URI; import java.util.List; import java.util.SortedMap; +import java.util.SortedSet; import static net.ripe.rpki.util.Streams.streamToSortedMap; @@ -51,19 +42,25 @@ public class AspaEntity extends EntitySupport { @Getter private PublishedObject publishedObject; + @Getter + @Setter + @Column(name = "profile_version", nullable = false) + private Long profileVersion; + @Transient private AspaCms cms; - public AspaEntity(OutgoingResourceCertificate eeCertificate, AspaCms aspaCms, String filename, URI directory) { + public AspaEntity(OutgoingResourceCertificate eeCertificate, AspaCms aspaCms, String filename, URI directory, long profileVersion) { super(); Validate.notNull(eeCertificate); Validate.notNull(aspaCms); this.certificate = eeCertificate; this.publishedObject = new PublishedObject( - eeCertificate.getSigningKeyPair(), filename, aspaCms.getEncoded(), true, directory, aspaCms.getValidityPeriod()); + eeCertificate.getSigningKeyPair(), filename, aspaCms.getEncoded(), true, directory, aspaCms.getValidityPeriod(), aspaCms.getSigningTime()); + this.profileVersion = profileVersion; } - public static SortedMap> entitiesToMaps(List entities) { + public static SortedMap> entitiesToMaps(List entities) { return streamToSortedMap( entities.stream(), AspaEntity::getCustomerAsn, @@ -106,11 +103,7 @@ public Asn getCustomerAsn() { return getAspaCms().getCustomerAsn(); } - public SortedMap getProviders() { - return streamToSortedMap( - getAspaCms().getProviderASSet().stream(), - ProviderAS::getProviderAsn, - providerAS -> AspaAfiLimit.fromOptionalAddressFamily(providerAS.getAfiLimit()) - ); + public SortedSet getProviders() { + return ImmutableSortedSet.copyOf(getAspaCms().getProviderASSet()); } } diff --git a/src/main/java/net/ripe/rpki/domain/aspa/AspaEntityServiceBean.java b/src/main/java/net/ripe/rpki/domain/aspa/AspaEntityServiceBean.java index a7d9563..893e1d7 100644 --- a/src/main/java/net/ripe/rpki/domain/aspa/AspaEntityServiceBean.java +++ b/src/main/java/net/ripe/rpki/domain/aspa/AspaEntityServiceBean.java @@ -10,7 +10,6 @@ import net.ripe.rpki.commons.crypto.ValidityPeriod; import net.ripe.rpki.commons.crypto.cms.aspa.AspaCms; import net.ripe.rpki.commons.crypto.cms.aspa.AspaCmsBuilder; -import net.ripe.rpki.commons.crypto.cms.aspa.ProviderAS; import net.ripe.rpki.commons.crypto.rfc3779.ResourceExtension; import net.ripe.rpki.commons.crypto.x509cert.CertificateInformationAccessUtil; import net.ripe.rpki.commons.crypto.x509cert.X509CertificateInformationAccessDescriptor; @@ -30,7 +29,6 @@ import net.ripe.rpki.domain.interca.CertificateIssuanceRequest; import net.ripe.rpki.domain.naming.RepositoryObjectNamingStrategy; import net.ripe.rpki.server.api.commands.CommandContext; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import net.ripe.rpki.server.api.services.command.UnparseableRpkiObjectException; import org.apache.commons.lang3.tuple.Pair; import org.joda.time.DateTime; @@ -50,6 +48,8 @@ @Service @Slf4j public class AspaEntityServiceBean implements AspaEntityService, CertificateAuthorityEventVisitor { + public static final long CURRENT_ASPA_PROFILE_VERSION = 16L; + private final CertificateAuthorityRepository certificateAuthorityRepository; private final AspaConfigurationRepository aspaConfigurationRepository; private final AspaEntityRepository aspaEntityRepository; @@ -97,39 +97,44 @@ public void visitIncomingCertificateRevokedEvent(IncomingCertificateRevokedEvent * are fully up-to-date with the configuration. */ public Pair, SortedMap> validateAspaConfiguration(ManagedCertificateAuthority ca) { - SortedMap aspaEntities = aspaEntityRepository.findCurrentByCertificateAuthority(ca).stream() - .collect(toSortedMap(AspaEntity::getCustomerAsn, x -> x)); + // Not all ASPA entities can necessarily be parsed (e.g. profile change). While all ASNs should have 0..1 ASPA, + // there can be many unparesable ASPAs + List aspaEntities = aspaEntityRepository.findCurrentByCertificateAuthority(ca); Optional maybeCurrentIncomingResourceCertificate = ca.findCurrentIncomingResourceCertificate(); if (!maybeCurrentIncomingResourceCertificate.isPresent()) { // No current resource certificate, so all ASPA entities are invalid and without resources there is // no applicable configuration - return Pair.of(aspaEntities.values(), Collections.emptySortedMap()); + return Pair.of(aspaEntities, Collections.emptySortedMap()); } IncomingResourceCertificate incomingResourceCertificate = maybeCurrentIncomingResourceCertificate.get(); - Map> validatedAspaEntities = aspaEntities.values().stream() - .collect(Collectors.partitioningBy(aspa -> isValidAspaEntity(incomingResourceCertificate, aspa))); + Map> validatedAspaEntities = aspaEntities.stream() + .collect(Collectors.partitioningBy(aspa -> isValidAspaEntity(incomingResourceCertificate, aspa))); - SortedMap aspaConfiguration = aspaConfigurationRepository.findByCertificateAuthority(ca) + // Aspa Configurations covered by resource certificate + SortedMap aspaConfiguration = aspaConfigurationRepository.findConfigurationsWithProvidersByCertificateAuthority(ca) .values() .stream() .filter(x -> incomingResourceCertificate.getCertifiedResources().contains(x.getCustomerAsn())) .collect(toSortedMap(AspaConfiguration::getCustomerAsn, x -> x)); - SortedMapDifference> difference = Maps.difference( + SortedMapDifference> difference = Maps.difference( AspaConfiguration.entitiesToMaps(aspaConfiguration), AspaEntity.entitiesToMaps(validatedAspaEntities.get(true)) ); + Map validAspaEntitiesByAsn = validatedAspaEntities.get(true) + .stream() + .collect(Collectors.toMap(AspaEntity::getCustomerAsn, x -> x)); + List invalidAspaEntities = Stream.concat( validatedAspaEntities.get(false).stream(), Stream.concat( difference.entriesOnlyOnRight().keySet().stream(), difference.entriesDiffering().keySet().stream() - ) - .map(aspaEntities::get) + ).map(validAspaEntitiesByAsn::get) ) .collect(Collectors.toList()); @@ -153,24 +158,51 @@ public void updateAspaIfNeeded(ManagedCertificateAuthority ca) { aspaEntity.revokeAndRemove(aspaEntityRepository); } for (AspaConfiguration aspaConfiguration : validated.getRight().values()) { - AspaEntity aspaEntity = createAspaEntity(ca, aspaConfiguration); - aspaEntityRepository.add(aspaEntity); + Optional aspaEntity = createAspaEntity(ca, aspaConfiguration); + aspaEntity.ifPresent(aspaEntityRepository::add); } } private static boolean isValidAspaEntity(IncomingResourceCertificate incomingResourceCertificate, AspaEntity aspa) { + boolean isValidAndCurrent = false; try { - return aspa.getCertificate().isValid() - && aspa.getCertificate().getSigningKeyPair().isCurrent() - && Objects.equals(incomingResourceCertificate.getPublicationUri(), aspa.getAspaCms().getParentCertificateUri()) - && incomingResourceCertificate.getCertifiedResources().contains(aspa.getCustomerAsn()); + isValidAndCurrent = aspa.getCertificate().isValid() + && aspa.getCertificate().getSigningKeyPair().isCurrent() + && aspa.getProfileVersion() == CURRENT_ASPA_PROFILE_VERSION + && Objects.equals(incomingResourceCertificate.getPublicationUri(), aspa.getAspaCms().getParentCertificateUri()) + && incomingResourceCertificate.getCertifiedResources().contains(aspa.getCustomerAsn()); + + return isValidAndCurrent; } catch (UnparseableRpkiObjectException e) { return false; + } finally { + try { + if (!isValidAndCurrent && log.isInfoEnabled()) { + log.info("Will re-issue ASPA at {} certificate-valid={} keypair-current={} profile-version={} (current={}) parent-uri={} resources-match={}", + aspa.getCertificate().isValid(), aspa.getCertificate().getSigningKeyPair().isCurrent(), aspa.getProfileVersion(), CURRENT_ASPA_PROFILE_VERSION, + Objects.equals(incomingResourceCertificate.getPublicationUri(), aspa.getAspaCms().getParentCertificateUri()), incomingResourceCertificate.getCertifiedResources().contains(aspa.getCustomerAsn()) + ); + } + } catch (Exception e) { + // Ignore exceptions while printing debug message. + } } } + /** + * Create AspaEntity if possible + * @param certificateAuthority CA to issue under + * @param aspaConfiguration configuration to apply + * @return AspaEntity if possible, or empty. + */ @VisibleForTesting - AspaEntity createAspaEntity(ManagedCertificateAuthority certificateAuthority, AspaConfiguration aspaConfiguration) { + Optional createAspaEntity(ManagedCertificateAuthority certificateAuthority, AspaConfiguration aspaConfiguration) { + // Filter out configurations that can not result in a valid ASPA entity, and would cause failures when trying + // to get the CMS payload + if (aspaConfiguration.getProviders().isEmpty() || !certificateAuthority.getCertifiedResources().contains(aspaConfiguration.getCustomerAsn())) { + return Optional.empty(); + } + DateTime now = DateTime.now(DateTimeZone.UTC); KeyPairEntity currentKeyPair = certificateAuthority.getCurrentKeyPair(); @@ -183,8 +215,8 @@ AspaEntity createAspaEntity(ManagedCertificateAuthority certificateAuthority, As AspaCms aspaCms = generateAspaCms(aspaConfiguration, eeKeyPair, endEntityCertificate.getCertificate()); URI publicationDirectory = CertificateInformationAccessUtil.extractPublicationDirectory( currentKeyPair.getCurrentIncomingCertificate().getSia()); - return new AspaEntity(endEntityCertificate, aspaCms, - informationAccessStrategy.aspaFilename(endEntityCertificate), publicationDirectory); + return Optional.of(new AspaEntity(endEntityCertificate, aspaCms, + informationAccessStrategy.aspaFilename(endEntityCertificate), publicationDirectory, CURRENT_ASPA_PROFILE_VERSION)); } private OutgoingResourceCertificate createEndEntityCertificate( @@ -199,10 +231,8 @@ private AspaCms generateAspaCms(AspaConfiguration aspaConfiguration, KeyPair eeK AspaCmsBuilder builder = new AspaCmsBuilder(); builder.withCertificate(endEntityX509ResourceCertificate); builder.withCustomerAsn(aspaConfiguration.getCustomerAsn()); - builder.withProviderASSet(aspaConfiguration.getProviders().entrySet().stream() - .map(entry -> new ProviderAS(entry.getKey(), entry.getValue().toOptionalAddressFamily())) - .collect(Collectors.toList()) - ); + builder.withProviderASSet(aspaConfiguration.getProviders()); + builder.withSignatureProvider(singleUseKeyPairFactory.signatureProvider()); return builder.build(eeKeyPair.getPrivate()); } diff --git a/src/main/java/net/ripe/rpki/domain/crl/CrlEntity.java b/src/main/java/net/ripe/rpki/domain/crl/CrlEntity.java index ce069b4..bca65f6 100644 --- a/src/main/java/net/ripe/rpki/domain/crl/CrlEntity.java +++ b/src/main/java/net/ripe/rpki/domain/crl/CrlEntity.java @@ -139,7 +139,7 @@ public void update(ValidityPeriod validityPeriod, ResourceCertificateRepository withdraw(); setPublishedObject(new PublishedObject( - keyPair, keyPair.getCrlFilename(), encoded, true, keyPair.getCertificateRepositoryLocation(), validityPeriod)); + keyPair, keyPair.getCrlFilename(), encoded, true, keyPair.getCertificateRepositoryLocation(), validityPeriod, builder.getThisUpdateTime())); } private X509CrlBuilder newCrlBuilderWithEntries(Collection revokedCertificates) { diff --git a/src/main/java/net/ripe/rpki/domain/manifest/ManifestEntity.java b/src/main/java/net/ripe/rpki/domain/manifest/ManifestEntity.java index f71c899..98f8837 100644 --- a/src/main/java/net/ripe/rpki/domain/manifest/ManifestEntity.java +++ b/src/main/java/net/ripe/rpki/domain/manifest/ManifestEntity.java @@ -154,7 +154,7 @@ public void update(OutgoingResourceCertificate eeCertificate, ManifestCms manifestCms = buildManifestCms(entries, eeCertificateKeyPair, signatureProvider); - publishedObject = new PublishedObject(keyPair, keyPair.getManifestFilename(), manifestCms.getEncoded(), false, keyPair.getCertificateRepositoryLocation(), manifestCms.getValidityPeriod()); + publishedObject = new PublishedObject(keyPair, keyPair.getManifestFilename(), manifestCms.getEncoded(), false, keyPair.getCertificateRepositoryLocation(), manifestCms.getValidityPeriod(), manifestCms.getSigningTime()); this.nextNumber++; } diff --git a/src/main/java/net/ripe/rpki/domain/roa/RoaEntity.java b/src/main/java/net/ripe/rpki/domain/roa/RoaEntity.java index 3d0aa40..f927669 100644 --- a/src/main/java/net/ripe/rpki/domain/roa/RoaEntity.java +++ b/src/main/java/net/ripe/rpki/domain/roa/RoaEntity.java @@ -59,7 +59,7 @@ public RoaEntity(OutgoingResourceCertificate eeCertificate, RoaCms roaCms, Strin Validate.notNull(roaCms); this.certificate = eeCertificate; this.publishedObject = new PublishedObject( - eeCertificate.getSigningKeyPair(), filename, roaCms.getEncoded(), true, directory, roaCms.getValidityPeriod()); + eeCertificate.getSigningKeyPair(), filename, roaCms.getEncoded(), true, directory, roaCms.getValidityPeriod(), roaCms.getSigningTime()); } @Transient diff --git a/src/main/java/net/ripe/rpki/offline/ra/service/TrustAnchorResponseProcessor.java b/src/main/java/net/ripe/rpki/offline/ra/service/TrustAnchorResponseProcessor.java index 102483a..1e4bf59 100644 --- a/src/main/java/net/ripe/rpki/offline/ra/service/TrustAnchorResponseProcessor.java +++ b/src/main/java/net/ripe/rpki/offline/ra/service/TrustAnchorResponseProcessor.java @@ -1,7 +1,9 @@ package net.ripe.rpki.offline.ra.service; +import lombok.extern.slf4j.Slf4j; import net.ripe.rpki.commons.crypto.CertificateRepositoryObject; import net.ripe.rpki.commons.crypto.util.EncodedPublicKey; +import net.ripe.rpki.commons.crypto.util.SignedObjectUtil; import net.ripe.rpki.domain.*; import net.ripe.rpki.domain.archive.KeyPairDeletionService; import net.ripe.rpki.domain.interca.CertificateIssuanceResponse; @@ -17,8 +19,8 @@ import net.ripe.rpki.commons.ta.domain.response.SigningResponse; import net.ripe.rpki.commons.ta.domain.response.TaResponse; import net.ripe.rpki.commons.ta.domain.response.TrustAnchorResponse; -import net.ripe.rpki.util.PublishedObjectUtil; import org.joda.time.DateTime; +import org.joda.time.Instant; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -41,6 +43,7 @@ * I.e. stuff like: - republishing the offline objects - notifying the * production CA about new certs / revoked keys */ +@Slf4j @Component public class TrustAnchorResponseProcessor { @@ -95,17 +98,27 @@ List applyChangeToPublishedObjects(Map result = new ArrayList<>(); final Map activeObjects = convertToMap(trustAnchorPublishedObjectRepository.findActiveObjects()); + // Use the same time as fallback for all files in a request + var now = Instant.now(); objectsToPublish.forEach((uri, objectToPublish) -> { + Instant creationTime; + try { + creationTime = SignedObjectUtil.getFileCreationTime(uri, objectToPublish.getEncoded()); + } catch (SignedObjectUtil.NoTimeParsedException e) { + log.error("Could not determine creation time for object: " + uri, e); + creationTime = now; + } + if (activeObjects.containsKey(uri)) { final TrustAnchorPublishedObject publishedObject = activeObjects.remove(uri); if (!objectsAreSame(publishedObject, objectToPublish, uri)) { publishedObject.withdraw(); result.add(publishedObject); - result.add(new TrustAnchorPublishedObject(uri, objectToPublish.getEncoded(), PublishedObjectUtil.getFileCreationTime(uri, objectToPublish.getEncoded()))); + result.add(new TrustAnchorPublishedObject(uri, objectToPublish.getEncoded(), creationTime)); } } else { - result.add(new TrustAnchorPublishedObject(uri, objectToPublish.getEncoded(), PublishedObjectUtil.getFileCreationTime(uri, objectToPublish.getEncoded()))); + result.add(new TrustAnchorPublishedObject(uri, objectToPublish.getEncoded(), creationTime)); } }); withdrawObjects(activeObjects.values()); diff --git a/src/main/java/net/ripe/rpki/rest/exception/RestExceptionControllerAdvice.java b/src/main/java/net/ripe/rpki/rest/exception/RestExceptionControllerAdvice.java index 936a0af..a414602 100644 --- a/src/main/java/net/ripe/rpki/rest/exception/RestExceptionControllerAdvice.java +++ b/src/main/java/net/ripe/rpki/rest/exception/RestExceptionControllerAdvice.java @@ -1,7 +1,7 @@ package net.ripe.rpki.rest.exception; import com.google.common.collect.ImmutableMap; -import net.ripe.rpki.server.api.services.command.DuplicateResourceException; +import net.ripe.rpki.server.api.services.command.IllegalResourceException; import net.ripe.rpki.server.api.services.command.EntityTagDoesNotMatchException; import net.ripe.rpki.server.api.services.command.NotHolderOfResourcesException; import net.ripe.rpki.server.api.services.command.PrivateAsnsUsedException; @@ -50,7 +50,7 @@ public class RestExceptionControllerAdvice { CaNameInvalidException.class, NotHolderOfResourcesException.class, PrivateAsnsUsedException.class, - DuplicateResourceException.class, + IllegalResourceException.class, ConstraintViolationException.class }) public ResponseEntity> exceptionsResultingInBadRequestHandler(HttpServletRequest req, Exception e) { diff --git a/src/main/java/net/ripe/rpki/rest/service/CaRoaConfigurationService.java b/src/main/java/net/ripe/rpki/rest/service/CaRoaConfigurationService.java index 7725996..1ef737d 100644 --- a/src/main/java/net/ripe/rpki/rest/service/CaRoaConfigurationService.java +++ b/src/main/java/net/ripe/rpki/rest/service/CaRoaConfigurationService.java @@ -10,6 +10,7 @@ import net.ripe.rpki.commons.validation.roa.AnnouncedRoute; import net.ripe.rpki.commons.validation.roa.RouteOriginValidationPolicy; import net.ripe.rpki.commons.validation.roa.RouteValidityState; +import net.ripe.rpki.domain.roa.RoaConfigurationRepository; import net.ripe.rpki.rest.pojo.BgpAnnouncement; import net.ripe.rpki.rest.pojo.BgpAnnouncementChange; import net.ripe.rpki.rest.pojo.PublishSet; @@ -17,11 +18,7 @@ import net.ripe.rpki.rest.pojo.ROAExtended; import net.ripe.rpki.rest.pojo.ROAWithAnnouncementStatus; import net.ripe.rpki.server.api.commands.UpdateRoaConfigurationCommand; -import net.ripe.rpki.server.api.dto.BgpRisEntry; -import net.ripe.rpki.server.api.dto.HostedCertificateAuthorityData; -import net.ripe.rpki.server.api.dto.RoaAlertConfigurationData; -import net.ripe.rpki.server.api.dto.RoaConfigurationData; -import net.ripe.rpki.server.api.dto.RoaConfigurationPrefixData; +import net.ripe.rpki.server.api.dto.*; import net.ripe.rpki.server.api.services.command.CommandService; import net.ripe.rpki.server.api.services.read.BgpRisEntryViewService; import net.ripe.rpki.server.api.services.read.RoaAlertConfigurationViewService; @@ -35,15 +32,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import static com.google.common.collect.ImmutableMap.of; @@ -66,6 +55,7 @@ public class CaRoaConfigurationService extends AbstractCaRestService { @Autowired public CaRoaConfigurationService(RoaViewService roaViewService, + RoaConfigurationRepository roaConfigurationRepository, BgpRisEntryViewService bgpRisEntryViewService, RoaAlertConfigurationViewService roaAlertConfigurationViewService, CommandService commandService) { @@ -195,6 +185,23 @@ public ResponseEntity stageRoaChanges(@PathVariable("caName") final CaName ca final Set currentRoutes = new HashSet<>(currentRoaConfiguration.toAllowedRoutes()); final Set futureRoutes = new HashSet<>(futureRoas.size()); + IpResourceSet affectedRanges; + try { + affectedRanges = buildAffectedRanges(futureRoas, futureRoutes, currentRoutes); + } catch (IllegalArgumentException e) { + return ResponseEntity.status(BAD_REQUEST).body(Map.of(ERROR, e.getMessage())); + } + + final List bgpAnnouncementChanges = getBgpAnnouncementChanges( + ca, currentRoutes, futureRoutes, bgpAnnouncements, affectedRanges); + + return ResponseEntity.ok() + .contentType(MediaType.APPLICATION_JSON) + .eTag(currentRoaConfiguration.entityTag()) + .body(bgpAnnouncementChanges); + } + + private IpResourceSet buildAffectedRanges(List futureRoas, Set futureRoutes, Set currentRoutes) { final IpResourceSet affectedRanges = new IpResourceSet(); for (ROA roa : futureRoas) { final IpRange roaIpRange = IpRange.parse(roa.getPrefix()); @@ -203,11 +210,35 @@ public ResponseEntity stageRoaChanges(@PathVariable("caName") final CaName ca if (!currentRoutes.contains(route)) affectedRanges.add(roaIpRange); } + + final Map> futureRoaMap = Utils.makeROAMap(futureRoas); + Optional e = Utils.validateUniqueROAs("Error in future ROAs", futureRoaMap); + if (e.isPresent()) { + throw new IllegalArgumentException(e.get()); + } + for (AllowedRoute route : currentRoutes) { if (!futureRoutes.contains(route)) affectedRanges.add(route.getPrefix()); + + final AnnouncedRoute key = new AnnouncedRoute(route.getAsn(), route.getPrefix()); + if (futureRoaMap.containsKey(key)) { + futureRoaMap.get(key).forEach(futureMaxLength -> { + if (!Objects.equals(futureMaxLength, route.getMaximumLength())) { + // the same ASN+prefix and different max length + throw new IllegalArgumentException(Utils.getSameROAErrorMessage(route, key, futureMaxLength)); + } + }); + } } + return affectedRanges; + } + private List getBgpAnnouncementChanges(HostedCertificateAuthorityData ca, + Set currentRoutes, + Set futureRoutes, + Map> bgpAnnouncements, + IpResourceSet affectedRanges) { final NestedIntervalMap> currentRouteMap = allowedRoutesToNestedIntervalMap(currentRoutes); final NestedIntervalMap> futureRouteMap = allowedRoutesToNestedIntervalMap(futureRoutes); final Set ignoredAnnouncements = getIgnoredAnnouncement(ca.getId()); @@ -228,11 +259,7 @@ public ResponseEntity stageRoaChanges(@PathVariable("caName") final CaName ca } } } - - return ResponseEntity.ok() - .contentType(MediaType.APPLICATION_JSON) - .eTag(currentRoaConfiguration.entityTag()) - .body(result); + return result; } @PostMapping(path = "publish") @@ -269,14 +296,21 @@ public ResponseEntity publishROAs(@PathVariable("caName") final CaName caName if (ifMatchHeader != null && publishSet.getIfMatch() != null && !ifMatchHeader.equals(publishSet.getIfMatch())) { return badRequest("`If-Match` header and `ifMatch` field do not match"); } - String ifMatch = StringUtils.defaultIfEmpty(ifMatchHeader, publishSet.getIfMatch()); + final String ifMatch = StringUtils.defaultIfEmpty(ifMatchHeader, publishSet.getIfMatch()); try { + Utils.validateNoIdenticalROAs( + roaViewService.getRoaConfiguration(ca.getId()), + publishSet.getAdded(), publishSet.getDeleted()) + .ifPresent(rc -> { + throw new IllegalArgumentException(rc); + }); + commandService.execute(new UpdateRoaConfigurationCommand( - ca.getVersionedId(), - Optional.ofNullable(ifMatch), - getRoaConfigurationPrefixDatas(publishSet.getAdded()), - getRoaConfigurationPrefixDatas(publishSet.getDeleted()) + ca.getVersionedId(), + Optional.ofNullable(ifMatch), + getRoaConfigurationPrefixDatas(publishSet.getAdded()), + getRoaConfigurationPrefixDatas(publishSet.getDeleted()) )); return noContent(); } catch (Exception e) { 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 7ce2117..6c932bf 100644 --- a/src/main/java/net/ripe/rpki/rest/service/PublisherRepositoriesService.java +++ b/src/main/java/net/ripe/rpki/rest/service/PublisherRepositoriesService.java @@ -57,7 +57,7 @@ @RequestMapping(path = API_URL_PREFIX + "/{caName}", produces = {APPLICATION_JSON}) @Tag(name = "/ca/{caName}", description = "Operations on CAs") public class PublisherRepositoriesService extends AbstractCaRestService { - private static final Charset CHARSET = StandardCharsets.UTF_8; + public static final String NON_HOSTED_PUBLISHERS_ARE_NOT_AVAILABLE = "non hosted publishers are not available for this instance."; private final CertificateAuthorityViewService certificateAuthorityViewService; private final CommandService commandService; @@ -105,7 +105,7 @@ public ResponseEntity provisionNonHostedPublicationRepository( log.info("Publisher request for non-hosted CA: {}", caName); if (maybeNonHostedPublisherRepositoryService.isEmpty()) { - return ResponseEntity.status(NOT_ACCEPTABLE).body("non hosted publishers are not available for this instance."); + return ResponseEntity.status(NOT_ACCEPTABLE).body(NON_HOSTED_PUBLISHERS_ARE_NOT_AVAILABLE); } var nonHostedPublisherRepositoryService = this.maybeNonHostedPublisherRepositoryService.orElseThrow(); @@ -122,7 +122,7 @@ public ResponseEntity provisionNonHostedPublicationRepository( UUID publisherHandle = UUID.randomUUID(); try { final InputStream uploadedInputStream = file.getInputStream(); - final String repositoryRequestBody = IOUtils.toString(uploadedInputStream, CHARSET); + final String repositoryRequestBody = IOUtils.toString(uploadedInputStream, StandardCharsets.UTF_8); PublisherRequest publisherRequest = new PublisherRequestSerializer().deserialize(repositoryRequestBody); // Core commands must be idempotent (and are automatically retried on transient failures) and this does not @@ -158,7 +158,7 @@ public ResponseEntity downloadNonHostedPublicationRepositoryResponse( @PathVariable("publisherHandle") UUID publisherHandle ) { if (maybeNonHostedPublisherRepositoryService.isEmpty()) { - return ResponseEntity.status(NOT_ACCEPTABLE).body("non hosted publishers are not available for this instance."); + return ResponseEntity.status(NOT_ACCEPTABLE).body(NON_HOSTED_PUBLISHERS_ARE_NOT_AVAILABLE); } log.info("Download repository non-hosted publication response for CA: {}", caName); @@ -175,7 +175,7 @@ public ResponseEntity downloadNonHostedPublicationRepositoryResponse( return ResponseEntity.ok() .header("content-disposition", "attachment; filename = " + filename) .contentType(TEXT_XML) - .body(xml.getBytes(CHARSET)); + .body(xml.getBytes(StandardCharsets.UTF_8)); } catch (EntityNotFoundException e) { throw new CaNotFoundException(e.getMessage()); } @@ -188,7 +188,7 @@ public ResponseEntity deleteNonHostedPublicationRepository( @PathVariable("publisherHandle") UUID publisherHandle ) { if (maybeNonHostedPublisherRepositoryService.isEmpty()) { - return ResponseEntity.status(NOT_ACCEPTABLE).body("non hosted publishers are not available for this instance."); + return ResponseEntity.status(NOT_ACCEPTABLE).body(NON_HOSTED_PUBLISHERS_ARE_NOT_AVAILABLE); } var nonHostedPublisherRepositoryService = this.maybeNonHostedPublisherRepositoryService.orElseThrow(); diff --git a/src/main/java/net/ripe/rpki/rest/service/Utils.java b/src/main/java/net/ripe/rpki/rest/service/Utils.java index c7e1c19..e76a55e 100644 --- a/src/main/java/net/ripe/rpki/rest/service/Utils.java +++ b/src/main/java/net/ripe/rpki/rest/service/Utils.java @@ -1,9 +1,12 @@ package net.ripe.rpki.rest.service; import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Streams; +import com.nimbusds.jose.util.Pair; import lombok.AccessLevel; import lombok.NoArgsConstructor; import lombok.NonNull; +import lombok.Value; import net.ripe.ipresource.Asn; import net.ripe.ipresource.IpRange; import net.ripe.ipresource.IpResource; @@ -17,6 +20,8 @@ import net.ripe.rpki.rest.pojo.ROA; import net.ripe.rpki.server.api.dto.BgpRisEntry; import net.ripe.rpki.server.api.dto.RoaAlertConfigurationData; +import net.ripe.rpki.server.api.dto.RoaConfigurationData; +import net.ripe.rpki.server.api.dto.RoaConfigurationPrefixData; import net.ripe.rpki.server.api.services.read.RoaAlertConfigurationViewService; import org.springframework.http.ResponseEntity; @@ -157,4 +162,102 @@ protected static ResponseEntity badRequestError(String errorMessage) { return ResponseEntity.badRequest().body(Map.of("error", errorMessage)); } + static Optional validateNoIdenticalROAs(RoaConfigurationData roaConfigurationData, List newRoas, List roasToDelete) { + final List existingROAs = roaConfigurationData.getPrefixes().stream() + .map(p -> new ExistingROA(p.getAsn(), p.getPrefix(), p.getNullableMaxLength())) + .collect(Collectors.toList()); + return validateNoIdenticalROAs(existingROAs, newRoas, roasToDelete); + } + + /** + * Validate that there are not existing ROAs having the same AS and Prefix + * but different Max Length fields. + * + * @return Optional error text for the first validation error that was found. + */ + static Optional validateNoIdenticalROAs(List existingROAs, List newRoas, List roasToDelete) { + + final Map> newOnes = makeROAMap(newRoas); + final Map> deletedOnes = makeROAMap(roasToDelete); + + Optional e = validateUniqueROAs("Error in new ROAs", newOnes); + if (e.isPresent()) return e; + + e = validateUniqueROAs("Error in deleted ROAs", deletedOnes); + if (e.isPresent()) return e; + + final Map> newOnesUnique = uniqueEntries(newOnes); + final Map> deletedOnesUnique = uniqueEntries(deletedOnes); + + for (var existingRoa : existingROAs) { + var key = new AnnouncedRoute(existingRoa.getAsn(), existingRoa.getPrefix()); + if (deletedOnesUnique.containsKey(key)) { + var maxLengthToDelete = deletedOnesUnique.get(key).orElse(null); + if (Objects.equals(existingRoa.getMaximumLength(), maxLengthToDelete)) { + newOnesUnique.remove(key); + } + } + } + + for (var existingRoa : existingROAs) { + var key = new AnnouncedRoute(existingRoa.getAsn(), existingRoa.getPrefix()); + if (newOnesUnique.containsKey(key)) { + final Integer newMaxLength = newOnesUnique.get(key).orElse(null); + if (!Objects.equals(existingRoa.getMaximumLength(), newMaxLength)) { + // we are not going to delete existing one + return Optional.of(getSameROAErrorMessage(existingRoa, key, newMaxLength)); + } + } + } + return Optional.empty(); + } + + public static Map> uniqueEntries(Map> m) { + var newM = new HashMap>(m.size()); + m.forEach((k, list) -> newM.put(k, Optional.ofNullable(list.get(0)))); + return newM; + } + + public static Optional validateUniqueROAs(String prefix, Map> newOnes) { + for (var e : newOnes.entrySet()) { + if (e.getValue().size() > 1) { + return Optional.of(String.format("%s: there are more than one pair (%s, %s), max lengths: %s", + prefix, e.getKey().getOriginAsn(), e.getKey().getPrefix(), e.getValue())); + } + } + return Optional.empty(); + } + + public static String getSameROAErrorMessage(Object existingRoa, AnnouncedRoute key, Integer newMaxLength) { + return String.format( + "There is an overlap in ROAs: existing %s has the same (ASN, prefix) as added %s", + existingRoa, + new ROA(key.getOriginAsn().toString(), key.getPrefix().toString(), newMaxLength)); + } + + public static Map> makeROAMap(List newRoas) { + return newRoas.stream().map(r -> { + AnnouncedRoute announcedRoute = new AnnouncedRoute(Asn.parse(r.getAsn()), IpRange.parse(r.getPrefix())); + return Pair.of(announcedRoute, Collections.singletonList(r.getMaxLength())); + }).collect(Collectors.toMap(Pair::getLeft, Pair::getRight, + (a, b) -> Streams.concat(a.stream(), b.stream()) + .collect(Collectors.toList()))); + } + + @Value + public static class ExistingROA { + Asn asn; + IpRange prefix; + Integer maximumLength; + + @Override + public String toString() { + return "ROA{" + + "asn=" + asn + + ", prefix=" + prefix + + ", maximumLength=" + maximumLength + + '}'; + } + } + } diff --git a/src/main/java/net/ripe/rpki/ripencc/services/impl/RestResourceServicesClient.java b/src/main/java/net/ripe/rpki/ripencc/services/impl/RestResourceServicesClient.java index 22d3008..9e6b75c 100644 --- a/src/main/java/net/ripe/rpki/ripencc/services/impl/RestResourceServicesClient.java +++ b/src/main/java/net/ripe/rpki/ripencc/services/impl/RestResourceServicesClient.java @@ -23,7 +23,7 @@ class RestResourceServicesClient implements ResourceServicesClient { private static final String TOTAL_RESOURCES = "total-resources"; - private static final String MONITORING_HEALTHCHECK = "monitoring/healthcheck"; + static final String HEALTHCHECK_PATH = "charged-resources-api/actuator/health"; private final Gson gson = new Gson(); private final Client resourceServices; @@ -47,7 +47,7 @@ public RestResourceServicesClient( public boolean isAvailable() { log.debug("Checking if internet resources REST API is available"); try ( - Response response = resourceServices.target(resourceServicesUrl).path(MONITORING_HEALTHCHECK) + Response response = resourceServices.target(resourceServicesUrl).path(HEALTHCHECK_PATH) .request(MediaType.APPLICATION_JSON) .head() ) { diff --git a/src/main/java/net/ripe/rpki/server/api/commands/UpdateAspaConfigurationCommand.java b/src/main/java/net/ripe/rpki/server/api/commands/UpdateAspaConfigurationCommand.java index a513a1d..138021e 100644 --- a/src/main/java/net/ripe/rpki/server/api/commands/UpdateAspaConfigurationCommand.java +++ b/src/main/java/net/ripe/rpki/server/api/commands/UpdateAspaConfigurationCommand.java @@ -33,7 +33,7 @@ public static String getHumanReadableAspaConfiguration(List provider.getProviderAsn() + " [" + provider.getAfiLimit() + "]") + .map(Object::toString) .collect(Collectors.joining(", ")); } } diff --git a/src/main/java/net/ripe/rpki/server/api/dto/AspaAfiLimit.java b/src/main/java/net/ripe/rpki/server/api/dto/AspaAfiLimit.java deleted file mode 100644 index 2a075bf..0000000 --- a/src/main/java/net/ripe/rpki/server/api/dto/AspaAfiLimit.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.ripe.rpki.server.api.dto; - -import net.ripe.rpki.commons.crypto.rfc3779.AddressFamily; - -import java.util.Optional; - -/** See ASPA profile afiLimit */ -public enum AspaAfiLimit { - /** The authorization is valid for both IPv4 and IPv6 announcements. */ - ANY, - /** the authorization is valid only for IPv4 announcements. */ - IPv4, - /** the authorization is valid only for IPv6 announcements. */ - IPv6; - - public static AspaAfiLimit fromOptionalAddressFamily(Optional maybeAddressFamily) { - return maybeAddressFamily - .map(addressFamily -> addressFamily == AddressFamily.IPV4 ? AspaAfiLimit.IPv4 : AspaAfiLimit.IPv6) - .orElse(AspaAfiLimit.ANY); - } - - public Optional toOptionalAddressFamily() { - switch (this) { - case ANY: - return Optional.empty(); - case IPv4: - return Optional.of(AddressFamily.IPV4); - case IPv6: - return Optional.of(AddressFamily.IPV6); - } - throw new IllegalStateException("unknown AspaAfiLimit " + this); - } -} diff --git a/src/main/java/net/ripe/rpki/server/api/dto/AspaConfigurationData.java b/src/main/java/net/ripe/rpki/server/api/dto/AspaConfigurationData.java index da0a0cd..2d014f7 100644 --- a/src/main/java/net/ripe/rpki/server/api/dto/AspaConfigurationData.java +++ b/src/main/java/net/ripe/rpki/server/api/dto/AspaConfigurationData.java @@ -10,6 +10,8 @@ import java.nio.charset.StandardCharsets; import java.util.List; import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.stream.Stream; import static net.ripe.rpki.util.Streams.streamToSortedMap; @@ -22,24 +24,23 @@ public class AspaConfigurationData { @NonNull Asn customerAsn; + /** + * Use a list of providers so we can perform additional validation and explicitly reject duplicate values. + * The entity restricts this to a Set w/ unique constraints in the database. + */ @NonNull - @NotEmpty - List providers; + List providers; - public static String entityTag(SortedMap> aspaConfiguration) { + public static String entityTag(SortedMap> aspaConfiguration) { String json = GSON.toJson(aspaConfiguration); return Streams.entityTag(Stream.of(json.getBytes(StandardCharsets.UTF_8))); } - public static SortedMap> dataToMaps(List configuration) { + public static SortedMap> dataToMaps(List configuration) { return streamToSortedMap( configuration.stream(), AspaConfigurationData::getCustomerAsn, - aspaConfiguration -> streamToSortedMap( - aspaConfiguration.getProviders().stream(), - AspaProviderData::getProviderAsn, - AspaProviderData::getAfiLimit - ) + ac -> new TreeSet<>(ac.getProviders()) ); } } diff --git a/src/main/java/net/ripe/rpki/server/api/dto/AspaProviderData.java b/src/main/java/net/ripe/rpki/server/api/dto/AspaProviderData.java deleted file mode 100644 index 065759d..0000000 --- a/src/main/java/net/ripe/rpki/server/api/dto/AspaProviderData.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.ripe.rpki.server.api.dto; - -import lombok.NonNull; -import lombok.Value; -import net.ripe.ipresource.Asn; - -@Value -public class AspaProviderData { - @NonNull - Asn providerAsn; - @NonNull - AspaAfiLimit afiLimit; -} diff --git a/src/main/java/net/ripe/rpki/server/api/dto/RoaConfigurationPrefixData.java b/src/main/java/net/ripe/rpki/server/api/dto/RoaConfigurationPrefixData.java index b7f24d1..f310ac3 100644 --- a/src/main/java/net/ripe/rpki/server/api/dto/RoaConfigurationPrefixData.java +++ b/src/main/java/net/ripe/rpki/server/api/dto/RoaConfigurationPrefixData.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Getter; import net.ripe.ipresource.Asn; import net.ripe.ipresource.IpRange; import net.ripe.rpki.commons.crypto.cms.roa.RoaPrefix; @@ -43,10 +44,12 @@ public class RoaConfigurationPrefixData extends ValueObjectSupport { }; // cycle when serialised using default serialiser + @Getter @JsonSerialize(using = ToStringSerializer.class) private final Asn asn; // cycle when serialised using default serialiser + @Getter @JsonSerialize(using = ToStringSerializer.class) private final IpRange prefix; @@ -64,24 +67,20 @@ public RoaConfigurationPrefixData(Asn asn, IpRange prefix, Integer maximumLength this(asn, new RoaPrefix(prefix, maximumLength)); } - public Asn getAsn() { - return asn; - } - @JsonIgnore public RoaPrefix getRoaPrefix() { return new RoaPrefix(prefix, maximumLength); } - public IpRange getPrefix() { - return prefix; - } - @JsonProperty("maxLength") public int getMaximumLength() { return maximumLength == null ? getPrefix().getPrefixLength() : maximumLength; } + public Integer getNullableMaxLength() { + return maximumLength; + } + @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/net/ripe/rpki/server/api/services/command/DuplicateResourceException.java b/src/main/java/net/ripe/rpki/server/api/services/command/IllegalResourceException.java similarity index 62% rename from src/main/java/net/ripe/rpki/server/api/services/command/DuplicateResourceException.java rename to src/main/java/net/ripe/rpki/server/api/services/command/IllegalResourceException.java index c0041be..a0a32c8 100644 --- a/src/main/java/net/ripe/rpki/server/api/services/command/DuplicateResourceException.java +++ b/src/main/java/net/ripe/rpki/server/api/services/command/IllegalResourceException.java @@ -4,11 +4,11 @@ /** * This exception indicates that duplicate resources are configured. */ -public class DuplicateResourceException extends CertificationException { +public class IllegalResourceException extends CertificationException { private static final long serialVersionUID = 1L; - public DuplicateResourceException(String message) { + public IllegalResourceException(String message) { super(message); } } diff --git a/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateAspaConfigurationCommandHandler.java b/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateAspaConfigurationCommandHandler.java index af7353a..c174d89 100644 --- a/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateAspaConfigurationCommandHandler.java +++ b/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateAspaConfigurationCommandHandler.java @@ -13,21 +13,17 @@ import net.ripe.rpki.domain.aspa.AspaConfiguration; import net.ripe.rpki.domain.aspa.AspaConfigurationRepository; import net.ripe.rpki.server.api.commands.UpdateAspaConfigurationCommand; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import net.ripe.rpki.server.api.dto.AspaConfigurationData; import net.ripe.rpki.server.api.services.command.CommandStatus; import net.ripe.rpki.server.api.services.command.CommandWithoutEffectException; -import net.ripe.rpki.server.api.services.command.DuplicateResourceException; +import net.ripe.rpki.server.api.services.command.IllegalResourceException; import net.ripe.rpki.server.api.services.command.EntityTagDoesNotMatchException; import net.ripe.rpki.server.api.services.command.NotHolderOfResourcesException; import net.ripe.rpki.server.api.services.command.PrivateAsnsUsedException; import org.springframework.beans.factory.annotation.Value; import javax.inject.Inject; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.SortedMap; +import java.util.*; import java.util.stream.Collectors; @@ -58,37 +54,55 @@ public Class commandType() { @Override public void handle(@NonNull UpdateAspaConfigurationCommand command, CommandStatus commandStatus) { + validateUpdateAspaConfigurationCommand(command); + + ManagedCertificateAuthority ca = lookupManagedCa(command.getCertificateAuthorityId()); SortedMap entities = aspaConfigurationRepository.findByCertificateAuthority(ca); - SortedMap> currentConfiguration = AspaConfiguration.entitiesToMaps(entities); + SortedMap> currentConfiguration = AspaConfiguration.entitiesToMaps(entities); validateEntityTag(command, currentConfiguration); - SortedMap> updatedConfiguration = parseUpdatedConfiguration(ca, command); + SortedMap> updatedConfiguration = parseUpdatedConfiguration(ca, command); - SortedMapDifference> difference = Maps.difference(currentConfiguration, updatedConfiguration); + SortedMapDifference> difference = Maps.difference(currentConfiguration, updatedConfiguration); if (difference.areEqual()) { throw new CommandWithoutEffectException(command); } for (Asn removed : difference.entriesOnlyOnLeft().keySet()) { aspaConfigurationRepository.remove(entities.get(removed)); } - for (Map.Entry> added : difference.entriesOnlyOnRight().entrySet()) { + for (Map.Entry> added : difference.entriesOnlyOnRight().entrySet()) { aspaConfigurationRepository.add(new AspaConfiguration(ca, added.getKey(), added.getValue())); } - for (Map.Entry>> updated : difference.entriesDiffering().entrySet()) { + for (Map.Entry>> updated : difference.entriesDiffering().entrySet()) { entities.get(updated.getKey()).setProviders(updated.getValue().rightValue()); } ca.markConfigurationUpdated(); } - private SortedMap> parseUpdatedConfiguration(ManagedCertificateAuthority ca, UpdateAspaConfigurationCommand command) { - SortedMap> updatedConfiguration; + private void validateUpdateAspaConfigurationCommand(UpdateAspaConfigurationCommand command) { + var configuration = command.getConfiguration(); + if (configuration.stream().map(AspaConfigurationData::getCustomerAsn).distinct().count() != configuration.size()) { + throw new IllegalResourceException("duplicate customer ASN in ASPA configuration"); + } + + if (configuration.stream().anyMatch(ac -> ac.getProviders().stream().distinct().count() != ac.getProviders().size())) { + throw new IllegalResourceException("duplicate provider ASN in ASPA configuration"); + } + + if (configuration.stream().anyMatch(ac -> ac.getProviders().isEmpty())) { + throw new IllegalResourceException("One of the configured ASPAs does not have providers"); + } + } + + private SortedMap> parseUpdatedConfiguration(ManagedCertificateAuthority ca, UpdateAspaConfigurationCommand command) { + SortedMap> updatedConfiguration; try { updatedConfiguration = AspaConfigurationData.dataToMaps(command.getConfiguration()); } catch (IllegalStateException e) { - throw new DuplicateResourceException("duplicate ASN in ASPA configuration"); + throw new IllegalResourceException("duplicate ASN in ASPA configuration"); } validateCustomerAsns(ca, updatedConfiguration); @@ -97,14 +111,14 @@ private SortedMap> parseUpdatedConfiguration(M return updatedConfiguration; } - private static void validateEntityTag(UpdateAspaConfigurationCommand command, SortedMap> currentConfiguration) { + private static void validateEntityTag(UpdateAspaConfigurationCommand command, SortedMap> currentConfiguration) { String entityTag = AspaConfigurationData.entityTag(currentConfiguration); if (!entityTag.equals(command.getIfMatch())) { throw new EntityTagDoesNotMatchException(entityTag, command.getIfMatch()); } } - private static void validateCustomerAsns(ManagedCertificateAuthority ca, SortedMap> updatedConfiguration) { + private static void validateCustomerAsns(ManagedCertificateAuthority ca, SortedMap> updatedConfiguration) { ImmutableResourceSet certifiedResources = ca.getCertifiedResources(); ImmutableResourceSet uncertifiedAsns = ImmutableResourceSet.of(updatedConfiguration.keySet()).difference(certifiedResources); if (!uncertifiedAsns.isEmpty()) { @@ -112,13 +126,13 @@ private static void validateCustomerAsns(ManagedCertificateAuthority ca, SortedM } } - private void validateProviderAsns(SortedMap> configuration) { - for (Map.Entry> aspa : configuration.entrySet()) { + private void validateProviderAsns(SortedMap> configuration) { + for (Map.Entry> aspa : configuration.entrySet()) { Asn customerAsn = aspa.getKey(); - Set providerAsns = aspa.getValue().keySet(); + Set providerAsns = aspa.getValue(); if (providerAsns.contains(customerAsn)) { - throw new DuplicateResourceException(String.format("customer %s appears in provider set %s", customerAsn, providerAsns)); + throw new IllegalResourceException(String.format("customer %s appears in provider set %s", customerAsn, providerAsns)); } } @@ -128,10 +142,10 @@ private void validateProviderAsns(SortedMap> c } } - private List findAddedPrivateAsns(SortedMap> configuration) { + private List findAddedPrivateAsns(SortedMap> configuration) { return configuration.values().stream() - .flatMap(providers -> providers.keySet().stream()) - .filter(privateAsns::contains) - .collect(Collectors.toList()); + .flatMap(Collection::stream) + .filter(privateAsns::contains) + .collect(Collectors.toList()); } } diff --git a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaAspaConfigurationRepository.java b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaAspaConfigurationRepository.java index 31a76ce..1dfa504 100644 --- a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaAspaConfigurationRepository.java +++ b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaAspaConfigurationRepository.java @@ -30,6 +30,19 @@ public SortedMap findByCertificateAuthority(ManagedCerti ); } + @Override + public SortedMap findConfigurationsWithProvidersByCertificateAuthority(ManagedCertificateAuthority ca) { + Stream aspaConfigurationStream = manager + .createQuery("from AspaConfiguration ac where ac.certificateAuthority.id = :caId and ac.providers is not empty order by customerAsn", AspaConfiguration.class) + .setParameter("caId", ca.getId()) + .getResultStream(); + return streamToSortedMap( + aspaConfigurationStream, + AspaConfiguration::getCustomerAsn, + x -> x + ); + } + @Override protected Class getEntityClass() { return AspaConfiguration.class; diff --git a/src/main/java/net/ripe/rpki/util/PublishedObjectUtil.java b/src/main/java/net/ripe/rpki/util/PublishedObjectUtil.java deleted file mode 100644 index db3d16f..0000000 --- a/src/main/java/net/ripe/rpki/util/PublishedObjectUtil.java +++ /dev/null @@ -1,51 +0,0 @@ -package net.ripe.rpki.util; - -import lombok.experimental.UtilityClass; -import lombok.extern.slf4j.Slf4j; -import net.ripe.rpki.commons.crypto.cms.GenericRpkiSignedObjectParser; -import net.ripe.rpki.commons.crypto.crl.X509Crl; -import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificateParser; -import net.ripe.rpki.commons.util.RepositoryObjectType; -import net.ripe.rpki.commons.validation.ValidationResult; -import org.joda.time.Instant; - -import java.net.URI; -import java.util.Base64; - -@Slf4j -@UtilityClass -public class PublishedObjectUtil { - - // FIXME: Should be moved into rpki-commons after we use >=1.35 in core because this is a port of code present in commons test-cases and in rsyncit. - public static Instant getFileCreationTime(URI uri, byte[] decoded) { - var objectUri = uri.toString(); - final RepositoryObjectType objectType = RepositoryObjectType.parse(objectUri); - try { - switch (objectType) { - case Manifest: - case Aspa: - case Roa: - case Gbr: - var signedObjectParser = new GenericRpkiSignedObjectParser(); - - signedObjectParser.parse(ValidationResult.withLocation(objectUri), decoded); - return Instant.ofEpochMilli(signedObjectParser.getSigningTime().getMillis()); - case Certificate: - X509ResourceCertificateParser x509CertificateParser = new X509ResourceCertificateParser(); - x509CertificateParser.parse(ValidationResult.withLocation(objectUri), decoded); - final var cert = x509CertificateParser.getCertificate().getCertificate(); - return Instant.ofEpochMilli(cert.getNotBefore().getTime()); - case Crl: - var x509Crl = X509Crl.parseDerEncoded(decoded, ValidationResult.withLocation(objectUri)); - var crl = x509Crl.getCrl(); - return Instant.ofEpochMilli(crl.getThisUpdate().getTime()); - case Unknown: - log.error("Unknown object type for object url = {}"); - return Instant.now(); - } - } catch (Exception e) { - log.error("Could not parse the object url = {}, body = {} :", objectUri, Base64.getEncoder().encodeToString(decoded)); - } - return Instant.now(); - } -} diff --git a/src/main/java/net/ripe/rpki/web/UpstreamCaController.java b/src/main/java/net/ripe/rpki/web/UpstreamCaController.java index 5a5730c..2718abe 100644 --- a/src/main/java/net/ripe/rpki/web/UpstreamCaController.java +++ b/src/main/java/net/ripe/rpki/web/UpstreamCaController.java @@ -28,6 +28,7 @@ import javax.security.auth.x500.X500Principal; import java.util.*; import java.util.function.Function; +import java.util.function.Supplier; import static java.nio.charset.StandardCharsets.UTF_8; @@ -39,6 +40,8 @@ public class UpstreamCaController extends BaseController { public static final String UPSTREAM_CA = "upstream-ca"; public static final String PAGE_TYPE = "pageType"; public static final String ACA_KEY_STATUS = "acaKeyStatus"; + public static final String ADMIN_INDEX_PAGE = "admin/index"; + public static final String ACTIVE_NODE_FORM = "activeNodeForm"; private final CertificateAuthorityViewService certificateAuthorityViewService; private final CommandService commandService; @@ -52,8 +55,7 @@ public UpstreamCaController(RepositoryConfiguration repositoryConfiguration, CommandService commandService, AllCaCertificateUpdateServiceBean allCaCertificateUpdateServiceBean, Map backgroundServiceMap, - GitProperties gitProperties - ) { + GitProperties gitProperties) { super(repositoryConfiguration, activeNodeService, gitProperties); this.certificateAuthorityViewService = certificateAuthorityViewService; this.commandService = commandService; @@ -75,6 +77,9 @@ public ModelAndView upstreamCa() { if (allResourcesCa.getTrustAnchorRequest() == null) { model.put(PAGE_TYPE, "create-request"); } else { + // Do not show any extra key life-cycle buttons to avoid + // clicking wrong button when uploading TA response. + model.put(ACA_KEY_STATUS, "none"); model.put(PAGE_TYPE, "download-request"); model.put("requestFileName", getRequestFileName(allResourcesCa.getTrustAnchorRequest())); } @@ -95,7 +100,7 @@ public Object downloadSignRequest() { final CertificateAuthorityData allResourcesCa = getAllResourcesCa(); if (allResourcesCa == null || allResourcesCa.getTrustAnchorRequest() == null) { final ActiveNodeForm node = new ActiveNodeForm(activeNodeService.getActiveNodeName()); - return new ModelAndView("admin/index", HttpStatus.NOT_FOUND) + return new ModelAndView(ADMIN_INDEX_PAGE, HttpStatus.NOT_FOUND) .addObject("error", "All Resources CA or signing request do not exist.") .addObject("activeNodeForm", node); } @@ -134,28 +139,31 @@ public Object uploadSignResponse(@RequestParam("response") MultipartFile file, R } @PostMapping({"/revoke-old-aca-key"}) - public RedirectView revokeOldAcaKey() { - return (RedirectView) withAllResourcesCa(allResourcesCa -> { - commandService.execute(new KeyManagementRevokeOldKeysCommand(allResourcesCa.getVersionedId())); - return new RedirectView(UPSTREAM_CA, true); - }); + public Object revokeOldAcaKey() { + return withAllResourcesCa(allResourcesCa -> + ifNoTaRequest(allResourcesCa, () -> { + commandService.execute(new KeyManagementRevokeOldKeysCommand(allResourcesCa.getVersionedId())); + return new RedirectView(UPSTREAM_CA, true); + })); } @PostMapping({"/activate-pending-aca-key"}) - public RedirectView activateAcaPendingKey() { - return (RedirectView) withAllResourcesCa(allResourcesCa -> { - commandService.execute(KeyManagementActivatePendingKeysCommand.manualActivationCommand(allResourcesCa.getVersionedId())); - allCaCertificateUpdateServiceBean.execute(Collections.emptyMap()); - return new RedirectView(UPSTREAM_CA, true); - }); + public Object activateAcaPendingKey() { + return withAllResourcesCa(allResourcesCa -> + ifNoTaRequest(allResourcesCa, () -> { + commandService.execute(KeyManagementActivatePendingKeysCommand.manualActivationCommand(allResourcesCa.getVersionedId())); + allCaCertificateUpdateServiceBean.execute(Collections.emptyMap()); + return new RedirectView(UPSTREAM_CA, true); + })); } @PostMapping({"/initiate-rolling-aca-key"}) - public RedirectView initiateRollingAcaKey() { - return (RedirectView) withAllResourcesCa(allResourcesCa -> { - commandService.execute(new KeyManagementInitiateRollCommand(allResourcesCa.getVersionedId(), 0)); - return new RedirectView(UPSTREAM_CA, true); - }); + public Object initiateRollingAcaKey() { + return withAllResourcesCa(allResourcesCa -> + ifNoTaRequest(allResourcesCa, () -> { + commandService.execute(new KeyManagementInitiateRollCommand(allResourcesCa.getVersionedId(), 0)); + return new RedirectView(UPSTREAM_CA, true); + })); } private Object withAllResourcesCa(Function f) { @@ -171,8 +179,18 @@ private Object withAllResourcesCa(Function f) { + if (allResourcesCa.getTrustAnchorRequest() != null) { + final ActiveNodeForm node = new ActiveNodeForm(activeNodeService.getActiveNodeName()); + return new ModelAndView("admin/index", HttpStatus.BAD_REQUEST) + .addObject("error", "All resources CA already has a TA request, it must be processed first.") + .addObject("activeNodeForm", node); } + return f.get(); } private static String getRequestFileName(TrustAnchorRequest taRequest) { diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 6fb1c04..760dd55 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -24,7 +24,6 @@ spring: devtools.livereload.enabled: false thymeleaf: cache: false - prefix: file:src/main/resources/WEB-INF/templates/ background-services: schedule.enable: false @@ -98,7 +97,7 @@ publication: server.url: https://localhost:7766/ riswhoisdump: - base.url: http://localhost:8080/certification/static/dev-ris + base.url: http://localhost:8080/certification/riswhois update.interval.hours: 1 ta.repository: diff --git a/src/main/resources/db/migration/V126__add_aspa_providers.sql b/src/main/resources/db/migration/V126__add_aspa_providers.sql new file mode 100644 index 0000000..954205c --- /dev/null +++ b/src/main/resources/db/migration/V126__add_aspa_providers.sql @@ -0,0 +1,13 @@ +ALTER TABLE aspaconfiguration_providers RENAME TO aspaconfiguration_providers_v14; + +CREATE TABLE aspaconfiguration_providers ( + aspa_configuration_id BIGINT NOT NULL, + providers numeric NOT NULL + ); +ALTER TABLE aspaconfiguration_providers + ADD CONSTRAINT aspaconfiguration_providers_pkey PRIMARY KEY (aspa_configuration_id, providers); +ALTER TABLE aspaconfiguration_providers + ADD CONSTRAINT aspaconfiguration_providers_id_fkey FOREIGN KEY (aspa_configuration_id) REFERENCES aspaconfiguration(id) ON UPDATE RESTRICT ON DELETE CASCADE; + +INSERT INTO aspaconfiguration_providers(aspa_configuration_id, providers) SELECT aspaconfiguration_id, provider_asn FROM aspaconfiguration_providers_v14; +DROP TABLE aspaconfiguration_providers_v14; \ No newline at end of file diff --git a/src/main/resources/db/migration/V127__add_aspa_profile_version.sql b/src/main/resources/db/migration/V127__add_aspa_profile_version.sql new file mode 100644 index 0000000..79fcefe --- /dev/null +++ b/src/main/resources/db/migration/V127__add_aspa_profile_version.sql @@ -0,0 +1,11 @@ +ALTER TABLE aspaentity ADD COLUMN profile_version bigint; +/* When this SQL is executed, no objects have transitioned to the new profile */ +UPDATE aspaentity SET profile_version = 14; + +ALTER TABLE aspaentity ALTER COLUMN profile_version SET NOT NULL; +/* + * All CAs need to be revisisted for a new ASPA profile + * + * This will cause the Public Repository Publication Service to visit all CAs + */ +UPDATE certificateauthority SET configuration_updated_at = NOW() WHERE type != 'NONHOSTED'; \ No newline at end of file diff --git a/src/test/resources/static/riswhois/riswhoisdump.IPv4.gz b/src/main/resources/static/riswhois/riswhoisdump.IPv4.gz similarity index 100% rename from src/test/resources/static/riswhois/riswhoisdump.IPv4.gz rename to src/main/resources/static/riswhois/riswhoisdump.IPv4.gz diff --git a/src/test/resources/static/riswhois/riswhoisdump.IPv6.gz b/src/main/resources/static/riswhois/riswhoisdump.IPv6.gz similarity index 100% rename from src/test/resources/static/riswhois/riswhoisdump.IPv6.gz rename to src/main/resources/static/riswhois/riswhoisdump.IPv6.gz diff --git a/src/test/java/net/ripe/rpki/domain/aspa/AspaConfigurationMaintenanceServiceBeanTest.java b/src/test/java/net/ripe/rpki/domain/aspa/AspaConfigurationMaintenanceServiceBeanTest.java index efbf560..c15fbba 100644 --- a/src/test/java/net/ripe/rpki/domain/aspa/AspaConfigurationMaintenanceServiceBeanTest.java +++ b/src/test/java/net/ripe/rpki/domain/aspa/AspaConfigurationMaintenanceServiceBeanTest.java @@ -1,5 +1,6 @@ package net.ripe.rpki.domain.aspa; +import com.google.common.collect.ImmutableSortedSet; import net.ripe.ipresource.Asn; import net.ripe.ipresource.ImmutableResourceSet; import net.ripe.ipresource.IpResourceSet; @@ -12,7 +13,6 @@ import net.ripe.rpki.domain.audit.CommandAudit; import net.ripe.rpki.server.api.commands.CommandContext; import net.ripe.rpki.server.api.commands.UpdateAllIncomingResourceCertificatesCommand; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,7 +50,7 @@ public class AspaConfigurationMaintenanceServiceBeanTest { @Before public void setUp() { - aspaConfiguration = new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, Collections.singletonMap(PROVIDER_ASN, AspaAfiLimit.IPv4)); + aspaConfiguration = new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, ImmutableSortedSet.of(PROVIDER_ASN)); when(certificateAuthority.getVersionedId()).thenReturn(CERTIFICATE_AUTHORITY_ID); when(certificateAuthorityRepository.findManagedCa(CA_ID)).thenReturn(certificateAuthority); when(aspaConfigurationRepository.findByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(CUSTOMER_ASN, aspaConfiguration))); diff --git a/src/test/java/net/ripe/rpki/domain/aspa/AspaEntityServiceBeanTest.java b/src/test/java/net/ripe/rpki/domain/aspa/AspaEntityServiceBeanTest.java index 1504bb1..e983044 100644 --- a/src/test/java/net/ripe/rpki/domain/aspa/AspaEntityServiceBeanTest.java +++ b/src/test/java/net/ripe/rpki/domain/aspa/AspaEntityServiceBeanTest.java @@ -5,8 +5,6 @@ import net.ripe.ipresource.ImmutableResourceSet; import net.ripe.ipresource.IpResourceSet; import net.ripe.rpki.commons.crypto.cms.aspa.AspaCms; -import net.ripe.rpki.commons.crypto.cms.aspa.ProviderAS; -import net.ripe.rpki.commons.crypto.rfc3779.AddressFamily; import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificate; import net.ripe.rpki.core.events.KeyPairActivatedEvent; import net.ripe.rpki.domain.CertificateAuthorityRepository; @@ -21,7 +19,6 @@ import net.ripe.rpki.domain.interca.CertificateIssuanceResponse; import net.ripe.rpki.server.api.commands.CommandContext; import net.ripe.rpki.server.api.commands.KeyManagementActivatePendingKeysCommand; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import org.joda.time.DateTimeUtils; import org.junit.After; import org.junit.Before; @@ -33,15 +30,13 @@ import java.math.BigInteger; import java.net.URI; -import java.util.Collections; -import java.util.Optional; -import java.util.TreeMap; -import java.util.UUID; +import java.util.*; import static net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificateTest.createSelfSignedCaResourceCertificateBuilder; import static net.ripe.rpki.domain.TestObjects.CA_ID; import static net.ripe.rpki.domain.TestObjects.PRODUCTION_CA_NAME; import static net.ripe.rpki.domain.TestObjects.TEST_VALIDITY_PERIOD; +import static net.ripe.rpki.domain.aspa.AspaEntityServiceBean.CURRENT_ASPA_PROFILE_VERSION; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -54,7 +49,9 @@ public class AspaEntityServiceBeanTest { public static final Asn CUSTOMER_ASN = Asn.parse("AS21212"); public static final Asn ASN_1 = Asn.parse("AS1"); + public static final SortedSet PROVIDER_ASN_1 = ImmutableSortedSet.of(ASN_1); public static final Asn ASN_2 = Asn.parse("AS2"); + public static final SortedSet PROVIDER_ASN_2 = ImmutableSortedSet.of(ASN_2); @Mock private CertificateAuthorityRepository certificateAuthorityRepository; @Mock @@ -78,10 +75,10 @@ public void setUp() { when(certificateAuthorityRepository.findManagedCa(CA_ID)).thenReturn(certificateAuthority); - AspaConfiguration aspaConfiguration = new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, Collections.singletonMap(ASN_1, AspaAfiLimit.ANY)); - when(aspaConfigurationRepository.findByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(CUSTOMER_ASN, aspaConfiguration))); + AspaConfiguration aspaConfiguration = new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, PROVIDER_ASN_1); + when(aspaConfigurationRepository.findConfigurationsWithProvidersByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(CUSTOMER_ASN, aspaConfiguration))); - aspaEntity = subject.createAspaEntity(certificateAuthority, new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, Collections.singletonMap(ASN_1, AspaAfiLimit.ANY))); + aspaEntity = subject.createAspaEntity(certificateAuthority, new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, PROVIDER_ASN_1)).get(); when(aspaEntityRepository.findCurrentByCertificateAuthority(certificateAuthority)).thenReturn(Collections.singletonList(aspaEntity)); } @@ -107,8 +104,8 @@ public void should_remove_aspa_entities_signed_by_old_key_on_activation_of_new_k @Test public void should_create_aspa_entity_when_aspa_configuration_for_new_customer_asn_is_added() { - AspaConfiguration aspaConfiguration = new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, Collections.singletonMap(ASN_2, AspaAfiLimit.IPv4)); - when(aspaConfigurationRepository.findByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(CUSTOMER_ASN, aspaConfiguration))); + AspaConfiguration aspaConfiguration = new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, PROVIDER_ASN_2); + when(aspaConfigurationRepository.findConfigurationsWithProvidersByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(CUSTOMER_ASN, aspaConfiguration))); subject.updateAspaIfNeeded(certificateAuthority); @@ -117,13 +114,31 @@ public void should_create_aspa_entity_when_aspa_configuration_for_new_customer_a AspaCms aspaCms = aspaEntityArgumentCaptor.getValue().getAspaCms(); assertThat(aspaCms.getCustomerAsn()).isEqualTo(CUSTOMER_ASN); - assertThat(aspaCms.getProviderASSet()).isEqualTo(ImmutableSortedSet.of(new ProviderAS(ASN_2, Optional.of(AddressFamily.IPV4)))); + assertThat(aspaCms.getProviderASSet()).isEqualTo(PROVIDER_ASN_2); + } + + /** + * Configuration no longer has providers: + * * old AspaEntity should be revoked + * * no new AspaEntity should be created (since providers MUST be present) + */ + @Test + public void should_not_create_aspaentity_for_configuration_without_provider() { + AspaConfiguration aspaConfiguration = new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, new TreeSet<>()); + when(aspaConfigurationRepository.findConfigurationsWithProvidersByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(CUSTOMER_ASN, aspaConfiguration))); + + subject.updateAspaIfNeeded(certificateAuthority); + + verify(aspaEntityRepository, never()).add(any()); + verify(aspaEntityRepository).remove(aspaEntity); + + assertThat(aspaEntity.isRevoked()).isTrue(); } @Test public void should_skip_customer_asn_in_configuration_if_not_covered_by_ca_certified_resource() { - AspaConfiguration aspaConfiguration = new AspaConfiguration(certificateAuthority, ASN_1, Collections.singletonMap(ASN_2, AspaAfiLimit.IPv4)); - when(aspaConfigurationRepository.findByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(ASN_1, aspaConfiguration))); + AspaConfiguration aspaConfiguration = new AspaConfiguration(certificateAuthority, ASN_1, PROVIDER_ASN_2); + when(aspaConfigurationRepository.findConfigurationsWithProvidersByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(ASN_1, aspaConfiguration))); subject.updateAspaIfNeeded(certificateAuthority); @@ -132,7 +147,7 @@ public void should_skip_customer_asn_in_configuration_if_not_covered_by_ca_certi @Test public void should_revoke_and_remove_aspa_entity_when_not_configured_for_customer_asn() { - when(aspaConfigurationRepository.findByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>()); + when(aspaConfigurationRepository.findConfigurationsWithProvidersByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>()); subject.updateAspaIfNeeded(certificateAuthority); @@ -142,8 +157,8 @@ public void should_revoke_and_remove_aspa_entity_when_not_configured_for_custome @Test public void should_revoke_and_remove_old_aspa_entity_and_create_new_aspa_entity_when_providers_changed() { - AspaConfiguration aspaConfiguration = new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, Collections.singletonMap(ASN_2, AspaAfiLimit.IPv4)); - when(aspaConfigurationRepository.findByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(CUSTOMER_ASN, aspaConfiguration))); + AspaConfiguration aspaConfiguration = new AspaConfiguration(certificateAuthority, CUSTOMER_ASN, PROVIDER_ASN_2); + when(aspaConfigurationRepository.findConfigurationsWithProvidersByCertificateAuthority(certificateAuthority)).thenReturn(new TreeMap<>(Collections.singletonMap(CUSTOMER_ASN, aspaConfiguration))); subject.updateAspaIfNeeded(certificateAuthority); @@ -154,7 +169,7 @@ public void should_revoke_and_remove_old_aspa_entity_and_create_new_aspa_entity_ AspaCms aspaCms = aspaEntityArgumentCaptor.getValue().getAspaCms(); assertThat(aspaCms.getCustomerAsn()).isEqualTo(CUSTOMER_ASN); - assertThat(aspaCms.getProviderASSet()).isEqualTo(ImmutableSortedSet.of(new ProviderAS(ASN_2, Optional.of(AddressFamily.IPV4)))); + assertThat(aspaCms.getProviderASSet()).isEqualTo(PROVIDER_ASN_2); } @Test @@ -173,6 +188,25 @@ public void should_reissue_aspas_when_parent_certificate_location_changed() { assertThat(updatedAspaEntity.getProviders()).isEqualTo(aspaEntity.getProviders()); } + @Test + public void should_reissue_aspas_when_aspa_profile_not_current() { + activeKeyPair.getCurrentIncomingCertificate().setPublicationUri(URI.create("rsync://rsync.example.com/new/publication/uri.cer")); + aspaEntity.setProfileVersion(14L); + + subject.updateAspaIfNeeded(certificateAuthority); + + assertThat(aspaEntity.isRevoked()).isTrue(); + verify(aspaEntityRepository).remove(aspaEntity); + + ArgumentCaptor aspaEntityArgumentCaptor = ArgumentCaptor.forClass(AspaEntity.class); + verify(aspaEntityRepository).add(aspaEntityArgumentCaptor.capture()); + AspaEntity updatedAspaEntity = aspaEntityArgumentCaptor.getValue(); + assertThat(updatedAspaEntity.getCustomerAsn()).isEqualTo(aspaEntity.getCustomerAsn()); + assertThat(updatedAspaEntity.getProviders()).isEqualTo(aspaEntity.getProviders()); + // Verify that we track the current version + assertThat(updatedAspaEntity.getProfileVersion()).isEqualTo(CURRENT_ASPA_PROFILE_VERSION); + } + @Test public void should_reissue_aspas_when_ee_certificate_is_not_valid() { aspaEntity.getCertificate().revoke(); diff --git a/src/test/java/net/ripe/rpki/offline/ra/service/TrustAnchorResponseProcessorTest.java b/src/test/java/net/ripe/rpki/offline/ra/service/TrustAnchorResponseProcessorTest.java index 189b60a..7e1a3c3 100644 --- a/src/test/java/net/ripe/rpki/offline/ra/service/TrustAnchorResponseProcessorTest.java +++ b/src/test/java/net/ripe/rpki/offline/ra/service/TrustAnchorResponseProcessorTest.java @@ -3,6 +3,9 @@ import net.ripe.ipresource.IpResourceSet; import net.ripe.rpki.commons.FixedDateRule; import net.ripe.rpki.commons.crypto.CertificateRepositoryObject; +import net.ripe.rpki.commons.crypto.UnknownCertificateRepositoryObject; +import net.ripe.rpki.commons.crypto.crl.CrlLocator; +import net.ripe.rpki.commons.crypto.crl.X509Crl; import net.ripe.rpki.commons.crypto.x509cert.X509CertificateInformationAccessDescriptor; import net.ripe.rpki.commons.crypto.x509cert.X509ResourceCertificate; import net.ripe.rpki.commons.ta.domain.request.*; @@ -10,6 +13,9 @@ import net.ripe.rpki.commons.ta.domain.response.RevocationResponse; import net.ripe.rpki.commons.ta.domain.response.SigningResponse; import net.ripe.rpki.commons.ta.domain.response.TrustAnchorResponse; +import net.ripe.rpki.commons.validation.ValidationOptions; +import net.ripe.rpki.commons.validation.ValidationResult; +import net.ripe.rpki.commons.validation.objectvalidators.CertificateRepositoryObjectValidationContext; import net.ripe.rpki.domain.*; import net.ripe.rpki.domain.archive.KeyPairDeletionService; import net.ripe.rpki.domain.interca.CertificateIssuanceResponse; @@ -128,6 +134,32 @@ public void should_process_response_and_re_publish_object() { assertThat(actual1.getCreatedAt()).isEqualTo(NEW_CERTIFICATE.getValidityPeriod().getNotValidBefore().toInstant()); } + @Test + public void should_process_response_for_unknown_file_and_use_now_as_default_time() { + var now = Instant.now(); + var secondObjectUri = NEW_CERTIFICATE_PUBLICATION_BASE_URI.resolve("secondObject.bin"); + + var toPublishObjects = Map.of( + NEW_CERTIFICATE_PUBLICATION_URI, NEW_CERTIFICATE, + secondObjectUri, new UnknownCertificateRepositoryObject(new byte[]{'d', 'e', 'a', 'd', 'b', 'e', 'e', 'f'}) + ); + + final List publishedObjects = subject.applyChangeToPublishedObjects(toPublishObjects); + + assertThat(publishedObjects.size()).isEqualTo(2); + + final TrustAnchorPublishedObject actual0 = publishedObjects.stream().filter(obj -> NEW_CERTIFICATE_PUBLICATION_URI.equals(obj.getUri())).findFirst().get(); + assertThat(actual0.getUri()).isEqualTo(NEW_CERTIFICATE_PUBLICATION_URI); + assertThat(actual0.getContent()).isEqualTo(NEW_CERTIFICATE.getEncoded()); + assertThat(actual0.getStatus()).isEqualTo(PublicationStatus.TO_BE_PUBLISHED); + assertThat(actual0.getCreatedAt()).isEqualTo(NEW_CERTIFICATE.getValidityPeriod().getNotValidBefore().toInstant()); + + final TrustAnchorPublishedObject actual1 = publishedObjects.stream().filter(obj -> secondObjectUri.equals(obj.getUri())).findFirst().get(); + assertThat(actual1.getUri()).isEqualTo(secondObjectUri); + assertThat(actual1.getStatus()).isEqualTo(PublicationStatus.TO_BE_PUBLISHED); + assertThat(actual1.getCreatedAt()).isGreaterThanOrEqualTo(now); + } + @Test(expected = OfflineResponseProcessorException.class ) public void shouldRejectWhenResponseSeemsToReferToOtherRequest() { // make a pending request dated to 1 millisecond after epoch start diff --git a/src/test/java/net/ripe/rpki/rest/service/CaAspaConfigurationServiceTest.java b/src/test/java/net/ripe/rpki/rest/service/CaAspaConfigurationServiceTest.java index eacdfb2..00562b0 100644 --- a/src/test/java/net/ripe/rpki/rest/service/CaAspaConfigurationServiceTest.java +++ b/src/test/java/net/ripe/rpki/rest/service/CaAspaConfigurationServiceTest.java @@ -1,13 +1,13 @@ package net.ripe.rpki.rest.service; +import com.google.common.collect.ImmutableSortedSet; import com.google.gson.Gson; +import lombok.extern.slf4j.Slf4j; import net.ripe.ipresource.Asn; import net.ripe.rpki.TestRpkiBootApplication; import net.ripe.rpki.commons.util.VersionedId; import net.ripe.rpki.server.api.commands.UpdateAspaConfigurationCommand; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import net.ripe.rpki.server.api.dto.AspaConfigurationData; -import net.ripe.rpki.server.api.dto.AspaProviderData; import net.ripe.rpki.server.api.dto.HostedCertificateAuthorityData; import net.ripe.rpki.server.api.services.command.CommandService; import net.ripe.rpki.server.api.services.command.EntityTagDoesNotMatchException; @@ -30,6 +30,9 @@ import javax.security.auth.x500.X500Principal; import java.util.Collections; import java.util.List; +import java.util.Random; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import static net.ripe.rpki.rest.service.AbstractCaRestService.API_URL_PREFIX; import static org.assertj.core.api.Assertions.assertThat; @@ -41,6 +44,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +@Slf4j @ActiveProfiles("test") @RunWith(SpringRunner.class) @AutoConfigureMockMvc @@ -49,7 +53,7 @@ public class CaAspaConfigurationServiceTest { private static final long CA_ID = 123L; public static final List ASPA_CONFIGURATION_DATA = Collections.singletonList( - new AspaConfigurationData(Asn.parse("AS1"), Collections.singletonList(new AspaProviderData(Asn.parse("AS5"), AspaAfiLimit.ANY))) + new AspaConfigurationData(Asn.parse("AS1"), List.of(Asn.parse("AS5"))) ); public static final String ASPA_CONFIGURATION_ETAG = AspaConfigurationData.entityTag(AspaConfigurationData.dataToMaps(ASPA_CONFIGURATION_DATA)); public static final Gson GSON = new Gson(); @@ -85,16 +89,18 @@ public void shouldReturnAspaConfigurationForCa() throws Exception { .andExpect(jsonPath("$.aspaConfigurations", hasSize(1))) .andExpect(jsonPath("$.aspaConfigurations[0].customerAsn").value("AS1")) .andExpect(jsonPath("$.aspaConfigurations[0].providers", hasSize(1))) - .andExpect(jsonPath("$.aspaConfigurations[0].providers[0].providerAsn").value("AS5")) - .andExpect(jsonPath("$.aspaConfigurations[0].providers[0].afiLimit").value("ANY")); + .andDo(res -> log.info(res.getResponse().getContentAsString())); } @Test - public void shouldUpdateAspaConfigurationForCa() throws Exception { + public void shouldUpdateAspaConfigurationForCa_with_provider() throws Exception { + var nextAs = new Random().nextInt(2048) + 1024; + mockMvc.perform(Rest.put(API_URL_PREFIX + "/123/aspa") .header(HttpHeaders.IF_MATCH, ASPA_CONFIGURATION_ETAG) - .content("{\"ifMatch\":" + GSON.toJson(ASPA_CONFIGURATION_ETAG) + ",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[{\"providerAsn\":\"AS5\",\"afiLimit\":\"ANY\"}]}]}") + .content("{\"ifMatch\":" + GSON.toJson(ASPA_CONFIGURATION_ETAG) + ",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[\"AS" + nextAs + "\"]}]}") ) + .andDo(res -> log.info(res.getResponse().getContentAsString())) .andExpect(status().isNoContent()); ArgumentCaptor commandArgumentCaptor = ArgumentCaptor.forClass(UpdateAspaConfigurationCommand.class); @@ -102,16 +108,48 @@ public void shouldUpdateAspaConfigurationForCa() throws Exception { assertThat(commandArgumentCaptor.getValue().getIfMatch()).isEqualTo(ASPA_CONFIGURATION_ETAG); } + @Test + public void shouldUpdateAspaConfigurationForCa_with_multiple_provider() throws Exception { + var providers = IntStream.range(1000, 1100) + .mapToObj(i -> "AS"+i) + .collect(Collectors.toList()); + + mockMvc.perform(Rest.put(API_URL_PREFIX + "/123/aspa") + .header(HttpHeaders.IF_MATCH, ASPA_CONFIGURATION_ETAG) + .content("{\"ifMatch\":" + GSON.toJson(ASPA_CONFIGURATION_ETAG) + ",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\": " + GSON.toJson(providers) + "}]}") + ) + .andDo(res -> log.info(res.getResponse().getContentAsString())) + .andExpect(status().isNoContent()); + + ArgumentCaptor commandArgumentCaptor = ArgumentCaptor.forClass(UpdateAspaConfigurationCommand.class); + verify(commandService).execute(commandArgumentCaptor.capture()); + assertThat(commandArgumentCaptor.getValue().getIfMatch()).isEqualTo(ASPA_CONFIGURATION_ETAG); + } + + @Test + public void shouldUpdateAspaConfigurationForCa_no_providers() throws Exception { + mockMvc.perform(Rest.put(API_URL_PREFIX + "/123/aspa") + .header(HttpHeaders.IF_MATCH, ASPA_CONFIGURATION_ETAG) + .content("{\"ifMatch\":" + GSON.toJson(ASPA_CONFIGURATION_ETAG) + ",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[]}]}") + ) + .andDo(res -> log.info(res.getResponse().getContentAsString())) + .andExpect(status().isNoContent()); + + ArgumentCaptor commandArgumentCaptor = ArgumentCaptor.forClass(UpdateAspaConfigurationCommand.class); + verify(commandService).execute(commandArgumentCaptor.capture()); + assertThat(commandArgumentCaptor.getValue().getIfMatch()).isEqualTo(ASPA_CONFIGURATION_ETAG); + } + @Test public void shouldFailWhenAspaConfigurationIsMissingOrMalformed() throws Exception { + // etag mismatch assertBadRequest("{\"ifMatch\":\"etag\"}"); - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\"}]}"); - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"bad-asn\",\"providers\":[{\"providerAsn\":\"AS5\",\"afiLimit\":\"ANY\"}]}]}"); - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[{\"providerAsn\":\"AS5\"}]}]}"); - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[{\"providerAsn\":\"AS5\",\"afiLimit\":\"BAD-LIMIT\"}]}]}"); - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS99999999999999\",\"providers\":[{\"providerAsn\":\"AS5\",\"afiLimit\":\"ANY\"}]}]}"); - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"FOO1\",\"providers\":[{\"providerAsn\":\"AS5\",\"afiLimit\":\"ANY\"}]}]}"); - assertBadRequest("{\"ifMatch\":\"etag\",'aspaConfigurations':[{\"customerAsn\":\"FOO1\",\"providers\":[{\"providerAsn\":\"AS5\",\"afiLimit\":\"ANY\"}]}]}"); + // Other cases do not send an etag + assertBadRequest("{\"aspaConfigurations\":[{\"customerAsn\":\"AS1\"}]}"); + assertBadRequest("{\"aspaConfigurations\":[{\"customerAsn\":\"bad-asn\",\"providers\":[\"AS5\"]}]}"); + assertBadRequest("{\"aspaConfigurations\":[{\"customerAsn\":\"AS99999999999999\",\"providers\":[\"AS5\"]}]}"); + assertBadRequest("{\"aspaConfigurations\":[{\"customerAsn\":\"FOO1\",\"providers\":[\"AS5\"]}]}"); + assertBadRequest("{'aspaConfigurations':[{\"customerAsn\":\"FOO1\",\"providers\":[\"AS5\"]}]}"); } @Test @@ -141,23 +179,14 @@ public void shouldFailWhenIfMatchHeaderAndFieldDoNotMatch() throws Exception { ) .andExpect(status().isBadRequest()); } - - @Test - public void shouldFailWhenDuplicateAsnsAreConfigured() throws Exception { - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[]},{\"customerAsn\":\"AS1\",\"providers\":[]}]}"); - } - @Test public void shouldFailWhenProvidersIsEmpty() throws Exception { - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":null}]}"); - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[]}]}"); + assertBadRequest("{\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":null}]}"); } @Test public void shouldFailWhenProviderInformationIsMissing() throws Exception { - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[{}]}]}"); - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[{\"providerAsn\":\"AS123\"}]}]}"); - assertBadRequest("{\"ifMatch\":\"etag\",\"aspaConfigurations\":[{\"customerAsn\":\"AS1\",\"providers\":[{\"afiLimit\":\"ANY\"}]}]}"); + assertBadRequest("{\"aspaConfigurations\":[{\"customerAsn\":\"AS1\"}]}"); } private void assertBadRequest(String content) throws Exception { @@ -165,7 +194,7 @@ private void assertBadRequest(String content) throws Exception { .header(HttpHeaders.IF_MATCH, ASPA_CONFIGURATION_ETAG) .content(content) ) + .andDo(res -> log.info(res.getResponse().getContentAsString())) .andExpect(status().isBadRequest()); } - } diff --git a/src/test/java/net/ripe/rpki/rest/service/CaRoaConfigurationServiceTest.java b/src/test/java/net/ripe/rpki/rest/service/CaRoaConfigurationServiceTest.java index 7b8b536..90bfcd4 100644 --- a/src/test/java/net/ripe/rpki/rest/service/CaRoaConfigurationServiceTest.java +++ b/src/test/java/net/ripe/rpki/rest/service/CaRoaConfigurationServiceTest.java @@ -115,7 +115,7 @@ public void shouldPostROAtoStageNoRealChange() throws Exception { } @Test - public void shouldPostROAtoStageBreakLength() throws Exception { + public void shouldPreventFromStaginBreakingChanges() throws Exception { when(roaViewService.getRoaConfiguration(CA_ID)).thenReturn(new RoaConfigurationData(Collections.singletonList( new RoaConfigurationPrefixData(new Asn(10), IpRange.parse(TESTNET_1), 32)))); @@ -132,15 +132,11 @@ public void shouldPostROAtoStageBreakLength() throws Exception { mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/roas/stage") .content("[{\"asn\" : \"AS10\", \"prefix\" : \"" + TESTNET_1 + "\", \"maximalLength\" : \"24\"}]")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value("1")) - .andExpect(jsonPath("$.[0].asn").value("AS10")) - .andExpect(jsonPath("$.[0].prefix").value("192.0.2.0/28")) - .andExpect(jsonPath("$.[0].visibility").value("1000")) - .andExpect(jsonPath("$.[0].suppressed").value("false")) - .andExpect(jsonPath("$.[0].verified").value("true")) - .andExpect(jsonPath("$.[0].currentState").value("VALID")) - .andExpect(jsonPath("$.[0].futureState").value("INVALID_LENGTH")); + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error") + .value("There is an overlap in ROAs: existing " + + "AllowedRoute[asn=AS10,maximumLength=32,prefix=192.0.2.0/24] has the same (ASN, prefix) as added " + + "ROA{asn='AS10', prefix='192.0.2.0/24', maximalLength=24}")); } /** @@ -342,6 +338,31 @@ public void shouldReturnNotChangesForExactlySameROAs() throws Exception { .andExpect(jsonPath("$.[0].verified").value("true")); } + @Test + public void shouldNotCrashOnStagingDuplicates() throws Exception { + // Have a large ROA covering everything + when(roaViewService.getRoaConfiguration(CA_ID)).thenReturn(new RoaConfigurationData(Arrays.asList( + new RoaConfigurationPrefixData(new Asn(11), IpRange.parse("148.139.0.0/24"), 26)))); + + // this resource set here doesn't matter, it's only used for `findMostSpecificContainedAndNotContained` + ImmutableResourceSet ipResourceSet = ImmutableResourceSet.parse("127.0.0.1, ::1"); + when(certificateAuthorityData.getResources()).thenReturn(ipResourceSet); + + Map> bgpRisEntries = new HashMap<>(); + bgpRisEntries.put(true, Collections.singletonList(new BgpRisEntry(new Asn(11), IpRange.parse("148.139.0.0/24"), 16))); + when(bgpRisEntryViewService.findMostSpecificContainedAndNotContained(ipResourceSet)).thenReturn(bgpRisEntries); + + // Submit another ROA that is more specific for the announcement + mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/roas/stage") + .content("[" + + "{\"asn\" : \"AS11\", \"prefix\" : \"148.139.0.0/16\", \"maximalLength\" : \"22\"}," + + "{\"asn\" : \"AS11\", \"prefix\" : \"148.139.0.0/16\", \"maximalLength\" : \"24\"}" + + "]")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value( + "Error in future ROAs: there are more than one pair (AS11, 148.139.0.0/16), max lengths: [22, 24]")); + } + @Test public void shouldReturnAffectingROAsAllIsFine() throws Exception { @@ -424,9 +445,12 @@ public void shouldPublishRoas() throws Exception { ImmutableResourceSet ipResourceSet = ImmutableResourceSet.parse("193.0.24.0/21, 2001:67c:64::/48"); when(certificateAuthorityData.getResources()).thenReturn(ipResourceSet); - when(certificateAuthorityData.getVersionedId()).thenReturn(VersionedId.parse("1")); + RoaConfigurationData roaConfigurationData = new RoaConfigurationData(Collections.singletonList( + new RoaConfigurationPrefixData(new Asn(10), IpRange.parse("193.0.24.0/21"), 21))); + when(roaViewService.getRoaConfiguration(CA_ID)).thenReturn(roaConfigurationData); + ArgumentCaptor argument = ArgumentCaptor.forClass(UpdateRoaConfigurationCommand.class); mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/roas/publish") @@ -459,6 +483,10 @@ public void should_use_ifMatch_field_when_header_not_provider() throws Exception when(certificateAuthorityData.getVersionedId()).thenReturn(VersionedId.parse("1")); ArgumentCaptor argument = ArgumentCaptor.forClass(UpdateRoaConfigurationCommand.class); + RoaConfigurationData roaConfigurationData = new RoaConfigurationData(Collections.singletonList( + new RoaConfigurationPrefixData(new Asn(10), IpRange.parse("193.0.24.0/21"), 21))); + when(roaViewService.getRoaConfiguration(CA_ID)).thenReturn(roaConfigurationData); + mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/roas/publish") .content("{ " + "\"ifMatch\" : \"\\\"if-match-value\\\"\"," + @@ -523,15 +551,18 @@ public void shouldNotAddRoasIfMissingMaxLength() throws Exception { public void shouldNotAddPrivateRoas() throws Exception { ImmutableResourceSet ipResourceSet = ImmutableResourceSet.parse("193.0.24.0/21, 2001:67c:64::/48"); when(certificateAuthorityData.getResources()).thenReturn(ipResourceSet); - when(certificateAuthorityData.getVersionedId()).thenReturn(VersionedId.parse("1")); + + RoaConfigurationData roaConfigurationData = new RoaConfigurationData(Collections.singletonList( + new RoaConfigurationPrefixData(new Asn(10), IpRange.parse("193.0.24.0/21"), 21))); + when(roaViewService.getRoaConfiguration(CA_ID)).thenReturn(roaConfigurationData); + when(commandService.execute(isA(UpdateRoaConfigurationCommand.class))) .thenThrow(new PrivateAsnsUsedException("ROA configuration", Collections.singletonList(new Asn(64512L)))); mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/roas/publish") .content("{ \"added\" : [{\"asn\" : \"AS64512\", \"prefix\" : \"193.0.24.0/21\", \"maximalLength\" : " + "\"21\"}] }")) .andExpect(status().is(400)); - } @Test diff --git a/src/test/java/net/ripe/rpki/rest/service/UtilsTest.java b/src/test/java/net/ripe/rpki/rest/service/UtilsTest.java index 1364b68..6e49df5 100644 --- a/src/test/java/net/ripe/rpki/rest/service/UtilsTest.java +++ b/src/test/java/net/ripe/rpki/rest/service/UtilsTest.java @@ -1,14 +1,17 @@ package net.ripe.rpki.rest.service; +import net.ripe.ipresource.Asn; import net.ripe.ipresource.IpRange; import net.ripe.rpki.rest.pojo.ROA; import org.junit.Test; -import static net.ripe.rpki.rest.service.Utils.errorsInUserInputRoas; -import static net.ripe.rpki.rest.service.Utils.maxLengthIsValid; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import static net.ripe.rpki.rest.service.Utils.*; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class UtilsTest { @Test @@ -50,4 +53,52 @@ public void shouldRejectUndershootMaxLengthV6() { public void shouldRejectTooLongMaxLengthV6() { assertFalse(maxLengthIsValid(IpRange.parse("2001:DB8::/48"), 133)); } + + @Test + public void shouldValidateSameROAUpdate() { + assertEquals(Optional.empty(), + validateNoIdenticalROAs(Collections.emptyList(), Collections.emptyList(), Collections.emptyList())); + + assertEquals(Optional.empty(), + validateNoIdenticalROAs( + List.of(new ExistingROA(new Asn(10L), IpRange.parse("192.0.2.0/24"), null)), + Collections.emptyList(), + Collections.emptyList())); + + assertEquals(Optional.of( + "There is an overlap in ROAs: existing ROA{asn=AS10, prefix=192.0.1.0/24, maximumLength=28} " + + "has the same (ASN, prefix) as added ROA{asn='AS10', prefix='192.0.1.0/24', maximalLength=27}"), + validateNoIdenticalROAs( + List.of(new ExistingROA(new Asn(10L), IpRange.parse("192.0.1.0/24"), 28)), + List.of(new ROA("AS10", "192.0.1.0/24", 27)), + Collections.emptyList())); + + assertEquals(Optional.of( + "There is an overlap in ROAs: existing ROA{asn=AS10, prefix=192.0.1.0/24, maximumLength=28} " + + "has the same (ASN, prefix) as added ROA{asn='AS10', prefix='192.0.1.0/24', maximalLength=null}"), + validateNoIdenticalROAs( + List.of(new ExistingROA(new Asn(10L), IpRange.parse("192.0.1.0/24"), 28)), + List.of(new ROA("AS10", "192.0.1.0/24", null)), + Collections.emptyList())); + + assertEquals(Optional.empty(), + validateNoIdenticalROAs( + List.of(new ExistingROA(new Asn(10L), IpRange.parse("192.0.1.0/24"), 28)), + List.of(new ROA("AS10", "192.0.1.0/24", 27)), + List.of(new ROA("AS10", "192.0.1.0/24", 28)) + )); + + assertEquals(Optional.empty(), + validateNoIdenticalROAs( + List.of(new ExistingROA(new Asn(10L), IpRange.parse("192.0.2.0/24"), null)), + List.of(new ROA("AS10", "192.0.2.0/24", 27)), + List.of(new ROA("AS10", "192.0.2.0/24", null)) + )); + + assertEquals(Optional.empty(), + validateNoIdenticalROAs( + List.of(new ExistingROA(new Asn(10L), IpRange.parse("192.0.2.0/24"), 27)), + List.of(new ROA("AS10", "192.0.2.0/24", null)), + List.of(new ROA("AS10", "192.0.2.0/24", 27)))); + } } diff --git a/src/test/java/net/ripe/rpki/rest/service/monitoring/AspaServiceTest.java b/src/test/java/net/ripe/rpki/rest/service/monitoring/AspaServiceTest.java index c84441b..0b9308f 100644 --- a/src/test/java/net/ripe/rpki/rest/service/monitoring/AspaServiceTest.java +++ b/src/test/java/net/ripe/rpki/rest/service/monitoring/AspaServiceTest.java @@ -7,7 +7,6 @@ import net.ripe.rpki.domain.aspa.AspaConfiguration; import net.ripe.rpki.domain.aspa.AspaConfigurationRepository; import net.ripe.rpki.rest.service.Rest; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -19,10 +18,7 @@ import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; -import java.util.Arrays; -import java.util.Collections; -import java.util.Map; -import java.util.TreeMap; +import java.util.*; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; import static org.hamcrest.Matchers.hasSize; @@ -48,12 +44,12 @@ public class AspaServiceTest { @Test public void shouldReturnProperAspas() throws Exception { - Map providers = new TreeMap<>(); - providers.put(Asn.parse("AS10"), AspaAfiLimit.IPv4); - providers.put(Asn.parse("AS11"), AspaAfiLimit.IPv6); - providers.put(Asn.parse("AS12"), AspaAfiLimit.ANY); + SortedSet providers = new TreeSet<>(); + providers.add(Asn.parse("AS10")); + providers.add(Asn.parse("AS11")); + providers.add(Asn.parse("AS12")); when(aspaConfigurationRepository.findAll()).thenReturn(Arrays.asList( - new AspaConfiguration(mock(ManagedCertificateAuthority.class), Asn.parse("AS1"), Collections.emptyMap()), + new AspaConfiguration(mock(ManagedCertificateAuthority.class), Asn.parse("AS1"), Collections.emptySortedSet()), new AspaConfiguration(mock(ManagedCertificateAuthority.class), Asn.parse("AS2"), providers) )); @@ -64,12 +60,9 @@ public void shouldReturnProperAspas() throws Exception { .andExpect(jsonPath("$.aspaConfigurations[0].providers", hasSize(0))) .andExpect(jsonPath("$.aspaConfigurations[1].customerAsn").value("AS2")) .andExpect(jsonPath("$.aspaConfigurations[1].providers", hasSize(3))) - .andExpect(jsonPath("$.aspaConfigurations[1].providers[0].providerAsn").value("AS10")) - .andExpect(jsonPath("$.aspaConfigurations[1].providers[0].afiLimit").value("IPv4")) - .andExpect(jsonPath("$.aspaConfigurations[1].providers[1].providerAsn").value("AS11")) - .andExpect(jsonPath("$.aspaConfigurations[1].providers[1].afiLimit").value("IPv6")) - .andExpect(jsonPath("$.aspaConfigurations[1].providers[2].providerAsn").value("AS12")) - .andExpect(jsonPath("$.aspaConfigurations[1].providers[2].afiLimit").value("ANY")) + .andExpect(jsonPath("$.aspaConfigurations[1].providers[0]").value("AS10")) + .andExpect(jsonPath("$.aspaConfigurations[1].providers[1]").value("AS11")) + .andExpect(jsonPath("$.aspaConfigurations[1].providers[2]").value("AS12")) .andExpect(jsonPath("$.metadata").isMap()) .andExpect(jsonPath("$.aspaConfigurations", hasSize(2))); } diff --git a/src/test/java/net/ripe/rpki/ripencc/services/impl/RestResourceServicesClientTest.java b/src/test/java/net/ripe/rpki/ripencc/services/impl/RestResourceServicesClientTest.java index 3b98a59..2a5201d 100644 --- a/src/test/java/net/ripe/rpki/ripencc/services/impl/RestResourceServicesClientTest.java +++ b/src/test/java/net/ripe/rpki/ripencc/services/impl/RestResourceServicesClientTest.java @@ -25,7 +25,6 @@ public class RestResourceServicesClientTest { private static final String BASE_URL = "/resource-services/"; private static final String MEMBER_RESOURCES_URL = "member-resources"; private static final String TOTAL_RESOURCES_URL = "total-resources"; - private static final String MONITORING_HEALTHCHECK = "monitoring/healthcheck"; private static final int PORT = 7575; @@ -51,14 +50,16 @@ private void stubHttpCallForTheIndexPage() { @Test public void shouldBeAvailable() { - stubFor(head(urlEqualTo(BASE_URL+ MONITORING_HEALTHCHECK)).withHeader("Accept", equalTo(APPLICATION_JSON)).willReturn(aResponse().withStatus(200))); + stubFor(head(urlEqualTo(BASE_URL + RestResourceServicesClient.HEALTHCHECK_PATH)) + .withHeader("Accept", equalTo(APPLICATION_JSON)).willReturn(aResponse().withStatus(200))); assertTrue(subject.isAvailable()); } @Test public void shouldBeUnavailable() { - stubFor(head(urlEqualTo(BASE_URL+ MONITORING_HEALTHCHECK)).withHeader("Accept", equalTo(APPLICATION_JSON)).willReturn(aResponse().withStatus(500))); + stubFor(head(urlEqualTo(BASE_URL+ RestResourceServicesClient.HEALTHCHECK_PATH)) + .withHeader("Accept", equalTo(APPLICATION_JSON)).willReturn(aResponse().withStatus(500))); assertFalse(subject.isAvailable()); } diff --git a/src/test/java/net/ripe/rpki/server/api/commands/UpdateAspaConfigurationCommandTest.java b/src/test/java/net/ripe/rpki/server/api/commands/UpdateAspaConfigurationCommandTest.java index 4dc5731..105a971 100644 --- a/src/test/java/net/ripe/rpki/server/api/commands/UpdateAspaConfigurationCommandTest.java +++ b/src/test/java/net/ripe/rpki/server/api/commands/UpdateAspaConfigurationCommandTest.java @@ -3,12 +3,11 @@ import net.ripe.ipresource.Asn; import net.ripe.rpki.commons.util.VersionedId; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import net.ripe.rpki.server.api.dto.AspaConfigurationData; -import net.ripe.rpki.server.api.dto.AspaProviderData; import org.junit.Test; import java.util.Collections; +import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -21,10 +20,10 @@ public void getCommandSummary() { "etag", Collections.singletonList(new AspaConfigurationData( Asn.parse("AS13"), - Collections.singletonList(new AspaProviderData(Asn.parse("AS1234"), AspaAfiLimit.ANY)) + List.of(Asn.parse("AS1234")) )) ); - assertThat(command.getCommandSummary()).isEqualTo("Update ASPA configuration to: AS13 -> AS1234 [ANY]."); + assertThat(command.getCommandSummary()).isEqualTo("Update ASPA configuration to: AS13 -> AS1234."); } } \ No newline at end of file diff --git a/src/test/java/net/ripe/rpki/services/impl/handlers/IssueUpdatedManifestAndCrlCommandHandlerTest.java b/src/test/java/net/ripe/rpki/services/impl/handlers/IssueUpdatedManifestAndCrlCommandHandlerTest.java index 7e37e84..297047c 100644 --- a/src/test/java/net/ripe/rpki/services/impl/handlers/IssueUpdatedManifestAndCrlCommandHandlerTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/handlers/IssueUpdatedManifestAndCrlCommandHandlerTest.java @@ -1,5 +1,6 @@ package net.ripe.rpki.services.impl.handlers; +import com.google.common.collect.ImmutableSortedSet; import net.ripe.ipresource.Asn; import net.ripe.ipresource.IpRange; import net.ripe.rpki.commons.crypto.cms.manifest.ManifestCms; @@ -7,7 +8,6 @@ import net.ripe.rpki.domain.aspa.*; import net.ripe.rpki.domain.roa.*; import net.ripe.rpki.server.api.commands.IssueUpdatedManifestAndCrlCommand; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import net.ripe.rpki.server.api.services.command.CommandWithoutEffectException; import org.junit.Before; import org.junit.Test; @@ -98,7 +98,7 @@ public void should_update_roa_entities() { @Test public void should_update_aspa_entities() { - aspaConfigurationRepository.add(new AspaConfiguration(ca, Asn.parse("AS64512"), Collections.singletonMap(Asn.parse("AS1"), AspaAfiLimit.ANY))); + aspaConfigurationRepository.add(new AspaConfiguration(ca, Asn.parse("AS64512"), ImmutableSortedSet.of(Asn.parse("AS1")))); ca.markConfigurationUpdated(); assertThat(aspaEntityRepository.findCurrentByCertificateAuthority(ca)).describedAs("current ASPA entities").isEmpty(); diff --git a/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateAspaConfigurationCommandHandlerTest.java b/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateAspaConfigurationCommandHandlerTest.java index 6c867e7..32ef811 100644 --- a/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateAspaConfigurationCommandHandlerTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateAspaConfigurationCommandHandlerTest.java @@ -1,5 +1,6 @@ package net.ripe.rpki.services.impl.handlers; +import com.google.common.collect.ImmutableSortedSet; import net.ripe.ipresource.Asn; import net.ripe.ipresource.ImmutableResourceSet; import net.ripe.rpki.commons.util.VersionedId; @@ -8,22 +9,17 @@ import net.ripe.rpki.domain.aspa.AspaConfiguration; import net.ripe.rpki.domain.aspa.AspaConfigurationRepository; import net.ripe.rpki.server.api.commands.UpdateAspaConfigurationCommand; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import net.ripe.rpki.server.api.dto.AspaConfigurationData; -import net.ripe.rpki.server.api.dto.AspaProviderData; import net.ripe.rpki.server.api.services.command.CommandWithoutEffectException; -import net.ripe.rpki.server.api.services.command.DuplicateResourceException; +import net.ripe.rpki.server.api.services.command.IllegalResourceException; import net.ripe.rpki.server.api.services.command.EntityTagDoesNotMatchException; import net.ripe.rpki.server.api.services.command.NotHolderOfResourcesException; import net.ripe.rpki.server.api.services.command.PrivateAsnsUsedException; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; -import java.util.Arrays; -import java.util.Collections; -import java.util.SortedMap; -import java.util.TreeMap; +import java.util.*; import static java.util.Collections.singletonList; import static net.ripe.rpki.domain.TestObjects.CA_ID; @@ -35,7 +31,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -public class UpdateAspaConfigurationCommandHandlerTest { +class UpdateAspaConfigurationCommandHandlerTest { private static final String PRIVATE_ASNS = "64512-65535, 4200000000-4294967294"; private static final String EMPTY_CONFIGURATION_ETAG = AspaConfigurationData.entityTag(Collections.emptySortedMap()); @@ -43,7 +39,7 @@ public class UpdateAspaConfigurationCommandHandlerTest { private ManagedCertificateAuthority managedCertificateAuthority; private UpdateAspaConfigurationCommandHandler subject; - @Before + @BeforeEach public void setUp() { CertificateAuthorityRepository certificateAuthorityRepository = mock(CertificateAuthorityRepository.class); aspaConfigurationRepository = mock(AspaConfigurationRepository.class); @@ -56,41 +52,41 @@ public void setUp() { } @Test - public void should_add_new_customer_asn() { + void should_add_new_customer_asn() { UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), EMPTY_CONFIGURATION_ETAG, singletonList(new AspaConfigurationData( Asn.parse("AS1234"), - singletonList(new AspaProviderData(Asn.parse("AS3"), AspaAfiLimit.IPv4)) + List.of(Asn.parse("AS3")) ))); subject.handle(command); ArgumentCaptor aspaConfigurationArgumentCaptor = ArgumentCaptor.forClass(AspaConfiguration.class); verify(aspaConfigurationRepository).add(aspaConfigurationArgumentCaptor.capture()); - assertThat(aspaConfigurationArgumentCaptor.getValue().getProviders()).isEqualTo(Collections.singletonMap(Asn.parse("AS3"), AspaAfiLimit.IPv4)); + assertThat(aspaConfigurationArgumentCaptor.getValue().getProviders()).isEqualTo(ImmutableSortedSet.of(Asn.parse("AS3"))); verify(managedCertificateAuthority).markConfigurationUpdated(); } @Test - public void should_replace_provider_asns_for_matching_customer_asn() { - AspaConfiguration aspa_as1234 = new AspaConfiguration(managedCertificateAuthority, Asn.parse("AS1234"), Collections.singletonMap(Asn.parse("AS1"), AspaAfiLimit.ANY)); + void should_replace_provider_asns_for_matching_customer_asn() { + AspaConfiguration aspa_as1234 = new AspaConfiguration(managedCertificateAuthority, Asn.parse("AS1234"), ImmutableSortedSet.of(Asn.parse("AS1"))); SortedMap aspaCo = new TreeMap<>(); aspaCo.put(aspa_as1234.getCustomerAsn(), aspa_as1234); when(aspaConfigurationRepository.findByCertificateAuthority(managedCertificateAuthority)).thenReturn(aspaCo); UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), AspaConfigurationData.entityTag(entitiesToMaps(aspaCo)), singletonList(new AspaConfigurationData( Asn.parse("AS1234"), - singletonList(new AspaProviderData(Asn.parse("AS3"), AspaAfiLimit.IPv4)) + List.of(Asn.parse("AS3")) ))); subject.handle(command); - assertThat(aspa_as1234.getProviders()).isEqualTo(Collections.singletonMap(Asn.parse("AS3"), AspaAfiLimit.IPv4)); + assertThat(aspa_as1234.getProviders()).isEqualTo(ImmutableSortedSet.of(Asn.parse("AS3"))); verify(managedCertificateAuthority).markConfigurationUpdated(); } @Test - public void should_remove_customer_asn() { - AspaConfiguration aspa_as1234 = new AspaConfiguration(managedCertificateAuthority, Asn.parse("AS1234"), Collections.singletonMap(Asn.parse("AS1"), AspaAfiLimit.ANY)); + void should_remove_customer_asn() { + AspaConfiguration aspa_as1234 = new AspaConfiguration(managedCertificateAuthority, Asn.parse("AS1234"), ImmutableSortedSet.of(Asn.parse("AS1"))); SortedMap aspaCo = new TreeMap<>(); aspaCo.put(aspa_as1234.getCustomerAsn(), aspa_as1234); when(aspaConfigurationRepository.findByCertificateAuthority(managedCertificateAuthority)).thenReturn(aspaCo); @@ -104,15 +100,15 @@ public void should_remove_customer_asn() { } @Test - public void should_have_no_effect_when_there_are_no_differences() { - AspaConfiguration aspa_as1234 = new AspaConfiguration(managedCertificateAuthority, Asn.parse("AS1234"), Collections.singletonMap(Asn.parse("AS1"), AspaAfiLimit.ANY)); + void should_have_no_effect_when_there_are_no_differences() { + AspaConfiguration aspa_as1234 = new AspaConfiguration(managedCertificateAuthority, Asn.parse("AS1234"), ImmutableSortedSet.of(Asn.parse("AS1"))); SortedMap aspaCo = new TreeMap<>(); aspaCo.put(aspa_as1234.getCustomerAsn(), aspa_as1234); when(aspaConfigurationRepository.findByCertificateAuthority(managedCertificateAuthority)).thenReturn(aspaCo); UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), AspaConfigurationData.entityTag(entitiesToMaps(aspaCo)), singletonList(new AspaConfigurationData( Asn.parse("AS1234"), - singletonList(new AspaProviderData(Asn.parse("AS1"), AspaAfiLimit.ANY)) + List.of(Asn.parse("AS1")) ))); assertThatThrownBy(() -> subject.handle(command)).isInstanceOf(CommandWithoutEffectException.class); @@ -120,78 +116,90 @@ public void should_have_no_effect_when_there_are_no_differences() { } @Test - public void should_notify_aspa_entity_service_on_configuration_change() { + void should_notify_aspa_entity_service_on_configuration_change() { subject.handle(new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), EMPTY_CONFIGURATION_ETAG, singletonList(new AspaConfigurationData( Asn.parse("AS1234"), - singletonList(new AspaProviderData(Asn.parse("AS1"), AspaAfiLimit.ANY)) + List.of(Asn.parse("AS1")) )))); verify(managedCertificateAuthority).markConfigurationUpdated(); } @Test - public void should_reject_customer_asn_if_not_certified() { + void should_reject_customer_asn_if_not_certified() { UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), EMPTY_CONFIGURATION_ETAG, singletonList(new AspaConfigurationData( Asn.parse("AS9000"), - singletonList(new AspaProviderData(Asn.parse("AS3"), AspaAfiLimit.IPv4)) + List.of(Asn.parse("AS3")) ))); assertThatThrownBy(() -> subject.handle(command)).isInstanceOf(NotHolderOfResourcesException.class); } @Test - public void should_reject_use_of_private_asns_in_providers() { + void should_reject_use_of_private_asns_in_providers() { UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), EMPTY_CONFIGURATION_ETAG, singletonList(new AspaConfigurationData( Asn.parse("AS1234"), - singletonList(new AspaProviderData(Asn.parse("AS64512"), AspaAfiLimit.ANY)) + List.of(Asn.parse("AS64512")) ))); assertThatThrownBy(() -> subject.handle(command)).isInstanceOf(PrivateAsnsUsedException.class); } @Test - public void should_reject_duplicate_customer_asns() { + void should_reject_duplicate_customer_asns() { UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), EMPTY_CONFIGURATION_ETAG, Arrays.asList(new AspaConfigurationData( Asn.parse("AS1234"), - singletonList(new AspaProviderData(Asn.parse("AS9"), AspaAfiLimit.ANY)) + List.of(Asn.parse("AS9")) ), new AspaConfigurationData( Asn.parse("AS1234"), - singletonList(new AspaProviderData(Asn.parse("AS10"), AspaAfiLimit.ANY)) + List.of(Asn.parse("AS10")) ))); - assertThatThrownBy(() -> subject.handle(command)).isInstanceOfSatisfying(DuplicateResourceException.class, exception -> { - assertThat(exception.getMessage()).isEqualTo("duplicate ASN in ASPA configuration"); + assertThatThrownBy(() -> subject.handle(command)).isInstanceOfSatisfying(IllegalResourceException.class, exception -> { + assertThat(exception.getMessage()).isEqualTo("duplicate customer ASN in ASPA configuration"); }); } @Test - public void should_reject_duplicate_provider_asns() { + void should_reject_duplicate_provider_asns() { UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), EMPTY_CONFIGURATION_ETAG, singletonList(new AspaConfigurationData( Asn.parse("AS1234"), - Arrays.asList(new AspaProviderData(Asn.parse("AS9"), AspaAfiLimit.ANY), new AspaProviderData(Asn.parse("AS9"), AspaAfiLimit.IPv4)) + List.of(Asn.parse("AS9"), Asn.parse("AS9")) ))); - assertThatThrownBy(() -> subject.handle(command)).isInstanceOfSatisfying(DuplicateResourceException.class, exception -> { - assertThat(exception.getMessage()).isEqualTo("duplicate ASN in ASPA configuration"); + assertThatThrownBy(() -> subject.handle(command)).isInstanceOfSatisfying(IllegalResourceException.class, exception -> { + assertThat(exception.getMessage()).isEqualTo("duplicate provider ASN in ASPA configuration"); }); } @Test - public void should_reject_aspa_when_customer_asn_appears_in_provider_asn() { + void should_reject_empty_provider_asns() { + UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), EMPTY_CONFIGURATION_ETAG, singletonList(new AspaConfigurationData( + Asn.parse("AS1234"), + List.of() + ))); + + assertThatThrownBy(() -> subject.handle(command)).isInstanceOfSatisfying(IllegalResourceException.class, exception -> { + assertThat(exception.getMessage()).isEqualTo("One of the configured ASPAs does not have providers"); + }); + } + + @Test + void should_reject_aspa_when_customer_asn_appears_in_provider_asn() { UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), EMPTY_CONFIGURATION_ETAG, singletonList(new AspaConfigurationData( Asn.parse("AS1234"), - Collections.singletonList(new AspaProviderData(Asn.parse("AS1234"), AspaAfiLimit.ANY)) + List.of(Asn.parse("AS1234")) ))); - assertThatThrownBy(() -> subject.handle(command)).isInstanceOfSatisfying(DuplicateResourceException.class, exception -> { + assertThatThrownBy(() -> subject.handle(command)).isInstanceOfSatisfying(IllegalResourceException.class, exception -> { assertThat(exception.getMessage()).isEqualTo("customer AS1234 appears in provider set [AS1234]"); }); } @Test - public void should_reject_command_if_entity_tag_does_not_match_current_configuration() { + void should_reject_command_if_entity_tag_does_not_match_current_configuration() { UpdateAspaConfigurationCommand command = new UpdateAspaConfigurationCommand(new VersionedId(CA_ID), "no-match", singletonList(new AspaConfigurationData( Asn.parse("AS1234"), - singletonList(new AspaProviderData(Asn.parse("AS1"), AspaAfiLimit.ANY)) + List.of(Asn.parse("AS1")) ))); assertThatThrownBy(() -> subject.handle(command)).isInstanceOf(EntityTagDoesNotMatchException.class); diff --git a/src/test/java/net/ripe/rpki/services/impl/jpa/JpaAspaConfigurationRepositoryTest.java b/src/test/java/net/ripe/rpki/services/impl/jpa/JpaAspaConfigurationRepositoryTest.java index c5b01cd..e4cd76e 100644 --- a/src/test/java/net/ripe/rpki/services/impl/jpa/JpaAspaConfigurationRepositoryTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/jpa/JpaAspaConfigurationRepositoryTest.java @@ -5,15 +5,14 @@ import net.ripe.rpki.domain.ProductionCertificateAuthority; import net.ripe.rpki.domain.TestObjects; import net.ripe.rpki.domain.aspa.AspaConfiguration; -import net.ripe.rpki.domain.inmemory.InMemoryResourceCertificateRepository; -import net.ripe.rpki.server.api.dto.AspaAfiLimit; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import javax.transaction.Transactional; import java.util.SortedMap; -import java.util.TreeMap; +import java.util.SortedSet; +import java.util.TreeSet; import static org.assertj.core.api.Assertions.assertThat; @@ -39,10 +38,10 @@ public void shouldReturnEmpty() { @Test public void shouldCreateAndGetBack() { - SortedMap providers = new TreeMap<>(); - providers.put(Asn.parse("AS10"), AspaAfiLimit.ANY); - providers.put(Asn.parse("AS11"), AspaAfiLimit.IPv4); - providers.put(Asn.parse("AS12"), AspaAfiLimit.IPv6); + SortedSet providers = new TreeSet<>(); + providers.add(Asn.parse("AS10")); + providers.add(Asn.parse("AS11")); + providers.add(Asn.parse("AS12")); final AspaConfiguration aspa1 = new AspaConfiguration(ca, Asn.parse("AS1"), providers); subject.add(aspa1); final SortedMap byCa = subject.findByCertificateAuthority(ca); diff --git a/src/test/java/net/ripe/rpki/web/UpstreamCaControllerTest.java b/src/test/java/net/ripe/rpki/web/UpstreamCaControllerTest.java index 29abf7b..f4a90d8 100644 --- a/src/test/java/net/ripe/rpki/web/UpstreamCaControllerTest.java +++ b/src/test/java/net/ripe/rpki/web/UpstreamCaControllerTest.java @@ -167,15 +167,38 @@ public void should_revoke_old_aca_key() throws Exception { verify(commandService, times(1)).execute(isA(KeyManagementRevokeOldKeysCommand.class)); } + @Test + public void should_NOT_revoke_old_aca_key_when_TA_request_is_present() throws Exception { + TrustAnchorRequest taRequest = mock(TrustAnchorRequest.class); + when(aca.getTrustAnchorRequest()).thenReturn(taRequest); + mockMvc.perform(post("/admin/revoke-old-aca-key")).andReturn(); + verify(commandService, times(0)).execute(isA(KeyManagementRevokeOldKeysCommand.class)); + } + @Test public void should_activate_pending_aca_key() throws Exception { mockMvc.perform(post("/admin/activate-pending-aca-key")).andReturn(); verify(commandService, times(1)).execute(isA(KeyManagementActivatePendingKeysCommand.class)); } + @Test + public void should_NOT_activate_pending_aca_key_when_TA_request_is_present() throws Exception { + TrustAnchorRequest taRequest = mock(TrustAnchorRequest.class); + when(aca.getTrustAnchorRequest()).thenReturn(taRequest); + mockMvc.perform(post("/admin/activate-pending-aca-key")).andReturn(); + verify(commandService, times(0)).execute(isA(KeyManagementActivatePendingKeysCommand.class)); + } @Test public void should_initiate_key_roll() throws Exception { mockMvc.perform(post("/admin/initiate-rolling-aca-key")).andReturn(); verify(commandService, times(1)).execute(isA(KeyManagementInitiateRollCommand.class)); } + + @Test + public void should_NOT_initiate_key_roll_when_TA_request_is_present() throws Exception { + TrustAnchorRequest taRequest = mock(TrustAnchorRequest.class); + when(aca.getTrustAnchorRequest()).thenReturn(taRequest); + mockMvc.perform(post("/admin/initiate-rolling-aca-key")).andReturn(); + verify(commandService, times(0)).execute(isA(KeyManagementInitiateRollCommand.class)); + } }