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

Integrated code lifecycle: Show SSH fingerprints of Artemis #9650

Draft
wants to merge 63 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
3786b60
added new table to add multiple keys for users
SimonEntholzer Oct 14, 2024
4922371
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 14, 2024
94f39c3
enable cloning
SimonEntholzer Oct 15, 2024
af8f80b
fixed css
SimonEntholzer Oct 17, 2024
366cbaf
revert debug change
SimonEntholzer Oct 17, 2024
6e4c8ac
added migration mysql
SimonEntholzer Oct 17, 2024
bded252
added migration for postgres
SimonEntholzer Oct 17, 2024
002de21
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 17, 2024
5b7bb96
cleaned up UI
SimonEntholzer Oct 18, 2024
7f10abe
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 20, 2024
5805139
fixed build agent authentication
SimonEntholzer Oct 20, 2024
a186891
fixed authentication in tests
SimonEntholzer Oct 20, 2024
b4e75a5
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 20, 2024
74b4a32
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 21, 2024
4b3c308
finish UI
SimonEntholzer Oct 21, 2024
9616e2d
added javadocs
SimonEntholzer Oct 21, 2024
72e6ec9
more docs
SimonEntholzer Oct 21, 2024
06d87ff
server tests
SimonEntholzer Oct 21, 2024
cbec60e
fix server style
SimonEntholzer Oct 22, 2024
f04d01e
moved deleteall
SimonEntholzer Oct 22, 2024
a61e360
add code rabbit suggestions
SimonEntholzer Oct 22, 2024
6a704e2
let code button detect if a user has ssh keys or not
SimonEntholzer Oct 22, 2024
11994ee
improve Server test coverage
SimonEntholzer Oct 22, 2024
f4cde1b
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 22, 2024
e0a01c4
added date picker
SimonEntholzer Oct 22, 2024
fb2b67a
cleaned up client code and added tests
SimonEntholzer Oct 22, 2024
75e3a0b
Use DTO instead of database entity
SimonEntholzer Oct 22, 2024
52c6b3b
moved DTO
SimonEntholzer Oct 22, 2024
c90d4e7
add change suggestions
SimonEntholzer Oct 22, 2024
73e147a
remove debugging statement
SimonEntholzer Oct 22, 2024
71ef085
fix client test style
SimonEntholzer Oct 23, 2024
54c64f1
Merge remote-tracking branch 'refs/remotes/origin/develop' into featu…
SimonEntholzer Oct 23, 2024
7d92399
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 24, 2024
8f96960
replaced put with post
SimonEntholzer Oct 24, 2024
abc1f0f
remove button
SimonEntholzer Oct 24, 2024
8ab97e3
fix copy paste error
SimonEntholzer Oct 24, 2024
9aa50d5
fix tests
SimonEntholzer Oct 24, 2024
7de1858
fix typo
SimonEntholzer Oct 24, 2024
e8ac909
improved client coverage
SimonEntholzer Oct 24, 2024
2079800
import correct dayjs
SimonEntholzer Oct 24, 2024
2c7e18e
remove unused translation keys
SimonEntholzer Oct 25, 2024
c29d5d3
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 25, 2024
3f179a4
added suggestions
SimonEntholzer Oct 26, 2024
6807a32
handle case when user inputs too long label
SimonEntholzer Oct 26, 2024
39b1458
improve error handling when deleting key
SimonEntholzer Oct 26, 2024
1038cf2
add code rabbit suggestions
SimonEntholzer Oct 26, 2024
3b1a0db
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 26, 2024
44b33ec
add code rabbit suggestions
SimonEntholzer Oct 26, 2024
0219251
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 28, 2024
6086881
fix expected http error code in test
SimonEntholzer Oct 28, 2024
56b7d53
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 28, 2024
8e4a98b
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 28, 2024
c92dc8f
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 29, 2024
46a755c
Update src/main/java/de/tum/cit/aet/artemis/programming/service/UserS…
SimonEntholzer Oct 29, 2024
4d521ef
add code suggestions
SimonEntholzer Oct 29, 2024
d5ca308
Merge branch 'develop' into feature/localVC/support-multiple-SSH-keys
SimonEntholzer Oct 30, 2024
e06bf0b
remove empty line from master.xml from merge conflict
SimonEntholzer Oct 30, 2024
0de92c3
server code
SimonEntholzer Oct 31, 2024
73d9603
client code
SimonEntholzer Oct 31, 2024
4d0a068
Merge branch 'refs/heads/feature/localVC/support-multiple-SSH-keys' i…
SimonEntholzer Oct 31, 2024
d1ff8da
added fingerprints sub page
SimonEntholzer Oct 31, 2024
76d4856
added serve tests
SimonEntholzer Nov 1, 2024
9c5cc4d
added jest tests
SimonEntholzer Nov 1, 2024
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 @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;

