From 5bcbdb417e945a80fcef8ad99a56fc685826985b Mon Sep 17 00:00:00 2001 From: RPKI Team at RIPE NCC Date: Thu, 16 May 2024 10:01:25 +0000 Subject: [PATCH] RIPE NCC has merged 11a030be0 * Update node Docker tag to v22 [42ee05bee] * Update dependency org.eclipse.jgit:org.eclipse.jgit to v6 [447b855d2] * Cleanup left-over 'authorization.admin.role' in profiles [da3e72785] * Update dependency org.sonarqube:org.sonarqube.gradle.plugin to v5 [1adb4a140] * Update dependency io.sentry:sentry-bom to v7 [d3a369f76] * Update dependency commons-codec:commons-codec to v1.17.0 [df69caf44] * Refactor Mac initialisation [f29622783] * Update dependency com.gorylenko.gradle-git-properties:com.gorylenko.gradle-git-properties.gradle.plugin to v2.4.2 [6f8670965] * More Sonarqube quirks [38a6f0e38] * Sonarqube issues [b69e7e8bc] * Better error messages [50baec128] * Formatting [b8f2f3aa4] * Make tokens URL-safe [8315ccd6b] * Use proper hmac, higher entropy secrets [f8063063c] * Include a unique id from the CA into the email unsubscribe token [76d67e240] * Use global secret instead of per-configuration token [d790f1417] * Include URL in RRDP duration tracker [d379c9d3c] * DBProvider 1.6 [c1344ea79] * Update dependency org.wiremock:wiremock-jetty12 to v3.5.4 [0fb6ccc99] * Fix compilation [89122afc0] * Fix migration numbers [11d0c758b] * Add authentication-first URL [ea5f7806e] * Revert error processing in the method [32f355f88] * Syntax fix [c1dd5814d] * Fix response building [c1d4c0959] * Error handling [a37cf9900] * Introduce separate alertUnsubscribeUri property [c7e8a1cbf] * Use PathVariables like all the other code does [de7f52739] * Fix NULL value [05db29b46] * Fix tests [a9b551ad1] * Delete unused method [2bd5eb3ca] * Improve email template tests [e4aa937d7] * Extend templates with unsubscribe URL [ebff2c8ec] * Pass around unsubscribeToken [9885d1432] * Cleanup smells [8be42f963] * Cleanup imports [cac48e104] * Refactor [5378dd72c] * Add unsubscribeToken [cd583ee56] * Formatting [5c9adc0d3] * Add unsubscribeToken field [58278c3c9] * Use commands to unsubscribe [005824705] * Fix broken tests [185ac4e72] * Add unsubscribe API end-point [0c89379ad] --- .gitlab-ci.yml | 2 +- README.md | 2 +- build.gradle | 6 +- buildSrc/build.gradle | 6 +- .../rpki-ripe-ncc.build-conventions.gradle | 7 +- hsm/build.gradle | 4 +- .../domain/alerts/RoaAlertConfiguration.java | 1 + .../RoaAlertConfigurationRepository.java | 4 + .../server/ExternalPublishingServer.java | 3 +- .../ripe/rpki/rest/service/EmailService.java | 78 ++++++++++++ .../api/dto/RoaAlertSubscriptionData.java | 25 +--- .../rpki/services/impl/EmailSenderBean.java | 84 ------------- .../rpki/services/impl/RoaAlertChecker.java | 5 +- .../impl/{ => email}/EmailSender.java | 19 +-- .../services/impl/email/EmailSenderBean.java | 119 ++++++++++++++++++ .../rpki/services/impl/email/EmailTokens.java | 65 ++++++++++ .../SubscribeToRoaAlertCommandHandler.java | 18 ++- ...UnsubscribeFromRoaAlertCommandHandler.java | 11 +- .../impl/jpa/JpaPropertyEntityRepository.java | 2 +- .../JpaRoaAlertConfigurationRepository.java | 10 ++ src/main/resources/application-local.yml | 4 +- src/main/resources/application-pilot.yml | 3 + .../resources/application-prepdev.properties | 2 + .../application-production.properties | 2 + src/main/resources/application.yml | 4 +- .../email-templates/roa-alert-email.txt | 3 +- .../subscribe-confirmation-daily.txt | 3 +- .../subscribe-confirmation-weekly.txt | 3 +- .../rpki/rest/service/AlertServiceTest.java | 7 +- .../services/impl/RoaAlertCheckerTest.java | 69 ++++++---- ...oaAlertBackgroundServiceDailyBeanTest.java | 4 +- .../impl/{ => email}/EmailSenderBeanTest.java | 47 ++++--- .../impl/{ => email}/EmailTemplatesTest.java | 5 +- ...SubscribeToRoaAlertCommandHandlerTest.java | 16 ++- ...bscribeFromRoaAlertCommandHandlerTest.java | 6 +- ...paRoaAlertConfigurationRepositoryTest.java | 17 ++- src/test/resources/application-test.yml | 10 +- 37 files changed, 464 insertions(+), 212 deletions(-) create mode 100644 src/main/java/net/ripe/rpki/rest/service/EmailService.java delete mode 100644 src/main/java/net/ripe/rpki/services/impl/EmailSenderBean.java rename src/main/java/net/ripe/rpki/services/impl/{ => email}/EmailSender.java (66%) create mode 100644 src/main/java/net/ripe/rpki/services/impl/email/EmailSenderBean.java create mode 100644 src/main/java/net/ripe/rpki/services/impl/email/EmailTokens.java rename src/test/java/net/ripe/rpki/services/impl/{ => email}/EmailSenderBeanTest.java (64%) rename src/test/java/net/ripe/rpki/services/impl/{ => email}/EmailTemplatesTest.java (90%) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 558df4b..efc456c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -140,7 +140,7 @@ sonarqube: - if: $CI_COMMIT_BRANCH == "next" control/run-on-staging: - image: node:21-alpine + image: node:22-alpine stage: qa script: - ./scripts/gitlab-deploy-check diff --git a/README.md b/README.md index 9a43674..6d683f0 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ spring.security.oauth2.client: client-secret: '' ``` -Also change the `authorization.admin.role` to `ROLE_USER`. +Also change the `admin.authorization.enabled` to `true`. Make sure you do not check in your secrets! diff --git a/build.gradle b/build.gradle index 5fbea8d..af93f9e 100644 --- a/build.gradle +++ b/build.gradle @@ -44,7 +44,7 @@ dependencies { implementation "org.thymeleaf:thymeleaf:3.1.2.RELEASE" implementation "org.thymeleaf:thymeleaf-spring6:3.1.2.RELEASE" - implementation platform('io.sentry:sentry-bom:6.34.0') + implementation platform('io.sentry:sentry-bom:7.9.0') implementation 'io.sentry:sentry-spring-boot-starter' implementation 'io.sentry:sentry-logback' @@ -58,7 +58,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.10.1' implementation 'com.jamesmurty.utils:java-xmlbuilder:1.3' - implementation 'commons-codec:commons-codec:1.16.1' + implementation 'commons-codec:commons-codec:1.17.0' implementation 'commons-io:commons-io:2.16.1' implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5' implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5' @@ -72,7 +72,7 @@ dependencies { exclude group: 'org.hamcrest', module: 'hamcrest-core' } - testImplementation "org.wiremock:wiremock-jetty12:3.5.2" + testImplementation "org.wiremock:wiremock-jetty12:3.5.4" testImplementation 'net.jqwik:jqwik:1.8.4' testImplementation "net.ripe.rpki:rpki-commons:$rpki_commons_version:tests" testImplementation 'org.assertj:assertj-core' diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 5bcdbc7..88fc782 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -10,9 +10,9 @@ repositories { dependencies { implementation 'io.freefair.lombok:io.freefair.lombok.gradle.plugin:8.6' - implementation('com.gorylenko.gradle-git-properties:com.gorylenko.gradle-git-properties.gradle.plugin:2.4.1') { + implementation('com.gorylenko.gradle-git-properties:com.gorylenko.gradle-git-properties.gradle.plugin:2.4.2') { exclude group: 'org.eclipse.jgit', module: 'org.eclipse.jgit' } - implementation 'org.eclipse.jgit:org.eclipse.jgit:5.13.3.202401111512-r' - implementation 'org.sonarqube:org.sonarqube.gradle.plugin:4.4.1.3373' + implementation 'org.eclipse.jgit:org.eclipse.jgit:6.9.0.202403050737-r' + implementation 'org.sonarqube:org.sonarqube.gradle.plugin:5.0.0.4638' } diff --git a/buildSrc/src/main/groovy/rpki-ripe-ncc.build-conventions.gradle b/buildSrc/src/main/groovy/rpki-ripe-ncc.build-conventions.gradle index 5f88a66..72898a4 100644 --- a/buildSrc/src/main/groovy/rpki-ripe-ncc.build-conventions.gradle +++ b/buildSrc/src/main/groovy/rpki-ripe-ncc.build-conventions.gradle @@ -35,9 +35,10 @@ repositories { maven { url = uri('https://maven.nexus.ripe.net/repository/maven-third-party') } - maven { - url = uri('https://maven.nexus.ripe.net/repository/maven-third-party-snapshots') - } + // Use when testing new third party dependencies + // maven { + // url = uri('https://maven.nexus.ripe.net/repository/maven-third-party-snapshots') + // } } java { diff --git a/hsm/build.gradle b/hsm/build.gradle index 1a760c7..e175c69 100644 --- a/hsm/build.gradle +++ b/hsm/build.gradle @@ -31,8 +31,8 @@ dependencies { } } thalesImplementation "net.ripe.rpki:rpki-commons:$rpki_commons_version" - // 2024-4-16: Test DBProvider snapshot provided by Entrust - thalesImplementation 'com.thales.esecurity.asg.ripe.db-jceprovider:DBProvider:1.6-SNAPSHOT' + // 2024-4-26: Final DBProvider 1.6 provided by Entrust + thalesImplementation 'com.thales.esecurity.asg.ripe.db-jceprovider:DBProvider:1.6' // **When using JDK 11** make sure the matching version of nCipherKM is on classpath because DBProvider depends on it. thalesImplementation 'com.ncipher.nfast:nCipherKM:13.4.5' 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 c235d66..4e37afe 100644 --- a/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfiguration.java +++ b/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfiguration.java @@ -34,6 +34,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.UUID; import java.util.stream.Collectors; @Entity diff --git a/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfigurationRepository.java b/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfigurationRepository.java index 0a2fe3c..d687081 100644 --- a/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfigurationRepository.java +++ b/src/main/java/net/ripe/rpki/domain/alerts/RoaAlertConfigurationRepository.java @@ -2,6 +2,8 @@ import java.util.Collection; import java.util.List; +import java.util.Optional; +import java.util.UUID; public interface RoaAlertConfigurationRepository { @@ -16,4 +18,6 @@ public interface RoaAlertConfigurationRepository { void remove(RoaAlertConfiguration entity); List findByEmail(String email); + + Optional findByUnsubscribeToken(UUID unsubscribeToken); } diff --git a/src/main/java/net/ripe/rpki/publication/server/ExternalPublishingServer.java b/src/main/java/net/ripe/rpki/publication/server/ExternalPublishingServer.java index 9f0ee74..de72756 100644 --- a/src/main/java/net/ripe/rpki/publication/server/ExternalPublishingServer.java +++ b/src/main/java/net/ripe/rpki/publication/server/ExternalPublishingServer.java @@ -132,9 +132,10 @@ private Counter createCounter(MeterRegistry meterRegistry, String operation, Obj .register(meterRegistry); } - private static Timer createTimer(MeterRegistry meterRegistry, String status) { + private Timer createTimer(MeterRegistry meterRegistry, String status) { return Timer.builder("rpkicore.publication.request.duration") .tag("status", status) + .tag("uri", this.publishingServerUrl.toString()) .description("Time for publication HTTP request") .publishPercentileHistogram() .minimumExpectedValue(Duration.ofMillis(4)) diff --git a/src/main/java/net/ripe/rpki/rest/service/EmailService.java b/src/main/java/net/ripe/rpki/rest/service/EmailService.java new file mode 100644 index 0000000..4821b55 --- /dev/null +++ b/src/main/java/net/ripe/rpki/rest/service/EmailService.java @@ -0,0 +1,78 @@ +package net.ripe.rpki.rest.service; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.core.MediaType; +import lombok.extern.slf4j.Slf4j; +import net.ripe.rpki.domain.alerts.RoaAlertConfigurationRepository; +import net.ripe.rpki.server.api.commands.UnsubscribeFromRoaAlertCommand; +import net.ripe.rpki.server.api.dto.RoaAlertSubscriptionData; +import net.ripe.rpki.server.api.services.command.CommandService; +import net.ripe.rpki.services.impl.email.EmailTokens; +import org.apache.logging.log4j.util.Strings; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.springframework.http.HttpStatus.NOT_FOUND; + +@Slf4j +@Scope("prototype") +@RestController +@RequestMapping(path = "/api/email", produces = MediaType.APPLICATION_JSON) +@Tag(name = "/api/email", description = "Manage email subscriptions") +public class EmailService extends RestService { + + public static final String ERROR = "error"; + + private final CommandService commandService; + private final RoaAlertConfigurationRepository roaAlertConfigurationRepository; + private final EmailTokens emailTokens; + + @Autowired + public EmailService(CommandService commandService, + EmailTokens emailTokens, + RoaAlertConfigurationRepository roaAlertConfigurationRepository) { + this.commandService = commandService; + this.roaAlertConfigurationRepository = roaAlertConfigurationRepository; + this.emailTokens = emailTokens; + } + + @PostMapping("/unsubscribe/{email}/{token}") + @Operation(summary = "Implement one-click unsubscribe functionality.") + public ResponseEntity unsubscribe( + @PathVariable("email") final String email, + @PathVariable("token") final String token) { + + if (Strings.isBlank(token)) { + return ResponseEntity.status(NOT_FOUND).body(Map.of(ERROR, "Unknown or invalid token: " + token)); + } + var configurations = roaAlertConfigurationRepository.findByEmail(email); + if (configurations.isEmpty()) { + return ResponseEntity.status(NOT_FOUND).body(Map.of(ERROR, "Unknown email " + email)); + } + var unsubscribedAnyone = new AtomicBoolean(false); + configurations.forEach(configuration -> { + RoaAlertSubscriptionData subscriptionOrNull = configuration.getSubscriptionOrNull(); + var ca = configuration.getCertificateAuthority(); + var configurationToken = emailTokens.createUnsubscribeToken(EmailTokens.uniqueId(ca.getUuid()), email); + if (subscriptionOrNull != null + && subscriptionOrNull.getEmails().contains(email) + && token.equals(configurationToken)) { + commandService.execute(new UnsubscribeFromRoaAlertCommand(ca.getVersionedId(), email)); + unsubscribedAnyone.set(true); + } + }); + if (unsubscribedAnyone.get()) { + return ResponseEntity.ok().body(Map.of("success", "Unsubscribed " + email)); + } + return ResponseEntity.status(NOT_FOUND).body(Map.of(ERROR, "Unknown token " + token)); + } +} 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 3264796..5799825 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 @@ -1,40 +1,27 @@ package net.ripe.rpki.server.api.dto; +import lombok.Getter; import net.ripe.rpki.commons.validation.roa.RouteValidityState; import net.ripe.rpki.domain.alerts.RoaAlertFrequency; import net.ripe.rpki.server.api.support.objects.ValueObjectSupport; import java.util.*; - +@Getter public class RoaAlertSubscriptionData extends ValueObjectSupport { private final List emails; - private final EnumSet routeValidityStates; private final RoaAlertFrequency frequency; + private final EnumSet routeValidityStates; public RoaAlertSubscriptionData(String email, Collection routeValidityStates, RoaAlertFrequency frequency) { - this.emails = new ArrayList<>(); - emails.add(email); - this.routeValidityStates = EnumSet.copyOf(routeValidityStates); - this.frequency = frequency; + this(List.of(email), routeValidityStates, frequency); } - public RoaAlertSubscriptionData(List emails, Collection routeValidityStates, RoaAlertFrequency frequency) { + public RoaAlertSubscriptionData(List emails, Collection routeValidityStates, + RoaAlertFrequency frequency) { this.emails = new ArrayList<>(emails); this.routeValidityStates = EnumSet.copyOf(routeValidityStates); this.frequency = frequency; } - - public List getEmails() { - return emails; - } - - public RoaAlertFrequency getFrequency() { - return frequency; - } - - public Set getRouteValidityStates() { - return routeValidityStates; - } } diff --git a/src/main/java/net/ripe/rpki/services/impl/EmailSenderBean.java b/src/main/java/net/ripe/rpki/services/impl/EmailSenderBean.java deleted file mode 100644 index 6dcff97..0000000 --- a/src/main/java/net/ripe/rpki/services/impl/EmailSenderBean.java +++ /dev/null @@ -1,84 +0,0 @@ -package net.ripe.rpki.services.impl; - -import com.google.common.annotations.VisibleForTesting; -import lombok.extern.slf4j.Slf4j; -import net.ripe.rpki.server.api.configuration.Environment; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.mail.MailException; -import org.springframework.mail.MailSender; -import org.springframework.mail.SimpleMailMessage; -import org.springframework.stereotype.Component; -import org.thymeleaf.TemplateEngine; -import org.thymeleaf.context.Context; -import org.thymeleaf.templatemode.TemplateMode; -import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; -import org.thymeleaf.templateresolver.ITemplateResolver; - -import java.util.Map; -import java.util.TreeMap; - -@Component -@Slf4j -public class EmailSenderBean implements EmailSender { - - private final MailSender mailSender; - - private final SimpleMailMessage templateMessage; - - private final TemplateEngine templateEngine; - private final Map defaultParameters = new TreeMap<>(); - - @Autowired - public EmailSenderBean(MailSender mailSender, @Value("${mail.template.parameters.rpkiDashboardUri}") String rpkiDashboardUri) { - this.mailSender = mailSender; - this.defaultParameters.put("rpkiDashboardUri", rpkiDashboardUri); - - this.templateMessage = new SimpleMailMessage(); - templateMessage.setFrom("noreply@ripe.net"); - - templateEngine = new TemplateEngine(); - templateEngine.addTemplateResolver(textTemplateResolver()); - - log.debug("configured email sender with default parameters: {}", defaultParameters); - } - - @VisibleForTesting - protected static ITemplateResolver textTemplateResolver() { - final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); - templateResolver.setOrder(Integer.valueOf(1)); - templateResolver.setTemplateMode(TemplateMode.TEXT); - templateResolver.setCharacterEncoding("UTF-8"); - templateResolver.setCacheable(false); - return templateResolver; - } - - public void sendEmail(String emailTo, String subject, EmailTemplates template, Map parameters) { - SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage); - msg.setSubject(subject); - msg.setTo(emailTo); - - log.info("Rendering Email template {}", template.templateName); - msg.setText(renderTemplate(template.templateName, parameters)); - - if (!Environment.isLocal()) { - try { - log.info("Sending email with subject: {} to: {} ", subject, emailTo); - this.mailSender.send(msg); - } catch (MailException e) { - log.warn("Couldn't send email to '" + emailTo + "'.", e); - } - } else { - log.info("Not sending message in DEVELOPMENT mode:\n" + msg.toString()); - } - } - - private String renderTemplate(String nameOfTemplate, Map parameters) { - Context context = new Context(); - context.setVariables(this.defaultParameters); - context.setVariables(parameters); - - return templateEngine.process(nameOfTemplate, context); - } - -} diff --git a/src/main/java/net/ripe/rpki/services/impl/RoaAlertChecker.java b/src/main/java/net/ripe/rpki/services/impl/RoaAlertChecker.java index 2a3f986..f234a8b 100644 --- a/src/main/java/net/ripe/rpki/services/impl/RoaAlertChecker.java +++ b/src/main/java/net/ripe/rpki/services/impl/RoaAlertChecker.java @@ -12,6 +12,8 @@ import net.ripe.rpki.server.api.ports.InternalNamePresenter; import net.ripe.rpki.server.api.services.read.BgpRisEntryViewService; import net.ripe.rpki.server.api.services.read.RoaViewService; +import net.ripe.rpki.services.impl.email.EmailSender; +import net.ripe.rpki.services.impl.email.EmailTokens; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -152,7 +154,8 @@ private void sendRoaAlertEmailToSubscription(RoaAlertConfigurationData configura email, String.format(EmailSender.EmailTemplates.ROA_ALERT.templateSubject, humanizedCaName), EmailSender.EmailTemplates.ROA_ALERT, - parameters) + parameters, + EmailTokens.uniqueId(configuration.getCertificateAuthority().getUuid())) ); } diff --git a/src/main/java/net/ripe/rpki/services/impl/EmailSender.java b/src/main/java/net/ripe/rpki/services/impl/email/EmailSender.java similarity index 66% rename from src/main/java/net/ripe/rpki/services/impl/EmailSender.java rename to src/main/java/net/ripe/rpki/services/impl/email/EmailSender.java index 502a3c5..97038b3 100644 --- a/src/main/java/net/ripe/rpki/services/impl/EmailSender.java +++ b/src/main/java/net/ripe/rpki/services/impl/email/EmailSender.java @@ -1,26 +1,27 @@ -package net.ripe.rpki.services.impl; +package net.ripe.rpki.services.impl.email; import java.util.Map; public interface EmailSender { - void sendEmail(String emailTo, String subject, EmailTemplates template, Map parameters); + void 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"), - 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"), - ROA_ALERT("email-templates/roa-alert-email.txt", "Resource Certification (RPKI) alerts for %s"); + 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); public final String templateName; public final String templateSubject; + public final boolean generateUnsubcribeUrl; - private EmailTemplates(String templateName, String subject) { + EmailTemplates(String templateName, String subject, boolean generateUnsubcribeUrl) { this.templateName = templateName; this.templateSubject = subject; + this.generateUnsubcribeUrl = generateUnsubcribeUrl; } - - } + } 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 new file mode 100644 index 0000000..8ffa7a3 --- /dev/null +++ b/src/main/java/net/ripe/rpki/services/impl/email/EmailSenderBean.java @@ -0,0 +1,119 @@ +package net.ripe.rpki.services.impl.email; + +import com.google.common.annotations.VisibleForTesting; +import jakarta.mail.Message; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import lombok.extern.slf4j.Slf4j; +import net.ripe.rpki.server.api.configuration.Environment; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.MailException; +import org.springframework.mail.MailSender; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.stereotype.Component; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; +import org.thymeleaf.templateresolver.ITemplateResolver; + +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +@Component +@Slf4j +public class EmailSenderBean implements EmailSender { + + private final MailSender mailSender; + private final SimpleMailMessage templateMessage; + + private final TemplateEngine templateEngine; + private final Map defaultParameters = new TreeMap<>(); + private final EmailTokens emailTokens; + + @Autowired + public EmailSenderBean(MailSender mailSender, EmailTokens emailTokens, + @Value("${mail.template.parameters.rpkiDashboardUri}") String rpkiDashboardUri) { + this.mailSender = mailSender; + this.defaultParameters.put("rpkiDashboardUri", rpkiDashboardUri); + this.emailTokens = emailTokens; + + this.templateMessage = new SimpleMailMessage(); + templateMessage.setFrom("noreply@ripe.net"); + + templateEngine = new TemplateEngine(); + templateEngine.addTemplateResolver(textTemplateResolver()); + + log.debug("configured email sender with default parameters: {}", defaultParameters); + } + + @VisibleForTesting + protected static ITemplateResolver textTemplateResolver() { + final ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); + templateResolver.setOrder(1); + templateResolver.setTemplateMode(TemplateMode.TEXT); + templateResolver.setCharacterEncoding("UTF-8"); + templateResolver.setCacheable(false); + return templateResolver; + } + + @Override + public void 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; + } + + try { + JavaMailSenderImpl javaMailSender = (JavaMailSenderImpl) mailSender; + MimeMessage message = javaMailSender.createMimeMessage(); + + message.setFrom(new InternetAddress("rpki@ripe.net")); + message.setRecipient(Message.RecipientType.TO, new InternetAddress(emailTo)); + message.setSubject(subject); + var parametersUpdated = parameters; + if (template.generateUnsubcribeUrl) { + var unsubscribeUri = emailTokens.makeUnsubscribeUrl(uniqueId, emailTo); + message.addHeader("List-Unsubscribe", "<" + unsubscribeUri + ">"); + message.addHeader("List-Unsubscribe-Post", "List-Unsubscribe=One-Click"); + parametersUpdated = extendParameters(parameters, unsubscribeUri); + } + + log.info("Rendering Email template {}", template.templateName); + message.setText(renderTemplate(template.templateName, parametersUpdated)); + + if (!Environment.isLocal()) { + try { + log.info("Sending email with subject: {} to: {} ", subject, emailTo); + javaMailSender.send(message); + } catch (MailException e) { + log.warn("Couldn't send email to '" + emailTo + "'.", e); + } + } else { + log.info("Not sending message in DEVELOPMENT mode:\n" + message); + } + } catch (Exception e) { + log.error("Failed to send email", e); + } + } + + private Map extendParameters(Map parameters, String unsubscribeUri) { + if (unsubscribeUri == null) { + return parameters; + } + var m = new HashMap<>(parameters); + m.put("unsubscribeUri", unsubscribeUri); + return m; + } + + private String renderTemplate(String nameOfTemplate, Map parameters) { + Context context = new Context(); + context.setVariables(this.defaultParameters); + context.setVariables(parameters); + return templateEngine.process(nameOfTemplate, context); + } + +} diff --git a/src/main/java/net/ripe/rpki/services/impl/email/EmailTokens.java b/src/main/java/net/ripe/rpki/services/impl/email/EmailTokens.java new file mode 100644 index 0000000..6fba58d --- /dev/null +++ b/src/main/java/net/ripe/rpki/services/impl/email/EmailTokens.java @@ -0,0 +1,65 @@ +package net.ripe.rpki.services.impl.email; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +@Component +public class EmailTokens { + + private final String authUnsubscribeUri; + private final String apiUnsubscribeUri; + private final Mac mac; + + public EmailTokens(@Value("${mail.unsubscribe.secret}") String unsubscribeSecret, + @Value("${mail.template.parameters.authUnsubscribeUri}") String authUnsubscribeUri, + @Value("${mail.template.parameters.apiUnsubscribeUri}") String apiUnsubscribeUri) { + this.authUnsubscribeUri = authUnsubscribeUri; + this.apiUnsubscribeUri = apiUnsubscribeUri; + this.mac = initMac(unsubscribeSecret); + } + + /** + * This is to generate some unique identifier corresponding to a CA to mix + * into the unsubscribe tokens. + */ + public static String uniqueId(Object o) { + return String.valueOf(o); + } + + public static String enc(String s) { + return URLEncoder.encode(s, StandardCharsets.UTF_8); + } + + /** + * A token used for an unsubscribe link is a hmac of a server-wide secret, + * unique id corresponding to the CA and the user's email. + */ + public String createUnsubscribeToken(String uniqueId, String email) { + var bytes = mac.doFinal((uniqueId + email).getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().encodeToString(bytes); + } + + private static Mac initMac(String unsubscribeSecret) { + try { + var algorithm = "HmacSHA256"; + var mac = Mac.getInstance(algorithm); + SecretKeySpec secretKeySpec = new SecretKeySpec(unsubscribeSecret.getBytes(StandardCharsets.UTF_8), algorithm); + mac.init(secretKeySpec); + return mac; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public String makeUnsubscribeUrl(String uniqueId, String email) { + var unsubscribeToken = createUnsubscribeToken(uniqueId, email); + var apiUrl = apiUnsubscribeUri + "/" + enc(email) + "/" + unsubscribeToken; + return authUnsubscribeUri + enc(apiUrl); + } +} 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 32184cc..4ae3609 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 @@ -9,9 +9,11 @@ import net.ripe.rpki.server.api.dto.RoaAlertConfigurationData; import net.ripe.rpki.server.api.dto.RoaAlertSubscriptionData; import net.ripe.rpki.server.api.services.command.CommandStatus; -import net.ripe.rpki.services.impl.EmailSender; +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.Set; import java.util.stream.Collectors; @@ -52,7 +54,8 @@ private void createConfigurationAndSendConfirmation(SubscribeToRoaAlertCommand c var emailTemplate = getConfirmationTemplate(configuration); emailSender.sendEmail(command.getEmail(), emailTemplate.templateSubject, emailTemplate, - Collections.singletonMap(SUBSCRIPTION, configuration.toData())); + Collections.singletonMap(SUBSCRIPTION, configuration.toData()), + EmailTokens.uniqueId(configuration.getCertificateAuthority().getUuid())); } private EmailSender.EmailTemplates getConfirmationTemplate(RoaAlertConfiguration configuration) { @@ -81,12 +84,15 @@ private void updateConfigurationAndSendConfirmation(RoaAlertConfiguration config Sets.difference(newEmailAddress, oldEmailAddress).forEach(email -> { var emailTemplate = getConfirmationTemplate(configuration); emailSender.sendEmail(email, emailTemplate.templateSubject, emailTemplate, - Collections.singletonMap(SUBSCRIPTION, newConfiguration)); + Collections.singletonMap(SUBSCRIPTION, newConfiguration), + EmailTokens.uniqueId(configuration.getCertificateAuthority().getUuid())); }); - Sets.difference(oldEmailAddress, newEmailAddress).forEach(email -> { - emailSender.sendEmail(email, EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE.templateSubject, EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE, Collections.singletonMap(SUBSCRIPTION, oldConfiguration)); - }); + 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()))); } private RoaAlertConfiguration createConfiguration(SubscribeToRoaAlertCommand command) { 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 6e5997f..fcc6f51 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 @@ -6,9 +6,11 @@ import net.ripe.rpki.server.api.commands.UnsubscribeFromRoaAlertCommand; import net.ripe.rpki.server.api.dto.RoaAlertSubscriptionData; import net.ripe.rpki.server.api.services.command.CommandStatus; -import net.ripe.rpki.services.impl.EmailSender; +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; @@ -43,7 +45,10 @@ public void handle(UnsubscribeFromRoaAlertCommand command, CommandStatus command } configuration.removeEmail(command.getEmail()); - emailSender.sendEmail(normEmail(command.getEmail()), EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE.templateSubject, EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE, - Collections.singletonMap("subscription", configuration.toData())); + emailSender.sendEmail(normEmail(command.getEmail()), + EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE.templateSubject, + EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE, + Collections.singletonMap("subscription", configuration.toData()), + EmailTokens.uniqueId(configuration.getCertificateAuthority().getUuid())); } } diff --git a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaPropertyEntityRepository.java b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaPropertyEntityRepository.java index 989277a..3102ee1 100644 --- a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaPropertyEntityRepository.java +++ b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaPropertyEntityRepository.java @@ -40,7 +40,7 @@ public PropertyEntity getByKey(String key) { } @Override - public void createOrUpdate(String key, String value) { + public synchronized void createOrUpdate(String key, String value) { PropertyEntity entity = findByKey(key); if (entity == null) { entity = new PropertyEntity(key, value); diff --git a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaAlertConfigurationRepository.java b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaAlertConfigurationRepository.java index 99c878a..8f2b9de 100644 --- a/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaAlertConfigurationRepository.java +++ b/src/main/java/net/ripe/rpki/services/impl/jpa/JpaRoaAlertConfigurationRepository.java @@ -10,6 +10,8 @@ import jakarta.persistence.NoResultException; import jakarta.persistence.Query; import java.util.List; +import java.util.Optional; +import java.util.UUID; @Component public class JpaRoaAlertConfigurationRepository extends JpaRepository implements RoaAlertConfigurationRepository { @@ -44,4 +46,12 @@ public List findByEmail(String email) { query.setParameter("email", "%" + email + "%"); return query.getResultList(); } + + @SuppressWarnings("unchecked") + @Override + public Optional findByUnsubscribeToken(UUID unsubscribeToken) { + return createQuery("SELECT rac FROM RoaAlertConfiguration rac WHERE unsubscribeToken = :token") + .setParameter("token", unsubscribeToken) + .getResultList().stream().findAny(); + } } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 971648d..a1436b9 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -18,6 +18,8 @@ admin.authorization.enabled: false mail: host: localhost port: 1025 + unsubscribe: + secret: dQl9TJEu3JQMvyEOEwkut4T1zOiHN0lyLYRUWI7q8jWejnDtGT30WrVCGVFmY7iZ spring: main: @@ -56,7 +58,7 @@ resource.services: # # Application specific settings # -authorization.admin.role: ROLE_ANONYMOUS + # We externalise application secrets in a file outside the application on the classpath. api-keys.properties: "classpath:/test-api-keys.properties" diff --git a/src/main/resources/application-pilot.yml b/src/main/resources/application-pilot.yml index eb8c829..fadb58c 100644 --- a/src/main/resources/application-pilot.yml +++ b/src/main/resources/application-pilot.yml @@ -19,6 +19,9 @@ resource.services.url: https://rsng.ripe.net/resource-services # mail.template.parameters: rpkiDashboardUri: "https://localcert.ripe.net/#/rpki" + authUnsubscribeUri: "https://access.ripe.net/?originalUrl=" + apiUnsubscribeUri: "https://localcert.ripe.net/api/rpki/unsubscribe-alerts" + key.management.data: archive.directory: /cert/hsmkeys/shared-keys.archive diff --git a/src/main/resources/application-prepdev.properties b/src/main/resources/application-prepdev.properties index 67dd362..58abb27 100644 --- a/src/main/resources/application-prepdev.properties +++ b/src/main/resources/application-prepdev.properties @@ -2,6 +2,8 @@ server.address=localhost system.setup.and.testing.api.enabled: true mail.template.parameters.rpkiDashboardUri=https://my.prepdev.ripe.net/#/rpki +mail.template.parameters.authUnsubscribeUri=https://access.prepdev.ripe.net/?originalUrl= +mail.template.parameters.apiUnsubscribeUri=https://my.prepdev.ripe.net/api/rpki/unsubscribe-alerts # The locally accessible root directory for the public certificate repository. # Files written here should be visible externally using the diff --git a/src/main/resources/application-production.properties b/src/main/resources/application-production.properties index c97a9fd..c2fb6c5 100644 --- a/src/main/resources/application-production.properties +++ b/src/main/resources/application-production.properties @@ -3,6 +3,8 @@ logging.config=classpath:logback/logback-production.xml server.address=localhost mail.template.parameters.rpkiDashboardUri=https://my.ripe.net/#/rpki +mail.template.parameters.authUnsubscribeUri=https://access.ripe.net/?originalUrl= +mail.template.parameters.apiUnsubscribeUri=https://my.ripe.net/api/rpki/unsubscribe-alerts # The locally accessible root directory for the public certificate repository. # Files written here should be visible externally using the diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 978f15e..d74e999 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -50,7 +50,9 @@ mail: host: localhost port: 25 template.parameters: - rpkiDashboardUri: "https://my.ripe.net/#/rpki" + rpkiDashboardUri: "https://my.ripe.net/#/rpki" + authUnsubscribeUri: "https://access.ripe.net/?originalUrl=" + apiUnsubscribeUri: "https://my.ripe.net/api/rpki/unsubscribe-alerts" # do not expose the default endpoints for security. # opt-in to prometheus and info endpoint. diff --git a/src/main/resources/email-templates/roa-alert-email.txt b/src/main/resources/email-templates/roa-alert-email.txt index 653086e..fc65be8 100644 --- a/src/main/resources/email-templates/roa-alert-email.txt +++ b/src/main/resources/email-templates/roa-alert-email.txt @@ -46,4 +46,5 @@ alerts have been muted.[/] - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - You are able to fix and ignore reported issues, change your alert -settings, or unsubscribe by visiting [[${rpkiDashboardUri}]]. +settings, or unsubscribe by visiting [[${rpkiDashboardUri}]] or +directly using [(${unsubscribeUri})]. \ No newline at end of file diff --git a/src/main/resources/email-templates/subscribe-confirmation-daily.txt b/src/main/resources/email-templates/subscribe-confirmation-daily.txt index 21e0e8e..135e932 100644 --- a/src/main/resources/email-templates/subscribe-confirmation-daily.txt +++ b/src/main/resources/email-templates/subscribe-confirmation-daily.txt @@ -4,4 +4,5 @@ Once every 24 hours, you will receive email alerts from the RIPE NCC Resource Certification (RPKI) service. You are able to fix and ignore reported issues, change your alert -settings, or unsubscribe by visiting [[${rpkiDashboardUri}]]. +settings, or unsubscribe by visiting [[${rpkiDashboardUri}]] or +directly using [(${unsubscribeUri})]. diff --git a/src/main/resources/email-templates/subscribe-confirmation-weekly.txt b/src/main/resources/email-templates/subscribe-confirmation-weekly.txt index a45a339..e63cfb3 100644 --- a/src/main/resources/email-templates/subscribe-confirmation-weekly.txt +++ b/src/main/resources/email-templates/subscribe-confirmation-weekly.txt @@ -4,4 +4,5 @@ Every week (on Mondays), you will receive email alerts from the RIPE NCC Resource Certification (RPKI) service. You are able to fix and ignore reported issues, change your alert -settings, or unsubscribe by visiting [[${rpkiDashboardUri}]]. +settings, or unsubscribe by visiting [[${rpkiDashboardUri}]] or +directly using [(${unsubscribeUri})]. 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 0f834eb..658bf36 100644 --- a/src/test/java/net/ripe/rpki/rest/service/AlertServiceTest.java +++ b/src/test/java/net/ripe/rpki/rest/service/AlertServiceTest.java @@ -33,10 +33,7 @@ import org.springframework.test.web.servlet.MockMvc; import javax.security.auth.x500.X500Principal; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -68,7 +65,7 @@ public class AlertServiceTest { @MockBean private CommandService commandService; - private HostedCertificateAuthorityData certificateAuthorityData = mock(HostedCertificateAuthorityData.class); + private final HostedCertificateAuthorityData certificateAuthorityData = mock(HostedCertificateAuthorityData.class); @Autowired private MockMvc mockMvc; diff --git a/src/test/java/net/ripe/rpki/services/impl/RoaAlertCheckerTest.java b/src/test/java/net/ripe/rpki/services/impl/RoaAlertCheckerTest.java index 013df35..f8994db 100644 --- a/src/test/java/net/ripe/rpki/services/impl/RoaAlertCheckerTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/RoaAlertCheckerTest.java @@ -14,6 +14,9 @@ import net.ripe.rpki.server.api.ports.InternalNamePresenter; import net.ripe.rpki.server.api.services.read.RoaViewService; import net.ripe.rpki.services.impl.background.RoaAlertBackgroundServiceDailyBeanTest; +import net.ripe.rpki.services.impl.email.EmailSender; +import net.ripe.rpki.services.impl.email.EmailSenderBean; +import net.ripe.rpki.services.impl.email.EmailTokens; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -21,18 +24,17 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.mail.MailSender; import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import jakarta.mail.internet.MimeMessage; import javax.security.auth.x500.X500Principal; import java.util.Arrays; import java.util.Collections; +import java.util.UUID; import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.isA; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; @RunWith(MockitoJUnitRunner.class) @@ -59,16 +61,23 @@ public class RoaAlertCheckerTest { private BgpRisEntryRepositoryBean bgpRisEntryRepository; @Mock - private MailSender mailSender; + private JavaMailSenderImpl mailSender; @Mock private InternalNamePresenter internalNamePresenter; private RoaAlertChecker subject; + private final String rpkiDashboardUri = "https://my.ripe.net/#/rpki"; + private final String authUnsubscribeUri = "http://access.ripe.net/?originalUrl="; + private final String apiUnsubscribeUri = "http://my.ripe.net/api/email/unsubscribe-alerts"; + private final String unsubscribeSecret = UUID.randomUUID().toString(); + + private final EmailTokens emailTokens = new EmailTokens(unsubscribeSecret, authUnsubscribeUri, apiUnsubscribeUri); + @Before public void setup() { - EmailSender emailSenderBean = new EmailSenderBean(mailSender, "https://my.ripe.net/#/rpki"); + EmailSender emailSenderBean = new EmailSenderBean(mailSender, emailTokens, rpkiDashboardUri); subject = new RoaAlertChecker(roaService, bgpRisEntryRepository, internalNamePresenter, emailSenderBean, new SimpleMeterRegistry()); System.setProperty(Environment.APPLICATION_ENVIRONMENT_KEY, "junit"); @@ -80,14 +89,15 @@ public void tearDown() { } @Test - public void shouldCheckRoasAgainstBgpForInvalidLength() { + public void shouldCheckRoasAgainstBgpForInvalidLength() throws Exception { when(internalNamePresenter.humanizeCaName(isA(X500Principal.class))).thenReturn("zz.example"); when(roaService.getRoaConfiguration(CA_ID)).thenReturn(ROA_CONFIGURATION_DATA); when(bgpRisEntryRepository.findMostSpecificOverlapping(CERTIFIED_RESOURCES)).thenReturn(Arrays.asList(BGP_RIS_ENTRY_1, BGP_RIS_ENTRY_1_1)); + when(mailSender.createMimeMessage()).thenReturn(new JavaMailSenderImpl().createMimeMessage()); subject.checkAndSendRoaAlertEmailToSubscription(ALERT_SUBSCRIPTION_DATA); - ArgumentCaptor capturedMessage = ArgumentCaptor.forClass(SimpleMailMessage.class); + ArgumentCaptor capturedMessage = ArgumentCaptor.forClass(MimeMessage.class); verify(mailSender).send(capturedMessage.capture()); String expected = "Dear colleague,\n" + @@ -112,21 +122,32 @@ public void shouldCheckRoasAgainstBgpForInvalidLength() { "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n" + "\n" + "You are able to fix and ignore reported issues, change your alert\n" + - "settings, or unsubscribe by visiting https://my.ripe.net/#/rpki.\n"; + "settings, or unsubscribe by visiting " + rpkiDashboardUri + " or\n" + + "directly using " + unsubscribeUrl() + "."; assertEquals("Resource Certification (RPKI) alerts for zz.example", capturedMessage.getValue().getSubject()); - assertEquals(expected, capturedMessage.getValue().getText()); + assertEquals(expected, capturedMessage.getValue().getContent()); + } + + private String unsubscribeUrl() { + String emails1 = ALERT_SUBSCRIPTION_DATA.getSubscription().getEmails().get(0); + var encodedEmail = EmailTokens.enc(emails1); + var uniqueId = EmailTokens.uniqueId(ALERT_SUBSCRIPTION_DATA.getCertificateAuthority().getUuid()); + var unsubscribeToken = emailTokens.createUnsubscribeToken(uniqueId, emails1); + return authUnsubscribeUri + EmailTokens.enc(apiUnsubscribeUri + "/" + encodedEmail + "/" + unsubscribeToken); } @Test - public void shouldCheckRoasAgainstBgpForInvalidAsn() { + public void shouldCheckRoasAgainstBgpForInvalidAsn() throws Exception { when(internalNamePresenter.humanizeCaName(isA(X500Principal.class))).thenReturn("zz.example"); when(roaService.getRoaConfiguration(CA_ID)).thenReturn(ROA_CONFIGURATION_DATA); when(bgpRisEntryRepository.findMostSpecificOverlapping(CERTIFIED_RESOURCES)).thenReturn(Arrays.asList(BGP_RIS_ENTRY_2, BGP_RIS_ENTRY_2_1, BGP_RIS_ENTRY_2_2)); + when(mailSender.createMimeMessage()).thenReturn(new JavaMailSenderImpl().createMimeMessage()); + subject.checkAndSendRoaAlertEmailToSubscription(ALERT_SUBSCRIPTION_DATA); - ArgumentCaptor capturedMessage = ArgumentCaptor.forClass(SimpleMailMessage.class); + ArgumentCaptor capturedMessage = ArgumentCaptor.forClass(MimeMessage.class); verify(mailSender).send(capturedMessage.capture()); String expected = "Dear colleague,\n" + @@ -155,10 +176,11 @@ public void shouldCheckRoasAgainstBgpForInvalidAsn() { "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n" + "\n" + "You are able to fix and ignore reported issues, change your alert\n" + - "settings, or unsubscribe by visiting https://my.ripe.net/#/rpki.\n"; + "settings, or unsubscribe by visiting " + rpkiDashboardUri + " or\n" + + "directly using " + unsubscribeUrl() + "."; assertEquals("Resource Certification (RPKI) alerts for zz.example", capturedMessage.getValue().getSubject()); - assertEquals(expected, capturedMessage.getValue().getText()); + assertEquals(expected, capturedMessage.getValue().getContent()); } @Test @@ -172,11 +194,12 @@ public void should_not_alert_on_ignored_announcements() { } @Test - public void shouldListIgnoredAnnouncementsInEmail() { + public void shouldListIgnoredAnnouncementsInEmail() throws Exception { when(roaService.getRoaConfiguration(CA_ID)).thenReturn(ROA_CONFIGURATION_DATA); when(bgpRisEntryRepository.findMostSpecificOverlapping(CERTIFIED_RESOURCES)).thenReturn(Collections.singleton(BGP_RIS_ENTRY_1)); - subject.checkAndSendRoaAlertEmailToSubscription(ALERT_SUBSCRIPTION_DATA.withIgnoredAnnouncements(Collections.singleton(new AnnouncedRoute(Asn.parse("AS65535"), IpRange.parse("127.0.0.0/12"))))); + subject.checkAndSendRoaAlertEmailToSubscription(ALERT_SUBSCRIPTION_DATA.withIgnoredAnnouncements( + Collections.singleton(new AnnouncedRoute(Asn.parse("AS65535"), IpRange.parse("127.0.0.0/12"))))); verify(mailSender, never()).send(isA(SimpleMailMessage.class)); @@ -184,9 +207,12 @@ public void shouldListIgnoredAnnouncementsInEmail() { when(roaService.getRoaConfiguration(CA_ID)).thenReturn(ROA_CONFIGURATION_DATA); when(bgpRisEntryRepository.findMostSpecificOverlapping(CERTIFIED_RESOURCES)).thenReturn(Collections.singleton(BGP_RIS_ENTRY_2)); - subject.checkAndSendRoaAlertEmailToSubscription(ALERT_SUBSCRIPTION_DATA.withIgnoredAnnouncements(Collections.singleton(new AnnouncedRoute(Asn.parse("AS12345"), IpRange.parse("127.0.0.0/12"))))); - ArgumentCaptor capturedMessage = ArgumentCaptor.forClass(SimpleMailMessage.class); + when(mailSender.createMimeMessage()).thenReturn(new JavaMailSenderImpl().createMimeMessage()); + subject.checkAndSendRoaAlertEmailToSubscription(ALERT_SUBSCRIPTION_DATA.withIgnoredAnnouncements( + Collections.singleton(new AnnouncedRoute(Asn.parse("AS12345"), IpRange.parse("127.0.0.0/12"))))); + + ArgumentCaptor capturedMessage = ArgumentCaptor.forClass(MimeMessage.class); verify(mailSender).send(capturedMessage.capture()); String expected = "Dear colleague,\n" + @@ -216,9 +242,10 @@ public void shouldListIgnoredAnnouncementsInEmail() { "\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\n" + "\n" + "You are able to fix and ignore reported issues, change your alert\n" + - "settings, or unsubscribe by visiting https://my.ripe.net/#/rpki.\n"; + "settings, or unsubscribe by visiting " + rpkiDashboardUri + " or\n" + + "directly using " + unsubscribeUrl() + "."; assertEquals("Resource Certification (RPKI) alerts for zz.example", capturedMessage.getValue().getSubject()); - assertEquals(expected, capturedMessage.getValue().getText()); + assertEquals(expected, capturedMessage.getValue().getContent()); } } 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 4d60ae9..d3bac33 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 @@ -23,6 +23,7 @@ import javax.security.auth.x500.X500Principal; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.UUID; import static org.mockito.Mockito.doThrow; @@ -38,8 +39,9 @@ public class RoaAlertBackgroundServiceDailyBeanTest { ImmutableResourceSet.ALL_PRIVATE_USE_RESOURCES, Collections.emptyList()); public static final RoaAlertConfigurationData ALERT_SUBSCRIPTION_DATA = new RoaAlertConfigurationData(CA_DATA, - new RoaAlertSubscriptionData("joeok@example.com", Arrays.asList(RouteValidityState.INVALID_ASN, + new RoaAlertSubscriptionData(List.of("joeok@example.com"), Arrays.asList(RouteValidityState.INVALID_ASN, RouteValidityState.INVALID_LENGTH, RouteValidityState.UNKNOWN), RoaAlertFrequency.DAILY)); + 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)); diff --git a/src/test/java/net/ripe/rpki/services/impl/EmailSenderBeanTest.java b/src/test/java/net/ripe/rpki/services/impl/email/EmailSenderBeanTest.java similarity index 64% rename from src/test/java/net/ripe/rpki/services/impl/EmailSenderBeanTest.java rename to src/test/java/net/ripe/rpki/services/impl/email/EmailSenderBeanTest.java index 19ff649..1e71d30 100644 --- a/src/test/java/net/ripe/rpki/services/impl/EmailSenderBeanTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/email/EmailSenderBeanTest.java @@ -1,4 +1,4 @@ -package net.ripe.rpki.services.impl; +package net.ripe.rpki.services.impl.email; import net.ripe.ipresource.Asn; import net.ripe.ipresource.ImmutableResourceSet; @@ -12,7 +12,6 @@ import net.ripe.rpki.server.api.dto.RoaAlertConfigurationData; import net.ripe.rpki.server.api.dto.RoaAlertSubscriptionData; import org.apache.commons.lang3.RandomStringUtils; -import org.apache.commons.lang3.RandomUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,31 +19,34 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mail.MailSender; -import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import jakarta.mail.internet.MimeMessage; import javax.security.auth.x500.X500Principal; import java.security.SecureRandom; import java.util.*; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.Mockito.reset; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class EmailSenderBeanTest { @Mock - private MailSender mailSender; - private ArgumentCaptor messageCapture; + private JavaMailSenderImpl mailSender; + private ArgumentCaptor messageCapture; private EmailSenderBean subject; + private final String rpkiDashboardUri = "http://localhost/unit-testing"; + private final String authUnsubscribeUri = "http://localhost/?originalUrl="; + private final String apiUnsubscribeUri = "http://localhost/api/rpki/unsubscribe-alerts"; + private final String uniqueId = "12345678"; + @BeforeEach public void setUp() { - messageCapture = ArgumentCaptor.forClass(SimpleMailMessage.class); - - subject = new EmailSenderBean(mailSender, "http://localhost/unit-testing"); - + messageCapture = ArgumentCaptor.forClass(MimeMessage.class); + var emails = new EmailTokens("secret", authUnsubscribeUri, apiUnsubscribeUri); + subject = new EmailSenderBean(mailSender, emails, rpkiDashboardUri); System.setProperty(Environment.APPLICATION_ENVIRONMENT_KEY, "junit"); } @@ -54,24 +56,29 @@ public static void tearDown() { } @Test - public void shouldSendEmail() { + void shouldSendEmail() throws Exception { String emailTo = "email@example.com"; var template = EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE; - subject.sendEmail(emailTo, template.templateSubject, template, Collections.singletonMap("field", "value")); + when(mailSender.createMimeMessage()).thenReturn(new JavaMailSenderImpl().createMimeMessage()); + subject.sendEmail(emailTo, template.templateSubject, template, Collections.singletonMap("field", "value"), uniqueId); verify(mailSender).send(messageCapture.capture()); - assertThat(messageCapture.getValue().getTo()[0]).isEqualTo(emailTo); + assertThat(messageCapture.getValue().getAllRecipients()[0].toString()).isEqualTo(emailTo); assertThat(messageCapture.getValue().getSubject()).isEqualTo(template.templateSubject); - assertThat(messageCapture.getValue().getText()).hasSizeGreaterThan(100); + assertThat((String)messageCapture.getValue().getContent()).hasSizeGreaterThan(100); } @Test - public void shouldRenderAllTemplates() { - for (var template : EmailSender.EmailTemplates.values()) { - subject.sendEmail("user@example.org", template.templateSubject, template, variablesFor(template)); + void shouldRenderAllTemplates() throws Exception { + for (var template : List.of(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY, EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY)) { + when(mailSender.createMimeMessage()).thenReturn(new JavaMailSenderImpl().createMimeMessage()); + subject.sendEmail("user@example.org", template.templateSubject, template, variablesFor(template), uniqueId); verify(mailSender).send(messageCapture.capture()); - assertThat(messageCapture.getValue().getText()).isNotBlank(); + String content = (String) messageCapture.getValue().getContent(); + assertThat(content).isNotBlank(); + assertThat(content).contains(rpkiDashboardUri); + assertThat(content).contains(authUnsubscribeUri); reset(mailSender); } } diff --git a/src/test/java/net/ripe/rpki/services/impl/EmailTemplatesTest.java b/src/test/java/net/ripe/rpki/services/impl/email/EmailTemplatesTest.java similarity index 90% rename from src/test/java/net/ripe/rpki/services/impl/EmailTemplatesTest.java rename to src/test/java/net/ripe/rpki/services/impl/email/EmailTemplatesTest.java index 8aa6eb2..5606885 100644 --- a/src/test/java/net/ripe/rpki/services/impl/EmailTemplatesTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/email/EmailTemplatesTest.java @@ -1,14 +1,13 @@ -package net.ripe.rpki.services.impl; +package net.ripe.rpki.services.impl.email; import lombok.extern.slf4j.Slf4j; +import net.ripe.rpki.services.impl.email.EmailSenderBean; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.thymeleaf.TemplateEngine; import org.thymeleaf.context.Context; import java.util.Arrays; -import java.util.List; -import java.util.Map; @Slf4j public class EmailTemplatesTest { 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 617fa44..322fa15 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 @@ -9,7 +9,7 @@ import net.ripe.rpki.domain.alerts.RoaAlertConfigurationRepository; import net.ripe.rpki.domain.alerts.RoaAlertFrequency; import net.ripe.rpki.server.api.commands.SubscribeToRoaAlertCommand; -import net.ripe.rpki.services.impl.EmailSender; +import net.ripe.rpki.services.impl.email.EmailSender; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -17,10 +17,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import java.util.Collection; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; +import java.util.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; @@ -77,7 +74,7 @@ public void shouldCreateRoaAlertSubscriptionAndSendConfirmationEmail() { verify(repository).add(alertCapture.capture()); verify(emailSender).sendEmail(emailCapture.capture(), isA(String.class), - eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY), isA(Map.class)); + eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_WEEKLY), isA(Map.class), isA(String.class)); assertEquals(RoaAlertFrequency.WEEKLY, alertCapture.getValue().getFrequency()); assertEquals(email, emailCapture.getValue()); } @@ -98,7 +95,7 @@ public void shouldUpdateRoaAlertSubscriptionAndNotSendConfirmationEmail() { assertEquals(newValidityStates, configuration.getSubscriptionOrNull().getRouteValidityStates()); verify(emailSender, times(0)).sendEmail(anyString(), eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY.templateSubject), - eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY), isA(Map.class)); + eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY), isA(Map.class), isA(String.class)); } @SuppressWarnings("unchecked") @@ -113,8 +110,9 @@ public void shouldUpdateRoaAlertSubscriptionAndSendConfirmationEmails() { 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), - eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY), isA(Map.class)); + verify(emailSender, times(1)).sendEmail(eq(newEmail), + eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY.templateSubject), + eq(EmailSender.EmailTemplates.ROA_ALERT_SUBSCRIBE_CONFIRMATION_DAILY), isA(Map.class), isA(String.class)); List emails = configuration.getSubscriptionOrNull().getEmails(); assertTrue(emails.contains(oldEmail)); assertTrue(emails.contains(newEmail)); 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 b1d2b60..9480d59 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 @@ -9,7 +9,7 @@ import net.ripe.rpki.domain.alerts.RoaAlertConfigurationRepository; import net.ripe.rpki.domain.alerts.RoaAlertFrequency; import net.ripe.rpki.server.api.commands.UnsubscribeFromRoaAlertCommand; -import net.ripe.rpki.services.impl.EmailSender; +import net.ripe.rpki.services.impl.email.EmailSender; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -70,7 +70,9 @@ public void shouldUpdateRoaSpecificationAndSendConfirmationEmail() { subject.handle(new UnsubscribeFromRoaAlertCommand(TEST_VERSIONED_CA_ID, email)); - verify(emailSender).sendEmail(emailCapture.capture(), eq(EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE.templateSubject), eq(EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE), isA(Map.class)); + verify(emailSender).sendEmail(emailCapture.capture(), + eq(EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE.templateSubject), + eq(EmailSender.EmailTemplates.ROA_ALERT_UNSUBSCRIBE), isA(Map.class), isA(String.class)); assertEquals(email, emailCapture.getValue()); } } diff --git a/src/test/java/net/ripe/rpki/services/impl/jpa/JpaRoaAlertConfigurationRepositoryTest.java b/src/test/java/net/ripe/rpki/services/impl/jpa/JpaRoaAlertConfigurationRepositoryTest.java index 654cd46..54a1b92 100644 --- a/src/test/java/net/ripe/rpki/services/impl/jpa/JpaRoaAlertConfigurationRepositoryTest.java +++ b/src/test/java/net/ripe/rpki/services/impl/jpa/JpaRoaAlertConfigurationRepositoryTest.java @@ -9,14 +9,14 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import java.nio.charset.StandardCharsets; import jakarta.transaction.Transactional; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; -import static net.ripe.rpki.commons.validation.roa.RouteValidityState.INVALID_ASN; -import static net.ripe.rpki.commons.validation.roa.RouteValidityState.INVALID_LENGTH; -import static net.ripe.rpki.commons.validation.roa.RouteValidityState.UNKNOWN; +import static net.ripe.rpki.commons.validation.roa.RouteValidityState.*; import static org.junit.Assert.assertEquals; @Transactional @@ -25,12 +25,10 @@ public class JpaRoaAlertConfigurationRepositoryTest extends CertificationDomainT @Autowired private RoaAlertConfigurationRepository subject; - private ProductionCertificateAuthority ca; - @Before public void setUp() { clearDatabase(); - ca = createInitialisedProdCaWithRipeResources(); + ProductionCertificateAuthority ca = createInitialisedProdCaWithRipeResources(); entityManager.persist(ca); RoaAlertConfiguration weekly = new RoaAlertConfiguration(ca, "weekly@alert", Arrays.asList(INVALID_ASN, INVALID_LENGTH, UNKNOWN), RoaAlertFrequency.WEEKLY); subject.add(weekly); @@ -47,4 +45,11 @@ public void shouldFindAll() { Collection all = subject.findAll(); assertEquals(1, all.size()); } + + @Test + public void shouldFindByEmail() { + List all = subject.findAll().stream().collect(Collectors.toList()); + var c = subject.findByEmail(all.get(0).getSubscriptionOrNull().getEmails().get(0)); + assertEquals(c, all); + } } diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 9664a8e..ef2e796 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -9,8 +9,10 @@ signature: provider: SunRsaSign mail: - host: localhost - port: 1025 + host: localhost + port: 1025 + unsubscribe: + secret: dQl9TJEu3JQMvyEOEwkut4T1zOiHN0lyLYRUWI7q8jWejnDtGT30WrVCGVFmY7iZ spring: datasource: @@ -20,6 +22,7 @@ spring: background-services: schedule.enable: false + system.setup.and.testing.api.enabled: true # **Disable** authentication for the administration web UI. @@ -53,7 +56,6 @@ riswhoisdump.base.url: http://localhost:8080/certification/static/riswhois/ # Application specific settings # # bcrypt.using(rounds=14).hash("test") -authorization.admin.role: ROLE_ANONYMOUS api-keys.properties: "classpath:/test-api-keys.properties" intermediate.ca: @@ -105,3 +107,5 @@ non-hosted: token: "krill-dev-token" certificate.authority.invariant.checking.enabled: true + +