Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Programming exercises: Add notifications for personal VCS access token creation and expiry #10059

Open
wants to merge 7 commits into
base: feature/ssh/email-notifications-2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ public enum NotificationType {
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, SSH_KEY_EXPIRES_SOON,
SSH_KEY_HAS_EXPIRED
SSH_KEY_HAS_EXPIRED, VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_EXPIRED
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_TUTOR;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UNASSIGNED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UPDATED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_ADDED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRED;

import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
Expand Down Expand Up @@ -156,6 +158,10 @@ public class NotificationConstants {

public static final String SSH_KEY_HAS_EXPIRED_TITLE = "artemisApp.singleUserNotification.title.sshKeyHasExpired";

public static final String VCS_ACCESS_TOKEN_ADDED_TITLE = "artemisApp.singleUserNotification.title.vcsAccessTokenAdded";

public static final String VCS_ACCESS_TOKEN_EXPIRED_TITLE = "artemisApp.singleUserNotification.title.vcsAccessTokenExpired";

// Texts
public static final String LIVE_EXAM_EXERCISE_UPDATE_NOTIFICATION_TEXT = "artemisApp.groupNotification.text.liveExamExerciseUpdate";

Expand Down Expand Up @@ -295,6 +301,10 @@ public class NotificationConstants {

public static final String SSH_KEY_HAS_EXPIRED_TEXT = "artemisApp.singleUserNotification.text.sshKeyHasExpired";

public static final String VCS_ACCESS_TOKEN_ADDED_TEXT = "artemisApp.singleUserNotification.text.vcsAccessTokenAdded";

public static final String VCS_ACCESS_TOKEN_EXPIRED_TEXT = "artemisApp.singleUserNotification.text.vcsAccessTokenExpired";

// bidirectional map
private static final BiMap<NotificationType, String> NOTIFICATION_TYPE_AND_TITLE_MAP = new ImmutableBiMap.Builder<NotificationType, String>()
.put(EXERCISE_SUBMISSION_ASSESSED, EXERCISE_SUBMISSION_ASSESSED_TITLE).put(ATTACHMENT_CHANGE, ATTACHMENT_CHANGE_TITLE).put(EXERCISE_RELEASED, EXERCISE_RELEASED_TITLE)
Expand All @@ -321,7 +331,8 @@ 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).put(SSH_KEY_EXPIRES_SOON, SSH_KEY_EXPIRES_SOON_TITLE).put(SSH_KEY_HAS_EXPIRED, SSH_KEY_HAS_EXPIRED_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)
.put(VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_ADDED_TITLE).put(VCS_ACCESS_TOKEN_EXPIRED, VCS_ACCESS_TOKEN_EXPIRED_TITLE).build();

/**
* Finds the corresponding NotificationType for the provided notification title
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_STUDENT_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_REGISTRATION_TUTOR_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.TUTORIAL_GROUP_UNASSIGNED_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_ADDED_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_ADDED_TITLE;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRED_TEXT;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.VCS_ACCESS_TOKEN_EXPIRED_TITLE;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.findCorrespondingNotificationTitleOrThrow;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createConversationCreationTarget;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationTargetFactory.createConversationDeletionTarget;
Expand Down Expand Up @@ -191,6 +195,27 @@ public static SingleUserNotification createNotification(UserSshPublicKey key, No
}
}

/**
* Creates a user notification based on the given SSH key and notification type.
*
* @param vcsAccessToken The access token of the user
* @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(String vcsAccessToken, NotificationType notificationType, User recipient) {
switch (notificationType) {
case VCS_ACCESS_TOKEN_ADDED -> {
return new SingleUserNotification(recipient, VCS_ACCESS_TOKEN_ADDED_TITLE, VCS_ACCESS_TOKEN_ADDED_TEXT, true, new String[] {});
}
case VCS_ACCESS_TOKEN_EXPIRED -> {
return new SingleUserNotification(recipient, VCS_ACCESS_TOKEN_EXPIRED_TITLE, VCS_ACCESS_TOKEN_EXPIRED_TEXT, true, new String[] {});
}
default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType);
}
}

/**
* Creates an instance of SingleUserNotification based on plagiarisms.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ private String createContentForNotificationEmailByType(NotificationType notifica
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);
case VCS_ACCESS_TOKEN_ADDED -> templateEngine.process("mail/notification/vcsAccessTokenAddedEmail", context);
case VCS_ACCESS_TOKEN_EXPIRED -> templateEngine.process("mail/notification/vcsAccessTokenExpiredEmail", context);

default -> throw new UnsupportedOperationException("Unsupported NotificationType: " + notificationType);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_TUTOR;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UNASSIGNED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UPDATED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_ADDED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRED;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.findCorrespondingNotificationType;
import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

Expand Down Expand Up @@ -144,6 +146,10 @@ public class NotificationSettingsService {

public static final String NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED = "notification.user-notification.ssh-key-has-expired";

public static final String NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_ADDED = "notification.user-notification.vcs-access-token-added";

public static final String NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRED = "notification.user-notification.vcs-access-token-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<NotificationSetting> DEFAULT_NOTIFICATION_SETTINGS = new HashSet<>(Arrays.asList(
Expand Down Expand Up @@ -188,7 +194,9 @@ public class NotificationSettingsService {
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)));
new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED),
new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_ADDED),
new NotificationSetting(true, true, false, NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRED)));

/**
* This is the place where the mapping between SettingId and NotificationTypes happens on the server side
Expand Down Expand Up @@ -225,7 +233,9 @@ public class NotificationSettingsService {
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 }));
Map.entry(NOTIFICATION_USER_NOTIFICATION_SSH_KEY_HAS_EXPIRED, new NotificationType[] { SSH_KEY_HAS_EXPIRED }),
Map.entry(NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_ADDED, new NotificationType[] { VCS_ACCESS_TOKEN_ADDED }),
Map.entry(NOTIFICATION_USER_NOTIFICATION_VCS_ACCESS_TOKEN_EXPIRED, new NotificationType[] { VCS_ACCESS_TOKEN_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/
Expand All @@ -236,7 +246,7 @@ public class NotificationSettingsService {
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, SSH_KEY_EXPIRES_SOON,
SSH_KEY_HAS_EXPIRED);
SSH_KEY_HAS_EXPIRED, VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_STUDENT;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_REGISTRATION_TUTOR;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.TUTORIAL_GROUP_UNASSIGNED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_ADDED;
import static de.tum.cit.aet.artemis.communication.domain.NotificationType.VCS_ACCESS_TOKEN_EXPIRED;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.CONVERSATION_ADD_USER_CHANNEL_TITLE;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.CONVERSATION_ADD_USER_GROUP_CHAT_TITLE;
import static de.tum.cit.aet.artemis.communication.domain.notification.NotificationConstants.CONVERSATION_CREATE_GROUP_CHAT_TITLE;
Expand Down Expand Up @@ -150,6 +152,7 @@ private SingleUserNotification createSingleUserNotification(Object notificationS
((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);
case VCS_ACCESS_TOKEN_ADDED, VCS_ACCESS_TOKEN_EXPIRED -> createNotification(typeSpecificInformation.getVcsAccessToken(), notificationType, typeSpecificInformation);
default -> throw new UnsupportedOperationException("Can not create notification for type : " + notificationType);
};
}
Expand Down Expand Up @@ -282,7 +285,7 @@ public void notifyUserAboutSoonExpiringSshKey(User recipient, UserSshPublicKey k
}

/**
* Notify user about an upcoming expiry of an SSH key
* Notify user about the expiration of an SSH key
*
* @param recipient the user to whose account the SSH key was added
* @param key the key which was added
Expand All @@ -291,6 +294,24 @@ public void notifyUserAboutExpiredSshKey(User recipient, UserSshPublicKey key) {
notifyRecipientWithNotificationType(key, SSH_KEY_HAS_EXPIRED, recipient, null);
}

/**
* Notify user about the addition of a VCS access token
*
* @param recipient the user to whose account the SSH key was added
*/
public void notifyUserAboutNewlyAddedVcsAccessToken(User recipient) {
notifyRecipientWithNotificationType(null, VCS_ACCESS_TOKEN_ADDED, recipient, null);
}

/**
* Notify user about the expiration of the VCS access token
*
* @param recipient the user to whose account the SSH key was added
*/
public void notifyUserAboutExpiredVcsAccessToken(User recipient) {
notifyRecipientWithNotificationType(null, VCS_ACCESS_TOKEN_EXPIRED, recipient, null);
}

/**
* Notify student about possible plagiarism case.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public interface UserRepository extends ArtemisJpaRepository<User, Long>, JpaSpe

Optional<User> findOneByEmailIgnoreCase(String email);

List<User> findByvcsAccessTokenExpiryDateBetween(ZonedDateTime from, ZonedDateTime to);

@EntityGraph(type = LOAD, attributePaths = { "groups" })
Optional<User> findOneWithGroupsByEmailIgnoreCase(String email);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

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.dto.PasswordChangeDTO;
import de.tum.cit.aet.artemis.core.dto.UserDTO;
Expand Down Expand Up @@ -64,15 +65,18 @@ public class AccountResource {

private final FileService fileService;

private final SingleUserNotificationService singleUserNotificationService;

private static final float MAX_PROFILE_PICTURE_FILESIZE_IN_MEGABYTES = 0.1f;

public AccountResource(UserRepository userRepository, UserService userService, UserCreationService userCreationService, AccountService accountService,
FileService fileService) {
public AccountResource(UserRepository userRepository, UserService userService, UserCreationService userCreationService, AccountService accountService, FileService fileService,
SingleUserNotificationService singleUserNotificationService) {
this.userRepository = userRepository;
this.userService = userService;
this.userCreationService = userCreationService;
this.accountService = accountService;
this.fileService = fileService;
this.singleUserNotificationService = singleUserNotificationService;
}

/**
Expand Down Expand Up @@ -140,6 +144,7 @@ public ResponseEntity<UserDTO> createVcsAccessToken(@RequestParam("expiryDate")

userRepository.updateUserVcsAccessToken(user.getId(), LocalVCPersonalAccessTokenManagementService.generateSecureVCSAccessToken(), expiryDate);
log.debug("Successfully created a VCS access token for user {}", user.getLogin());
singleUserNotificationService.notifyUserAboutNewlyAddedVcsAccessToken(user);
user = userRepository.getUser();
UserDTO userDTO = new UserDTO();
userDTO.setLogin(user.getLogin());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package de.tum.cit.aet.artemis.programming.service.tokens;

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.function.Function;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;

@Profile(PROFILE_SCHEDULING)
@Service
public class UserTokenExpiryNotificationService {

private static final Logger log = LoggerFactory.getLogger(UserTokenExpiryNotificationService.class);

private final SingleUserNotificationService singleUserNotificationService;

private final UserRepository userRepository;

public UserTokenExpiryNotificationService(SingleUserNotificationService singleUserNotificationService, UserRepository userRepository) {
this.singleUserNotificationService = singleUserNotificationService;
this.userRepository = userRepository;
}

/**
* Schedules VCS access token expiry notifications to users every morning at 6:00:00 am
*/
@Scheduled(cron = "0 0 6 * * *")
public void sendTokenExpirationNotifications() {
log.info("Sending Token expiration notifications to single user");
notifyOnExpiredToken();
}

/**
* Notifies the user at the day of key expiry, that the key has expired
*/
public void notifyOnExpiredToken() {
notifyUsersForKeyExpiryWindow(now().minusDays(1), now(), (user) -> {
singleUserNotificationService.notifyUserAboutExpiredVcsAccessToken(user);
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, Function<User, Void> notifyFunction) {
userRepository.findByvcsAccessTokenExpiryDateBetween(fromDate, toDate).forEach(notifyFunction::apply);
}
}
Loading
Loading