import jakarta.validation.Valid;
Expand All @@ -22,6 +22,7 @@
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.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -45,8 +46,10 @@
import de.tum.cit.aet.artemis.core.service.FileService;
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.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.localvc.LocalVCPersonalAccessTokenManagementService;
import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils;

/**
* REST controller for managing the current user's account.
Expand All @@ -67,6 +70,8 @@ public class AccountResource {

private final UserService userService;

private final UserSshPublicKeyService userSshPublicKeyService;

private final UserCreationService userCreationService;

private final AccountService accountService;
Expand All @@ -75,13 +80,14 @@ public class AccountResource {

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,
UserSshPublicKeyService userSshPublicKeyService) {
this.userRepository = userRepository;
this.userService = userService;
this.userCreationService = userCreationService;
this.accountService = accountService;
this.fileService = fileService;
this.userSshPublicKeyService = userSshPublicKeyService;
}

/**
Expand Down Expand Up @@ -133,46 +139,87 @@ public ResponseEntity<Void> changePassword(@RequestBody PasswordChangeDTO passwo
}

/**
* PUT account/ssh-public-key : sets the ssh public key
* GET account/ssh-public-keys : retrieves all SSH keys of a user
*
* @param sshPublicKey the ssh public key to set
* @return the ResponseEntity containing all public SSH keys of a user with status 200 (OK)
*/
@GetMapping("account/ssh-public-keys")
@EnforceAtLeastStudent
public ResponseEntity<List<UserSshPublicKeyDTO>> getSshPublicKeys() {
User user = userRepository.getUser();
List<UserSshPublicKeyDTO> keys = userSshPublicKeyService.getAllSshKeysForUser(user);
return ResponseEntity.ok(keys);
}

/**
* GET account/ssh-public-key : gets the ssh public key
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
* @param keyId The id of the key that should be fetched
*
* @return the ResponseEntity containing the requested public SSH key of a user with status 200 (OK), or with status 403 (Access Forbidden) if the key does not exist or is not
* owned by the requesting user
*/
@GetMapping("account/ssh-public-key/{keyId}")
@EnforceAtLeastStudent
public ResponseEntity<UserSshPublicKeyDTO> getSshPublicKey(@PathVariable Long keyId) {
User user = userRepository.getUser();
UserSshPublicKey key = userSshPublicKeyService.getSshKeyForUser(user, keyId);
return ResponseEntity.ok(UserSshPublicKeyDTO.of(key));
}

/**
* GET account/has-ssh-public-key : gets the ssh public key
*
* @return the ResponseEntity containing true if the User has SSH keys, and false if it does not, with status 200 (OK)
*/
@PutMapping("account/ssh-public-key")
@GetMapping("account/has-ssh-public-keys")
@EnforceAtLeastStudent
public ResponseEntity<Void> addSshPublicKey(@RequestBody String sshPublicKey) throws GeneralSecurityException, IOException {
public ResponseEntity<Boolean> hasUserSSHkeys() {
User user = userRepository.getUser();
boolean hasKeys = userSshPublicKeyService.hasUserSSHkeys(user.getId());
return ResponseEntity.ok(hasKeys);
}

/**
* POST account/ssh-public-key : creates a new ssh public key for a user
*
* @param sshPublicKey the ssh public key to create
*
* @return the ResponseEntity with status 200 (OK), or with status 400 (Bad Request) when the SSH key is malformed, the label is too long, or when a key with the same hash
* already exists
*/
@PostMapping("account/ssh-public-key")
@EnforceAtLeastStudent
public ResponseEntity<Void> addSshPublicKey(@RequestBody UserSshPublicKeyDTO 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);
keyEntry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(sshPublicKey.publicKey());
}
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);

userSshPublicKeyService.createSshKeyForUser(user, keyEntry, sshPublicKey);
return ResponseEntity.ok().build();
}

/**
* PUT account/ssh-public-key : sets the ssh public key
* Delete - account/ssh-public-key : deletes the ssh public key by its keyId
*
* @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request)
* @param keyId The id of the key that should be deleted
*
* @return the ResponseEntity with status 200 (OK) when the deletion succeeded, or with status 403 (Access Forbidden) if the key does not belong to the user, or does not exist
*/
@DeleteMapping("account/ssh-public-key")
@DeleteMapping("account/ssh-public-key/{keyId}")
@EnforceAtLeastStudent
public ResponseEntity<Void> deleteSshPublicKey() {
public ResponseEntity<Void> deleteSshPublicKey(@PathVariable Long keyId) {
User user = userRepository.getUser();
log.debug("REST request to remove SSH key of user {}", user.getLogin());
userRepository.updateUserSshPublicKeyHash(user.getId(), null, null);
userSshPublicKeyService.deleteUserSshPublicKey(user.getId(), keyId);

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

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
Loading