From dd58d88a849d2e002f03b757a5804d1b4ce97d3d Mon Sep 17 00:00:00 2001 From: rmiccoli Date: Thu, 2 May 2024 14:46:53 +0200 Subject: [PATCH] Add AUP reminder and user suspension/restore messages --- .../iam/api/aup/AupSignatureController.java | 1 - .../mw/iam/api/aup/DefaultAupService.java | 1 + .../mw/iam/api/aup/model/AupConverter.java | 4 +- .../it/infn/mw/iam/api/aup/model/AupDTO.java | 19 ++- .../aup/model/AupRemindersAndSignature.java | 38 +++++ .../AupRemindersAndSignatureValidator.java | 104 ++++++++++++ .../provisioning/ScimUserProvisioning.java | 18 +- .../it/infn/mw/iam/config/TaskConfig.java | 9 + .../core/user/DefaultIamAccountService.java | 8 +- .../mw/iam/core/web/aup/AupReminderTask.java | 118 ++++++++++++++ .../iam/notification/NotificationFactory.java | 13 +- .../TransientNotificationFactory.java | 124 +++++++++++++- .../email-templates/accountRestored.ftl | 7 + .../email-templates/accountSuspended.ftl | 9 + .../email-templates/aupExpirationMessage.ftl | 12 ++ .../email-templates/aupSignatureRequest.ftl | 9 + .../email-templates/signAupReminder.ftl | 15 ++ .../components/aup/aup.component.html | 9 + .../components/aup/aup.component.js | 6 +- .../components/aup/aup.create.dialog.html | 12 +- .../components/aup/aup.edit.dialog.html | 10 ++ .../iam/test/api/aup/AupIntegrationTests.java | 154 +++++++++++++++++- .../test/api/aup/AupReminderTaskTests.java | 154 ++++++++++++++++++ .../mw/iam/test/api/aup/AupTestSupport.java | 1 + .../it/infn/mw/iam/test/login/LoginTests.java | 1 + .../test/oauth/RefreshTokenGranterTests.java | 1 + ...ResourceOwnerPasswordCredentialsTests.java | 1 + .../mw/iam/test/oauth/TokenExchangeTests.java | 1 + .../authzcode/AuthorizationCodeTests.java | 1 + .../repository/IamAupRepositoryTests.java | 1 + .../test/scim/user/ScimUserCreationTests.java | 2 +- .../test/service/IamAccountServiceTests.java | 6 +- .../infn/mw/iam/core/IamNotificationType.java | 2 +- .../infn/mw/iam/persistence/model/IamAup.java | 11 ++ .../repository/IamAccountRepository.java | 1 - .../repository/IamAupSignatureRepository.java | 14 +- .../IamEmailNotificationRepository.java | 13 ++ .../migration/h2/V105__add_aup_reminders.sql | 1 + .../db/migration/h2/V19__aup_tables.sql | 2 +- .../mysql/V105__add_aup_reminders.sql | 1 + .../java/it/infn/mw/voms/VomsAcTests.java | 1 + 41 files changed, 882 insertions(+), 33 deletions(-) create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupRemindersAndSignature.java create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupRemindersAndSignatureValidator.java create mode 100644 iam-login-service/src/main/java/it/infn/mw/iam/core/web/aup/AupReminderTask.java create mode 100644 iam-login-service/src/main/resources/email-templates/accountRestored.ftl create mode 100644 iam-login-service/src/main/resources/email-templates/accountSuspended.ftl create mode 100644 iam-login-service/src/main/resources/email-templates/aupExpirationMessage.ftl create mode 100644 iam-login-service/src/main/resources/email-templates/aupSignatureRequest.ftl create mode 100644 iam-login-service/src/main/resources/email-templates/signAupReminder.ftl create mode 100644 iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupReminderTaskTests.java create mode 100644 iam-persistence/src/main/resources/db/migration/h2/V105__add_aup_reminders.sql create mode 100644 iam-persistence/src/main/resources/db/migration/mysql/V105__add_aup_reminders.sql diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/AupSignatureController.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/AupSignatureController.java index d6f7ebe7b..5780e1a00 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/AupSignatureController.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/AupSignatureController.java @@ -69,7 +69,6 @@ public class AupSignatureController { private final IamAupRepository aupRepo; private final TimeProvider timeProvider; private final ApplicationEventPublisher eventPublisher; - public AupSignatureController(AupSignatureConverter conv, AccountUtils utils, IamAupSignatureRepository signatureRepo, IamAupRepository aupRepo, TimeProvider timeProvider, ApplicationEventPublisher publisher) { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/DefaultAupService.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/DefaultAupService.java index d6f53975c..2675030a5 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/DefaultAupService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/DefaultAupService.java @@ -90,6 +90,7 @@ public IamAup updateAup(AupDTO aupDto) { aup.setDescription(aupDto.getDescription()); aup.setUrl(aupDto.getUrl()); aup.setSignatureValidityInDays(aupDto.getSignatureValidityInDays()); + aup.setAupRemindersInDays(aupDto.getAupRemindersInDays()); /* * Due to transition from text to URL, when updating the AUP only URL is considered while text * is ignored and set to null diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupConverter.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupConverter.java index d23a2b025..0fec79bd3 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupConverter.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupConverter.java @@ -32,13 +32,15 @@ public IamAup entityFromDto(AupDTO dto) { aup.setSignatureValidityInDays(dto.getSignatureValidityInDays()); aup.setUrl(dto.getUrl()); aup.setText(dto.getText()); + aup.setAupRemindersInDays(dto.getAupRemindersInDays()); return aup; } @Override public AupDTO dtoFromEntity(IamAup entity) { return new AupDTO(entity.getUrl(), entity.getText(), entity.getDescription(), - entity.getSignatureValidityInDays(), entity.getCreationTime(), entity.getLastUpdateTime()); + entity.getSignatureValidityInDays(), entity.getCreationTime(), entity.getLastUpdateTime(), + entity.getAupRemindersInDays()); } } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupDTO.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupDTO.java index af404ab61..f0cbf4aee 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupDTO.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupDTO.java @@ -17,9 +17,7 @@ import java.util.Date; -import javax.validation.constraints.Min; import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.URL; @@ -30,6 +28,7 @@ import it.infn.mw.iam.api.scim.controller.utils.JsonDateSerializer; import it.infn.mw.iam.api.validators.NoQueryParamsUrl; +@AupRemindersAndSignature public class AupDTO { @NotBlank(message = "Invalid AUP: the AUP URL cannot be blank") @@ -43,10 +42,10 @@ public class AupDTO { message = "Invalid AUP: the description string must be at most 128 characters long") String description; - @NotNull(message = "Invalid AUP: signatureValidityInDays is required") - @Min(value = 0L, message = "Invalid AUP: signatureValidityInDays must be >= 0") Long signatureValidityInDays; + String aupRemindersInDays = "30,15,1"; + @JsonSerialize(using = JsonDateSerializer.class) Date creationTime; @@ -57,13 +56,15 @@ public AupDTO(@JsonProperty("url") String url, @JsonProperty("text") String text @JsonProperty("description") String description, @JsonProperty("signatureValidityInDays") Long signatureValidityInDays, @JsonProperty("creationTime") Date creationTime, - @JsonProperty("lastUpdateTime") Date lastUpdateTime) { + @JsonProperty("lastUpdateTime") Date lastUpdateTime, + @JsonProperty("aupRemindersInDays") String aupRemindersInDays) { this.url = url; this.description = description; this.signatureValidityInDays = signatureValidityInDays; this.creationTime = creationTime; this.lastUpdateTime = lastUpdateTime; this.text = text; + this.aupRemindersInDays = aupRemindersInDays; } public String getDescription() { @@ -101,6 +102,14 @@ public void setSignatureValidityInDays(Long signatureValidityInDays) { this.signatureValidityInDays = signatureValidityInDays; } + public String getAupRemindersInDays() { + return aupRemindersInDays; + } + + public void setAupRemindersInDays(String aupRemindersInDays) { + this.aupRemindersInDays = aupRemindersInDays; + } + public Date getCreationTime() { return creationTime; diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupRemindersAndSignature.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupRemindersAndSignature.java new file mode 100644 index 000000000..af121c57d --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupRemindersAndSignature.java @@ -0,0 +1,38 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.aup.model; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.validation.Constraint; +import javax.validation.Payload; + +@Retention(RUNTIME) +@Target({TYPE}) +@Constraint(validatedBy = AupRemindersAndSignatureValidator.class) +public @interface AupRemindersAndSignature { + + String message() default "Invalid AUP"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupRemindersAndSignatureValidator.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupRemindersAndSignatureValidator.java new file mode 100644 index 000000000..2609699d9 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/aup/model/AupRemindersAndSignatureValidator.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.api.aup.model; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class AupRemindersAndSignatureValidator implements ConstraintValidator { + + @Override + public boolean isValid(AupDTO value, ConstraintValidatorContext context) { + + Long signatureValidityInDays = value.getSignatureValidityInDays(); + String aupRemindersInDays = value.getAupRemindersInDays(); + + if (signatureValidityInDays == null) { + context.disableDefaultConstraintViolation(); + context + .buildConstraintViolationWithTemplate("Invalid AUP: signatureValidityInDays is required") + .addPropertyNode("signatureValidityInDays") + .addConstraintViolation(); + return false; + } + + if (signatureValidityInDays < 0) { + context.disableDefaultConstraintViolation(); + context + .buildConstraintViolationWithTemplate("Invalid AUP: signatureValidityInDays must be >= 0") + .addPropertyNode("signatureValidityInDays") + .addConstraintViolation(); + return false; + } + + if (aupRemindersInDays == null || aupRemindersInDays.isEmpty()) { + context.disableDefaultConstraintViolation(); + context + .buildConstraintViolationWithTemplate( + "Invalid AUP: aupRemindersInDays cannot be empty or null") + .addConstraintViolation(); + return false; + } + + try { + List numbers = Arrays.stream(aupRemindersInDays.split(",")) + .map(String::trim) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + if (numbers.stream().anyMatch(i -> i <= 0)) { + context.disableDefaultConstraintViolation(); + context + .buildConstraintViolationWithTemplate( + "Invalid AUP: zero or negative values for reminders are not allowed") + .addConstraintViolation(); + return false; + } + + if (numbers.stream().anyMatch(i -> i >= signatureValidityInDays)) { + context.disableDefaultConstraintViolation(); + context + .buildConstraintViolationWithTemplate( + "Invalid AUP: aupRemindersInDays must be smaller than signatureValidityInDays") + .addConstraintViolation(); + return false; + } + + Set uniqueNumbers = new HashSet<>(numbers); + if (uniqueNumbers.size() != numbers.size()) { + context.disableDefaultConstraintViolation(); + context + .buildConstraintViolationWithTemplate("Invalid AUP: duplicate values for reminders are not allowed") + .addConstraintViolation(); + return false; + } + + return true; + } catch (NumberFormatException e) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate("Invalid AUP: non-integer value found") + .addConstraintViolation(); + return false; + } + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimUserProvisioning.java b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimUserProvisioning.java index 8e1d7fdf4..ad3931674 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimUserProvisioning.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/api/scim/provisioning/ScimUserProvisioning.java @@ -65,6 +65,7 @@ import it.infn.mw.iam.core.user.IamAccountService; import it.infn.mw.iam.core.user.exception.CredentialAlreadyBoundException; import it.infn.mw.iam.core.user.exception.UserAlreadyExistsException; +import it.infn.mw.iam.notification.NotificationFactory; import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.repository.IamAccountRepository; import it.infn.mw.iam.registration.validation.UsernameValidator; @@ -84,6 +85,7 @@ public class ScimUserProvisioning private final IamAccountRepository accountRepository; private final UserConverter userConverter; private final DefaultAccountUpdaterFactory updatersFactory; + private final NotificationFactory notificationFactory; private ApplicationEventPublisher eventPublisher; @@ -91,12 +93,13 @@ public ScimUserProvisioning(IamAccountService accountService, OAuth2TokenEntityService tokenService, IamAccountRepository accountRepository, PasswordEncoder passwordEncoder, UserConverter userConverter, OidcIdConverter oidcIdConverter, SamlIdConverter samlIdConverter, SshKeyConverter sshKeyConverter, - X509CertificateConverter x509CertificateConverter, - UsernameValidator usernameValidator) { + X509CertificateConverter x509CertificateConverter, UsernameValidator usernameValidator, + NotificationFactory notificationFactory) { this.accountService = accountService; this.accountRepository = accountRepository; this.userConverter = userConverter; + this.notificationFactory = notificationFactory; this.updatersFactory = new DefaultAccountUpdaterFactory(passwordEncoder, accountRepository, accountService, tokenService, oidcIdConverter, samlIdConverter, sshKeyConverter, x509CertificateConverter, usernameValidator); @@ -266,11 +269,22 @@ private void executePatchOperation(IamAccount account, ScimPatchOperation> operations) { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/config/TaskConfig.java b/iam-login-service/src/main/java/it/infn/mw/iam/config/TaskConfig.java index a9943bfc4..9ce296cc9 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/config/TaskConfig.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/config/TaskConfig.java @@ -37,6 +37,7 @@ import it.infn.mw.iam.config.lifecycle.LifecycleProperties; import it.infn.mw.iam.core.lifecycle.ExpiredAccountsHandler; import it.infn.mw.iam.core.user.IamAccountService; +import it.infn.mw.iam.core.web.aup.AupReminderTask; import it.infn.mw.iam.core.web.wellknown.IamWellKnownInfoProvider; import it.infn.mw.iam.notification.NotificationDelivery; import it.infn.mw.iam.notification.NotificationDeliveryTask; @@ -84,6 +85,9 @@ public class TaskConfig implements SchedulingConfigurer { @Autowired ExpiredAccountsHandler expiredAccountsHandler; + @Autowired + AupReminderTask aupReminderTask; + @Autowired CacheManager cacheManager; @@ -127,6 +131,11 @@ public void clearExpiredDeviceCodes() { deviceCodeService.clearExpiredDeviceCodes(); } + @Scheduled(fixedRateString = "${task.aupReminder:14400}", timeUnit = TimeUnit.SECONDS) + public void scheduledAupRemindersTask() { + aupReminderTask.sendAupReminders(); + } + public void schedulePendingNotificationsDelivery(final ScheduledTaskRegistrar taskRegistrar) { if (notificationTaskPeriodMsec < 0) { diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/user/DefaultIamAccountService.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/DefaultIamAccountService.java index 061fc0351..e2eb9911c 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/core/user/DefaultIamAccountService.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/user/DefaultIamAccountService.java @@ -54,6 +54,7 @@ import it.infn.mw.iam.core.user.exception.CredentialAlreadyBoundException; import it.infn.mw.iam.core.user.exception.InvalidCredentialException; import it.infn.mw.iam.core.user.exception.UserAlreadyExistsException; +import it.infn.mw.iam.notification.NotificationFactory; import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamAccountGroupMembership; import it.infn.mw.iam.persistence.model.IamAttribute; @@ -81,11 +82,13 @@ public class DefaultIamAccountService implements IamAccountService, ApplicationE private ApplicationEventPublisher eventPublisher; private final OAuth2TokenEntityService tokenService; private final IamAccountClientRepository accountClientRepo; + private final NotificationFactory notificationFactory; public DefaultIamAccountService(Clock clock, IamAccountRepository accountRepo, IamGroupRepository groupRepo, IamAuthoritiesRepository authoritiesRepo, PasswordEncoder passwordEncoder, ApplicationEventPublisher eventPublisher, - OAuth2TokenEntityService tokenService, IamAccountClientRepository accountClientRepo) { + OAuth2TokenEntityService tokenService, IamAccountClientRepository accountClientRepo, + NotificationFactory notificationFactory) { this.clock = clock; this.accountRepo = accountRepo; @@ -95,6 +98,7 @@ public DefaultIamAccountService(Clock clock, IamAccountRepository accountRepo, this.eventPublisher = eventPublisher; this.tokenService = tokenService; this.accountClientRepo = accountClientRepo; + this.notificationFactory = notificationFactory; } private void labelSetEvent(IamAccount account, IamLabel label) { @@ -403,6 +407,7 @@ public IamAccount disableAccount(IamAccount account) { account.touch(); accountRepo.save(account); eventPublisher.publishEvent(new AccountDisabledEvent(this, account)); + notificationFactory.createAccountSuspendedMessage(account); return account; } @@ -412,6 +417,7 @@ public IamAccount restoreAccount(IamAccount account) { account.touch(); accountRepo.save(account); eventPublisher.publishEvent(new AccountRestoredEvent(this, account)); + notificationFactory.createAccountRestoredMessage(account); return account; } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/core/web/aup/AupReminderTask.java b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/aup/AupReminderTask.java new file mode 100644 index 000000000..52d8bcf41 --- /dev/null +++ b/iam-login-service/src/main/java/it/infn/mw/iam/core/web/aup/AupReminderTask.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.core.web.aup; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import it.infn.mw.iam.notification.NotificationFactory; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamAup; +import it.infn.mw.iam.persistence.model.IamAupSignature; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; +import it.infn.mw.iam.persistence.repository.IamAupRepository; +import it.infn.mw.iam.persistence.repository.IamAupSignatureRepository; +import it.infn.mw.iam.persistence.repository.IamEmailNotificationRepository; + +@Component +public class AupReminderTask { + + @Autowired + IamAccountRepository accounts; + + @Autowired + IamAupRepository aupRepo; + + @Autowired + NotificationFactory notification; + + @Autowired + IamAupSignatureRepository aupSignatureRepo; + + @Autowired + IamEmailNotificationRepository emailNotificationRepo; + + public void sendAupReminders() { + aupRepo.findDefaultAup().ifPresent(aup -> { + LocalDate currentDate = LocalDate.now(); + LocalDate expirationDate = currentDate.minusDays(aup.getSignatureValidityInDays()); + Date expirationDateAsDate = toDate(expirationDate); + Date expirationDatePlusOneDayAsDate = toDate(expirationDate.plusDays(1)); + List reminderIntervals = parseReminderIntervals(aup.getAupRemindersInDays()); + + reminderIntervals.forEach( + interval -> processRemindersForInterval(aup, currentDate, interval, expirationDate)); + + List expiredSignatures = aupSignatureRepo.findByAupAndSignatureTime(aup, + expirationDateAsDate, expirationDatePlusOneDayAsDate); + + // check if an email of type AUP_EXPIRATION does not already exist, because it is never deleted + expiredSignatures.forEach(s -> { + if (isExpiredSignatureEmailNotAlreadySentFor(s.getAccount())) { + notification.createAupSignatureExpMessage(s.getAccount()); + } + }); + }); + } + + private void processRemindersForInterval(IamAup aup, LocalDate currentDate, Integer interval, + LocalDate expirationDate) { + LocalDate reminderDate = expirationDate.plusDays(interval); + Date reminderDateAsDate = toDate(reminderDate); + Date reminderDatePlusOneAsDate = toDate(reminderDate.plusDays(1)); + Date tomorrowAsDate = toDate(currentDate.plusDays(1)); + + List signatures = aupSignatureRepo.findByAupAndSignatureTime(aup, + reminderDateAsDate, reminderDatePlusOneAsDate); + + // check if an email of type AUP_REMINDER does not already exist, because it is never deleted + signatures.forEach(s -> { + if (isAupReminderEmailNotAlreadySentFor(s.getAccount(), tomorrowAsDate)) { + notification.createAupReminderMessage(s.getAccount(), aup); + } + }); + } + + public boolean isExpiredSignatureEmailNotAlreadySentFor(IamAccount account) { + return emailNotificationRepo + .countAupExpirationMessPerAccount(account.getUserInfo().getEmail()) == 0; + } + + public boolean isAupReminderEmailNotAlreadySentFor(IamAccount account, Date tomorrowAsDate) { + return emailNotificationRepo.countAupRemindersPerAccount(account.getUserInfo().getEmail(), + tomorrowAsDate) == 0; + } + + private Date toDate(LocalDate localDate) { + return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + private static List parseReminderIntervals(String aupRemindersInDays) { + List result = new ArrayList<>(); + String[] parts = aupRemindersInDays.split("\\s*,\\s*"); + for (String part : parts) { + result.add(Integer.parseInt(part.trim())); + } + return result; + } + +} diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/notification/NotificationFactory.java b/iam-login-service/src/main/java/it/infn/mw/iam/notification/NotificationFactory.java index 7d2536a11..a16359b05 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/notification/NotificationFactory.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/notification/NotificationFactory.java @@ -21,6 +21,7 @@ import org.mitre.oauth2.model.ClientDetailsEntity; import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamAup; import it.infn.mw.iam.persistence.model.IamEmailNotification; import it.infn.mw.iam.persistence.model.IamGroupRequest; import it.infn.mw.iam.persistence.model.IamRegistrationRequest; @@ -31,7 +32,8 @@ public interface NotificationFactory { IamEmailNotification createAccountActivatedMessage(IamRegistrationRequest request); - IamEmailNotification createRequestRejectedMessage(IamRegistrationRequest request, Optional motivation); + IamEmailNotification createRequestRejectedMessage(IamRegistrationRequest request, + Optional motivation); IamEmailNotification createAdminHandleRequestMessage(IamRegistrationRequest request); @@ -46,4 +48,13 @@ public interface NotificationFactory { IamEmailNotification createClientStatusChangedMessageFor(ClientDetailsEntity client, List accounts); + IamEmailNotification createAupReminderMessage(IamAccount account, IamAup aup); + + IamEmailNotification createAupSignatureExpMessage(IamAccount account); + + IamEmailNotification createAupSignatureRequestMessage(IamAccount account); + + IamEmailNotification createAccountSuspendedMessage(IamAccount account); + + IamEmailNotification createAccountRestoredMessage(IamAccount account); } diff --git a/iam-login-service/src/main/java/it/infn/mw/iam/notification/TransientNotificationFactory.java b/iam-login-service/src/main/java/it/infn/mw/iam/notification/TransientNotificationFactory.java index b25dcec60..1cef9174f 100644 --- a/iam-login-service/src/main/java/it/infn/mw/iam/notification/TransientNotificationFactory.java +++ b/iam-login-service/src/main/java/it/infn/mw/iam/notification/TransientNotificationFactory.java @@ -18,7 +18,9 @@ import static java.util.Arrays.asList; import java.io.IOException; -import java.util.ArrayList; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -35,6 +37,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.ui.freemarker.FreeMarkerTemplateUtils; +import com.google.common.collect.Lists; + import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; @@ -44,6 +48,7 @@ import it.infn.mw.iam.notification.service.resolver.AdminNotificationDeliveryStrategy; import it.infn.mw.iam.notification.service.resolver.GroupManagerNotificationDeliveryStrategy; import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamAup; import it.infn.mw.iam.persistence.model.IamEmailNotification; import it.infn.mw.iam.persistence.model.IamGroupRequest; import it.infn.mw.iam.persistence.model.IamNotificationReceiver; @@ -57,6 +62,8 @@ public class TransientNotificationFactory implements NotificationFactory { private static final String USERNAME_FIELD = "username"; private static final String GROUPNAME_FIELD = "groupName"; private static final String MOTIVATION_FIELD = "motivation"; + private static final String AUP_PATH = "%s/iam/aup/sign"; + private static final String AUP_URL = "aupUrl"; @Value("${iam.baseUrl}") private String baseUrl; @@ -271,7 +278,7 @@ public IamEmailNotification createClientStatusChangedMessageFor(ClientDetailsEnt recipients.add(a.getUserInfo().getEmail()); } - List emails = new ArrayList<>(recipients); + List emails = Lists.newArrayList(recipients); if (emails.isEmpty()) { LOG.warn("No email to send notification to for client {}", client.getClientId()); @@ -286,6 +293,119 @@ public IamEmailNotification createClientStatusChangedMessageFor(ClientDetailsEnt return notification; } + @Override + public IamEmailNotification createAupReminderMessage(IamAccount account, IamAup aup) { + String recipient = account.getUserInfo().getName(); + String aupUrl = String.format(AUP_PATH, baseUrl); + + LocalDate now = LocalDate.now(); + long signatureValidityInDays = aup.getSignatureValidityInDays(); + LocalDate signatureTime = account.getAupSignature() + .getSignatureTime() + .toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate(); + LocalDate signatureValidTime = signatureTime.plusDays(signatureValidityInDays); + long missingDays = ChronoUnit.DAYS.between(now, signatureValidTime); + + Map model = new HashMap<>(); + model.put(RECIPIENT_FIELD, recipient); + model.put(AUP_URL, aupUrl); + model.put(ORGANISATION_NAME, organisationName); + model.put("missingDays", missingDays); + + String subject = "AUP signature reminder"; + + IamEmailNotification notification = createMessage("signAupReminder.ftl", model, + IamNotificationType.AUP_REMINDER, subject, asList(account.getUserInfo().getEmail())); + + LOG.debug("Created reminder message for signing the account {} AUP. Signing URL: {}", + account.getUuid(), aupUrl); + + return notification; + } + + @Override + public IamEmailNotification createAupSignatureExpMessage(IamAccount account) { + String recipient = account.getUserInfo().getName(); + String aupUrl = String.format(AUP_PATH, baseUrl); + + Map model = new HashMap<>(); + model.put(RECIPIENT_FIELD, recipient); + model.put(AUP_URL, aupUrl); + model.put(ORGANISATION_NAME, organisationName); + + String subject = "AUP signature expiration"; + + IamEmailNotification notification = createMessage("aupExpirationMessage.ftl", model, + IamNotificationType.AUP_EXPIRATION, subject, asList(account.getUserInfo().getEmail())); + + LOG.debug("Created AUP expiration message for the account {}. AUP signing URL: {}", + account.getUuid(), aupUrl); + + return notification; + + } + + @Override + public IamEmailNotification createAupSignatureRequestMessage(IamAccount account) { + String recipient = account.getUserInfo().getName(); + String aupUrl = String.format(AUP_PATH, baseUrl); + + Map model = new HashMap<>(); + model.put(RECIPIENT_FIELD, recipient); + model.put(AUP_URL, aupUrl); + model.put(ORGANISATION_NAME, organisationName); + + String subject = "AUP signature request"; + + IamEmailNotification notification = + createMessage("aupSignatureRequest.ftl", model, IamNotificationType.AUP_SIGNATURE_REQUEST, + subject, asList(account.getUserInfo().getEmail())); + + LOG.debug("Created AUP signature request message for the account {}. AUP signing URL: {}", + account.getUuid(), aupUrl); + + return notification; + } + + @Override + public IamEmailNotification createAccountSuspendedMessage(IamAccount account) { + String recipient = account.getUserInfo().getName(); + + Map model = new HashMap<>(); + model.put(RECIPIENT_FIELD, recipient); + model.put(ORGANISATION_NAME, organisationName); + + String subject = "Account suspended"; + + IamEmailNotification notification = createMessage("accountSuspended.ftl", model, + IamNotificationType.ACCOUNT_SUSPENDED, subject, asList(account.getUserInfo().getEmail())); + + LOG.debug("Created suspension message for the account {}", account.getUuid()); + + return notification; + } + + @Override + public IamEmailNotification createAccountRestoredMessage(IamAccount account) { + String recipient = account.getUserInfo().getName(); + + Map model = new HashMap<>(); + model.put(RECIPIENT_FIELD, recipient); + model.put(ORGANISATION_NAME, organisationName); + + String subject = "Account restored"; + + IamEmailNotification notification = createMessage("accountRestored.ftl", model, + IamNotificationType.ACCOUNT_RESTORED, subject, asList(account.getUserInfo().getEmail())); + + LOG.debug("Created restoration message for the account {}", account.getUuid()); + + return notification; + + } + protected IamEmailNotification createMessage(String templateName, Map model, IamNotificationType messageType, String subject, List receiverAddress) { diff --git a/iam-login-service/src/main/resources/email-templates/accountRestored.ftl b/iam-login-service/src/main/resources/email-templates/accountRestored.ftl new file mode 100644 index 000000000..1656dc8a9 --- /dev/null +++ b/iam-login-service/src/main/resources/email-templates/accountRestored.ftl @@ -0,0 +1,7 @@ +Dear ${recipient}, + +this mail is to inform that your account in ${organisationName} has been restored. + +You will be able to obtain JWT tokens/VOMS credentials again. + +The ${organisationName} registration service diff --git a/iam-login-service/src/main/resources/email-templates/accountSuspended.ftl b/iam-login-service/src/main/resources/email-templates/accountSuspended.ftl new file mode 100644 index 000000000..117a9217b --- /dev/null +++ b/iam-login-service/src/main/resources/email-templates/accountSuspended.ftl @@ -0,0 +1,9 @@ +Dear ${recipient}, + +this mail is to inform that your account in ${organisationName} has been suspended. + +You will not be able to obtain JWT tokens/VOMS credentials anymore. + +Please contact administrators for any questions. + +The ${organisationName} registration service diff --git a/iam-login-service/src/main/resources/email-templates/aupExpirationMessage.ftl b/iam-login-service/src/main/resources/email-templates/aupExpirationMessage.ftl new file mode 100644 index 000000000..9670b6184 --- /dev/null +++ b/iam-login-service/src/main/resources/email-templates/aupExpirationMessage.ftl @@ -0,0 +1,12 @@ +Dear ${recipient}, + +you failed to sign the Acceptable Usage Policy (AUP) in time. + +You will NOT be able to obtain JWT tokens/VOMS credentials +for the ${organisationName} organization until you explicitly accept the AUP. + +To sign the AUP, point your browser to the following URL: + +${aupUrl} + +The ${organisationName} registration service diff --git a/iam-login-service/src/main/resources/email-templates/aupSignatureRequest.ftl b/iam-login-service/src/main/resources/email-templates/aupSignatureRequest.ftl new file mode 100644 index 000000000..d8840c63d --- /dev/null +++ b/iam-login-service/src/main/resources/email-templates/aupSignatureRequest.ftl @@ -0,0 +1,9 @@ +Dear ${recipient}, + +an administrator requested that you re-sign the Acceptable Usage Policy (AUP). + +To sign the AUP, point your browser to the following URL: + +${aupUrl} + +The ${organisationName} registration service diff --git a/iam-login-service/src/main/resources/email-templates/signAupReminder.ftl b/iam-login-service/src/main/resources/email-templates/signAupReminder.ftl new file mode 100644 index 000000000..f5c387743 --- /dev/null +++ b/iam-login-service/src/main/resources/email-templates/signAupReminder.ftl @@ -0,0 +1,15 @@ +Dear ${recipient}, + +we kindly remind you to sign the Acceptable Usage Policy (AUP). + +The AUP will expire in ${missingDays} days. + +After this date you will NOT be able to obtain JWT tokens/VOMS credentials +for the ${organisationName} organization. + +Note, however, that you can sign the AUP at *any* time +by simply pointing your browser to the following URL: + +${aupUrl} + +The ${organisationName} registration service diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.component.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.component.html index 0980bae8a..b6d4ee8b1 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.component.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.component.html @@ -67,6 +67,15 @@

If set to zero, the AUP signature will be asked only at registration time. +
+ +

+ {{$ctrl.aup.data.aupRemindersInDays}} +

+ + Indicate a sequence of three days representing how many days before the AUP expiration reminder messages must be sent. + +
diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.component.js b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.component.js index 22836b6a7..e55a0823d 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.component.js +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.component.js @@ -73,7 +73,8 @@ self.reset = function() { self.aupVal = { url: self.aup.url, - signatureValidityInDays: self.aup.signatureValidityInDays + signatureValidityInDays: self.aup.signatureValidityInDays, + aupRemindersInDays: self.aup.aupRemindersInDays }; }; @@ -104,7 +105,8 @@ self.reset = function() { self.aupVal = { url: "", - signatureValidityInDays: 0 + signatureValidityInDays: 0, + aupRemindersInDays: "30,15,1" }; }; diff --git a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.create.dialog.html b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.create.dialog.html index c0cc0ac8e..a436247a0 100644 --- a/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.create.dialog.html +++ b/iam-login-service/src/main/webapp/resources/iam/apps/dashboard-app/components/aup/aup.create.dialog.html @@ -45,10 +45,20 @@
+
+ + + + Indicate a sequence of comma-separated numbers representing how many days before the AUP expiration reminder messages must be sent. + + + Required input + +
+
+ + + + Indicate a sequence of comma-separated numbers representing how many days before the AUP expiration reminder messages must be sent. + + + Required input + +

Editing the AUP will not trigger an AUP signature diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupIntegrationTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupIntegrationTests.java index 2033f7b87..b6d891540 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupIntegrationTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupIntegrationTests.java @@ -15,8 +15,8 @@ */ package it.infn.mw.iam.test.api.aup; -import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; @@ -125,7 +125,9 @@ public void aupIsReturnedIfDefined() throws Exception { @Test public void aupCreationRequiresAuthenticatedUser() throws JsonProcessingException, Exception { Date now = new Date(); - AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, DEFAULT_AUP_DESC, -1L, now, now); + String reminders = "1,15,30"; + AupDTO aup = + new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, DEFAULT_AUP_DESC, -1L, now, now, reminders); mvc .perform( @@ -138,7 +140,9 @@ public void aupCreationRequiresAuthenticatedUser() throws JsonProcessingExceptio @WithMockUser(username = "test", roles = {"USER"}) public void aupCreationRequiresAdminPrivileges() throws JsonProcessingException, Exception { Date now = new Date(); - AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, DEFAULT_AUP_DESC, -1L, now, now); + String reminders = "1,15,30"; + AupDTO aup = + new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, DEFAULT_AUP_DESC, -1L, now, now, reminders); mvc .perform( @@ -224,11 +228,11 @@ public void aupDescriptionNoLongerThan128Chars() throws JsonProcessingException, } - @Test @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) public void aupCreationRequiresSignatureValidityDays() throws JsonProcessingException, Exception { - AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, null, null, null); + String reminders = "1,15,30"; + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, null, null, null, reminders); Date now = new Date(); mockTimeProvider.setTime(now.getTime()); @@ -244,7 +248,8 @@ public void aupCreationRequiresSignatureValidityDays() throws JsonProcessingExce @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) public void aupCreationRequiresPositiveSignatureValidityDays() throws JsonProcessingException, Exception { - AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, -1L, null, null); + String reminders = "1,15,30"; + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, -1L, null, null, reminders); Date now = new Date(); mockTimeProvider.setTime(now.getTime()); @@ -255,6 +260,109 @@ public void aupCreationRequiresPositiveSignatureValidityDays() .andExpect(jsonPath("$.error").value("Invalid AUP: signatureValidityInDays must be >= 0")); } + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void aupCreationRequiresAupRemindersInDays() throws Exception { + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, 3L, null, null, null); + Date now = new Date(); + mockTimeProvider.setTime(now.getTime()); + + mvc + .perform( + post("/iam/aup").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(aup))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("Invalid AUP: aupRemindersInDays cannot be empty or null")); + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void aupCreationRequiresAupRemindersInDaysNotEmpty() throws Exception { + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, 3L, null, null, ""); + Date now = new Date(); + mockTimeProvider.setTime(now.getTime()); + + mvc + .perform( + post("/iam/aup").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(aup))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("Invalid AUP: aupRemindersInDays cannot be empty or null")); + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void aupCreationRequiresNoZeroInAupRemindersInDays() throws Exception { + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, 3L, null, null, "0"); + Date now = new Date(); + mockTimeProvider.setTime(now.getTime()); + + mvc + .perform( + post("/iam/aup").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(aup))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value( + "Invalid AUP: zero or negative values for reminders are not allowed")); + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void aupCreationRequiresPositiveAupRemindersInDays() throws Exception { + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, 3L, null, null, "-22"); + Date now = new Date(); + mockTimeProvider.setTime(now.getTime()); + + mvc + .perform( + post("/iam/aup").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(aup))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value( + "Invalid AUP: zero or negative values for reminders are not allowed")); + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void aupCreationRequiresNoLettersInAupRemindersInDays() throws Exception { + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, 3L, null, null, "ciao"); + Date now = new Date(); + mockTimeProvider.setTime(now.getTime()); + + mvc + .perform( + post("/iam/aup").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(aup))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value( + "Invalid AUP: non-integer value found")); + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void aupCreationRequiresNoDuplicationInAupRemindersInDays() throws Exception { + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, 31L, null, null, "30,15,15"); + Date now = new Date(); + mockTimeProvider.setTime(now.getTime()); + + mvc + .perform( + post("/iam/aup").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(aup))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value( + "Invalid AUP: duplicate values for reminders are not allowed")); + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void aupCreationRequiresAupRemindersInDaysSmallerThanAupExpirationDays() throws Exception { + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, 3L, null, null, "4"); + Date now = new Date(); + mockTimeProvider.setTime(now.getTime()); + + mvc + .perform( + post("/iam/aup").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(aup))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value( + "Invalid AUP: aupRemindersInDays must be smaller than signatureValidityInDays")); + } + @Test @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) public void aupCreationWorks() throws JsonProcessingException, Exception { @@ -285,6 +393,36 @@ public void aupCreationWorks() throws JsonProcessingException, Exception { assertThat(createdAup.getLastUpdateTime(), creationAndLastUpdateTimeMatcher); } + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void whiteSpacesAllowedAmongAupRemindersDays() throws Exception { + AupDTO aup = new AupDTO(DEFAULT_AUP_URL, DEFAULT_AUP_TEXT, null, 31L, null, null, " 30, 15, 7 "); + + Date now = new Date(); + mockTimeProvider.setTime(now.getTime()); + + mvc + .perform( + post("/iam/aup").contentType(APPLICATION_JSON).content(mapper.writeValueAsString(aup))) + .andExpect(status().isCreated()); + + + String aupJson = mvc.perform(get("/iam/aup")) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + AupDTO createdAup = mapper.readValue(aupJson, AupDTO.class); + + DateEqualModulo1Second creationAndLastUpdateTimeMatcher = new DateEqualModulo1Second(now); + assertThat(createdAup.getUrl(), equalTo(aup.getUrl())); + assertThat(createdAup.getDescription(), equalTo(aup.getDescription())); + assertThat(createdAup.getSignatureValidityInDays(), equalTo(aup.getSignatureValidityInDays())); + assertThat(createdAup.getCreationTime(), creationAndLastUpdateTimeMatcher); + assertThat(createdAup.getLastUpdateTime(), creationAndLastUpdateTimeMatcher); + } + @Test @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) public void aupCreationFailsIfAupAlreadyDefined() throws JsonProcessingException, Exception { @@ -396,7 +534,7 @@ public void aupUpdateWorks() throws JsonProcessingException, Exception { aup.setUrl(UPDATED_AUP_URL); aup.setDescription(UPDATED_AUP_DESC); - aup.setSignatureValidityInDays(18L); + aup.setSignatureValidityInDays(31L); // Time travel 1 minute in the future Date then = new Date(now.getTime() + TimeUnit.MINUTES.toMillis(1)); @@ -416,7 +554,7 @@ public void aupUpdateWorks() throws JsonProcessingException, Exception { assertThat(updatedAup.getDescription(), equalTo(UPDATED_AUP_DESC)); assertThat(updatedAup.getCreationTime(), new DateEqualModulo1Second(now)); assertThat(updatedAup.getLastUpdateTime(), new DateEqualModulo1Second(now)); - assertThat(updatedAup.getSignatureValidityInDays(), equalTo(18L)); + assertThat(updatedAup.getSignatureValidityInDays(), equalTo(31L)); } } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupReminderTaskTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupReminderTaskTests.java new file mode 100644 index 000000000..e5beeb017 --- /dev/null +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupReminderTaskTests.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) Istituto Nazionale di Fisica Nucleare (INFN). 2016-2021 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package it.infn.mw.iam.test.api.aup; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; + +import it.infn.mw.iam.IamLoginService; +import it.infn.mw.iam.core.web.aup.AupReminderTask; +import it.infn.mw.iam.persistence.model.IamAccount; +import it.infn.mw.iam.persistence.model.IamAup; +import it.infn.mw.iam.persistence.repository.IamAccountRepository; +import it.infn.mw.iam.persistence.repository.IamAupRepository; +import it.infn.mw.iam.persistence.repository.IamAupSignatureRepository; +import it.infn.mw.iam.persistence.repository.IamEmailNotificationRepository; +import it.infn.mw.iam.service.aup.DefaultAupSignatureCheckService; +import it.infn.mw.iam.test.core.CoreControllerTestSupport; +import it.infn.mw.iam.test.notification.NotificationTestConfig; +import it.infn.mw.iam.test.util.WithAnonymousUser; +import it.infn.mw.iam.test.util.annotation.IamMockMvcIntegrationTest; +import it.infn.mw.iam.test.util.notification.MockNotificationDelivery; + +@RunWith(SpringRunner.class) +@IamMockMvcIntegrationTest +@SpringBootTest(classes = {IamLoginService.class, CoreControllerTestSupport.class, + NotificationTestConfig.class}, webEnvironment = WebEnvironment.MOCK) +@WithAnonymousUser +@TestPropertySource(properties = {"notification.disable=false"}) +public class AupReminderTaskTests extends AupTestSupport { + + @Autowired + private DefaultAupSignatureCheckService service; + + @Autowired + private IamAccountRepository accountRepo; + + @Autowired + private IamAupSignatureRepository signatureRepo; + + @Autowired + private IamEmailNotificationRepository notificationRepo; + + @Autowired + private AupReminderTask aupReminderTask; + + @Autowired + private MockNotificationDelivery notificationDelivery; + + @Autowired + private IamAupRepository aupRepo; + + @After + public void tearDown() { + notificationDelivery.clearDeliveredNotifications(); + aupRepo.deleteAll(); + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void aupReminderEmailWorks() { + IamAup aup = buildDefaultAup(); + aup.setSignatureValidityInDays(30L); + aupRepo.save(aup); + + Date now = new Date(); + LocalDate today = LocalDate.now(); + LocalDate tomorrow = today.plusDays(1); + Date tomorrowDate = Date.from(tomorrow.atStartOfDay(ZoneId.systemDefault()).toInstant()); + + IamAccount testAccount = accountRepo.findByUsername("test") + .orElseThrow(() -> new AssertionError("Expected test account not found")); + + assertThat(service.needsAupSignature(testAccount), is(true)); + + signatureRepo.createSignatureForAccount(aup, testAccount, now); + + assertThat(service.needsAupSignature(testAccount), is(false)); + + assertThat(notificationRepo.countAupRemindersPerAccount(testAccount.getUserInfo().getEmail(), + tomorrowDate), equalTo(0)); + + aupReminderTask.sendAupReminders(); + notificationDelivery.sendPendingNotifications(); + assertThat(notificationRepo.countAupRemindersPerAccount(testAccount.getUserInfo().getEmail(), + tomorrowDate), equalTo(1)); + + } + + @Test + @WithMockUser(username = "admin", roles = {"ADMIN", "USER"}) + public void aupExpirationEmailWorks() { + IamAup aup = buildDefaultAup(); + aup.setSignatureValidityInDays(2L); + + LocalDate today = LocalDate.now(); + LocalDate twoDaysAgo = today.minusDays(2); + + Date date = Date.from(twoDaysAgo.atStartOfDay(ZoneId.systemDefault()).toInstant()); + aup.setCreationTime(date); + aup.setLastUpdateTime(date); + + aupRepo.save(aup); + + IamAccount testAccount = accountRepo.findByUsername("test") + .orElseThrow(() -> new AssertionError("Expected test account not found")); + + signatureRepo.createSignatureForAccount(aup, testAccount, date); + + assertThat( + notificationRepo.countAupExpirationMessPerAccount(testAccount.getUserInfo().getEmail()), + equalTo(0)); + + aupReminderTask.sendAupReminders(); + notificationDelivery.sendPendingNotifications(); + assertThat( + notificationRepo.countAupExpirationMessPerAccount(testAccount.getUserInfo().getEmail()), + equalTo(1)); + + aupReminderTask.sendAupReminders(); + notificationDelivery.sendPendingNotifications(); + assertThat( + notificationRepo.countAupExpirationMessPerAccount(testAccount.getUserInfo().getEmail()), + equalTo(1)); + + } +} diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupTestSupport.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupTestSupport.java index 336433a4c..80b7bd72d 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupTestSupport.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/api/aup/AupTestSupport.java @@ -37,6 +37,7 @@ public IamAup buildDefaultAup() { aup.setCreationTime(now); aup.setLastUpdateTime(now); aup.setSignatureValidityInDays(365L); + aup.setAupRemindersInDays("30,15,1"); return aup; } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/login/LoginTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/login/LoginTests.java index 74efa1586..7fbb1f033 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/login/LoginTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/login/LoginTests.java @@ -115,6 +115,7 @@ public void loginRedirectsToSignAupPageWhenNeeded() throws Exception { aup.setUrl("http://default-aup.org/"); aup.setDescription("AUP description"); aup.setSignatureValidityInDays(0L); + aup.setAupRemindersInDays("30,15,1"); aupRepo.save(aup); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/RefreshTokenGranterTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/RefreshTokenGranterTests.java index c675a2203..d3bda38bc 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/RefreshTokenGranterTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/RefreshTokenGranterTests.java @@ -93,6 +93,7 @@ public void testTokenRefreshFailsIfAupIsNotSigned() throws Exception { aup.setUrl("http://default-aup.org/"); aup.setDescription("AUP description"); aup.setSignatureValidityInDays(0L); + aup.setAupRemindersInDays("30,15,1"); aupRepo.save(aup); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/ResourceOwnerPasswordCredentialsTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/ResourceOwnerPasswordCredentialsTests.java index 7775a224a..7f89daa3a 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/ResourceOwnerPasswordCredentialsTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/ResourceOwnerPasswordCredentialsTests.java @@ -123,6 +123,7 @@ public void testResourceOwnerPasswordCredentialsFailsIfAupIsNotSigned() throws E aup.setUrl("http://default-aup.org/"); aup.setDescription("AUP description"); aup.setSignatureValidityInDays(0L); + aup.setAupRemindersInDays("30,15,1"); aupRepo.save(aup); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/TokenExchangeTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/TokenExchangeTests.java index 8ea60bc2c..c4f367ecd 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/TokenExchangeTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/TokenExchangeTests.java @@ -178,6 +178,7 @@ public void testImpersonationFlowFailsIfAUPNotSigned() throws Exception { aup.setUrl("http://default-aup.org/"); aup.setDescription("AUP description"); aup.setSignatureValidityInDays(0L); + aup.setAupRemindersInDays("30,15,1"); aupRepo.save(aup); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/authzcode/AuthorizationCodeTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/authzcode/AuthorizationCodeTests.java index 7b90fa9c9..3d1fa7718 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/authzcode/AuthorizationCodeTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/oauth/authzcode/AuthorizationCodeTests.java @@ -137,6 +137,7 @@ public void testOidcAuthorizationCodeFlowWithAUPSignature() throws Exception { aup.setUrl("http://default-aup.org/"); aup.setDescription("AUP description"); aup.setSignatureValidityInDays(0L); + aup.setAupRemindersInDays("30,15,1"); aupRepo.save(aup); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/repository/IamAupRepositoryTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/repository/IamAupRepositoryTests.java index 2c606f6cb..dd0e46a47 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/repository/IamAupRepositoryTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/repository/IamAupRepositoryTests.java @@ -64,6 +64,7 @@ public void aupCreationWorks() { assertThat(aup.getCreationTime(), new DateEqualModulo1Second(creationTime)); assertThat(aup.getLastUpdateTime(), new DateEqualModulo1Second(creationTime)); assertThat(aup.getSignatureValidityInDays(), equalTo(365L)); + assertThat(aup.getAupRemindersInDays(), equalTo("30,15,1")); } diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimUserCreationTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimUserCreationTests.java index 8a44ca106..d7fee48be 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimUserCreationTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/scim/user/ScimUserCreationTests.java @@ -494,7 +494,7 @@ public void testUserCreationWithAupSignatureIsIgnored() throws Exception { final String AUP_DESCRIPTION = "Test AUP"; final Date currentDate = new Date(); - AupDTO aup = new AupDTO(AUP_URL, "", AUP_DESCRIPTION, 0L, currentDate, currentDate); + AupDTO aup = new AupDTO(AUP_URL, "", AUP_DESCRIPTION, 0L, currentDate, currentDate, "30,15,1"); aupService.saveAup(aup); Calendar cal = Calendar.getInstance(); diff --git a/iam-login-service/src/test/java/it/infn/mw/iam/test/service/IamAccountServiceTests.java b/iam-login-service/src/test/java/it/infn/mw/iam/test/service/IamAccountServiceTests.java index fded5a4ad..40194b820 100644 --- a/iam-login-service/src/test/java/it/infn/mw/iam/test/service/IamAccountServiceTests.java +++ b/iam-login-service/src/test/java/it/infn/mw/iam/test/service/IamAccountServiceTests.java @@ -64,6 +64,7 @@ import it.infn.mw.iam.core.user.exception.CredentialAlreadyBoundException; import it.infn.mw.iam.core.user.exception.InvalidCredentialException; import it.infn.mw.iam.core.user.exception.UserAlreadyExistsException; +import it.infn.mw.iam.notification.NotificationFactory; import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamOidcId; import it.infn.mw.iam.persistence.model.IamSamlId; @@ -103,6 +104,9 @@ public class IamAccountServiceTests extends IamAccountServiceTestSupport { @Mock private OAuth2TokenEntityService tokenService; + @Mock + private NotificationFactory notificationFactory; + private Clock clock = Clock.fixed(NOW, ZoneId.systemDefault()); private DefaultIamAccountService accountService; @@ -128,7 +132,7 @@ public void setup() { when(passwordEncoder.encode(any())).thenReturn(PASSWORD); accountService = new DefaultIamAccountService(clock, accountRepo, groupRepo, authoritiesRepo, - passwordEncoder, eventPublisher, tokenService, accountClientRepo); + passwordEncoder, eventPublisher, tokenService, accountClientRepo, notificationFactory); } @Test(expected = NullPointerException.class) diff --git a/iam-persistence/src/main/java/it/infn/mw/iam/core/IamNotificationType.java b/iam-persistence/src/main/java/it/infn/mw/iam/core/IamNotificationType.java index 16ec24c16..ba3c26c74 100644 --- a/iam-persistence/src/main/java/it/infn/mw/iam/core/IamNotificationType.java +++ b/iam-persistence/src/main/java/it/infn/mw/iam/core/IamNotificationType.java @@ -16,5 +16,5 @@ package it.infn.mw.iam.core; public enum IamNotificationType { - CONFIRMATION, RESETPASSWD, ACTIVATED, REJECTED, GROUP_MEMBERSHIP, CLIENT_STATUS + CONFIRMATION, RESETPASSWD, ACTIVATED, REJECTED, GROUP_MEMBERSHIP, AUP_REMINDER, AUP_EXPIRATION, AUP_SIGNATURE_REQUEST, ACCOUNT_SUSPENDED, ACCOUNT_RESTORED, CLIENT_STATUS } diff --git a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/model/IamAup.java b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/model/IamAup.java index c39a926e2..0039e0746 100644 --- a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/model/IamAup.java +++ b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/model/IamAup.java @@ -50,6 +50,9 @@ public class IamAup implements Serializable { @Column(name = "sig_validity_days", nullable = false) Long signatureValidityInDays; + @Column(name = "aup_reminders_days", nullable = false) + String aupRemindersInDays; + @Temporal(TemporalType.TIMESTAMP) @Column(name = "creation_time", nullable = false) Date creationTime; @@ -137,6 +140,14 @@ public void setSignatureValidityInDays(Long signatureValidityInDays) { this.signatureValidityInDays = signatureValidityInDays; } + public String getAupRemindersInDays() { + return aupRemindersInDays; + } + + public void setAupRemindersInDays(String aupRemindersInDays) { + this.aupRemindersInDays = aupRemindersInDays; + } + public Date getCreationTime() { return creationTime; diff --git a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamAccountRepository.java b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamAccountRepository.java index 225a9637e..38c196d60 100644 --- a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamAccountRepository.java +++ b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamAccountRepository.java @@ -141,7 +141,6 @@ Page findByLabelNameAndValue(@Param("name") String name, @Param("val @Query("select a from IamAccount a where a.active = TRUE") Page findActiveAccounts(Pageable op); - @Modifying @Query("delete from IamAccountGroupMembership") void deleteAllAccountGroupMemberships(); diff --git a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamAupSignatureRepository.java b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamAupSignatureRepository.java index cdbbeec4d..69e324df7 100644 --- a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamAupSignatureRepository.java +++ b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamAupSignatureRepository.java @@ -15,11 +15,13 @@ */ package it.infn.mw.iam.persistence.repository; - - +import java.util.Date; +import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.Param; import it.infn.mw.iam.persistence.model.IamAccount; import it.infn.mw.iam.persistence.model.IamAup; @@ -28,8 +30,12 @@ public interface IamAupSignatureRepository extends PagingAndSortingRepository, IamAupSignatureRepositoryCustom { + @Query("select ias from IamAupSignature ias where ias.aup = :aup and :signatureTime <= ias.signatureTime and ias.signatureTime < :plusOne") + List findByAupAndSignatureTime(@Param("aup") IamAup aup, + @Param("signatureTime") Date signatureTime, @Param("plusOne") Date plusOne); + Optional findByAupAndAccount(IamAup aup, IamAccount account); - + Long deleteByAup(IamAup aup); - + } diff --git a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamEmailNotificationRepository.java b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamEmailNotificationRepository.java index fdb932798..16f970bdc 100644 --- a/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamEmailNotificationRepository.java +++ b/iam-persistence/src/main/java/it/infn/mw/iam/persistence/repository/IamEmailNotificationRepository.java @@ -46,4 +46,17 @@ List findByStatusWithUpdateTime( Integer countByDeliveryStatus(IamDeliveryStatus deliveryStatus); List findByNotificationType(IamNotificationType notificationType); + + @Query("select count(n) from IamEmailNotification n join n.receivers r where n.notificationType = it.infn.mw.iam.core.IamNotificationType.AUP_REMINDER" + + " and CURRENT_DATE <= n.lastUpdate and n.lastUpdate < :tomorrow" + + " and n.deliveryStatus <> it.infn.mw.iam.core.IamDeliveryStatus.DELIVERY_ERROR" + + " and r.emailAddress = :email_address") + Integer countAupRemindersPerAccount(@Param("email_address") String emailAddress, + @Param("tomorrow") Date tomorrow); + + @Query("select count(n) from IamEmailNotification n join n.receivers r where n.notificationType = it.infn.mw.iam.core.IamNotificationType.AUP_EXPIRATION" + + " and n.deliveryStatus <> it.infn.mw.iam.core.IamDeliveryStatus.DELIVERY_ERROR" + + " and r.emailAddress = :email_address") + Integer countAupExpirationMessPerAccount(@Param("email_address") String emailAddress); + } diff --git a/iam-persistence/src/main/resources/db/migration/h2/V105__add_aup_reminders.sql b/iam-persistence/src/main/resources/db/migration/h2/V105__add_aup_reminders.sql new file mode 100644 index 000000000..101cb09f8 --- /dev/null +++ b/iam-persistence/src/main/resources/db/migration/h2/V105__add_aup_reminders.sql @@ -0,0 +1 @@ +ALTER TABLE iam_aup ADD COLUMN aup_reminders_days VARCHAR(128) NOT NULL DEFAULT '30,15,1'; \ No newline at end of file diff --git a/iam-persistence/src/main/resources/db/migration/h2/V19__aup_tables.sql b/iam-persistence/src/main/resources/db/migration/h2/V19__aup_tables.sql index 18f449f46..8ad7b522c 100644 --- a/iam-persistence/src/main/resources/db/migration/h2/V19__aup_tables.sql +++ b/iam-persistence/src/main/resources/db/migration/h2/V19__aup_tables.sql @@ -3,7 +3,7 @@ CREATE TABLE iam_aup (ID BIGINT IDENTITY NOT NULL, description VARCHAR(128), last_update_time TIMESTAMP NOT NULL, name VARCHAR(36) NOT NULL UNIQUE, - sig_validity_days BIGINT NOT NULL, + sig_validity_days BIGINT NOT NULL, text LONGVARCHAR NOT NULL, PRIMARY KEY (ID)); diff --git a/iam-persistence/src/main/resources/db/migration/mysql/V105__add_aup_reminders.sql b/iam-persistence/src/main/resources/db/migration/mysql/V105__add_aup_reminders.sql new file mode 100644 index 000000000..101cb09f8 --- /dev/null +++ b/iam-persistence/src/main/resources/db/migration/mysql/V105__add_aup_reminders.sql @@ -0,0 +1 @@ +ALTER TABLE iam_aup ADD COLUMN aup_reminders_days VARCHAR(128) NOT NULL DEFAULT '30,15,1'; \ No newline at end of file diff --git a/iam-voms-aa/src/test/java/it/infn/mw/voms/VomsAcTests.java b/iam-voms-aa/src/test/java/it/infn/mw/voms/VomsAcTests.java index 493d2a448..dfcb709c2 100644 --- a/iam-voms-aa/src/test/java/it/infn/mw/voms/VomsAcTests.java +++ b/iam-voms-aa/src/test/java/it/infn/mw/voms/VomsAcTests.java @@ -165,6 +165,7 @@ public void userWithExpiredAUPDoesNotGetAc() throws Exception { aup.setUrl("http://default-aup.org/"); aup.setDescription("AUP description"); aup.setSignatureValidityInDays(0L); + aup.setAupRemindersInDays("30,15,1"); aupRepo.save(aup);