From 5cd247e90f9feb9aabea4340e4d20dd8e308a168 Mon Sep 17 00:00:00 2001 From: Simon Date: Sat, 25 May 2024 08:38:33 +0200 Subject: [PATCH 1/4] initial commit --- .../artemis/service/ParticipationServiceTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java index 34af5ff620f5..563dfef6f725 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/service/ParticipationServiceTest.java @@ -5,6 +5,7 @@ import java.net.URISyntaxException; import java.time.ZonedDateTime; import java.util.List; +import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.AfterEach; @@ -14,6 +15,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.mockito.MockitoAnnotations; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.security.test.context.support.WithMockUser; import de.tum.in.www1.artemis.AbstractSpringIntegrationJenkinsGitlabTest; @@ -119,6 +121,19 @@ void testCreateParticipationForExternalSubmission() throws Exception { assertThat(programmingSubmission.getResults()).isNullOrEmpty(); // results are not added in the invoked method above } + @Test + @WithMockUser(username = TEST_PREFIX + "instructor1", roles = "INSTRUCTOR") + void testGetBuildJobIdsForResultsOfParticipation() throws Exception { + Optional student = userRepository.findOneWithGroupsAndAuthoritiesByLogin(TEST_PREFIX + "student1"); + participationUtilService.mockCreationOfExerciseParticipation(false, null, programmingExercise, uriService, versionControlService, continuousIntegrationService); + + StudentParticipation participation = participationService.createParticipationWithEmptySubmissionIfNotExisting(programmingExercise, student.orElseThrow(), + SubmissionType.EXTERNAL); + + var resultMap = request.get("/api/participations/" + participation.getId() + "/results/build-job-ids", HttpStatus.OK, Map.class); + assertThat(resultMap).size().isEqualTo(0); + } + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void testStartExerciseWithInitializationDate_newParticipation() { From 726434cf83e79d9c0c941d68f45dab008fde92e0 Mon Sep 17 00:00:00 2001 From: Entholzer Date: Sun, 9 Jun 2024 22:25:33 +0200 Subject: [PATCH 2/4] generate new VCS access tokens --- .../Artemis__Server__LocalVC___LocalCI_.xml | 4 +- .../tum/in/www1/artemis/config/Constants.java | 5 ++ .../artemis/repository/UserRepository.java | 11 +++ .../gitlab/GitlabInfoContributor.java | 4 +- .../localvc/LocalVCInfoContributor.java | 2 +- ...CPersonalAccessTokenManagementService.java | 74 +++++++++++++++++++ .../resources/config/application-localvc.yml | 1 + .../localvcci/LocalVCInfoContributorTest.java | 2 +- 8 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCPersonalAccessTokenManagementService.java diff --git a/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI_.xml b/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI_.xml index fc0c6977974e..bf04b8954c1b 100644 --- a/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI_.xml +++ b/.idea/runConfigurations/Artemis__Server__LocalVC___LocalCI_.xml @@ -1,6 +1,6 @@ - - + \ No newline at end of file diff --git a/src/main/java/de/tum/in/www1/artemis/config/Constants.java b/src/main/java/de/tum/in/www1/artemis/config/Constants.java index 64aa8021e96d..2b85232a9d1d 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/Constants.java +++ b/src/main/java/de/tum/in/www1/artemis/config/Constants.java @@ -285,6 +285,11 @@ public final class Constants { */ public static final String PROFILE_LOCALCI = "localci"; + /** + * The name of the Spring profile used for gitlab. + */ + public static final String PROFILE_GITLAB = "gitlab"; + /** * The name of the Spring profile used to process build jobs in a local CI setup. */ diff --git a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java index a443b3ecdf0d..1e7958da7927 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/UserRepository.java @@ -522,6 +522,17 @@ OR CONCAT(user.firstName, ' ', user.lastName) LIKE %:#{#loginOrName}% """) void updateUserSshPublicKeyHash(@Param("userId") long userId, @Param("sshPublicKeyHash") String sshPublicKeyHash, @Param("sshPublicKey") String sshPublicKey); + @Modifying + @Transactional // ok because of modifying query + @Query(""" + UPDATE User user + SET user.vcsAccessToken = :vcsAccessToken, + user.vcsAccessTokenExpiryDate = :vcsAccessTokenExpiryDate + WHERE user.id = :userId + """) + void updateUserVCAccessToken(@Param("userId") long userId, @Param("vcsAccessToken") String vcsAccessToken, + @Param("vcsAccessTokenExpiryDate") ZonedDateTime vcsAccessTokenExpiryDate); + @Modifying @Transactional // ok because of modifying query @Query(""" diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitlabInfoContributor.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitlabInfoContributor.java index 6465d6186cbe..38f28476ca6e 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitlabInfoContributor.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/gitlab/GitlabInfoContributor.java @@ -1,5 +1,7 @@ package de.tum.in.www1.artemis.service.connectors.gitlab; +import static de.tum.in.www1.artemis.config.Constants.PROFILE_GITLAB; + import java.net.URL; import java.util.Optional; @@ -12,7 +14,7 @@ import de.tum.in.www1.artemis.config.Constants; @Component -@Profile("gitlab") +@Profile(PROFILE_GITLAB) public class GitlabInfoContributor implements InfoContributor { @Value("${artemis.version-control.url}") diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCInfoContributor.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCInfoContributor.java index 358b0c9490fd..3f5223b0526f 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCInfoContributor.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCInfoContributor.java @@ -43,7 +43,7 @@ public void contribute(Info.Builder builder) { // TODO: only activate this when access tokens are available and make sure this does not lead to issues // TODO: If activated, reflect this in LocalVCInfoContributorTest // with the account.service.ts and its check if the access token is required - builder.withDetail(Constants.INFO_VERSION_CONTROL_ACCESS_TOKEN_DETAIL, false); + builder.withDetail(Constants.INFO_VERSION_CONTROL_ACCESS_TOKEN_DETAIL, true); // Store ssh url template try { diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCPersonalAccessTokenManagementService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCPersonalAccessTokenManagementService.java new file mode 100644 index 000000000000..372f3337155e --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/localvc/LocalVCPersonalAccessTokenManagementService.java @@ -0,0 +1,74 @@ +package de.tum.in.www1.artemis.service.connectors.localvc; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_LOCALVC; + +import java.security.SecureRandom; +import java.time.Duration; +import java.time.ZonedDateTime; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.in.www1.artemis.domain.User; +import de.tum.in.www1.artemis.repository.UserRepository; +import de.tum.in.www1.artemis.service.connectors.vcs.VcsTokenManagementService; +import de.tum.in.www1.artemis.web.rest.UserResource; + +@Service +@Profile(PROFILE_LOCALVC) +public class LocalVCPersonalAccessTokenManagementService extends VcsTokenManagementService { + + private static final Logger log = LoggerFactory.getLogger(UserResource.class); + + private final UserRepository userRepository; + + @Value("${artemis.version-control.version-control-access-token:#{false}}") + private Boolean versionControlAccessToken; + + @Value("${artemis.version-control.vc-access-token-max-lifetime-in-days:365}") + private int vcMaxLifetimeInDays; + + private static final String TOKEN_PREFIX = "vcpat-"; + + private static final int RANDOM_STRING_LENGTH = 40; + + private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + public LocalVCPersonalAccessTokenManagementService(UserRepository userRepository) { + super(); + this.userRepository = userRepository; + } + + @Override + public void createAccessToken(User user, Duration lifetime) { + if (versionControlAccessToken) { + userRepository.updateUserVCAccessToken(user.getId(), generateSecureToken(), ZonedDateTime.now().plus(Duration.ofDays(vcMaxLifetimeInDays))); + log.info("Created access token for user {}", user.getId()); + } + } + + @Override + public void renewAccessToken(User user, Duration newLifetime) { + if (versionControlAccessToken) { + if (user.getVcsAccessTokenExpiryDate() != null && user.getVcsAccessTokenExpiryDate().isBefore(ZonedDateTime.now())) { + // todo create new one here and + userRepository.updateUserVCAccessToken(user.getId(), user.getVcsAccessToken(), ZonedDateTime.now().plus(Duration.ofDays(vcMaxLifetimeInDays))); + } + log.info("Renewed access token for user {}", user.getId()); + } + } + + public static String generateSecureToken() { + SecureRandom secureRandom = new SecureRandom(); + StringBuilder randomString = new StringBuilder(RANDOM_STRING_LENGTH); + + for (int i = 0; i < RANDOM_STRING_LENGTH; i++) { + int randomIndex = secureRandom.nextInt(CHARACTERS.length()); + randomString.append(CHARACTERS.charAt(randomIndex)); + } + return TOKEN_PREFIX + randomString.toString(); + } +} diff --git a/src/main/resources/config/application-localvc.yml b/src/main/resources/config/application-localvc.yml index 31ef753a7862..43d0f5ef6164 100644 --- a/src/main/resources/config/application-localvc.yml +++ b/src/main/resources/config/application-localvc.yml @@ -10,4 +10,5 @@ artemis: # In a multi node setup, this folder should be in a shared file system area (e.g. based on NFS), so that user can access the same files over multiple nodes. local-vcs-repo-path: ./local-vcs-repos url: http://localhost:8000 + vc-access-token-max-lifetime-in-days: 365 diff --git a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCInfoContributorTest.java b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCInfoContributorTest.java index e08769b1e46d..0210bbd5f5ad 100644 --- a/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCInfoContributorTest.java +++ b/src/test/java/de/tum/in/www1/artemis/localvcci/LocalVCInfoContributorTest.java @@ -25,7 +25,7 @@ void testContribute() { } Info info = builder.build(); - assertThat((Boolean) info.getDetails().get("versionControlAccessToken")).isFalse(); + assertThat((Boolean) info.getDetails().get("versionControlAccessToken")).isTrue(); } } From b39b6c326eabffec462bd98560e80886d56eae61 Mon Sep 17 00:00:00 2001 From: Simon Entholzer Date: Tue, 11 Jun 2024 10:43:54 +0200 Subject: [PATCH 3/4] add vcs token at user creation --- .../service/user/UserCreationService.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/in/www1/artemis/service/user/UserCreationService.java b/src/main/java/de/tum/in/www1/artemis/service/user/UserCreationService.java index 7d97eb892bf7..105915402798 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/user/UserCreationService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/user/UserCreationService.java @@ -1,12 +1,15 @@ package de.tum.in.www1.artemis.service.user; import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE; +import static de.tum.in.www1.artemis.config.Constants.PROFILE_LOCALVC; import static de.tum.in.www1.artemis.security.Role.EDITOR; import static de.tum.in.www1.artemis.security.Role.INSTRUCTOR; import static de.tum.in.www1.artemis.security.Role.STUDENT; import static de.tum.in.www1.artemis.security.Role.TEACHING_ASSISTANT; +import java.time.Duration; import java.time.Instant; +import java.time.ZonedDateTime; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -32,7 +35,9 @@ import de.tum.in.www1.artemis.repository.OrganizationRepository; import de.tum.in.www1.artemis.repository.UserRepository; import de.tum.in.www1.artemis.security.SecurityUtils; +import de.tum.in.www1.artemis.service.ProfileService; import de.tum.in.www1.artemis.service.connectors.ci.CIUserManagementService; +import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCPersonalAccessTokenManagementService; import de.tum.in.www1.artemis.service.connectors.vcs.VcsUserManagementService; import de.tum.in.www1.artemis.web.rest.vm.ManagedUserVM; import tech.jhipster.security.RandomUtil; @@ -41,6 +46,8 @@ @Service public class UserCreationService { + private final ProfileService profileService; + @Value("${artemis.user-management.use-external}") private Boolean useExternalUserManagement; @@ -56,6 +63,12 @@ public class UserCreationService { @Value("${info.guided-tour.course-group-instructors:#{null}}") private Optional tutorialGroupInstructors; + @Value("${artemis.version-control.version-control-access-token:#{false}}") + private Boolean versionControlAccessToken; + + @Value("${artemis.version-control.vc-access-token-max-lifetime-in-days:365}") + private int vcMaxLifetimeInDays; + private static final Logger log = LoggerFactory.getLogger(UserCreationService.class); private final UserRepository userRepository; @@ -76,7 +89,7 @@ public class UserCreationService { public UserCreationService(UserRepository userRepository, PasswordService passwordService, AuthorityRepository authorityRepository, CourseRepository courseRepository, Optional optionalVcsUserManagementService, Optional optionalCIUserManagementService, CacheManager cacheManager, - OrganizationRepository organizationRepository) { + OrganizationRepository organizationRepository, ProfileService profileService) { this.userRepository = userRepository; this.passwordService = passwordService; this.authorityRepository = authorityRepository; @@ -85,6 +98,7 @@ public UserCreationService(UserRepository userRepository, PasswordService passwo this.optionalCIUserManagementService = optionalCIUserManagementService; this.cacheManager = cacheManager; this.organizationRepository = organizationRepository; + this.profileService = profileService; } /** @@ -130,6 +144,11 @@ public User createUser(String login, @Nullable String password, @Nullable Set new HashSet<>(Set.of(...)) @@ -185,6 +204,12 @@ public User createUser(ManagedUserVM userDTO) { catch (InvalidDataAccessApiUsageException | PatternSyntaxException pse) { log.warn("Could not retrieve matching organizations from pattern: {}", pse.getMessage()); } + + // new users gets new VCS access tokens, for the LocalVC setup + if (versionControlAccessToken && profileService.isProfileActive(PROFILE_LOCALVC)) { + user.setVcsAccessToken(LocalVCPersonalAccessTokenManagementService.generateSecureToken()); + user.setVcsAccessTokenExpiryDate(ZonedDateTime.now().plus(Duration.ofDays(vcMaxLifetimeInDays))); + } user.setGroups(userDTO.getGroups()); user.setActivated(true); user.setInternal(true); From 624206854b5052a86c7687b8fc19ca7251449585 Mon Sep 17 00:00:00 2001 From: Simon Entholzer Date: Tue, 11 Jun 2024 10:49:02 +0200 Subject: [PATCH 4/4] renew tokens at startup --- .../artemis/service/connectors/vcs/VcsTokenRenewalService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VcsTokenRenewalService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VcsTokenRenewalService.java index 784a1a31bb77..6fd5ef217464 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VcsTokenRenewalService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/vcs/VcsTokenRenewalService.java @@ -50,6 +50,7 @@ public VcsTokenRenewalService(@Value("${artemis.version-control.version-control- this.versionControlAccessToken = versionControlAccessToken; this.vcsTokenManagementService = vcsTokenManagementService; this.userRepository = userRepository; + renewAllVcsAccessTokens(); } /**