diff --git a/build.gradle b/build.gradle index 3bab61d..77933ce 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ dependencies { implementation "org.thymeleaf:thymeleaf:3.1.2.RELEASE" implementation "org.thymeleaf:thymeleaf-spring6:3.1.3.RELEASE" - implementation platform('io.sentry:sentry-bom:7.20.0') + implementation platform('io.sentry:sentry-bom:8.2.0') implementation 'io.sentry:sentry-spring-boot-starter' implementation 'io.sentry:sentry-logback' @@ -53,12 +53,12 @@ dependencies { implementation 'org.springdoc:springdoc-openapi-ui:1.8.0' runtimeOnly 'io.micrometer:micrometer-registry-prometheus' - implementation 'org.postgresql:postgresql:42.7.4' + implementation 'org.postgresql:postgresql:42.7.5' runtimeOnly 'org.springframework.boot:spring-boot-starter-tomcat' - implementation 'com.google.code.gson:gson:2.11.0' + implementation 'com.google.code.gson:gson:2.12.1' implementation 'com.jamesmurty.utils:java-xmlbuilder:1.3' - implementation 'commons-codec:commons-codec:1.17.2' + implementation 'commons-codec:commons-codec:1.18.0' implementation 'commons-io:commons-io:2.18.0' implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5' implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5' diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index eaeb175..3f04d0f 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -9,7 +9,7 @@ repositories { } dependencies { - implementation 'io.freefair.lombok:io.freefair.lombok.gradle.plugin:8.11' + implementation 'io.freefair.lombok:io.freefair.lombok.gradle.plugin:8.12.2' implementation('com.gorylenko.gradle-git-properties:com.gorylenko.gradle-git-properties.gradle.plugin:2.4.2') { exclude group: 'org.eclipse.jgit', module: 'org.eclipse.jgit' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9..9bbc975 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a79..37f853b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f3b75f3..faf9300 100755 --- a/gradlew +++ b/gradlew @@ -205,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. diff --git a/src/main/java/net/ripe/rpki/domain/ManagedCertificateAuthority.java b/src/main/java/net/ripe/rpki/domain/ManagedCertificateAuthority.java index 46bbb0c..34b1952 100644 --- a/src/main/java/net/ripe/rpki/domain/ManagedCertificateAuthority.java +++ b/src/main/java/net/ripe/rpki/domain/ManagedCertificateAuthority.java @@ -66,7 +66,7 @@ public static EventSubscription subscribe(final CertificateAuthorityEventVisitor private final Set keyPairs = new HashSet<>(); /** - * The last time the ASPA or ROA configuration was updated. This can bever be equal to {@link #configurationAppliedAt}. + * The last time the ASPA or ROA configuration was updated. This can never be equal to {@link #configurationAppliedAt}. */ @Column(name = "configuration_updated_at") @NonNull diff --git a/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfiguration.java b/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfiguration.java index 4e37afe..a4647b2 100644 --- a/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfiguration.java +++ b/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfiguration.java @@ -2,6 +2,7 @@ import com.google.common.collect.Sets; import lombok.Getter; +import lombok.Setter; import net.ripe.rpki.commons.validation.roa.AnnouncedRoute; import net.ripe.rpki.commons.validation.roa.RouteValidityState; import net.ripe.rpki.domain.CertificateAuthority; @@ -34,7 +35,6 @@ import java.util.List; import java.util.Locale; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; @Entity @@ -44,6 +44,7 @@ public class RoaAlertConfiguration extends EntitySupport { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "roa_alert_conf_seq") + @Getter private Long id; @Getter @@ -68,6 +69,11 @@ public class RoaAlertConfiguration extends EntitySupport { @CollectionTable(name = "roa_alert_configuration_ignored", joinColumns = @JoinColumn(name = "roa_alert_configuration_id")) private Set ignored = new HashSet<>(); + @Basic(optional=false) + @Getter + @Setter + private boolean notifyOnRoaChanges; + private static final String EMAIL_SEPARATOR = ","; public RoaAlertConfiguration() { @@ -81,12 +87,7 @@ public RoaAlertConfiguration(CertificateAuthority certificateAuthority) { public RoaAlertConfiguration(CertificateAuthority certificateAuthority, String email, Collection routeValidityStates, RoaAlertFrequency frequency) { this(certificateAuthority); - setSubscription(new RoaAlertSubscriptionData(email, routeValidityStates, frequency)); - } - - @Override - public Object getId() { - return id; + setSubscription(new RoaAlertSubscriptionData(List.of(email), routeValidityStates, frequency, false)); } public void clearSubscription() { @@ -100,6 +101,7 @@ public void setSubscription(RoaAlertSubscriptionData subscription) { addEmails(subscription); routeValidityStates = StringUtils.join(subscription.getRouteValidityStates(), ","); frequency = subscription.getFrequency(); + notifyOnRoaChanges = subscription.isNotifyOnRoaChanges(); } private void addEmails(RoaAlertSubscriptionData subscription) { @@ -124,7 +126,7 @@ public RoaAlertSubscriptionData getSubscriptionOrNull() { if (email.isEmpty()) { return null; } - return new RoaAlertSubscriptionData(Arrays.asList(email.split(",")), getRouteValidityStates(), frequency); + return new RoaAlertSubscriptionData(Arrays.asList(email.split(",")), getRouteValidityStates(), frequency, notifyOnRoaChanges); } public Set getIgnored() { diff --git a/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationRepository.java b/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationRepository.java index e5c1a76..e46684c 100644 --- a/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationRepository.java +++ b/src/main/java/net/ripe/rpki/domain/roa/RoaConfigurationRepository.java @@ -1,15 +1,11 @@ package net.ripe.rpki.domain.roa; -import net.ripe.ipresource.Asn; -import net.ripe.ipresource.IpResourceRange; import net.ripe.rpki.domain.ManagedCertificateAuthority; import net.ripe.rpki.server.api.dto.RoaConfigurationPrefixData; -import net.ripe.rpki.server.api.support.objects.CaName; import java.time.Instant; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Optional; public interface RoaConfigurationRepository { @@ -40,7 +36,7 @@ default void removePrefixes(RoaConfiguration roaConfiguration, Collection prefixesToAdd, - Collection prefixesToRemove); + RoaConfiguration.PrefixDiff mergePrefixes(RoaConfiguration configuration, + Collection prefixesToAdd, + Collection prefixesToRemove); } diff --git a/src/main/java/net/ripe/rpki/rest/pojo/Subscriptions.java b/src/main/java/net/ripe/rpki/rest/pojo/Subscriptions.java index b812f86..562c741 100644 --- a/src/main/java/net/ripe/rpki/rest/pojo/Subscriptions.java +++ b/src/main/java/net/ripe/rpki/rest/pojo/Subscriptions.java @@ -1,28 +1,31 @@ package net.ripe.rpki.rest.pojo; +import com.google.common.collect.Sets; +import lombok.*; +import net.ripe.rpki.domain.alerts.RoaAlertFrequency; + +import java.util.Collection; import java.util.Collections; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -import net.ripe.rpki.domain.alerts.RoaAlertFrequency; - +@ToString +@NoArgsConstructor +@AllArgsConstructor public class Subscriptions { + @Setter private Set emails; + @Setter private Set routeValidityStates; + @Getter private RoaAlertFrequency frequency; - - public Subscriptions() { - } - - public Subscriptions(Set emails, Set routeValidityStates, RoaAlertFrequency frequency) { - this.routeValidityStates = routeValidityStates; - this.emails = emails; - this.frequency = frequency; - } + @Getter + @Setter + private boolean notifyOnRoaChanges = false; public Subscriptions(Set emails, Set routeValidityStates) { - this(emails, routeValidityStates, RoaAlertFrequency.DAILY); + this(emails, routeValidityStates, RoaAlertFrequency.DAILY, false); } public Set getRouteValidityStates() { @@ -31,30 +34,17 @@ public Set getRouteValidityStates() { routeValidityStates.stream().filter(Objects::nonNull).collect(Collectors.toSet()); } - public void setRouteValidityStates(Set routeValidityStates) { - this.routeValidityStates = routeValidityStates; - } - public Set getEmails() { return emails == null ? Collections.emptySet() : emails.stream().filter(Objects::nonNull).collect(Collectors.toSet()); } - public void setEmails(Set emails) { - this.emails = emails; - } - - public RoaAlertFrequency getFrequency() { - return frequency; + public static Subscriptions defaultSubscriptions(Collection emails, Set validityStates) { + return new Subscriptions(Sets.newHashSet(emails), validityStates); } - @Override - public String toString() { - return "Subscriptions{" + - "emails=" + emails + - ", routeValidityStates=" + routeValidityStates + - ", frequency=" + frequency + - '}'; + public static Subscriptions defaultSubscriptions() { + return new Subscriptions(Collections.emptySet(), Collections.emptySet()); } } diff --git a/src/main/java/net/ripe/rpki/rest/service/AlertService.java b/src/main/java/net/ripe/rpki/rest/service/AlertService.java index ae94deb..9ae5023 100644 --- a/src/main/java/net/ripe/rpki/rest/service/AlertService.java +++ b/src/main/java/net/ripe/rpki/rest/service/AlertService.java @@ -15,6 +15,7 @@ import net.ripe.rpki.server.api.commands.SubscribeToRoaAlertCommand; import net.ripe.rpki.server.api.commands.UnsubscribeFromRoaAlertCommand; import net.ripe.rpki.server.api.commands.UpdateRoaAlertIgnoredAnnouncedRoutesCommand; +import net.ripe.rpki.server.api.commands.UpdateRoaChangeAlertCommand; import net.ripe.rpki.server.api.dto.HostedCertificateAuthorityData; import net.ripe.rpki.server.api.dto.RoaAlertConfigurationData; import net.ripe.rpki.server.api.services.command.CommandService; @@ -55,70 +56,42 @@ public ResponseEntity getAlertsForCa(@PathVariable("caName") fina final HostedCertificateAuthorityData ca = getCa(HostedCertificateAuthorityData.class, caName); final RoaAlertConfigurationData configuration = roaAlertConfigurationViewService.findRoaAlertSubscription(ca.getId()); if (configuration == null) { - return ok(new Subscriptions(Collections.emptySet(), Collections.emptySet())); + return ok(Subscriptions.defaultSubscriptions()); } final Set validityStates = configuration.getRouteValidityStates().stream() .map(RouteValidityState::name) .collect(Collectors.toSet()); - final RoaAlertFrequency frequency = configuration.getSubscription() == null ? - RoaAlertFrequency.DAILY : configuration.getSubscription().getFrequency(); - - return ok(new Subscriptions(Sets.newHashSet(configuration.getEmails()), validityStates, frequency)); + var subscription = configuration.getSubscription(); + if (subscription == null) { + return ok(Subscriptions.defaultSubscriptions(configuration.getEmails(), validityStates)); + } + return ok(new Subscriptions(Sets.newHashSet(configuration.getEmails()), + validityStates, + subscription.getFrequency(), + subscription.isNotifyOnRoaChanges())); } @PostMapping(consumes = {APPLICATION_JSON}) @Operation(summary = "Subscribe/Unsubscribe for alerts about invalid or unknown announcements") - public ResponseEntity subscribe(@PathVariable("caName") final CaName caName, @RequestBody final Subscriptions newSubscription) { + public ResponseEntity subscribe(@PathVariable("caName") final CaName caName, + @RequestBody final Subscriptions newSubscription) { log.info("Subscribing to alerts about invalid or unknown announcement caName[{}], subscription {}", caName, newSubscription); if (newSubscription == null) { return badRequest("No valid subscription provided"); } - - final Set newValidityStates = newSubscription.getRouteValidityStates().stream() - .map(RouteValidityState::valueOf) - .collect(Collectors.toSet()); - - final Set newEmails = newSubscription.getEmails(); - - final HostedCertificateAuthorityData ca = getCa(HostedCertificateAuthorityData.class, caName); - final RoaAlertConfigurationData currentConfiguration = roaAlertConfigurationViewService.findRoaAlertSubscription(ca.getId()); - final Set currentEmails = currentConfiguration == null || currentConfiguration.getEmails() == null ? - Collections.emptySet() : new HashSet<>(currentConfiguration.getEmails()); - final Set currentValidityStates = currentConfiguration == null || currentConfiguration.getRouteValidityStates() == null ? - Collections.emptySet() : currentConfiguration.getRouteValidityStates(); - final RoaAlertFrequency currentFrequency = currentConfiguration == null || currentConfiguration.getSubscription() == null ? - null : currentConfiguration.getSubscription().getFrequency(); - - if (newValidityStates.isEmpty()) { - currentEmails.forEach(email -> - commandService.execute(new UnsubscribeFromRoaAlertCommand(ca.getVersionedId(), email))); - } else { - // Unsubscribe addresses that are no longer in the new subscription list. - currentEmails.stream() - .filter(object -> !newEmails.contains(object)) - .forEach(email -> commandService.execute(new UnsubscribeFromRoaAlertCommand(ca.getVersionedId(), email))); - - // If both validity and frequency remains, only subscribe additional email. - if (newValidityStates.equals(currentValidityStates) && newSubscription.getFrequency().equals(currentFrequency)) { - newEmails.stream() - .filter(email -> !currentEmails.contains(email)) - .forEach(email -> - commandService.execute(new SubscribeToRoaAlertCommand(ca.getVersionedId(), email, newValidityStates, newSubscription.getFrequency()))); - - } else { - // Either validity or frequency changes so these guys have to be subscribed. - newEmails.forEach(email -> - commandService.execute(new SubscribeToRoaAlertCommand(ca.getVersionedId(), email, newValidityStates, newSubscription.getFrequency()))); - } + if (newSubscription.getFrequency() == null) { + return badRequest("No valid subscription frequency provided"); } + doSubscribe(caName, newSubscription); return ok(); } @PostMapping(path = "/suppress", consumes = {APPLICATION_JSON}) @Operation(summary = "Suppress alerts for announcements") - public ResponseEntity suppress(@PathVariable("caName") final CaName caName, @RequestBody final List announcements) { + public ResponseEntity suppress(@PathVariable("caName") final CaName caName, + @RequestBody final List announcements) { log.info("Suppress alerts for announcements for CA: {}", caName); HostedCertificateAuthorityData ca = getCa(HostedCertificateAuthorityData.class, caName); return processMuteOrUnMute(ca, getAnnouncedRoutes(announcements), Collections.emptySet()); @@ -126,13 +99,16 @@ public ResponseEntity suppress(@PathVariable("caName") final CaName caName, @ @PostMapping(path = "/unsuppress", consumes = {APPLICATION_JSON}) @Operation(summary = "Enable alerts for announcements") - public ResponseEntity enable(@PathVariable("caName") final CaName caName, @RequestBody final List announcements) { + public ResponseEntity enable(@PathVariable("caName") final CaName caName, + @RequestBody final List announcements) { log.info("Enable alerts for announcements for CA: {}", caName); HostedCertificateAuthorityData ca = getCa(HostedCertificateAuthorityData.class, caName); return processMuteOrUnMute(ca, Collections.emptySet(), getAnnouncedRoutes(announcements)); } - private ResponseEntity processMuteOrUnMute(final HostedCertificateAuthorityData ca, final Collection toMute, final Collection toUnmute) { + private ResponseEntity processMuteOrUnMute(final HostedCertificateAuthorityData ca, + final Collection toMute, + final Collection toUnmute) { commandService.execute(new UpdateRoaAlertIgnoredAnnouncedRoutesCommand(ca.getVersionedId(), toMute, toUnmute)); return created(); } @@ -142,4 +118,69 @@ private Collection getAnnouncedRoutes(List anno .map(bgp -> new AnnouncedRoute(Asn.parse(bgp.getAsn()), IpRange.parse(bgp.getPrefix()))).toList(); } + private void doSubscribe(CaName caName, Subscriptions newSubscription) { + + final Set newEmails = newSubscription.getEmails(); + final Set newValidityStates = newSubscription.getRouteValidityStates().stream() + .map(RouteValidityState::valueOf) + .collect(Collectors.toSet()); + + final HostedCertificateAuthorityData ca = getCa(HostedCertificateAuthorityData.class, caName); + final RoaAlertConfigurationData currentConfiguration = roaAlertConfigurationViewService.findRoaAlertSubscription(ca.getId()); + + if (currentConfiguration == null) { + if (newValidityStates.isEmpty() && newSubscription.isNotifyOnRoaChanges()) { + commandService.execute(new UpdateRoaChangeAlertCommand(ca.getVersionedId(), true)); + } else { + newEmails.forEach(email -> + commandService.execute(new SubscribeToRoaAlertCommand(ca.getVersionedId(), + email, newValidityStates, + newSubscription.getFrequency(), + newSubscription.isNotifyOnRoaChanges()))); + } + } else { + final Set currentEmails = currentConfiguration.getEmails() == null ? + Collections.emptySet() : new HashSet<>(currentConfiguration.getEmails()); + final Set currentValidityStates = currentConfiguration.getRouteValidityStates() == null ? + Collections.emptySet() : currentConfiguration.getRouteValidityStates(); + final RoaAlertFrequency currentFrequency = currentConfiguration.getSubscription() == null ? + null : currentConfiguration.getSubscription().getFrequency(); + + currentEmails.stream() + .filter(object -> !newEmails.contains(object)) + .forEach(email -> + commandService.execute(new UnsubscribeFromRoaAlertCommand(ca.getVersionedId(), + email, newSubscription.isNotifyOnRoaChanges()))); + + // If both validity and frequency stay the same, only subscribe additional email. + if (Objects.equals(newValidityStates, currentValidityStates) && Objects.equals(newSubscription.getFrequency(), currentFrequency)) { + if (newEmails.equals(currentEmails)) { + // if emails also stay the same, the only thing that can change is notifyOnRoaChanges flag. + // In this case issue special command updating only this flag. + if (newSubscription.isNotifyOnRoaChanges() != currentConfiguration.isNotifyOnRoaChanges()) { + commandService.execute(new UpdateRoaChangeAlertCommand(ca.getVersionedId(), newSubscription.isNotifyOnRoaChanges())); + } + } else { + newEmails.stream() + .filter(email -> !currentEmails.contains(email)) + .forEach(email -> + commandService.execute(new SubscribeToRoaAlertCommand(ca.getVersionedId(), + email, newValidityStates, + newSubscription.getFrequency(), + newSubscription.isNotifyOnRoaChanges()))); + } + } else { + if (!newValidityStates.isEmpty() && newSubscription.getFrequency() != null) { + // Either validity or frequency changes so these guys have to be subscribed. + newEmails.forEach(email -> + commandService.execute(new SubscribeToRoaAlertCommand(ca.getVersionedId(), + email, newValidityStates, + newSubscription.getFrequency(), + newSubscription.isNotifyOnRoaChanges()))); + } + } + } + } + + } 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 745bf25..1101da5 100644 --- a/src/main/java/net/ripe/rpki/rest/service/CaRoaConfigurationService.java +++ b/src/main/java/net/ripe/rpki/rest/service/CaRoaConfigurationService.java @@ -15,6 +15,7 @@ import net.ripe.rpki.rest.pojo.ROAWithAnnouncementStatus; import net.ripe.rpki.server.api.commands.UpdateRoaConfigurationCommand; import net.ripe.rpki.server.api.dto.*; +import net.ripe.rpki.server.api.security.RunAsUserHolder; 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; @@ -293,7 +294,8 @@ public ResponseEntity publishROAs(@PathVariable("caName") final CaName caName ca.getVersionedId(), Optional.ofNullable(ifMatch), getRoaConfigurationPrefixDatas(publishSet.getAdded()), - getRoaConfigurationPrefixDatas(publishSet.getDeleted()) + getRoaConfigurationPrefixDatas(publishSet.getDeleted()), + RunAsUserHolder.get().getCertificationUserId() )); return noContent(); } catch (Exception e) { diff --git a/src/main/java/net/ripe/rpki/rest/service/EmailService.java b/src/main/java/net/ripe/rpki/rest/service/EmailService.java index 4821b55..4d2233b 100644 --- a/src/main/java/net/ripe/rpki/rest/service/EmailService.java +++ b/src/main/java/net/ripe/rpki/rest/service/EmailService.java @@ -66,7 +66,7 @@ public ResponseEntity unsubscribe( if (subscriptionOrNull != null && subscriptionOrNull.getEmails().contains(email) && token.equals(configurationToken)) { - commandService.execute(new UnsubscribeFromRoaAlertCommand(ca.getVersionedId(), email)); + commandService.execute(new UnsubscribeFromRoaAlertCommand(ca.getVersionedId(), email, configuration.isNotifyOnRoaChanges())); unsubscribedAnyone.set(true); } }); diff --git a/src/main/java/net/ripe/rpki/server/api/commands/SubscribeToRoaAlertCommand.java b/src/main/java/net/ripe/rpki/server/api/commands/SubscribeToRoaAlertCommand.java index bd0abcf..5791572 100644 --- a/src/main/java/net/ripe/rpki/server/api/commands/SubscribeToRoaAlertCommand.java +++ b/src/main/java/net/ripe/rpki/server/api/commands/SubscribeToRoaAlertCommand.java @@ -19,33 +19,35 @@ public class SubscribeToRoaAlertCommand extends CertificateAuthorityCommand { private final String email; private final Collection routeValidityStates; private final RoaAlertFrequency frequency; + private final boolean notifyOnRoaChanges; public SubscribeToRoaAlertCommand(VersionedId certificateAuthorityId, String email, Collection routeValidityStates) { - this(certificateAuthorityId, email, routeValidityStates, RoaAlertFrequency.DAILY); + this(certificateAuthorityId, email, routeValidityStates, RoaAlertFrequency.DAILY, false); } public SubscribeToRoaAlertCommand(VersionedId certificateAuthorityId, String email, Collection routeValidityStates, - RoaAlertFrequency frequency) { + RoaAlertFrequency frequency, + boolean notifyOnRoaChanges) { super(certificateAuthorityId, CertificateAuthorityCommandGroup.USER); Validate.notEmpty(email, "email is required"); Validate.notEmpty(routeValidityStates, "routeValidityStates is required"); this.email = email; this.routeValidityStates = EnumSet.copyOf(routeValidityStates); this.frequency = frequency; + this.notifyOnRoaChanges = notifyOnRoaChanges; } - // Let's make this conform to human repre - private String validitySummary(){ - if(routeValidityStates.contains(RouteValidityState.UNKNOWN)) - return "invalid and unknown announcements."; - else - return "invalid announcements only."; + // Let's make this conform to human representation + private String roaSummary() { + var stateMessage = routeValidityStates.contains(RouteValidityState.UNKNOWN) ? "invalid and unknown announcements" : "invalid announcements"; + var roaMessage = notifyOnRoaChanges ? " and ROA changes." : "."; + return stateMessage + roaMessage; } @Override public String getCommandSummary() { - return "Subscribed " + email + " to " + frequency.toString().toLowerCase() + " ROA alerts for "+validitySummary(); + return "Subscribed " + email + " to " + frequency.toString().toLowerCase() + " ROA alerts for " + roaSummary(); } } diff --git a/src/main/java/net/ripe/rpki/server/api/commands/UnsubscribeFromRoaAlertCommand.java b/src/main/java/net/ripe/rpki/server/api/commands/UnsubscribeFromRoaAlertCommand.java index f2a728e..b494236 100644 --- a/src/main/java/net/ripe/rpki/server/api/commands/UnsubscribeFromRoaAlertCommand.java +++ b/src/main/java/net/ripe/rpki/server/api/commands/UnsubscribeFromRoaAlertCommand.java @@ -5,20 +5,23 @@ /** * UN-Subscribe an email address to alerts about BGP updates seen by RIS - * that are invalidated by the CA's ROAs. + * that are invalidated by the CA's ROAs. */ +@Getter public class UnsubscribeFromRoaAlertCommand extends CertificateAuthorityCommand { - @Getter private final String email; + private final boolean notifyOnRoaChanges; - public UnsubscribeFromRoaAlertCommand(VersionedId certificateAuthorityId, String email) { + public UnsubscribeFromRoaAlertCommand(VersionedId certificateAuthorityId, String email, boolean notifyOnRoaChanges) { super(certificateAuthorityId, CertificateAuthorityCommandGroup.USER); this.email = email; + this.notifyOnRoaChanges = notifyOnRoaChanges; } @Override public String getCommandSummary() { - return "Unsubscribed " + email + " from ROA alerts."; + var roaSummary = notifyOnRoaChanges ? " and ROA changes." : "."; + return "Unsubscribed " + email + " from ROA alerts" + roaSummary; } } diff --git a/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaAlertIgnoredAnnouncedRoutesCommand.java b/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaAlertIgnoredAnnouncedRoutesCommand.java index 889b80a..4c3506c 100644 --- a/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaAlertIgnoredAnnouncedRoutesCommand.java +++ b/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaAlertIgnoredAnnouncedRoutesCommand.java @@ -17,7 +17,9 @@ public class UpdateRoaAlertIgnoredAnnouncedRoutesCommand extends CertificateAuth private final List deletions; - public UpdateRoaAlertIgnoredAnnouncedRoutesCommand(VersionedId certificateAuthorityId, Collection added, Collection deleted) { + public UpdateRoaAlertIgnoredAnnouncedRoutesCommand(VersionedId certificateAuthorityId, + Collection added, + Collection deleted) { super(certificateAuthorityId, CertificateAuthorityCommandGroup.USER); this.additions = new ArrayList<>(added); this.additions.sort(RouteData.ROUTE_DATA_COMPARATOR); diff --git a/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaChangeAlertCommand.java b/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaChangeAlertCommand.java new file mode 100644 index 0000000..fa4c9e4 --- /dev/null +++ b/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaChangeAlertCommand.java @@ -0,0 +1,21 @@ +package net.ripe.rpki.server.api.commands; + +import lombok.Getter; +import net.ripe.rpki.commons.util.VersionedId; + +@Getter +public class UpdateRoaChangeAlertCommand extends CertificateAuthorityCommand { + + private final boolean notifyOnRoaChanges; + + public UpdateRoaChangeAlertCommand(VersionedId certificateAuthorityId, boolean notifyOnRoaChanges) { + super(certificateAuthorityId, CertificateAuthorityCommandGroup.USER); + this.notifyOnRoaChanges = notifyOnRoaChanges; + } + + @Override + public String getCommandSummary() { + var action = notifyOnRoaChanges ? "Subscribed " : "Unsubscribed "; + return action + " from ROA change alerts."; + } +} diff --git a/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommand.java b/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommand.java index aa86504..c6c6ceb 100644 --- a/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommand.java +++ b/src/main/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommand.java @@ -4,6 +4,7 @@ import net.ripe.rpki.commons.util.VersionedId; import net.ripe.rpki.commons.validation.roa.RoaPrefixData; import net.ripe.rpki.server.api.dto.RoaConfigurationPrefixData; +import net.ripe.rpki.server.api.security.CertificationUserId; import org.apache.commons.lang.StringUtils; import java.util.*; @@ -21,17 +22,21 @@ public class UpdateRoaConfigurationCommand extends CertificateAuthorityModificat private final List additions; private final List deletions; + @Getter + private final CertificationUserId userId; public UpdateRoaConfigurationCommand(VersionedId certificateAuthorityId, Optional ifMatch, Collection added, - Collection deleted) { + Collection deleted, + CertificationUserId certificationUserId) { super(certificateAuthorityId, CertificateAuthorityCommandGroup.USER); this.ifMatch = ifMatch; this.additions = new ArrayList<>(added); this.additions.sort(RoaPrefixData.ROA_PREFIX_DATA_COMPARATOR); this.deletions = new ArrayList<>(deleted); this.deletions.sort(RoaPrefixData.ROA_PREFIX_DATA_COMPARATOR); + this.userId = certificationUserId; } public List getAdditions() { diff --git a/src/main/java/net/ripe/rpki/server/api/dto/CertificateAuthorityHistoryItem.java b/src/main/java/net/ripe/rpki/server/api/dto/CertificateAuthorityHistoryItem.java index 9158461..655283a 100644 --- a/src/main/java/net/ripe/rpki/server/api/dto/CertificateAuthorityHistoryItem.java +++ b/src/main/java/net/ripe/rpki/server/api/dto/CertificateAuthorityHistoryItem.java @@ -1,5 +1,6 @@ package net.ripe.rpki.server.api.dto; +import lombok.Getter; import net.ripe.rpki.server.api.support.objects.ValueObjectSupport; import org.joda.time.DateTime; @@ -7,7 +8,9 @@ public abstract class CertificateAuthorityHistoryItem extends ValueObjectSupport implements Serializable { + @Getter private final DateTime executionTime; + @Getter private final String principal; private final String commandSummary; @@ -17,14 +20,6 @@ protected CertificateAuthorityHistoryItem(DateTime executionTime, String princip this.commandSummary = commandSummary; } - public DateTime getExecutionTime() { - return executionTime; - } - - public String getPrincipal() { - return principal; - } - public String getSummary() { return commandSummary; } diff --git a/src/main/java/net/ripe/rpki/server/api/dto/RoaAlertConfigurationData.java b/src/main/java/net/ripe/rpki/server/api/dto/RoaAlertConfigurationData.java index 93d3c6c..678f4f0 100644 --- a/src/main/java/net/ripe/rpki/server/api/dto/RoaAlertConfigurationData.java +++ b/src/main/java/net/ripe/rpki/server/api/dto/RoaAlertConfigurationData.java @@ -1,5 +1,6 @@ package net.ripe.rpki.server.api.dto; +import lombok.Getter; import lombok.ToString; import net.ripe.rpki.commons.validation.roa.AnnouncedRoute; import net.ripe.rpki.commons.validation.roa.RouteValidityState; @@ -8,6 +9,7 @@ import java.util.*; @ToString +@Getter public class RoaAlertConfigurationData extends ValueObjectSupport { private final CertificateAuthorityData certificateAuthority; @@ -18,24 +20,14 @@ public RoaAlertConfigurationData(CertificateAuthorityData certificateAuthority, this(certificateAuthority, subscription, Collections.emptySet()); } - public RoaAlertConfigurationData(CertificateAuthorityData certificateAuthority, RoaAlertSubscriptionData subscription, Collection ignoredAnnouncements) { + public RoaAlertConfigurationData(CertificateAuthorityData certificateAuthority, + RoaAlertSubscriptionData subscription, + Collection ignoredAnnouncements) { this.certificateAuthority = certificateAuthority; this.subscription = subscription; this.ignoredAnnouncements = new HashSet<>(ignoredAnnouncements); } - public CertificateAuthorityData getCertificateAuthority() { - return certificateAuthority; - } - - public RoaAlertSubscriptionData getSubscription() { - return subscription; - } - - public Set getIgnoredAnnouncements() { - return ignoredAnnouncements; - } - public List getEmails() { return subscription == null ? Collections.emptyList() : subscription.getEmails(); } @@ -44,6 +36,10 @@ public Set getRouteValidityStates() { return subscription == null ? Collections.emptySet() : subscription.getRouteValidityStates(); } + public boolean isNotifyOnRoaChanges() { + return subscription != null && subscription.isNotifyOnRoaChanges(); + } + public boolean hasSubscription() { return subscription != null; } diff --git a/src/main/java/net/ripe/rpki/server/api/dto/RoaAlertSubscriptionData.java b/src/main/java/net/ripe/rpki/server/api/dto/RoaAlertSubscriptionData.java index 5799825..63e3ef9 100644 --- a/src/main/java/net/ripe/rpki/server/api/dto/RoaAlertSubscriptionData.java +++ b/src/main/java/net/ripe/rpki/server/api/dto/RoaAlertSubscriptionData.java @@ -5,7 +5,10 @@ import net.ripe.rpki.domain.alerts.RoaAlertFrequency; import net.ripe.rpki.server.api.support.objects.ValueObjectSupport; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; @Getter public class RoaAlertSubscriptionData extends ValueObjectSupport { @@ -13,15 +16,19 @@ public class RoaAlertSubscriptionData extends ValueObjectSupport { private final List emails; private final RoaAlertFrequency frequency; private final EnumSet routeValidityStates; + private final boolean notifyOnRoaChanges; - public RoaAlertSubscriptionData(String email, Collection routeValidityStates, RoaAlertFrequency frequency) { - this(List.of(email), routeValidityStates, frequency); + public RoaAlertSubscriptionData(String email, Collection routeValidityStates, + RoaAlertFrequency frequency, boolean notifyOnRoaChanges) { + this(List.of(email), routeValidityStates, frequency, notifyOnRoaChanges); } public RoaAlertSubscriptionData(List emails, Collection routeValidityStates, - RoaAlertFrequency frequency) { + RoaAlertFrequency frequency, + boolean notifyOnRoaChanges) { this.emails = new ArrayList<>(emails); this.routeValidityStates = EnumSet.copyOf(routeValidityStates); this.frequency = frequency; + this.notifyOnRoaChanges = notifyOnRoaChanges; } } diff --git a/src/main/java/net/ripe/rpki/server/api/security/CertificationUserId.java b/src/main/java/net/ripe/rpki/server/api/security/CertificationUserId.java index 993eb63..78e6db5 100644 --- a/src/main/java/net/ripe/rpki/server/api/security/CertificationUserId.java +++ b/src/main/java/net/ripe/rpki/server/api/security/CertificationUserId.java @@ -1,25 +1,17 @@ package net.ripe.rpki.server.api.security; +import lombok.AllArgsConstructor; +import lombok.Getter; import net.ripe.rpki.server.api.support.objects.ValueObjectSupport; import java.util.UUID; +@AllArgsConstructor public class CertificationUserId extends ValueObjectSupport { - private static final UUID SYSTEM_ID = UUID.fromString("3b22801d-c151-4bcc-9298-a93df3f365d9"); - public static final CertificationUserId SYSTEM = new CertificationUserId(SYSTEM_ID); - - private final UUID id; + private static final UUID SYSTEM_ID = UUID.fromString("3b22801d-c151-4bcc-9298-a93df3f365d9"); + public static final CertificationUserId SYSTEM = new CertificationUserId(SYSTEM_ID); - public CertificationUserId(UUID id) { - this.id = id; - } - - public CertificationUserId(String id) { - this.id = UUID.fromString(id); - } - - public UUID getId() { - return id; - } + @Getter + private final UUID id; } diff --git a/src/main/java/net/ripe/rpki/server/api/security/RunAsUser.java b/src/main/java/net/ripe/rpki/server/api/security/RunAsUser.java index c9509ea..88a17cc 100644 --- a/src/main/java/net/ripe/rpki/server/api/security/RunAsUser.java +++ b/src/main/java/net/ripe/rpki/server/api/security/RunAsUser.java @@ -1,5 +1,6 @@ package net.ripe.rpki.server.api.security; +import lombok.Getter; import net.ripe.rpki.server.api.support.objects.ValueObjectSupport; import org.apache.commons.lang.Validate; @@ -10,7 +11,9 @@ public final class RunAsUser extends ValueObjectSupport { private final CertificationUserId userId; + @Getter private final String friendlyName; + @Getter private final List roles; /** @@ -37,11 +40,4 @@ public CertificationUserId getCertificationUserId() { return userId; } - public String getFriendlyName() { - return friendlyName; - } - - public List getRoles() { - return roles; - } } diff --git a/src/main/java/net/ripe/rpki/services/impl/background/RoaNotificationService.java b/src/main/java/net/ripe/rpki/services/impl/background/RoaNotificationService.java new file mode 100644 index 0000000..38152f0 --- /dev/null +++ b/src/main/java/net/ripe/rpki/services/impl/background/RoaNotificationService.java @@ -0,0 +1,100 @@ +package net.ripe.rpki.services.impl.background; + +import lombok.extern.slf4j.Slf4j; +import net.ripe.ipresource.Asn; +import net.ripe.ipresource.IpRange; +import net.ripe.rpki.domain.ManagedCertificateAuthority; +import net.ripe.rpki.domain.alerts.RoaAlertConfiguration; +import net.ripe.rpki.domain.alerts.RoaAlertConfigurationRepository; +import net.ripe.rpki.domain.roa.RoaConfigurationPrefix; +import net.ripe.rpki.server.api.dto.RoaAlertSubscriptionData; +import net.ripe.rpki.server.api.ports.InternalNamePresenter; +import net.ripe.rpki.server.api.security.CertificationUserId; +import net.ripe.rpki.services.impl.email.EmailSender; +import net.ripe.rpki.services.impl.email.EmailTokens; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Triple; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Slf4j +@Service +public class RoaNotificationService { + + private final RoaAlertConfigurationRepository roaAlertConfigurationRepository; + private final EmailSender emailSender; + private final InternalNamePresenter internalNamePresenter; + + @Autowired + public RoaNotificationService(RoaAlertConfigurationRepository roaAlertConfigurationRepository, + EmailSender emailSender, + InternalNamePresenter internalNamePresenter) { + this.roaAlertConfigurationRepository = roaAlertConfigurationRepository; + this.emailSender = emailSender; + this.internalNamePresenter = internalNamePresenter; + } + + public List notifyAboutRoaChanges(ManagedCertificateAuthority ca, + CertificationUserId userId, + List added, + List removed) { + RoaAlertConfiguration configuration = roaAlertConfigurationRepository.findByCertificateAuthorityIdOrNull(ca.getId()); + if (configuration == null || !configuration.isNotifyOnRoaChanges() || (added.isEmpty() && removed.isEmpty())) { + return Collections.emptyList(); + } + RoaAlertSubscriptionData subscriptionOrNull = configuration.getSubscriptionOrNull(); + if (subscriptionOrNull == null) { + return Collections.emptyList(); + } + var humanizedCaName = internalNamePresenter.humanizeCaName(ca.getName()); + var ssoEmail = internalNamePresenter.humanizeUserPrincipal(userId.getId().toString()); + + var parameters = Map.of( + "humanizedCaName", humanizedCaName, + "roas", showRoas(added, removed), + "ssoEmail", ssoEmail + ); + + return subscriptionOrNull.getEmails().stream() + .map(email -> emailSender.sendEmail( + email, + String.format(EmailSender.EmailTemplates.ROA_CHANGE_ALERT.templateSubject, humanizedCaName), + EmailSender.EmailTemplates.ROA_CHANGE_ALERT, + parameters, + EmailTokens.uniqueId(ca.getUuid()))) + .toList(); + } + + public record Roa(String asn, String prefix, String maxLength, char operation) { + } + + private List showRoas(List added, List removed) { + BiFunction padded = (o, size) -> + StringUtils.rightPad(o.toString(), size); + + BiFunction textRoa = (r, operation) -> { + IpRange ip = r.getPrefix(); + return new Roa( + padded.apply(r.getAsn().longValue(), 10), + padded.apply(ip, 50), + padded.apply(r.getMaximumLength(), 12), operation); + }; + + return Stream.concat( + added.stream().map(r -> Triple.of(r.getAsn(), r.getPrefix(), textRoa.apply(r, 'A'))), + removed.stream().map(r -> Triple.of(r.getAsn(), r.getPrefix(), textRoa.apply(r, 'D'))) + ) + .sorted(Comparator.comparing((Triple t) -> t.getLeft()).thenComparing(Triple::getMiddle)) + .map(Triple::getRight) + .toList(); + } + +} diff --git a/src/main/java/net/ripe/rpki/services/impl/email/EmailSender.java b/src/main/java/net/ripe/rpki/services/impl/email/EmailSender.java index 97038b3..7624341 100644 --- a/src/main/java/net/ripe/rpki/services/impl/email/EmailSender.java +++ b/src/main/java/net/ripe/rpki/services/impl/email/EmailSender.java @@ -4,24 +4,38 @@ public interface EmailSender { - void sendEmail(String emailTo, String subject, EmailTemplates template, Map parameters, String uniqueId); + ResultingEmail sendEmail(String emailTo, String subject, EmailTemplates template, Map parameters, String uniqueId); // Limit the number of possible inputs to allow us to check all templates in tests. enum EmailTemplates { - ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY("email-templates/subscribe-confirmation-weekly.txt", "Your Resource Certification (RPKI) alerts subscription", true), - ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY("email-templates/subscribe-confirmation-daily.txt", "Your Resource Certification (RPKI) alerts subscription", true), - ROA_ALERT_UNSUBSCRIBE("email-templates/unsubscribe-confirmation.txt", "Unsubscribe from Resource Certification (RPKI) alerts", false), - ROA_ALERT("email-templates/roa-alert-email.txt", "Resource Certification (RPKI) alerts for %s", true); + ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY("email-templates/subscribe-confirmation-weekly.txt", + "Your Resource Certification (RPKI) alerts subscription"), + ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY("email-templates/subscribe-confirmation-daily.txt", + "Your Resource Certification (RPKI) alerts subscription"), + ROA_ALERT_UNSUBSCRIBE("email-templates/unsubscribe-confirmation.txt", + "Unsubscribe from Resource Certification (RPKI) alerts", false), + ROA_ALERT("email-templates/roa-alert-email.txt", "Resource Certification (RPKI) alerts for %s"), + ROA_CHANGE_ALERT("email-templates/roa-change-alert-email.txt", "ROAs changed for %s"), + ROA_CHANGE_ALERT_SUBSCRIBE_CONFIRMATION("email-templates/subscribe-confirmation-change.txt", + "Your Resource Certification (RPKI) ROA change alerts subscription"), + ROA_CHANGE_ALERT_UNSUBSCRIBE_CONFIRMATION("email-templates/unsubscribe-confirmation-change.txt", + "Your Resource Certification (RPKI) ROA change alerts subscription"); public final String templateName; public final String templateSubject; - public final boolean generateUnsubcribeUrl; + public final boolean generateUnsubscribeUrl; - EmailTemplates(String templateName, String subject, boolean generateUnsubcribeUrl) { + EmailTemplates(String templateName, String subject) { + this(templateName, subject, true); + } + + EmailTemplates(String templateName, String subject, boolean generateUnsubscribeUrl) { this.templateName = templateName; this.templateSubject = subject; - this.generateUnsubcribeUrl = generateUnsubcribeUrl; + this.generateUnsubscribeUrl = generateUnsubscribeUrl; } } + record ResultingEmail(String email, String subject, String body) {} + } diff --git a/src/main/java/net/ripe/rpki/services/impl/email/EmailSenderBean.java b/src/main/java/net/ripe/rpki/services/impl/email/EmailSenderBean.java index 8ffa7a3..3419d3f 100644 --- a/src/main/java/net/ripe/rpki/services/impl/email/EmailSenderBean.java +++ b/src/main/java/net/ripe/rpki/services/impl/email/EmailSenderBean.java @@ -61,10 +61,10 @@ protected static ITemplateResolver textTemplateResolver() { } @Override - public void sendEmail(String emailTo, String subject, EmailTemplates template, Map parameters, String uniqueId) { + public ResultingEmail sendEmail(String emailTo, String subject, EmailTemplates template, Map parameters, String uniqueId) { if (!(mailSender instanceof JavaMailSenderImpl)) { log.error("mailSender is not configured properly, {}", mailSender.getClass()); - return; + return null; } try { @@ -75,7 +75,7 @@ public void sendEmail(String emailTo, String subject, EmailTemplates template, M message.setRecipient(Message.RecipientType.TO, new InternetAddress(emailTo)); message.setSubject(subject); var parametersUpdated = parameters; - if (template.generateUnsubcribeUrl) { + if (template.generateUnsubscribeUrl) { var unsubscribeUri = emailTokens.makeUnsubscribeUrl(uniqueId, emailTo); message.addHeader("List-Unsubscribe", "<" + unsubscribeUri + ">"); message.addHeader("List-Unsubscribe-Post", "List-Unsubscribe=One-Click"); @@ -83,7 +83,8 @@ public void sendEmail(String emailTo, String subject, EmailTemplates template, M } log.info("Rendering Email template {}", template.templateName); - message.setText(renderTemplate(template.templateName, parametersUpdated)); + var body = renderTemplate(template.templateName, parametersUpdated); + message.setText(body); if (!Environment.isLocal()) { try { @@ -95,8 +96,10 @@ public void sendEmail(String emailTo, String subject, EmailTemplates template, M } else { log.info("Not sending message in DEVELOPMENT mode:\n" + message); } + return new ResultingEmail(emailTo, subject, body); } catch (Exception e) { log.error("Failed to send email", e); + return null; } } diff --git a/src/main/java/net/ripe/rpki/services/impl/handlers/SubscribeToRoaAlertCommandHandler.java b/src/main/java/net/ripe/rpki/services/impl/handlers/SubscribeToRoaAlertCommandHandler.java index 4ae3609..d5c9ae4 100644 --- a/src/main/java/net/ripe/rpki/services/impl/handlers/SubscribeToRoaAlertCommandHandler.java +++ b/src/main/java/net/ripe/rpki/services/impl/handlers/SubscribeToRoaAlertCommandHandler.java @@ -1,6 +1,6 @@ package net.ripe.rpki.services.impl.handlers; -import com.google.common.collect.Sets; +import jakarta.inject.Inject; import net.ripe.rpki.domain.CertificateAuthorityRepository; import net.ripe.rpki.domain.ManagedCertificateAuthority; import net.ripe.rpki.domain.alerts.RoaAlertConfiguration; @@ -10,18 +10,17 @@ import net.ripe.rpki.server.api.dto.RoaAlertSubscriptionData; import net.ripe.rpki.server.api.services.command.CommandStatus; import net.ripe.rpki.services.impl.email.EmailSender; - -import jakarta.inject.Inject; import net.ripe.rpki.services.impl.email.EmailTokens; -import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @Handler public class SubscribeToRoaAlertCommandHandler extends AbstractCertificateAuthorityCommandHandler { - public static final String SUBSCRIPTION = "subscription"; private final RoaAlertConfigurationRepository repository; @@ -54,52 +53,50 @@ private void createConfigurationAndSendConfirmation(SubscribeToRoaAlertCommand c var emailTemplate = getConfirmationTemplate(configuration); emailSender.sendEmail(command.getEmail(), emailTemplate.templateSubject, emailTemplate, - Collections.singletonMap(SUBSCRIPTION, configuration.toData()), + makeParameters(configuration.toData()), EmailTokens.uniqueId(configuration.getCertificateAuthority().getUuid())); } private EmailSender.EmailTemplates getConfirmationTemplate(RoaAlertConfiguration configuration) { - switch (configuration.getFrequency()) { - case DAILY: - return EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY; - case WEEKLY: - return EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY; - default: - throw new IllegalStateException("Frequency should not be null"); - } + return switch (configuration.getFrequency()) { + case DAILY -> EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY; + case WEEKLY -> EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY; + }; } private void updateConfigurationAndSendConfirmation(RoaAlertConfiguration configuration, SubscribeToRoaAlertCommand command) { RoaAlertConfigurationData oldConfiguration = configuration.toData(); - configuration.setSubscription(new RoaAlertSubscriptionData(command.getEmail(), command.getRouteValidityStates(), command.getFrequency())); + configuration.setSubscription(new RoaAlertSubscriptionData(List.of(command.getEmail()), + command.getRouteValidityStates(), command.getFrequency(), command.isNotifyOnRoaChanges())); RoaAlertConfigurationData newConfiguration = configuration.toData(); - final Set oldEmailAddress = oldConfiguration.getEmails().stream().map(RoaAlertConfiguration::normEmail).collect(Collectors.toSet()); - final Set newEmailAddress = newConfiguration.getEmails().stream().map(RoaAlertConfiguration::normEmail).collect(Collectors.toSet()); - - if (oldEmailAddress.equals(newEmailAddress)) - return; + var oldEmailAddress = oldConfiguration.getEmails() + .stream().map(RoaAlertConfiguration::normEmail).collect(Collectors.toSet()); - // elements LHS not in RHS - Sets.difference(newEmailAddress, oldEmailAddress).forEach(email -> { + var normNewEmail = RoaAlertConfiguration.normEmail(command.getEmail()); + if (!oldEmailAddress.contains(normNewEmail)) { + var parametersSubscribe = makeParameters(newConfiguration, command.isNotifyOnRoaChanges()); var emailTemplate = getConfirmationTemplate(configuration); - emailSender.sendEmail(email, emailTemplate.templateSubject, emailTemplate, - Collections.singletonMap(SUBSCRIPTION, newConfiguration), + emailSender.sendEmail(normNewEmail, emailTemplate.templateSubject, emailTemplate, parametersSubscribe, EmailTokens.uniqueId(configuration.getCertificateAuthority().getUuid())); - }); + } + } + + public static Map makeParameters(RoaAlertConfigurationData configuration) { + return makeParameters(configuration, false); + } - Sets.difference(oldEmailAddress, newEmailAddress).forEach(email -> - emailSender.sendEmail(email, EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE.templateSubject, - EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE, - Collections.singletonMap(SUBSCRIPTION, oldConfiguration), - EmailTokens.uniqueId(configuration.getCertificateAuthority().getUuid()))); + public static Map makeParameters(RoaAlertConfigurationData configuration, boolean notifyOnChange) { + return Map.of("subscription", configuration, + "roaChangeSubscription", notifyOnChange ? + "Also you are subscribed to alerts about ROA changes." : ""); } private RoaAlertConfiguration createConfiguration(SubscribeToRoaAlertCommand command) { ManagedCertificateAuthority certificateAuthority = lookupManagedCa(command.getCertificateAuthorityId()); RoaAlertConfiguration configuration = new RoaAlertConfiguration(certificateAuthority); - configuration.setSubscription(new RoaAlertSubscriptionData(command.getEmail(), - command.getRouteValidityStates(), command.getFrequency())); + configuration.setSubscription(new RoaAlertSubscriptionData(List.of(command.getEmail()), + command.getRouteValidityStates(), command.getFrequency(), command.isNotifyOnRoaChanges())); repository.add(configuration); return configuration; } diff --git a/src/main/java/net/ripe/rpki/services/impl/handlers/UnsubscribeFromRoaAlertCommandHandler.java b/src/main/java/net/ripe/rpki/services/impl/handlers/UnsubscribeFromRoaAlertCommandHandler.java index fcc6f51..ba59aaf 100644 --- a/src/main/java/net/ripe/rpki/services/impl/handlers/UnsubscribeFromRoaAlertCommandHandler.java +++ b/src/main/java/net/ripe/rpki/services/impl/handlers/UnsubscribeFromRoaAlertCommandHandler.java @@ -1,5 +1,7 @@ package net.ripe.rpki.services.impl.handlers; +import jakarta.inject.Inject; +import lombok.extern.slf4j.Slf4j; import net.ripe.rpki.domain.CertificateAuthorityRepository; import net.ripe.rpki.domain.alerts.RoaAlertConfiguration; import net.ripe.rpki.domain.alerts.RoaAlertConfigurationRepository; @@ -7,16 +9,12 @@ import net.ripe.rpki.server.api.dto.RoaAlertSubscriptionData; import net.ripe.rpki.server.api.services.command.CommandStatus; import net.ripe.rpki.services.impl.email.EmailSender; - -import jakarta.inject.Inject; import net.ripe.rpki.services.impl.email.EmailTokens; -import java.util.Collections; - import static net.ripe.rpki.domain.alerts.RoaAlertConfiguration.normEmail; - @Handler +@Slf4j public class UnsubscribeFromRoaAlertCommandHandler extends AbstractCertificateAuthorityCommandHandler { private final RoaAlertConfigurationRepository repository; @@ -43,12 +41,18 @@ public void handle(UnsubscribeFromRoaAlertCommand command, CommandStatus command if (subscription == null) { return; } + configuration.setNotifyOnRoaChanges(command.isNotifyOnRoaChanges()); + if (!subscription.getEmails().contains(normEmail(command.getEmail()))) { + log.info("Trying to unsubscribe the address {} that is not amongst subscribed addresses {}", command.getEmail(), subscription.getEmails()); + return; + } configuration.removeEmail(command.getEmail()); + var parameters = SubscribeToRoaAlertCommandHandler.makeParameters(configuration.toData()); emailSender.sendEmail(normEmail(command.getEmail()), EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE.templateSubject, EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE, - Collections.singletonMap("subscription", configuration.toData()), + parameters, EmailTokens.uniqueId(configuration.getCertificateAuthority().getUuid())); } } diff --git a/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaChangeAlertCommandHandler.java b/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaChangeAlertCommandHandler.java new file mode 100644 index 0000000..6ca9ecc --- /dev/null +++ b/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaChangeAlertCommandHandler.java @@ -0,0 +1,66 @@ +package net.ripe.rpki.services.impl.handlers; + +import jakarta.inject.Inject; +import net.ripe.rpki.domain.CertificateAuthorityRepository; +import net.ripe.rpki.domain.ManagedCertificateAuthority; +import net.ripe.rpki.domain.alerts.RoaAlertConfiguration; +import net.ripe.rpki.domain.alerts.RoaAlertConfigurationRepository; +import net.ripe.rpki.server.api.commands.UpdateRoaChangeAlertCommand; +import net.ripe.rpki.server.api.services.command.CommandStatus; +import net.ripe.rpki.services.impl.email.EmailSender; +import net.ripe.rpki.services.impl.email.EmailTokens; + +import java.util.Collections; + + +@Handler +public class UpdateRoaChangeAlertCommandHandler extends AbstractCertificateAuthorityCommandHandler { + + private final RoaAlertConfigurationRepository repository; + + private final EmailSender emailSender; + + @Inject + public UpdateRoaChangeAlertCommandHandler(CertificateAuthorityRepository certificateAuthorityRepository, + RoaAlertConfigurationRepository repository, + EmailSender emailSender) { + super(certificateAuthorityRepository); + this.repository = repository; + this.emailSender = emailSender; + } + + @Override + public Class commandType() { + return UpdateRoaChangeAlertCommand.class; + } + + @Override + public void handle(UpdateRoaChangeAlertCommand command, CommandStatus commandStatus) { + final RoaAlertConfiguration configuration = repository.findByCertificateAuthorityIdOrNull(command.getCertificateAuthorityId()); + if (configuration == null) { + createNewAlertConfiguration(command, command.isNotifyOnRoaChanges()); + // there wasn't any configuration, so no emails to notify + } else if (command.isNotifyOnRoaChanges() != configuration.isNotifyOnRoaChanges()) { + configuration.setNotifyOnRoaChanges(command.isNotifyOnRoaChanges()); + EmailSender.EmailTemplates template = command.isNotifyOnRoaChanges() ? + EmailSender.EmailTemplates.ROA_CHANGE_ALERT_SUBSCRIBE_CONFIRMATION : + EmailSender.EmailTemplates.ROA_CHANGE_ALERT_UNSUBSCRIBE_CONFIRMATION; + sendEmails(configuration, template); + } + } + + private void sendEmails(RoaAlertConfiguration newConfiguration, EmailSender.EmailTemplates template) { + newConfiguration.getSubscriptionOrNull().getEmails().forEach(email -> + emailSender.sendEmail(email, + template.templateSubject, template, + Collections.singletonMap("subscription", newConfiguration), + EmailTokens.uniqueId(newConfiguration.getCertificateAuthority().getUuid()))); + } + + private void createNewAlertConfiguration(UpdateRoaChangeAlertCommand command, boolean notifyOnRoaChanges) { + ManagedCertificateAuthority certificateAuthority = lookupManagedCa(command.getCertificateAuthorityId()); + RoaAlertConfiguration configuration = new RoaAlertConfiguration(certificateAuthority); + configuration.setNotifyOnRoaChanges(notifyOnRoaChanges); + repository.add(configuration); + } +} diff --git a/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandler.java b/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandler.java index 43986fb..1380a24 100644 --- a/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandler.java +++ b/src/main/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandler.java @@ -7,6 +7,7 @@ import net.ripe.ipresource.IpResourceType; import net.ripe.rpki.domain.CertificateAuthorityRepository; import net.ripe.rpki.domain.ManagedCertificateAuthority; +import net.ripe.rpki.domain.alerts.RoaAlertConfigurationRepository; import net.ripe.rpki.domain.roa.RoaConfiguration; import net.ripe.rpki.domain.roa.RoaConfigurationPrefix; import net.ripe.rpki.domain.roa.RoaConfigurationRepository; @@ -17,6 +18,7 @@ import net.ripe.rpki.server.api.services.command.NotHolderOfResourcesException; import net.ripe.rpki.server.api.services.command.PrivateAsnsUsedException; import net.ripe.rpki.services.impl.background.RoaMetricsService; +import net.ripe.rpki.services.impl.background.RoaNotificationService; import org.springframework.beans.factory.annotation.Value; import jakarta.inject.Inject; @@ -30,18 +32,23 @@ public class UpdateRoaConfigurationCommandHandler extends AbstractCertificateAut private final RoaConfigurationRepository roaConfigurationRepository; private final ImmutableResourceSet privateAsnRanges; private final RoaMetricsService roaMetricsService; + private final RoaNotificationService roaNotificationService; @Inject public UpdateRoaConfigurationCommandHandler(CertificateAuthorityRepository certificateAuthorityRepository, RoaConfigurationRepository roaConfigurationRepository, @Value("${private.asns.ranges}") String privateASNS, - RoaMetricsService roaMetricsService) { + RoaMetricsService roaMetricsService, + RoaNotificationService roaNotificationService) { super(certificateAuthorityRepository); this.roaConfigurationRepository = roaConfigurationRepository; this.roaMetricsService = roaMetricsService; this.privateAsnRanges = ImmutableResourceSet.parse(privateASNS); - Preconditions.checkArgument(privateAsnRanges.stream().allMatch(a -> a.getType() == IpResourceType.ASN), "Only ASNs allowed for private ASN ranges: %s", privateAsnRanges); + this.roaNotificationService = roaNotificationService; + + Preconditions.checkArgument(privateAsnRanges.stream().allMatch(a -> a.getType() == IpResourceType.ASN), + "Only ASNs allowed for private ASN ranges: %s", privateAsnRanges); } @Override @@ -58,12 +65,14 @@ public void handle(@NonNull UpdateRoaConfigurationCommand command, CommandStatus validateAsns(command); validateAddedPrefixes(ca, command.getAdditions()); - roaConfigurationRepository.mergePrefixes(configuration, + RoaConfiguration.PrefixDiff prefixDiff = roaConfigurationRepository.mergePrefixes(configuration, RoaConfigurationPrefix.fromData(command.getAdditions()), RoaConfigurationPrefix.fromData(command.getDeletions())); ca.markConfigurationUpdated(); + roaNotificationService.notifyAboutRoaChanges(ca, command.getUserId(), prefixDiff.added(), prefixDiff.removed()); + roaMetricsService.countAdded(command.getAdditions().size()); roaMetricsService.countDeleted(command.getDeletions().size()); } diff --git a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepository.java b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepository.java index 3a45e93..ad74083 100644 --- a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepository.java +++ b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaConfigurationRepository.java @@ -84,11 +84,12 @@ public Optional lastModified() { } @Override - public void mergePrefixes(RoaConfiguration configuration, - Collection prefixesToAdd, - Collection prefixesToRemove) { + public RoaConfiguration.PrefixDiff mergePrefixes(RoaConfiguration configuration, + Collection prefixesToAdd, + Collection prefixesToRemove) { var diff = configuration.mergePrefixes(prefixesToAdd, prefixesToRemove); applyDiff(configuration, diff); + return diff; } public void applyDiff(RoaConfiguration configuration, diff --git a/src/main/resources/db/migration/V133__roa_alerts_add_notify_on_roa_changes.sql b/src/main/resources/db/migration/V133__roa_alerts_add_notify_on_roa_changes.sql new file mode 100644 index 0000000..194681a --- /dev/null +++ b/src/main/resources/db/migration/V133__roa_alerts_add_notify_on_roa_changes.sql @@ -0,0 +1 @@ +ALTER TABLE roa_alert_configuration ADD COLUMN notify_on_roa_changes BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/src/main/resources/email-templates/roa-change-alert-email.txt b/src/main/resources/email-templates/roa-change-alert-email.txt new file mode 100644 index 0000000..46a0cb3 --- /dev/null +++ b/src/main/resources/email-templates/roa-change-alert-email.txt @@ -0,0 +1,10 @@ +This is an automated email to inform you that user [[${ssoEmail}]] made changes +to one or more ROAs for your organisation [[${humanizedCaName}]]. + +ASN Prefix Max Length (A)dd/(D)elete +=============================================================================== +[# th:each="roa : ${roas}" ][(${roa.asn})][( ${roa.prefix})][( ${roa.maxLength})][( ${roa.operation})] +[/] + +You can review and change your ROAs at: [[${rpkiDashboardUri}]] +You can unsubscribe from these alerts at: [(${unsubscribeUri})] \ No newline at end of file diff --git a/src/main/resources/email-templates/subscribe-confirmation-change.txt b/src/main/resources/email-templates/subscribe-confirmation-change.txt new file mode 100644 index 0000000..71102a1 --- /dev/null +++ b/src/main/resources/email-templates/subscribe-confirmation-change.txt @@ -0,0 +1,8 @@ +Thank you for subscribing. + +You will receive email alerts from the RIPE NCC Resource +Certification (RPKI) service for every ROA change for your CA. + +You are able to fix and ignore reported issues, change your alert +settings, or unsubscribe by visiting [[${rpkiDashboardUri}]] or +directly using [(${unsubscribeUri})]. diff --git a/src/main/resources/email-templates/subscribe-confirmation-daily.txt b/src/main/resources/email-templates/subscribe-confirmation-daily.txt index 135e932..3850f98 100644 --- a/src/main/resources/email-templates/subscribe-confirmation-daily.txt +++ b/src/main/resources/email-templates/subscribe-confirmation-daily.txt @@ -1,7 +1,7 @@ Thank you for subscribing. Once every 24 hours, you will receive email alerts from the RIPE NCC -Resource Certification (RPKI) service. +Resource Certification (RPKI) service. [(${roaChangeSubscription})] You are able to fix and ignore reported issues, change your alert settings, or unsubscribe by visiting [[${rpkiDashboardUri}]] or diff --git a/src/main/resources/email-templates/subscribe-confirmation-weekly.txt b/src/main/resources/email-templates/subscribe-confirmation-weekly.txt index e63cfb3..e005d8b 100644 --- a/src/main/resources/email-templates/subscribe-confirmation-weekly.txt +++ b/src/main/resources/email-templates/subscribe-confirmation-weekly.txt @@ -1,7 +1,7 @@ Thank you for subscribing. Every week (on Mondays), you will receive email alerts from the RIPE NCC -Resource Certification (RPKI) service. +Resource Certification (RPKI) service. [(${roaChangeSubscription})] You are able to fix and ignore reported issues, change your alert settings, or unsubscribe by visiting [[${rpkiDashboardUri}]] or diff --git a/src/main/resources/email-templates/unsubscribe-confirmation-change.txt b/src/main/resources/email-templates/unsubscribe-confirmation-change.txt new file mode 100644 index 0000000..22e1011 --- /dev/null +++ b/src/main/resources/email-templates/unsubscribe-confirmation-change.txt @@ -0,0 +1,3 @@ +You have been unsubscribed from Resource Certification (RPKI) ROA changes alerts. + +Please visit [[${rpkiDashboardUri}]] to change your alert settings. diff --git a/src/main/resources/email-templates/unsubscribe-confirmation.txt b/src/main/resources/email-templates/unsubscribe-confirmation.txt index f1946d2..b8b8e92 100644 --- a/src/main/resources/email-templates/unsubscribe-confirmation.txt +++ b/src/main/resources/email-templates/unsubscribe-confirmation.txt @@ -1,4 +1,3 @@ -You have been unsubscribed from Resource Certification (RPKI) alerts. +You have been unsubscribed from Resource Certification (RPKI) alerts. [(${roaChangeSubscription})] -Please visit [[${rpkiDashboardUri}]] to change your alert -settings. +Please visit [[${rpkiDashboardUri}]] to change your alert settings. diff --git a/src/test/java/net/ripe/rpki/application/impl/CommandAuditServiceBeanTest.java b/src/test/java/net/ripe/rpki/application/impl/CommandAuditServiceBeanTest.java index 127a14b..b640263 100644 --- a/src/test/java/net/ripe/rpki/application/impl/CommandAuditServiceBeanTest.java +++ b/src/test/java/net/ripe/rpki/application/impl/CommandAuditServiceBeanTest.java @@ -96,7 +96,7 @@ public void should_extract_emails_from_subscribe_command_summary() { String email = "user_some.email+ripe.net@gmail.com"; recordCommand(new SubscribeToRoaAlertCommand(ca.getVersionedId(), email, List.of(RouteValidityState.INVALID_ASN)), "some event"); - recordCommand(new UnsubscribeFromRoaAlertCommand(ca.getVersionedId(), email), "some event 2"); + recordCommand(new UnsubscribeFromRoaAlertCommand(ca.getVersionedId(), email, false), "some event 2"); entityManager.flush(); Map emailMentions = subject.findMentionsInSummary(email); diff --git a/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceStatisticsTest.java b/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceStatisticsTest.java index 741a998..e26d2ee 100644 --- a/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceStatisticsTest.java +++ b/src/test/java/net/ripe/rpki/core/read/services/ca/CertificateAuthorityViewServiceStatisticsTest.java @@ -24,7 +24,6 @@ import javax.security.auth.x500.X500Principal; import java.security.SecureRandom; -import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -32,7 +31,7 @@ import java.util.stream.IntStream; import static net.ripe.ipresource.ImmutableResourceSet.parse; -import static org.assertj.core.api.Assertions.as; +import static net.ripe.rpki.server.api.security.RunAsUser.ADMIN; import static org.assertj.core.api.Assertions.assertThat; @Transactional @@ -138,7 +137,8 @@ public void testGetCaStatEvents() { commandService.execute(new UpdateRoaConfigurationCommand( child.getVersionedId(), Optional.empty(), List.of(), - ALL_ROA_CONFIGURATIONS.stream().map(ca -> ca.toData()).collect(Collectors.toList())) + ALL_ROA_CONFIGURATIONS.stream().map(ca -> ca.toData()).toList(), + ADMIN.getCertificationUserId()) ); assertThat(subject.getCaStatEvents()) @@ -151,7 +151,8 @@ public void testGetCaStatEvents() { commandService.execute(new UpdateRoaConfigurationCommand( child.getVersionedId(), Optional.empty(), ALL_ROA_CONFIGURATIONS.stream().map(ca -> ca.toData()).collect(Collectors.toList()), - List.of()) + List.of(), + ADMIN.getCertificationUserId()) ); assertThat(subject.getCaStatEvents()) .asInstanceOf(InstanceOfAssertFactories.list(CaStatRoaEvent.class)) diff --git a/src/test/java/net/ripe/rpki/domain/CertificationDomainTestCase.java b/src/test/java/net/ripe/rpki/domain/CertificationDomainTestCase.java index 674887a..80d1506 100644 --- a/src/test/java/net/ripe/rpki/domain/CertificationDomainTestCase.java +++ b/src/test/java/net/ripe/rpki/domain/CertificationDomainTestCase.java @@ -163,15 +163,6 @@ protected T withTx(Supplier c) { return transactionTemplate.execute(transactionStatus -> c.get()); } - protected CertificateAuthority createCaIfDoesntExist(CertificateAuthority ca) { - final CertificateAuthority existing = certificateAuthorityRepository.find(ca.getId()); - if (existing == null) { - certificateAuthorityRepository.add(ca); - return certificateAuthorityRepository.find(ca.getId()); - } - return existing; - } - protected CommandStatus execute(CertificateAuthorityCommand command) { try { return commandService.execute(command); diff --git a/src/test/java/net/ripe/rpki/domain/TestObjects.java b/src/test/java/net/ripe/rpki/domain/TestObjects.java index 190e828..87a0ce6 100644 --- a/src/test/java/net/ripe/rpki/domain/TestObjects.java +++ b/src/test/java/net/ripe/rpki/domain/TestObjects.java @@ -182,14 +182,20 @@ public static ProductionCertificateAuthority createInitialisedProdCaWithRipeReso return createInitialisedProdCaWithRipeResources(new InMemoryCertificateAuthorityRepository(), new InMemoryResourceCertificateRepository(), certificationConfiguration); } - public static ProductionCertificateAuthority createInitialisedProdCaWithRipeResources(CertificateAuthorityRepository certificateAuthorityRepository, ResourceCertificateRepository resourceCertificateRepository, RepositoryConfiguration certificationConfiguration) { + public static ProductionCertificateAuthority createInitialisedProdCaWithRipeResources(CertificateAuthorityRepository certificateAuthorityRepository, + ResourceCertificateRepository resourceCertificateRepository, + RepositoryConfiguration certificationConfiguration) { ProductionCertificateAuthority ca = new ProductionCertificateAuthority(CA_ID, PRODUCTION_CA_NAME, UUID.randomUUID(), null); createInitialisedKeyPair(certificateAuthorityRepository, resourceCertificateRepository, certificationConfiguration, ca, "TEST-KEY"); Validate.isTrue(ca.hasCurrentKeyPair()); return ca; } - static KeyPairEntity createInitialisedKeyPair(CertificateAuthorityRepository certificateAuthorityRepository, ResourceCertificateRepository resourceCertificateRepository, RepositoryConfiguration certificationConfiguration, ProductionCertificateAuthority ca, String name) { + static KeyPairEntity createInitialisedKeyPair(CertificateAuthorityRepository certificateAuthorityRepository, + ResourceCertificateRepository resourceCertificateRepository, + RepositoryConfiguration certificationConfiguration, + ProductionCertificateAuthority ca, + String name) { KeyPairEntity kp = createTestKeyPair(name); ca.addKeyPair(kp); // Implicitly persists the keypair before it is used in a outgoing resource certificate @@ -198,7 +204,9 @@ static KeyPairEntity createInitialisedKeyPair(CertificateAuthorityRepository cer return kp; } - static void issueSelfSignedCertificates(ResourceCertificateRepository resourceCertificateRepository, RepositoryConfiguration certificationConfiguration, ProductionCertificateAuthority ca) { + static void issueSelfSignedCertificates(ResourceCertificateRepository resourceCertificateRepository, + RepositoryConfiguration certificationConfiguration, + ProductionCertificateAuthority ca) { for (KeyPairEntity kp : ca.getKeyPairs()) { if (kp.findCurrentIncomingCertificate().isEmpty()) { ResourceCertificateInformationAccessStrategy ias = new ResourceCertificateInformationAccessStrategyBean(); diff --git a/src/test/java/net/ripe/rpki/rest/service/AlertServiceTest.java b/src/test/java/net/ripe/rpki/rest/service/AlertServiceTest.java index 658bf36..cca0731 100644 --- a/src/test/java/net/ripe/rpki/rest/service/AlertServiceTest.java +++ b/src/test/java/net/ripe/rpki/rest/service/AlertServiceTest.java @@ -7,10 +7,7 @@ import net.ripe.rpki.commons.util.VersionedId; import net.ripe.rpki.commons.validation.roa.RouteValidityState; import net.ripe.rpki.domain.alerts.RoaAlertFrequency; -import net.ripe.rpki.server.api.commands.CertificateAuthorityCommand; -import net.ripe.rpki.server.api.commands.SubscribeToRoaAlertCommand; -import net.ripe.rpki.server.api.commands.UnsubscribeFromRoaAlertCommand; -import net.ripe.rpki.server.api.commands.UpdateRoaAlertIgnoredAnnouncedRoutesCommand; +import net.ripe.rpki.server.api.commands.*; import net.ripe.rpki.server.api.dto.CertificateAuthorityData; import net.ripe.rpki.server.api.dto.HostedCertificateAuthorityData; import net.ripe.rpki.server.api.dto.RoaAlertConfigurationData; @@ -39,8 +36,7 @@ import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static net.ripe.rpki.rest.service.AbstractCaRestService.API_URL_PREFIX; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -78,14 +74,28 @@ public void init() { } @Test - public void shouldGetExistingAlerts() throws Exception { + public void shouldNotCrashWithEmptySubscription() throws Exception { + CertificateAuthorityData caData = mock(CertificateAuthorityData.class); + when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)).thenReturn( + new RoaAlertConfigurationData(caData, null)); + + mockMvc.perform(Rest.get(API_URL_PREFIX + "/123/alerts")) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)) + .andExpect(jsonPath("$.frequency").value("DAILY")) + .andExpect(jsonPath("$.notifyOnRoaChanges").value("false")); + } + @Test + public void shouldGetExistingAlerts() throws Exception { CertificateAuthorityData caData = mock(CertificateAuthorityData.class); RoaAlertSubscriptionData roaSubscriptionData = new RoaAlertSubscriptionData( Arrays.asList("festeban@ripe.net", "bad@ripe.net"), - Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY); + Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), + RoaAlertFrequency.WEEKLY, true); - when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)).thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); + when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)).thenReturn( + new RoaAlertConfigurationData(caData, roaSubscriptionData)); mockMvc.perform(Rest.get(API_URL_PREFIX + "/123/alerts")) .andExpect(status().isOk()) @@ -94,7 +104,8 @@ public void shouldGetExistingAlerts() throws Exception { .andExpect(jsonPath("$.routeValidityStates[1]").value("UNKNOWN")) .andExpect(jsonPath("$.emails[0]").value("festeban@ripe.net")) .andExpect(jsonPath("$.emails[1]").value("bad@ripe.net")) - .andExpect(jsonPath("$.frequency").value("WEEKLY")); + .andExpect(jsonPath("$.frequency").value("WEEKLY")) + .andExpect(jsonPath("$.notifyOnRoaChanges").value("true")); } @Test @@ -105,20 +116,24 @@ public void shouldSubscribeToAlerts() throws Exception { CertificateAuthorityData caData = mock(CertificateAuthorityData.class); RoaAlertSubscriptionData roaSubscriptionData = new RoaAlertSubscriptionData( Arrays.asList("festeban@ripe.net", "bad@ripe.net"), - Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), RoaAlertFrequency.DAILY); - when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)).thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); + Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), + RoaAlertFrequency.DAILY, true); + + when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)) + .thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); ArgumentCaptor commandArgument = ArgumentCaptor.forClass(CertificateAuthorityCommand.class); mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/alerts", - "{\"routeValidityStates\" : [\"INVALID_LENGTH\"], \"emails\" : [\"bad1@ripe.net\"]}")) + "{\"routeValidityStates\" : [\"INVALID_LENGTH\"], " + + "\"emails\" : [\"bad1@ripe.net\"]," + + "\"frequency\" : \"WEEKLY\"}")) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)); verify(commandService, times(3)).execute(commandArgument.capture()); List commands = commandArgument.getAllValues(); - UnsubscribeFromRoaAlertCommand unsubscribe1 = (UnsubscribeFromRoaAlertCommand) commands.get(0); UnsubscribeFromRoaAlertCommand unsubscribe2 = (UnsubscribeFromRoaAlertCommand) commands.get(1); SubscribeToRoaAlertCommand subscribe = (SubscribeToRoaAlertCommand) commands.get(2); @@ -134,19 +149,34 @@ public void shouldSubscribeToAlerts() throws Exception { Sets.newHashSet(unsubscribe1.getEmail(), unsubscribe2.getEmail())); } + @Test + public void shouldRejectNoFrequency() throws Exception { + mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/alerts", + "{\"routeValidityStates\" : [\"INVALID_LENGTH\"], " + + "\"emails\" : [\"bad1@ripe.net\"]}")) + .andExpect(status().isBadRequest()) + .andExpect(content().string("{\"error\":\"No valid subscription frequency provided\"}")); + } + @Test public void shouldSubscribeToAlertsWhenOnlyValidityStatusChanges() throws Exception { CertificateAuthorityData caData = mock(CertificateAuthorityData.class); RoaAlertSubscriptionData roaSubscriptionData = new RoaAlertSubscriptionData( Collections.singletonList("bad@ripe.net"), - Collections.singletonList(RouteValidityState.INVALID_ASN), RoaAlertFrequency.DAILY); - when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)).thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); + Collections.singletonList(RouteValidityState.INVALID_ASN), + RoaAlertFrequency.DAILY, true); + + when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)).thenReturn( + new RoaAlertConfigurationData(caData, roaSubscriptionData)); ArgumentCaptor commandArgument = ArgumentCaptor.forClass(CertificateAuthorityCommand.class); mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/alerts", - "{\"routeValidityStates\" : [\"INVALID_LENGTH\", \"INVALID_ASN\"], \"emails\" : [\"bad@ripe.net\"], \"frequency\":\"WEEKLY\"}")) + "{\"routeValidityStates\" : [\"INVALID_LENGTH\", \"INVALID_ASN\"], " + + "\"emails\" : [\"bad@ripe.net\"], " + + "\"notifyOnRoaChanges\" : \"false\", " + + "\"frequency\" : \"WEEKLY\"}")) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)); @@ -156,6 +186,7 @@ public void shouldSubscribeToAlertsWhenOnlyValidityStatusChanges() throws Except SubscribeToRoaAlertCommand subscribe = (SubscribeToRoaAlertCommand) commands.get(0); assertEquals("bad@ripe.net", subscribe.getEmail()); assertEquals(RoaAlertFrequency.WEEKLY, subscribe.getFrequency()); + assertFalse(subscribe.isNotifyOnRoaChanges()); assertEquals( Sets.newHashSet(RouteValidityState.INVALID_LENGTH, RouteValidityState.INVALID_ASN), @@ -168,14 +199,19 @@ public void subscribeOnlyAdditionalEmailsWhenValidityAndFrequencyUnchanged() thr CertificateAuthorityData caData = mock(CertificateAuthorityData.class); RoaAlertSubscriptionData roaSubscriptionData = new RoaAlertSubscriptionData( Arrays.asList("badweekly@ripe.net"), - Collections.singletonList(RouteValidityState.INVALID_ASN), RoaAlertFrequency.WEEKLY); - when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)).thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); + Collections.singletonList(RouteValidityState.INVALID_ASN), + RoaAlertFrequency.WEEKLY, false); + + when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)) + .thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); ArgumentCaptor commandArgument = ArgumentCaptor.forClass(CertificateAuthorityCommand.class); mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/alerts", - "{\"routeValidityStates\" : [\"INVALID_ASN\"], \"emails\" : [\"badweekly@ripe.net\",\"boyweekly@ripe.net\"], " + - "\"frequency\":\"WEEKLY\"}")) + "{\"routeValidityStates\" : [\"INVALID_ASN\"], " + + "\"emails\" : [\"badweekly@ripe.net\",\"boyweekly@ripe.net\"], " + + "\"notifyOnRoaChanges\" : \"true\", " + + "\"frequency\" : \"WEEKLY\"}")) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)); @@ -186,6 +222,7 @@ public void subscribeOnlyAdditionalEmailsWhenValidityAndFrequencyUnchanged() thr SubscribeToRoaAlertCommand subscribe = (SubscribeToRoaAlertCommand) commands.get(0); assertEquals("boyweekly@ripe.net", subscribe.getEmail()); assertEquals(RoaAlertFrequency.WEEKLY, subscribe.getFrequency()); + assertTrue(subscribe.isNotifyOnRoaChanges()); assertEquals( Sets.newHashSet(RouteValidityState.INVALID_ASN), @@ -198,14 +235,19 @@ public void resubscribeEveryoneWhenFrequencyChanges() throws Exception { CertificateAuthorityData caData = mock(CertificateAuthorityData.class); RoaAlertSubscriptionData roaSubscriptionData = new RoaAlertSubscriptionData( Arrays.asList("bad@ripe.net","boy@ripe.net"), - Collections.singletonList(RouteValidityState.INVALID_ASN), RoaAlertFrequency.WEEKLY); - when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)).thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); + Collections.singletonList(RouteValidityState.INVALID_ASN), + RoaAlertFrequency.WEEKLY, true); + + when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)) + .thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); ArgumentCaptor commandArgument = ArgumentCaptor.forClass(CertificateAuthorityCommand.class); mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/alerts", - "{\"routeValidityStates\" : [\"INVALID_ASN\"], \"emails\" : [\"bad@ripe.net\",\"boy@ripe.net\"], " + - "\"frequency\":\"DAILY\"}")) + "{\"routeValidityStates\" : [\"INVALID_ASN\"], " + + "\"emails\" : [\"bad@ripe.net\",\"boy@ripe.net\"], " + + "\"notifyOnRoaChanges\" : \"true\", " + + "\"frequency\":\"DAILY\"}")) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)); @@ -218,13 +260,12 @@ public void resubscribeEveryoneWhenFrequencyChanges() throws Exception { assertTrue(emails.contains("boy@ripe.net")); Set frequencies = commands.stream().map(c -> ((SubscribeToRoaAlertCommand) c).getFrequency().toString()).collect(Collectors.toSet()); - assertTrue(frequencies.size() == 1); + assertEquals(1, frequencies.size()); assertTrue(frequencies.contains("DAILY")); Set validities = commands.stream().map(c -> ((SubscribeToRoaAlertCommand) c).getRouteValidityStates().toString()).collect(Collectors.toSet()); - assertTrue(validities.size() == 1); + assertEquals(1, validities.size()); assertTrue(validities.contains("[INVALID_ASN]")); - } @Test @@ -234,13 +275,15 @@ public void shouldUnsubscribeAnyoneWhenStatusesAreEmpty() throws Exception { RoaAlertSubscriptionData roaSubscriptionData = new RoaAlertSubscriptionData( Arrays.asList("festeban@ripe.net", "bad@ripe.net"), Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), - RoaAlertFrequency.DAILY); + RoaAlertFrequency.DAILY, false); when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)).thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); ArgumentCaptor commandArgument = ArgumentCaptor.forClass(CertificateAuthorityCommand.class); mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/alerts", - "{\"routeValidityStates\" : [], \"emails\" : [\"bad1@ripe.net\"]}")) + "{\"routeValidityStates\" : [], " + + "\"emails\" : [\"bad1@ripe.net\"], " + + "\"frequency\":\"DAILY\"}")) .andExpect(status().isOk()) .andExpect(content().contentType(APPLICATION_JSON)); @@ -255,6 +298,102 @@ public void shouldUnsubscribeAnyoneWhenStatusesAreEmpty() throws Exception { Sets.newHashSet(unsubscribe1.getEmail(), unsubscribe2.getEmail())); } + @Test + public void shouldSubscribeOrUnsubscribeToRoaChanges() throws Exception { + + CertificateAuthorityData caData = mock(CertificateAuthorityData.class); + RoaAlertSubscriptionData roaSubscriptionData = new RoaAlertSubscriptionData( + Arrays.asList("festeban@ripe.net", "bad@ripe.net"), + Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), + RoaAlertFrequency.DAILY, true); + + when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)) + .thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); + + ArgumentCaptor commandArgument = ArgumentCaptor.forClass(CertificateAuthorityCommand.class); + + mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/alerts", + "{\"routeValidityStates\" : [], " + + "\"emails\" : [\"bad1@ripe.net\"]," + + "\"frequency\":\"DAILY\"}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)); + + verify(commandService, times(2)).execute(commandArgument.capture()); + List commands = commandArgument.getAllValues(); + + UnsubscribeFromRoaAlertCommand unsubscribe1 = (UnsubscribeFromRoaAlertCommand) commands.get(0); + UnsubscribeFromRoaAlertCommand unsubscribe2 = (UnsubscribeFromRoaAlertCommand) commands.get(1); + assertFalse(unsubscribe1.isNotifyOnRoaChanges()); + assertFalse(unsubscribe2.isNotifyOnRoaChanges()); + + assertEquals( + Sets.newHashSet("bad@ripe.net", "festeban@ripe.net"), + Sets.newHashSet(unsubscribe1.getEmail(), unsubscribe2.getEmail())); + } + + + @Test + public void shouldOnlySubscribeOrUnsubscribeToRoaChangesAndNothingElse() throws Exception { + + CertificateAuthorityData caData = mock(CertificateAuthorityData.class); + RoaAlertSubscriptionData roaSubscriptionData = new RoaAlertSubscriptionData( + Arrays.asList("festeban@ripe.net", "bad@ripe.net"), + Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), + RoaAlertFrequency.DAILY, true); + + when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)) + .thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); + + ArgumentCaptor commandArgument = ArgumentCaptor.forClass(CertificateAuthorityCommand.class); + + // Change nothing except for notifyOnRoaChanges + mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/alerts", + "{\"routeValidityStates\" : [\"INVALID_ASN\", \"UNKNOWN\"], " + + "\"notifyOnRoaChanges\" : \"false\", " + + "\"emails\" : [\"festeban@ripe.net\", \"bad@ripe.net\"], " + + "\"frequency\" : \"DAILY\"}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)); + + verify(commandService, times(1)).execute(commandArgument.capture()); + List commands = commandArgument.getAllValues(); + + UpdateRoaChangeAlertCommand roaChangeUpdate = (UpdateRoaChangeAlertCommand) commands.get(0); + assertFalse(roaChangeUpdate.isNotifyOnRoaChanges()); + } + + + @Test + public void shouldSubscribeToRoaChangesWhenSubscribing() throws Exception { + + CertificateAuthorityData caData = mock(CertificateAuthorityData.class); + RoaAlertSubscriptionData roaSubscriptionData = new RoaAlertSubscriptionData( + Arrays.asList("bad@ripe.net"), + Arrays.asList(RouteValidityState.INVALID_ASN), + RoaAlertFrequency.DAILY, true); + + when(roaAlertConfigurationViewService.findRoaAlertSubscription(CA_ID)) + .thenReturn(new RoaAlertConfigurationData(caData, roaSubscriptionData)); + + ArgumentCaptor commandArgument = ArgumentCaptor.forClass(CertificateAuthorityCommand.class); + + mockMvc.perform(Rest.post(API_URL_PREFIX + "/123/alerts", + "{\"routeValidityStates\" : [\"INVALID_ASN\"], " + + "\"emails\" : [\"bad@ripe.net\", \"festeban@ripe.net\"]," + + "\"frequency\":\"DAILY\"}")) + .andExpect(status().isOk()) + .andExpect(content().contentType(APPLICATION_JSON)); + + verify(commandService, times(1)).execute(commandArgument.capture()); + List commands = commandArgument.getAllValues(); + + SubscribeToRoaAlertCommand subscribe = (SubscribeToRoaAlertCommand) commands.get(0); + assertFalse(subscribe.isNotifyOnRoaChanges()); + + assertEquals("festeban@ripe.net", subscribe.getEmail()); + } + @Test public void shouldMuteAnnouncements() throws Exception { diff --git a/src/test/java/net/ripe/rpki/rest/service/AnnouncementServiceTest.java b/src/test/java/net/ripe/rpki/rest/service/AnnouncementServiceTest.java index 89dd81c..3f1fa5b 100644 --- a/src/test/java/net/ripe/rpki/rest/service/AnnouncementServiceTest.java +++ b/src/test/java/net/ripe/rpki/rest/service/AnnouncementServiceTest.java @@ -396,7 +396,7 @@ private RoaAlertConfigurationData getRoaAlertConfigurationData(Asn asn, IpRange final List routeValidityStates = Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN); final RoaAlertSubscriptionData subscription = new RoaAlertSubscriptionData("joe@example.com", - routeValidityStates, RoaAlertFrequency.DAILY); + routeValidityStates, RoaAlertFrequency.DAILY, false); return new RoaAlertConfigurationData(caData, subscription, ignoredAnnouncements); } diff --git a/src/test/java/net/ripe/rpki/server/api/commands/SubscribeToRoaAlertCommandTest.java b/src/test/java/net/ripe/rpki/server/api/commands/SubscribeToRoaAlertCommandTest.java index 51c7359..b12b192 100644 --- a/src/test/java/net/ripe/rpki/server/api/commands/SubscribeToRoaAlertCommandTest.java +++ b/src/test/java/net/ripe/rpki/server/api/commands/SubscribeToRoaAlertCommandTest.java @@ -17,12 +17,24 @@ public class SubscribeToRoaAlertCommandTest { @Test public void shouldHaveDescriptiveLogEntryForInvalidOnly() { subject = new SubscribeToRoaAlertCommand(new VersionedId(1), "bob@example.net", Collections.singletonList(RouteValidityState.INVALID_ASN)); - assertEquals("Subscribed bob@example.net to daily ROA alerts for invalid announcements only.", subject.getCommandSummary()); + assertEquals("Subscribed bob@example.net to daily ROA alerts for invalid announcements.", subject.getCommandSummary()); } @Test - public void shouldHaveDescriptiveLogEntryForInvalidAndUnknown() { - subject = new SubscribeToRoaAlertCommand(new VersionedId(1), "bob@example.net", Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY); - assertEquals("Subscribed bob@example.net to weekly ROA alerts for invalid and unknown announcements.", subject.getCommandSummary()); + public void shouldHaveDescriptiveLogEntryForInvalidAndUnknownWithRoaChanges() { + subject = new SubscribeToRoaAlertCommand(new VersionedId(1), "bob@example.net", + Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), + RoaAlertFrequency.WEEKLY, true); + assertEquals("Subscribed bob@example.net to weekly ROA alerts for invalid and unknown announcements and ROA changes.", + subject.getCommandSummary()); + } + + @Test + public void shouldHaveDescriptiveLogEntryForInvalidAndUnknownNoRoaChanges() { + subject = new SubscribeToRoaAlertCommand(new VersionedId(1), "bob@example.net", + Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.UNKNOWN), + RoaAlertFrequency.WEEKLY, false); + assertEquals("Subscribed bob@example.net to weekly ROA alerts for invalid and unknown announcements.", + subject.getCommandSummary()); } } diff --git a/src/test/java/net/ripe/rpki/server/api/commands/UnsubscribeFromRoaAlertCommandTest.java b/src/test/java/net/ripe/rpki/server/api/commands/UnsubscribeFromRoaAlertCommandTest.java index c3cc845..2bee7a1 100644 --- a/src/test/java/net/ripe/rpki/server/api/commands/UnsubscribeFromRoaAlertCommandTest.java +++ b/src/test/java/net/ripe/rpki/server/api/commands/UnsubscribeFromRoaAlertCommandTest.java @@ -1,22 +1,18 @@ package net.ripe.rpki.server.api.commands; import net.ripe.rpki.commons.util.VersionedId; -import org.junit.Before; import org.junit.Test; +import java.util.function.Function; + import static org.junit.Assert.assertEquals; public class UnsubscribeFromRoaAlertCommandTest { - private UnsubscribeFromRoaAlertCommand subject; - - @Before - public void setUp() { - subject = new UnsubscribeFromRoaAlertCommand(new VersionedId(1), "bob@example.net"); - } - @Test public void shouldHaveDescriptiveLogEntry() { - assertEquals("Unsubscribed bob@example.net from ROA alerts.", subject.getCommandSummary()); + Function makeCommand = notify -> new UnsubscribeFromRoaAlertCommand(new VersionedId(1), "bob@example.net", notify); + assertEquals("Unsubscribed bob@example.net from ROA alerts.", makeCommand.apply(false).getCommandSummary()); + assertEquals("Unsubscribed bob@example.net from ROA alerts and ROA changes.", makeCommand.apply(true).getCommandSummary()); } } diff --git a/src/test/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommandTest.java b/src/test/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommandTest.java index 8a6bdc4..8de92ba 100644 --- a/src/test/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommandTest.java +++ b/src/test/java/net/ripe/rpki/server/api/commands/UpdateRoaConfigurationCommandTest.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Optional; +import static net.ripe.rpki.server.api.security.RunAsUser.ADMIN; import static org.junit.Assert.assertEquals; public class UpdateRoaConfigurationCommandTest { @@ -20,8 +21,12 @@ public class UpdateRoaConfigurationCommandTest { @Before public void setUp() { - List added = Arrays.asList(new RoaConfigurationPrefixData(Asn.parse("123"), IpRange.parse("10.64.0.0/12"), 24), new RoaConfigurationPrefixData(Asn.parse("123"), IpRange.parse("10.32.0.0/12"), null)); - subject = new UpdateRoaConfigurationCommand(new VersionedId(1), Optional.empty(), added, Collections.emptyList()); + List added = Arrays.asList( + new RoaConfigurationPrefixData(Asn.parse("123"), IpRange.parse("10.64.0.0/12"), 24), + new RoaConfigurationPrefixData(Asn.parse("123"), IpRange.parse("10.32.0.0/12"), null)); + + subject = new UpdateRoaConfigurationCommand( + new VersionedId(1), Optional.empty(), added, Collections.emptyList(), ADMIN.getCertificationUserId()); } @Test diff --git a/src/test/java/net/ripe/rpki/services/impl/background/RoaAlertBackgroundServiceDailyBeanTest.java b/src/test/java/net/ripe/rpki/services/impl/background/RoaAlertBackgroundServiceDailyBeanTest.java index d3bac33..3322471 100644 --- a/src/test/java/net/ripe/rpki/services/impl/background/RoaAlertBackgroundServiceDailyBeanTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/background/RoaAlertBackgroundServiceDailyBeanTest.java @@ -39,12 +39,12 @@ public class RoaAlertBackgroundServiceDailyBeanTest { ImmutableResourceSet.ALL_PRIVATE_USE_RESOURCES, Collections.emptyList()); public static final RoaAlertConfigurationData ALERT_SUBSCRIPTION_DATA = new RoaAlertConfigurationData(CA_DATA, - new RoaAlertSubscriptionData(List.of("joeok@example.com"), Arrays.asList(RouteValidityState.INVALID_ASN, - RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN), RoaAlertFrequency.DAILY)); + new RoaAlertSubscriptionData("joeok@example.com", Arrays.asList(RouteValidityState.INVALID_ASN, + RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN), RoaAlertFrequency.DAILY, false)); private static final RoaAlertConfigurationData ALERT_SUBSCRIPTION_ERROR = new RoaAlertConfigurationData(CA_DATA, new RoaAlertSubscriptionData("errorjohn@example.com", Arrays.asList(RouteValidityState.INVALID_ASN, - RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN), RoaAlertFrequency.DAILY)); + RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN), RoaAlertFrequency.DAILY, true)); @Mock private ActiveNodeService activeNodeService; diff --git a/src/test/java/net/ripe/rpki/services/impl/background/RoaAlertBackgroundServiceWeeklyBeanTest.java b/src/test/java/net/ripe/rpki/services/impl/background/RoaAlertBackgroundServiceWeeklyBeanTest.java index e7cccf6..9f0eee3 100644 --- a/src/test/java/net/ripe/rpki/services/impl/background/RoaAlertBackgroundServiceWeeklyBeanTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/background/RoaAlertBackgroundServiceWeeklyBeanTest.java @@ -39,11 +39,11 @@ public class RoaAlertBackgroundServiceWeeklyBeanTest { private static final RoaAlertConfigurationData ALERT_SUBSCRIPTION_ERROR = new RoaAlertConfigurationData(CA_DATA, new RoaAlertSubscriptionData("errorjohn@example.com", Arrays.asList(RouteValidityState.INVALID_ASN, - RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY)); + RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY, false)); private static final RoaAlertConfigurationData ALERT_SUBSCRIPTION_WEEKLY = new RoaAlertConfigurationData(CA_DATA, new RoaAlertSubscriptionData("weeklyjoe@example.com", Arrays.asList(RouteValidityState.INVALID_ASN, - RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY)); + RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY, true)); @Mock private ActiveNodeService activeNodeService; @@ -56,7 +56,8 @@ public class RoaAlertBackgroundServiceWeeklyBeanTest { @Before public void setup() { - subject = new RoaAlertBackgroundServiceWeeklyBean(new BackgroundTaskRunner(activeNodeService, new SimpleMeterRegistry()), roaAlertConfigurationViewService, roaAlertChecker); + subject = new RoaAlertBackgroundServiceWeeklyBean(new BackgroundTaskRunner( + activeNodeService, new SimpleMeterRegistry()), roaAlertConfigurationViewService, roaAlertChecker); } @Test diff --git a/src/test/java/net/ripe/rpki/services/impl/background/RoaNotificationServiceTest.java b/src/test/java/net/ripe/rpki/services/impl/background/RoaNotificationServiceTest.java new file mode 100644 index 0000000..58e1683 --- /dev/null +++ b/src/test/java/net/ripe/rpki/services/impl/background/RoaNotificationServiceTest.java @@ -0,0 +1,113 @@ +package net.ripe.rpki.services.impl.background; + +import jakarta.transaction.Transactional; +import net.ripe.ipresource.Asn; +import net.ripe.ipresource.IpRange; +import net.ripe.rpki.commons.validation.roa.RouteValidityState; +import net.ripe.rpki.domain.CertificateAuthorityRepository; +import net.ripe.rpki.domain.CertificationDomainTestCase; +import net.ripe.rpki.domain.HostedCertificateAuthority; +import net.ripe.rpki.domain.alerts.RoaAlertConfiguration; +import net.ripe.rpki.domain.alerts.RoaAlertConfigurationRepository; +import net.ripe.rpki.domain.alerts.RoaAlertFrequency; +import net.ripe.rpki.domain.roa.RoaConfigurationPrefix; +import net.ripe.rpki.server.api.ports.InternalNamePresenter; +import net.ripe.rpki.server.api.security.CertificationUserId; +import net.ripe.rpki.services.impl.email.EmailSender; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; + +import javax.security.auth.x500.X500Principal; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.when; + +@Transactional +public class RoaNotificationServiceTest extends CertificationDomainTestCase { + + private static final long HOSTED_CA_ID = 454L; + private static final X500Principal CHILD_CA_NAME = new X500Principal("CN=child"); + + private static final String ADMIN_RIPE_NET = "admin@ripe.net"; + private static final CertificationUserId USER_ID = new CertificationUserId(UUID.randomUUID()); + + @Autowired + private EmailSender emailSender; + + @Autowired + private RoaAlertConfigurationRepository roaAlertConfigurationRepository; + + @Autowired + private CertificateAuthorityRepository certificateAuthorityRepository; + + @MockBean + private InternalNamePresenter internalNamePresenter; + + private RoaNotificationService roaNotificationService; + + private HostedCertificateAuthority childCa; + + @Before + @Override + public void setupTest() { + clearDatabase(); + var parent = createInitialisedProdCaWithRipeResources(); + certificateAuthorityRepository.add(parent); + childCa = new HostedCertificateAuthority(HOSTED_CA_ID, CHILD_CA_NAME, UUID.randomUUID(), parent); + certificateAuthorityRepository.add(childCa); + when(internalNamePresenter.humanizeCaName(CHILD_CA_NAME)).thenReturn("Better name"); + when(internalNamePresenter.humanizeUserPrincipal(USER_ID.getId().toString())).thenReturn(ADMIN_RIPE_NET); + roaNotificationService = new RoaNotificationService(roaAlertConfigurationRepository, emailSender, internalNamePresenter); + } + + @Test + public void testNotifyNobodyBasedOnFlag() { + RoaAlertConfiguration r = new RoaAlertConfiguration(childCa, "bad@ripe.net", List.of(RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY); + r.setNotifyOnRoaChanges(false); + roaAlertConfigurationRepository.add(r); + var messages = roaNotificationService.notifyAboutRoaChanges(childCa, USER_ID, Collections.emptyList(), Collections.emptyList()); + assertEquals(0, messages.size()); + } + + @Test + public void testNoNotifyIfNothingChanges() { + var email = "bad@ripe.net"; + RoaAlertConfiguration r = new RoaAlertConfiguration(childCa, email, List.of(RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY); + r.setNotifyOnRoaChanges(true); + roaAlertConfigurationRepository.add(r); + var messages = roaNotificationService.notifyAboutRoaChanges(childCa, USER_ID, Collections.emptyList(), Collections.emptyList()); + assertEquals(0, messages.size()); + } + + @Test + public void testNotifyRoas() { + var email = "bad@ripe.net"; + RoaAlertConfiguration r = new RoaAlertConfiguration(childCa, email, List.of(RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY); + r.setNotifyOnRoaChanges(true); + roaAlertConfigurationRepository.add(r); + + var now = Instant.now(); + var roa1 = new RoaConfigurationPrefix(Asn.parse("AS64396"), IpRange.parse("192.0.2.0/24"), null, now); + var roa2 = new RoaConfigurationPrefix(Asn.parse("AS64397"), IpRange.parse("198.51.100.0/24"), 32, now); + var roa3 = new RoaConfigurationPrefix(Asn.parse("AS123"), IpRange.parse("fd00:550:ffff:ffff:ffff:ffff:ffff:ffff/128"), 128, now); + + var messages = roaNotificationService.notifyAboutRoaChanges(childCa, USER_ID, List.of(roa1, roa3), List.of(roa2)); + assertEquals(1, messages.size()); + + var message = messages.iterator().next(); + assertTrue(message.body().contains("123 fd00:550:ffff:ffff:ffff:ffff:ffff:ffff/128 128 A")); + assertTrue(message.body().contains("64396 192.0.2.0/24 24 A")); + assertTrue(message.body().contains("64397 198.51.100.0/24 32 D")); + assertTrue(message.body().contains( + "This is an automated email to inform you that user admin@ripe.net made changes\n" + + "to one or more ROAs for your organisation Better name.")); + } + +} \ No newline at end of file diff --git a/src/test/java/net/ripe/rpki/services/impl/email/EmailSenderBeanTest.java b/src/test/java/net/ripe/rpki/services/impl/email/EmailSenderBeanTest.java index 1e71d30..e115d10 100644 --- a/src/test/java/net/ripe/rpki/services/impl/email/EmailSenderBeanTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/email/EmailSenderBeanTest.java @@ -99,7 +99,7 @@ private Map variablesFor(EmailSender.EmailTemplates template) { ); var configuration = new RoaAlertConfigurationData( ca, - new RoaAlertSubscriptionData("user@example.org", List.of(RouteValidityState.values()), RoaAlertFrequency.DAILY) + new RoaAlertSubscriptionData("user@example.org", List.of(RouteValidityState.values()), RoaAlertFrequency.DAILY, false) ); return Map.of( "humanizedCaName", RandomStringUtils.randomAlphabetic(12), diff --git a/src/test/java/net/ripe/rpki/services/impl/email/EmailTemplatesTest.java b/src/test/java/net/ripe/rpki/services/impl/email/EmailTemplatesTest.java index 5606885..a57116a 100644 --- a/src/test/java/net/ripe/rpki/services/impl/email/EmailTemplatesTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/email/EmailTemplatesTest.java @@ -18,6 +18,7 @@ public void setUp() { templateEngine = new TemplateEngine(); templateEngine.addTemplateResolver(EmailSenderBean.textTemplateResolver()); } + @Test public void testTemplateForSyntacticValdidity() { // Add required variables (i.e. those that are dereferenced) here diff --git a/src/test/java/net/ripe/rpki/services/impl/handlers/SubscribeToRoaAlertCommandHandlerTest.java b/src/test/java/net/ripe/rpki/services/impl/handlers/SubscribeToRoaAlertCommandHandlerTest.java index 322fa15..5c13fcb 100644 --- a/src/test/java/net/ripe/rpki/services/impl/handlers/SubscribeToRoaAlertCommandHandlerTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/handlers/SubscribeToRoaAlertCommandHandlerTest.java @@ -17,7 +17,10 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import java.util.*; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -45,6 +48,8 @@ public class SubscribeToRoaAlertCommandHandlerTest { private ArgumentCaptor alertCapture; + private ArgumentCaptor> parametersCapture; + private SubscribeToRoaAlertCommandHandler subject; @Before @@ -52,6 +57,7 @@ public void setUp() { certificateAuthority = TestObjects.createInitialisedProdCaWithRipeResources(); emailCapture = ArgumentCaptor.forClass(String.class); alertCapture = ArgumentCaptor.forClass(RoaAlertConfiguration.class); + parametersCapture = ArgumentCaptor.captor(); subject = new SubscribeToRoaAlertCommandHandler(certificateAuthorityRepository, repository, emailSender); } @@ -70,13 +76,68 @@ public void shouldCreateRoaAlertSubscriptionAndSendConfirmationEmail() { subject.handle(new SubscribeToRoaAlertCommand(TEST_VERSIONED_CA_ID, email, EnumSet.of(RouteValidityState.INVALID_ASN, RouteValidityState.INVALID_LENGTH, - RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY)); + RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY, true)); verify(repository).add(alertCapture.capture()); verify(emailSender).sendEmail(emailCapture.capture(), isA(String.class), - eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY), isA(Map.class), isA(String.class)); + eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY), + parametersCapture.capture(), + isA(String.class)); assertEquals(RoaAlertFrequency.WEEKLY, alertCapture.getValue().getFrequency()); + assertTrue(alertCapture.getValue().isNotifyOnRoaChanges()); + assertEquals(email, emailCapture.getValue()); + assertEquals("", parametersCapture.getValue().get("roaChangeSubscription")); + } + + @Test + public void shouldCreateRoaAlertSubscriptionAndSendConfirmationEmailWithROAChangesUnsubscribed() { + final String email = "joe@example.com"; + final String email2 = "festeban@ripe.net"; + when(certificateAuthorityRepository.findManagedCa(TEST_CA_ID)).thenReturn(certificateAuthority); + + RoaAlertConfiguration configuration = new RoaAlertConfiguration(certificateAuthority, email2, + EnumSet.of(RouteValidityState.INVALID_ASN), RoaAlertFrequency.DAILY); + configuration.setNotifyOnRoaChanges(true); + + when(repository.findByCertificateAuthorityIdOrNull(TEST_CA_ID)).thenReturn(configuration); + + subject.handle(new SubscribeToRoaAlertCommand(TEST_VERSIONED_CA_ID, email, + EnumSet.of(RouteValidityState.INVALID_ASN, RouteValidityState.INVALID_LENGTH, + RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY, false)); + + verify(emailSender).sendEmail(emailCapture.capture(), isA(String.class), + eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY), + parametersCapture.capture(), + isA(String.class)); + + assertEquals(email, emailCapture.getValue()); + assertEquals("", parametersCapture.getValue().get("roaChangeSubscription")); + } + + @Test + public void shouldCreateRoaAlertSubscriptionAndSendConfirmationEmailWithROAChangesSubscribed() { + final String email = "joe@example.com"; + final String email2 = "festeban@ripe.net"; + when(certificateAuthorityRepository.findManagedCa(TEST_CA_ID)).thenReturn(certificateAuthority); + + RoaAlertConfiguration configuration = new RoaAlertConfiguration(certificateAuthority, email2, + EnumSet.of(RouteValidityState.INVALID_ASN), RoaAlertFrequency.DAILY); + configuration.setNotifyOnRoaChanges(false); + + when(repository.findByCertificateAuthorityIdOrNull(TEST_CA_ID)).thenReturn(configuration); + + subject.handle(new SubscribeToRoaAlertCommand(TEST_VERSIONED_CA_ID, email, + EnumSet.of(RouteValidityState.INVALID_ASN, RouteValidityState.INVALID_LENGTH, + RouteValidityState.UNKNOWN), RoaAlertFrequency.WEEKLY, true)); + + verify(emailSender).sendEmail(emailCapture.capture(), isA(String.class), + eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY), + parametersCapture.capture(), + isA(String.class)); + assertEquals(email, emailCapture.getValue()); + assertEquals("Also you are subscribed to alerts about ROA changes.", + parametersCapture.getValue().get("roaChangeSubscription")); } @SuppressWarnings("unchecked") @@ -88,7 +149,6 @@ public void shouldUpdateRoaAlertSubscriptionAndNotSendConfirmationEmail() { RoaAlertConfiguration configuration = new RoaAlertConfiguration(certificateAuthority, email, oldValidityStates, RoaAlertFrequency.DAILY); -// when(certificateAuthorityRepository.findHostedCa(TEST_CA_ID)).thenReturn(certificateAuthority); when(repository.findByCertificateAuthorityIdOrNull(TEST_CA_ID)).thenReturn(configuration); subject.handle(new SubscribeToRoaAlertCommand(TEST_VERSIONED_CA_ID, email, newValidityStates)); @@ -108,7 +168,8 @@ public void shouldUpdateRoaAlertSubscriptionAndSendConfirmationEmails() { when(repository.findByCertificateAuthorityIdOrNull(TEST_CA_ID)).thenReturn(configuration); - subject.handle(new SubscribeToRoaAlertCommand(TEST_VERSIONED_CA_ID, newEmail, EnumSet.of(RouteValidityState.INVALID_ASN, RouteValidityState.INVALID_LENGTH))); + subject.handle(new SubscribeToRoaAlertCommand(TEST_VERSIONED_CA_ID, newEmail, + EnumSet.of(RouteValidityState.INVALID_ASN, RouteValidityState.INVALID_LENGTH))); verify(emailSender, times(1)).sendEmail(eq(newEmail), eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY.templateSubject), diff --git a/src/test/java/net/ripe/rpki/services/impl/handlers/UnsubscribeFromRoaAlertCommandHandlerTest.java b/src/test/java/net/ripe/rpki/services/impl/handlers/UnsubscribeFromRoaAlertCommandHandlerTest.java index 9480d59..9c12243 100644 --- a/src/test/java/net/ripe/rpki/services/impl/handlers/UnsubscribeFromRoaAlertCommandHandlerTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/handlers/UnsubscribeFromRoaAlertCommandHandlerTest.java @@ -68,7 +68,7 @@ public void shouldUpdateRoaSpecificationAndSendConfirmationEmail() { RouteValidityState.UNKNOWN), RoaAlertFrequency.DAILY); when(repository.findByCertificateAuthorityIdOrNull(TEST_CA_ID)).thenReturn(configuration); - subject.handle(new UnsubscribeFromRoaAlertCommand(TEST_VERSIONED_CA_ID, email)); + subject.handle(new UnsubscribeFromRoaAlertCommand(TEST_VERSIONED_CA_ID, email, false)); verify(emailSender).sendEmail(emailCapture.capture(), eq(EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE.templateSubject), diff --git a/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandlerTest.java b/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandlerTest.java index 4d71766..5d8001a 100644 --- a/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandlerTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/handlers/UpdateRoaConfigurationCommandHandlerTest.java @@ -14,6 +14,7 @@ import net.ripe.rpki.server.api.services.command.NotHolderOfResourcesException; import net.ripe.rpki.server.api.services.command.PrivateAsnsUsedException; import net.ripe.rpki.services.impl.background.RoaMetricsService; +import net.ripe.rpki.services.impl.background.RoaNotificationService; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -22,11 +23,10 @@ import java.util.Collections; import java.util.Optional; +import static net.ripe.rpki.server.api.security.RunAsUser.ADMIN; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.*; @Transactional @Commit // don't rollback, we want all constrains to be checked @@ -39,7 +39,6 @@ public class UpdateRoaConfigurationCommandHandlerTest extends CertificationDomai private static final IpRange PREFIX1 = IpRange.parse("10.0.0.0/8"); private static final IpRange PREFIX2 = IpRange.parse("172.16.0.0/12"); - private static final IpRange PREFIX3 = IpRange.parse("192.168.0.0/16"); private ManagedCertificateAuthority certificateAuthority; @@ -55,8 +54,9 @@ public void setUp() { clearDatabase(); certificateAuthority = createInitialisedProdCaWithRipeResources(); roaMetricsService = mock(RoaMetricsService.class); + var roaNotificationService = mock(RoaNotificationService.class); subject = new UpdateRoaConfigurationCommandHandler(certificateAuthorityRepository, - roaConfigurationRepository, PRIVATE_ASNS, roaMetricsService); + roaConfigurationRepository, PRIVATE_ASNS, roaMetricsService, roaNotificationService); } @Test @@ -66,7 +66,7 @@ public void should_add_new_additions() { certificateAuthority.getVersionedId(), Optional.of(configuration.convertToData().entityTag()), Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), - Collections.emptyList())); + Collections.emptyList(), ADMIN.getCertificationUserId())); var config = roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority); assertThat(config.getPrefixes()).hasSize(1); @@ -85,8 +85,8 @@ public void should_reject_if_etag_does_not_match_current_configuration() { certificateAuthority.getVersionedId(), Optional.of("bad-etag"), Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), - Collections.emptyList() - ); + Collections.emptyList(), + ADMIN.getCertificationUserId()); assertThatThrownBy(() -> subject.handle(command)).isInstanceOf(EntityTagDoesNotMatchException.class); } @@ -97,7 +97,8 @@ public void should_reject_new_additions_of_private_ASN() { certificateAuthority.getVersionedId(), Optional.empty(), Collections.singletonList(new RoaConfigurationPrefixData(PRIVATE_ASN, PREFIX1, null)), - Collections.emptyList())); + Collections.emptyList(), + ADMIN.getCertificationUserId())); verifyNoMoreInteractions(roaMetricsService); } @@ -107,8 +108,8 @@ public void should_reject_uncertified_prefixes() { certificateAuthority.getVersionedId(), Optional.empty(), Collections.singletonList(new RoaConfigurationPrefixData(ASN, IpRange.parse("1.0.0.0/8"), null)), - Collections.emptyList() - ); + Collections.emptyList(), + ADMIN.getCertificationUserId()); assertThatThrownBy(() -> subject.handle(command)).isInstanceOf(NotHolderOfResourcesException.class); } @@ -121,7 +122,7 @@ public void should_remove_deletions() { certificateAuthority.getVersionedId(), Optional.empty(), Collections.emptyList(), - Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)))); + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), ADMIN.getCertificationUserId())); var config = roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority); assertThat(config.getPrefixes()).isEmpty(); @@ -138,7 +139,7 @@ public void should_notify_roa_entity_service_on_configuration_change() { certificateAuthority.getVersionedId(), Optional.empty(), Collections.emptyList(), - Collections.emptyList())); + Collections.emptyList(), ADMIN.getCertificationUserId())); assertThat(certificateAuthority.isConfigurationCheckNeeded()).isTrue(); } @@ -150,13 +151,13 @@ public void should_replace_roa_prefix() { certificateAuthority.getVersionedId(), Optional.of(configuration.convertToData().entityTag()), Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), - Collections.emptyList())); + Collections.emptyList(), ADMIN.getCertificationUserId())); subject.handle(new UpdateRoaConfigurationCommand( certificateAuthority.getVersionedId(), Optional.of(roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority).convertToData().entityTag()), Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX2, null)), - Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)))); + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), ADMIN.getCertificationUserId())); var config = roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority); assertThat(config.getPrefixes()).hasSize(1); @@ -174,13 +175,13 @@ public void should_replace_roa_max_len() { certificateAuthority.getVersionedId(), Optional.of(configuration.convertToData().entityTag()), Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), - Collections.emptyList())); + Collections.emptyList(), ADMIN.getCertificationUserId())); subject.handle(new UpdateRoaConfigurationCommand( certificateAuthority.getVersionedId(), Optional.of(roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority).convertToData().entityTag()), Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, 17)), - Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)))); + Collections.singletonList(new RoaConfigurationPrefixData(ASN, PREFIX1, null)), ADMIN.getCertificationUserId())); var config = roaConfigurationRepository.getOrCreateByCertificateAuthority(certificateAuthority); assertThat(config.getPrefixes()).hasSize(1); diff --git a/src/test/java/net/ripe/rpki/services/impl/jpa/JpaResourceCertificateRepositoryTest.java b/src/test/java/net/ripe/rpki/services/impl/jpa/JpaResourceCertificateRepositoryTest.java index 54f33af..ae4596f 100644 --- a/src/test/java/net/ripe/rpki/services/impl/jpa/JpaResourceCertificateRepositoryTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/jpa/JpaResourceCertificateRepositoryTest.java @@ -1,5 +1,7 @@ package net.ripe.rpki.services.impl.jpa; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; import net.ripe.ipresource.Asn; import net.ripe.ipresource.ImmutableResourceSet; import net.ripe.ipresource.IpRange; @@ -17,9 +19,7 @@ import org.junit.Before; import org.junit.Test; -import jakarta.inject.Inject; import javax.security.auth.x500.X500Principal; -import jakarta.transaction.Transactional; import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -27,6 +27,7 @@ import static net.ripe.rpki.commons.crypto.util.KeyPairFactoryTest.TEST_KEY_PAIR; import static net.ripe.rpki.domain.TestObjects.PRODUCTION_CA_NAME; import static net.ripe.rpki.domain.TestObjects.PRODUCTION_CA_RESOURCES; +import static net.ripe.rpki.server.api.security.RunAsUser.ADMIN; import static org.assertj.core.api.Assertions.assertThat; public class JpaResourceCertificateRepositoryTest extends CertificationDomainTestCase { @@ -63,7 +64,7 @@ public void outgoing_resource_certificate_should_change_to_expired_after_not_val ca.getVersionedId(), Optional.empty(), Collections.singleton(new RoaConfigurationPrefixData(Asn.parse("AS3333"), IpRange.parse("10.0.0.0/8"), null)), - Collections.emptyList())); + Collections.emptyList(), ADMIN.getCertificationUserId())); commandService.execute(new IssueUpdatedManifestAndCrlCommand(ca.getVersionedId())); // CA certificate, EE certificate for ROA, EE certificate for manifest