From ac6e1763214786eeb49fd2ddef27121f18ce944e Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 30 Nov 2024 07:28:42 +0100 Subject: [PATCH 01/15] add mail notification --- .../domain/NotificationType.java | 2 +- .../notification/NotificationConstants.java | 6 +++- .../SingleUserNotificationFactory.java | 6 ++++ .../service/notifications/MailService.java | 7 ++++ .../NotificationSettingsService.java | 9 +++-- .../SingleUserNotificationService.java | 16 ++++++++- .../service/UserSshPublicKeyService.java | 7 +++- .../localvc/ssh/SshPublicKeysResource.java | 1 - src/main/resources/i18n/messages.properties | 7 ++++ .../resources/i18n/messages_de.properties | 9 +++++ .../resources/i18n/messages_en.properties | 7 ++++ .../mail/notification/sshKeyAddedEmail.html | 34 +++++++++++++++++++ .../notification/sshKeyExpiresSoonEmail.html | 30 ++++++++++++++++ 13 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 src/main/resources/templates/mail/notification/sshKeyAddedEmail.html create mode 100644 src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java index 8a3a5db896b5..2118cdf88f1c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java @@ -9,5 +9,5 @@ public enum NotificationType { TUTORIAL_GROUP_MULTIPLE_REGISTRATION_TUTOR, TUTORIAL_GROUP_DEREGISTRATION_TUTOR, TUTORIAL_GROUP_DELETED, TUTORIAL_GROUP_UPDATED, TUTORIAL_GROUP_ASSIGNED, TUTORIAL_GROUP_UNASSIGNED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, CONVERSATION_USER_MENTIONED, CONVERSATION_CREATE_ONE_TO_ONE_CHAT, CONVERSATION_CREATE_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT, CONVERSATION_ADD_USER_CHANNEL, CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_CHANNEL, - CONVERSATION_DELETE_CHANNEL, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_BUILD_RUN_UPDATE + CONVERSATION_DELETE_CHANNEL, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_BUILD_RUN_UPDATE, SSH_KEY_ADDED } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java index 46f1ff9a2218..ae4f63332694 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java @@ -43,6 +43,7 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_REPOSITORY_LOCKS; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_TEST_CASES_CHANGED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.QUIZ_EXERCISE_STARTED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DELETED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; @@ -147,6 +148,8 @@ public class NotificationConstants { public static final String TUTORIAL_GROUP_UNASSIGNED_TITLE = "artemisApp.singleUserNotification.title.tutorialGroupUnassigned"; + public static final String SSH_KEY_ADDED_TITLE = "artemisApp.singleUserNotification.title.sshKeyAdded"; + // Texts public static final String LIVE_EXAM_EXERCISE_UPDATE_NOTIFICATION_TEXT = "artemisApp.groupNotification.text.liveExamExerciseUpdate"; @@ -305,7 +308,8 @@ public class NotificationConstants { .put(CONVERSATION_ADD_USER_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT_TITLE).put(CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_GROUP_CHAT_TITLE) .put(CONVERSATION_REMOVE_USER_CHANNEL, CONVERSATION_REMOVE_USER_CHANNEL_TITLE).put(CONVERSATION_DELETE_CHANNEL, CONVERSATION_DELETE_CHANNEL_TITLE) .put(DATA_EXPORT_CREATED, DATA_EXPORT_CREATED_TITLE).put(DATA_EXPORT_FAILED, DATA_EXPORT_FAILED_TITLE) - .put(PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_REPOSITORY_LOCKS_TITLE).put(PROGRAMMING_BUILD_RUN_UPDATE, PROGRAMMING_BUILD_RUN_UPDATE_TITLE).build(); + .put(PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_REPOSITORY_LOCKS_TITLE).put(PROGRAMMING_BUILD_RUN_UPDATE, PROGRAMMING_BUILD_RUN_UPDATE_TITLE) + .put(SSH_KEY_ADDED, SSH_KEY_ADDED_TITLE).build(); /** * Finds the corresponding NotificationType for the provided notification title diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java index 71dc52ae4e93..9c065d93b3d7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java @@ -49,6 +49,7 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_PLAGIARISM_CASE_STUDENT_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_STUDENT_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_TUTOR_TEXT; @@ -80,6 +81,7 @@ import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.tutorialgroup.domain.TutorialGroup; public class SingleUserNotificationFactory { @@ -153,6 +155,10 @@ public static SingleUserNotification createNotification(DataExport dataExport, N return notification; } + public static SingleUserNotification createNotification(UserSshPublicKey key, User recipient) { + return new SingleUserNotification(recipient, SSH_KEY_ADDED_TITLE, null, false, new String[] {}); + } + @NotificationPlaceholderCreator(values = { DATA_EXPORT_CREATED, DATA_EXPORT_FAILED }) public static String[] createPlaceholdersDataExport() { return new String[] {}; diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java index 88926325aa4e..a2f9529219fa 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java @@ -33,6 +33,7 @@ import de.tum.cit.aet.artemis.exercise.domain.Exercise; import de.tum.cit.aet.artemis.exercise.domain.participation.StudentParticipation; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; /** * Service for preparing and sending emails. @@ -86,6 +87,8 @@ public class MailService implements InstantNotificationService { private static final String NOTIFICATION_TYPE = "notificationType"; + private static final String SSH_KEY = "sshKey"; + // time related variables private static final String TIME_SERVICE = "timeService"; @@ -263,6 +266,9 @@ public void sendNotification(Notification notification, User user, Object notifi if (notificationSubject instanceof PlagiarismCase plagiarismCase) { subject = setPlagiarismContextAndSubject(context, notificationType, notification, plagiarismCase); } + if (notificationSubject instanceof UserSshPublicKey userSshPublicKey) { + context.setVariable(SSH_KEY, userSshPublicKey); + } if (notificationSubject instanceof SingleUserNotificationService.TutorialGroupNotificationSubject tutorialGroupNotificationSubject) { setContextForTutorialGroupNotifications(context, notificationType, tutorialGroupNotificationSubject); @@ -393,6 +399,7 @@ private String createContentForNotificationEmailByType(NotificationType notifica case TUTORIAL_GROUP_UPDATED -> templateEngine.process("mail/notification/tutorialGroupUpdatedEmail", context); case DATA_EXPORT_CREATED -> templateEngine.process("mail/notification/dataExportCreatedEmail", context); case DATA_EXPORT_FAILED -> templateEngine.process("mail/notification/dataExportFailedEmail", context); + case SSH_KEY_ADDED -> templateEngine.process("mail/notification/sshKeyAddedEmail", context); default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java index 62f4912b9332..dc2bd0d01ad0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java @@ -32,6 +32,7 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_VERDICT_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_TEST_CASES_CHANGED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.QUIZ_EXERCISE_STARTED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DELETED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; @@ -134,6 +135,9 @@ public class NotificationSettingsService { public static final String NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED = "notification.user-notification.data-export-failed"; + // ssh user notification settings group + public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED = "notification.user-notification.ssh-key-added"; + // if webapp or email is not explicitly set for a specific setting -> no support for this communication channel for this setting // this has to match the properties in the notification settings structure file on the client that hides the related UI elements public static final Set DEFAULT_NOTIFICATION_SETTINGS = new HashSet<>(Arrays.asList( @@ -175,7 +179,8 @@ public class NotificationSettingsService { new NotificationSetting(true, false, true, NOTIFICATION__USER_NOTIFICATION__USER_MENTION), // data export notification setting (cannot be overridden by user) new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED), - new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED))); + new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED), + new NotificationSetting(false, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED))); /** * This is the place where the mapping between SettingId and NotificationTypes happens on the server side @@ -219,7 +224,7 @@ public class NotificationSettingsService { PLAGIARISM_CASE_VERDICT_STUDENT, TUTORIAL_GROUP_REGISTRATION_STUDENT, TUTORIAL_GROUP_REGISTRATION_TUTOR, TUTORIAL_GROUP_MULTIPLE_REGISTRATION_TUTOR, TUTORIAL_GROUP_DEREGISTRATION_STUDENT, TUTORIAL_GROUP_DEREGISTRATION_TUTOR, TUTORIAL_GROUP_DELETED, TUTORIAL_GROUP_UPDATED, TUTORIAL_GROUP_ASSIGNED, TUTORIAL_GROUP_UNASSIGNED, NEW_EXERCISE_POST, NEW_LECTURE_POST, NEW_REPLY_FOR_LECTURE_POST, NEW_COURSE_POST, NEW_REPLY_FOR_COURSE_POST, NEW_REPLY_FOR_EXERCISE_POST, - QUIZ_EXERCISE_STARTED, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE); + QUIZ_EXERCISE_STARTED, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, SSH_KEY_ADDED); // More information on supported notification types can be found here: https://docs.artemis.cit.tum.de/user/notifications/ // Please adapt the above docs if you change the supported notification types diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java index 33def5698b9f..f7f061bb7e13 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java @@ -13,6 +13,7 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_EXERCISE_POST; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_LECTURE_POST; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_VERDICT_STUDENT; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_TUTOR; @@ -33,6 +34,7 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_REPLY_FOR_EXAM_POST_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_REPLY_FOR_EXERCISE_POST_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_REPLY_FOR_LECTURE_POST_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.findCorrespondingNotificationTitleOrThrow; import static de.tum.cit.aet.artemis.communication.domain.notification.SingleUserNotificationFactory.createNotification; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsCommunicationChannel.WEBAPP; @@ -71,6 +73,7 @@ import de.tum.cit.aet.artemis.exercise.service.ExerciseDateService; import de.tum.cit.aet.artemis.fileupload.domain.FileUploadExercise; import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismCase; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.tutorialgroup.domain.TutorialGroup; @Profile(PROFILE_CORE) @@ -145,6 +148,7 @@ private SingleUserNotification createSingleUserNotification(Object notificationS createNotification(((NewReplyNotificationSubject) notificationSubject).answerPost, notificationType, ((NewReplyNotificationSubject) notificationSubject).user, ((NewReplyNotificationSubject) notificationSubject).responsibleUser); case DATA_EXPORT_CREATED, DATA_EXPORT_FAILED -> createNotification((DataExport) notificationSubject, notificationType, typeSpecificInformation); + case SSH_KEY_ADDED -> createNotification((UserSshPublicKey) notificationSubject, typeSpecificInformation); default -> throw new UnsupportedOperationException("Can not create notification for type : " + notificationType); }; } @@ -256,6 +260,16 @@ public void notifyUserAboutDataExportFailure(DataExport dataExport) { notifyRecipientWithNotificationType(dataExport, DATA_EXPORT_FAILED, dataExport.getUser(), null); } + /** + * Notify user about the addition of an SSH key in the settings + * + * @param recipient the user to whose account the SSH key was added + * @param key the key which was added + */ + public void notifyUserAboutNewlyAddedSshKey(User recipient, UserSshPublicKey key) { + notifyRecipientWithNotificationType(key, SSH_KEY_ADDED, recipient, null); + } + /** * Notify student about possible plagiarism case. * @@ -526,7 +540,7 @@ else if (channel.getIsCourseWide()) { } private boolean shouldNotificationBeSaved(SingleUserNotification notification) { - if (Objects.equals(notification.getTitle(), CONVERSATION_CREATE_ONE_TO_ONE_CHAT_TITLE)) { + if (Objects.equals(notification.getTitle(), CONVERSATION_CREATE_ONE_TO_ONE_CHAT_TITLE) || Objects.equals(notification.getTitle(), SSH_KEY_ADDED_TITLE)) { return false; } else if (Objects.equals(notification.getTitle(), CONVERSATION_CREATE_GROUP_CHAT_TITLE) || Objects.equals(notification.getTitle(), CONVERSATION_DELETE_CHANNEL_TITLE) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 232afd327663..ed2fab3b7ba8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -15,6 +15,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; +import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; @@ -29,8 +30,11 @@ public class UserSshPublicKeyService { private final UserSshPublicKeyRepository userSshPublicKeyRepository; - public UserSshPublicKeyService(UserSshPublicKeyRepository userSshPublicKeyRepository) { + private final SingleUserNotificationService singleUserNotificationService; + + public UserSshPublicKeyService(UserSshPublicKeyRepository userSshPublicKeyRepository, SingleUserNotificationService singleUserNotificationService) { this.userSshPublicKeyRepository = userSshPublicKeyRepository; + this.singleUserNotificationService = singleUserNotificationService; } /** @@ -57,6 +61,7 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP newUserSshPublicKey.setCreationDate(ZonedDateTime.now()); newUserSshPublicKey.setExpiryDate(sshPublicKey.expiryDate()); userSshPublicKeyRepository.save(newUserSshPublicKey); + singleUserNotificationService.notifyUserAboutNewlyAddedSshKey(user, newUserSshPublicKey); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java index 8eb38fdfd263..044e2b840cda 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java @@ -92,7 +92,6 @@ public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKeyDTO ssh catch (IllegalArgumentException e) { throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true); } - userSshPublicKeyService.createSshKeyForUser(user, keyEntry, sshPublicKey); return ResponseEntity.ok().build(); } diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 0879e526d4f4..a6559328572b 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -126,6 +126,12 @@ email.successfulDataExportCreationsAdmin.title = Successfully created requested email.successfulDataExportCreationsAdmin.text = Data exports for the following users were successfully created when the data export creation job was ran: email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} +# SSH User Settings +email.sshKeyAdded.title = New SSH key added to account +email.notification.sshKeyAdded.title = The following SSH key was added to your account: +email.notification.sshKeyAdded.ifMistake = If you believe this key was added in error, you can remove the key and disable access at the following location: +email.notification.sshKeyExpiry.sshKeysExpiryWarning = One of your SSH keys will expire in a few days. + # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there artemisApp.groupNotification.title.attachmentChange = Attachment updated @@ -152,3 +158,4 @@ artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Tutorial Group artemisApp.singleUserNotification.title.dataExportCreated = Your Artemis data export has been successfully created artemisApp.singleUserNotification.title.dataExportFailed = Your Artemis data export could not be created +artemisApp.singleUserNotification.title.sshKeyAdded = New SSH key added to account diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties index 289a66b117a2..e3700255d5dd 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -125,6 +125,13 @@ email.dataExportFailedAdmin.githubLink = Link um ein Issue im Artemis GitHub Pro email.successfulDataExportCreationsAdmin.title = Angeforderte Datenexporte wurden f?r deine Instanz erfolgreich erstellt email.successfulDataExportCreationsAdmin.text = Datenexporte f?r die folgenden Nutzer wurden erfolgreich erstellt als der Job um die Datenexporte zu erstellen zuletzt ausgef?hrt wurde: email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} + +# SSH User Settings +email.sshKeyAdded.title = Neuer SSH Schlüssel zu Account hinzugefügt +email.notification.sshKeyAdded.title = Der folgende SSH Schlüssel wurde zu deinem Benutzerkonto hinzugefügt: +email.notification.sshKeyAdded.ifMistake = Wenn du glaubst, dass dieser Schlüssel irrtümlich hinzugefügt wurde, kannst du den Schlüssel entfernen und den Zugriff an folgender Stelle deaktivieren: +email.notification.sshKeyExpiry.sshKeysExpiryWarning = Einer deiner SSH Schlüssel läuft in wenigen Tagen ab. + # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there artemisApp.groupNotification.title.attachmentChange = Anhang aktualisiert @@ -145,9 +152,11 @@ artemisApp.singleUserNotification.title.tutorialGroupDeregistrationTutor = Ein S artemisApp.singleUserNotification.title.tutorialGroupMultipleRegistrationTutor = Mehrere Studenten wurden zu deiner Ãœbungsgruppe registriert artemisApp.singleUserNotification.title.tutorialGroupAssigned = Du wurdest zugeteilt eine Ãœbungsgruppe zu leiten artemisApp.singleUserNotification.title.tutorialGroupUnassigned = Dir wurde die Leitung einer Ãœbungsgruppe entzogen +artemisApp.singleUserNotification.title.newSshKeyAdded = Neuer SSH Schlüssel hinzugefügt artemisApp.tutorialGroupNotification.title.tutorialGroupDeleted = Ãœbungsgruppe gelöscht artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Ãœbungsgruppe aktualisiert artemisApp.singleUserNotification.title.dataExportCreated = Dein Artemis Datenexport wurde erfolgreich erstellt artemisApp.singleUserNotification.title.dataExportFailed = Dein Artemis Datenexport konnte nicht erstellt werden +artemisApp.singleUserNotification.title.sshKeyAdded = Neuer SSH Schlüssel hinzugefügt diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties index ef2bc15f8c9b..90ca7ddf67d2 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -126,6 +126,12 @@ email.successfulDataExportCreationsAdmin.title = Successfully created requested email.successfulDataExportCreationsAdmin.text = Data exports for the following users were successfully created when the data export creation job was running: email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} +# SSH User Settings +email.sshKeyAdded.title = New SSH key added to account +email.notification.sshKeyAdded.title = The following SSH key was added to your account: +email.notification.sshKeyAdded.ifMistake = If you believe this key was added in error, you can remove the key and disable access at the following location: +email.notification.sshKeyExpiry.sshKeysExpiryWarning = One of your SSH keys will expire in a few days. + # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there artemisApp.groupNotification.title.attachmentChange = Attachment updated @@ -151,3 +157,4 @@ artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Tutorial Group artemisApp.singleUserNotification.title.dataExportCreated = Your Artemis data export has been successfully created artemisApp.singleUserNotification.title.dataExportFailed = Your Artemis data export could not be created +artemisApp.singleUserNotification.title.sshKeyAdded = New SSH key added to account diff --git a/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html b/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html new file mode 100644 index 000000000000..b322aa7af8c7 --- /dev/null +++ b/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html @@ -0,0 +1,34 @@ + + + + + + + + + +
+ + +

+ If adding it was by mistake, remove it here: +

+

+ SSH key label +

+

+ SSH key hash +

+

+

+ If adding it was by mistake, remove it here: + + Login + link +

+ + +
+ + + diff --git a/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html b/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html new file mode 100644 index 000000000000..9096f350b2a4 --- /dev/null +++ b/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html @@ -0,0 +1,30 @@ + +The following SSH key was added to your account: + +
+ SSH key label + SSH key hash +
+ +If adding it was by mistake, remove it here: + + + + + + + + + + + + + +
+ bob +
+ + + + + From 50506b767536dcceaf6cc4d7c88837283e4f8411 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 30 Nov 2024 16:25:07 +0100 Subject: [PATCH 02/15] add scheduled expiry warnings --- .../domain/NotificationType.java | 3 +- .../notification/NotificationConstants.java | 14 ++++- .../SingleUserNotificationFactory.java | 32 +++++++++-- .../service/notifications/MailService.java | 9 +++ .../NotificationSettingsService.java | 20 +++++-- .../SingleUserNotificationService.java | 27 ++++++++- .../UserSshPublicKeyRepository.java | 3 + ...SshPublicKeyExpiryNotificationService.java | 57 +++++++++++++++++++ .../UserSshPublicKeyService.java | 2 +- .../SshPublicKeysResource.java | 4 +- src/main/resources/i18n/messages.properties | 9 ++- .../resources/i18n/messages_de.properties | 20 ++++--- .../resources/i18n/messages_en.properties | 9 ++- .../mail/notification/sshKeyAddedEmail.html | 6 +- .../notification/sshKeyExpiresSoonEmail.html | 50 +++++++++------- .../notification/sshKeyHasExpiredEmail.html | 40 +++++++++++++ src/main/webapp/i18n/de/notification.json | 10 +++- src/main/webapp/i18n/en/notification.json | 10 +++- 18 files changed, 273 insertions(+), 52 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java rename src/main/java/de/tum/cit/aet/artemis/programming/service/{ => sshuserkeys}/UserSshPublicKeyService.java (99%) rename src/main/java/de/tum/cit/aet/artemis/programming/web/{localvc/ssh => sshuserkeys}/SshPublicKeysResource.java (97%) create mode 100644 src/main/resources/templates/mail/notification/sshKeyHasExpiredEmail.html diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java index 2118cdf88f1c..2a85412663bb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/NotificationType.java @@ -9,5 +9,6 @@ public enum NotificationType { TUTORIAL_GROUP_MULTIPLE_REGISTRATION_TUTOR, TUTORIAL_GROUP_DEREGISTRATION_TUTOR, TUTORIAL_GROUP_DELETED, TUTORIAL_GROUP_UPDATED, TUTORIAL_GROUP_ASSIGNED, TUTORIAL_GROUP_UNASSIGNED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, CONVERSATION_USER_MENTIONED, CONVERSATION_CREATE_ONE_TO_ONE_CHAT, CONVERSATION_CREATE_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT, CONVERSATION_ADD_USER_CHANNEL, CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_CHANNEL, - CONVERSATION_DELETE_CHANNEL, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_BUILD_RUN_UPDATE, SSH_KEY_ADDED + CONVERSATION_DELETE_CHANNEL, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_BUILD_RUN_UPDATE, SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON, + SSH_KEY_HAS_EXPIRED } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java index ae4f63332694..b9c8b651c3e3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/NotificationConstants.java @@ -44,6 +44,8 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_TEST_CASES_CHANGED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.QUIZ_EXERCISE_STARTED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DELETED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; @@ -150,6 +152,10 @@ public class NotificationConstants { public static final String SSH_KEY_ADDED_TITLE = "artemisApp.singleUserNotification.title.sshKeyAdded"; + public static final String SSH_KEY_EXPIRES_SOON_TITLE = "artemisApp.singleUserNotification.title.sshKeyExpiresSoon"; + + public static final String SSH_KEY_HAS_EXPIRED_TITLE = "artemisApp.singleUserNotification.title.sshKeyHasExpired"; + // Texts public static final String LIVE_EXAM_EXERCISE_UPDATE_NOTIFICATION_TEXT = "artemisApp.groupNotification.text.liveExamExerciseUpdate"; @@ -283,6 +289,12 @@ public class NotificationConstants { public static final String CONVERSATION_DELETE_CHANNEL_TEXT = "artemisApp.singleUserNotification.text.deleteChannel"; + public static final String SSH_KEY_ADDED_TEXT = "artemisApp.singleUserNotification.text.sshKeyAdded"; + + public static final String SSH_KEY_EXPIRES_SOON_TEXT = "artemisApp.singleUserNotification.text.sshKeyExpiresSoon"; + + public static final String SSH_KEY_HAS_EXPIRED_TEXT = "artemisApp.singleUserNotification.text.sshKeyHasExpired"; + // bidirectional map private static final BiMap NOTIFICATION_TYPE_AND_TITLE_MAP = new ImmutableBiMap.Builder() .put(EXERCISE_SUBMISSION_ASSESSED, EXERCISE_SUBMISSION_ASSESSED_TITLE).put(ATTACHMENT_CHANGE, ATTACHMENT_CHANGE_TITLE).put(EXERCISE_RELEASED, EXERCISE_RELEASED_TITLE) @@ -309,7 +321,7 @@ public class NotificationConstants { .put(CONVERSATION_REMOVE_USER_CHANNEL, CONVERSATION_REMOVE_USER_CHANNEL_TITLE).put(CONVERSATION_DELETE_CHANNEL, CONVERSATION_DELETE_CHANNEL_TITLE) .put(DATA_EXPORT_CREATED, DATA_EXPORT_CREATED_TITLE).put(DATA_EXPORT_FAILED, DATA_EXPORT_FAILED_TITLE) .put(PROGRAMMING_REPOSITORY_LOCKS, PROGRAMMING_REPOSITORY_LOCKS_TITLE).put(PROGRAMMING_BUILD_RUN_UPDATE, PROGRAMMING_BUILD_RUN_UPDATE_TITLE) - .put(SSH_KEY_ADDED, SSH_KEY_ADDED_TITLE).build(); + .put(SSH_KEY_ADDED, SSH_KEY_ADDED_TITLE).put(SSH_KEY_EXPIRES_SOON, SSH_KEY_EXPIRES_SOON_TITLE).put(SSH_KEY_HAS_EXPIRED, SSH_KEY_HAS_EXPIRED_TITLE).build(); /** * Finds the corresponding NotificationType for the provided notification title diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java index 9c065d93b3d7..2aebe5cbf043 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java @@ -21,6 +21,8 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_EXERCISE_POST; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_LECTURE_POST; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_VERDICT_STUDENT; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_TUTOR; @@ -49,7 +51,12 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_PLAGIARISM_CASE_STUDENT_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_EXPIRES_SOON_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_EXPIRES_SOON_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_HAS_EXPIRED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_HAS_EXPIRED_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_STUDENT_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_TUTOR_TEXT; @@ -68,6 +75,7 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createPlagiarismCaseTarget; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createTutorialGroupTarget; +import java.time.format.DateTimeFormatter; import java.util.Set; import jakarta.validation.constraints.NotNull; @@ -155,15 +163,31 @@ public static SingleUserNotification createNotification(DataExport dataExport, N return notification; } - public static SingleUserNotification createNotification(UserSshPublicKey key, User recipient) { - return new SingleUserNotification(recipient, SSH_KEY_ADDED_TITLE, null, false, new String[] {}); - } - @NotificationPlaceholderCreator(values = { DATA_EXPORT_CREATED, DATA_EXPORT_FAILED }) public static String[] createPlaceholdersDataExport() { return new String[] {}; } + public static SingleUserNotification createNotification(UserSshPublicKey key, NotificationType notificationType, User recipient) { + switch (notificationType) { + case SSH_KEY_ADDED -> { + return new SingleUserNotification(recipient, SSH_KEY_ADDED_TITLE, SSH_KEY_ADDED_TEXT, true, new String[] {}); + } + case SSH_KEY_EXPIRES_SOON -> { + return new SingleUserNotification(recipient, SSH_KEY_EXPIRES_SOON_TITLE, SSH_KEY_EXPIRES_SOON_TEXT, true, createPlaceholdersSshKeyNotification(key)); + } + case SSH_KEY_HAS_EXPIRED -> { + return new SingleUserNotification(recipient, SSH_KEY_HAS_EXPIRED_TITLE, SSH_KEY_HAS_EXPIRED_TEXT, true, createPlaceholdersSshKeyNotification(key)); + } + default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); + } + } + + @NotificationPlaceholderCreator(values = { SSH_KEY_EXPIRES_SOON, SSH_KEY_HAS_EXPIRED }) + public static String[] createPlaceholdersSshKeyNotification(UserSshPublicKey key) { + return new String[] { key.getLabel(), key.getExpiryDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")) }; + } + /** * Creates an instance of SingleUserNotification based on plagiarisms. * diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java index a2f9529219fa..2d7b4214aa58 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/MailService.java @@ -5,6 +5,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; import java.net.URL; +import java.time.format.DateTimeFormatter; import java.util.HashMap; import java.util.List; import java.util.Locale; @@ -89,6 +90,8 @@ public class MailService implements InstantNotificationService { private static final String SSH_KEY = "sshKey"; + private static final String SSH_KEY_EXPIRY_DATE = "expiryDate"; + // time related variables private static final String TIME_SERVICE = "timeService"; @@ -268,6 +271,9 @@ public void sendNotification(Notification notification, User user, Object notifi } if (notificationSubject instanceof UserSshPublicKey userSshPublicKey) { context.setVariable(SSH_KEY, userSshPublicKey); + if (userSshPublicKey.getExpiryDate() != null) { + context.setVariable(SSH_KEY_EXPIRY_DATE, userSshPublicKey.getExpiryDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))); + } } if (notificationSubject instanceof SingleUserNotificationService.TutorialGroupNotificationSubject tutorialGroupNotificationSubject) { @@ -400,6 +406,9 @@ private String createContentForNotificationEmailByType(NotificationType notifica case DATA_EXPORT_CREATED -> templateEngine.process("mail/notification/dataExportCreatedEmail", context); case DATA_EXPORT_FAILED -> templateEngine.process("mail/notification/dataExportFailedEmail", context); case SSH_KEY_ADDED -> templateEngine.process("mail/notification/sshKeyAddedEmail", context); + case SSH_KEY_EXPIRES_SOON -> templateEngine.process("mail/notification/sshKeyExpiresSoonEmail", context); + case SSH_KEY_HAS_EXPIRED -> templateEngine.process("mail/notification/sshKeyHasExpiredEmail", context); + default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); }; } diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java index dc2bd0d01ad0..c953c3700dae 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/NotificationSettingsService.java @@ -33,6 +33,8 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PROGRAMMING_TEST_CASES_CHANGED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.QUIZ_EXERCISE_STARTED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DELETED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; @@ -138,6 +140,10 @@ public class NotificationSettingsService { // ssh user notification settings group public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED = "notification.user-notification.ssh-key-added"; + public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON = "notification.user-notification.ssh-key-expires-soon"; + + public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED = "notification.user-notification.ssh-key-has-expired"; + // if webapp or email is not explicitly set for a specific setting -> no support for this communication channel for this setting // this has to match the properties in the notification settings structure file on the client that hides the related UI elements public static final Set DEFAULT_NOTIFICATION_SETTINGS = new HashSet<>(Arrays.asList( @@ -177,10 +183,12 @@ public class NotificationSettingsService { new NotificationSetting(true, false, true, NOTIFICATION__USER_NOTIFICATION__NEW_REPLY_IN_CONVERSATION_MESSAGE), // user mention notification setting group new NotificationSetting(true, false, true, NOTIFICATION__USER_NOTIFICATION__USER_MENTION), - // data export notification setting (cannot be overridden by user) + // data export and SSH notification setting (cannot be overridden by user) new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_FAILED), new NotificationSetting(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED), - new NotificationSetting(false, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED))); + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON), + new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED))); /** * This is the place where the mapping between SettingId and NotificationTypes happens on the server side @@ -214,7 +222,10 @@ public class NotificationSettingsService { new NotificationType[] { CONVERSATION_NEW_MESSAGE, CONVERSATION_CREATE_ONE_TO_ONE_CHAT, CONVERSATION_CREATE_GROUP_CHAT, CONVERSATION_ADD_USER_GROUP_CHAT, CONVERSATION_ADD_USER_CHANNEL, CONVERSATION_REMOVE_USER_GROUP_CHAT, CONVERSATION_REMOVE_USER_CHANNEL }), Map.entry(NOTIFICATION__USER_NOTIFICATION__NEW_REPLY_IN_CONVERSATION_MESSAGE, new NotificationType[] { CONVERSATION_NEW_REPLY_MESSAGE }), - Map.entry(NOTIFICATION__USER_NOTIFICATION__USER_MENTION, new NotificationType[] { CONVERSATION_USER_MENTIONED })); + Map.entry(NOTIFICATION__USER_NOTIFICATION__USER_MENTION, new NotificationType[] { CONVERSATION_USER_MENTIONED }), + Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_ADDED, new NotificationType[] { SSH_KEY_ADDED }), + Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_EXPIRES_SOON, new NotificationType[] { SSH_KEY_EXPIRES_SOON }), + Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED, new NotificationType[] { SSH_KEY_HAS_EXPIRED })); // This set has to equal the UI configuration in the client notification settings structure file! // More information on supported notification types can be found here: https://docs.artemis.cit.tum.de/user/notifications/ @@ -224,7 +235,8 @@ public class NotificationSettingsService { PLAGIARISM_CASE_VERDICT_STUDENT, TUTORIAL_GROUP_REGISTRATION_STUDENT, TUTORIAL_GROUP_REGISTRATION_TUTOR, TUTORIAL_GROUP_MULTIPLE_REGISTRATION_TUTOR, TUTORIAL_GROUP_DEREGISTRATION_STUDENT, TUTORIAL_GROUP_DEREGISTRATION_TUTOR, TUTORIAL_GROUP_DELETED, TUTORIAL_GROUP_UPDATED, TUTORIAL_GROUP_ASSIGNED, TUTORIAL_GROUP_UNASSIGNED, NEW_EXERCISE_POST, NEW_LECTURE_POST, NEW_REPLY_FOR_LECTURE_POST, NEW_COURSE_POST, NEW_REPLY_FOR_COURSE_POST, NEW_REPLY_FOR_EXERCISE_POST, - QUIZ_EXERCISE_STARTED, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, SSH_KEY_ADDED); + QUIZ_EXERCISE_STARTED, DATA_EXPORT_CREATED, DATA_EXPORT_FAILED, CONVERSATION_NEW_MESSAGE, CONVERSATION_NEW_REPLY_MESSAGE, SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON, + SSH_KEY_HAS_EXPIRED); // More information on supported notification types can be found here: https://docs.artemis.cit.tum.de/user/notifications/ // Please adapt the above docs if you change the supported notification types diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java index f7f061bb7e13..f052c6362dea 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/service/notifications/SingleUserNotificationService.java @@ -14,6 +14,8 @@ import static de.tum.cit.aet.artemis.communication.domain.NotificationType.NEW_REPLY_FOR_LECTURE_POST; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.PLAGIARISM_CASE_VERDICT_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_ADDED; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_EXPIRES_SOON; +import static de.tum.cit.aet.artemis.communication.domain.NotificationType.SSH_KEY_HAS_EXPIRED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_ASSIGNED; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_STUDENT; import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_DEREGISTRATION_TUTOR; @@ -34,7 +36,6 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_REPLY_FOR_EXAM_POST_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_REPLY_FOR_EXERCISE_POST_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_REPLY_FOR_LECTURE_POST_TITLE; -import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.findCorrespondingNotificationTitleOrThrow; import static de.tum.cit.aet.artemis.communication.domain.notification.SingleUserNotificationFactory.createNotification; import static de.tum.cit.aet.artemis.communication.service.notifications.NotificationSettingsCommunicationChannel.WEBAPP; @@ -148,7 +149,7 @@ private SingleUserNotification createSingleUserNotification(Object notificationS createNotification(((NewReplyNotificationSubject) notificationSubject).answerPost, notificationType, ((NewReplyNotificationSubject) notificationSubject).user, ((NewReplyNotificationSubject) notificationSubject).responsibleUser); case DATA_EXPORT_CREATED, DATA_EXPORT_FAILED -> createNotification((DataExport) notificationSubject, notificationType, typeSpecificInformation); - case SSH_KEY_ADDED -> createNotification((UserSshPublicKey) notificationSubject, typeSpecificInformation); + case SSH_KEY_ADDED, SSH_KEY_EXPIRES_SOON, SSH_KEY_HAS_EXPIRED -> createNotification((UserSshPublicKey) notificationSubject, notificationType, typeSpecificInformation); default -> throw new UnsupportedOperationException("Can not create notification for type : " + notificationType); }; } @@ -270,6 +271,26 @@ public void notifyUserAboutNewlyAddedSshKey(User recipient, UserSshPublicKey key notifyRecipientWithNotificationType(key, SSH_KEY_ADDED, recipient, null); } + /** + * Notify user about an upcoming expiry of an SSH key + * + * @param recipient the user of whose account the SSH key will expire soon + * @param key the key which was added + */ + public void notifyUserAboutSoonExpiringSshKey(User recipient, UserSshPublicKey key) { + notifyRecipientWithNotificationType(key, SSH_KEY_EXPIRES_SOON, recipient, null); + } + + /** + * Notify user about an upcoming expiry of an SSH key + * + * @param recipient the user to whose account the SSH key was added + * @param key the key which was added + */ + public void notifyUserAboutExpiredSshKey(User recipient, UserSshPublicKey key) { + notifyRecipientWithNotificationType(key, SSH_KEY_HAS_EXPIRED, recipient, null); + } + /** * Notify student about possible plagiarism case. * @@ -540,7 +561,7 @@ else if (channel.getIsCourseWide()) { } private boolean shouldNotificationBeSaved(SingleUserNotification notification) { - if (Objects.equals(notification.getTitle(), CONVERSATION_CREATE_ONE_TO_ONE_CHAT_TITLE) || Objects.equals(notification.getTitle(), SSH_KEY_ADDED_TITLE)) { + if (Objects.equals(notification.getTitle(), CONVERSATION_CREATE_ONE_TO_ONE_CHAT_TITLE)) { return false; } else if (Objects.equals(notification.getTitle(), CONVERSATION_CREATE_GROUP_CHAT_TITLE) || Objects.equals(notification.getTitle(), CONVERSATION_DELETE_CHANNEL_TITLE) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java index b177b7b72089..303d4acfb893 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java @@ -2,6 +2,7 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import java.time.ZonedDateTime; import java.util.List; import java.util.Optional; @@ -21,6 +22,8 @@ public interface UserSshPublicKeyRepository extends ArtemisJpaRepository findByIdAndUserId(Long keyId, Long userId); + List findByExpiryDateBetween(ZonedDateTime from, ZonedDateTime to); + boolean existsByIdAndUserId(Long id, Long userId); boolean existsByUserId(Long userId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java new file mode 100644 index 000000000000..aff3dff21f05 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java @@ -0,0 +1,57 @@ +package de.tum.cit.aet.artemis.programming.service.sshuserkeys; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; + +import java.time.ZonedDateTime; + +import org.springframework.context.annotation.Profile; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.communication.service.notifications.SingleUserNotificationService; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; + +@Profile(PROFILE_SCHEDULING) +@Service +public class UserSshPublicKeyExpiryNotificationService { + + private final UserSshPublicKeyRepository userSshPublicKeyRepository; + + private final SingleUserNotificationService singleUserNotificationService; + + private final UserRepository userRepository; + + public UserSshPublicKeyExpiryNotificationService(UserSshPublicKeyRepository userSshPublicKeyRepository, SingleUserNotificationService singleUserNotificationService, + UserRepository userRepository) { + this.userSshPublicKeyRepository = userSshPublicKeyRepository; + this.singleUserNotificationService = singleUserNotificationService; + this.userRepository = userRepository; + } + + /** + * Notifies the user one week in advance about the upcoming expiry of one of their SSH keys + */ + @Scheduled(cron = "0 0 6 * * *") // execute this every night at 6:00:00 am + public void notifyUserOnUpcomingKeyExpiry() { + ZonedDateTime fromDate = ZonedDateTime.now().plusDays(6); + ZonedDateTime toDate = ZonedDateTime.now().plusDays(7); + userSshPublicKeyRepository.findByExpiryDateBetween(fromDate, toDate).forEach(userSshPublicKey -> { + var user = userRepository.findById(userSshPublicKey.getUserId()); + user.ifPresent(recipient -> singleUserNotificationService.notifyUserAboutSoonExpiringSshKey(recipient, userSshPublicKey)); + }); + } + + /** + * Notifies the user about the expiry of one of their SSH keys + */ + @Scheduled(cron = "0 0 6 * * *") // execute this every night at 6:00:00 am + public void notifyUserOnKeyExpiry() { + ZonedDateTime fromDate = ZonedDateTime.now().minusDays(1); + ZonedDateTime toDate = ZonedDateTime.now(); + userSshPublicKeyRepository.findByExpiryDateBetween(fromDate, toDate).forEach(userSshPublicKey -> { + var user = userRepository.findById(userSshPublicKey.getUserId()); + user.ifPresent(recipient -> singleUserNotificationService.notifyUserAboutExpiredSshKey(recipient, userSshPublicKey)); + }); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java similarity index 99% rename from src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java rename to src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java index ed2fab3b7ba8..13129c15bff3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java @@ -1,4 +1,4 @@ -package de.tum.cit.aet.artemis.programming.service; +package de.tum.cit.aet.artemis.programming.service.sshuserkeys; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/sshuserkeys/SshPublicKeysResource.java similarity index 97% rename from src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java rename to src/main/java/de/tum/cit/aet/artemis/programming/web/sshuserkeys/SshPublicKeysResource.java index 044e2b840cda..cb6da7284b5c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshPublicKeysResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/sshuserkeys/SshPublicKeysResource.java @@ -1,4 +1,4 @@ -package de.tum.cit.aet.artemis.programming.web.localvc.ssh; +package de.tum.cit.aet.artemis.programming.web.sshuserkeys; import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALVC; @@ -25,7 +25,7 @@ import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.programming.dto.UserSshPublicKeyDTO; -import de.tum.cit.aet.artemis.programming.service.UserSshPublicKeyService; +import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyService; @Profile(PROFILE_LOCALVC) @RestController diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index a6559328572b..6580e702bd3c 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -130,7 +130,12 @@ email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} email.sshKeyAdded.title = New SSH key added to account email.notification.sshKeyAdded.title = The following SSH key was added to your account: email.notification.sshKeyAdded.ifMistake = If you believe this key was added in error, you can remove the key and disable access at the following location: -email.notification.sshKeyExpiry.sshKeysExpiryWarning = One of your SSH keys will expire in a few days. +email.notification.sshKeyExpiry.sshKeyExpiresSoonWarning = One of your SSH keys will expire in a few days. +email.notification.sshKeyExpiry.sshKeysHasExpiredWarning = One of your SSH keys has expired. +email.notification.sshKeyExpiry.expiryDate = Expiry date: +email.notification.sshKeyExpiry.renew = You can renew your SSH key here: +email.notification.sshKeyExpiry.sshKeyHash = SSH key hash: +email.notification.sshKeyExpiry.sshKeyLabel = SSH key label: # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there @@ -159,3 +164,5 @@ artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Tutorial Group artemisApp.singleUserNotification.title.dataExportCreated = Your Artemis data export has been successfully created artemisApp.singleUserNotification.title.dataExportFailed = Your Artemis data export could not be created artemisApp.singleUserNotification.title.sshKeyAdded = New SSH key added to account +artemisApp.singleUserNotification.title.sshKeyExpiresSoon = SSH key expires soon +artemisApp.singleUserNotification.title.sshKeyHasExpired = SSH key has expired diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties index e3700255d5dd..0f9b45ee28a9 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -93,7 +93,7 @@ email.notification.aux.difficulty.hard=Schwer # Plagiarism email.plagiarism.title=Neuer Plagiatsfall: Ãœbung "{0}" im Kurs "{1}" -email.plagiarism.cpc.title=Neue signifikante ?bereinstimmung: Aufgabe "{0}" im Kurs "{1}" +email.plagiarism.cpc.title=Neue signifikante Ãœbereinstimmung: Aufgabe "{0}" im Kurs "{1}" email.notification.title.post.plagiarismVerdict=Entscheidung zum Plagiatsfall in der Aufgabe {0} gefallen email.notification.aux.plagiarismVerdict.plagiarism=Der Fall wird als Plagiat angesehen! email.notification.aux.plagiarismVerdict.point.deduction=Wegen des Plagiatsfalls ziehen wir dir Punkte in der Aufgabe ab! @@ -127,10 +127,15 @@ email.successfulDataExportCreationsAdmin.text = Datenexporte f?r die folgenden N email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} # SSH User Settings -email.sshKeyAdded.title = Neuer SSH Schlüssel zu Account hinzugefügt -email.notification.sshKeyAdded.title = Der folgende SSH Schlüssel wurde zu deinem Benutzerkonto hinzugefügt: -email.notification.sshKeyAdded.ifMistake = Wenn du glaubst, dass dieser Schlüssel irrtümlich hinzugefügt wurde, kannst du den Schlüssel entfernen und den Zugriff an folgender Stelle deaktivieren: -email.notification.sshKeyExpiry.sshKeysExpiryWarning = Einer deiner SSH Schlüssel läuft in wenigen Tagen ab. +email.sshKeyAdded.title = Neuer SSH-Schlüssel zu Account hinzugefügt +email.notification.sshKeyAdded.title = Der folgende SSH-Schlüssel wurde zu deinem Benutzerkonto hinzugefügt: +email.notification.sshKeyAdded.ifMistake = Wenn du glaubst, dass dieser Schlüssel irrtümlich hinzugefügt wurde, kannst du den Schlüssel entfernen und den Zugriff an folgender Stelle deaktivieren: +email.notification.sshKeyExpiry.sshKeyExpiresSoonWarning = Einer deiner SSH-Schlüssel läuft in wenigen Tagen ab. +email.notification.sshKeyExpiry.sshKeysHasExpiredWarning = Einer deiner SSH-Schlüssel ist abgelaufen. +email.notification.sshKeyExpiry.expiryDate = Ablaufdatum: +email.notification.sshKeyExpiry.renew = Hier kannst du deinen SSH-Schlüssel aktualisieren: +email.notification.sshKeyExpiry.sshKeyHash = SSH-Schlüssel-Hash: +email.notification.sshKeyExpiry.sshKeyLabel = SSH-Schlüssel-Label: # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there @@ -152,11 +157,12 @@ artemisApp.singleUserNotification.title.tutorialGroupDeregistrationTutor = Ein S artemisApp.singleUserNotification.title.tutorialGroupMultipleRegistrationTutor = Mehrere Studenten wurden zu deiner Ãœbungsgruppe registriert artemisApp.singleUserNotification.title.tutorialGroupAssigned = Du wurdest zugeteilt eine Ãœbungsgruppe zu leiten artemisApp.singleUserNotification.title.tutorialGroupUnassigned = Dir wurde die Leitung einer Ãœbungsgruppe entzogen -artemisApp.singleUserNotification.title.newSshKeyAdded = Neuer SSH Schlüssel hinzugefügt artemisApp.tutorialGroupNotification.title.tutorialGroupDeleted = Ãœbungsgruppe gelöscht artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Ãœbungsgruppe aktualisiert artemisApp.singleUserNotification.title.dataExportCreated = Dein Artemis Datenexport wurde erfolgreich erstellt artemisApp.singleUserNotification.title.dataExportFailed = Dein Artemis Datenexport konnte nicht erstellt werden -artemisApp.singleUserNotification.title.sshKeyAdded = Neuer SSH Schlüssel hinzugefügt +artemisApp.singleUserNotification.title.sshKeyAdded = Neuer SSH-Schlüssel hinzugefügt +artemisApp.singleUserNotification.title.sshKeyExpiresSoon = SSH-Schlüssel läuft bald ab +artemisApp.singleUserNotification.title.sshKeyHasExpired = SSH-Schlüssel ist abgelaufen diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties index 90ca7ddf67d2..5cc1bab0b4f0 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -130,7 +130,12 @@ email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} email.sshKeyAdded.title = New SSH key added to account email.notification.sshKeyAdded.title = The following SSH key was added to your account: email.notification.sshKeyAdded.ifMistake = If you believe this key was added in error, you can remove the key and disable access at the following location: -email.notification.sshKeyExpiry.sshKeysExpiryWarning = One of your SSH keys will expire in a few days. +email.notification.sshKeyExpiry.sshKeyExpiresSoonWarning = One of your SSH keys will expire in a few days. +email.notification.sshKeyExpiry.sshKeysHasExpiredWarning = One of your SSH keys has expired. +email.notification.sshKeyExpiry.expiryDate = Expiry date: +email.notification.sshKeyExpiry.renew = You can renew your SSH key here: +email.notification.sshKeyExpiry.sshKeyHash = SSH key hash: +email.notification.sshKeyExpiry.sshKeyLabel = SSH key label: # Email Subjects # The reason for the format artemisApp.{notificationCategory}.title.{notificicationType} is that these placeholders are also used in the client and this is the format used there @@ -158,3 +163,5 @@ artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Tutorial Group artemisApp.singleUserNotification.title.dataExportCreated = Your Artemis data export has been successfully created artemisApp.singleUserNotification.title.dataExportFailed = Your Artemis data export could not be created artemisApp.singleUserNotification.title.sshKeyAdded = New SSH key added to account +artemisApp.singleUserNotification.title.sshKeyExpiresSoon = SSH key expires soon +artemisApp.singleUserNotification.title.sshKeyHasExpired = SSH key has expired diff --git a/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html b/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html index b322aa7af8c7..f6919ce7c289 100644 --- a/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html +++ b/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html @@ -11,13 +11,13 @@

- If adding it was by mistake, remove it here: + The following SSH key was added to your account:

- SSH key label +

- SSH key hash +

diff --git a/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html b/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html index 9096f350b2a4..260d4f1e3f6f 100644 --- a/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html +++ b/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html @@ -1,30 +1,40 @@ - -The following SSH key was added to your account: - -

- SSH key label - SSH key hash -
- -If adding it was by mistake, remove it here: - - - - - - + - - + + +
- bob + + +

+ One of your SSH keys will expire in a few days: +

+

+ SSH key label + SSH key label +

+

+ SSH key hash + SSH key hash +

+

+ + +

+

+

+ You can renew your SSH key here: + + Login + link +

+ +
- - diff --git a/src/main/resources/templates/mail/notification/sshKeyHasExpiredEmail.html b/src/main/resources/templates/mail/notification/sshKeyHasExpiredEmail.html new file mode 100644 index 000000000000..587ff6d7d1a5 --- /dev/null +++ b/src/main/resources/templates/mail/notification/sshKeyHasExpiredEmail.html @@ -0,0 +1,40 @@ + + + + + + + + + +
+ + +

+ One of your SSH keys has expired. +

+

+ SSH key label + SSH key label +

+

+ SSH key hash + SSH key hash +

+

+ + +

+

+

+ You can renew your SSH key here: + + Login + link +

+ + +
+ + + diff --git a/src/main/webapp/i18n/de/notification.json b/src/main/webapp/i18n/de/notification.json index 5c8f606c432c..0f5dd8ff6261 100644 --- a/src/main/webapp/i18n/de/notification.json +++ b/src/main/webapp/i18n/de/notification.json @@ -123,7 +123,10 @@ "addUserGroupChat": "Du wurdest zu einem Gruppenchat hinzugefügt", "createGroupChat": "Neuer Gruppenchat", "dataExportCreated": "Datenexport erstellt", - "dataExportFailed": "Datenexport fehlgeschlagen" + "dataExportFailed": "Datenexport fehlgeschlagen", + "sshKeyAdded": "Neuer SSH-Schlüssel hinzugefügt", + "sshKeyExpiresSoon": "SSH-Schlüssel läuft bald ab", + "sshKeyHasExpired": "SSH-Schlüssel ist abgelaufen" }, "text": { "newReplyForExercisePost": "Auf deinen Beitrag zur Aufgabe \"{{ placeholderValues.8 }}\" im Kurs \"{{ placeholderValues.0 }}\" wurde geantwortet: \"{{ placeholderValues.5 }}\"", @@ -150,7 +153,10 @@ "addUserGroupChat": "Du wurdest von {{ placeholderValues.1 }} zu einem neuen Gruppenchat im Kurs {{ placeholderValues.0 }} hinzugefügt.", "createGroupChat": "Du wurdest von {{ placeholderValues.1 }} zu einem neuen Gruppenchat im Kurs {{ placeholderValues.0 }} hinzugefügt.", "dataExportCreated": "Dein Datenexport wurde erstellt und kann nun heruntergeladen werden.", - "dataExportFailed": "Dein Datenexport konnte nicht erstellt werden. Bitte versuche es später erneut." + "dataExportFailed": "Dein Datenexport konnte nicht erstellt werden. Bitte versuche es später erneut.", + "sshKeyAdded": "Du hast erfolgreich einen SSH-Schlüssel hinzugefügt", + "sshKeyExpiresSoon": "Dein SSH-Schlüssel mit dem Label \"{{ placeholderValues.0}}\" läuft am {{ placeholderValues.1 }} ab.", + "sshKeyHasExpired": "Dein SSH-Schlüssel mit dem Label \"{{ placeholderValues.0}}\" ist am {{ placeholderValues.1 }} abgelaufen." } }, "tutorialGroupNotification": { diff --git a/src/main/webapp/i18n/en/notification.json b/src/main/webapp/i18n/en/notification.json index 6c1e49a8bad0..e4ba3381e92c 100644 --- a/src/main/webapp/i18n/en/notification.json +++ b/src/main/webapp/i18n/en/notification.json @@ -123,7 +123,10 @@ "addUserGroupChat": "You have been added to a group chat", "createGroupChat": "New group chat", "dataExportCreated": "Data export created", - "dataExportFailed": "Data export failed" + "dataExportFailed": "Data export failed", + "sshKeyAdded": "New SSH key added", + "sshKeyExpiresSoon": "SSH key will expire soon", + "sshKeyHasExpired": "SSH key has expired" }, "text": { "newReplyForExercisePost": "Your post regarding exercise \"{{ placeholderValues.8 }}\" in the course \"{{ placeholderValues.0 }}\" got a new reply: \"{{ placeholderValues.5 }}\"", @@ -150,7 +153,10 @@ "addUserGroupChat": "You have been added to a new group chat by {{ placeholderValues.1 }} in course {{ placeholderValues.0 }}.", "createGroupChat": "You have been added to a new group chat by {{ placeholderValues.1 }} in course {{ placeholderValues.0 }}.", "dataExportCreated": "Your data export has been created and can be downloaded.", - "dataExportFailed": "Your data export could not be created. Please try again later." + "dataExportFailed": "Your data export could not be created. Please try again later.", + "sshKeyAdded": "You have successfully added a new SSH key", + "sshKeyExpiresSoon": "Your SSH key with the label \"{{ placeholderValues.0}}\" will expire on {{ placeholderValues.1 }}.", + "sshKeyHasExpired": "Your SSH key with the label \"{{ placeholderValues.0}}\" has expired on {{ placeholderValues.1 }}." } }, "tutorialGroupNotification": { From bf99c2112bc02c128a9bf861d3edc4f25db29ac7 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 3 Dec 2024 12:17:07 +0100 Subject: [PATCH 03/15] let keys expire on 3:00 --- .../UserSshPublicKeyExpiryNotificationService.java | 4 ++-- .../service/sshuserkeys/UserSshPublicKeyService.java | 7 ++++++- .../details/ssh-user-settings-key-details.component.html | 3 ++- .../details/ssh-user-settings-key-details.component.ts | 5 +++++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java index aff3dff21f05..2cbe6cfd304d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java @@ -32,7 +32,7 @@ public UserSshPublicKeyExpiryNotificationService(UserSshPublicKeyRepository user /** * Notifies the user one week in advance about the upcoming expiry of one of their SSH keys */ - @Scheduled(cron = "0 0 6 * * *") // execute this every night at 6:00:00 am + @Scheduled(cron = "0 0 7 * * *") // execute this every night at 7:00:00 am public void notifyUserOnUpcomingKeyExpiry() { ZonedDateTime fromDate = ZonedDateTime.now().plusDays(6); ZonedDateTime toDate = ZonedDateTime.now().plusDays(7); @@ -45,7 +45,7 @@ public void notifyUserOnUpcomingKeyExpiry() { /** * Notifies the user about the expiry of one of their SSH keys */ - @Scheduled(cron = "0 0 6 * * *") // execute this every night at 6:00:00 am + @Scheduled(cron = "0 0 7 * * *") // execute this every night at 7:00:00 am public void notifyUserOnKeyExpiry() { ZonedDateTime fromDate = ZonedDateTime.now().minusDays(1); ZonedDateTime toDate = ZonedDateTime.now(); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java index 13129c15bff3..50b537f9fe40 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java @@ -59,7 +59,12 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP newUserSshPublicKey.setKeyHash(keyHash); setLabelForKey(newUserSshPublicKey, sshPublicKey.label()); newUserSshPublicKey.setCreationDate(ZonedDateTime.now()); - newUserSshPublicKey.setExpiryDate(sshPublicKey.expiryDate()); + + if (sshPublicKey.expiryDate() != null) { + var expiryDate = sshPublicKey.expiryDate().withHour(3).withMinute(0).withSecond(0).withNano(0).plusDays(1); + newUserSshPublicKey.setExpiryDate(expiryDate); + } + userSshPublicKeyRepository.save(newUserSshPublicKey); singleUserNotificationService.notifyUserAboutNewlyAddedSshKey(user, newUserSshPublicKey); } diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html index d1ecbd74bbc3..3d7f5c9874d3 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html @@ -72,6 +72,7 @@

} @if (displayedExpiryDate) {
-
+
{{ displayedExpiryDate | artemisDate: 'long-date' }}
diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts index 459a0c2edc4e..0d559818052b 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -9,6 +9,7 @@ import { getOS } from 'app/shared/util/os-detector.util'; import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; import dayjs from 'dayjs/esm'; import { SshUserSettingsService } from 'app/shared/user-settings/ssh-settings/ssh-user-settings.service'; +import { DateTimePickerType } from 'app/shared/date-time-picker/date-time-picker.component'; @Component({ selector: 'jhi-account-information', @@ -44,6 +45,7 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { displayedKeyLabel = ''; displayedSshKey = ''; displayedKeyHash = ''; + hasExpired? = false; displayedExpiryDate?: dayjs.Dayjs; isExpiryDateValid = false; displayCreationDate: dayjs.Dayjs; @@ -80,6 +82,7 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { this.displayCreationDate = publicKey.creationDate; this.displayedExpiryDate = publicKey.expiryDate; this.displayedLastUsedDate = publicKey.lastUsedDate; + this.hasExpired = publicKey.expiryDate && dayjs().isAfter(dayjs(publicKey.expiryDate)); this.isLoading = false; }), ) @@ -138,4 +141,6 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { this.copyInstructions = 'Ctrl + C'; } } + + protected readonly DateTimePickerType = DateTimePickerType; } From 4bda92234a88e6d4f60a105c064ac77a6d4d19a6 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 3 Dec 2024 12:25:53 +0100 Subject: [PATCH 04/15] execute every minute (todo remove this again) --- .../UserSshPublicKeyExpiryNotificationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java index 2cbe6cfd304d..969952d62c4a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java @@ -32,7 +32,7 @@ public UserSshPublicKeyExpiryNotificationService(UserSshPublicKeyRepository user /** * Notifies the user one week in advance about the upcoming expiry of one of their SSH keys */ - @Scheduled(cron = "0 0 7 * * *") // execute this every night at 7:00:00 am + @Scheduled(cron = "0 * * * * *") // execute this every morning at 7:00:00 am public void notifyUserOnUpcomingKeyExpiry() { ZonedDateTime fromDate = ZonedDateTime.now().plusDays(6); ZonedDateTime toDate = ZonedDateTime.now().plusDays(7); @@ -45,7 +45,7 @@ public void notifyUserOnUpcomingKeyExpiry() { /** * Notifies the user about the expiry of one of their SSH keys */ - @Scheduled(cron = "0 0 7 * * *") // execute this every night at 7:00:00 am + @Scheduled(cron = "0 * * * * *") // execute this every morning at 7:00:00 am public void notifyUserOnKeyExpiry() { ZonedDateTime fromDate = ZonedDateTime.now().minusDays(1); ZonedDateTime toDate = ZonedDateTime.now(); From 1189383d70dc3efd70c2df3aefabd2ac12401ad0 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 3 Dec 2024 13:18:51 +0100 Subject: [PATCH 05/15] set cron task to correct time (7am) again --- .../UserSshPublicKeyExpiryNotificationService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java index 969952d62c4a..f4c2141c6aaf 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java @@ -32,7 +32,7 @@ public UserSshPublicKeyExpiryNotificationService(UserSshPublicKeyRepository user /** * Notifies the user one week in advance about the upcoming expiry of one of their SSH keys */ - @Scheduled(cron = "0 * * * * *") // execute this every morning at 7:00:00 am + @Scheduled(cron = "0 0 7 * * *") // execute this every morning at 7:00:00 am public void notifyUserOnUpcomingKeyExpiry() { ZonedDateTime fromDate = ZonedDateTime.now().plusDays(6); ZonedDateTime toDate = ZonedDateTime.now().plusDays(7); @@ -45,7 +45,7 @@ public void notifyUserOnUpcomingKeyExpiry() { /** * Notifies the user about the expiry of one of their SSH keys */ - @Scheduled(cron = "0 * * * * *") // execute this every morning at 7:00:00 am + @Scheduled(cron = "0 0 7 * * *") // execute this every morning at 7:00:00 am public void notifyUserOnKeyExpiry() { ZonedDateTime fromDate = ZonedDateTime.now().minusDays(1); ZonedDateTime toDate = ZonedDateTime.now(); From 62c454faa64b8b0407b40840dab5c1d5408dbf86 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 3 Dec 2024 15:14:49 +0100 Subject: [PATCH 06/15] added integration tests --- .../SingleUserNotificationServiceTest.java | 107 +++++++++++++++++- .../icl/LocalVCSshSettingsTest.java | 6 + 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java index 347438c89a97..d581ac8bc172 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java @@ -22,6 +22,9 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.MESSAGE_REPLY_IN_CONVERSATION_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.NEW_PLAGIARISM_CASE_STUDENT_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.PLAGIARISM_CASE_VERDICT_STUDENT_TITLE; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_ADDED_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_EXPIRES_SOON_TEXT; +import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.SSH_KEY_HAS_EXPIRED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TEXT; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_ASSIGNED_TITLE; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_DEREGISTRATION_STUDENT_TITLE; @@ -49,6 +52,7 @@ import static org.mockito.Mockito.verify; import java.io.IOException; +import java.security.GeneralSecurityException; import java.time.ZonedDateTime; import java.util.Comparator; import java.util.List; @@ -58,7 +62,10 @@ import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; +import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -101,6 +108,9 @@ import de.tum.cit.aet.artemis.plagiarism.domain.PlagiarismVerdict; import de.tum.cit.aet.artemis.plagiarism.domain.text.TextPlagiarismResult; import de.tum.cit.aet.artemis.plagiarism.domain.text.TextSubmissionElement; +import de.tum.cit.aet.artemis.programming.dto.UserSshPublicKeyDTO; +import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyExpiryNotificationService; +import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyService; import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationIndependentTest; import de.tum.cit.aet.artemis.text.domain.TextExercise; import de.tum.cit.aet.artemis.text.util.TextExerciseFactory; @@ -134,6 +144,12 @@ class SingleUserNotificationServiceTest extends AbstractSpringIntegrationIndepen @Autowired private ParticipationUtilService participationUtilService; + @Autowired + private UserSshPublicKeyExpiryNotificationService userSshPublicKeyExpiryNotificationService; + + @Autowired + private UserSshPublicKeyService userSshPublicKeyService; + @Captor private ArgumentCaptor appleNotificationCaptor; @@ -379,6 +395,95 @@ void testNotifyUsersAboutAssessedExerciseSubmission() { assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(studentWithParticipationAndSubmissionAndManualResult); } + // UserSshPublicKey related (expiry warning and newly created key) + + @Nested + class UserSshPublicKeyExpiryNotificationServiceShould { + + String RSA_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbgjoSpKnry5yuMiWh/uwhMG2Jq5Sh8Uw9vz+39or2i"; + + long KEY_ID = 4L; + + String KEY_LABEL = "key "; + + List sentNotifications; + + @AfterEach + void tearDown() { + assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.getFirst()).getText()).isEqualTo(SSH_KEY_ADDED_TEXT); + + userSshPublicKeyRepository.deleteAll(); + } + + @Test + void notifyUserAboutNewlyCreatedSshKeyWithExpirationDate() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, null); + + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + sentNotifications = notificationRepository.findAll(); + } + + @Test + void notifyUserAboutNewlyCreatedSshKeyWithNoDate() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(15)); + + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + sentNotifications = notificationRepository.findAll(); + } + + @Test + void notifyUserAboutUpcomingSshKeyExpiry() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(6)); + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + userSshPublicKeyExpiryNotificationService.notifyUserOnUpcomingKeyExpiry(); + + sentNotifications = notificationRepository.findAll(); + assertThat(sentNotifications).hasSize(2); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.get(1)).getText()).isEqualTo(SSH_KEY_EXPIRES_SOON_TEXT); + } + + @Test + void notifyUserAboutExpiredSshKey() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().minusDays(1)); + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + userSshPublicKeyExpiryNotificationService.notifyUserOnKeyExpiry(); + + sentNotifications = notificationRepository.findAll(); + assertThat(sentNotifications).hasSize(2); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.get(1)).getText()).isEqualTo(SSH_KEY_HAS_EXPIRED_TEXT); + } + + @Test + void notNotifyUserAboutUpcomingSshKeyExpiryWhenKeyDoesNotExpireSoon() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(100)); + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + userSshPublicKeyExpiryNotificationService.notifyUserOnUpcomingKeyExpiry(); + + sentNotifications = notificationRepository.findAll(); + assertThat(sentNotifications).hasSize(1); + } + + @Test + void notNotifyUserAboutExpiredSshKeyWhenKeyIsNotExpired() throws GeneralSecurityException, IOException { + UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(100)); + userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); + + userSshPublicKeyExpiryNotificationService.notifyUserOnKeyExpiry(); + + sentNotifications = notificationRepository.findAll(); + assertThat(sentNotifications).hasSize(1); + } + } + // Plagiarism related /** @@ -576,7 +681,7 @@ void testDataExportNotification_dataExportFailed() { } /** - * Checks if an email was created and send + * Checks if an email was created and sent */ private void verifyEmail() { verify(javaMailSender, timeout(1000)).send(any(MimeMessage.class)); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshSettingsTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshSettingsTest.java index fffe4a6ec0b3..bdcb2a2741b8 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshSettingsTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshSettingsTest.java @@ -1,8 +1,12 @@ package de.tum.cit.aet.artemis.programming.icl; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.test.context.support.WithMockUser; @@ -18,11 +22,13 @@ class LocalVCSshSettingsTest extends AbstractSpringIntegrationLocalCILocalVCTest @BeforeEach void setUp() throws Exception { + doNothing().when(singleUserNotificationService).notifyUserAboutNewlyAddedSshKey(any(), any()); sshSettingsTestService.setup(TEST_PREFIX); } @AfterEach void teardown() throws Exception { + Mockito.reset(singleUserNotificationService); sshSettingsTestService.tearDown(TEST_PREFIX); } From 2232df36c52c3576c51dc337063d66d6fe5235c5 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 3 Dec 2024 17:11:25 +0100 Subject: [PATCH 07/15] move asserts from teardown into check method --- .../SingleUserNotificationServiceTest.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java index d581ac8bc172..381427f66d9d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java @@ -410,10 +410,6 @@ class UserSshPublicKeyExpiryNotificationServiceShould { @AfterEach void tearDown() { - assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); - assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); - assertThat((sentNotifications.getFirst()).getText()).isEqualTo(SSH_KEY_ADDED_TEXT); - userSshPublicKeyRepository.deleteAll(); } @@ -424,6 +420,7 @@ void notifyUserAboutNewlyCreatedSshKeyWithExpirationDate() throws GeneralSecurit userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); sentNotifications = notificationRepository.findAll(); + checkFirstNotification(); } @Test @@ -433,6 +430,7 @@ void notifyUserAboutNewlyCreatedSshKeyWithNoDate() throws GeneralSecurityExcepti userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); sentNotifications = notificationRepository.findAll(); + checkFirstNotification(); } @Test @@ -446,6 +444,7 @@ void notifyUserAboutUpcomingSshKeyExpiry() throws GeneralSecurityException, IOEx assertThat(sentNotifications).hasSize(2); assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); assertThat((sentNotifications.get(1)).getText()).isEqualTo(SSH_KEY_EXPIRES_SOON_TEXT); + checkFirstNotification(); } @Test @@ -459,6 +458,7 @@ void notifyUserAboutExpiredSshKey() throws GeneralSecurityException, IOException assertThat(sentNotifications).hasSize(2); assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); assertThat((sentNotifications.get(1)).getText()).isEqualTo(SSH_KEY_HAS_EXPIRED_TEXT); + checkFirstNotification(); } @Test @@ -470,6 +470,7 @@ void notNotifyUserAboutUpcomingSshKeyExpiryWhenKeyDoesNotExpireSoon() throws Gen sentNotifications = notificationRepository.findAll(); assertThat(sentNotifications).hasSize(1); + checkFirstNotification(); } @Test @@ -481,6 +482,13 @@ void notNotifyUserAboutExpiredSshKeyWhenKeyIsNotExpired() throws GeneralSecurity sentNotifications = notificationRepository.findAll(); assertThat(sentNotifications).hasSize(1); + checkFirstNotification(); + } + + void checkFirstNotification() { + assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); + assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); + assertThat((sentNotifications.getFirst()).getText()).isEqualTo(SSH_KEY_ADDED_TEXT); } } From cf1d06011907f8de132328623da878a16c641542 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 7 Dec 2024 12:34:48 +0100 Subject: [PATCH 08/15] remove notification placeholder --- .../notification/SingleUserNotificationFactory.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java index 4800a35fd85b..5d224c216837 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java @@ -75,7 +75,6 @@ import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createPlagiarismCaseTarget; import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createTutorialGroupTarget; -import java.time.format.DateTimeFormatter; import java.util.Set; import jakarta.validation.constraints.NotNull; @@ -174,20 +173,15 @@ public static SingleUserNotification createNotification(UserSshPublicKey key, No return new SingleUserNotification(recipient, SSH_KEY_ADDED_TITLE, SSH_KEY_ADDED_TEXT, true, new String[] {}); } case SSH_KEY_EXPIRES_SOON -> { - return new SingleUserNotification(recipient, SSH_KEY_EXPIRES_SOON_TITLE, SSH_KEY_EXPIRES_SOON_TEXT, true, createPlaceholdersSshKeyNotification(key)); + return new SingleUserNotification(recipient, SSH_KEY_EXPIRES_SOON_TITLE, SSH_KEY_EXPIRES_SOON_TEXT, true, new String[] {}); } case SSH_KEY_HAS_EXPIRED -> { - return new SingleUserNotification(recipient, SSH_KEY_HAS_EXPIRED_TITLE, SSH_KEY_HAS_EXPIRED_TEXT, true, createPlaceholdersSshKeyNotification(key)); + return new SingleUserNotification(recipient, SSH_KEY_HAS_EXPIRED_TITLE, SSH_KEY_HAS_EXPIRED_TEXT, true, new String[] {}); } default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); } } - @NotificationPlaceholderCreator(values = { SSH_KEY_EXPIRES_SOON, SSH_KEY_HAS_EXPIRED }) - public static String[] createPlaceholdersSshKeyNotification(UserSshPublicKey key) { - return new String[] { key.getLabel(), key.getExpiryDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm")) }; - } - /** * Creates an instance of SingleUserNotification based on plagiarisms. * From 23773829b4198f03eace96b44ed3f50d647bc509 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 7 Dec 2024 16:36:47 +0100 Subject: [PATCH 09/15] added java docs --- .../notification/SingleUserNotificationFactory.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java index 5d224c216837..89df0016e4da 100644 --- a/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java +++ b/src/main/java/de/tum/cit/aet/artemis/communication/domain/notification/SingleUserNotificationFactory.java @@ -167,6 +167,15 @@ public static String[] createPlaceholdersDataExport() { return new String[] {}; } + /** + * Creates a user notification based on the given SSH key and notification type. + * + * @param key The SSH key related to the notification (currently unused). + * @param notificationType The type of notification to create (e.g., key added, expiring, or expired). + * @param recipient The user who will receive the notification. + * @return A configured {@link SingleUserNotification}. + * @throws UnsupportedOperationException if the notification type is unsupported. + */ public static SingleUserNotification createNotification(UserSshPublicKey key, NotificationType notificationType, User recipient) { switch (notificationType) { case SSH_KEY_ADDED -> { From da8822c6a689803c563e26ac7d152d9f7685c1b8 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 10 Dec 2024 14:19:51 +0100 Subject: [PATCH 10/15] make mails consistent --- .../templates/mail/notification/sshKeyAddedEmail.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html b/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html index f6919ce7c289..b79a14946627 100644 --- a/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html +++ b/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html @@ -14,10 +14,12 @@ The following SSH key was added to your account:

- + SSH key label + SSH key label

- + SSH key hash + SSH key hash

From 66da8a7c179f8ada34538ebb412d9e72b93f291b Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:30:37 +0100 Subject: [PATCH 11/15] Update src/main/resources/i18n/messages_en.properties Co-authored-by: Yannik Schmidt --- src/main/resources/i18n/messages_en.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties index 5cc1bab0b4f0..01e56d22ce6a 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -128,7 +128,7 @@ email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} # SSH User Settings email.sshKeyAdded.title = New SSH key added to account -email.notification.sshKeyAdded.title = The following SSH key was added to your account: +email.notification.sshKeyAdded.title = A new SSH key was added to your account. email.notification.sshKeyAdded.ifMistake = If you believe this key was added in error, you can remove the key and disable access at the following location: email.notification.sshKeyExpiry.sshKeyExpiresSoonWarning = One of your SSH keys will expire in a few days. email.notification.sshKeyExpiry.sshKeysHasExpiredWarning = One of your SSH keys has expired. From 5cf594d12a6deca0bdd94bbe04d1917b9e7b95c1 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:30:52 +0100 Subject: [PATCH 12/15] Update src/main/resources/i18n/messages_de.properties Co-authored-by: Yannik Schmidt --- src/main/resources/i18n/messages_de.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties index 0f9b45ee28a9..0960e6223fec 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -128,7 +128,7 @@ email.successfulDataExportCreationsAdmin.userLogin = \u2022 {0} # SSH User Settings email.sshKeyAdded.title = Neuer SSH-Schlüssel zu Account hinzugefügt -email.notification.sshKeyAdded.title = Der folgende SSH-Schlüssel wurde zu deinem Benutzerkonto hinzugefügt: +email.notification.sshKeyAdded.title = Ein neuer SSH-Schlüssel wurde zu deinem Benutzerkonto hinzugefügt. email.notification.sshKeyAdded.ifMistake = Wenn du glaubst, dass dieser Schlüssel irrtümlich hinzugefügt wurde, kannst du den Schlüssel entfernen und den Zugriff an folgender Stelle deaktivieren: email.notification.sshKeyExpiry.sshKeyExpiresSoonWarning = Einer deiner SSH-Schlüssel läuft in wenigen Tagen ab. email.notification.sshKeyExpiry.sshKeysHasExpiredWarning = Einer deiner SSH-Schlüssel ist abgelaufen. From abc334ae257a9adade1453e9276e863579c17530 Mon Sep 17 00:00:00 2001 From: entholzer Date: Fri, 13 Dec 2024 17:09:27 +0100 Subject: [PATCH 13/15] add foreign key constraint migration for user_id --- .../liquibase/changelog/20241213144500_changelog.xml | 12 ++++++++++++ src/main/resources/config/liquibase/master.xml | 1 + 2 files changed, 13 insertions(+) create mode 100644 src/main/resources/config/liquibase/changelog/20241213144500_changelog.xml diff --git a/src/main/resources/config/liquibase/changelog/20241213144500_changelog.xml b/src/main/resources/config/liquibase/changelog/20241213144500_changelog.xml new file mode 100644 index 000000000000..7d9c8db45fd5 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241213144500_changelog.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 9dd06528e6f3..d1012d23a45b 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -41,6 +41,7 @@ + From f98546782e52ebf2fcb269226a28dde13f058d4e Mon Sep 17 00:00:00 2001 From: entholzer Date: Fri, 13 Dec 2024 17:17:42 +0100 Subject: [PATCH 14/15] refactored for improved query usage --- .../core/repository/UserRepository.java | 2 + .../core/service/user/UserService.java | 8 ++- .../UserSshPublicKeyRepository.java | 6 ++ ...SshPublicKeyExpiryNotificationService.java | 58 ++++++++++++++----- .../sshuserkeys/UserSshPublicKeyService.java | 9 +++ .../SingleUserNotificationServiceTest.java | 26 ++++++--- 6 files changed, 83 insertions(+), 26 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java index 0d3280cf5d96..e2a8206a1b78 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java @@ -580,6 +580,8 @@ default Page searchAllUsersByLoginOrNameInGroupAndConvertToDTO(Pageable @EntityGraph(type = LOAD, attributePaths = { "groups", "authorities" }) Set findAllWithGroupsAndAuthoritiesByIsDeletedIsFalseAndLoginIn(Set logins); + List findAllByIdIn(List ids); + /** * Searches for users by their login or full name. * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java index 4ac3f22aaa17..2ba2fc4ef90d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/service/user/UserService.java @@ -66,6 +66,7 @@ import de.tum.cit.aet.artemis.programming.domain.ParticipationVCSAccessToken; import de.tum.cit.aet.artemis.programming.service.ParticipationVcsAccessTokenService; import de.tum.cit.aet.artemis.programming.service.ci.CIUserManagementService; +import de.tum.cit.aet.artemis.programming.service.sshuserkeys.UserSshPublicKeyService; import de.tum.cit.aet.artemis.programming.service.vcs.VcsUserManagementService; import tech.jhipster.security.RandomUtil; @@ -117,11 +118,13 @@ public class UserService { private final SavedPostRepository savedPostRepository; + private final UserSshPublicKeyService userSshPublicKeyService; + public UserService(UserCreationService userCreationService, UserRepository userRepository, AuthorityService authorityService, AuthorityRepository authorityRepository, CacheManager cacheManager, Optional ldapUserService, GuidedTourSettingsRepository guidedTourSettingsRepository, PasswordService passwordService, Optional optionalVcsUserManagementService, Optional optionalCIUserManagementService, InstanceMessageSendService instanceMessageSendService, FileService fileService, ScienceEventApi scienceEventApi, - ParticipationVcsAccessTokenService participationVCSAccessTokenService, SavedPostRepository savedPostRepository) { + ParticipationVcsAccessTokenService participationVCSAccessTokenService, SavedPostRepository savedPostRepository, UserSshPublicKeyService userSshPublicKeyService) { this.userCreationService = userCreationService; this.userRepository = userRepository; this.authorityService = authorityService; @@ -137,6 +140,7 @@ public UserService(UserCreationService userCreationService, UserRepository userR this.scienceEventApi = scienceEventApi; this.participationVCSAccessTokenService = participationVCSAccessTokenService; this.savedPostRepository = savedPostRepository; + this.userSshPublicKeyService = userSshPublicKeyService; } /** @@ -321,6 +325,7 @@ public User registerUser(UserDTO userDTO, String password) { catch (VersionControlException e) { log.error("An error occurred while registering GitLab user {}:", savedNonActivatedUser.getLogin(), e); participationVCSAccessTokenService.deleteAllByUserId(savedNonActivatedUser.getId()); + userSshPublicKeyService.deleteAllByUserId(savedNonActivatedUser.getId()); userRepository.delete(savedNonActivatedUser); clearUserCaches(savedNonActivatedUser); userRepository.flush(); @@ -469,6 +474,7 @@ public void updateUserInConnectorsAndAuthProvider(User user, String oldUserLogin public void softDeleteUser(String login) { userRepository.findOneWithGroupsByLogin(login).ifPresent(user -> { participationVCSAccessTokenService.deleteAllByUserId(user.getId()); + userSshPublicKeyService.deleteAllByUserId(user.getId()); user.setDeleted(true); anonymizeUser(user); log.warn("Soft Deleted User: {}", user); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java index 303d4acfb893..c87d229f1e75 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java @@ -7,7 +7,9 @@ import java.util.Optional; import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository; import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; @@ -27,4 +29,8 @@ public interface UserSshPublicKeyRepository extends ArtemisJpaRepository { - var user = userRepository.findById(userSshPublicKey.getUserId()); - user.ifPresent(recipient -> singleUserNotificationService.notifyUserAboutSoonExpiringSshKey(recipient, userSshPublicKey)); + @Scheduled(cron = "0 * * * * *") + public void sendKeyExpirationNotifications() { + notifyUserOnExpiredKey(); + notifyUserOnUpcomingKeyExpiry(); + } + + /** + * Notifies the user at the day of key expiry, that the key has expired + */ + public void notifyUserOnExpiredKey() { + notifyUsersForKeyExpiryWindow(now().minusDays(1), now(), (user, key) -> { + singleUserNotificationService.notifyUserAboutExpiredSshKey(user, key); + return null; }); } /** - * Notifies the user about the expiry of one of their SSH keys + * Notifies the user one week in advance about the upcoming expiry */ - @Scheduled(cron = "0 0 7 * * *") // execute this every morning at 7:00:00 am - public void notifyUserOnKeyExpiry() { - ZonedDateTime fromDate = ZonedDateTime.now().minusDays(1); - ZonedDateTime toDate = ZonedDateTime.now(); - userSshPublicKeyRepository.findByExpiryDateBetween(fromDate, toDate).forEach(userSshPublicKey -> { - var user = userRepository.findById(userSshPublicKey.getUserId()); - user.ifPresent(recipient -> singleUserNotificationService.notifyUserAboutExpiredSshKey(recipient, userSshPublicKey)); + public void notifyUserOnUpcomingKeyExpiry() { + notifyUsersForKeyExpiryWindow(now().plusDays(6), now().plusDays(7), (user, key) -> { + singleUserNotificationService.notifyUserAboutSoonExpiringSshKey(user, key); + return null; }); } + + /** + * Notifies users whose SSH keys are expiring within the specified date range, with the notification specified by the + * notifyFunction + * + * @param fromDate the start of the expiry date range + * @param toDate the end of the expiry date range + * @param notifyFunction a function to handle user notification + */ + private void notifyUsersForKeyExpiryWindow(ZonedDateTime fromDate, ZonedDateTime toDate, BiFunction notifyFunction) { + var soonExpiringKeys = userSshPublicKeyRepository.findByExpiryDateBetween(fromDate, toDate); + List users = userRepository.findAllByIdIn(soonExpiringKeys.stream().map(UserSshPublicKey::getUserId).toList()); + Map userMap = users.stream().collect(Collectors.toMap(User::getId, Function.identity())); + soonExpiringKeys.forEach(key -> notifyFunction.apply(userMap.get(key.getUserId()), key)); + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java index 50b537f9fe40..fba1cd8cd4f9 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java @@ -146,4 +146,13 @@ public void deleteUserSshPublicKey(Long userId, Long keyId) { public boolean hasUserSSHkeys(Long userId) { return userSshPublicKeyRepository.existsByUserId(userId); } + + /** + * Deletes all the ssh keys of a user + * + * @param userId the ID of the user. + */ + public void deleteAllByUserId(Long userId) { + userSshPublicKeyRepository.deleteAllByUserId(userId); + } } diff --git a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java index 379bf9165b77..0dbdfae61e53 100644 --- a/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/communication/notification/SingleUserNotificationServiceTest.java @@ -401,7 +401,7 @@ void testNotifyUsersAboutAssessedExerciseSubmission() { // UserSshPublicKey related (expiry warning and newly created key) @Nested - class UserSshPublicKeyExpiryNotificationServiceShould { + class UserSshPublicKeyExpiryNotification { String RSA_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbgjoSpKnry5yuMiWh/uwhMG2Jq5Sh8Uw9vz+39or2i"; @@ -417,7 +417,7 @@ void tearDown() { } @Test - void notifyUserAboutNewlyCreatedSshKeyWithExpirationDate() throws GeneralSecurityException, IOException { + void shouldNotifyUserAboutNewlyCreatedSshKeyWithExpirationDate() throws GeneralSecurityException, IOException { UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, null); userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); @@ -427,7 +427,7 @@ void notifyUserAboutNewlyCreatedSshKeyWithExpirationDate() throws GeneralSecurit } @Test - void notifyUserAboutNewlyCreatedSshKeyWithNoDate() throws GeneralSecurityException, IOException { + void shouldNotifyUserAboutNewlyCreatedSshKeyWithNoDate() throws GeneralSecurityException, IOException { UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(15)); userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); @@ -437,7 +437,7 @@ void notifyUserAboutNewlyCreatedSshKeyWithNoDate() throws GeneralSecurityExcepti } @Test - void notifyUserAboutUpcomingSshKeyExpiry() throws GeneralSecurityException, IOException { + void shouldNotifyUserAboutUpcomingSshKeyExpiry() throws GeneralSecurityException, IOException { UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(6)); userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); @@ -451,11 +451,11 @@ void notifyUserAboutUpcomingSshKeyExpiry() throws GeneralSecurityException, IOEx } @Test - void notifyUserAboutExpiredSshKey() throws GeneralSecurityException, IOException { + void shouldNotifyUserAboutExpiredSshKey() throws GeneralSecurityException, IOException { UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().minusDays(1)); userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); - userSshPublicKeyExpiryNotificationService.notifyUserOnKeyExpiry(); + userSshPublicKeyExpiryNotificationService.notifyUserOnExpiredKey(); sentNotifications = notificationRepository.findAll(); assertThat(sentNotifications).hasSize(2); @@ -465,7 +465,7 @@ void notifyUserAboutExpiredSshKey() throws GeneralSecurityException, IOException } @Test - void notNotifyUserAboutUpcomingSshKeyExpiryWhenKeyDoesNotExpireSoon() throws GeneralSecurityException, IOException { + void shouldNotNotifyUserAboutUpcomingSshKeyExpiryWhenKeyDoesNotExpireSoon() throws GeneralSecurityException, IOException { UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(100)); userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); @@ -477,17 +477,25 @@ void notNotifyUserAboutUpcomingSshKeyExpiryWhenKeyDoesNotExpireSoon() throws Gen } @Test - void notNotifyUserAboutExpiredSshKeyWhenKeyIsNotExpired() throws GeneralSecurityException, IOException { + void shouldNotNotifyUserAboutExpiredSshKeyWhenKeyIsNotExpired() throws GeneralSecurityException, IOException { UserSshPublicKeyDTO keyDTO = new UserSshPublicKeyDTO(KEY_ID, KEY_LABEL, RSA_KEY, null, null, null, ZonedDateTime.now().plusDays(100)); userSshPublicKeyService.createSshKeyForUser(user, AuthorizedKeyEntry.parseAuthorizedKeyEntry(keyDTO.publicKey()), keyDTO); - userSshPublicKeyExpiryNotificationService.notifyUserOnKeyExpiry(); + userSshPublicKeyExpiryNotificationService.notifyUserOnExpiredKey(); sentNotifications = notificationRepository.findAll(); assertThat(sentNotifications).hasSize(1); checkFirstNotification(); } + @Test + void scheduleKeyExpiryNotifications() { + userSshPublicKeyExpiryNotificationService.sendKeyExpirationNotifications(); + + sentNotifications = notificationRepository.findAll(); + assertThat(sentNotifications).hasSize(0); + } + void checkFirstNotification() { assertThat(sentNotifications.getFirst()).isInstanceOf(SingleUserNotification.class); assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(user); From 3abb64bfefb8e5bf06ce873458ae539f610a33be Mon Sep 17 00:00:00 2001 From: entholzer Date: Fri, 13 Dec 2024 17:20:15 +0100 Subject: [PATCH 15/15] set time for scheduled task back to 7:00 --- .../sshuserkeys/UserSshPublicKeyExpiryNotificationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java index 91ca9bfb503d..c72c4aa2b7e2 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java @@ -40,7 +40,7 @@ public UserSshPublicKeyExpiryNotificationService(UserSshPublicKeyRepository user /** * Schedules SSH key expiry notifications to users every morning at 7:00:00 am */ - @Scheduled(cron = "0 * * * * *") + @Scheduled(cron = "0 0 7 * * *") public void sendKeyExpirationNotifications() { notifyUserOnExpiredKey(); notifyUserOnUpcomingKeyExpiry();