Skip to content

Commit

Permalink
Integrated code lifecycle: Let users generate personal access tokens (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonEntholzer authored and MichaelOwenDyer committed Sep 3, 2024
1 parent ae5538a commit d7389b0
Show file tree
Hide file tree
Showing 24 changed files with 897 additions and 150 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,17 @@ default Page<User> findAllWithGroupsByIsDeletedIsFalse(Pageable pageable) {
""")
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 updateUserVcsAccessToken(@Param("userId") long userId, @Param("vcsAccessToken") String vcsAccessToken,
@Param("vcsAccessTokenExpiryDate") ZonedDateTime vcsAccessTokenExpiryDate);

@Modifying
@Transactional // ok because of modifying query
@Query("""
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/service/dto/UserDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ public class UserDTO extends AuditingEntityDTO {

private String vcsAccessToken;

private ZonedDateTime vcsAccessTokenExpiryDate;

private String sshPublicKey;

private ZonedDateTime irisAccepted;
Expand Down Expand Up @@ -250,6 +252,14 @@ public void setVcsAccessToken(String vcsAccessToken) {
this.vcsAccessToken = vcsAccessToken;
}

public void setVcsAccessTokenExpiryDate(ZonedDateTime zoneDateTime) {
this.vcsAccessTokenExpiryDate = zoneDateTime;
}

public ZonedDateTime getVcsAccessTokenExpiryDate() {
return vcsAccessTokenExpiryDate;
}

public String getSshPublicKey() {
return sshPublicKey;
}
Expand Down
136 changes: 136 additions & 0 deletions src/main/java/de/tum/in/www1/artemis/web/rest/AccountResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,42 @@

import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.time.ZonedDateTime;
import java.util.Optional;

import jakarta.validation.Valid;
import jakarta.ws.rs.BadRequestException;

import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import de.tum.in.www1.artemis.config.icl.ssh.HashUtils;
import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.repository.UserRepository;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent;
import de.tum.in.www1.artemis.service.AccountService;
import de.tum.in.www1.artemis.service.connectors.localvc.LocalVCPersonalAccessTokenManagementService;
import de.tum.in.www1.artemis.service.dto.PasswordChangeDTO;
import de.tum.in.www1.artemis.service.dto.UserDTO;
import de.tum.in.www1.artemis.service.user.UserCreationService;
import de.tum.in.www1.artemis.service.user.UserService;
import de.tum.in.www1.artemis.web.rest.errors.AccessForbiddenException;
import de.tum.in.www1.artemis.web.rest.errors.BadRequestAlertException;
import de.tum.in.www1.artemis.web.rest.errors.EmailAlreadyUsedException;
import de.tum.in.www1.artemis.web.rest.errors.PasswordViolatesRequirementsException;

Expand All @@ -34,6 +49,11 @@
@RequestMapping("api/")
public class AccountResource {

@Value("${jhipster.clientApp.name}")
private String applicationName;

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

private final UserRepository userRepository;

private final UserService userService;
Expand Down Expand Up @@ -96,4 +116,120 @@ public ResponseEntity<Void> changePassword(@RequestBody PasswordChangeDTO passwo

return ResponseEntity.ok().build();
}

/**
* PUT account/ssh-public-key : sets the ssh public key
*
* @param sshPublicKey the ssh public key to set
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@PutMapping("account/ssh-public-key")
@EnforceAtLeastStudent
public ResponseEntity<Void> addSshPublicKey(@RequestBody String sshPublicKey) throws GeneralSecurityException, IOException {

User user = userRepository.getUser();
log.debug("REST request to add SSH key to user {}", user.getLogin());
// Parse the public key string
AuthorizedKeyEntry keyEntry;
try {
keyEntry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(sshPublicKey);
}
catch (IllegalArgumentException e) {
throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true);
}
// Extract the PublicKey object
PublicKey publicKey = keyEntry.resolvePublicKey(null, null, null);
String keyHash = HashUtils.getSha512Fingerprint(publicKey);
userRepository.updateUserSshPublicKeyHash(user.getId(), keyHash, sshPublicKey);
return ResponseEntity.ok().build();
}

/**
* PUT account/ssh-public-key : sets the ssh public key
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@DeleteMapping("account/ssh-public-key")
@EnforceAtLeastStudent
public ResponseEntity<Void> deleteSshPublicKey() {
User user = userRepository.getUser();
log.debug("REST request to remove SSH key of user {}", user.getLogin());
userRepository.updateUserSshPublicKeyHash(user.getId(), null, null);

log.debug("Successfully deleted SSH key of user {}", user.getLogin());
return ResponseEntity.ok().build();
}

/**
* PUT account/user-vcs-access-token : creates a vcsAccessToken for a user
*
* @param expiryDate The expiry date which should be set for the token
* @return the ResponseEntity with a userDTO containing the token: with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@PutMapping("account/user-vcs-access-token")
@EnforceAtLeastStudent
public ResponseEntity<UserDTO> createVcsAccessToken(@RequestParam("expiryDate") ZonedDateTime expiryDate) {
User user = userRepository.getUser();
log.debug("REST request to create a new VCS access token for user {}", user.getLogin());
if (expiryDate.isBefore(ZonedDateTime.now()) || expiryDate.isAfter(ZonedDateTime.now().plusYears(1))) {
throw new BadRequestException("Invalid expiry date provided");
}

userRepository.updateUserVcsAccessToken(user.getId(), LocalVCPersonalAccessTokenManagementService.generateSecureVCSAccessToken(), expiryDate);
log.debug("Successfully created a VCS access token for user {}", user.getLogin());
user = userRepository.getUser();
UserDTO userDTO = new UserDTO();
userDTO.setLogin(user.getLogin());
userDTO.setVcsAccessToken(user.getVcsAccessToken());
userDTO.setVcsAccessTokenExpiryDate(user.getVcsAccessTokenExpiryDate());
return ResponseEntity.ok(userDTO);
}

/**
* DELETE account/user-vcs-access-token : deletes the vcsAccessToken of a user
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@DeleteMapping("account/user-vcs-access-token")
@EnforceAtLeastStudent
public ResponseEntity<Void> deleteVcsAccessToken() {
User user = userRepository.getUser();
log.debug("REST request to remove VCS access token key of user {}", user.getLogin());
userRepository.updateUserVcsAccessToken(user.getId(), null, null);
log.debug("Successfully deleted VCS access token of user {}", user.getLogin());
return ResponseEntity.ok().build();
}

/**
* GET account/participation-vcs-access-token : get the vcsToken for of a user for a participation
*
* @param participationId the participation for which the access token should be fetched
*
* @return the versionControlAccessToken belonging to the provided participation and user
*/
@GetMapping("account/participation-vcs-access-token")
@EnforceAtLeastStudent
public ResponseEntity<String> getVcsAccessToken(@RequestParam("participationId") Long participationId) {
User user = userRepository.getUser();

log.debug("REST request to get VCS access token of user {} for participation {}", user.getLogin(), participationId);
return ResponseEntity.ok(userService.getParticipationVcsAccessTokenForUserAndParticipationIdOrElseThrow(user, participationId).getVcsAccessToken());
}

/**
* PUT account/participation-vcs-access-token : get the vcsToken for of a user for a participation
*
* @param participationId the participation for which the access token should be fetched
*
* @return the versionControlAccessToken belonging to the provided participation and user
*/
@PutMapping("account/participation-vcs-access-token")
@EnforceAtLeastStudent
public ResponseEntity<String> createVcsAccessToken(@RequestParam("participationId") Long participationId) {
User user = userRepository.getUser();

log.debug("REST request to create a new VCS access token for user {} for participation {}", user.getLogin(), participationId);
return ResponseEntity.ok(userService.createParticipationVcsAccessTokenForUserAndParticipationIdOrElseThrow(user, participationId).getVcsAccessToken());
}
}
88 changes: 0 additions & 88 deletions src/main/java/de/tum/in/www1/artemis/web/rest/UserResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,18 @@

import static de.tum.in.www1.artemis.config.Constants.PROFILE_CORE;

import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;

import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -29,7 +23,6 @@
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import de.tum.in.www1.artemis.config.icl.ssh.HashUtils;
import de.tum.in.www1.artemis.domain.User;
import de.tum.in.www1.artemis.repository.UserRepository;
import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor;
Expand All @@ -39,7 +32,6 @@
import de.tum.in.www1.artemis.service.dto.UserInitializationDTO;
import de.tum.in.www1.artemis.service.user.UserCreationService;
import de.tum.in.www1.artemis.service.user.UserService;
import de.tum.in.www1.artemis.web.rest.util.HeaderUtil;
import tech.jhipster.web.util.PaginationUtil;

/**
Expand Down Expand Up @@ -67,9 +59,6 @@
@RequestMapping("api/")
public class UserResource {

@Value("${jhipster.clientApp.name}")
private String applicationName;

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

private final UserService userService;
Expand Down Expand Up @@ -180,81 +169,4 @@ public ResponseEntity<Void> setIrisAcceptedToTimestamp() {
userRepository.updateIrisAcceptedToDate(user.getId(), ZonedDateTime.now());
return ResponseEntity.ok().build();
}

/**
* PUT users/sshpublickey : sets the ssh public key
*
* @param sshPublicKey the ssh public key to set
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@PutMapping("users/sshpublickey")
@EnforceAtLeastStudent
public ResponseEntity<Void> addSshPublicKey(@RequestBody String sshPublicKey) throws GeneralSecurityException, IOException {

User user = userRepository.getUser();
log.debug("REST request to add SSH key to user {}", user.getLogin());
// Parse the public key string
AuthorizedKeyEntry keyEntry;
try {
keyEntry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(sshPublicKey);
}
catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().headers(HeaderUtil.createFailureAlert(applicationName, true, "sshUserSettings", "saveSshKeyError", "Invalid SSH key format"))
.body(null);
}
// Extract the PublicKey object
PublicKey publicKey = keyEntry.resolvePublicKey(null, null, null);
String keyHash = HashUtils.getSha512Fingerprint(publicKey);
userRepository.updateUserSshPublicKeyHash(user.getId(), keyHash, sshPublicKey);
return ResponseEntity.ok().build();
}

/**
* PUT users/sshpublickey : sets the ssh public key
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
*/
@DeleteMapping("users/sshpublickey")
@EnforceAtLeastStudent
public ResponseEntity<Void> deleteSshPublicKey() {
User user = userRepository.getUser();
log.debug("REST request to remove SSH key of user {}", user.getLogin());
userRepository.updateUserSshPublicKeyHash(user.getId(), null, null);

log.debug("Successfully deleted SSH key of user {}", user.getLogin());
return ResponseEntity.ok().build();
}

/**
* GET users/vcsToken : get the vcsToken for of a user for a participation
*
* @param participationId the participation for which the access token should be fetched
*
* @return the versionControlAccessToken belonging to the provided participation and user
*/
@GetMapping("users/vcsToken")
@EnforceAtLeastStudent
public ResponseEntity<String> getVcsAccessToken(@RequestParam("participationId") Long participationId) {
User user = userRepository.getUser();

log.debug("REST request to get VCS access token of user {} for participation {}", user.getLogin(), participationId);
return ResponseEntity.ok(userService.getParticipationVcsAccessTokenForUserAndParticipationIdOrElseThrow(user, participationId).getVcsAccessToken());
}

/**
* PUT users/vcsToken : get the vcsToken for of a user for a participation
*
* @param participationId the participation for which the access token should be fetched
*
* @return the versionControlAccessToken belonging to the provided participation and user
*/
@PutMapping("users/vcsToken")
@EnforceAtLeastStudent
public ResponseEntity<String> createVcsAccessToken(@RequestParam("participationId") Long participationId) {
User user = userRepository.getUser();

log.debug("REST request to create a new VCS access token for user {} for participation {}", user.getLogin(), participationId);
return ResponseEntity.ok(userService.createParticipationVcsAccessTokenForUserAndParticipationIdOrElseThrow(user, participationId).getVcsAccessToken());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ public ResponseEntity<UserDTO> getAccount() {
UserDTO userDTO = new UserDTO(user);
// we set this value on purpose here: the user can only fetch their own information, make the token available for constructing the token-based clone-URL
userDTO.setVcsAccessToken(user.getVcsAccessToken());
userDTO.setVcsAccessTokenExpiryDate(user.getVcsAccessTokenExpiryDate());
userDTO.setSshPublicKey(user.getSshPublicKey());
log.info("GET /account {} took {}ms", user.getLogin(), System.currentTimeMillis() - start);
return ResponseEntity.ok(userDTO);
Expand Down
Loading

0 comments on commit d7389b0

Please sign in to comment.