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..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 + 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 46f1ff9a2218..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 @@ -43,6 +43,9 @@ 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.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; @@ -147,6 +150,12 @@ 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"; + + 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"; @@ -280,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) @@ -305,7 +320,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).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 301cb1bf955b..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 @@ -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,6 +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; @@ -80,6 +88,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 { @@ -158,6 +167,30 @@ 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 -> { + 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, new String[] {}); + } + case SSH_KEY_HAS_EXPIRED -> { + return new SingleUserNotification(recipient, SSH_KEY_HAS_EXPIRED_TITLE, SSH_KEY_HAS_EXPIRED_TEXT, true, new String[] {}); + } + default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType); + } + } + /** * 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 88926325aa4e..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; @@ -33,6 +34,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 +88,10 @@ public class MailService implements InstantNotificationService { private static final String NOTIFICATION_TYPE = "notificationType"; + 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"; @@ -263,6 +269,12 @@ 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 (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) { setContextForTutorialGroupNotifications(context, notificationType, tutorialGroupNotificationSubject); @@ -393,6 +405,10 @@ 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); + 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 62f4912b9332..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 @@ -32,6 +32,9 @@ 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.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; @@ -134,6 +137,13 @@ 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"; + + 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( @@ -173,9 +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(true, true, true, NOTIFICATION_USER_NOTIFICATION_DATA_EXPORT_CREATED), + 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 @@ -209,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/ @@ -219,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); + 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 33def5698b9f..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 @@ -13,6 +13,9 @@ 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.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; @@ -71,6 +74,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 +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, SSH_KEY_EXPIRES_SOON, SSH_KEY_HAS_EXPIRED -> createNotification((UserSshPublicKey) notificationSubject, notificationType, typeSpecificInformation); default -> throw new UnsupportedOperationException("Can not create notification for type : " + notificationType); }; } @@ -256,6 +261,36 @@ 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 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. * 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 b177b7b72089..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 @@ -2,11 +2,14 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE; +import java.time.ZonedDateTime; import java.util.List; 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; @@ -21,7 +24,13 @@ 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); + + @Transactional // ok because of delete + @Modifying + void deleteAllByUserId(Long id); } 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..c72c4aa2b7e2 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyExpiryNotificationService.java @@ -0,0 +1,83 @@ +package de.tum.cit.aet.artemis.programming.service.sshuserkeys; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_SCHEDULING; +import static java.time.ZonedDateTime.now; + +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +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.domain.User; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; +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; + } + + /** + * Schedules SSH key expiry notifications to users every morning at 7:00:00 am + */ + @Scheduled(cron = "0 0 7 * * *") + 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 one week in advance about the upcoming expiry + */ + 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/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/sshuserkeys/UserSshPublicKeyService.java similarity index 86% 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 232afd327663..fba1cd8cd4f9 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; @@ -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; } /** @@ -55,8 +59,14 @@ 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); } /** @@ -136,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/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 8eb38fdfd263..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 @@ -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/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 1dbecdb3ea97..971258cf6d69 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -42,6 +42,7 @@ + diff --git a/src/main/resources/i18n/messages.properties b/src/main/resources/i18n/messages.properties index 0879e526d4f4..6580e702bd3c 100644 --- a/src/main/resources/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -126,6 +126,17 @@ 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.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 artemisApp.groupNotification.title.attachmentChange = Attachment updated @@ -152,3 +163,6 @@ 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 289a66b117a2..0960e6223fec 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! @@ -125,6 +125,18 @@ 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 = 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. +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 artemisApp.groupNotification.title.attachmentChange = Anhang aktualisiert @@ -151,3 +163,6 @@ artemisApp.tutorialGroupNotification.title.tutorialGroupUpdated = Übungsgruppe 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.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 ef2bc15f8c9b..01e56d22ce6a 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -126,6 +126,17 @@ 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 = 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. +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 artemisApp.groupNotification.title.attachmentChange = Attachment updated @@ -151,3 +162,6 @@ 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 new file mode 100644 index 000000000000..b79a14946627 --- /dev/null +++ b/src/main/resources/templates/mail/notification/sshKeyAddedEmail.html @@ -0,0 +1,36 @@ + + + + + + + + + +
+ + +

+ The following SSH key was added to your account: +

+

+ SSH key label + SSH key label +

+

+ SSH key hash + 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..260d4f1e3f6f --- /dev/null +++ b/src/main/resources/templates/mail/notification/sshKeyExpiresSoonEmail.html @@ -0,0 +1,40 @@ + + + + + + + + + +
+ + +

+ 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/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 661cd1c3ae3d..800d0988a07b 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; } 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": { 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 e30b73f91e5b..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 @@ -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; @@ -382,6 +398,111 @@ void testNotifyUsersAboutAssessedExerciseSubmission() { assertThat(((SingleUserNotification) sentNotifications.getFirst()).getRecipient()).isEqualTo(studentWithParticipationAndSubmissionAndManualResult); } + // UserSshPublicKey related (expiry warning and newly created key) + + @Nested + class UserSshPublicKeyExpiryNotification { + + String RSA_KEY = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbgjoSpKnry5yuMiWh/uwhMG2Jq5Sh8Uw9vz+39or2i"; + + long KEY_ID = 4L; + + String KEY_LABEL = "key "; + + List sentNotifications; + + @AfterEach + void tearDown() { + userSshPublicKeyRepository.deleteAll(); + } + + @Test + 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); + + sentNotifications = notificationRepository.findAll(); + checkFirstNotification(); + } + + @Test + 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); + + sentNotifications = notificationRepository.findAll(); + checkFirstNotification(); + } + + @Test + 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); + + 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); + checkFirstNotification(); + } + + @Test + 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.notifyUserOnExpiredKey(); + + 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); + checkFirstNotification(); + } + + @Test + 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); + + userSshPublicKeyExpiryNotificationService.notifyUserOnUpcomingKeyExpiry(); + + sentNotifications = notificationRepository.findAll(); + assertThat(sentNotifications).hasSize(1); + checkFirstNotification(); + } + + @Test + 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.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); + assertThat((sentNotifications.getFirst()).getText()).isEqualTo(SSH_KEY_ADDED_TEXT); + } + } + // Plagiarism related /** @@ -581,7 +702,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); }