Skip to content

Commit

Permalink
Integrated code lifecycle: Support multiple SSH keys per user (#9478)
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonEntholzer authored Nov 29, 2024
1 parent f81e35b commit 58b8b59
Show file tree
Hide file tree
Showing 47 changed files with 1,917 additions and 587 deletions.
27 changes: 0 additions & 27 deletions src/main/java/de/tum/cit/aet/artemis/core/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -156,23 +156,6 @@ public class User extends AbstractAuditingEntity implements Participant {
@Column(name = "vcs_access_token_expiry_date")
private ZonedDateTime vcsAccessTokenExpiryDate = null;

/**
* The actual full public ssh key of a user used to authenticate git clone and git push operations if available
*/
@Nullable
@JsonIgnore
@Column(name = "ssh_public_key")
private final String sshPublicKey = null;

/**
* A hash of the public ssh key for fast comparison in the database (with an index)
*/
@Nullable
@Size(max = 100)
@JsonIgnore
@Column(name = "ssh_public_key_hash")
private final String sshPublicKeyHash = null;

@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "user_groups", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "user_groups")
Expand Down Expand Up @@ -560,14 +543,4 @@ public void hasAcceptedIrisElseThrow() {
throw new AccessForbiddenException("The user has not accepted the Iris privacy policy yet.");
}
}

@Nullable
public String getSshPublicKey() {
return sshPublicKey;
}

@Nullable
public @Size(max = 100) String getSshPublicKeyHash() {
return sshPublicKeyHash;
}
}
20 changes: 0 additions & 20 deletions src/main/java/de/tum/cit/aet/artemis/core/dto/UserDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,6 @@ public class UserDTO extends AuditingEntityDTO {

private ZonedDateTime vcsAccessTokenExpiryDate;

private String sshPublicKey;

private String sshKeyHash;

private ZonedDateTime irisAccepted;

public UserDTO() {
Expand Down Expand Up @@ -262,14 +258,6 @@ public ZonedDateTime getVcsAccessTokenExpiryDate() {
return vcsAccessTokenExpiryDate;
}

public String getSshPublicKey() {
return sshPublicKey;
}

public void setSshPublicKey(String sshPublicKey) {
this.sshPublicKey = sshPublicKey;
}

@Override
public String toString() {
return "UserDTO{" + "login='" + login + '\'' + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", email='" + email + '\'' + ", imageUrl='"
Expand All @@ -293,12 +281,4 @@ public ZonedDateTime getIrisAccepted() {
public void setIrisAccepted(ZonedDateTime irisAccepted) {
this.irisAccepted = irisAccepted;
}

public String getSshKeyHash() {
return sshKeyHash;
}

public void setSshKeyHash(String sshKeyHash) {
this.sshKeyHash = sshKeyHash;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -724,16 +724,6 @@ default Page<User> searchAllWithGroupsByLoginOrNameInCourseAndReturnPage(Pageabl
""")
void updateUserLanguageKey(@Param("userId") long userId, @Param("languageKey") String languageKey);

@Modifying
@Transactional // ok because of modifying query
@Query("""
UPDATE User user
SET user.sshPublicKeyHash = :sshPublicKeyHash,
user.sshPublicKey = :sshPublicKey
WHERE user.id = :userId
""")
void updateUserSshPublicKeyHash(@Param("userId") long userId, @Param("sshPublicKeyHash") String sshPublicKeyHash, @Param("sshPublicKey") String sshPublicKey);

@Modifying
@Transactional // ok because of modifying query
@Query("""
Expand Down Expand Up @@ -1120,8 +1110,6 @@ default boolean isCurrentUser(String login) {
return SecurityUtils.getCurrentUserLogin().map(currentLogin -> currentLogin.equals(login)).orElse(false);
}

Optional<User> findBySshPublicKeyHash(String keyString);

/**
* Finds all users which a non-null VCS access token that expires before some given date.
*
Expand Down
49 changes: 0 additions & 49 deletions src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,15 @@

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
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.context.annotation.Profile;
Expand Down Expand Up @@ -45,7 +41,6 @@
import de.tum.cit.aet.artemis.core.service.user.UserCreationService;
import de.tum.cit.aet.artemis.core.service.user.UserService;
import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCPersonalAccessTokenManagementService;
import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils;

/**
* REST controller for managing the current user's account.
Expand Down Expand Up @@ -128,50 +123,6 @@ 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
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@ public ResponseEntity<UserDTO> getAccount() {
// 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());
userDTO.setSshKeyHash(user.getSshPublicKeyHash());
log.info("GET /account {} took {}ms", user.getLogin(), System.currentTimeMillis() - start);
return ResponseEntity.ok(userDTO);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package de.tum.cit.aet.artemis.programming.domain;

import java.time.ZonedDateTime;

import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

import de.tum.cit.aet.artemis.core.domain.DomainObject;

/**
* A public SSH key of a user.
*/
@Entity
@Table(name = "user_public_ssh_key")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
public class UserSshPublicKey extends DomainObject {

/**
* The user who is owner of the public key
*/
@NotNull
@Column(name = "user_id")
private long userId;

/**
* The label of the SSH key shwon in the UI
*/
@Size(max = 50)
@Column(name = "label", length = 50)
private String label;

/**
* The actual full public ssh key of a user used to authenticate git clone and git push operations if available
*/
@NotNull
@Column(name = "public_key")
private String publicKey;

/**
* A hash of the public ssh key for fast comparison in the database (with an index)
*/
@Size(max = 100)
@Column(name = "key_hash")
private String keyHash;

/**
* The creation date of the public SSH key
*/
@Column(name = "creation_date")
private ZonedDateTime creationDate = null;

/**
* The last used date of the public SSH key
*/
@Nullable
@Column(name = "last_used_date")
private ZonedDateTime lastUsedDate = null;

/**
* The expiry date of the public SSH key
*/
@Nullable
@Column(name = "expiry_date")
private ZonedDateTime expiryDate = null;

public @NotNull long getUserId() {
return userId;
}

public void setUserId(@NotNull long userId) {
this.userId = userId;
}

public @Size(max = 50) String getLabel() {
return label;
}

public void setLabel(@Size(max = 50) String label) {
this.label = label;
}

public String getPublicKey() {
return publicKey;
}

public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}

@Nullable
public @Size(max = 100) String getKeyHash() {
return keyHash;
}

public void setKeyHash(@Nullable @Size(max = 100) String keyHash) {
this.keyHash = keyHash;
}

public ZonedDateTime getCreationDate() {
return creationDate;
}

public void setCreationDate(ZonedDateTime creationDate) {
this.creationDate = creationDate;
}

@Nullable
public ZonedDateTime getLastUsedDate() {
return lastUsedDate;
}

public void setLastUsedDate(@Nullable ZonedDateTime lastUsedDate) {
this.lastUsedDate = lastUsedDate;
}

@Nullable
public ZonedDateTime getExpiryDate() {
return expiryDate;
}

public void setExpiryDate(@Nullable ZonedDateTime expiryDate) {
this.expiryDate = expiryDate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package de.tum.cit.aet.artemis.programming.dto;

import java.time.ZonedDateTime;

import com.fasterxml.jackson.annotation.JsonInclude;

import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey;

@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record UserSshPublicKeyDTO(Long id, String label, String publicKey, String keyHash, ZonedDateTime creationDate, ZonedDateTime lastUsedDate, ZonedDateTime expiryDate) {

public static UserSshPublicKeyDTO of(UserSshPublicKey userSshPublicKey) {
return new UserSshPublicKeyDTO(userSshPublicKey.getId(), userSshPublicKey.getLabel(), userSshPublicKey.getPublicKey(), userSshPublicKey.getKeyHash(),
userSshPublicKey.getCreationDate(), userSshPublicKey.getLastUsedDate(), userSshPublicKey.getExpiryDate());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package de.tum.cit.aet.artemis.programming.repository;

import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_CORE;

import java.util.List;
import java.util.Optional;

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Repository;

import de.tum.cit.aet.artemis.core.repository.base.ArtemisJpaRepository;
import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey;

@Profile(PROFILE_CORE)
@Repository
public interface UserSshPublicKeyRepository extends ArtemisJpaRepository<UserSshPublicKey, Long> {

List<UserSshPublicKey> findAllByUserId(Long userId);

Optional<UserSshPublicKey> findByKeyHash(String keyHash);

Optional<UserSshPublicKey> findByIdAndUserId(Long keyId, Long userId);

boolean existsByIdAndUserId(Long id, Long userId);

boolean existsByUserId(Long userId);
}
Loading

0 comments on commit 58b8b59

Please sign in to comment.