From 3786b6074ff4288a1c25fc6d83e76c8d86b0cd7e Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 14 Oct 2024 16:28:14 +0200 Subject: [PATCH 01/47] added new table to add multiple keys for users --- .../aet/artemis/core/web/AccountResource.java | 31 +++-- .../programming/domain/UserPublicSshKey.java | 107 +++++++++++++++++ .../UserPublicSshKeyRepository.java | 18 +++ .../service/UserSshPublicKeyService.java | 46 +++++++ .../changelog/20241013150000_changelog.xml | 36 ++++++ .../resources/config/liquibase/master.xml | 1 + .../webapp/app/core/auth/account.service.ts | 35 ++++-- .../programming/user-ssh-public-key.model.ts | 10 ++ .../ssh-user-settings.component.html | 113 +++++++++++------- .../ssh-user-settings.component.scss | 9 +- .../ssh-user-settings.component.ts | 103 ++++++++++------ .../ssh-user-settings.component.spec.ts | 12 +- 12 files changed, 412 insertions(+), 109 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/domain/UserPublicSshKey.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java create mode 100644 src/main/resources/config/liquibase/changelog/20241013150000_changelog.xml create mode 100644 src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 2a441e4cd035..ae98b3866296 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -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; @@ -45,8 +45,9 @@ 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.UserPublicSshKey; +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. @@ -67,6 +68,8 @@ public class AccountResource { private final UserService userService; + private final UserSshPublicKeyService userSshPublicKeyService; + private final UserCreationService userCreationService; private final AccountService accountService; @@ -75,13 +78,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; } /** @@ -132,6 +136,20 @@ public ResponseEntity changePassword(@RequestBody PasswordChangeDTO passwo return ResponseEntity.ok().build(); } + /** + * GET account/ssh-public-keys : sets the ssh public key + * + * @return the ResponseEntity containing all public SSH keys of a user with status 200 (OK), or with status 400 (Bad Request) + */ + @GetMapping("account/ssh-public-keys") + @EnforceAtLeastStudent + public ResponseEntity> getSshPublicKey() { + User user = userRepository.getUser(); + log.debug("REST request to add SSH key to user {}", user.getLogin()); + List keys = userSshPublicKeyService.getAllSshKeysForUser(user); + return ResponseEntity.ok(keys); + } + /** * PUT account/ssh-public-key : sets the ssh public key * @@ -154,9 +172,8 @@ public ResponseEntity addSshPublicKey(@RequestBody String sshPublicKey) th 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.addSshKeyForUser(user, keyEntry, sshPublicKey); + // userRepository.updateUserSshPublicKeyHash(user.getId(), keyHash, sshPublicKey); return ResponseEntity.ok().build(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserPublicSshKey.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserPublicSshKey.java new file mode 100644 index 000000000000..eb6a28867ab5 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserPublicSshKey.java @@ -0,0 +1,107 @@ +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; +import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.programming.service.vcs.VcsTokenRenewalService; + +/** + * A public SSH key of a user. + */ +@Entity +@Table(name = "user_public_ssh_key") +@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) +public class UserPublicSshKey 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 + */ + @Column(name = "public_key") + private String publicKey; + + /** + * A hash of the public ssh key for fast comparison in the database (with an index) + */ + @Nullable + @Size(max = 100) + @Column(name = "key_hash") + private String keyHash; + + /** + * The expiry date of the VCS access token. + * This is used for checking if a access token needs to be renewed. + * + * @see VcsTokenRenewalService + * @see UserRepository#getUsersWithAccessTokenExpirationDateBefore + */ + @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; + } + + @Nullable + public ZonedDateTime getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(@Nullable ZonedDateTime expiryDate) { + this.expiryDate = expiryDate; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java new file mode 100644 index 000000000000..5b10cfef03d8 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java @@ -0,0 +1,18 @@ +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 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.UserPublicSshKey; + +@Profile(PROFILE_CORE) +@Repository +public interface UserPublicSshKeyRepository extends ArtemisJpaRepository { + + List findAllByUserId(Long userId); +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java new file mode 100644 index 000000000000..1bf49a271f70 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -0,0 +1,46 @@ +package de.tum.cit.aet.artemis.programming.service; + +import static de.tum.cit.aet.artemis.core.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 org.apache.sshd.common.config.keys.AuthorizedKeyEntry; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.programming.domain.UserPublicSshKey; +import de.tum.cit.aet.artemis.programming.repository.UserPublicSshKeyRepository; +import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; + +@Profile(PROFILE_CORE) +@Service +public class UserSshPublicKeyService { + + private final UserPublicSshKeyRepository userPublicSshKeyRepository; + + public UserSshPublicKeyService(UserPublicSshKeyRepository userPublicSshKeyRepository) { + this.userPublicSshKeyRepository = userPublicSshKeyRepository; + } + + public void addSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, String sshPublicKey) throws GeneralSecurityException, IOException { + PublicKey publicKey = keyEntry.resolvePublicKey(null, null, null); + String keyHash = HashUtils.getSha512Fingerprint(publicKey); + + UserPublicSshKey userPublicSSHKey = new UserPublicSshKey(); + userPublicSSHKey.setUserId(user.getId()); + userPublicSSHKey.setLabel("Key 1"); + userPublicSSHKey.setPublicKey(sshPublicKey); + userPublicSSHKey.setKeyHash(keyHash); + userPublicSSHKey.setExpiryDate(ZonedDateTime.now().plusMonths(12)); + userPublicSshKeyRepository.save(userPublicSSHKey); + } + + public List getAllSshKeysForUser(User user) { + return userPublicSshKeyRepository.findAllByUserId(user.getId()); + } +} diff --git a/src/main/resources/config/liquibase/changelog/20241013150000_changelog.xml b/src/main/resources/config/liquibase/changelog/20241013150000_changelog.xml new file mode 100644 index 000000000000..54f3e49b45c6 --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241013150000_changelog.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index d496528a13ec..57fd83fe180c 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -28,6 +28,7 @@ + diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts index 8c22990b2a17..8d8e879d5837 100644 --- a/src/main/webapp/app/core/auth/account.service.ts +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -1,7 +1,7 @@ import { Injectable, inject } from '@angular/core'; import { SessionStorageService } from 'ngx-webstorage'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; -import { BehaviorSubject, Observable, lastValueFrom, of } from 'rxjs'; +import { BehaviorSubject, Observable, lastValueFrom, of, tap } from 'rxjs'; import { catchError, distinctUntilChanged, map } from 'rxjs/operators'; import { Course } from 'app/entities/course.model'; import { User } from 'app/core/user/user.model'; @@ -13,6 +13,7 @@ import { Exercise, getCourseFromExercise } from 'app/entities/exercise.model'; import { Authority } from 'app/shared/constants/authority.constants'; import { TranslateService } from '@ngx-translate/core'; import { EntityResponseType } from 'app/complaints/complaint.service'; +import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; export interface IAccountService { save: (account: any) => Observable>; @@ -30,7 +31,7 @@ export interface IAccountService { isAuthenticated: () => boolean; getAuthenticationState: () => Observable; getImageUrl: () => string | undefined; - addSshPublicKey: (sshPublicKey: string) => Observable; + addNewSshPublicKey: (userSshPublicKey: UserSshPublicKey) => Observable>; } @Injectable({ providedIn: 'root' }) @@ -327,23 +328,31 @@ export class AccountService implements IAccountService { /** * Sends the added SSH key to the server * - * @param sshPublicKey + * @param userSshPublicKey */ - addSshPublicKey(sshPublicKey: string): Observable { - if (this.userIdentity) { - this.userIdentity.sshPublicKey = sshPublicKey; - } - return this.http.put('api/account/ssh-public-key', sshPublicKey); + addNewSshPublicKey(userSshPublicKey: UserSshPublicKey): Observable> { + return this.http.put('api/account/ssh-public-key', userSshPublicKey, { observe: 'response' }); + } + + /** + * Retrieves all public SSH keys of a user + */ + getAllSshPublicKeys(): Observable { + return this.http.get('api/account/ssh-public-keys'); } /** * Sends a request to the server to delete the user's current SSH key */ - deleteSshPublicKey(): Observable { - if (this.userIdentity) { - this.userIdentity.sshPublicKey = undefined; - } - return this.http.delete('api/account/ssh-public-key'); + deleteSshPublicKey(sshPublicKeyHash: string): Observable { + const params = new HttpParams().set('sshPublicKeyHash', sshPublicKeyHash); + return this.http.delete('api/account/ssh-public-key', { params }).pipe( + tap(() => { + if (this.userIdentity) { + this.userIdentity.sshPublicKey = undefined; + } + }), + ); } /** diff --git a/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts b/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts new file mode 100644 index 000000000000..64c5e3cb80f5 --- /dev/null +++ b/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts @@ -0,0 +1,10 @@ +import { BaseEntity } from 'app/shared/model/base-entity'; +import dayjs from 'dayjs/esm'; + +export class UserSshPublicKey implements BaseEntity { + id?: number; + label: string; + publicKey: string; + keyHash: string; + expiryDate?: dayjs.Dayjs; +} diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html index 7372a07ac241..d820d83ca39c 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html @@ -1,10 +1,10 @@

-@if (currentUser) { +@if (!isLoading) {
- @if (keyCount === 0 && !showSshKey) { + @if (keyCount === 0 && !showKeyDetailsView) {

@@ -21,17 +21,24 @@

} - @if (keyCount > 0 && !showSshKey) { -
-

- + @if (keyCount > 0 && !showKeyDetailsView) { +
+
+

+ +

@@ -48,50 +55,52 @@

- - -
-
- {{ sshKeyHash }} -
- + @for (key of sshPublicKeys; track key; let i = $index) { + + +
{{ key.label }}
+
+ {{ key.keyHash }} +
+ - -
- } - - @if (showSshKey) { + + @if (showKeyDetailsView) {
- @if (isKeyReadonly) { + @if (inCreateMode) {

} @else {

@@ -107,7 +116,7 @@

- +
@@ -117,11 +126,25 @@

- @if (!isKeyReadonly) { +
+

+ +
+ + @if (!inCreateMode) { +
+

+
+ {{ displayedKeyHash }} +
+
+ } + + @if (!inCreateMode) {
(); - dialogError$ = this.dialogErrorSource.asObservable(); constructor( @@ -48,31 +59,39 @@ export class SshUserSettingsComponent implements OnInit { this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); }); this.setMessageBasedOnOS(getOS()); - this.authStateSubscription = this.accountService - .getAuthenticationState() + this.accountServiceSubscription = this.accountService + .getAllSshPublicKeys() .pipe( - tap((user: User) => { - this.storedSshKey = user.sshPublicKey || ''; - this.sshKey = this.storedSshKey; - this.sshKeyHash = user.sshKeyHash || ''; - this.currentUser = user; - // currently only 0 or 1 key are supported - this.keyCount = this.sshKey ? 1 : 0; - this.isKeyReadonly = !!this.sshKey; - return this.currentUser; + tap((publicKeys: UserSshPublicKey[]) => { + this.alertService.success('artemisApp.userSettings.sshSettingsPage.saveSuccess'); + this.sshPublicKeys = publicKeys; + this.keyCount = publicKeys.length; + this.isLoading = false; }), ) .subscribe(); } + ngOnDestroy() { + this.accountServiceSubscription.unsubscribe(); + } + saveSshKey() { - this.accountService.addSshPublicKey(this.sshKey).subscribe({ - next: () => { - this.alertService.success('artemisApp.userSettings.sshSettingsPage.saveSuccess'); - this.showSshKey = false; - this.storedSshKey = this.sshKey; + const newUserSshKey = { + id: this.displayedKeyId, + label: this.displayedKeyLabel, + publicKey: this.displayedSshKey, + expiryDate: this.displayedExpiryDate, + } as UserSshPublicKey; + + this.accountService.addNewSshPublicKey(newUserSshKey).subscribe({ + next: (res) => { + const newlyCreatedKey = res.body!; + this.sshPublicKeys.push(newlyCreatedKey); + this.showKeyDetailsView = false; this.keyCount = this.keyCount + 1; - this.isKeyReadonly = true; + this.inCreateMode = true; + this.alertService.success('artemisApp.userSettings.sshSettingsPage.saveSuccess'); }, error: () => { this.alertService.error('artemisApp.userSettings.sshSettingsPage.saveFailure'); @@ -81,14 +100,12 @@ export class SshUserSettingsComponent implements OnInit { } deleteSshKey() { - this.showSshKey = false; - this.accountService.deleteSshPublicKey().subscribe({ + this.showKeyDetailsView = false; + this.accountService.deleteSshPublicKey('ab').subscribe({ next: () => { this.alertService.success('artemisApp.userSettings.sshSettingsPage.deleteSuccess'); - this.sshKey = ''; - this.storedSshKey = ''; this.keyCount = this.keyCount - 1; - this.isKeyReadonly = false; + this.inCreateMode = false; }, error: () => { this.alertService.error('artemisApp.userSettings.sshSettingsPage.deleteFailure'); @@ -98,12 +115,24 @@ export class SshUserSettingsComponent implements OnInit { } cancelEditingSshKey() { - this.showSshKey = !this.showSshKey; - this.sshKey = this.storedSshKey; + this.showKeyDetailsView = false; } - protected readonly ButtonType = ButtonType; - protected readonly ButtonSize = ButtonSize; + editExistingSshKey(key: UserSshPublicKey) { + this.showKeyDetailsView = true; + this.inCreateMode = false; + this.displayedKeyId = key.id; + this.displayedSshKey = key.publicKey; + this.displayedKeyLabel = key.label; + this.displayedKeyHash = key.keyHash; + this.displayedExpiryDate = key.expiryDate; + } + + createNewSshKey() { + this.showKeyDetailsView = true; + this.inCreateMode = false; + this.displayedKeyId = undefined; + } private setMessageBasedOnOS(os: string): void { switch (os) { diff --git a/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts b/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts index 29b647dfb946..28d21fe2cc84 100644 --- a/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts +++ b/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts @@ -76,10 +76,10 @@ describe('SshUserSettingsComponent', () => { accountServiceMock.addSshPublicKey.mockReturnValue(of(new HttpResponse({ status: 200 }))); comp.ngOnInit(); comp.sshKey = 'new-key'; - comp.showSshKey = true; + comp.showKeyDetailsView = true; comp.saveSshKey(); expect(accountServiceMock.addSshPublicKey).toHaveBeenCalledWith('new-key'); - expect(comp.showSshKey).toBeFalse(); + expect(comp.showKeyDetailsView).toBeFalse(); }); it('should delete SSH key and disable edit mode', () => { @@ -87,10 +87,10 @@ describe('SshUserSettingsComponent', () => { comp.ngOnInit(); const empty = ''; comp.sshKey = 'new-key'; - comp.showSshKey = true; + comp.showKeyDetailsView = true; comp.deleteSshKey(); expect(accountServiceMock.deleteSshPublicKey).toHaveBeenCalled(); - expect(comp.showSshKey).toBeFalse(); + expect(comp.showKeyDetailsView).toBeFalse(); expect(comp.storedSshKey).toEqual(empty); }); @@ -113,10 +113,10 @@ describe('SshUserSettingsComponent', () => { const oldKey = 'old-key'; const newKey = 'new-key'; comp.sshKey = oldKey; - comp.showSshKey = true; + comp.showKeyDetailsView = true; comp.saveSshKey(); expect(comp.storedSshKey).toEqual(oldKey); - comp.showSshKey = true; + comp.showKeyDetailsView = true; comp.sshKey = newKey; comp.cancelEditingSshKey(); expect(comp.storedSshKey).toEqual(oldKey); From 94f39c37ff7de81c35a30160f3c805d0e12de774 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 15 Oct 2024 21:38:45 +0200 Subject: [PATCH 02/47] enable cloning --- .../aet/artemis/core/web/AccountResource.java | 32 ++-- ...ublicSshKey.java => UserSshPublicKey.java} | 40 ++++- .../UserPublicSshKeyRepository.java | 9 +- .../service/UserSshPublicKeyService.java | 50 ++++-- .../GitPublickeyAuthenticatorService.java | 15 +- .../changelog/20241013150000_changelog.xml | 6 + .../webapp/app/core/auth/account.service.ts | 22 +-- .../programming/user-ssh-public-key.model.ts | 2 +- .../code-button/code-button.component.ts | 2 +- ...h-user-settings-key-details.component.html | 70 ++++++++ ...ssh-user-settings-key-details.component.ts | 152 ++++++++++++++++++ .../ssh-user-settings.component.html | 119 +++----------- .../ssh-user-settings.component.scss | 30 +++- .../ssh-user-settings.component.ts | 88 +--------- .../user-settings/user-settings.module.ts | 2 + .../user-settings/user-settings.route.ts | 15 ++ .../content/scss/themes/_dark-variables.scss | 2 +- src/main/webapp/i18n/de/userSettings.json | 4 +- src/main/webapp/i18n/en/userSettings.json | 4 +- 19 files changed, 427 insertions(+), 237 deletions(-) rename src/main/java/de/tum/cit/aet/artemis/programming/domain/{UserPublicSshKey.java => UserSshPublicKey.java} (73%) create mode 100644 src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html create mode 100644 src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index ae98b3866296..38cbf8070c75 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -45,7 +45,7 @@ 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.UserPublicSshKey; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.programming.service.UserSshPublicKeyService; import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCPersonalAccessTokenManagementService; @@ -143,13 +143,25 @@ public ResponseEntity changePassword(@RequestBody PasswordChangeDTO passwo */ @GetMapping("account/ssh-public-keys") @EnforceAtLeastStudent - public ResponseEntity> getSshPublicKey() { + public ResponseEntity> getSshPublicKey() { User user = userRepository.getUser(); - log.debug("REST request to add SSH key to user {}", user.getLogin()); - List keys = userSshPublicKeyService.getAllSshKeysForUser(user); + List keys = userSshPublicKeyService.getAllSshKeysForUser(user); return ResponseEntity.ok(keys); } + /** + * GET account/ssh-public-key : sets the ssh public key + * + * @return the ResponseEntity containing the requested public SSH key of a user with status 200 (OK), or with status 400 (Bad Request) + */ + @GetMapping("account/ssh-public-key") + @EnforceAtLeastStudent + public ResponseEntity getSshPublicKey(@RequestParam("keyId") Long keyId) { + User user = userRepository.getUser(); + UserSshPublicKey key = userSshPublicKeyService.getSshKeyForUser(user, keyId); + return ResponseEntity.ok(key); + } + /** * PUT account/ssh-public-key : sets the ssh public key * @@ -159,20 +171,20 @@ public ResponseEntity> getSshPublicKey() { */ @PutMapping("account/ssh-public-key") @EnforceAtLeastStudent - public ResponseEntity addSshPublicKey(@RequestBody String sshPublicKey) throws GeneralSecurityException, IOException { + public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKey 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.getPublicKey()); } catch (IllegalArgumentException e) { throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true); } // Extract the PublicKey object - userSshPublicKeyService.addSshKeyForUser(user, keyEntry, sshPublicKey); + userSshPublicKeyService.createSshKeyForUser(user, keyEntry, sshPublicKey); // userRepository.updateUserSshPublicKeyHash(user.getId(), keyHash, sshPublicKey); return ResponseEntity.ok().build(); } @@ -184,12 +196,12 @@ public ResponseEntity addSshPublicKey(@RequestBody String sshPublicKey) th */ @DeleteMapping("account/ssh-public-key") @EnforceAtLeastStudent - public ResponseEntity deleteSshPublicKey() { + public ResponseEntity deleteSshPublicKey(@RequestParam("keyId") 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(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserPublicSshKey.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java similarity index 73% rename from src/main/java/de/tum/cit/aet/artemis/programming/domain/UserPublicSshKey.java rename to src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java index eb6a28867ab5..a4ef2bcdddb1 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserPublicSshKey.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java @@ -13,8 +13,6 @@ import org.hibernate.annotations.CacheConcurrencyStrategy; import de.tum.cit.aet.artemis.core.domain.DomainObject; -import de.tum.cit.aet.artemis.core.repository.UserRepository; -import de.tum.cit.aet.artemis.programming.service.vcs.VcsTokenRenewalService; /** * A public SSH key of a user. @@ -22,7 +20,7 @@ @Entity @Table(name = "user_public_ssh_key") @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class UserPublicSshKey extends DomainObject { +public class UserSshPublicKey extends DomainObject { /** * The user who is owner of the public key @@ -53,11 +51,20 @@ public class UserPublicSshKey extends DomainObject { private String keyHash; /** - * The expiry date of the VCS access token. - * This is used for checking if a access token needs to be renewed. - * - * @see VcsTokenRenewalService - * @see UserRepository#getUsersWithAccessTokenExpirationDateBefore + * The expiry date of the public SSH key + */ + @Column(name = "creation_date") + private ZonedDateTime creationDate = null; + + /** + * The expiry 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") @@ -96,6 +103,23 @@ 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; diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java index 5b10cfef03d8..94f618ea3c5f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java @@ -3,16 +3,19 @@ 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.UserPublicSshKey; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; @Profile(PROFILE_CORE) @Repository -public interface UserPublicSshKeyRepository extends ArtemisJpaRepository { +public interface UserPublicSshKeyRepository extends ArtemisJpaRepository { - List findAllByUserId(Long userId); + List findAllByUserId(Long userId); + + Optional findByKeyHash(String keyHash); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 1bf49a271f70..1f34e85feee8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -7,13 +7,17 @@ import java.security.PublicKey; import java.time.ZonedDateTime; import java.util.List; +import java.util.Objects; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; import de.tum.cit.aet.artemis.core.domain.User; -import de.tum.cit.aet.artemis.programming.domain.UserPublicSshKey; +import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; +import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; +import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.programming.repository.UserPublicSshKeyRepository; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; @@ -27,20 +31,46 @@ public UserSshPublicKeyService(UserPublicSshKeyRepository userPublicSshKeyReposi this.userPublicSshKeyRepository = userPublicSshKeyRepository; } - public void addSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, String sshPublicKey) throws GeneralSecurityException, IOException { + public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshPublicKey sshPublicKey) throws GeneralSecurityException, IOException { PublicKey publicKey = keyEntry.resolvePublicKey(null, null, null); String keyHash = HashUtils.getSha512Fingerprint(publicKey); - UserPublicSshKey userPublicSSHKey = new UserPublicSshKey(); - userPublicSSHKey.setUserId(user.getId()); - userPublicSSHKey.setLabel("Key 1"); - userPublicSSHKey.setPublicKey(sshPublicKey); - userPublicSSHKey.setKeyHash(keyHash); - userPublicSSHKey.setExpiryDate(ZonedDateTime.now().plusMonths(12)); - userPublicSshKeyRepository.save(userPublicSSHKey); + if (userPublicSshKeyRepository.findByKeyHash(keyHash).isPresent()) { + throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true); + } + + UserSshPublicKey newUserSshPublicKey = new UserSshPublicKey(); + newUserSshPublicKey.setUserId(user.getId()); + newUserSshPublicKey.setLabel(sshPublicKey.getLabel()); + newUserSshPublicKey.setPublicKey(sshPublicKey.getPublicKey()); + newUserSshPublicKey.setKeyHash(keyHash); + newUserSshPublicKey.setExpiryDate(sshPublicKey.getExpiryDate()); + newUserSshPublicKey.setCreationDate(ZonedDateTime.now()); + newUserSshPublicKey.setExpiryDate(sshPublicKey.getExpiryDate()); + userPublicSshKeyRepository.save(newUserSshPublicKey); + } + + public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { + var userSshPublicKey = userPublicSshKeyRepository.findByIdElseThrow(keyId); + if (Objects.equals(userSshPublicKey.getUserId(), user.getId())) { + return userSshPublicKey; + } + else { + throw new EntityNotFoundException(); + } } - public List getAllSshKeysForUser(User user) { + public List getAllSshKeysForUser(User user) { return userPublicSshKeyRepository.findAllByUserId(user.getId()); } + + public void deleteUserSshPublicKey(Long userId, Long keyId) { + var key = userPublicSshKeyRepository.findById(userId); + if (key.isPresent() && key.get().getUserId().equals(userId)) { + userPublicSshKeyRepository.deleteById(userId); + } + else { + throw new AccessForbiddenException("SSH key", keyId); + } + } } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java index 4f0cad0aa4e9..b141e8011031 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java @@ -18,6 +18,7 @@ import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.programming.repository.UserPublicSshKeyRepository; import de.tum.cit.aet.artemis.programming.service.localci.SharedQueueManagementService; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.SshConstants; @@ -32,19 +33,27 @@ public class GitPublickeyAuthenticatorService implements PublickeyAuthenticator private final Optional localCIBuildJobQueueService; - public GitPublickeyAuthenticatorService(UserRepository userRepository, Optional localCIBuildJobQueueService) { + private final UserPublicSshKeyRepository userPublicSshKeyRepository; + + public GitPublickeyAuthenticatorService(UserRepository userRepository, Optional localCIBuildJobQueueService, + UserPublicSshKeyRepository userPublicSshKeyRepository) { this.userRepository = userRepository; this.localCIBuildJobQueueService = localCIBuildJobQueueService; + this.userPublicSshKeyRepository = userPublicSshKeyRepository; } @Override public boolean authenticate(String username, PublicKey publicKey, ServerSession session) { String keyHash = HashUtils.getSha512Fingerprint(publicKey); - var user = userRepository.findBySshPublicKeyHash(keyHash); + var userSshPublicKey = userPublicSshKeyRepository.findByKeyHash(keyHash); + if (userSshPublicKey.isEmpty()) { + return false; + } + var user = userRepository.findById(userSshPublicKey.get().getUserId()); if (user.isPresent()) { try { // Retrieve the stored public key string - String storedPublicKeyString = user.get().getSshPublicKey(); + String storedPublicKeyString = userSshPublicKey.get().getPublicKey(); // Parse the stored public key string AuthorizedKeyEntry keyEntry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(storedPublicKeyString); diff --git a/src/main/resources/config/liquibase/changelog/20241013150000_changelog.xml b/src/main/resources/config/liquibase/changelog/20241013150000_changelog.xml index 54f3e49b45c6..71112d602b10 100644 --- a/src/main/resources/config/liquibase/changelog/20241013150000_changelog.xml +++ b/src/main/resources/config/liquibase/changelog/20241013150000_changelog.xml @@ -22,6 +22,12 @@ + + + + + + diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts index 8d8e879d5837..01e5994dbadf 100644 --- a/src/main/webapp/app/core/auth/account.service.ts +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -1,7 +1,7 @@ import { Injectable, inject } from '@angular/core'; import { SessionStorageService } from 'ngx-webstorage'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; -import { BehaviorSubject, Observable, lastValueFrom, of, tap } from 'rxjs'; +import { BehaviorSubject, Observable, lastValueFrom, of } from 'rxjs'; import { catchError, distinctUntilChanged, map } from 'rxjs/operators'; import { Course } from 'app/entities/course.model'; import { User } from 'app/core/user/user.model'; @@ -341,18 +341,20 @@ export class AccountService implements IAccountService { return this.http.get('api/account/ssh-public-keys'); } + /** + * Retrieves a specific public SSH keys of a user + */ + getSshPublicKey(keyId: number): Observable { + const params = new HttpParams().set('keyId', keyId); + return this.http.get('api/account/ssh-public-key', { params }); + } + /** * Sends a request to the server to delete the user's current SSH key */ - deleteSshPublicKey(sshPublicKeyHash: string): Observable { - const params = new HttpParams().set('sshPublicKeyHash', sshPublicKeyHash); - return this.http.delete('api/account/ssh-public-key', { params }).pipe( - tap(() => { - if (this.userIdentity) { - this.userIdentity.sshPublicKey = undefined; - } - }), - ); + deleteSshPublicKey(keyId: number): Observable { + const params = new HttpParams().set('keyId', keyId); + return this.http.delete('api/account/ssh-public-key', { params }); } /** diff --git a/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts b/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts index 64c5e3cb80f5..bde84efe2893 100644 --- a/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts +++ b/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts @@ -2,7 +2,7 @@ import { BaseEntity } from 'app/shared/model/base-entity'; import dayjs from 'dayjs/esm'; export class UserSshPublicKey implements BaseEntity { - id?: number; + id: number; label: string; publicKey: string; keyHash: string; diff --git a/src/main/webapp/app/shared/components/code-button/code-button.component.ts b/src/main/webapp/app/shared/components/code-button/code-button.component.ts index ba1c7601f42c..028e831f68ce 100644 --- a/src/main/webapp/app/shared/components/code-button/code-button.component.ts +++ b/src/main/webapp/app/shared/components/code-button/code-button.component.ts @@ -159,7 +159,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { public useSshUrl() { this.useSsh = true; this.useToken = false; - this.copyEnabled = this.useSsh && (!!this.user.sshPublicKey || this.gitlabVCEnabled); + this.copyEnabled = true; // this.useSsh && (!!this.user.sshPublicKey || this.gitlabVCEnabled); this.storeToLocalStorage(); } diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html new file mode 100644 index 000000000000..6c4df4531929 --- /dev/null +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html @@ -0,0 +1,70 @@ +

+ +

+@if (!isLoading) { +
+ +
+ @if (!inCreateMode) { +

+ } @else { +

+ } + +
+

+ + +

+
+ +
+

+ +
+ +
+

+ + {{ copyInstructions }} +

+
+ +
+

+ +
+ + @if (!inCreateMode) { +
+

+
+ {{ displayedKeyHash }} +
+
+ } +
+ @if (inCreateMode) { +
+ +
+ } +
+ +
+
+
+
+} diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts new file mode 100644 index 000000000000..7744288406fa --- /dev/null +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -0,0 +1,152 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { AccountService } from 'app/core/auth/account.service'; +import { Subject, Subscription, concatMap, filter, tap } from 'rxjs'; +import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { PROFILE_LOCALVC } from 'app/app.constants'; +import { faEdit, faEllipsis, faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; +import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; +import { AlertService } from 'app/core/util/alert.service'; +import { getOS } from 'app/shared/util/os-detector.util'; +import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; +import dayjs from 'dayjs/esm'; + +@Component({ + selector: 'jhi-account-information', + templateUrl: './ssh-user-settings-key-details.component.html', + styleUrls: ['../../user-settings.scss', '../ssh-user-settings.component.scss'], +}) +export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { + readonly documentationType: DocumentationType = 'SshSetup'; + readonly invalidKeyFormat = 'invalidKeyFormat'; + readonly keyAlreadyExists = 'keyAlreadyExists'; + + subscription: Subscription; + + sshPublicKey: UserSshPublicKey; + localVCEnabled = false; + + // state change variables + inCreateMode = false; // true when editing existing key, false when creating new key + + keyCount = 0; + isLoading = true; + copyInstructions = ''; + + // Key details from input fields + displayedKeyId?: number = undefined; // undefined when creating a new key + displayedKeyLabel = ''; + displayedSshKey = ''; + displayedKeyHash = ''; + displayedExpiryDate?: dayjs.Dayjs; + + readonly faEdit = faEdit; + readonly faSave = faSave; + readonly faTrash = faTrash; + readonly faEllipsis = faEllipsis; + protected readonly ButtonType = ButtonType; + protected readonly ButtonSize = ButtonSize; + private dialogErrorSource = new Subject(); + dialogError$ = this.dialogErrorSource.asObservable(); + + constructor( + private accountService: AccountService, + private profileService: ProfileService, + private route: ActivatedRoute, + private router: Router, + private alertService: AlertService, + ) {} + + ngOnInit() { + this.profileService.getProfileInfo().subscribe((profileInfo) => { + this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + }); + this.setMessageBasedOnOS(getOS()); + + this.subscription = this.route.params + .pipe( + filter((params) => { + const keyId = Number(params['keyId']); + if (keyId) { + this.inCreateMode = false; + return true; + } else { + this.isLoading = false; + this.inCreateMode = true; + return false; + } + }), + concatMap((params) => { + return this.accountService.getSshPublicKey(Number(params['keyId'])); + }), + tap((publicKey: UserSshPublicKey) => { + this.displayedSshKey = publicKey.publicKey; + this.isLoading = false; + }), + ) + .subscribe(); + } + + ngOnDestroy() { + this.subscription.unsubscribe(); + } + + saveSshKey() { + const newUserSshKey = { + label: this.displayedKeyLabel, + publicKey: this.displayedSshKey, + expiryDate: this.displayedExpiryDate, + } as UserSshPublicKey; + this.accountService.addNewSshPublicKey(newUserSshKey).subscribe({ + next: () => { + this.alertService.success('artemisApp.userSettings.sshSettingsPage.saveSuccess'); + this.goBack(); + }, + error: (error) => { + const errorKey = error.error.errorKey; + if (errorKey == this.invalidKeyFormat || errorKey == this.keyAlreadyExists) { + this.alertService.error(`artemisApp.userSettings.sshSettingsPage.${errorKey}`); + } else { + this.alertService.error('artemisApp.userSettings.sshSettingsPage.saveFailure'); + } + }, + }); + } + + goBack() { + if (this.inCreateMode) { + this.router.navigate(['../'], { relativeTo: this.route }); + } else { + this.router.navigate(['../../'], { relativeTo: this.route }); + } + } + + editExistingSshKey(key: UserSshPublicKey) { + this.inCreateMode = false; + this.displayedKeyId = key.id; + this.displayedSshKey = key.publicKey; + this.displayedKeyLabel = key.label; + this.displayedKeyHash = key.keyHash; + this.displayedExpiryDate = key.expiryDate; + } + + private setMessageBasedOnOS(os: string): void { + switch (os) { + case 'Windows': + this.copyInstructions = 'cat ~/.ssh/id_ed25519.pub | clip'; + break; + case 'MacOS': + this.copyInstructions = 'pbcopy < ~/.ssh/id_ed25519.pub'; + break; + case 'Linux': + this.copyInstructions = 'xclip -selection clipboard < ~/.ssh/id_ed25519.pub'; + break; + case 'Android': + this.copyInstructions = 'termux-clipboard-set < ~/.ssh/id_ed25519.pub'; + break; + default: + this.copyInstructions = 'Ctrl + C'; + } + } +} diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html index d820d83ca39c..bc5f591ed501 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html @@ -1,10 +1,10 @@

-@if (!isLoading) { +@if (!isLoading && localVCEnabled) {
- @if (keyCount === 0 && !showKeyDetailsView) { + @if (keyCount === 0) {

@@ -17,27 +17,23 @@

- + + +

} - @if (keyCount > 0 && !showKeyDetailsView) { + @if (keyCount > 0) {

- +
+ + + +

@@ -57,9 +53,9 @@

@for (key of sshPublicKeys; track key; let i = $index) { - +
{{ key.label }}
-
+
{{ key.keyHash }}
@@ -72,19 +68,19 @@

@@ -96,84 +92,5 @@

} - - - @if (showKeyDetailsView) { -
- @if (inCreateMode) { -

- } @else { -

- } - -
-

- - - -

-
- -
-

- -
- -
-

- - {{ copyInstructions }} -

-
- -
-

- -
- - @if (!inCreateMode) { -
-

-
- {{ displayedKeyHash }} -
-
- } - - @if (!inCreateMode) { -
-
- -
-
- -
-
- } @else { -
-
- -
-
- } -
- }
} diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss index 502ffbd1deb9..677ac65abe31 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss @@ -34,6 +34,15 @@ td { border-radius: 0; } +.rounded-btn { + border-radius: 2px; + border-top-width: 0; +} + +.bottom-border { + border-bottom-width: 2px; +} + .container { position: relative; } @@ -61,14 +70,21 @@ td { transform: translateY(-50%); } +.abbreviate-hash { + font-size: x-small; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + tbody tr:hover { - background-color: var(--ssh-key-table-hover-background); + background-color: var(--ssh-key-settings-table-hover-background); } /* dd container */ .dropdown { display: inline-block; - position: relative; + position: absolute; outline: none; margin: 0; } @@ -84,7 +100,7 @@ tbody tr:hover { /* dd content */ .dropdown .dropdown-content { position: absolute; - top: 50%; + top: -25%; min-width: 120%; box-shadow: 0 8px 16px var(--ssh-key-settings-shadow); z-index: 100000; // makes sure the drop down menu is always shown at the highest view plane @@ -96,6 +112,7 @@ tbody tr:hover { /* show dd content */ .dropdown:focus .dropdown-content { + z-index: 100000; outline: none; transform: translateY(20px); visibility: visible; @@ -128,14 +145,17 @@ tbody tr:hover { } .dropdown-button { - margin: auto; color: var(--ssh-key-settings-text-color); background-color: var(--dropdown-bg); border-color: var(--dropdown-bg); - width: 80px; + max-width: 80px; + width: 100%; + text-align: left; + font-size: smaller; } .dropdown-button:hover { background-color: var(--ssh-key-settings-dropdown-buttons-hover); border-color: var(--ssh-key-settings-dropdown-buttons-hover); + display: block; } diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts index ed55e272fbdf..833d4e6d6b3e 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts @@ -7,9 +7,7 @@ import { faEdit, faEllipsis, faSave, faTrash } from '@fortawesome/free-solid-svg import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; import { AlertService } from 'app/core/util/alert.service'; -import { getOS } from 'app/shared/util/os-detector.util'; import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; -import dayjs from 'dayjs/esm'; @Component({ selector: 'jhi-account-information', @@ -22,20 +20,8 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { sshPublicKeys: UserSshPublicKey[] = []; localVCEnabled = false; - // state change variables - showKeyDetailsView = false; - inCreateMode = false; // true when editing existing key, false when creating new key - keyCount = 0; isLoading = true; - copyInstructions = ''; - - // Key details from input fields - displayedKeyId?: number = undefined; // undefined when creating a new key - displayedKeyLabel = ''; - displayedSshKey = ''; - displayedKeyHash = ''; - displayedExpiryDate?: dayjs.Dayjs; readonly faEdit = faEdit; readonly faSave = faSave; @@ -58,12 +44,10 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); }); - this.setMessageBasedOnOS(getOS()); this.accountServiceSubscription = this.accountService .getAllSshPublicKeys() .pipe( tap((publicKeys: UserSshPublicKey[]) => { - this.alertService.success('artemisApp.userSettings.sshSettingsPage.saveSuccess'); this.sshPublicKeys = publicKeys; this.keyCount = publicKeys.length; this.isLoading = false; @@ -76,36 +60,15 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { this.accountServiceSubscription.unsubscribe(); } - saveSshKey() { - const newUserSshKey = { - id: this.displayedKeyId, - label: this.displayedKeyLabel, - publicKey: this.displayedSshKey, - expiryDate: this.displayedExpiryDate, - } as UserSshPublicKey; - - this.accountService.addNewSshPublicKey(newUserSshKey).subscribe({ - next: (res) => { - const newlyCreatedKey = res.body!; - this.sshPublicKeys.push(newlyCreatedKey); - this.showKeyDetailsView = false; - this.keyCount = this.keyCount + 1; - this.inCreateMode = true; - this.alertService.success('artemisApp.userSettings.sshSettingsPage.saveSuccess'); - }, - error: () => { - this.alertService.error('artemisApp.userSettings.sshSettingsPage.saveFailure'); - }, - }); - } - - deleteSshKey() { - this.showKeyDetailsView = false; - this.accountService.deleteSshPublicKey('ab').subscribe({ + deleteSshKey(key: UserSshPublicKey) { + this.accountService.deleteSshPublicKey(key.id).subscribe({ next: () => { this.alertService.success('artemisApp.userSettings.sshSettingsPage.deleteSuccess'); this.keyCount = this.keyCount - 1; - this.inCreateMode = false; + const index = this.sshPublicKeys.indexOf(key); + if (index >= 0) { + this.sshPublicKeys.splice(index, 1); + } }, error: () => { this.alertService.error('artemisApp.userSettings.sshSettingsPage.deleteFailure'); @@ -113,43 +76,4 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { }); this.dialogErrorSource.next(''); } - - cancelEditingSshKey() { - this.showKeyDetailsView = false; - } - - editExistingSshKey(key: UserSshPublicKey) { - this.showKeyDetailsView = true; - this.inCreateMode = false; - this.displayedKeyId = key.id; - this.displayedSshKey = key.publicKey; - this.displayedKeyLabel = key.label; - this.displayedKeyHash = key.keyHash; - this.displayedExpiryDate = key.expiryDate; - } - - createNewSshKey() { - this.showKeyDetailsView = true; - this.inCreateMode = false; - this.displayedKeyId = undefined; - } - - private setMessageBasedOnOS(os: string): void { - switch (os) { - case 'Windows': - this.copyInstructions = 'cat ~/.ssh/id_ed25519.pub | clip'; - break; - case 'MacOS': - this.copyInstructions = 'pbcopy < ~/.ssh/id_ed25519.pub'; - break; - case 'Linux': - this.copyInstructions = 'xclip -selection clipboard < ~/.ssh/id_ed25519.pub'; - break; - case 'Android': - this.copyInstructions = 'termux-clipboard-set < ~/.ssh/id_ed25519.pub'; - break; - default: - this.copyInstructions = 'Ctrl + C'; - } - } } diff --git a/src/main/webapp/app/shared/user-settings/user-settings.module.ts b/src/main/webapp/app/shared/user-settings/user-settings.module.ts index cd87f408b5a9..11c876830750 100644 --- a/src/main/webapp/app/shared/user-settings/user-settings.module.ts +++ b/src/main/webapp/app/shared/user-settings/user-settings.module.ts @@ -12,6 +12,7 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time-picker.module'; import { IdeSettingsComponent } from 'app/shared/user-settings/ide-preferences/ide-settings.component'; import { DocumentationLinkComponent } from 'app/shared/components/documentation-link/documentation-link.component'; +import { SshUserSettingsKeyDetailsComponent } from 'app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component'; @NgModule({ imports: [RouterModule.forChild(userSettingsState), ArtemisSharedModule, ArtemisSharedComponentModule, ClipboardModule, FormDateTimePickerModule, DocumentationLinkComponent], @@ -20,6 +21,7 @@ import { DocumentationLinkComponent } from 'app/shared/components/documentation- NotificationSettingsComponent, ScienceSettingsComponent, SshUserSettingsComponent, + SshUserSettingsKeyDetailsComponent, VcsAccessTokensSettingsComponent, IdeSettingsComponent, ], diff --git a/src/main/webapp/app/shared/user-settings/user-settings.route.ts b/src/main/webapp/app/shared/user-settings/user-settings.route.ts index 367c8322a632..41d8a2084df7 100644 --- a/src/main/webapp/app/shared/user-settings/user-settings.route.ts +++ b/src/main/webapp/app/shared/user-settings/user-settings.route.ts @@ -8,6 +8,7 @@ import { ScienceSettingsComponent } from 'app/shared/user-settings/science-setti import { SshUserSettingsComponent } from 'app/shared/user-settings/ssh-settings/ssh-user-settings.component'; import { VcsAccessTokensSettingsComponent } from 'app/shared/user-settings/vcs-access-tokens-settings/vcs-access-tokens-settings.component'; import { IdeSettingsComponent } from 'app/shared/user-settings/ide-preferences/ide-settings.component'; +import { SshUserSettingsKeyDetailsComponent } from 'app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component'; export const userSettingsState: Routes = [ { @@ -52,6 +53,20 @@ export const userSettingsState: Routes = [ pageTitle: 'artemisApp.userSettings.categories.SSH_SETTINGS', }, }, + { + path: 'ssh/add', + component: SshUserSettingsKeyDetailsComponent, + data: { + pageTitle: 'artemisApp.userSettings.categories.SSH_SETTINGS', + }, + }, + { + path: 'ssh/view/:keyId', + component: SshUserSettingsKeyDetailsComponent, + data: { + pageTitle: 'artemisApp.userSettings.categories.SSH_SETTINGS', + }, + }, { path: 'vcs-token', component: VcsAccessTokensSettingsComponent, diff --git a/src/main/webapp/content/scss/themes/_dark-variables.scss b/src/main/webapp/content/scss/themes/_dark-variables.scss index daa039c0e167..8ed8a5523727 100644 --- a/src/main/webapp/content/scss/themes/_dark-variables.scss +++ b/src/main/webapp/content/scss/themes/_dark-variables.scss @@ -664,7 +664,7 @@ $iris-rate-background: var(--neutral-dark-l-15); $cropper-overlay-color: transparent; // Settings -$ssh-key-table-hover-background: $gray-900; +$ssh-key-settings-table-hover-background: $gray-900; $ssh-key-settings-dropdown-buttons: $gray-800; $ssh-key-settings-dropdown-buttons-hover: $gray-700; $ssh-key-settings-text-color: $white; diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index 6a2ebecc1763..3800c61376d8 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -22,7 +22,9 @@ "sshSettingsInfoText": "Hier kannst du einen öffentlichen SSH-Schlüssel festlegen, um Git-Repositories über SSH zu klonen. Weitere Einzelheiten findest du in der Dokumentation.", "deleteSshKeyQuestion": "Möchtest du deinen öffentlichen SSH-Schlüssel wirklich löschen?", "saveSshKey": "Speichern", - "saveFailure": "SSH-Schlüssel konnte nicht gespeichert werden. Stelle sicher, dass es ein gültiger und unterstützter Schlüssel im richtigen Format ist.", + "invalidKeyFormat": "SSH-Schlüssel konnte nicht gespeichert werden. Stelle sicher, dass es ein gültiger und unterstützter Schlüssel im richtigen Format ist.", + "keyAlreadyExists": "SSH-Schlüssel konnte nicht gespeichert werden. Du verwendest diesen Schlüssel schon.", + "saveFailure": "SSH-Schlüssel konnte nicht gespeichert werden.", "saveSuccess": "SSH-Schlüssel erfolgreich gespeichert.", "deleteFailure": "SSH-Schlüssel konnte nicht gelöscht werden.", "deleteSuccess": "SSH-Schlüssel erfolgreich gelöscht.", diff --git a/src/main/webapp/i18n/en/userSettings.json b/src/main/webapp/i18n/en/userSettings.json index 29afe83a0c3c..d6a856b854d3 100644 --- a/src/main/webapp/i18n/en/userSettings.json +++ b/src/main/webapp/i18n/en/userSettings.json @@ -22,7 +22,9 @@ "sshSettingsInfoText": "Here you can set a public SSH key to clone git repositories via SSH. For more details see the documentation.", "deleteSshKeyQuestion": "Are you sure you want to delete your public SSH key?", "saveSshKey": "Save", - "saveFailure": "Failed to save SSH key. Make sure it is a valid and supported key and in the correct format.", + "invalidKeyFormat": "Failed to save SSH key. Make sure the key is in a valid and supported format.", + "keyAlreadyExists": "Failed to save SSH key. You already use the provided key.", + "saveFailure": "Failed to save SSH key.", "saveSuccess": "Successfully save SSH key.", "deleteFailure": "Failed to delete SSH key.", "deleteSuccess": "Successfully deleted SSH key.", From af8f80b873aeb1cace4931bc08cb4e2899fcf08c Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 17 Oct 2024 20:18:02 +0200 Subject: [PATCH 03/47] fixed css --- .../service/BuildJobContainerService.java | 4 +- .../service/UserSshPublicKeyService.java | 6 +- .../changelog/20241013150001_changelog.xml | 106 ++++++++++++++++++ .../resources/config/liquibase/master.xml | 1 + .../ssh-user-settings.component.html | 103 ++++++++++++++--- .../ssh-user-settings.component.scss | 80 ++----------- .../ssh-user-settings.component.ts | 8 +- 7 files changed, 213 insertions(+), 95 deletions(-) create mode 100644 src/main/resources/config/liquibase/changelog/20241013150001_changelog.xml diff --git a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java index b7c97daa9786..6cd686f401c7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java +++ b/src/main/java/de/tum/cit/aet/artemis/buildagent/service/BuildJobContainerService.java @@ -103,8 +103,8 @@ public CreateContainerResponse configureContainer(String containerName, String i // container from exiting until it finishes. // It waits until the script that is running the tests (see below execCreateCmdResponse) is completed, and until the result files are extracted which is indicated // by the creation of a file "stop_container.txt" in the container's root directory. - .withCmd("sh", "-c", "while [ ! -f " + LOCALCI_WORKING_DIRECTORY + "/stop_container.txt ]; do sleep 0.5; done") - // .withCmd("tail", "-f", "/dev/null") // Activate for debugging purposes instead of the above command to get a running container that you can peek into using + // .withCmd("sh", "-c", "while [ ! -f " + LOCALCI_WORKING_DIRECTORY + "/stop_container.txt ]; do sleep 0.5; done") + .withCmd("tail", "-f", "/dev/null") // Activate for debugging purposes instead of the above command to get a running container that you can peek into using // "docker exec -it /bin/bash". .exec(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 1f34e85feee8..b219824029d7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -65,9 +65,9 @@ public List getAllSshKeysForUser(User user) { } public void deleteUserSshPublicKey(Long userId, Long keyId) { - var key = userPublicSshKeyRepository.findById(userId); - if (key.isPresent() && key.get().getUserId().equals(userId)) { - userPublicSshKeyRepository.deleteById(userId); + var keys = userPublicSshKeyRepository.findAllByUserId(userId); + if (!keys.isEmpty() && keys.stream().map(UserSshPublicKey::getId).toList().contains(keyId)) { + userPublicSshKeyRepository.deleteById(keyId); } else { throw new AccessForbiddenException("SSH key", keyId); diff --git a/src/main/resources/config/liquibase/changelog/20241013150001_changelog.xml b/src/main/resources/config/liquibase/changelog/20241013150001_changelog.xml new file mode 100644 index 000000000000..f01847215b8d --- /dev/null +++ b/src/main/resources/config/liquibase/changelog/20241013150001_changelog.xml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 57fd83fe180c..c48e1ec82958 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -29,6 +29,7 @@ + diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html index bc5f591ed501..93e661d59087 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html @@ -61,30 +61,99 @@

-
-
-

+ @if (inCreateMode) { +

+ } @else { +

{{ displayedKeyLabel }}

+ }
@@ -30,19 +34,26 @@

-
-

- -
+ @if (inCreateMode) { +
+

+ +
+ } +
@if (inCreateMode) {
diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts index 7744288406fa..1f85a52aa0d6 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -1,10 +1,8 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { AccountService } from 'app/core/auth/account.service'; import { Subject, Subscription, concatMap, filter, tap } from 'rxjs'; -import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { ActivatedRoute, Router } from '@angular/router'; -import { PROFILE_LOCALVC } from 'app/app.constants'; -import { faEdit, faEllipsis, faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faEdit, faSave } from '@fortawesome/free-solid-svg-icons'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; import { AlertService } from 'app/core/util/alert.service'; @@ -25,12 +23,10 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { subscription: Subscription; sshPublicKey: UserSshPublicKey; - localVCEnabled = false; // state change variables inCreateMode = false; // true when editing existing key, false when creating new key - keyCount = 0; isLoading = true; copyInstructions = ''; @@ -40,11 +36,10 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { displayedSshKey = ''; displayedKeyHash = ''; displayedExpiryDate?: dayjs.Dayjs; + displayCreationDate: dayjs.Dayjs; readonly faEdit = faEdit; readonly faSave = faSave; - readonly faTrash = faTrash; - readonly faEllipsis = faEllipsis; protected readonly ButtonType = ButtonType; protected readonly ButtonSize = ButtonSize; private dialogErrorSource = new Subject(); @@ -52,16 +47,12 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { constructor( private accountService: AccountService, - private profileService: ProfileService, private route: ActivatedRoute, private router: Router, private alertService: AlertService, ) {} ngOnInit() { - this.profileService.getProfileInfo().subscribe((profileInfo) => { - this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); - }); this.setMessageBasedOnOS(getOS()); this.subscription = this.route.params @@ -82,6 +73,9 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { }), tap((publicKey: UserSshPublicKey) => { this.displayedSshKey = publicKey.publicKey; + this.displayedKeyLabel = publicKey.label; + this.displayedKeyHash = publicKey.keyHash; + this.displayCreationDate = publicKey.creationDate; this.isLoading = false; }), ) diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html index 93e661d59087..795dff8ce150 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html @@ -54,10 +54,18 @@

-
{{ key.label }}
+
+ {{ key.label }} +
{{ key.keyHash }}
+
+
+
+ {{ key.creationDate | artemisDate: 'long-date' }} +
+
diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss index 4ae41a2b9caa..44105fcef90c 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss @@ -11,6 +11,15 @@ textarea { border-color: var(--border-color) !important; } +.bold-text { + font-weight: bold; +} + +.text-and-date { + display: flex; + gap: 5px; +} + .small-text-area { height: 30px; } @@ -23,6 +32,10 @@ table { text-align: left; } +.smaller-font { + font-size: smaller; +} + th, td { padding: 15px 15px; diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index 3800c61376d8..b69f53e33a57 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -44,9 +44,17 @@ "keysTablePageTitle": "SSH-Schlüssel", "keys": "Schlüssel", "actions": "Aktionen", - "keyName": "Key 1 (Aktuell wird nur ein Schlüssel unterstützt)" + "keyName": "Key 1 (Aktuell wird nur ein Schlüssel unterstützt)", + "label": "Label", + "creationDate": "Erstellungsdatum", + "createdOn": "Erstellt am", + "lastUsed": "Letzte Verwendung", + "lastUsedOn": "Letzte Verwendung am", + "expiryDate": "Ablaufdatum", + "leaveEmpty": "Wenn du das Label leer lässt, wird der Kommentar aus dem SSH-Schlüssel verwendet.", + "fingerprint": "Fingerabdruck" }, - "vcsAccessTokensSettingsPage": { + "vcsAccessTokensSettingsFingerabdruckPage": { "addTokenTitle": "Neues Zugriffstoken erzeugen", "infoText": "Du kannst ein persönliches Zugriffstoken generieren, um mit dem Artemis Local Version Control System zu interagieren. Verwende es um dich über HTTP bei Git zu authentifizieren.", "deleteVcsAccessTokenQuestion": "Möchtest du dein Zugriffstoken für das Versionskontrollsystem wirklich löschen? Du kannst dich nicht mehr bei lokalen Repositories authentifizieren, die mit diesem Token geklont wurden.", diff --git a/src/main/webapp/i18n/en/userSettings.json b/src/main/webapp/i18n/en/userSettings.json index d6a856b854d3..6a6e926f3b0c 100644 --- a/src/main/webapp/i18n/en/userSettings.json +++ b/src/main/webapp/i18n/en/userSettings.json @@ -44,7 +44,15 @@ "keysTablePageTitle": "SSH keys", "keys": "Keys", "actions": "Actions", - "keyName": "Key 1 (at the moment you can only have one key)" + "keyName": "Key 1 (at the moment you can only have one key)", + "label": "Label", + "creationDate": "Creation date:", + "createdOn": "Created on", + "lastUsed": "Last used", + "lastUsedOn": "Last used on", + "expiryDate": "Expiry date", + "leaveEmpty": "If you leave the label empty, the comment from the SSH key will be used.", + "fingerprint": "Fingerprint" }, "vcsAccessTokensSettingsPage": { "addTokenTitle": "Add personal access token", From 580513992e8706e0f5cf21390ae2bedc2b6a5ebd Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 20 Oct 2024 19:33:02 +0200 Subject: [PATCH 08/47] fixed build agent authentication --- .../GitPublickeyAuthenticatorService.java | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java index b141e8011031..33ff37b58b36 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java @@ -18,6 +18,7 @@ import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; import de.tum.cit.aet.artemis.core.repository.UserRepository; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.programming.repository.UserPublicSshKeyRepository; import de.tum.cit.aet.artemis.programming.service.localci.SharedQueueManagementService; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; @@ -46,36 +47,45 @@ public GitPublickeyAuthenticatorService(UserRepository userRepository, Optional< public boolean authenticate(String username, PublicKey publicKey, ServerSession session) { String keyHash = HashUtils.getSha512Fingerprint(publicKey); var userSshPublicKey = userPublicSshKeyRepository.findByKeyHash(keyHash); - if (userSshPublicKey.isEmpty()) { - return false; + if (userSshPublicKey.isPresent()) { + return authenticateUser(userSshPublicKey.get(), publicKey, session); + } + else { + return authenticateBuildAgent(publicKey, session); } - var user = userRepository.findById(userSshPublicKey.get().getUserId()); - if (user.isPresent()) { - try { - // Retrieve the stored public key string - String storedPublicKeyString = userSshPublicKey.get().getPublicKey(); - - // Parse the stored public key string - AuthorizedKeyEntry keyEntry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(storedPublicKeyString); - PublicKey storedPublicKey = keyEntry.resolvePublicKey(null, null, null); - - // Compare the stored public key with the provided public key - if (Objects.equals(storedPublicKey, publicKey)) { - log.debug("Found user {} for public key authentication", user.get().getLogin()); - session.setAttribute(SshConstants.USER_KEY, user.get()); - session.setAttribute(SshConstants.IS_BUILD_AGENT_KEY, false); - return true; - } - else { - log.warn("Public key mismatch for user {}", user.get().getLogin()); - } + } + + public boolean authenticateUser(UserSshPublicKey storedKey, PublicKey providedKey, ServerSession session) { + try { + var user = userRepository.findById(storedKey.getUserId()); + if (user.isEmpty()) { + return false; + } + // Retrieve and parse the stored public key string + AuthorizedKeyEntry keyEntry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(storedKey.getPublicKey()); + PublicKey storedPublicKey = keyEntry.resolvePublicKey(null, null, null); + + // Compare the stored public key with the provided public key + if (Objects.equals(storedPublicKey, providedKey)) { + log.debug("Found user {} for public key authentication", user.get().getLogin()); + session.setAttribute(SshConstants.USER_KEY, user.get()); + session.setAttribute(SshConstants.IS_BUILD_AGENT_KEY, false); + return true; } - catch (Exception e) { - log.error("Failed to convert stored public key string to PublicKey object", e); + else { + log.warn("Public key mismatch for user {}", user.get().getLogin()); } } - else if (localCIBuildJobQueueService.isPresent() - && localCIBuildJobQueueService.get().getBuildAgentInformation().stream().anyMatch(agent -> checkPublicKeyMatchesBuildAgentPublicKey(agent, publicKey))) { + catch (Exception e) { + log.error("Failed to convert stored public key string to PublicKey object", e); + } + return false; + } + + private boolean authenticateBuildAgent(PublicKey providedKey, ServerSession session) { + if (localCIBuildJobQueueService.isPresent() + && localCIBuildJobQueueService.get().getBuildAgentInformation().stream().anyMatch(agent -> checkPublicKeyMatchesBuildAgentPublicKey(agent, providedKey))) { + log.info("Authenticating as build agent"); session.setAttribute(SshConstants.IS_BUILD_AGENT_KEY, true); return true; From a186891cd486818492f041e26b872cabcb023fe7 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sun, 20 Oct 2024 21:58:57 +0200 Subject: [PATCH 09/47] fixed authentication in tests --- .../core/repository/UserRepository.java | 12 --------- .../aet/artemis/core/web/AccountResource.java | 1 - ...y.java => UserSshPublicKeyRepository.java} | 2 +- .../service/UserSshPublicKeyService.java | 20 +++++++------- .../GitPublickeyAuthenticatorService.java | 10 +++---- .../icl/LocalVCSshIntegrationTest.java | 27 ++++++++++++++++--- 6 files changed, 39 insertions(+), 33 deletions(-) rename src/main/java/de/tum/cit/aet/artemis/programming/repository/{UserPublicSshKeyRepository.java => UserSshPublicKeyRepository.java} (90%) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java index 5b66b31aee98..0d3280cf5d96 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/repository/UserRepository.java @@ -724,16 +724,6 @@ default Page 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(""" @@ -1120,8 +1110,6 @@ default boolean isCurrentUser(String login) { return SecurityUtils.getCurrentUserLogin().map(currentLogin -> currentLogin.equals(login)).orElse(false); } - Optional findBySshPublicKeyHash(String keyString); - /** * Finds all users which a non-null VCS access token that expires before some given date. * diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 38cbf8070c75..030f77da982d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -185,7 +185,6 @@ public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKey sshPub } // Extract the PublicKey object userSshPublicKeyService.createSshKeyForUser(user, keyEntry, sshPublicKey); - // userRepository.updateUserSshPublicKeyHash(user.getId(), keyHash, sshPublicKey); return ResponseEntity.ok().build(); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java similarity index 90% rename from src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java rename to src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java index 94f618ea3c5f..8602e39862a0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserPublicSshKeyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java @@ -13,7 +13,7 @@ @Profile(PROFILE_CORE) @Repository -public interface UserPublicSshKeyRepository extends ArtemisJpaRepository { +public interface UserSshPublicKeyRepository extends ArtemisJpaRepository { List findAllByUserId(Long userId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index b219824029d7..bb7d7f0b2aed 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -18,24 +18,24 @@ import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; -import de.tum.cit.aet.artemis.programming.repository.UserPublicSshKeyRepository; +import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; @Profile(PROFILE_CORE) @Service public class UserSshPublicKeyService { - private final UserPublicSshKeyRepository userPublicSshKeyRepository; + private final UserSshPublicKeyRepository userSshPublicKeyRepository; - public UserSshPublicKeyService(UserPublicSshKeyRepository userPublicSshKeyRepository) { - this.userPublicSshKeyRepository = userPublicSshKeyRepository; + public UserSshPublicKeyService(UserSshPublicKeyRepository userSshPublicKeyRepository) { + this.userSshPublicKeyRepository = userSshPublicKeyRepository; } public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshPublicKey sshPublicKey) throws GeneralSecurityException, IOException { PublicKey publicKey = keyEntry.resolvePublicKey(null, null, null); String keyHash = HashUtils.getSha512Fingerprint(publicKey); - if (userPublicSshKeyRepository.findByKeyHash(keyHash).isPresent()) { + if (userSshPublicKeyRepository.findByKeyHash(keyHash).isPresent()) { throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true); } @@ -47,11 +47,11 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP newUserSshPublicKey.setExpiryDate(sshPublicKey.getExpiryDate()); newUserSshPublicKey.setCreationDate(ZonedDateTime.now()); newUserSshPublicKey.setExpiryDate(sshPublicKey.getExpiryDate()); - userPublicSshKeyRepository.save(newUserSshPublicKey); + userSshPublicKeyRepository.save(newUserSshPublicKey); } public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { - var userSshPublicKey = userPublicSshKeyRepository.findByIdElseThrow(keyId); + var userSshPublicKey = userSshPublicKeyRepository.findByIdElseThrow(keyId); if (Objects.equals(userSshPublicKey.getUserId(), user.getId())) { return userSshPublicKey; } @@ -61,13 +61,13 @@ public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { } public List getAllSshKeysForUser(User user) { - return userPublicSshKeyRepository.findAllByUserId(user.getId()); + return userSshPublicKeyRepository.findAllByUserId(user.getId()); } public void deleteUserSshPublicKey(Long userId, Long keyId) { - var keys = userPublicSshKeyRepository.findAllByUserId(userId); + var keys = userSshPublicKeyRepository.findAllByUserId(userId); if (!keys.isEmpty() && keys.stream().map(UserSshPublicKey::getId).toList().contains(keyId)) { - userPublicSshKeyRepository.deleteById(keyId); + userSshPublicKeyRepository.deleteById(keyId); } else { throw new AccessForbiddenException("SSH key", keyId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java index 33ff37b58b36..501f78e2054e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java @@ -19,7 +19,7 @@ import de.tum.cit.aet.artemis.buildagent.dto.BuildAgentInformation; import de.tum.cit.aet.artemis.core.repository.UserRepository; import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; -import de.tum.cit.aet.artemis.programming.repository.UserPublicSshKeyRepository; +import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; import de.tum.cit.aet.artemis.programming.service.localci.SharedQueueManagementService; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.SshConstants; @@ -34,19 +34,19 @@ public class GitPublickeyAuthenticatorService implements PublickeyAuthenticator private final Optional localCIBuildJobQueueService; - private final UserPublicSshKeyRepository userPublicSshKeyRepository; + private final UserSshPublicKeyRepository userSshPublicKeyRepository; public GitPublickeyAuthenticatorService(UserRepository userRepository, Optional localCIBuildJobQueueService, - UserPublicSshKeyRepository userPublicSshKeyRepository) { + UserSshPublicKeyRepository userSshPublicKeyRepository) { this.userRepository = userRepository; this.localCIBuildJobQueueService = localCIBuildJobQueueService; - this.userPublicSshKeyRepository = userPublicSshKeyRepository; + this.userSshPublicKeyRepository = userSshPublicKeyRepository; } @Override public boolean authenticate(String username, PublicKey publicKey, ServerSession session) { String keyHash = HashUtils.getSha512Fingerprint(publicKey); - var userSshPublicKey = userPublicSshKeyRepository.findByKeyHash(keyHash); + var userSshPublicKey = userSshPublicKeyRepository.findByKeyHash(keyHash); if (userSshPublicKey.isPresent()) { return authenticateUser(userSshPublicKey.get(), publicKey, session); } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java index bca0ed60beb9..a8ad2f40f8da 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java @@ -12,6 +12,7 @@ import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; +import java.time.ZonedDateTime; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -30,6 +31,8 @@ import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.User; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; +import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; import de.tum.cit.aet.artemis.programming.service.localvc.SshGitCommandFactoryService; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.SshGitCommand; @@ -42,6 +45,9 @@ class LocalVCSshIntegrationTest extends LocalVCIntegrationTest { @Autowired private SshServer sshServer; + @Autowired + private UserSshPublicKeyRepository userSshPublicKeyRepository; + @Override protected String getTestPrefix() { return TEST_PREFIX; @@ -200,6 +206,7 @@ private AbstractSession getCurrentServerSession(User user) { private KeyPair setupKeyPairAndAddToUser() throws GeneralSecurityException, IOException { User user = userTestRepository.getUser(); + userSshPublicKeyRepository.deleteAll(); KeyPair rsaKeyPair = generateKeyPair(); String sshPublicKey = writePublicKeyToString(rsaKeyPair.getPublic(), user.getLogin() + "@host"); @@ -209,10 +216,11 @@ private KeyPair setupKeyPairAndAddToUser() throws GeneralSecurityException, IOEx PublicKey publicKey = keyEntry.resolvePublicKey(null, null, null); String keyHash = HashUtils.getSha512Fingerprint(publicKey); - userTestRepository.updateUserSshPublicKeyHash(user.getId(), keyHash, sshPublicKey); - user = userTestRepository.getUser(); - - assertThat(user.getSshPublicKey()).isEqualTo(sshPublicKey); + var userPublicSshKey = createNewPublicKey(keyHash, sshPublicKey, user); + userSshPublicKeyRepository.save(userPublicSshKey); + var fetchedKey = userSshPublicKeyRepository.findAllByUserId(user.getId()); + assertThat(fetchedKey).isNotEmpty(); + assertThat(fetchedKey.getFirst().getPublicKey()).isEqualTo(sshPublicKey); return rsaKeyPair; } @@ -235,4 +243,15 @@ private static KeyPair generateKeyPair() { throw new RuntimeException(e); } } + + private static UserSshPublicKey createNewPublicKey(String keyHash, String publicKey, User user) { + UserSshPublicKey userSshPublicKey = new UserSshPublicKey(); + userSshPublicKey.setLabel("Key 1"); + userSshPublicKey.setPublicKey(publicKey); + userSshPublicKey.setKeyHash(keyHash); + userSshPublicKey.setUserId(user.getId()); + userSshPublicKey.setCreationDate(ZonedDateTime.now()); + + return userSshPublicKey; + } } From 4b3c30808eb0afc97fc1b4bce50b60465026cd07 Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 21 Oct 2024 18:15:44 +0200 Subject: [PATCH 10/47] finish UI --- .../aet/artemis/core/web/AccountResource.java | 6 +- .../service/UserSshPublicKeyService.java | 22 ++- .../programming/user-ssh-public-key.model.ts | 1 + ...h-user-settings-key-details.component.html | 157 +++++++++++++----- ...ssh-user-settings-key-details.component.ts | 28 +++- .../ssh-user-settings.component.html | 104 +++--------- .../ssh-user-settings.component.scss | 10 +- .../ssh-user-settings.component.ts | 8 + src/main/webapp/i18n/de/userSettings.json | 13 +- src/main/webapp/i18n/en/userSettings.json | 13 +- 10 files changed, 226 insertions(+), 136 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 030f77da982d..8586198a198d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -163,9 +163,9 @@ public ResponseEntity getSshPublicKey(@RequestParam("keyId") L } /** - * PUT account/ssh-public-key : sets the ssh public key + * PUT account/ssh-public-key : creates a new ssh public key for a user * - * @param sshPublicKey the ssh public key to set + * @param sshPublicKey the ssh public key to create * * @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request) */ @@ -189,7 +189,7 @@ public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKey sshPub } /** - * 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) */ diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index bb7d7f0b2aed..19ada44ce216 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -25,6 +25,8 @@ @Service public class UserSshPublicKeyService { + private final String KEY_DEFAULT_LABEL = "Key 1"; + private final UserSshPublicKeyRepository userSshPublicKeyRepository; public UserSshPublicKeyService(UserSshPublicKeyRepository userSshPublicKeyRepository) { @@ -36,20 +38,36 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP String keyHash = HashUtils.getSha512Fingerprint(publicKey); if (userSshPublicKeyRepository.findByKeyHash(keyHash).isPresent()) { - throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true); + throw new BadRequestAlertException("Key already exists", "SSH key", "keyAlreadyExists", true); } UserSshPublicKey newUserSshPublicKey = new UserSshPublicKey(); newUserSshPublicKey.setUserId(user.getId()); - newUserSshPublicKey.setLabel(sshPublicKey.getLabel()); newUserSshPublicKey.setPublicKey(sshPublicKey.getPublicKey()); newUserSshPublicKey.setKeyHash(keyHash); + setLabelForKey(newUserSshPublicKey, sshPublicKey.getLabel()); + newUserSshPublicKey.setExpiryDate(sshPublicKey.getExpiryDate()); newUserSshPublicKey.setCreationDate(ZonedDateTime.now()); newUserSshPublicKey.setExpiryDate(sshPublicKey.getExpiryDate()); userSshPublicKeyRepository.save(newUserSshPublicKey); } + public void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { + if (label == null || label.isEmpty()) { + String[] parts = newSshPublicKey.getPublicKey().split("\\s+"); + if (parts.length >= 3) { + newSshPublicKey.setLabel(parts[2]); + } + else { + newSshPublicKey.setLabel(KEY_DEFAULT_LABEL); + } + } + else { + newSshPublicKey.setLabel(label); + } + } + public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { var userSshPublicKey = userSshPublicKeyRepository.findByIdElseThrow(keyId); if (Objects.equals(userSshPublicKey.getUserId(), user.getId())) { diff --git a/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts b/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts index 52ec6b45d04a..4d53e9d2cef5 100644 --- a/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts +++ b/src/main/webapp/app/entities/programming/user-ssh-public-key.model.ts @@ -9,4 +9,5 @@ export class UserSshPublicKey implements BaseEntity { expiryDate?: dayjs.Dayjs; lastUsedDate?: dayjs.Dayjs; creationDate: dayjs.Dayjs; + hasExpired?: boolean; } diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html index a30300bf3189..ac5347aa3fce 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html @@ -1,64 +1,129 @@ -

- -

-@if (!isLoading) { +@if (isLoading) { +

+} @else { + @if (!isCreateMode) { +

+ } @else { +

+ } +
- +
- @if (!inCreateMode) { -

- } @else { -

- } -

- +

-
- @if (inCreateMode) { -

- } @else { -

{{ displayedKeyLabel }}

- } - -
- -
-

- - {{ copyInstructions }} -

+ @if (isCreateMode) { +
+

+
+ } @else { +
+

{{ displayedKeyLabel }}

+
+ } +
+
- @if (inCreateMode) { -
-

- -
- } + + @if (isCreateMode) { +
+

+ + {{ copyInstructions }} +

+
- +
+ +

+
+ +
+
+ +
+ + @if (selectedOption === 'useExpiration') { +
+

+ + @if (displayedExpiryDate) { +
+ + + {{ displayedExpiryDate | artemisDate: 'long-date' }} + +
+ } +
+ } + } @else { + + @if (displayCreationDate) { +
+
+
+ {{ displayCreationDate | artemisDate: 'long-date' }} +
+
+ } + @if (displayedLastUsedDate) { +
+
+
+ {{ displayedLastUsedDate | artemisDate: 'long-date' }} +
+
+ } + @if (displayedExpiryDate) { +
+
+
+ {{ displayedExpiryDate | artemisDate: 'long-date' }} +
+
+ } } - --> -
- @if (inCreateMode) { +
+ @if (isCreateMode) {

diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts index 1f85a52aa0d6..c6ec490d6ec1 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -25,10 +25,11 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { sshPublicKey: UserSshPublicKey; // state change variables - inCreateMode = false; // true when editing existing key, false when creating new key + isCreateMode = false; // true when editing existing key, false when creating new key isLoading = true; copyInstructions = ''; + selectedOption: string = 'doNotUseExpiration'; // Key details from input fields displayedKeyId?: number = undefined; // undefined when creating a new key @@ -37,6 +38,12 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { displayedKeyHash = ''; displayedExpiryDate?: dayjs.Dayjs; displayCreationDate: dayjs.Dayjs; + displayedLastUsedDate?: dayjs.Dayjs; + daysUntilExpiry?: number; + minDays = 1; + maxDays = 13337; + + currentDate: dayjs.Dayjs; readonly faEdit = faEdit; readonly faSave = faSave; @@ -54,17 +61,18 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { ngOnInit() { this.setMessageBasedOnOS(getOS()); + this.currentDate = dayjs(); this.subscription = this.route.params .pipe( filter((params) => { const keyId = Number(params['keyId']); if (keyId) { - this.inCreateMode = false; + this.isCreateMode = false; return true; } else { this.isLoading = false; - this.inCreateMode = true; + this.isCreateMode = true; return false; } }), @@ -76,6 +84,8 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { this.displayedKeyLabel = publicKey.label; this.displayedKeyHash = publicKey.keyHash; this.displayCreationDate = publicKey.creationDate; + this.displayedExpiryDate = publicKey.expiryDate; + this.displayedLastUsedDate = publicKey.lastUsedDate; this.isLoading = false; }), ) @@ -86,6 +96,14 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { this.subscription.unsubscribe(); } + sendTheNewValue() { + if (this.daysUntilExpiry) { + this.displayedExpiryDate = this.currentDate.add(this.daysUntilExpiry, 'day'); + } else { + this.displayedExpiryDate = undefined; + } + } + saveSshKey() { const newUserSshKey = { label: this.displayedKeyLabel, @@ -109,7 +127,7 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { } goBack() { - if (this.inCreateMode) { + if (this.isCreateMode) { this.router.navigate(['../'], { relativeTo: this.route }); } else { this.router.navigate(['../../'], { relativeTo: this.route }); @@ -117,7 +135,7 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { } editExistingSshKey(key: UserSshPublicKey) { - this.inCreateMode = false; + this.isCreateMode = false; this.displayedKeyId = key.id; this.displayedSshKey = key.publicKey; this.displayedKeyLabel = key.label; diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html index 795dff8ce150..be0a76985b30 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html @@ -1,6 +1,6 @@ -

- -

+ +

+ @if (!isLoading && localVCEnabled) {
@@ -27,14 +27,6 @@

-
-

-
- - - -
-

@@ -54,18 +46,28 @@

-
+
{{ key.label }}
{{ key.keyHash }}
-
-
-
- {{ key.creationDate | artemisDate: 'long-date' }} + @if (key.expiryDate && key.hasExpired) { +
+
+
+ {{ key.expiryDate | artemisDate: 'long-date' }} +
-
+ } + @if (key.expiryDate && !key.hasExpired) { +
+
+
+ {{ key.expiryDate | artemisDate: 'long-date' }} +
+
+ } @@ -102,71 +104,19 @@

- - } +
+
+
+ + + +
+
}

diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss index 44105fcef90c..d8d61db460ff 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss @@ -2,7 +2,7 @@ textarea { max-width: 600px; width: 100%; height: 150px; - color: red; + resize: none; font-size: small; font-family: Bitstream Vera Sans Mono, @@ -15,6 +15,10 @@ textarea { font-weight: bold; } +.large-text { + font-size: large; +} + .text-and-date { display: flex; gap: 5px; @@ -24,6 +28,10 @@ textarea { height: 30px; } +.days-input-filed { + max-width: 150px; +} + table { width: 100%; border-collapse: collapse; diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts index 403297d43a9b..7943de14d8ed 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts @@ -9,6 +9,7 @@ import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; import { AlertService } from 'app/core/util/alert.service'; import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; +import dayjs from 'dayjs/esm'; @Component({ selector: 'jhi-account-information', @@ -33,6 +34,7 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { private accountServiceSubscription: Subscription; private dialogErrorSource = new Subject(); + currentDate: dayjs.Dayjs; dialogError$ = this.dialogErrorSource.asObservable(); @ViewChild('itemsDrop', { static: true }) itemsDrop: NgbDropdown; @@ -44,6 +46,7 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { ) {} ngOnInit() { + this.currentDate = dayjs(); this.profileService.getProfileInfo().subscribe((profileInfo) => { this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); }); @@ -52,6 +55,11 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { .pipe( tap((publicKeys: UserSshPublicKey[]) => { this.sshPublicKeys = publicKeys; + this.sshPublicKeys.forEach((key) => { + if (key.expiryDate && dayjs().isAfter(dayjs(key.expiryDate))) { + key.hasExpired = true; + } + }); this.keyCount = publicKeys.length; this.isLoading = false; }), diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index 58baba0c4358..c89519694d2b 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -51,8 +51,19 @@ "lastUsed": "Letzte Verwendung", "lastUsedOn": "Letzte Verwendung am", "expiryDate": "Ablaufdatum", + "expiresOn": "Läuft ab am", + "hasExpiredOn": "Abgelaufen am", "leaveEmpty": "Wenn du das Label leer lässt, wird der Kommentar aus dem SSH-Schlüssel verwendet.", - "fingerprint": "Fingerabdruck" + "fingerprint": "Fingerabdruck", + "commentUsedAsLabel": "Wenn du kein Label hinzufügst, wird der Schlüsselkommentar (sofern vorhanden) als Label verwendet.", + "expiry": { + "title": "Ablauf", + "info": "Für zusätzliche Sicherheit kannst du festlegen, dass dieser Schlüssel automatisch abläuft.", + "doNotExpire": "Kein Abluafsdatum", + "expireAutomatically": "Automatisch ablaufen", + "daysUntilExpiry": "Tage bis zum Ablauf", + "keyWillExpireOn": "Der Schlüssel läuft ab am" + } }, "vcsAccessTokensSettingsFingerabdruckPage": { "addTokenTitle": "Neues Zugriffstoken erzeugen", diff --git a/src/main/webapp/i18n/en/userSettings.json b/src/main/webapp/i18n/en/userSettings.json index 6a6e926f3b0c..579fd1fc39d0 100644 --- a/src/main/webapp/i18n/en/userSettings.json +++ b/src/main/webapp/i18n/en/userSettings.json @@ -51,8 +51,19 @@ "lastUsed": "Last used", "lastUsedOn": "Last used on", "expiryDate": "Expiry date", + "expiresOn": "Expires on", + "hasExpiredOn": "Expired on", "leaveEmpty": "If you leave the label empty, the comment from the SSH key will be used.", - "fingerprint": "Fingerprint" + "fingerprint": "Fingerprint", + "commentUsedAsLabel": "If you do not add a label, the key comment will be used as the default label if present.", + "expiry": { + "title": "Expiry", + "info": "For added security, you can set this key to automatically expire.", + "doNotExpire": "Do not expire", + "expireAutomatically": "Expire automatically", + "daysUntilExpiry": "Days until expiry", + "keyWillExpireOn": "Key will expire on" + } }, "vcsAccessTokensSettingsPage": { "addTokenTitle": "Add personal access token", From 9616e2dca19d04fb5976fd603237c6dce4828566 Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 21 Oct 2024 18:22:06 +0200 Subject: [PATCH 11/47] added javadocs --- .../service/UserSshPublicKeyService.java | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 19ada44ce216..3c8178df8fb4 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -25,7 +25,7 @@ @Service public class UserSshPublicKeyService { - private final String KEY_DEFAULT_LABEL = "Key 1"; + private static final String KEY_DEFAULT_LABEL = "Key 1"; private final UserSshPublicKeyRepository userSshPublicKeyRepository; @@ -33,6 +33,14 @@ public UserSshPublicKeyService(UserSshPublicKeyRepository userSshPublicKeyReposi this.userSshPublicKeyRepository = userSshPublicKeyRepository; } + /** + * Creates a new SSH public key for the specified user, ensuring that the key is unique + * based on its SHA-512 hash fingerprint. If the key already exists, an exception is thrown. + * + * @param user the {@link User} for whom the SSH key is being created. + * @param keyEntry the {@link AuthorizedKeyEntry} containing the SSH public key details, used to resolve the {@link PublicKey}. + * @param sshPublicKey the {@link UserSshPublicKey} object containing metadata about the SSH key such as the key itself, label, and expiry date. + */ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshPublicKey sshPublicKey) throws GeneralSecurityException, IOException { PublicKey publicKey = keyEntry.resolvePublicKey(null, null, null); String keyHash = HashUtils.getSha512Fingerprint(publicKey); @@ -53,6 +61,13 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP userSshPublicKeyRepository.save(newUserSshPublicKey); } + /** + * Sets the label for the provided SSH public key. If the given label is null or empty, + * the label is extracted from the public key or defaults to a predefined value. + * + * @param newSshPublicKey the {@link UserSshPublicKey} for which the label is being set. + * @param label the label to assign to the SSH key, or null/empty to use the default logic. + */ public void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { if (label == null || label.isEmpty()) { String[] parts = newSshPublicKey.getPublicKey().split("\\s+"); @@ -68,6 +83,14 @@ public void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { } } + /** + * Retrieves the SSH public key for the specified user by key ID. + * + * @param user the {@link User} to whom the SSH key belongs. + * @param keyId the ID of the SSH key. + * @return the {@link UserSshPublicKey} if found and belongs to the user. + * @throws EntityNotFoundException if the key does not belong to the user. + */ public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { var userSshPublicKey = userSshPublicKeyRepository.findByIdElseThrow(keyId); if (Objects.equals(userSshPublicKey.getUserId(), user.getId())) { @@ -78,10 +101,23 @@ public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { } } + /** + * Retrieves all SSH public keys associated with the specified user. + * + * @param user the {@link User} whose SSH keys are to be retrieved. + * @return a list of {@link UserSshPublicKey} objects for the user. + */ public List getAllSshKeysForUser(User user) { return userSshPublicKeyRepository.findAllByUserId(user.getId()); } + /** + * Deletes the specified SSH public key for the given user ID. + * + * @param userId the ID of the user. + * @param keyId the ID of the SSH key to delete. + * @throws AccessForbiddenException if the key does not belong to the user. + */ public void deleteUserSshPublicKey(Long userId, Long keyId) { var keys = userSshPublicKeyRepository.findAllByUserId(userId); if (!keys.isEmpty() && keys.stream().map(UserSshPublicKey::getId).toList().contains(keyId)) { From 72e6ec9ab83a2267e03ec12f9107757dc57952a4 Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 21 Oct 2024 18:42:13 +0200 Subject: [PATCH 12/47] more docs --- .../GitPublickeyAuthenticatorService.java | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java index 501f78e2054e..6795e4aa3658 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.security.GeneralSecurityException; import java.security.PublicKey; +import java.time.ZonedDateTime; import java.util.Objects; import java.util.Optional; @@ -47,14 +48,24 @@ public GitPublickeyAuthenticatorService(UserRepository userRepository, Optional< public boolean authenticate(String username, PublicKey publicKey, ServerSession session) { String keyHash = HashUtils.getSha512Fingerprint(publicKey); var userSshPublicKey = userSshPublicKeyRepository.findByKeyHash(keyHash); - if (userSshPublicKey.isPresent()) { - return authenticateUser(userSshPublicKey.get(), publicKey, session); - } - else { - return authenticateBuildAgent(publicKey, session); - } + return userSshPublicKey.map(sshPublicKey -> { + ZonedDateTime expiryDate = sshPublicKey.getExpiryDate(); + if (expiryDate == null || expiryDate.isAfter(ZonedDateTime.now())) { + return authenticateUser(sshPublicKey, publicKey, session); + } + return false; + }).orElseGet(() -> authenticateBuildAgent(publicKey, session)); } + /** + * Tries to authenticate a user by the provided key + * + * @param storedKey The key stored in the Artemis database + * @param providedKey The key provided by the user for authentication + * @param session The SSH server session + * + * @return true if the authentication succeeds, and false if it doesn't + */ public boolean authenticateUser(UserSshPublicKey storedKey, PublicKey providedKey, ServerSession session) { try { var user = userRepository.findById(storedKey.getUserId()); @@ -82,6 +93,14 @@ public boolean authenticateUser(UserSshPublicKey storedKey, PublicKey providedKe return false; } + /** + * Tries to authenticate a build agent by the provided key + * + * @param providedKey The key provided by the user for authentication + * @param session The SSH server session + * + * @return true if the authentication succeeds, and false if it doesn't + */ private boolean authenticateBuildAgent(PublicKey providedKey, ServerSession session) { if (localCIBuildJobQueueService.isPresent() && localCIBuildJobQueueService.get().getBuildAgentInformation().stream().anyMatch(agent -> checkPublicKeyMatchesBuildAgentPublicKey(agent, providedKey))) { @@ -93,6 +112,14 @@ private boolean authenticateBuildAgent(PublicKey providedKey, ServerSession sess return false; } + /** + * Checks whether a provided key matches the build agents public key + * + * @param agent The build agent which tires to be authenticated by Artemis + * @param publicKey The provided public key + * + * @return true if the build agents has this public key, and false if it doesn't + */ private boolean checkPublicKeyMatchesBuildAgentPublicKey(BuildAgentInformation agent, PublicKey publicKey) { if (agent.publicSshKey() == null) { return false; From 06d87ff4734959348c0545f10f5b1a2f37ae9bf6 Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 21 Oct 2024 19:57:50 +0200 Subject: [PATCH 13/47] server tests --- .../UserAccountLocalVcsIntegrationTest.java | 26 +++- .../core/user/util/UserTestService.java | 113 ++++++++++++++++-- .../resources/config/application-artemis.yml | 2 +- src/test/resources/config/application.yml | 3 + 4 files changed, 132 insertions(+), 12 deletions(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java index a3313ce7c592..f3cdb4f3dca2 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java @@ -28,7 +28,31 @@ void teardown() throws Exception { @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void addAndDeleteSshPublicKeyByUser() throws Exception { + void addSshPublicKeyForUser() throws Exception { + userTestService.addUserSshPublicKey(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void failToAddSameSshPublicKeyTwiceForUser() throws Exception { + userTestService.failToAddPublicSSHkeyTwice(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void failToAddInvalidSshPublicKeyForUser() throws Exception { + userTestService.failToAddInvalidPublicSSHkey(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void getSshPublicKeysByUser() throws Exception { + // TODO userTestService.(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void deleteSshPublicKeyByUser() throws Exception { userTestService.addAndDeleteSshPublicKey(); } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index 77ea61f417ee..2164f6536bf9 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -27,6 +27,9 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.util.LinkedMultiValueMap; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; + import de.tum.cit.aet.artemis.atlas.domain.science.ScienceEvent; import de.tum.cit.aet.artemis.atlas.domain.science.ScienceEventType; import de.tum.cit.aet.artemis.atlas.test_repository.ScienceEventTestRepository; @@ -51,7 +54,9 @@ import de.tum.cit.aet.artemis.exercise.test_repository.SubmissionTestRepository; import de.tum.cit.aet.artemis.lti.service.LtiService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; +import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.programming.repository.ParticipationVCSAccessTokenRepository; +import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; import de.tum.cit.aet.artemis.programming.service.ci.CIUserManagementService; import de.tum.cit.aet.artemis.programming.service.vcs.VcsUserManagementService; import de.tum.cit.aet.artemis.programming.util.MockDelegate; @@ -100,6 +105,9 @@ public class UserTestService { @Autowired private ScienceEventTestRepository scienceEventRepository; + @Autowired + private UserSshPublicKeyRepository userSshPublicKeyRepository; + @Autowired private MockMvc mockMvc; @@ -865,23 +873,99 @@ public void initializeUserExternal() throws Exception { assertThat(currentUser.isInternal()).isFalse(); } + public void getUserSshPublicKeys() throws Exception { + userSshPublicKeyRepository.deleteAll(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + User user = userTestRepository.getUser(); + var validKey = postNewKeyToServer(); + + } + + public void getUserSshPublicKey() throws Exception { + userSshPublicKeyRepository.deleteAll(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + User user = userTestRepository.getUser(); + var validKey = postNewKeyToServer(); + + } + + // Test + public void addUserSshPublicKey() throws Exception { + userSshPublicKeyRepository.deleteAll(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + User user = userTestRepository.getUser(); + var validKey = postNewKeyToServer(); + + var storedUserKey = userSshPublicKeyRepository.findAllByUserId(user.getId()).getFirst(); + assertThat(storedUserKey).isNotNull(); + assertThat(storedUserKey.getPublicKey()).isEqualTo(validKey.getPublicKey()); + } + + private UserSshPublicKey postNewKeyToServer() throws Exception { + User user = userTestRepository.getUser(); + var validKey = createNewValidSSHKey(user); + + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String json = ow.writeValueAsString(validKey); + + request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.OK, true); + return validKey; + } + + // Test + public void failToAddPublicSSHkeyTwice() throws Exception { + userSshPublicKeyRepository.deleteAll(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + User user = userTestRepository.getUser(); + + var validKey = createNewValidSSHKey(user); + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String json = ow.writeValueAsString(validKey); + + request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.OK, true); + request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.BAD_REQUEST, true); + } + + // Test + public void failToAddInvalidPublicSSHkey() throws Exception { + userSshPublicKeyRepository.deleteAll(); + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + + User user = userTestRepository.getUser(); + var userKey = createNewValidSSHKey(user); + userKey.setPublicKey("Invalid Key"); + + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String json = ow.writeValueAsString(userKey); + + request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.BAD_REQUEST, true); + } + // Test public void addAndDeleteSshPublicKey() throws Exception { + userSshPublicKeyRepository.deleteAll(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); + User user = userTestRepository.getUser(); + + var validKey = createNewValidSSHKey(user); + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String json = ow.writeValueAsString(validKey); - // adding invalid key should fail - String invalidSshKey = "invalid key"; - request.putWithResponseBody("/api/account/ssh-public-key", invalidSshKey, String.class, HttpStatus.BAD_REQUEST, true); + request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.OK, true); - // adding valid key should work correctly - String validSshKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbgjoSpKnry5yuMiWh/uwhMG2Jq5Sh8Uw9vz+39or2i email@abc.de"; - request.putWithResponseBody("/api/account/ssh-public-key", validSshKey, String.class, HttpStatus.OK, true); - assertThat(userTestRepository.getUser().getSshPublicKey()).isEqualTo(validSshKey); + var storedUserKey = userSshPublicKeyRepository.findAllByUserId(user.getId()).getFirst(); + assertThat(storedUserKey).isNotNull(); + assertThat(storedUserKey.getPublicKey()).isEqualTo(validKey.getPublicKey()); - // deleting the key shoul work correctly - request.delete("/api/account/ssh-public-key", HttpStatus.OK); - assertThat(userTestRepository.getUser().getSshPublicKey()).isEqualTo(null); + // deleting the key should work correctly + request.delete("/api/account/ssh-public-key?keyId=" + storedUserKey.getId(), HttpStatus.OK); + assertThat(userSshPublicKeyRepository.findAllByUserId(user.getId())).isEmpty(); } // Test @@ -1126,6 +1210,15 @@ public void testUserWithExternalStatus() throws Exception { } } + public static UserSshPublicKey createNewValidSSHKey(User user) { + UserSshPublicKey userSshPublicKey = new UserSshPublicKey(); + String validKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbgjoSpKnry5yuMiWh/uwhMG2Jq5Sh8Uw9vz+39or2i email@abc.de"; + userSshPublicKey.setPublicKey(validKey); + userSshPublicKey.setLabel("Key 1"); + userSshPublicKey.setUserId(user.getId()); + return userSshPublicKey; + } + // Test public void testUserWithExternalAndInternalStatus() throws Exception { final var params = createParamsForPagingRequest("USER", "INTERNAL,EXTERNAL", "WITHOUT_REG_NO", "", false); diff --git a/src/test/resources/config/application-artemis.yml b/src/test/resources/config/application-artemis.yml index c43ee1c7f401..bb921732c292 100644 --- a/src/test/resources/config/application-artemis.yml +++ b/src/test/resources/config/application-artemis.yml @@ -64,7 +64,7 @@ artemis: apollon: conversion-service-url: http://localhost:8080 telemetry: - enabled: true + enabled: false sendAdminDetails: true destination: http://localhost:8081 plagiarism-checks: diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index 16c56a619dd9..cc4431703e2e 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -74,6 +74,8 @@ artemis: default: "~~invalid~~" c_plus_plus: default: "~~invalid~~" + telemetry: + enabled: false # setting this to false will disable sending any information to the telemetry service spring: application: @@ -300,3 +302,4 @@ eureka: enabled: false service-url: defaultZone: http://admin:admin@localhost:8761/eureka/ + From cbec60eed25ff7340399177af30a411ec6720e0c Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 09:04:47 +0200 Subject: [PATCH 14/47] fix server style --- .../java/de/tum/cit/aet/artemis/core/web/AccountResource.java | 4 ++++ .../artemis/programming/service/UserSshPublicKeyService.java | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 8586198a198d..db263b2448ad 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -152,6 +152,8 @@ public ResponseEntity> getSshPublicKey() { /** * GET account/ssh-public-key : sets the ssh public key * + * @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 400 (Bad Request) */ @GetMapping("account/ssh-public-key") @@ -191,6 +193,8 @@ public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKey sshPub /** * Delete - account/ssh-public-key : deletes the ssh public key by its keyId * + * @param keyId The id of the key that should be deleted + * * @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request) */ @DeleteMapping("account/ssh-public-key") diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 3c8178df8fb4..6f5df49d52c3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -6,6 +6,7 @@ import java.security.GeneralSecurityException; import java.security.PublicKey; import java.time.ZonedDateTime; +import java.util.Arrays; import java.util.List; import java.util.Objects; @@ -72,7 +73,8 @@ public void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { if (label == null || label.isEmpty()) { String[] parts = newSshPublicKey.getPublicKey().split("\\s+"); if (parts.length >= 3) { - newSshPublicKey.setLabel(parts[2]); + String labelFromParts = String.join(" ", Arrays.copyOfRange(parts, 2, parts.length)); + newSshPublicKey.setLabel(labelFromParts); } else { newSshPublicKey.setLabel(KEY_DEFAULT_LABEL); From f04d01e48bfeda1ad980a440e8ddaf208e7644d0 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 11:54:03 +0200 Subject: [PATCH 15/47] moved deleteall --- .../cit/aet/artemis/core/user/util/UserTestService.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index 2164f6536bf9..cd2572f3edc6 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -139,7 +139,6 @@ public class UserTestService { public void setup(String testPrefix, MockDelegate mockDelegate) throws Exception { this.TEST_PREFIX = testPrefix; this.mockDelegate = mockDelegate; - List users = userUtilService.addUsers(testPrefix, NUMBER_OF_STUDENTS, NUMBER_OF_TUTORS, NUMBER_OF_EDITORS, NUMBER_OF_INSTRUCTORS); student = userTestRepository.getUserByLoginElseThrow(testPrefix + "student1"); student.setInternal(true); @@ -157,6 +156,8 @@ public void setup(String testPrefix, MockDelegate mockDelegate) throws Exception public void tearDown() throws IOException { userTestRepository.deleteAll(userTestRepository.searchAllByLoginOrName(Pageable.unpaged(), TEST_PREFIX)); + this.userSshPublicKeyRepository.deleteAll(); + } public User getStudent() { @@ -874,7 +875,6 @@ public void initializeUserExternal() throws Exception { } public void getUserSshPublicKeys() throws Exception { - userSshPublicKeyRepository.deleteAll(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); @@ -883,7 +883,6 @@ public void getUserSshPublicKeys() throws Exception { } public void getUserSshPublicKey() throws Exception { - userSshPublicKeyRepository.deleteAll(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); @@ -893,7 +892,6 @@ public void getUserSshPublicKey() throws Exception { // Test public void addUserSshPublicKey() throws Exception { - userSshPublicKeyRepository.deleteAll(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); @@ -917,7 +915,6 @@ private UserSshPublicKey postNewKeyToServer() throws Exception { // Test public void failToAddPublicSSHkeyTwice() throws Exception { - userSshPublicKeyRepository.deleteAll(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); @@ -932,7 +929,6 @@ public void failToAddPublicSSHkeyTwice() throws Exception { // Test public void failToAddInvalidPublicSSHkey() throws Exception { - userSshPublicKeyRepository.deleteAll(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); @@ -948,7 +944,6 @@ public void failToAddInvalidPublicSSHkey() throws Exception { // Test public void addAndDeleteSshPublicKey() throws Exception { - userSshPublicKeyRepository.deleteAll(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); From a61e36002bb994c7788ae28166cdbcefa1f43f71 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 13:46:59 +0200 Subject: [PATCH 16/47] add code rabbit suggestions --- .../programming/domain/UserSshPublicKey.java | 5 +-- .../UserSshPublicKeyRepository.java | 2 ++ .../service/UserSshPublicKeyService.java | 7 ++-- .../GitPublickeyAuthenticatorService.java | 2 +- ...h-user-settings-key-details.component.html | 2 +- .../ssh-user-settings.component.html | 8 ++--- .../ssh-user-settings.component.scss | 2 +- .../ssh-user-settings.component.ts | 35 ++++++++++++------- src/main/webapp/i18n/de/userSettings.json | 2 +- src/main/webapp/i18n/en/userSettings.json | 2 +- 10 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java index a4ef2bcdddb1..f7f2619a83fd 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java @@ -39,6 +39,7 @@ public class UserSshPublicKey extends DomainObject { /** * 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; @@ -51,13 +52,13 @@ public class UserSshPublicKey extends DomainObject { private String keyHash; /** - * The expiry date of the public SSH key + * The creation date of the public SSH key */ @Column(name = "creation_date") private ZonedDateTime creationDate = null; /** - * The expiry date of the public SSH key + * The last used date of the public SSH key */ @Nullable @Column(name = "last_used_date") diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java index 8602e39862a0..c0aa690916cb 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java @@ -18,4 +18,6 @@ public interface UserSshPublicKeyRepository extends ArtemisJpaRepository findAllByUserId(Long userId); Optional findByKeyHash(String keyHash); + + boolean existsByIdAndUserId(Long id, Long userId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 6f5df49d52c3..6d4e10f52896 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -55,8 +55,6 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP newUserSshPublicKey.setPublicKey(sshPublicKey.getPublicKey()); newUserSshPublicKey.setKeyHash(keyHash); setLabelForKey(newUserSshPublicKey, sshPublicKey.getLabel()); - - newUserSshPublicKey.setExpiryDate(sshPublicKey.getExpiryDate()); newUserSshPublicKey.setCreationDate(ZonedDateTime.now()); newUserSshPublicKey.setExpiryDate(sshPublicKey.getExpiryDate()); userSshPublicKeyRepository.save(newUserSshPublicKey); @@ -99,7 +97,7 @@ public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { return userSshPublicKey; } else { - throw new EntityNotFoundException(); + throw new AccessForbiddenException("SSH key", keyId); } } @@ -121,8 +119,7 @@ public List getAllSshKeysForUser(User user) { * @throws AccessForbiddenException if the key does not belong to the user. */ public void deleteUserSshPublicKey(Long userId, Long keyId) { - var keys = userSshPublicKeyRepository.findAllByUserId(userId); - if (!keys.isEmpty() && keys.stream().map(UserSshPublicKey::getId).toList().contains(keyId)) { + if (userSshPublicKeyRepository.existsByIdAndUserId(keyId, userId)) { userSshPublicKeyRepository.deleteById(keyId); } else { diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java index 6795e4aa3658..ea2f735dad05 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/GitPublickeyAuthenticatorService.java @@ -66,7 +66,7 @@ public boolean authenticate(String username, PublicKey publicKey, ServerSession * * @return true if the authentication succeeds, and false if it doesn't */ - public boolean authenticateUser(UserSshPublicKey storedKey, PublicKey providedKey, ServerSession session) { + private boolean authenticateUser(UserSshPublicKey storedKey, PublicKey providedKey, ServerSession session) { try { var user = userRepository.findById(storedKey.getUserId()); if (user.isEmpty()) { diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html index ac5347aa3fce..bc9ea527508d 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html @@ -72,7 +72,7 @@

type="number" [min]="minDays" [max]="maxDays" - class="form-control small-text-area days-input-filed" + class="form-control small-text-area days-input-field" [(ngModel)]="daysUntilExpiry" (input)="sendTheNewValue()" /> diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html index be0a76985b30..e5ce807d1f72 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html @@ -71,18 +71,18 @@

-

- +
} diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss index d8d61db460ff..435b70e55cc8 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss @@ -28,7 +28,7 @@ textarea { height: 30px; } -.days-input-filed { +.days-input-field { max-width: 150px; } diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts index 7943de14d8ed..e64c52572a0a 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts @@ -55,16 +55,21 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { .pipe( tap((publicKeys: UserSshPublicKey[]) => { this.sshPublicKeys = publicKeys; - this.sshPublicKeys.forEach((key) => { - if (key.expiryDate && dayjs().isAfter(dayjs(key.expiryDate))) { - key.hasExpired = true; - } - }); + + this.sshPublicKeys = this.sshPublicKeys.map((key) => ({ + ...key, + hasExpired: key.expiryDate && dayjs().isAfter(dayjs(key.expiryDate)), + })); this.keyCount = publicKeys.length; this.isLoading = false; }), ) - .subscribe(); + .subscribe({ + error: () => { + this.isLoading = false; + this.alertService.error('artemisApp.userSettings.sshSettingsPage.loadKeyFailure'); + }, + }); } ngOnDestroy() { @@ -75,17 +80,21 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { this.accountService.deleteSshPublicKey(key.id).subscribe({ next: () => { this.alertService.success('artemisApp.userSettings.sshSettingsPage.deleteSuccess'); - this.keyCount = this.keyCount - 1; - const index = this.sshPublicKeys.indexOf(key); - if (index >= 0) { - this.sshPublicKeys.splice(index, 1); - } + this.refreshSshKeys(); }, - error: (error) => { - console.log(error); + error: () => { this.alertService.error('artemisApp.userSettings.sshSettingsPage.deleteFailure'); }, }); this.dialogErrorSource.next(''); } + + private refreshSshKeys() { + this.isLoading = true; + this.accountService.getAllSshPublicKeys().subscribe((publicKeys: UserSshPublicKey[]) => { + this.sshPublicKeys = publicKeys; + this.keyCount = publicKeys.length; + this.isLoading = false; + }); + } } diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index c89519694d2b..47cfe82fdd70 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -24,6 +24,7 @@ "saveSshKey": "Speichern", "invalidKeyFormat": "SSH-Schlüssel konnte nicht gespeichert werden. Stelle sicher, dass es ein gültiger und unterstützter Schlüssel im richtigen Format ist.", "keyAlreadyExists": "SSH-Schlüssel konnte nicht gespeichert werden. Du verwendest diesen Schlüssel schon.", + "loadKeyFailure": "Laden der SSH-Schlüssel ist fehlgeschlagen.", "saveFailure": "SSH-Schlüssel konnte nicht gespeichert werden.", "saveSuccess": "SSH-Schlüssel erfolgreich gespeichert.", "deleteFailure": "SSH-Schlüssel konnte nicht gelöscht werden.", @@ -44,7 +45,6 @@ "keysTablePageTitle": "SSH-Schlüssel", "keys": "Schlüssel", "actions": "Aktionen", - "keyName": "Key 1 (Aktuell wird nur ein Schlüssel unterstützt)", "label": "Label", "creationDate": "Erstellungsdatum", "createdOn": "Erstellt am", diff --git a/src/main/webapp/i18n/en/userSettings.json b/src/main/webapp/i18n/en/userSettings.json index 579fd1fc39d0..3e1dd90516f5 100644 --- a/src/main/webapp/i18n/en/userSettings.json +++ b/src/main/webapp/i18n/en/userSettings.json @@ -24,6 +24,7 @@ "saveSshKey": "Save", "invalidKeyFormat": "Failed to save SSH key. Make sure the key is in a valid and supported format.", "keyAlreadyExists": "Failed to save SSH key. You already use the provided key.", + "loadKeyFailure": "Failed to load SSH keys.", "saveFailure": "Failed to save SSH key.", "saveSuccess": "Successfully save SSH key.", "deleteFailure": "Failed to delete SSH key.", @@ -44,7 +45,6 @@ "keysTablePageTitle": "SSH keys", "keys": "Keys", "actions": "Actions", - "keyName": "Key 1 (at the moment you can only have one key)", "label": "Label", "creationDate": "Creation date:", "createdOn": "Created on", From 6a704e25b44c67ab9d5f9e4a72356af027ae9955 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 14:40:57 +0200 Subject: [PATCH 17/47] let code button detect if a user has ssh keys or not --- .../aet/artemis/core/web/AccountResource.java | 13 ++++++ .../UserSshPublicKeyRepository.java | 2 + .../service/UserSshPublicKeyService.java | 10 +++++ .../webapp/app/core/auth/account.service.ts | 7 ++++ .../code-button/code-button.component.ts | 40 +++++++++++-------- 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index db263b2448ad..fb146038993f 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -164,6 +164,19 @@ public ResponseEntity getSshPublicKey(@RequestParam("keyId") L return ResponseEntity.ok(key); } + /** + * GET account/has-ssh-public-key : sets 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), or with status 400 (Bad Request) + */ + @GetMapping("account/has-ssh-public-keys") + @EnforceAtLeastStudent + public ResponseEntity hasUserSSHkeys() { + User user = userRepository.getUser(); + Boolean hasKey = userSshPublicKeyService.hasUserSSHkeys(user.getId()); + return ResponseEntity.ok(hasKey); + } + /** * PUT account/ssh-public-key : creates a new ssh public key for a user * diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java index c0aa690916cb..086b4b22b4ef 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java @@ -20,4 +20,6 @@ public interface UserSshPublicKeyRepository extends ArtemisJpaRepository findByKeyHash(String keyHash); boolean existsByIdAndUserId(Long id, Long userId); + + boolean existsByUserId(Long userId); } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 6d4e10f52896..78b6da383a4c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -126,4 +126,14 @@ public void deleteUserSshPublicKey(Long userId, Long keyId) { throw new AccessForbiddenException("SSH key", keyId); } } + + /** + * Returns whether the user of the specified id has stored SSH keys + * + * @param userId the ID of the user. + * @throws AccessForbiddenException if the key does not belong to the user. + */ + public Boolean hasUserSSHkeys(Long userId) { + return userSshPublicKeyRepository.existsByUserId(userId); + } } diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts index 01e5994dbadf..83785877416e 100644 --- a/src/main/webapp/app/core/auth/account.service.ts +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -341,6 +341,13 @@ export class AccountService implements IAccountService { return this.http.get('api/account/ssh-public-keys'); } + /** + * Checks if a user has SSH key stored in the database + */ + hasUserSshPublicKeys(): Observable { + return this.http.get('api/account/has-ssh-public-keys'); + } + /** * Retrieves a specific public SSH keys of a user */ diff --git a/src/main/webapp/app/shared/components/code-button/code-button.component.ts b/src/main/webapp/app/shared/components/code-button/code-button.component.ts index 028e831f68ce..71fabfa05810 100644 --- a/src/main/webapp/app/shared/components/code-button/code-button.component.ts +++ b/src/main/webapp/app/shared/components/code-button/code-button.component.ts @@ -56,6 +56,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { gitlabVCEnabled = false; showCloneUrlWithoutToken = true; copyEnabled? = true; + doesUserHavSSHkeys = false; sshKeyMissingTip: string; tokenMissingTip: string; @@ -86,27 +87,32 @@ export class CodeButtonComponent implements OnInit, OnChanges { private ideSettingsService: IdeSettingsService, ) {} - ngOnInit() { - this.accountService - .identity() - .then((user) => { - this.user = user!; - this.refreshTokenState(); + async ngOnInit() { + const user = await this.accountService.identity(); - this.copyEnabled = true; - this.useSsh = this.localStorage.retrieve('useSsh') || false; - this.useToken = this.localStorage.retrieve('useToken') || false; - this.localStorage.observe('useSsh').subscribe((useSsh) => (this.useSsh = useSsh || false)); - this.localStorage.observe('useToken').subscribe((useToken) => (this.useToken = useToken || false)); + this.user = user!; + this.refreshTokenState(); + + this.copyEnabled = true; + this.useSsh = this.localStorage.retrieve('useSsh') || false; + console.log(this.useSsh); + this.useToken = this.localStorage.retrieve('useToken') || false; + this.localStorage.observe('useSsh').subscribe((useSsh) => (this.useSsh = useSsh || false)); + this.localStorage.observe('useToken').subscribe((useToken) => (this.useToken = useToken || false)); + + if (this.useToken) { + this.useHttpsUrlWithToken(); + } + this.loadParticipationVcsAccessTokens(); + this.accountService.hasUserSshPublicKeys().subscribe({ + next: (res: boolean) => { + this.doesUserHavSSHkeys = res; if (this.useSsh) { this.useSshUrl(); } - if (this.useToken) { - this.useHttpsUrlWithToken(); - } - }) - .then(() => this.loadParticipationVcsAccessTokens()); + }, + }); // Get ssh information from the user this.profileService.getProfileInfo().subscribe((profileInfo) => { @@ -159,7 +165,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { public useSshUrl() { this.useSsh = true; this.useToken = false; - this.copyEnabled = true; // this.useSsh && (!!this.user.sshPublicKey || this.gitlabVCEnabled); + this.copyEnabled = this.doesUserHavSSHkeys || this.gitlabVCEnabled; this.storeToLocalStorage(); } From 11994ee4f17e058f293f888c3611ac6583513f07 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 16:25:47 +0200 Subject: [PATCH 18/47] improve Server test coverage --- .../service/UserSshPublicKeyService.java | 1 + .../UserAccountLocalVcsIntegrationTest.java | 18 +++++ .../core/user/util/UserTestService.java | 76 ++++++++++++++----- 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 78b6da383a4c..3a4685d563a0 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -131,6 +131,7 @@ public void deleteUserSshPublicKey(Long userId, Long keyId) { * Returns whether the user of the specified id has stored SSH keys * * @param userId the ID of the user. + * @return true if the user has SSH keys, false if not * @throws AccessForbiddenException if the key does not belong to the user. */ public Boolean hasUserSSHkeys(Long userId) { diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java index f3cdb4f3dca2..10fd59c51d66 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java @@ -26,18 +26,36 @@ void teardown() throws Exception { userTestService.tearDown(); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void getUserSshPublicKeys() throws Exception { + userTestService.getUserSshPublicKeys(); + } + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void addSshPublicKeyForUser() throws Exception { userTestService.addUserSshPublicKey(); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void addSshPublicKeyForUserWithoutLabel() throws Exception { + userTestService.addUserSshPublicKeyWithOutLabel(); + } + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void failToAddSameSshPublicKeyTwiceForUser() throws Exception { userTestService.failToAddPublicSSHkeyTwice(); } + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void failToAddOrDeleteSshPublicKeyWithInvalidKeyId() throws Exception { + userTestService.failToAddOrDeleteWithInvalidKeyId(); + } + @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void failToAddInvalidSshPublicKeyForUser() throws Exception { diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index cd2572f3edc6..b9979d436281 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -127,6 +127,10 @@ public class UserTestService { private static final int NUMBER_OF_INSTRUCTORS = 1; + private static final String sshKey1 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJxKWdvcbNTWl4vBjsijoY5HN5dpjxU40huy1PFpdd2o keyComment1 many comments"; + + private static final String sshKey2 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbgjoSpKnry5yuMiWh/uwhMG2Jq5Sh8Uw9vz+39or2i"; + @Autowired private ParticipationVCSAccessTokenRepository participationVCSAccessTokenRepository; @@ -878,16 +882,18 @@ public void getUserSshPublicKeys() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); - var validKey = postNewKeyToServer(); - } + var validKey = createNewValidSSHKey(user, sshKey1); + postNewValidKeyToServer(validKey); - public void getUserSshPublicKey() throws Exception { - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.TEXT_PLAIN); - User user = userTestRepository.getUser(); - var validKey = postNewKeyToServer(); + List response = request.getList("/api/account/ssh-public-keys", HttpStatus.OK, UserSshPublicKey.class); + System.out.println(response); + assertThat(response.size()).isEqualTo(1); + UserSshPublicKey userKey = response.getFirst(); + request.get("/api/account/ssh-public-key?keyId=" + userKey.getId(), HttpStatus.OK, UserSshPublicKey.class); + var hasSSHkeys = request.get("/api/account/has-ssh-public-keys", HttpStatus.OK, Boolean.class); + assertThat(hasSSHkeys).isTrue(); } // Test @@ -895,22 +901,37 @@ public void addUserSshPublicKey() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); - var validKey = postNewKeyToServer(); + + var validKey = createNewValidSSHKey(user, sshKey1); + postNewValidKeyToServer(validKey); var storedUserKey = userSshPublicKeyRepository.findAllByUserId(user.getId()).getFirst(); assertThat(storedUserKey).isNotNull(); assertThat(storedUserKey.getPublicKey()).isEqualTo(validKey.getPublicKey()); } - private UserSshPublicKey postNewKeyToServer() throws Exception { + // Test + public void addUserSshPublicKeyWithOutLabel() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); - var validKey = createNewValidSSHKey(user); - ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); - String json = ow.writeValueAsString(validKey); + var validKey = createNewValidSSHKey(user, sshKey1); + validKey.setLabel(null); + postNewValidKeyToServer(validKey); + + var validKey2 = createNewValidSSHKey(user, sshKey2); + validKey.setLabel(""); + postNewValidKeyToServer(validKey2); + var storedUserKeys = userSshPublicKeyRepository.findAllByUserId(user.getId()); + assertThat(storedUserKeys.size()).isEqualTo(2); + } + + private void postNewValidKeyToServer(UserSshPublicKey key) throws Exception { + ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); + String json = ow.writeValueAsString(key); request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.OK, true); - return validKey; } // Test @@ -919,7 +940,7 @@ public void failToAddPublicSSHkeyTwice() throws Exception { headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); - var validKey = createNewValidSSHKey(user); + var validKey = createNewValidSSHKey(user, sshKey1); ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); String json = ow.writeValueAsString(validKey); @@ -927,13 +948,31 @@ public void failToAddPublicSSHkeyTwice() throws Exception { request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.BAD_REQUEST, true); } + // Test + public void failToAddOrDeleteWithInvalidKeyId() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.TEXT_PLAIN); + User user = userTestRepository.getUser(); + + var validKey = createNewValidSSHKey(user, sshKey1); + postNewValidKeyToServer(validKey); + var userKey = userSshPublicKeyRepository.findAll().getFirst(); + userKey.setUserId(12L); + userSshPublicKeyRepository.save(userKey); + + request.delete("/api/account/ssh-public-key?keyId=3443", HttpStatus.FORBIDDEN); + request.get("/api/account/ssh-public-key?keyId=43443", HttpStatus.NOT_FOUND, UserSshPublicKey.class); + request.get("/api/account/ssh-public-key?keyId=" + userKey.getId(), HttpStatus.FORBIDDEN, UserSshPublicKey.class); + + } + // Test public void failToAddInvalidPublicSSHkey() throws Exception { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); - var userKey = createNewValidSSHKey(user); + var userKey = createNewValidSSHKey(user, sshKey1); userKey.setPublicKey("Invalid Key"); ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); @@ -948,7 +987,7 @@ public void addAndDeleteSshPublicKey() throws Exception { headers.setContentType(MediaType.TEXT_PLAIN); User user = userTestRepository.getUser(); - var validKey = createNewValidSSHKey(user); + var validKey = createNewValidSSHKey(user, sshKey1); ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); String json = ow.writeValueAsString(validKey); @@ -1205,10 +1244,9 @@ public void testUserWithExternalStatus() throws Exception { } } - public static UserSshPublicKey createNewValidSSHKey(User user) { + public static UserSshPublicKey createNewValidSSHKey(User user, String keyString) { UserSshPublicKey userSshPublicKey = new UserSshPublicKey(); - String validKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEbgjoSpKnry5yuMiWh/uwhMG2Jq5Sh8Uw9vz+39or2i email@abc.de"; - userSshPublicKey.setPublicKey(validKey); + userSshPublicKey.setPublicKey(keyString); userSshPublicKey.setLabel("Key 1"); userSshPublicKey.setUserId(user.getId()); return userSshPublicKey; From e0a01c4848a797ce3bffe9202f512881179140f4 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 20:15:08 +0200 Subject: [PATCH 19/47] added date picker --- ...h-user-settings-key-details.component.html | 36 ++++++++----------- ...ssh-user-settings-key-details.component.ts | 22 ++---------- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html index bc9ea527508d..1c28d0440a37 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html @@ -65,25 +65,21 @@

+ @if (selectedOption === 'useExpiration') {
-

- - @if (displayedExpiryDate) { -
- - - {{ displayedExpiryDate | artemisDate: 'long-date' }} - -
- } +
+ +
} } @else { @@ -118,11 +114,7 @@

Date: Tue, 22 Oct 2024 22:11:20 +0200 Subject: [PATCH 20/47] cleaned up client code and added tests --- ...ssh-user-settings-key-details.component.ts | 3 +- .../ssh-user-settings.component.html | 2 +- .../ssh-user-settings.component.ts | 58 +++---- ...ser-settings-key-details.component.spec.ts | 163 ++++++++++++++++++ .../ssh-user-settings.component.spec.ts | 152 +++++----------- 5 files changed, 227 insertions(+), 151 deletions(-) create mode 100644 src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts index 105cf209cc8d..32c7cebfc2e4 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -26,13 +26,12 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { // state change variables isCreateMode = false; // true when editing existing key, false when creating new key - isLoading = true; + copyInstructions = ''; selectedOption: string = 'doNotUseExpiration'; // Key details from input fields - displayedKeyId?: number = undefined; // undefined when creating a new key displayedKeyLabel = ''; displayedSshKey = ''; displayedKeyHash = ''; diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html index e5ce807d1f72..0bdc36730756 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html @@ -1,7 +1,7 @@

-@if (!isLoading && localVCEnabled) { +@if (!isLoading) {
@if (keyCount === 0) { diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts index e64c52572a0a..b1b525e80ac5 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts @@ -1,8 +1,6 @@ -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { AccountService } from 'app/core/auth/account.service'; -import { Subject, Subscription, tap } from 'rxjs'; -import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; -import { PROFILE_LOCALVC } from 'app/app.constants'; +import { Subject, tap } from 'rxjs'; import { faEdit, faEllipsis, faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; @@ -16,12 +14,10 @@ import dayjs from 'dayjs/esm'; templateUrl: './ssh-user-settings.component.html', styleUrls: ['../user-settings.scss', './ssh-user-settings.component.scss'], }) -export class SshUserSettingsComponent implements OnInit, OnDestroy { +export class SshUserSettingsComponent implements OnInit { readonly documentationType: DocumentationType = 'SshSetup'; sshPublicKeys: UserSshPublicKey[] = []; - localVCEnabled = false; - keyCount = 0; isLoading = true; @@ -32,7 +28,6 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { protected readonly ButtonType = ButtonType; protected readonly ButtonSize = ButtonSize; - private accountServiceSubscription: Subscription; private dialogErrorSource = new Subject(); currentDate: dayjs.Dayjs; dialogError$ = this.dialogErrorSource.asObservable(); @@ -41,16 +36,29 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { constructor( private accountService: AccountService, - private profileService: ProfileService, private alertService: AlertService, ) {} ngOnInit() { this.currentDate = dayjs(); - this.profileService.getProfileInfo().subscribe((profileInfo) => { - this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC); + this.refreshSshKeys(); + } + + deleteSshKey(key: UserSshPublicKey) { + this.accountService.deleteSshPublicKey(key.id).subscribe({ + next: () => { + this.alertService.success('artemisApp.userSettings.sshSettingsPage.deleteSuccess'); + this.refreshSshKeys(); + }, + error: () => { + this.alertService.error('artemisApp.userSettings.sshSettingsPage.deleteFailure'); + }, }); - this.accountServiceSubscription = this.accountService + this.dialogErrorSource.next(''); + } + + private refreshSshKeys() { + this.accountService .getAllSshPublicKeys() .pipe( tap((publicKeys: UserSshPublicKey[]) => { @@ -71,30 +79,4 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { }, }); } - - ngOnDestroy() { - this.accountServiceSubscription.unsubscribe(); - } - - deleteSshKey(key: UserSshPublicKey) { - this.accountService.deleteSshPublicKey(key.id).subscribe({ - next: () => { - this.alertService.success('artemisApp.userSettings.sshSettingsPage.deleteSuccess'); - this.refreshSshKeys(); - }, - error: () => { - this.alertService.error('artemisApp.userSettings.sshSettingsPage.deleteFailure'); - }, - }); - this.dialogErrorSource.next(''); - } - - private refreshSshKeys() { - this.isLoading = true; - this.accountService.getAllSshPublicKeys().subscribe((publicKeys: UserSshPublicKey[]) => { - this.sshPublicKeys = publicKeys; - this.keyCount = publicKeys.length; - this.isLoading = false; - }); - } } diff --git a/src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts b/src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts new file mode 100644 index 000000000000..a1a58b3509ef --- /dev/null +++ b/src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts @@ -0,0 +1,163 @@ +import { HttpErrorResponse } from '@angular/common/http'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AccountService } from 'app/core/auth/account.service'; +import { of, throwError } from 'rxjs'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ArtemisTestModule } from '../../test.module'; +import { MockNgbModalService } from '../../helpers/mocks/service/mock-ngb-modal.service'; +import { MockTranslateService, TranslatePipeMock } from '../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { SshUserSettingsKeyDetailsComponent } from 'app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component'; +import { MockActivatedRoute } from '../../helpers/mocks/activated-route/mock-activated-route'; +import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; +import dayjs from 'dayjs'; +import { AlertService } from 'app/core/util/alert.service'; + +describe('SshUserSettingsComponent', () => { + let fixture: ComponentFixture; + let comp: SshUserSettingsKeyDetailsComponent; + const mockKey = 'mock-key'; + const invalidKeyFormat = 'invalidKeyFormat'; + const keyAlreadyExists = 'keyAlreadyExists'; + const mockedUserSshKeys = { + id: 3, + publicKey: mockKey, + label: 'Key label', + keyHash: 'Key hash', + } as UserSshPublicKey; + let router: Router; + let accountServiceMock: { + getSshPublicKey: jest.Mock; + addSshPublicKey: jest.Mock; + addNewSshPublicKey: jest.Mock; + }; + let alertServiceMock: { + error: jest.Mock; + success: jest.Mock; + }; + let translateService: TranslateService; + let activatedRoute: MockActivatedRoute; + + beforeEach(async () => { + accountServiceMock = { + getSshPublicKey: jest.fn(), + addSshPublicKey: jest.fn(), + addNewSshPublicKey: jest.fn(), + }; + alertServiceMock = { + error: jest.fn(), + success: jest.fn(), + }; + const routerMock = { navigate: jest.fn() }; + await TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [SshUserSettingsKeyDetailsComponent, TranslatePipeMock], + providers: [ + { + provide: ActivatedRoute, + useValue: new MockActivatedRoute({}), + }, + { provide: AccountService, useValue: accountServiceMock }, + { provide: TranslateService, useClass: MockTranslateService }, + { provide: NgbModal, useClass: MockNgbModalService }, + { provide: Router, useValue: routerMock }, + { provide: AlertService, useValue: alertServiceMock }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(SshUserSettingsKeyDetailsComponent); + comp = fixture.componentInstance; + translateService = TestBed.inject(TranslateService); + translateService.currentLang = 'en'; + + router = TestBed.inject(Router); + activatedRoute = TestBed.inject(ActivatedRoute) as unknown as MockActivatedRoute; + }); + + it('should initialize view for adding new keys and save new key', () => { + accountServiceMock.addNewSshPublicKey.mockReturnValue(of({})); + comp.ngOnInit(); + expect(accountServiceMock.getSshPublicKey).not.toHaveBeenCalled(); + expect(comp.isLoading).toBeFalse(); + comp.displayedSshKey = mockKey; + comp.displayedExpiryDate = dayjs(); + comp.displayedKeyLabel = 'label'; + comp.validateExpiryDate(); + expect(comp.isExpiryDateValid).toBeTrue(); + comp.saveSshKey(); + expect(alertServiceMock.success).toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalledWith(['../'], { relativeTo: activatedRoute }); + }); + + it('should initialize view for adding new keys and fail saving key', () => { + const httpError1 = new HttpErrorResponse({ + error: { errorKey: invalidKeyFormat }, + status: 400, + }); + const httpError2 = new HttpErrorResponse({ + error: { errorKey: keyAlreadyExists }, + status: 400, + }); + accountServiceMock.addNewSshPublicKey.mockReturnValue(throwError(() => httpError1)); + comp.ngOnInit(); + expect(accountServiceMock.getSshPublicKey).not.toHaveBeenCalled(); + expect(comp.isLoading).toBeFalse(); + comp.displayedSshKey = mockKey; + comp.displayedExpiryDate = dayjs(); + comp.displayedKeyLabel = 'label'; + comp.validateExpiryDate(); + expect(comp.isExpiryDateValid).toBeTrue(); + comp.saveSshKey(); + accountServiceMock.addNewSshPublicKey.mockReturnValue(throwError(() => httpError2)); + comp.saveSshKey(); + expect(alertServiceMock.error).toHaveBeenCalled(); + expect(router.navigate).not.toHaveBeenCalled(); + }); + + it('should initialize key details view with key loaded', async () => { + accountServiceMock.getSshPublicKey.mockReturnValue(of(mockedUserSshKeys)); + activatedRoute.setParameters({ keyId: 1 }); + comp.ngOnInit(); + expect(accountServiceMock.getSshPublicKey).toHaveBeenCalled(); + expect(comp.isLoading).toBeFalse(); + expect(comp.displayedSshKey).toEqual(mockKey); + comp.goBack(); + expect(router.navigate).toHaveBeenCalledWith(['../../'], { relativeTo: activatedRoute }); + }); + + it('should detect Windows', () => { + jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); + comp.ngOnInit(); + expect(comp.copyInstructions).toBe('cat ~/.ssh/id_ed25519.pub | clip'); + }); + + it('should detect MacOS', () => { + jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)'); + comp.ngOnInit(); + expect(comp.copyInstructions).toBe('pbcopy < ~/.ssh/id_ed25519.pub'); + }); + + it('should detect Linux', () => { + jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (X11; Linux x86_64)'); + comp.ngOnInit(); + expect(comp.copyInstructions).toBe('xclip -selection clipboard < ~/.ssh/id_ed25519.pub'); + }); + + it('should detect Android', () => { + jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Linux; Android 10; Pixel 3)'); + comp.ngOnInit(); + expect(comp.copyInstructions).toBe('termux-clipboard-set < ~/.ssh/id_ed25519.pub'); + }); + + it('should detect iOS', () => { + jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (iPhone; CPU iPhone OS 13_5)'); + comp.ngOnInit(); + expect(comp.copyInstructions).toBe('Ctrl + C'); + }); + + it('should return Unknown for unrecognized OS', () => { + jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Unknown OS)'); + comp.ngOnInit(); + expect(comp.copyInstructions).toBe('Ctrl + C'); + }); +}); diff --git a/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts b/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts index 28d21fe2cc84..f64bcc72d137 100644 --- a/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts +++ b/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts @@ -1,48 +1,58 @@ -import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; +import { HttpResponse } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AccountService } from 'app/core/auth/account.service'; -import { ProfileService } from 'app/shared/layouts/profiles/profile.service'; import { of, throwError } from 'rxjs'; -import { User } from 'app/core/user/user.model'; import { ArtemisTestModule } from '../../test.module'; -import { MockNgbModalService } from '../../helpers/mocks/service/mock-ngb-modal.service'; import { MockTranslateService, TranslatePipeMock } from '../../helpers/mocks/service/mock-translate.service'; import { TranslateService } from '@ngx-translate/core'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { PROFILE_LOCALVC } from 'app/app.constants'; import { SshUserSettingsComponent } from 'app/shared/user-settings/ssh-settings/ssh-user-settings.component'; +import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; +import { AlertService } from 'app/core/util/alert.service'; describe('SshUserSettingsComponent', () => { let fixture: ComponentFixture; let comp: SshUserSettingsComponent; - const mockKey = 'mock-key'; - + const mockKey = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJxKWdvcbNTWl4vBjsijoY5HN5dpjxU40huy1PFpdd2o comment'; + const mockedUserSshKeys = [ + { + id: 3, + publicKey: mockKey, + label: 'Key label', + keyHash: 'Key hash', + } as UserSshPublicKey, + { + id: 3, + publicKey: mockKey, + label: 'Key label', + keyHash: 'Key hash 2', + } as UserSshPublicKey, + ]; + let alertServiceMock: { + error: jest.Mock; + }; let accountServiceMock: { getAuthenticationState: jest.Mock; - addSshPublicKey: jest.Mock; deleteSshPublicKey: jest.Mock; + getAllSshPublicKeys: jest.Mock; }; - let profileServiceMock: { getProfileInfo: jest.Mock }; let translateService: TranslateService; beforeEach(async () => { - profileServiceMock = { - getProfileInfo: jest.fn(), - }; accountServiceMock = { getAuthenticationState: jest.fn(), - addSshPublicKey: jest.fn(), deleteSshPublicKey: jest.fn(), + getAllSshPublicKeys: jest.fn(), + }; + alertServiceMock = { + error: jest.fn(), }; - await TestBed.configureTestingModule({ imports: [ArtemisTestModule], declarations: [SshUserSettingsComponent, TranslatePipeMock], providers: [ { provide: AccountService, useValue: accountServiceMock }, - { provide: ProfileService, useValue: profileServiceMock }, + { provide: AlertService, useValue: alertServiceMock }, { provide: TranslateService, useClass: MockTranslateService }, - { provide: NgbModal, useClass: MockNgbModalService }, ], }).compileComponents(); fixture = TestBed.createComponent(SshUserSettingsComponent); @@ -51,110 +61,32 @@ describe('SshUserSettingsComponent', () => { translateService.currentLang = 'en'; }); - beforeEach(() => { - profileServiceMock.getProfileInfo.mockReturnValue(of({ activeProfiles: [PROFILE_LOCALVC] })); - accountServiceMock.getAuthenticationState.mockReturnValue(of({ id: 99, sshPublicKey: mockKey } as User)); - }); - - it('should initialize with localVC profile', async () => { - comp.ngOnInit(); - expect(profileServiceMock.getProfileInfo).toHaveBeenCalled(); - expect(accountServiceMock.getAuthenticationState).toHaveBeenCalled(); - expect(comp.localVCEnabled).toBeTrue(); - expect(comp.sshKey).toBe(mockKey); - }); - - it('should initialize with no localVC profile set', async () => { - profileServiceMock.getProfileInfo.mockReturnValue(of({ activeProfiles: [] })); + it('should initialize with User without keys', async () => { + accountServiceMock.getAllSshPublicKeys.mockReturnValue(of([] as UserSshPublicKey[])); comp.ngOnInit(); - expect(profileServiceMock.getProfileInfo).toHaveBeenCalled(); - expect(accountServiceMock.getAuthenticationState).toHaveBeenCalled(); - expect(comp.localVCEnabled).toBeFalse(); + expect(accountServiceMock.getAllSshPublicKeys).toHaveBeenCalled(); + expect(comp.keyCount).toBe(0); }); - it('should save SSH key and disable edit mode', () => { - accountServiceMock.addSshPublicKey.mockReturnValue(of(new HttpResponse({ status: 200 }))); + it('should initialize with User with keys', async () => { + accountServiceMock.getAllSshPublicKeys.mockReturnValue(of(mockedUserSshKeys as UserSshPublicKey[])); comp.ngOnInit(); - comp.sshKey = 'new-key'; - comp.showKeyDetailsView = true; - comp.saveSshKey(); - expect(accountServiceMock.addSshPublicKey).toHaveBeenCalledWith('new-key'); - expect(comp.showKeyDetailsView).toBeFalse(); + expect(accountServiceMock.getAllSshPublicKeys).toHaveBeenCalled(); + expect(comp.keyCount).toBe(2); }); - it('should delete SSH key and disable edit mode', () => { + it('should delete SSH key', async () => { + accountServiceMock.getAllSshPublicKeys.mockReturnValue(of(mockedUserSshKeys as UserSshPublicKey[])); accountServiceMock.deleteSshPublicKey.mockReturnValue(of(new HttpResponse({ status: 200 }))); comp.ngOnInit(); - const empty = ''; - comp.sshKey = 'new-key'; - comp.showKeyDetailsView = true; - comp.deleteSshKey(); + comp.deleteSshKey(mockedUserSshKeys[0]); expect(accountServiceMock.deleteSshPublicKey).toHaveBeenCalled(); - expect(comp.showKeyDetailsView).toBeFalse(); - expect(comp.storedSshKey).toEqual(empty); - }); - - it('should not delete and save on error response', () => { - const errorResponse = new HttpErrorResponse({ status: 500, statusText: 'Server Error', error: { message: 'Error occurred' } }); - accountServiceMock.deleteSshPublicKey.mockReturnValue(throwError(() => errorResponse)); - accountServiceMock.addSshPublicKey.mockReturnValue(throwError(() => errorResponse)); - comp.ngOnInit(); - const key = 'new-key'; - comp.sshKey = key; - comp.storedSshKey = key; - comp.saveSshKey(); - comp.deleteSshKey(); - expect(comp.storedSshKey).toEqual(key); - }); - - it('should cancel editing on cancel', () => { - accountServiceMock.addSshPublicKey.mockReturnValue(of(new HttpResponse({ status: 200 }))); - comp.ngOnInit(); - const oldKey = 'old-key'; - const newKey = 'new-key'; - comp.sshKey = oldKey; - comp.showKeyDetailsView = true; - comp.saveSshKey(); - expect(comp.storedSshKey).toEqual(oldKey); - comp.showKeyDetailsView = true; - comp.sshKey = newKey; - comp.cancelEditingSshKey(); - expect(comp.storedSshKey).toEqual(oldKey); - }); - - it('should detect Windows', () => { - jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); - comp.ngOnInit(); - expect(comp.copyInstructions).toBe('cat ~/.ssh/id_ed25519.pub | clip'); - }); - - it('should detect MacOS', () => { - jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)'); - comp.ngOnInit(); - expect(comp.copyInstructions).toBe('pbcopy < ~/.ssh/id_ed25519.pub'); - }); - - it('should detect Linux', () => { - jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (X11; Linux x86_64)'); - comp.ngOnInit(); - expect(comp.copyInstructions).toBe('xclip -selection clipboard < ~/.ssh/id_ed25519.pub'); - }); - - it('should detect Android', () => { - jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Linux; Android 10; Pixel 3)'); - comp.ngOnInit(); - expect(comp.copyInstructions).toBe('termux-clipboard-set < ~/.ssh/id_ed25519.pub'); - }); - - it('should detect iOS', () => { - jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (iPhone; CPU iPhone OS 13_5)'); - comp.ngOnInit(); - expect(comp.copyInstructions).toBe('Ctrl + C'); }); - it('should return Unknown for unrecognized OS', () => { - jest.spyOn(window.navigator, 'userAgent', 'get').mockReturnValue('Mozilla/5.0 (Unknown OS)'); + it('should fail to load SSH keys', () => { + accountServiceMock.getAllSshPublicKeys.mockReturnValue(throwError(() => new HttpResponse({ body: new Blob() }))); comp.ngOnInit(); - expect(comp.copyInstructions).toBe('Ctrl + C'); + expect(comp.keyCount).toBe(0); + expect(alertServiceMock.error).toHaveBeenCalled(); }); }); From 75e3a0b3a4e29d5a6cab6094a6bae81c037f8055 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 22:29:34 +0200 Subject: [PATCH 21/47] Use DTO instead of database entity --- .../aet/artemis/core/web/AccountResource.java | 13 +++++++------ .../exercise/dto/UserSshPublicKeyDTO.java | 16 ++++++++++++++++ .../service/UserSshPublicKeyService.java | 9 +++++---- .../artemis/core/user/util/UserTestService.java | 5 +++-- 4 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 src/main/java/de/tum/cit/aet/artemis/exercise/dto/UserSshPublicKeyDTO.java diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index fb146038993f..505841b38b5e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -45,6 +45,7 @@ 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.exercise.dto.UserSshPublicKeyDTO; import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.programming.service.UserSshPublicKeyService; import de.tum.cit.aet.artemis.programming.service.localvc.LocalVCPersonalAccessTokenManagementService; @@ -143,9 +144,9 @@ public ResponseEntity changePassword(@RequestBody PasswordChangeDTO passwo */ @GetMapping("account/ssh-public-keys") @EnforceAtLeastStudent - public ResponseEntity> getSshPublicKey() { + public ResponseEntity> getSshPublicKey() { User user = userRepository.getUser(); - List keys = userSshPublicKeyService.getAllSshKeysForUser(user); + List keys = userSshPublicKeyService.getAllSshKeysForUser(user).stream().map(UserSshPublicKeyDTO::of).toList(); return ResponseEntity.ok(keys); } @@ -158,10 +159,10 @@ public ResponseEntity> getSshPublicKey() { */ @GetMapping("account/ssh-public-key") @EnforceAtLeastStudent - public ResponseEntity getSshPublicKey(@RequestParam("keyId") Long keyId) { + public ResponseEntity getSshPublicKey(@RequestParam("keyId") Long keyId) { User user = userRepository.getUser(); UserSshPublicKey key = userSshPublicKeyService.getSshKeyForUser(user, keyId); - return ResponseEntity.ok(key); + return ResponseEntity.ok(UserSshPublicKeyDTO.of(key)); } /** @@ -186,14 +187,14 @@ public ResponseEntity hasUserSSHkeys() { */ @PutMapping("account/ssh-public-key") @EnforceAtLeastStudent - public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKey sshPublicKey) throws GeneralSecurityException, IOException { + public ResponseEntity 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.getPublicKey()); + keyEntry = AuthorizedKeyEntry.parseAuthorizedKeyEntry(sshPublicKey.publicKey()); } catch (IllegalArgumentException e) { throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true); diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/dto/UserSshPublicKeyDTO.java b/src/main/java/de/tum/cit/aet/artemis/exercise/dto/UserSshPublicKeyDTO.java new file mode 100644 index 000000000000..3b6065e5fb4c --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/exercise/dto/UserSshPublicKeyDTO.java @@ -0,0 +1,16 @@ +package de.tum.cit.aet.artemis.exercise.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()); + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 3a4685d563a0..ebb0ca7339f3 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -18,6 +18,7 @@ import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; +import de.tum.cit.aet.artemis.exercise.dto.UserSshPublicKeyDTO; import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; @@ -42,7 +43,7 @@ public UserSshPublicKeyService(UserSshPublicKeyRepository userSshPublicKeyReposi * @param keyEntry the {@link AuthorizedKeyEntry} containing the SSH public key details, used to resolve the {@link PublicKey}. * @param sshPublicKey the {@link UserSshPublicKey} object containing metadata about the SSH key such as the key itself, label, and expiry date. */ - public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshPublicKey sshPublicKey) throws GeneralSecurityException, IOException { + public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshPublicKeyDTO sshPublicKey) throws GeneralSecurityException, IOException { PublicKey publicKey = keyEntry.resolvePublicKey(null, null, null); String keyHash = HashUtils.getSha512Fingerprint(publicKey); @@ -52,11 +53,11 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP UserSshPublicKey newUserSshPublicKey = new UserSshPublicKey(); newUserSshPublicKey.setUserId(user.getId()); - newUserSshPublicKey.setPublicKey(sshPublicKey.getPublicKey()); + newUserSshPublicKey.setPublicKey(sshPublicKey.publicKey()); newUserSshPublicKey.setKeyHash(keyHash); - setLabelForKey(newUserSshPublicKey, sshPublicKey.getLabel()); + setLabelForKey(newUserSshPublicKey, sshPublicKey.label()); newUserSshPublicKey.setCreationDate(ZonedDateTime.now()); - newUserSshPublicKey.setExpiryDate(sshPublicKey.getExpiryDate()); + newUserSshPublicKey.setExpiryDate(sshPublicKey.expiryDate()); userSshPublicKeyRepository.save(newUserSshPublicKey); } diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index b9979d436281..4e0fa7a7e284 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -50,6 +50,7 @@ import de.tum.cit.aet.artemis.core.util.CourseUtilService; import de.tum.cit.aet.artemis.core.util.RequestUtilService; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; +import de.tum.cit.aet.artemis.exercise.dto.UserSshPublicKeyDTO; import de.tum.cit.aet.artemis.exercise.test_repository.ParticipationTestRepository; import de.tum.cit.aet.artemis.exercise.test_repository.SubmissionTestRepository; import de.tum.cit.aet.artemis.lti.service.LtiService; @@ -961,8 +962,8 @@ public void failToAddOrDeleteWithInvalidKeyId() throws Exception { userSshPublicKeyRepository.save(userKey); request.delete("/api/account/ssh-public-key?keyId=3443", HttpStatus.FORBIDDEN); - request.get("/api/account/ssh-public-key?keyId=43443", HttpStatus.NOT_FOUND, UserSshPublicKey.class); - request.get("/api/account/ssh-public-key?keyId=" + userKey.getId(), HttpStatus.FORBIDDEN, UserSshPublicKey.class); + request.get("/api/account/ssh-public-key?keyId=43443", HttpStatus.NOT_FOUND, UserSshPublicKeyDTO.class); + request.get("/api/account/ssh-public-key?keyId=" + userKey.getId(), HttpStatus.FORBIDDEN, UserSshPublicKeyDTO.class); } From 52c6b3b25ba4d7dc1ccad19441eb1d17f56f907f Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 22:33:33 +0200 Subject: [PATCH 22/47] moved DTO --- .../java/de/tum/cit/aet/artemis/core/web/AccountResource.java | 2 +- .../{exercise => programming}/dto/UserSshPublicKeyDTO.java | 2 +- .../artemis/programming/service/UserSshPublicKeyService.java | 2 +- .../de/tum/cit/aet/artemis/core/user/util/UserTestService.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/main/java/de/tum/cit/aet/artemis/{exercise => programming}/dto/UserSshPublicKeyDTO.java (93%) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 505841b38b5e..fc04e8ea5765 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -45,8 +45,8 @@ 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.exercise.dto.UserSshPublicKeyDTO; 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; diff --git a/src/main/java/de/tum/cit/aet/artemis/exercise/dto/UserSshPublicKeyDTO.java b/src/main/java/de/tum/cit/aet/artemis/programming/dto/UserSshPublicKeyDTO.java similarity index 93% rename from src/main/java/de/tum/cit/aet/artemis/exercise/dto/UserSshPublicKeyDTO.java rename to src/main/java/de/tum/cit/aet/artemis/programming/dto/UserSshPublicKeyDTO.java index 3b6065e5fb4c..ecfeedab8126 100644 --- a/src/main/java/de/tum/cit/aet/artemis/exercise/dto/UserSshPublicKeyDTO.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/dto/UserSshPublicKeyDTO.java @@ -1,4 +1,4 @@ -package de.tum.cit.aet.artemis.exercise.dto; +package de.tum.cit.aet.artemis.programming.dto; import java.time.ZonedDateTime; diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index ebb0ca7339f3..0ee65e9e0d69 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -18,8 +18,8 @@ import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; -import de.tum.cit.aet.artemis.exercise.dto.UserSshPublicKeyDTO; 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.repository.UserSshPublicKeyRepository; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index 4e0fa7a7e284..6df20a7a8bb7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -50,12 +50,12 @@ import de.tum.cit.aet.artemis.core.util.CourseUtilService; import de.tum.cit.aet.artemis.core.util.RequestUtilService; import de.tum.cit.aet.artemis.exercise.domain.SubmissionType; -import de.tum.cit.aet.artemis.exercise.dto.UserSshPublicKeyDTO; import de.tum.cit.aet.artemis.exercise.test_repository.ParticipationTestRepository; import de.tum.cit.aet.artemis.exercise.test_repository.SubmissionTestRepository; import de.tum.cit.aet.artemis.lti.service.LtiService; import de.tum.cit.aet.artemis.programming.domain.ProgrammingSubmission; 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.repository.ParticipationVCSAccessTokenRepository; import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; import de.tum.cit.aet.artemis.programming.service.ci.CIUserManagementService; From c90d4e7681c3a57ff252e83e2612cb57ad4f5864 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 22:56:46 +0200 Subject: [PATCH 23/47] add change suggestions --- .../tum/cit/aet/artemis/core/web/AccountResource.java | 10 +++++----- .../artemis/programming/domain/UserSshPublicKey.java | 1 - .../details/ssh-user-settings-key-details.component.ts | 2 +- .../ssh-settings/ssh-user-settings.component.scss | 8 -------- .../UserAccountLocalVcsIntegrationTest.java | 6 ------ 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index fc04e8ea5765..8fc0a68ed51d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -80,13 +80,13 @@ 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, - UserSshPublicKeyService userSSHPublicKeyService) { + UserSshPublicKeyService userSshPublicKeyService) { this.userRepository = userRepository; this.userService = userService; this.userCreationService = userCreationService; this.accountService = accountService; this.fileService = fileService; - this.userSshPublicKeyService = userSSHPublicKeyService; + this.userSshPublicKeyService = userSshPublicKeyService; } /** @@ -138,7 +138,7 @@ public ResponseEntity changePassword(@RequestBody PasswordChangeDTO passwo } /** - * GET account/ssh-public-keys : sets the ssh public key + * GET account/ssh-public-keys : retrieves all SSH keys of a user * * @return the ResponseEntity containing all public SSH keys of a user with status 200 (OK), or with status 400 (Bad Request) */ @@ -151,7 +151,7 @@ public ResponseEntity> getSshPublicKey() { } /** - * GET account/ssh-public-key : sets the ssh public key + * GET account/ssh-public-key : gets the ssh public key * * @param keyId The id of the key that should be fetched * @@ -166,7 +166,7 @@ public ResponseEntity getSshPublicKey(@RequestParam("keyId" } /** - * GET account/has-ssh-public-key : sets the ssh public 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), or with status 400 (Bad Request) */ diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java index f7f2619a83fd..ad3b5112c35a 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java @@ -46,7 +46,6 @@ public class UserSshPublicKey extends DomainObject { /** * A hash of the public ssh key for fast comparison in the database (with an index) */ - @Nullable @Size(max = 100) @Column(name = "key_hash") private String keyHash; diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts index 32c7cebfc2e4..56c67c9d43bc 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -25,7 +25,7 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { sshPublicKey: UserSshPublicKey; // state change variables - isCreateMode = false; // true when editing existing key, false when creating new key + isCreateMode = false; // true when creating new key, false when viewing existing key isLoading = true; copyInstructions = ''; diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss index 435b70e55cc8..b06f95174393 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss @@ -28,10 +28,6 @@ textarea { height: 30px; } -.days-input-field { - max-width: 150px; -} - table { width: 100%; border-collapse: collapse; @@ -40,10 +36,6 @@ table { text-align: left; } -.smaller-font { - font-size: smaller; -} - th, td { padding: 15px 15px; diff --git a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java index 10fd59c51d66..afb5a65a107d 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/authentication/UserAccountLocalVcsIntegrationTest.java @@ -62,12 +62,6 @@ void failToAddInvalidSshPublicKeyForUser() throws Exception { userTestService.failToAddInvalidPublicSSHkey(); } - @Test - @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") - void getSshPublicKeysByUser() throws Exception { - // TODO userTestService.(); - } - @Test @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") void deleteSshPublicKeyByUser() throws Exception { From 73e147ac2bebc7dbb01eb2ecc891b1f578a86715 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 22 Oct 2024 22:58:37 +0200 Subject: [PATCH 24/47] remove debugging statement --- .../de/tum/cit/aet/artemis/core/user/util/UserTestService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index 6df20a7a8bb7..f471f60aa80c 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -888,7 +888,6 @@ public void getUserSshPublicKeys() throws Exception { postNewValidKeyToServer(validKey); List response = request.getList("/api/account/ssh-public-keys", HttpStatus.OK, UserSshPublicKey.class); - System.out.println(response); assertThat(response.size()).isEqualTo(1); UserSshPublicKey userKey = response.getFirst(); From 71ef0855f25ca4b0d60d91e38ff624e72e5d3afb Mon Sep 17 00:00:00 2001 From: entholzer Date: Wed, 23 Oct 2024 22:19:40 +0200 Subject: [PATCH 25/47] fix client test style --- .../account/ssh-user-settings-key-details.component.spec.ts | 2 +- .../spec/helpers/mocks/service/mock-account.service.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts b/src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts index a1a58b3509ef..1b96b78f3a32 100644 --- a/src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts +++ b/src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts @@ -11,7 +11,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { SshUserSettingsKeyDetailsComponent } from 'app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component'; import { MockActivatedRoute } from '../../helpers/mocks/activated-route/mock-activated-route'; import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; -import dayjs from 'dayjs'; +import dayjs from 'dayjs/esm'; import { AlertService } from 'app/core/util/alert.service'; describe('SshUserSettingsComponent', () => { diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-account.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-account.service.ts index 421a28217750..61893f339731 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-account.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-account.service.ts @@ -3,6 +3,7 @@ import { Course } from 'app/entities/course.model'; import { IAccountService } from 'app/core/auth/account.service'; import { User } from 'app/core/user/user.model'; import { Exercise } from 'app/entities/exercise.model'; +import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; export class MockAccountService implements IAccountService { userIdentityValue: User | undefined; @@ -39,7 +40,7 @@ export class MockAccountService implements IAccountService { isOwnerOfParticipation = () => true; isAdmin = () => true; save = (account: any) => ({}) as any; - addSshPublicKey = (sshPublicKey: string) => of(); getVcsAccessToken = (participationId: number) => of(); createVcsAccessToken = (participationId: number) => of(); + addNewSshPublicKey = (userSshPublicKey: UserSshPublicKey) => of(); } From 8f9696005ed47f869c2d03d3d25e2a4fc1dcea46 Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 24 Oct 2024 13:28:50 +0200 Subject: [PATCH 26/47] replaced put with post --- .../aet/artemis/core/web/AccountResource.java | 13 +++--- .../webapp/app/core/auth/account.service.ts | 8 ++-- ...ructor-and-editor-container.component.html | 8 ++++ .../ssh-user-settings.component.html | 12 +---- .../ssh-user-settings.component.ts | 8 +++- .../core/user/util/UserTestService.java | 44 ++++++------------- .../icl/LocalVCSshIntegrationTest.java | 1 + 7 files changed, 41 insertions(+), 53 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 8fc0a68ed51d..945cc9d3dc5e 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -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; @@ -157,9 +158,9 @@ public ResponseEntity> getSshPublicKey() { * * @return the ResponseEntity containing the requested public SSH key of a user with status 200 (OK), or with status 400 (Bad Request) */ - @GetMapping("account/ssh-public-key") + @GetMapping("account/ssh-public-key/{keyId}") @EnforceAtLeastStudent - public ResponseEntity getSshPublicKey(@RequestParam("keyId") Long keyId) { + public ResponseEntity getSshPublicKey(@PathVariable Long keyId) { User user = userRepository.getUser(); UserSshPublicKey key = userSshPublicKeyService.getSshKeyForUser(user, keyId); return ResponseEntity.ok(UserSshPublicKeyDTO.of(key)); @@ -179,13 +180,13 @@ public ResponseEntity hasUserSSHkeys() { } /** - * PUT account/ssh-public-key : creates a new ssh public key for a user + * 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), with status 404 (Not Found), or with status 400 (Bad Request) */ - @PutMapping("account/ssh-public-key") + @PostMapping("account/ssh-public-key") @EnforceAtLeastStudent public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKeyDTO sshPublicKey) throws GeneralSecurityException, IOException { @@ -211,9 +212,9 @@ public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKeyDTO ssh * * @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request) */ - @DeleteMapping("account/ssh-public-key") + @DeleteMapping("account/ssh-public-key/{keyId}") @EnforceAtLeastStudent - public ResponseEntity deleteSshPublicKey(@RequestParam("keyId") Long keyId) { + public ResponseEntity deleteSshPublicKey(@PathVariable Long keyId) { User user = userRepository.getUser(); log.debug("REST request to remove SSH key of user {}", user.getLogin()); userSshPublicKeyService.deleteUserSshPublicKey(user.getId(), keyId); diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts index 83785877416e..3354b66418b9 100644 --- a/src/main/webapp/app/core/auth/account.service.ts +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -331,7 +331,7 @@ export class AccountService implements IAccountService { * @param userSshPublicKey */ addNewSshPublicKey(userSshPublicKey: UserSshPublicKey): Observable> { - return this.http.put('api/account/ssh-public-key', userSshPublicKey, { observe: 'response' }); + return this.http.post('api/account/ssh-public-key', userSshPublicKey, { observe: 'response' }); } /** @@ -352,16 +352,14 @@ export class AccountService implements IAccountService { * Retrieves a specific public SSH keys of a user */ getSshPublicKey(keyId: number): Observable { - const params = new HttpParams().set('keyId', keyId); - return this.http.get('api/account/ssh-public-key', { params }); + return this.http.get(`api/account/ssh-public-key/${keyId}`); } /** * Sends a request to the server to delete the user's current SSH key */ deleteSshPublicKey(keyId: number): Observable { - const params = new HttpParams().set('keyId', keyId); - return this.http.delete('api/account/ssh-public-key', { params }); + return this.http.delete(`api/account/ssh-public-key/${keyId}`); } /** diff --git a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html index 473ffcef7626..1584f7947622 100644 --- a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html +++ b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html @@ -79,6 +79,14 @@ [style.background-color]="selectedRepository === REPOSITORY.TEST ? '#3e8acc' : 'transparent'" jhiTranslate="artemisApp.editor.repoSelect.testRepo" > +
diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html index 0bdc36730756..a5650cef05a9 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html @@ -52,17 +52,9 @@

{{ key.keyHash }}

- @if (key.expiryDate && key.hasExpired) { + @if (key.expiryDate) {
-
-
- {{ key.expiryDate | artemisDate: 'long-date' }} -
-
- } - @if (key.expiryDate && !key.hasExpired) { -
-
+
{{ key.expiryDate | artemisDate: 'long-date' }}
diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts index b1b525e80ac5..ab392de6f41b 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AccountService } from 'app/core/auth/account.service'; import { Subject, tap } from 'rxjs'; import { faEdit, faEllipsis, faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; @@ -14,7 +14,7 @@ import dayjs from 'dayjs/esm'; templateUrl: './ssh-user-settings.component.html', styleUrls: ['../user-settings.scss', './ssh-user-settings.component.scss'], }) -export class SshUserSettingsComponent implements OnInit { +export class SshUserSettingsComponent implements OnInit, OnDestroy { readonly documentationType: DocumentationType = 'SshSetup'; sshPublicKeys: UserSshPublicKey[] = []; @@ -44,6 +44,10 @@ export class SshUserSettingsComponent implements OnInit { this.refreshSshKeys(); } + ngOnDestroy() { + this.dialogErrorSource.complete(); + } + deleteSshKey(key: UserSshPublicKey) { this.accountService.deleteSshPublicKey(key.id).subscribe({ next: () => { diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index f471f60aa80c..7baa9b8114a3 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -27,9 +27,6 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.util.LinkedMultiValueMap; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; - import de.tum.cit.aet.artemis.atlas.domain.science.ScienceEvent; import de.tum.cit.aet.artemis.atlas.domain.science.ScienceEventType; import de.tum.cit.aet.artemis.atlas.test_repository.ScienceEventTestRepository; @@ -885,13 +882,13 @@ public void getUserSshPublicKeys() throws Exception { User user = userTestRepository.getUser(); var validKey = createNewValidSSHKey(user, sshKey1); - postNewValidKeyToServer(validKey); + request.postWithResponseBody("/api/account/ssh-public-key", validKey, String.class, HttpStatus.OK); List response = request.getList("/api/account/ssh-public-keys", HttpStatus.OK, UserSshPublicKey.class); assertThat(response.size()).isEqualTo(1); UserSshPublicKey userKey = response.getFirst(); - request.get("/api/account/ssh-public-key?keyId=" + userKey.getId(), HttpStatus.OK, UserSshPublicKey.class); + request.get("/api/account/ssh-public-key/" + userKey.getId(), HttpStatus.OK, UserSshPublicKey.class); var hasSSHkeys = request.get("/api/account/has-ssh-public-keys", HttpStatus.OK, Boolean.class); assertThat(hasSSHkeys).isTrue(); } @@ -903,7 +900,7 @@ public void addUserSshPublicKey() throws Exception { User user = userTestRepository.getUser(); var validKey = createNewValidSSHKey(user, sshKey1); - postNewValidKeyToServer(validKey); + request.postWithResponseBody("/api/account/ssh-public-key", validKey, String.class, HttpStatus.OK); var storedUserKey = userSshPublicKeyRepository.findAllByUserId(user.getId()).getFirst(); assertThat(storedUserKey).isNotNull(); @@ -918,22 +915,16 @@ public void addUserSshPublicKeyWithOutLabel() throws Exception { var validKey = createNewValidSSHKey(user, sshKey1); validKey.setLabel(null); - postNewValidKeyToServer(validKey); + request.postWithResponseBody("/api/account/ssh-public-key", validKey, String.class, HttpStatus.OK); var validKey2 = createNewValidSSHKey(user, sshKey2); validKey.setLabel(""); - postNewValidKeyToServer(validKey2); + request.postWithResponseBody("/api/account/ssh-public-key", validKey2, String.class, HttpStatus.OK); var storedUserKeys = userSshPublicKeyRepository.findAllByUserId(user.getId()); assertThat(storedUserKeys.size()).isEqualTo(2); } - private void postNewValidKeyToServer(UserSshPublicKey key) throws Exception { - ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); - String json = ow.writeValueAsString(key); - request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.OK, true); - } - // Test public void failToAddPublicSSHkeyTwice() throws Exception { HttpHeaders headers = new HttpHeaders(); @@ -941,11 +932,9 @@ public void failToAddPublicSSHkeyTwice() throws Exception { User user = userTestRepository.getUser(); var validKey = createNewValidSSHKey(user, sshKey1); - ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); - String json = ow.writeValueAsString(validKey); - request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.OK, true); - request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.BAD_REQUEST, true); + request.postWithResponseBody("/api/account/ssh-public-key", validKey, String.class, HttpStatus.OK); + request.postWithResponseBody("/api/account/ssh-public-key", validKey, String.class, HttpStatus.BAD_REQUEST); } // Test @@ -955,14 +944,14 @@ public void failToAddOrDeleteWithInvalidKeyId() throws Exception { User user = userTestRepository.getUser(); var validKey = createNewValidSSHKey(user, sshKey1); - postNewValidKeyToServer(validKey); + request.postWithResponseBody("/api/account/ssh-public-key", validKey, String.class, HttpStatus.OK); var userKey = userSshPublicKeyRepository.findAll().getFirst(); userKey.setUserId(12L); userSshPublicKeyRepository.save(userKey); - request.delete("/api/account/ssh-public-key?keyId=3443", HttpStatus.FORBIDDEN); - request.get("/api/account/ssh-public-key?keyId=43443", HttpStatus.NOT_FOUND, UserSshPublicKeyDTO.class); - request.get("/api/account/ssh-public-key?keyId=" + userKey.getId(), HttpStatus.FORBIDDEN, UserSshPublicKeyDTO.class); + request.delete("/api/account/ssh-public-key/3443", HttpStatus.FORBIDDEN); + request.get("/api/account/ssh-public-key/43443", HttpStatus.NOT_FOUND, UserSshPublicKeyDTO.class); + request.get("/api/account/ssh-public-key/" + userKey.getId(), HttpStatus.FORBIDDEN, UserSshPublicKeyDTO.class); } @@ -975,10 +964,7 @@ public void failToAddInvalidPublicSSHkey() throws Exception { var userKey = createNewValidSSHKey(user, sshKey1); userKey.setPublicKey("Invalid Key"); - ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); - String json = ow.writeValueAsString(userKey); - - request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.BAD_REQUEST, true); + request.postWithResponseBody("/api/account/ssh-public-key", userKey, String.class, HttpStatus.BAD_REQUEST); } // Test @@ -988,17 +974,15 @@ public void addAndDeleteSshPublicKey() throws Exception { User user = userTestRepository.getUser(); var validKey = createNewValidSSHKey(user, sshKey1); - ObjectWriter ow = new ObjectMapper().writer().withDefaultPrettyPrinter(); - String json = ow.writeValueAsString(validKey); - request.putWithResponseBody("/api/account/ssh-public-key", json, String.class, HttpStatus.OK, true); + request.postWithResponseBody("/api/account/ssh-public-key", validKey, String.class, HttpStatus.OK); var storedUserKey = userSshPublicKeyRepository.findAllByUserId(user.getId()).getFirst(); assertThat(storedUserKey).isNotNull(); assertThat(storedUserKey.getPublicKey()).isEqualTo(validKey.getPublicKey()); // deleting the key should work correctly - request.delete("/api/account/ssh-public-key?keyId=" + storedUserKey.getId(), HttpStatus.OK); + request.delete("/api/account/ssh-public-key/" + storedUserKey.getId(), HttpStatus.OK); assertThat(userSshPublicKeyRepository.findAllByUserId(user.getId())).isEmpty(); } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java index e9e74931b195..32c1247950f7 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java @@ -25,6 +25,7 @@ import org.apache.sshd.common.session.helpers.AbstractSession; import org.apache.sshd.server.session.ServerSession; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.security.test.context.support.WithMockUser; From abc1f0f2fe2e1102278aacf0c89e0ebe3209549f Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 24 Oct 2024 13:32:53 +0200 Subject: [PATCH 27/47] remove button --- ...-editor-instructor-and-editor-container.component.html | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html index 1584f7947622..473ffcef7626 100644 --- a/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html +++ b/src/main/webapp/app/exercises/programming/manage/code-editor/code-editor-instructor-and-editor-container.component.html @@ -79,14 +79,6 @@ [style.background-color]="selectedRepository === REPOSITORY.TEST ? '#3e8acc' : 'transparent'" jhiTranslate="artemisApp.editor.repoSelect.testRepo" > -
From 8ab97e3ff4f8ffe6c78cdb530aa77ad4924d7386 Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 24 Oct 2024 15:47:56 +0200 Subject: [PATCH 28/47] fix copy paste error --- src/main/webapp/i18n/de/userSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index 47cfe82fdd70..87a024d67b39 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -65,7 +65,7 @@ "keyWillExpireOn": "Der Schlüssel läuft ab am" } }, - "vcsAccessTokensSettingsFingerabdruckPage": { + "vcsAccessTokensSettingsPage": { "addTokenTitle": "Neues Zugriffstoken erzeugen", "infoText": "Du kannst ein persönliches Zugriffstoken generieren, um mit dem Artemis Local Version Control System zu interagieren. Verwende es um dich über HTTP bei Git zu authentifizieren.", "deleteVcsAccessTokenQuestion": "Möchtest du dein Zugriffstoken für das Versionskontrollsystem wirklich löschen? Du kannst dich nicht mehr bei lokalen Repositories authentifizieren, die mit diesem Token geklont wurden.", From 9aa50d55b4b8291f71196145d59c7a2553f30d49 Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 24 Oct 2024 16:40:19 +0200 Subject: [PATCH 29/47] fix tests --- .../aet/artemis/core/web/AccountResource.java | 2 +- .../code-button/code-button.component.ts | 2 +- .../icl/LocalVCSshIntegrationTest.java | 5 -- .../base/AbstractArtemisIntegrationTest.java | 4 ++ .../shared/code-button.component.spec.ts | 49 ++++++++++--------- .../mocks/service/mock-account.service.ts | 1 + 6 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 945cc9d3dc5e..f7fed31b2ecc 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -145,7 +145,7 @@ public ResponseEntity changePassword(@RequestBody PasswordChangeDTO passwo */ @GetMapping("account/ssh-public-keys") @EnforceAtLeastStudent - public ResponseEntity> getSshPublicKey() { + public ResponseEntity> getSshPublicKeys() { User user = userRepository.getUser(); List keys = userSshPublicKeyService.getAllSshKeysForUser(user).stream().map(UserSshPublicKeyDTO::of).toList(); return ResponseEntity.ok(keys); diff --git a/src/main/webapp/app/shared/components/code-button/code-button.component.ts b/src/main/webapp/app/shared/components/code-button/code-button.component.ts index 71fabfa05810..9ff2d578f262 100644 --- a/src/main/webapp/app/shared/components/code-button/code-button.component.ts +++ b/src/main/webapp/app/shared/components/code-button/code-button.component.ts @@ -95,7 +95,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { this.copyEnabled = true; this.useSsh = this.localStorage.retrieve('useSsh') || false; - console.log(this.useSsh); + // console.log(this.useSsh); this.useToken = this.localStorage.retrieve('useToken') || false; this.localStorage.observe('useSsh').subscribe((useSsh) => (this.useSsh = useSsh || false)); this.localStorage.observe('useToken').subscribe((useToken) => (this.useToken = useToken || false)); diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java index 32c1247950f7..0ebcad2679b0 100644 --- a/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/LocalVCSshIntegrationTest.java @@ -25,13 +25,11 @@ import org.apache.sshd.common.session.helpers.AbstractSession; import org.apache.sshd.server.session.ServerSession; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Profile; import org.springframework.security.test.context.support.WithMockUser; import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.programming.domain.UserSshPublicKey; -import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; import de.tum.cit.aet.artemis.programming.service.localvc.SshGitCommandFactoryService; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.SshGitCommand; @@ -41,9 +39,6 @@ class LocalVCSshIntegrationTest extends LocalVCIntegrationTest { private static final String TEST_PREFIX = "localvcsshint"; - @Autowired - private UserSshPublicKeyRepository userSshPublicKeyRepository; - @Override protected String getTestPrefix() { return TEST_PREFIX; diff --git a/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractArtemisIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractArtemisIntegrationTest.java index 469e4f117837..3252a1afdc8b 100644 --- a/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractArtemisIntegrationTest.java +++ b/src/test/java/de/tum/cit/aet/artemis/shared/base/AbstractArtemisIntegrationTest.java @@ -59,6 +59,7 @@ import de.tum.cit.aet.artemis.lti.service.Lti13Service; import de.tum.cit.aet.artemis.modeling.service.ModelingSubmissionService; import de.tum.cit.aet.artemis.programming.domain.VcsRepositoryUri; +import de.tum.cit.aet.artemis.programming.repository.UserSshPublicKeyRepository; import de.tum.cit.aet.artemis.programming.service.GitService; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseGradingService; import de.tum.cit.aet.artemis.programming.service.ProgrammingExerciseParticipationService; @@ -188,6 +189,9 @@ public abstract class AbstractArtemisIntegrationTest implements MockDelegate { @Autowired protected UserTestRepository userTestRepository; + @Autowired + protected UserSshPublicKeyRepository userSshPublicKeyRepository; + @Autowired protected ExerciseTestRepository exerciseRepository; diff --git a/src/test/javascript/spec/component/shared/code-button.component.spec.ts b/src/test/javascript/spec/component/shared/code-button.component.spec.ts index 5665d6d4a1ef..181fda044dad 100644 --- a/src/test/javascript/spec/component/shared/code-button.component.spec.ts +++ b/src/test/javascript/spec/component/shared/code-button.component.spec.ts @@ -1,5 +1,5 @@ import { ClipboardModule } from '@angular/cdk/clipboard'; -import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; @@ -41,6 +41,7 @@ describe('CodeButtonComponent', () => { let localStorageUseSshObserveStubSubject: Subject; let localStorageUseSshStoreStub: jest.SpyInstance; let getVcsAccessTokenSpy: jest.SpyInstance; + let hasSshKeySpy: jest.SpyInstance; let createVcsAccessTokenSpy: jest.SpyInstance; const vcsToken: string = 'vcpat-xlhBs26D4F2CGlkCM59KVU8aaV9bYdX5Mg4IK6T8W3aT'; @@ -122,6 +123,7 @@ describe('CodeButtonComponent', () => { localStorageUseSshObserveStub = jest.spyOn(localStorageMock, 'observe'); localStorageUseSshStoreStub = jest.spyOn(localStorageMock, 'store'); getVcsAccessTokenSpy = jest.spyOn(accountService, 'getVcsAccessToken'); + hasSshKeySpy = jest.spyOn(accountService, 'hasUserSshPublicKeys'); createVcsAccessTokenSpy = jest.spyOn(accountService, 'createVcsAccessToken'); localStorageUseSshObserveStubSubject = new Subject(); @@ -138,6 +140,7 @@ describe('CodeButtonComponent', () => { createVcsAccessTokenSpy = jest .spyOn(accountService, 'createVcsAccessToken') .mockReturnValue(throwError(() => new HttpErrorResponse({ status: 400, statusText: 'Bad Request' }))); + hasSshKeySpy.mockReturnValue(of(true)); }); afterEach(() => { @@ -145,50 +148,52 @@ describe('CodeButtonComponent', () => { jest.restoreAllMocks(); }); - it('should initialize', fakeAsync(() => { + it('should initialize', async () => { stubServices(); component.ngOnInit(); - tick(); + + await fixture.whenStable(); + fixture.detectChanges(); expect(component.sshSettingsUrl).toBe(`${window.location.origin}/user-settings/ssh`); expect(component.sshTemplateUrl).toBe(info.sshCloneURLTemplate); expect(component.sshEnabled).toBe(!!info.sshCloneURLTemplate); expect(component.versionControlUrl).toBe(info.versionControlUrl); - })); + }); - it('should create new vcsAccessToken when it does not exist', fakeAsync(() => { + it('should create new vcsAccessToken when it does not exist', async () => { createVcsAccessTokenSpy = jest.spyOn(accountService, 'createVcsAccessToken').mockReturnValue(of(new HttpResponse({ body: vcsToken }))); getVcsAccessTokenSpy = jest.spyOn(accountService, 'getVcsAccessToken').mockReturnValue(throwError(() => new HttpErrorResponse({ status: 404, statusText: 'Not found' }))); stubServices(); participation.id = 1; component.useParticipationVcsAccessToken = true; component.participations = [participation]; + await component.ngOnInit(); + await fixture.whenStable(); component.ngOnChanges(); - tick(); - component.ngOnInit(); - tick(); + await fixture.whenStable(); expect(component.accessTokensEnabled).toBeTrue(); expect(component.user.vcsAccessToken).toEqual(vcsToken); expect(getVcsAccessTokenSpy).toHaveBeenCalled(); expect(createVcsAccessTokenSpy).toHaveBeenCalled(); - })); + }); - it('should not create new vcsAccessToken when it exists', fakeAsync(() => { + it('should not create new vcsAccessToken when it exists', async () => { participation.id = 1; component.participations = [participation]; component.useParticipationVcsAccessToken = true; stubServices(); + await component.ngOnInit(); + await fixture.whenStable(); component.ngOnChanges(); - tick(); - component.ngOnInit(); - tick(); + await fixture.whenStable(); expect(component.accessTokensEnabled).toBeTrue(); expect(component.user.vcsAccessToken).toEqual(vcsToken); expect(getVcsAccessTokenSpy).toHaveBeenCalled(); expect(createVcsAccessTokenSpy).not.toHaveBeenCalled(); - })); + }); it('should get ssh url (same url for team and individual participation)', () => { participation.repositoryUri = 'https://gitlab.ase.in.tum.de/scm/ITCPLEASE1/itcplease1-exercise.git'; @@ -358,47 +363,43 @@ describe('CodeButtonComponent', () => { expect(component.wasCopied).toBeFalse(); }); - it('should fetch and store ssh preference', fakeAsync(() => { + it('should fetch and store ssh preference', async () => { stubServices(); + hasSshKeySpy.mockReturnValue(of(false)); participation.repositoryUri = `https://gitlab.ase.in.tum.de/scm/ITCPLEASE1/itcplease1-exercise-team1.git`; component.participations = [participation]; component.activeParticipation = participation; component.sshEnabled = true; - + await component.ngOnInit(); + await fixture.whenStable(); fixture.detectChanges(); - tick(); expect(localStorageUseSshRetrieveStub).toHaveBeenNthCalledWith(1, 'useSsh'); expect(localStorageUseSshObserveStub).toHaveBeenNthCalledWith(1, 'useSsh'); expect(component.useSsh).toBeFalse(); fixture.debugElement.query(By.css('.code-button')).nativeElement.click(); - tick(); fixture.debugElement.query(By.css('#useSSHButton')).nativeElement.click(); - tick(); + expect(localStorageUseSshStoreStub).toHaveBeenNthCalledWith(1, 'useSsh', true); expect(component.useSsh).toBeTrue(); fixture.debugElement.query(By.css('#useHTTPSButton')).nativeElement.click(); - tick(); expect(localStorageUseSshStoreStub).toHaveBeenCalledWith('useSsh', false); expect(component.useSsh).toBeFalse(); fixture.debugElement.query(By.css('#useHTTPSWithTokenButton')).nativeElement.click(); - tick(); expect(localStorageUseSshStoreStub).toHaveBeenCalledWith('useSsh', false); expect(component.useSsh).toBeFalse(); expect(component.useToken).toBeTrue(); localStorageUseSshObserveStubSubject.next(true); - tick(); expect(component.useSsh).toBeTrue(); localStorageUseSshObserveStubSubject.next(false); - tick(); expect(component.useSsh).toBeFalse(); - })); + }); it.each([ [ diff --git a/src/test/javascript/spec/helpers/mocks/service/mock-account.service.ts b/src/test/javascript/spec/helpers/mocks/service/mock-account.service.ts index 61893f339731..ea6ee1a829a0 100644 --- a/src/test/javascript/spec/helpers/mocks/service/mock-account.service.ts +++ b/src/test/javascript/spec/helpers/mocks/service/mock-account.service.ts @@ -43,4 +43,5 @@ export class MockAccountService implements IAccountService { getVcsAccessToken = (participationId: number) => of(); createVcsAccessToken = (participationId: number) => of(); addNewSshPublicKey = (userSshPublicKey: UserSshPublicKey) => of(); + hasUserSshPublicKeys = () => of(); } From 7de1858b90b7b4d6070af4781067f3a9ddb6954c Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 24 Oct 2024 19:23:09 +0200 Subject: [PATCH 30/47] fix typo --- src/main/webapp/i18n/de/userSettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index 87a024d67b39..274224945f01 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -59,7 +59,7 @@ "expiry": { "title": "Ablauf", "info": "Für zusätzliche Sicherheit kannst du festlegen, dass dieser Schlüssel automatisch abläuft.", - "doNotExpire": "Kein Abluafsdatum", + "doNotExpire": "Kein Ablaufdatum", "expireAutomatically": "Automatisch ablaufen", "daysUntilExpiry": "Tage bis zum Ablauf", "keyWillExpireOn": "Der Schlüssel läuft ab am" From e8ac90968602610dec66f5a6775e3cd33370c5ca Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 24 Oct 2024 20:09:37 +0200 Subject: [PATCH 31/47] improved client coverage --- .../ssh-user-settings.component.spec.ts | 4 +- .../spec/service/account.service.spec.ts | 111 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts b/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts index f64bcc72d137..07a46ad53d4f 100644 --- a/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts +++ b/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts @@ -21,7 +21,7 @@ describe('SshUserSettingsComponent', () => { keyHash: 'Key hash', } as UserSshPublicKey, { - id: 3, + id: 4, publicKey: mockKey, label: 'Key label', keyHash: 'Key hash 2', @@ -72,6 +72,8 @@ describe('SshUserSettingsComponent', () => { accountServiceMock.getAllSshPublicKeys.mockReturnValue(of(mockedUserSshKeys as UserSshPublicKey[])); comp.ngOnInit(); expect(accountServiceMock.getAllSshPublicKeys).toHaveBeenCalled(); + expect(comp.sshPublicKeys).toHaveLength(2); + expect(comp.sshPublicKeys[0].publicKey).toEqual(mockKey); expect(comp.keyCount).toBe(2); }); diff --git a/src/test/javascript/spec/service/account.service.spec.ts b/src/test/javascript/spec/service/account.service.spec.ts index 36cf340f2ea7..4c88479d1de7 100644 --- a/src/test/javascript/spec/service/account.service.spec.ts +++ b/src/test/javascript/spec/service/account.service.spec.ts @@ -17,6 +17,8 @@ import { Participation } from 'app/entities/participation/participation.model'; import { Team } from 'app/entities/team.model'; import { SessionStorageService } from 'ngx-webstorage'; import { provideHttpClient } from '@angular/common/http'; +import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; +import dayjs from 'dayjs'; describe('AccountService', () => { let accountService: AccountService; @@ -80,6 +82,15 @@ describe('AccountService', () => { expect(accountService.isAuthenticated()).toBeTrue(); })); + it('should handle user SSH public key correctly', () => { + const sshKey = new UserSshPublicKey(); + sshKey.id = 123; + sshKey.label = 'test-label'; + + expect(sshKey.id).toBe(123); + expect(sshKey.label).toBe('test-label'); + }); + it('should fetch the user on identity if the userIdentity is defined yet (force=true)', fakeAsync(() => { let userReceived: User; accountService.userIdentity = user; @@ -563,4 +574,104 @@ describe('AccountService', () => { expect(fetchStub).not.toHaveBeenCalled(); }); }); + + describe('test SSH and access token related logic', () => { + let userSshPublicKey: UserSshPublicKey; + beforeEach(() => { + userSshPublicKey = new UserSshPublicKey(); + userSshPublicKey.publicKey = 'ssh-key 1234'; + userSshPublicKey.label = 'key 1'; + userSshPublicKey.id = 1; + userSshPublicKey.expiryDate = dayjs().subtract(5, 'day'); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('should send a new SSH public key', () => { + accountService.addNewSshPublicKey(userSshPublicKey).subscribe((response) => { + expect(response.body).toEqual(userSshPublicKey); + }); + + const req = httpMock.expectOne({ method: 'POST', url: 'api/account/ssh-public-key' }); + req.flush({}); + + expect(req.request.method).toBe('POST'); + }); + + it('should retrieve all SSH public keys', () => { + accountService.getAllSshPublicKeys().subscribe(() => {}); + + const req = httpMock.expectOne({ method: 'GET', url: 'api/account/ssh-public-keys' }); + req.flush({}); + expect(req.request.method).toBe('GET'); + }); + + it('should check if user has SSH public keys', () => { + accountService.hasUserSshPublicKeys().subscribe(() => {}); + + const req = httpMock.expectOne({ method: 'GET', url: 'api/account/has-ssh-public-keys' }); + req.flush({}); + expect(req.request.method).toBe('GET'); + }); + + it('should retrieve a specific SSH public key', () => { + const keyId = 1; + accountService.getSshPublicKey(keyId).subscribe(() => {}); + + const req = httpMock.expectOne({ method: 'GET', url: `api/account/ssh-public-key/${keyId}` }); + req.flush({}); + expect(req.request.method).toBe('GET'); + }); + + it('should delete a specific SSH public key', () => { + const keyId = 1; + + accountService.deleteSshPublicKey(keyId).subscribe(() => {}); + + const req = httpMock.expectOne({ method: 'DELETE', url: `api/account/ssh-public-key/${keyId}` }); + req.flush(null); + }); + + it('should delete user VCS access token', () => { + accountService.deleteUserVcsAccessToken().subscribe(() => {}); + + const req = httpMock.expectOne({ method: 'DELETE', url: 'api/account/user-vcs-access-token' }); + req.flush(null); + }); + + it('should add a new VCS access token', () => { + const expiryDate = '2024-10-10'; + + accountService.addNewVcsAccessToken(expiryDate).subscribe((response) => { + expect(response.status).toBe(200); + }); + + const req = httpMock.expectOne({ method: 'PUT', url: `api/account/user-vcs-access-token?expiryDate=${expiryDate}` }); + req.flush({ status: 200 }); + }); + + it('should get VCS access token for a participation', () => { + const participationId = 1; + const token = 'vcs-token'; + + accountService.getVcsAccessToken(participationId).subscribe((response) => { + expect(response.body).toEqual(token); + }); + + const req = httpMock.expectOne({ method: 'GET', url: `api/account/participation-vcs-access-token?participationId=${participationId}` }); + req.flush({ body: token }); + }); + + it('should create VCS access token for a participation', () => { + const participationId = 1; + const token = 'vcs-token'; + + accountService.createVcsAccessToken(participationId).subscribe(() => {}); + + const req = httpMock.expectOne({ method: 'PUT', url: `api/account/participation-vcs-access-token?participationId=${participationId}` }); + req.flush({ body: token }); + }); + }); }); From 207980081a281df3e65749c9165f1745f24b09b5 Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 24 Oct 2024 21:33:01 +0200 Subject: [PATCH 32/47] import correct dayjs --- src/test/javascript/spec/service/account.service.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/javascript/spec/service/account.service.spec.ts b/src/test/javascript/spec/service/account.service.spec.ts index 4c88479d1de7..a964ccc355ae 100644 --- a/src/test/javascript/spec/service/account.service.spec.ts +++ b/src/test/javascript/spec/service/account.service.spec.ts @@ -18,7 +18,7 @@ import { Team } from 'app/entities/team.model'; import { SessionStorageService } from 'ngx-webstorage'; import { provideHttpClient } from '@angular/common/http'; import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; -import dayjs from 'dayjs'; +import dayjs from 'dayjs/esm'; describe('AccountService', () => { let accountService: AccountService; From 2c7e18e2fb8af51f91d28f753c48150dcb75cde4 Mon Sep 17 00:00:00 2001 From: entholzer Date: Fri, 25 Oct 2024 15:29:36 +0200 Subject: [PATCH 33/47] remove unused translation keys --- src/main/webapp/i18n/de/userSettings.json | 1 - src/main/webapp/i18n/en/userSettings.json | 1 - 2 files changed, 2 deletions(-) diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index 274224945f01..563d52898106 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -53,7 +53,6 @@ "expiryDate": "Ablaufdatum", "expiresOn": "Läuft ab am", "hasExpiredOn": "Abgelaufen am", - "leaveEmpty": "Wenn du das Label leer lässt, wird der Kommentar aus dem SSH-Schlüssel verwendet.", "fingerprint": "Fingerabdruck", "commentUsedAsLabel": "Wenn du kein Label hinzufügst, wird der Schlüsselkommentar (sofern vorhanden) als Label verwendet.", "expiry": { diff --git a/src/main/webapp/i18n/en/userSettings.json b/src/main/webapp/i18n/en/userSettings.json index 3e1dd90516f5..1d70f65bd31d 100644 --- a/src/main/webapp/i18n/en/userSettings.json +++ b/src/main/webapp/i18n/en/userSettings.json @@ -53,7 +53,6 @@ "expiryDate": "Expiry date", "expiresOn": "Expires on", "hasExpiredOn": "Expired on", - "leaveEmpty": "If you leave the label empty, the comment from the SSH key will be used.", "fingerprint": "Fingerprint", "commentUsedAsLabel": "If you do not add a label, the key comment will be used as the default label if present.", "expiry": { From 3f179a48b2e078019426d04a5a45c4ac6e082a00 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 26 Oct 2024 16:35:59 +0200 Subject: [PATCH 34/47] added suggestions --- .../aet/artemis/core/web/AccountResource.java | 17 ++++++------- .../programming/domain/UserSshPublicKey.java | 6 ++--- .../service/UserSshPublicKeyService.java | 3 +-- .../webapp/app/core/auth/account.service.ts | 2 +- .../code-button/code-button.component.ts | 4 +-- ...h-user-settings-key-details.component.html | 6 ++--- ...ssh-user-settings-key-details.component.ts | 25 ++++++++----------- .../ssh-user-settings.component.scss | 4 +++ .../ssh-user-settings.component.ts | 23 +++++++---------- 9 files changed, 41 insertions(+), 49 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index f7fed31b2ecc..ff78d56c0875 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -141,7 +141,7 @@ public ResponseEntity changePassword(@RequestBody PasswordChangeDTO passwo /** * GET account/ssh-public-keys : retrieves all SSH keys of a user * - * @return the ResponseEntity containing all public SSH keys of a user with status 200 (OK), or with status 400 (Bad Request) + * @return the ResponseEntity containing all public SSH keys of a user with status 200 (OK) */ @GetMapping("account/ssh-public-keys") @EnforceAtLeastStudent @@ -156,7 +156,8 @@ public ResponseEntity> getSshPublicKeys() { * * @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 400 (Bad Request) + * @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 @@ -169,13 +170,13 @@ public ResponseEntity getSshPublicKey(@PathVariable Long ke /** * 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), or with status 400 (Bad Request) + * @return the ResponseEntity containing true if the User has SSH keys, and false if it does not, with status 200 (OK) */ @GetMapping("account/has-ssh-public-keys") @EnforceAtLeastStudent public ResponseEntity hasUserSSHkeys() { User user = userRepository.getUser(); - Boolean hasKey = userSshPublicKeyService.hasUserSSHkeys(user.getId()); + boolean hasKey = userSshPublicKeyService.hasUserSSHkeys(user.getId()); return ResponseEntity.ok(hasKey); } @@ -184,15 +185,13 @@ public ResponseEntity hasUserSSHkeys() { * * @param sshPublicKey the ssh public key to create * - * @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request) + * @return the ResponseEntity with status 200 (OK), or with status 400 (Bad Request) when the SSH key is malformed, or when a key with the same hash already exists */ @PostMapping("account/ssh-public-key") @EnforceAtLeastStudent public ResponseEntity 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.publicKey()); @@ -200,7 +199,7 @@ public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKeyDTO ssh catch (IllegalArgumentException e) { throw new BadRequestAlertException("Invalid SSH key format", "SSH key", "invalidKeyFormat", true); } - // Extract the PublicKey object + userSshPublicKeyService.createSshKeyForUser(user, keyEntry, sshPublicKey); return ResponseEntity.ok().build(); } @@ -210,7 +209,7 @@ public ResponseEntity addSshPublicKey(@RequestBody UserSshPublicKeyDTO ssh * * @param keyId The id of the key that should be deleted * - * @return the ResponseEntity with status 200 (OK), with status 404 (Not Found), or with status 400 (Bad Request) + * @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/{keyId}") @EnforceAtLeastStudent diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java index ad3b5112c35a..8348c7eb656c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/domain/UserSshPublicKey.java @@ -27,7 +27,7 @@ public class UserSshPublicKey extends DomainObject { */ @NotNull @Column(name = "user_id") - private Long userId; + private long userId; /** * The label of the SSH key shwon in the UI @@ -70,11 +70,11 @@ public class UserSshPublicKey extends DomainObject { @Column(name = "expiry_date") private ZonedDateTime expiryDate = null; - public @NotNull Long getUserId() { + public @NotNull long getUserId() { return userId; } - public void setUserId(@NotNull Long userId) { + public void setUserId(@NotNull long userId) { this.userId = userId; } diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 0ee65e9e0d69..d69d8d91228d 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -133,9 +133,8 @@ public void deleteUserSshPublicKey(Long userId, Long keyId) { * * @param userId the ID of the user. * @return true if the user has SSH keys, false if not - * @throws AccessForbiddenException if the key does not belong to the user. */ - public Boolean hasUserSSHkeys(Long userId) { + public boolean hasUserSSHkeys(Long userId) { return userSshPublicKeyRepository.existsByUserId(userId); } } diff --git a/src/main/webapp/app/core/auth/account.service.ts b/src/main/webapp/app/core/auth/account.service.ts index 9865d17219fb..5307f9a91d5a 100644 --- a/src/main/webapp/app/core/auth/account.service.ts +++ b/src/main/webapp/app/core/auth/account.service.ts @@ -329,7 +329,7 @@ export class AccountService implements IAccountService { /** * Sends the added SSH key to the server * - * @param userSshPublicKey + * @param userSshPublicKey The userSshPublicKey DTO containing the details for the new key which should be created */ addNewSshPublicKey(userSshPublicKey: UserSshPublicKey): Observable> { return this.http.post('api/account/ssh-public-key', userSshPublicKey, { observe: 'response' }); diff --git a/src/main/webapp/app/shared/components/code-button/code-button.component.ts b/src/main/webapp/app/shared/components/code-button/code-button.component.ts index 1113da8c555c..8fba06a21929 100644 --- a/src/main/webapp/app/shared/components/code-button/code-button.component.ts +++ b/src/main/webapp/app/shared/components/code-button/code-button.component.ts @@ -81,9 +81,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { ) {} async ngOnInit() { - const user = await this.accountService.identity(); - - this.user = user!; + this.user = (await this.accountService.identity())!; this.refreshTokenState(); this.copyEnabled = true; diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html index 1c28d0440a37..fb03aa467260 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html @@ -12,7 +12,7 @@

- +

@@ -33,7 +33,7 @@

@if (isCreateMode) {
-

+

{{ copyInstructions }}

@@ -42,7 +42,7 @@

-

+

diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts index 56c67c9d43bc..c7c48dbd3cb6 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { AccountService } from 'app/core/auth/account.service'; import { Subject, Subscription, concatMap, filter, tap } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; @@ -16,13 +16,21 @@ import dayjs from 'dayjs/esm'; styleUrls: ['../../user-settings.scss', '../ssh-user-settings.component.scss'], }) export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { + readonly accountService = inject(AccountService); + readonly route = inject(ActivatedRoute); + readonly router = inject(Router); + readonly alertService = inject(AlertService); + readonly documentationType: DocumentationType = 'SshSetup'; readonly invalidKeyFormat = 'invalidKeyFormat'; readonly keyAlreadyExists = 'keyAlreadyExists'; - subscription: Subscription; + readonly faEdit = faEdit; + readonly faSave = faSave; + protected readonly ButtonType = ButtonType; + protected readonly ButtonSize = ButtonSize; - sshPublicKey: UserSshPublicKey; + subscription: Subscription; // state change variables isCreateMode = false; // true when creating new key, false when viewing existing key @@ -41,20 +49,9 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { displayedLastUsedDate?: dayjs.Dayjs; currentDate: dayjs.Dayjs; - readonly faEdit = faEdit; - readonly faSave = faSave; - protected readonly ButtonType = ButtonType; - protected readonly ButtonSize = ButtonSize; private dialogErrorSource = new Subject(); dialogError$ = this.dialogErrorSource.asObservable(); - constructor( - private accountService: AccountService, - private route: ActivatedRoute, - private router: Router, - private alertService: AlertService, - ) {} - ngOnInit() { this.setMessageBasedOnOS(getOS()); this.currentDate = dayjs(); diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss index b06f95174393..42cced116319 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.scss @@ -19,6 +19,10 @@ textarea { font-size: large; } +.small-text { + font-size: small; +} + .text-and-date { display: flex; gap: 5px; diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts index ab392de6f41b..6a8fa04ff887 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { Component, OnDestroy, OnInit, inject } from '@angular/core'; import { AccountService } from 'app/core/auth/account.service'; import { Subject, tap } from 'rxjs'; import { faEdit, faEllipsis, faSave, faTrash } from '@fortawesome/free-solid-svg-icons'; @@ -6,7 +6,6 @@ import { DocumentationType } from 'app/shared/components/documentation-button/do import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; import { AlertService } from 'app/core/util/alert.service'; import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; -import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; import dayjs from 'dayjs/esm'; @Component({ @@ -15,11 +14,10 @@ import dayjs from 'dayjs/esm'; styleUrls: ['../user-settings.scss', './ssh-user-settings.component.scss'], }) export class SshUserSettingsComponent implements OnInit, OnDestroy { - readonly documentationType: DocumentationType = 'SshSetup'; + private accountService = inject(AccountService); + private alertService = inject(AlertService); - sshPublicKeys: UserSshPublicKey[] = []; - keyCount = 0; - isLoading = true; + readonly documentationType: DocumentationType = 'SshSetup'; readonly faEdit = faEdit; readonly faSave = faSave; @@ -27,17 +25,14 @@ export class SshUserSettingsComponent implements OnInit, OnDestroy { readonly faEllipsis = faEllipsis; protected readonly ButtonType = ButtonType; protected readonly ButtonSize = ButtonSize; - private dialogErrorSource = new Subject(); - currentDate: dayjs.Dayjs; - dialogError$ = this.dialogErrorSource.asObservable(); - @ViewChild('itemsDrop', { static: true }) itemsDrop: NgbDropdown; + sshPublicKeys: UserSshPublicKey[] = []; + keyCount = 0; + isLoading = true; - constructor( - private accountService: AccountService, - private alertService: AlertService, - ) {} + currentDate: dayjs.Dayjs; + dialogError$ = this.dialogErrorSource.asObservable(); ngOnInit() { this.currentDate = dayjs(); From 6807a32009865211ab3df8e3336df7b264ce8205 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 26 Oct 2024 17:14:27 +0200 Subject: [PATCH 35/47] handle case when user inputs too long label --- .../tum/cit/aet/artemis/core/web/AccountResource.java | 3 ++- .../programming/service/UserSshPublicKeyService.java | 11 +++++++---- .../components/code-button/code-button.component.ts | 7 +++---- .../ssh-user-settings-key-details.component.ts | 3 ++- src/main/webapp/i18n/de/userSettings.json | 1 + src/main/webapp/i18n/en/userSettings.json | 1 + 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index ff78d56c0875..0366970a5cd8 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -185,7 +185,8 @@ public ResponseEntity hasUserSSHkeys() { * * @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, or when a key with the same hash already exists + * @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 diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index d69d8d91228d..731e6bd64b8b 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -67,21 +67,24 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP * * @param newSshPublicKey the {@link UserSshPublicKey} for which the label is being set. * @param label the label to assign to the SSH key, or null/empty to use the default logic. + * @throws BadRequestAlertException if the key label is longer than 50 characters */ public void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { if (label == null || label.isEmpty()) { String[] parts = newSshPublicKey.getPublicKey().split("\\s+"); if (parts.length >= 3) { - String labelFromParts = String.join(" ", Arrays.copyOfRange(parts, 2, parts.length)); - newSshPublicKey.setLabel(labelFromParts); + label = String.join(" ", Arrays.copyOfRange(parts, 2, parts.length)); } else { - newSshPublicKey.setLabel(KEY_DEFAULT_LABEL); + label = KEY_DEFAULT_LABEL; } } - else { + if (label.length() <= 50) { newSshPublicKey.setLabel(label); } + else { + throw new BadRequestAlertException("Key label is too long", "SSH key", "keyLabelTooLong", true); + } } /** diff --git a/src/main/webapp/app/shared/components/code-button/code-button.component.ts b/src/main/webapp/app/shared/components/code-button/code-button.component.ts index 8fba06a21929..c823449f95da 100644 --- a/src/main/webapp/app/shared/components/code-button/code-button.component.ts +++ b/src/main/webapp/app/shared/components/code-button/code-button.component.ts @@ -49,7 +49,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { gitlabVCEnabled = false; showCloneUrlWithoutToken = true; copyEnabled? = true; - doesUserHavSSHkeys = false; + doesUserHaveSSHkeys = false; sshKeyMissingTip: string; tokenMissingTip: string; @@ -86,7 +86,6 @@ export class CodeButtonComponent implements OnInit, OnChanges { this.copyEnabled = true; this.useSsh = this.localStorage.retrieve('useSsh') || false; - // console.log(this.useSsh); this.useToken = this.localStorage.retrieve('useToken') || false; this.localStorage.observe('useSsh').subscribe((useSsh) => (this.useSsh = useSsh || false)); this.localStorage.observe('useToken').subscribe((useToken) => (this.useToken = useToken || false)); @@ -98,7 +97,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { this.accountService.hasUserSshPublicKeys().subscribe({ next: (res: boolean) => { - this.doesUserHavSSHkeys = res; + this.doesUserHaveSSHkeys = res; if (this.useSsh) { this.useSshUrl(); } @@ -156,7 +155,7 @@ export class CodeButtonComponent implements OnInit, OnChanges { public useSshUrl() { this.useSsh = true; this.useToken = false; - this.copyEnabled = this.doesUserHavSSHkeys || this.gitlabVCEnabled; + this.copyEnabled = this.doesUserHaveSSHkeys || this.gitlabVCEnabled; this.storeToLocalStorage(); } diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts index c7c48dbd3cb6..7ec0619d9ffc 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -24,6 +24,7 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { readonly documentationType: DocumentationType = 'SshSetup'; readonly invalidKeyFormat = 'invalidKeyFormat'; readonly keyAlreadyExists = 'keyAlreadyExists'; + readonly keyLabelTooLong = 'keyLabelTooLong'; readonly faEdit = faEdit; readonly faSave = faSave; @@ -102,7 +103,7 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { }, error: (error) => { const errorKey = error.error.errorKey; - if (errorKey == this.invalidKeyFormat || errorKey == this.keyAlreadyExists) { + if ([this.invalidKeyFormat, this.keyAlreadyExists, this.keyLabelTooLong].indexOf(errorKey) > -1) { this.alertService.error(`artemisApp.userSettings.sshSettingsPage.${errorKey}`); } else { this.alertService.error('artemisApp.userSettings.sshSettingsPage.saveFailure'); diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index 563d52898106..4d08bc1abb0d 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -24,6 +24,7 @@ "saveSshKey": "Speichern", "invalidKeyFormat": "SSH-Schlüssel konnte nicht gespeichert werden. Stelle sicher, dass es ein gültiger und unterstützter Schlüssel im richtigen Format ist.", "keyAlreadyExists": "SSH-Schlüssel konnte nicht gespeichert werden. Du verwendest diesen Schlüssel schon.", + "keyLabelTooLong": "SSH-Schlüssel konnte nicht gespeichert werden. Das Label darf maximal 50 Zeichen besitzen.", "loadKeyFailure": "Laden der SSH-Schlüssel ist fehlgeschlagen.", "saveFailure": "SSH-Schlüssel konnte nicht gespeichert werden.", "saveSuccess": "SSH-Schlüssel erfolgreich gespeichert.", diff --git a/src/main/webapp/i18n/en/userSettings.json b/src/main/webapp/i18n/en/userSettings.json index 1d70f65bd31d..c42a5c7b1ac3 100644 --- a/src/main/webapp/i18n/en/userSettings.json +++ b/src/main/webapp/i18n/en/userSettings.json @@ -24,6 +24,7 @@ "saveSshKey": "Save", "invalidKeyFormat": "Failed to save SSH key. Make sure the key is in a valid and supported format.", "keyAlreadyExists": "Failed to save SSH key. You already use the provided key.", + "keyLabelTooLong": "Failed to save SSH key. The key label is limited to 50 characters.", "loadKeyFailure": "Failed to load SSH keys.", "saveFailure": "Failed to save SSH key.", "saveSuccess": "Successfully save SSH key.", From 39b14587a4028c386192e6824a3701afa592f6d9 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 26 Oct 2024 17:25:36 +0200 Subject: [PATCH 36/47] improve error handling when deleting key --- .../service/UserSshPublicKeyService.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 731e6bd64b8b..13943246616c 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -93,16 +93,18 @@ public void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { * @param user the {@link User} to whom the SSH key belongs. * @param keyId the ID of the SSH key. * @return the {@link UserSshPublicKey} if found and belongs to the user. - * @throws EntityNotFoundException if the key does not belong to the user. + * @throws AccessForbiddenException if the key does not belong to the user, or does not exist */ public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { - var userSshPublicKey = userSshPublicKeyRepository.findByIdElseThrow(keyId); - if (Objects.equals(userSshPublicKey.getUserId(), user.getId())) { - return userSshPublicKey; + try { + var userSshPublicKey = userSshPublicKeyRepository.findByIdElseThrow(keyId); + if (Objects.equals(userSshPublicKey.getUserId(), user.getId())) { + return userSshPublicKey; + } } - else { - throw new AccessForbiddenException("SSH key", keyId); + catch (EntityNotFoundException ignored) { } + throw new AccessForbiddenException("SSH key", keyId); } /** From 1038cf2b8575c82ba218daeedc91d56577bf9368 Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 26 Oct 2024 17:32:16 +0200 Subject: [PATCH 37/47] add code rabbit suggestions --- .../de/tum/cit/aet/artemis/core/web/AccountResource.java | 6 +++--- .../programming/service/UserSshPublicKeyService.java | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java index 0366970a5cd8..37a9e088f5f6 100644 --- a/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/core/web/AccountResource.java @@ -147,7 +147,7 @@ public ResponseEntity changePassword(@RequestBody PasswordChangeDTO passwo @EnforceAtLeastStudent public ResponseEntity> getSshPublicKeys() { User user = userRepository.getUser(); - List keys = userSshPublicKeyService.getAllSshKeysForUser(user).stream().map(UserSshPublicKeyDTO::of).toList(); + List keys = userSshPublicKeyService.getAllSshKeysForUser(user); return ResponseEntity.ok(keys); } @@ -176,8 +176,8 @@ public ResponseEntity getSshPublicKey(@PathVariable Long ke @EnforceAtLeastStudent public ResponseEntity hasUserSSHkeys() { User user = userRepository.getUser(); - boolean hasKey = userSshPublicKeyService.hasUserSSHkeys(user.getId()); - return ResponseEntity.ok(hasKey); + boolean hasKeys = userSshPublicKeyService.hasUserSSHkeys(user.getId()); + return ResponseEntity.ok(hasKeys); } /** diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 13943246616c..553a68594e40 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -69,7 +69,7 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP * @param label the label to assign to the SSH key, or null/empty to use the default logic. * @throws BadRequestAlertException if the key label is longer than 50 characters */ - public void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { + private void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { if (label == null || label.isEmpty()) { String[] parts = newSshPublicKey.getPublicKey().split("\\s+"); if (parts.length >= 3) { @@ -113,8 +113,8 @@ public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { * @param user the {@link User} whose SSH keys are to be retrieved. * @return a list of {@link UserSshPublicKey} objects for the user. */ - public List getAllSshKeysForUser(User user) { - return userSshPublicKeyRepository.findAllByUserId(user.getId()); + public List getAllSshKeysForUser(User user) { + return userSshPublicKeyRepository.findAllByUserId(user.getId()).stream().map(UserSshPublicKeyDTO::of).toList(); } /** From 44b33ec6bb3684f33286b8a978feaf302a8a399c Mon Sep 17 00:00:00 2001 From: entholzer Date: Sat, 26 Oct 2024 17:38:23 +0200 Subject: [PATCH 38/47] add code rabbit suggestions --- .../artemis/programming/service/UserSshPublicKeyService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 553a68594e40..472eab401c82 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Objects; +import org.apache.commons.lang3.StringUtils; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Service; @@ -70,7 +71,7 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP * @throws BadRequestAlertException if the key label is longer than 50 characters */ private void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { - if (label == null || label.isEmpty()) { + if (StringUtils.isBlank(label)) { String[] parts = newSshPublicKey.getPublicKey().split("\\s+"); if (parts.length >= 3) { label = String.join(" ", Arrays.copyOfRange(parts, 2, parts.length)); From 6086881d8b077480e15f29bce17287a1b5d78b68 Mon Sep 17 00:00:00 2001 From: entholzer Date: Mon, 28 Oct 2024 17:05:18 +0100 Subject: [PATCH 39/47] fix expected http error code in test --- .../de/tum/cit/aet/artemis/core/user/util/UserTestService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java index 7baa9b8114a3..aa95dbf6d7a2 100644 --- a/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java +++ b/src/test/java/de/tum/cit/aet/artemis/core/user/util/UserTestService.java @@ -950,7 +950,7 @@ public void failToAddOrDeleteWithInvalidKeyId() throws Exception { userSshPublicKeyRepository.save(userKey); request.delete("/api/account/ssh-public-key/3443", HttpStatus.FORBIDDEN); - request.get("/api/account/ssh-public-key/43443", HttpStatus.NOT_FOUND, UserSshPublicKeyDTO.class); + request.get("/api/account/ssh-public-key/43443", HttpStatus.FORBIDDEN, UserSshPublicKeyDTO.class); request.get("/api/account/ssh-public-key/" + userKey.getId(), HttpStatus.FORBIDDEN, UserSshPublicKeyDTO.class); } From 46a755cc5b6f28ec9db0a8610b448b55ce736e76 Mon Sep 17 00:00:00 2001 From: Simon Entholzer <33342534+SimonEntholzer@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:59:55 +0100 Subject: [PATCH 40/47] Update src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Johannes Stöhr <38322605+JohannesStoehr@users.noreply.github.com> --- .../programming/service/UserSshPublicKeyService.java | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 472eab401c82..5726777b8285 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -97,15 +97,8 @@ private void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { * @throws AccessForbiddenException if the key does not belong to the user, or does not exist */ public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { - try { - var userSshPublicKey = userSshPublicKeyRepository.findByIdElseThrow(keyId); - if (Objects.equals(userSshPublicKey.getUserId(), user.getId())) { - return userSshPublicKey; - } - } - catch (EntityNotFoundException ignored) { - } - throw new AccessForbiddenException("SSH key", keyId); + Optional< UserSshPublicKey> userSshPublicKey = userSshPublicKeyRepository.findByIdAndUserId(keyId, user.getId()); + return userSshPublicKey.orThrow(new AccessForbiddenException("SSH key", keyId)); } /** From 4d521ef54b225b898b4ab159893e99a3b0482ae5 Mon Sep 17 00:00:00 2001 From: entholzer Date: Tue, 29 Oct 2024 13:14:35 +0100 Subject: [PATCH 41/47] add code suggestions --- .../repository/UserSshPublicKeyRepository.java | 2 ++ .../programming/service/UserSshPublicKeyService.java | 10 ++++++---- .../ssh-user-settings-key-details.component.html | 2 +- .../details/ssh-user-settings-key-details.component.ts | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java index 086b4b22b4ef..b177b7b72089 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/repository/UserSshPublicKeyRepository.java @@ -19,6 +19,8 @@ public interface UserSshPublicKeyRepository extends ArtemisJpaRepository findByKeyHash(String keyHash); + Optional findByIdAndUserId(Long keyId, Long userId); + boolean existsByIdAndUserId(Long id, Long userId); boolean existsByUserId(Long userId); diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java index 5726777b8285..4212cac6c4ce 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/UserSshPublicKeyService.java @@ -8,7 +8,7 @@ import java.time.ZonedDateTime; import java.util.Arrays; import java.util.List; -import java.util.Objects; +import java.util.Optional; import org.apache.commons.lang3.StringUtils; import org.apache.sshd.common.config.keys.AuthorizedKeyEntry; @@ -18,7 +18,6 @@ import de.tum.cit.aet.artemis.core.domain.User; import de.tum.cit.aet.artemis.core.exception.AccessForbiddenException; import de.tum.cit.aet.artemis.core.exception.BadRequestAlertException; -import de.tum.cit.aet.artemis.core.exception.EntityNotFoundException; 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.repository.UserSshPublicKeyRepository; @@ -73,6 +72,9 @@ public void createSshKeyForUser(User user, AuthorizedKeyEntry keyEntry, UserSshP private void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { if (StringUtils.isBlank(label)) { String[] parts = newSshPublicKey.getPublicKey().split("\\s+"); + + // we are only interested in the comment of the key. A typical key looks like this, the key prefix, the actual key and then the comment: + // ssh-rsa AAAAB3NzaC1yc2EAAAADAYVTLQ== comment if (parts.length >= 3) { label = String.join(" ", Arrays.copyOfRange(parts, 2, parts.length)); } @@ -97,8 +99,8 @@ private void setLabelForKey(UserSshPublicKey newSshPublicKey, String label) { * @throws AccessForbiddenException if the key does not belong to the user, or does not exist */ public UserSshPublicKey getSshKeyForUser(User user, Long keyId) { - Optional< UserSshPublicKey> userSshPublicKey = userSshPublicKeyRepository.findByIdAndUserId(keyId, user.getId()); - return userSshPublicKey.orThrow(new AccessForbiddenException("SSH key", keyId)); + Optional userSshPublicKey = userSshPublicKeyRepository.findByIdAndUserId(keyId, user.getId()); + return userSshPublicKey.orElseThrow(() -> new AccessForbiddenException("SSH key", keyId)); } /** diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html index fb03aa467260..d1ecbd74bbc3 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.html @@ -41,7 +41,7 @@

- +

diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts index 7ec0619d9ffc..fe12375d324e 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component.ts @@ -26,8 +26,8 @@ export class SshUserSettingsKeyDetailsComponent implements OnInit, OnDestroy { readonly keyAlreadyExists = 'keyAlreadyExists'; readonly keyLabelTooLong = 'keyLabelTooLong'; - readonly faEdit = faEdit; - readonly faSave = faSave; + protected readonly faEdit = faEdit; + protected readonly faSave = faSave; protected readonly ButtonType = ButtonType; protected readonly ButtonSize = ButtonSize; From e06bf0b5dbf02125fd607dfd5ebef1c0a12a77d4 Mon Sep 17 00:00:00 2001 From: entholzer Date: Wed, 30 Oct 2024 22:50:49 +0100 Subject: [PATCH 42/47] remove empty line from master.xml from merge conflict --- src/main/resources/config/liquibase/master.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/resources/config/liquibase/master.xml b/src/main/resources/config/liquibase/master.xml index 1a8a8fa64304..eac4d911931a 100644 --- a/src/main/resources/config/liquibase/master.xml +++ b/src/main/resources/config/liquibase/master.xml @@ -28,9 +28,8 @@ - + - From 0de92c33e9ed77b94476afc50575127daa7b2641 Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 31 Oct 2024 20:41:02 +0100 Subject: [PATCH 43/47] server code --- .../ssh/SshFingerprintsProviderService.java | 47 +++++++++++++++++++ .../ssh/SshFingerprintsProviderResource.java | 45 ++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/ssh/SshFingerprintsProviderService.java create mode 100644 src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshFingerprintsProviderResource.java diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/ssh/SshFingerprintsProviderService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/ssh/SshFingerprintsProviderService.java new file mode 100644 index 000000000000..22b868c8cc58 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/ssh/SshFingerprintsProviderService.java @@ -0,0 +1,47 @@ +package de.tum.cit.aet.artemis.programming.service.localvc.ssh; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALVC; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.server.SshServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; + +/** + * This class configures the JGit Servlet, which is used to receive Git push and fetch requests for local VC. + */ +@Profile(PROFILE_LOCALVC) +@Service +public class SshFingerprintsProviderService { + + private static final Logger log = LoggerFactory.getLogger(SshFingerprintsProviderService.class); + + private final SshServer sshServer; + + public SshFingerprintsProviderService(SshServer sshServer) { + this.sshServer = sshServer; + } + + public Map getSshFingerPrints() { + Map fingerprints = new HashMap<>(); + KeyPairProvider keyPairProvider = sshServer.getKeyPairProvider(); + if (keyPairProvider != null) { + try { + keyPairProvider.loadKeys(null).iterator() + .forEachRemaining(keyPair -> fingerprints.put(keyPair.getPublic().getAlgorithm(), HashUtils.getSha512Fingerprint(keyPair.getPublic()))); + + } + catch (IOException | GeneralSecurityException e) { + log.info("Could not load keys from the ssh server while trying to get SSH key fingerprints", e); + } + } + return fingerprints; + } +} diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshFingerprintsProviderResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshFingerprintsProviderResource.java new file mode 100644 index 000000000000..0bd2d70f8b64 --- /dev/null +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshFingerprintsProviderResource.java @@ -0,0 +1,45 @@ +package de.tum.cit.aet.artemis.programming.web.localvc.ssh; + +import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALVC; + +import java.io.IOException; +import java.util.Map; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import de.tum.cit.aet.artemis.core.security.annotations.EnforceNothing; +import de.tum.cit.aet.artemis.core.service.feature.Feature; +import de.tum.cit.aet.artemis.core.service.feature.FeatureToggle; +import de.tum.cit.aet.artemis.programming.service.localvc.ssh.SshFingerprintsProviderService; + +/** + * REST controller for managing. + */ +@Profile(PROFILE_LOCALVC) +@RestController +@RequestMapping("api/") +public class SshFingerprintsProviderResource { + + SshFingerprintsProviderService sshFingerprintsProviderService; + + SshFingerprintsProviderResource(SshFingerprintsProviderService sshFingerprintsProviderService) { + this.sshFingerprintsProviderService = sshFingerprintsProviderService; + } + + /** + * GET /ssh-fingerprints + * + * @return the SSH fingerprints for the keys a user uses + */ + @GetMapping(value = "ssh-fingerprints", produces = MediaType.APPLICATION_JSON_VALUE) + @EnforceNothing + @FeatureToggle(Feature.Exports) + public ResponseEntity> getSshFingerprints() throws IOException { + return ResponseEntity.ok().body(sshFingerprintsProviderService.getSshFingerPrints()); + } +} From 73d96036e05d8df20e2908bcaf5824a046e2ded9 Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 31 Oct 2024 20:42:23 +0100 Subject: [PATCH 44/47] client code --- .../ssh-user-settings-fingerprints.component.html | 0 .../ssh-settings/ssh-user-settings.service.ts | 14 ++++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.html create mode 100644 src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.service.ts diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.html new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.service.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.service.ts new file mode 100644 index 000000000000..56680203b9bf --- /dev/null +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@angular/core'; +import { firstValueFrom } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; + +@Injectable({ providedIn: 'root' }) +export class SshUserSettingsService { + error?: string; + + constructor(private http: HttpClient) {} + + public async getSshFingerprints(): Promise<{ [key: string]: string }> { + return await firstValueFrom(this.http.get<{ [key: string]: string }>('api/ssh-fingerprints')); + } +} From d1ff8da221d145cb2d3d799712f48119e49dd42f Mon Sep 17 00:00:00 2001 From: entholzer Date: Thu, 31 Oct 2024 22:22:52 +0100 Subject: [PATCH 45/47] added fingerprints sub page --- ...-user-settings-fingerprints.component.html | 56 +++++++++++++++++++ ...-user-settings-fingerprints.component.scss | 12 ++++ ...sh-user-settings-fingerprints.component.ts | 27 +++++++++ ...ssh-user-settings-fingerprints.service.ts} | 2 +- .../ssh-user-settings.component.html | 7 ++- .../user-settings/user-settings.module.ts | 2 + .../user-settings/user-settings.route.ts | 8 +++ src/main/webapp/i18n/de/userSettings.json | 4 ++ src/main/webapp/i18n/en/userSettings.json | 4 ++ 9 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.scss create mode 100644 src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.ts rename src/main/webapp/app/shared/user-settings/ssh-settings/{ssh-user-settings.service.ts => ssh-user-settings-fingerprints.service.ts} (89%) diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.html index e69de29bb2d1..44df3d891ced 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.html @@ -0,0 +1,56 @@ +

+ +
diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.scss b/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.scss new file mode 100644 index 000000000000..61b3ac821994 --- /dev/null +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.scss @@ -0,0 +1,12 @@ +.column { + float: left; + padding: 10px; +} + +.left { + width: 15%; +} + +.right { + width: 85%; +} diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.ts new file mode 100644 index 000000000000..9423779c5bc6 --- /dev/null +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { SshUserSettingsFingerprintsService } from 'app/shared/user-settings/ssh-settings/ssh-user-settings-fingerprints.service'; +import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; +import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; +import { filterInvalidFeedback } from 'app/exercises/modeling/assess/modeling-assessment.util'; + +@Component({ + selector: 'jhi-account-information', + templateUrl: './ssh-user-settings-fingerprints.component.html', + styleUrls: ['./ssh-user-settings-fingerprints.component.scss', '../ssh-user-settings.component.scss'], +}) +export class SshUserSettingsFingerprintsComponent implements OnInit { + readonly sshUserSettingsService = inject(SshUserSettingsFingerprintsService); + + protected sshFingerprints?: { [key: string]: string }; + + readonly documentationType: DocumentationType = 'SshSetup'; + protected readonly ButtonType = ButtonType; + + protected readonly ButtonSize = ButtonSize; + + async ngOnInit() { + this.sshFingerprints = await this.sshUserSettingsService.getSshFingerprints(); + } + + protected readonly filterInvalidFeedback = filterInvalidFeedback; +} diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.service.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings-fingerprints.service.ts similarity index 89% rename from src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.service.ts rename to src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings-fingerprints.service.ts index 56680203b9bf..085386a5cb57 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.service.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings-fingerprints.service.ts @@ -3,7 +3,7 @@ import { firstValueFrom } from 'rxjs'; import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) -export class SshUserSettingsService { +export class SshUserSettingsFingerprintsService { error?: string; constructor(private http: HttpClient) {} diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html index a5650cef05a9..be96fb1808f4 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user-settings.component.html @@ -27,7 +27,7 @@

-
+

@@ -103,6 +103,11 @@

+
+ + + +
diff --git a/src/main/webapp/app/shared/user-settings/user-settings.module.ts b/src/main/webapp/app/shared/user-settings/user-settings.module.ts index 11c876830750..e9c2ed73395e 100644 --- a/src/main/webapp/app/shared/user-settings/user-settings.module.ts +++ b/src/main/webapp/app/shared/user-settings/user-settings.module.ts @@ -13,6 +13,7 @@ import { FormDateTimePickerModule } from 'app/shared/date-time-picker/date-time- import { IdeSettingsComponent } from 'app/shared/user-settings/ide-preferences/ide-settings.component'; import { DocumentationLinkComponent } from 'app/shared/components/documentation-link/documentation-link.component'; import { SshUserSettingsKeyDetailsComponent } from 'app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component'; +import { SshUserSettingsFingerprintsComponent } from 'app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component'; @NgModule({ imports: [RouterModule.forChild(userSettingsState), ArtemisSharedModule, ArtemisSharedComponentModule, ClipboardModule, FormDateTimePickerModule, DocumentationLinkComponent], @@ -22,6 +23,7 @@ import { SshUserSettingsKeyDetailsComponent } from 'app/shared/user-settings/ssh ScienceSettingsComponent, SshUserSettingsComponent, SshUserSettingsKeyDetailsComponent, + SshUserSettingsFingerprintsComponent, VcsAccessTokensSettingsComponent, IdeSettingsComponent, ], diff --git a/src/main/webapp/app/shared/user-settings/user-settings.route.ts b/src/main/webapp/app/shared/user-settings/user-settings.route.ts index 41d8a2084df7..cbd0278d3bde 100644 --- a/src/main/webapp/app/shared/user-settings/user-settings.route.ts +++ b/src/main/webapp/app/shared/user-settings/user-settings.route.ts @@ -9,6 +9,7 @@ import { SshUserSettingsComponent } from 'app/shared/user-settings/ssh-settings/ import { VcsAccessTokensSettingsComponent } from 'app/shared/user-settings/vcs-access-tokens-settings/vcs-access-tokens-settings.component'; import { IdeSettingsComponent } from 'app/shared/user-settings/ide-preferences/ide-settings.component'; import { SshUserSettingsKeyDetailsComponent } from 'app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component'; +import { SshUserSettingsFingerprintsComponent } from 'app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component'; export const userSettingsState: Routes = [ { @@ -60,6 +61,13 @@ export const userSettingsState: Routes = [ pageTitle: 'artemisApp.userSettings.categories.SSH_SETTINGS', }, }, + { + path: 'ssh/fingerprints', + component: SshUserSettingsFingerprintsComponent, + data: { + pageTitle: 'artemisApp.userSettings.categories.SSH_SETTINGS', + }, + }, { path: 'ssh/view/:keyId', component: SshUserSettingsKeyDetailsComponent, diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index 4d08bc1abb0d..84953e9f1707 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -55,6 +55,10 @@ "expiresOn": "Läuft ab am", "hasExpiredOn": "Abgelaufen am", "fingerprint": "Fingerabdruck", + "fingerprints": "Fingerabdrücke", + "sshFingerprints": "SSH Fingerabdrücke", + "fingerprintsExplanation": "Mit SSH-Schlüsseln kannst du eine sichere Verbindung zwischen deinem Computer und Artemis herstellen. SSH-Fingerabdrücke stellen sicher, dass der Client eine Verbindung zum richtigen Host herstellt.", + "fingerprintsLearnMore": "Lerne mehr über Fingerabdrücke", "commentUsedAsLabel": "Wenn du kein Label hinzufügst, wird der Schlüsselkommentar (sofern vorhanden) als Label verwendet.", "expiry": { "title": "Ablauf", diff --git a/src/main/webapp/i18n/en/userSettings.json b/src/main/webapp/i18n/en/userSettings.json index c42a5c7b1ac3..5bf3c30161da 100644 --- a/src/main/webapp/i18n/en/userSettings.json +++ b/src/main/webapp/i18n/en/userSettings.json @@ -55,6 +55,10 @@ "expiresOn": "Expires on", "hasExpiredOn": "Expired on", "fingerprint": "Fingerprint", + "fingerprints": "Fingerprints", + "sshFingerprints": "SSH Fingerprints", + "fingerprintsExplanation": "SSH keys allow you to establish a secure connection between your computer and Artemis. SSH fingerprints verify that the client is connecting to the correct host.", + "fingerprintsLearnMore": "Learn more about fingerprints", "commentUsedAsLabel": "If you do not add a label, the key comment will be used as the default label if present.", "expiry": { "title": "Expiry", From 76d4856a39191f78f92f50e64e6712584b65b140 Mon Sep 17 00:00:00 2001 From: entholzer Date: Fri, 1 Nov 2024 18:27:31 +0100 Subject: [PATCH 46/47] added serve tests --- .../ssh/SshFingerprintsProviderService.java | 3 + .../ssh/SshFingerprintsProviderResource.java | 9 +-- ...shFingerprintsProviderIntegrationTest.java | 77 +++++++++++++++++++ 3 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 src/test/java/de/tum/cit/aet/artemis/programming/icl/SshFingerprintsProviderIntegrationTest.java diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/ssh/SshFingerprintsProviderService.java b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/ssh/SshFingerprintsProviderService.java index 22b868c8cc58..5d1db5b15e73 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/ssh/SshFingerprintsProviderService.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/service/localvc/ssh/SshFingerprintsProviderService.java @@ -7,6 +7,8 @@ import java.util.HashMap; import java.util.Map; +import jakarta.ws.rs.BadRequestException; + import org.apache.sshd.common.keyprovider.KeyPairProvider; import org.apache.sshd.server.SshServer; import org.slf4j.Logger; @@ -40,6 +42,7 @@ public Map getSshFingerPrints() { } catch (IOException | GeneralSecurityException e) { log.info("Could not load keys from the ssh server while trying to get SSH key fingerprints", e); + throw new BadRequestException("Could not load keys from the ssh server"); } } return fingerprints; diff --git a/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshFingerprintsProviderResource.java b/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshFingerprintsProviderResource.java index 0bd2d70f8b64..eaaf3d345fb7 100644 --- a/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshFingerprintsProviderResource.java +++ b/src/main/java/de/tum/cit/aet/artemis/programming/web/localvc/ssh/SshFingerprintsProviderResource.java @@ -2,7 +2,6 @@ import static de.tum.cit.aet.artemis.core.config.Constants.PROFILE_LOCALVC; -import java.io.IOException; import java.util.Map; import org.springframework.context.annotation.Profile; @@ -12,7 +11,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import de.tum.cit.aet.artemis.core.security.annotations.EnforceNothing; +import de.tum.cit.aet.artemis.core.security.annotations.EnforceAtLeastStudent; import de.tum.cit.aet.artemis.core.service.feature.Feature; import de.tum.cit.aet.artemis.core.service.feature.FeatureToggle; import de.tum.cit.aet.artemis.programming.service.localvc.ssh.SshFingerprintsProviderService; @@ -27,7 +26,7 @@ public class SshFingerprintsProviderResource { SshFingerprintsProviderService sshFingerprintsProviderService; - SshFingerprintsProviderResource(SshFingerprintsProviderService sshFingerprintsProviderService) { + public SshFingerprintsProviderResource(SshFingerprintsProviderService sshFingerprintsProviderService) { this.sshFingerprintsProviderService = sshFingerprintsProviderService; } @@ -37,9 +36,9 @@ public class SshFingerprintsProviderResource { * @return the SSH fingerprints for the keys a user uses */ @GetMapping(value = "ssh-fingerprints", produces = MediaType.APPLICATION_JSON_VALUE) - @EnforceNothing + @EnforceAtLeastStudent @FeatureToggle(Feature.Exports) - public ResponseEntity> getSshFingerprints() throws IOException { + public ResponseEntity> getSshFingerprints() { return ResponseEntity.ok().body(sshFingerprintsProviderService.getSshFingerPrints()); } } diff --git a/src/test/java/de/tum/cit/aet/artemis/programming/icl/SshFingerprintsProviderIntegrationTest.java b/src/test/java/de/tum/cit/aet/artemis/programming/icl/SshFingerprintsProviderIntegrationTest.java new file mode 100644 index 000000000000..7eb80589dfc4 --- /dev/null +++ b/src/test/java/de/tum/cit/aet/artemis/programming/icl/SshFingerprintsProviderIntegrationTest.java @@ -0,0 +1,77 @@ +package de.tum.cit.aet.artemis.programming.icl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Collections; +import java.util.Map; + +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.server.SshServer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.security.test.context.support.WithMockUser; + +import de.tum.cit.aet.artemis.programming.service.localvc.ssh.HashUtils; +import de.tum.cit.aet.artemis.shared.base.AbstractSpringIntegrationLocalCILocalVCTest; + +public class SshFingerprintsProviderIntegrationTest extends AbstractSpringIntegrationLocalCILocalVCTest { + + private static final String TEST_PREFIX = "sshFingerprintsTest"; + + @MockBean + private SshServer sshServer; + + @Mock + private KeyPairProvider keyPairProvider; + + private String expectedFingerprint; + + @BeforeEach + void setup() throws Exception { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair testKeyPair = keyPairGenerator.generateKeyPair(); + + expectedFingerprint = HashUtils.getSha512Fingerprint(testKeyPair.getPublic()); + doReturn(keyPairProvider).when(sshServer).getKeyPairProvider(); + doReturn(Collections.singleton(testKeyPair)).when(keyPairProvider).loadKeys(null); + } + + @Nested + class SshFingerprintsProvider { + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void shouldReturnFingerprints() throws Exception { + var response = request.get("/api/ssh-fingerprints", HttpStatus.OK, Map.class); + assertThat(response.get("RSA")).isNotNull(); + assertThat(response.get("RSA")).isEqualTo(expectedFingerprint); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void shouldReturnNoFingerprintsWithoutKeyProviderSetup() throws Exception { + doReturn(null).when(sshServer).getKeyPairProvider(); + + var response = request.get("/api/ssh-fingerprints", HttpStatus.OK, Map.class); + assertThat(response.isEmpty()).isTrue(); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "student1", roles = "USER") + void shouldReturnBadRequestWhenLoadKeysThrowsException() throws Exception { + doThrow(new IOException("Test exception")).when(keyPairProvider).loadKeys(null); + + request.get("/api/ssh-fingerprints", HttpStatus.BAD_REQUEST, Map.class); + } + } +} From 9c5cc4d6f325e3da4c58e5ca6c1be7fefb550bf6 Mon Sep 17 00:00:00 2001 From: entholzer Date: Fri, 1 Nov 2024 19:58:10 +0100 Subject: [PATCH 47/47] added jest tests --- ...sh-user-settings-fingerprints.component.ts | 3 -- ...er-settings-fingerprints.component.spec.ts | 44 +++++++++++++++++++ ...ser-settings-key-details.component.spec.ts | 8 ++-- .../ssh-user-settings.component.spec.ts | 4 +- ...user-settings-fingerprints.service.spec.ts | 29 ++++++++++++ 5 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 src/test/javascript/spec/component/account/ssh/ssh-user-settings-fingerprints.component.spec.ts rename src/test/javascript/spec/component/account/{ => ssh}/ssh-user-settings-key-details.component.spec.ts (95%) rename src/test/javascript/spec/component/account/{ => ssh}/ssh-user-settings.component.spec.ts (95%) create mode 100644 src/test/javascript/spec/service/ssh-user-settings-fingerprints.service.spec.ts diff --git a/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.ts b/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.ts index 9423779c5bc6..f33d4ccaf34f 100644 --- a/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.ts +++ b/src/main/webapp/app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit, inject } from '@angular/core'; import { SshUserSettingsFingerprintsService } from 'app/shared/user-settings/ssh-settings/ssh-user-settings-fingerprints.service'; import { ButtonSize, ButtonType } from 'app/shared/components/button.component'; import { DocumentationType } from 'app/shared/components/documentation-button/documentation-button.component'; -import { filterInvalidFeedback } from 'app/exercises/modeling/assess/modeling-assessment.util'; @Component({ selector: 'jhi-account-information', @@ -22,6 +21,4 @@ export class SshUserSettingsFingerprintsComponent implements OnInit { async ngOnInit() { this.sshFingerprints = await this.sshUserSettingsService.getSshFingerprints(); } - - protected readonly filterInvalidFeedback = filterInvalidFeedback; } diff --git a/src/test/javascript/spec/component/account/ssh/ssh-user-settings-fingerprints.component.spec.ts b/src/test/javascript/spec/component/account/ssh/ssh-user-settings-fingerprints.component.spec.ts new file mode 100644 index 000000000000..4e943cb67e5a --- /dev/null +++ b/src/test/javascript/spec/component/account/ssh/ssh-user-settings-fingerprints.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ArtemisTestModule } from '../../../test.module'; +import { TranslatePipeMock } from '../../../helpers/mocks/service/mock-translate.service'; +import { TranslateService } from '@ngx-translate/core'; +import { SshUserSettingsFingerprintsComponent } from 'app/shared/user-settings/ssh-settings/fingerprints/ssh-user-settings-fingerprints.component'; +import { SshUserSettingsFingerprintsService } from 'app/shared/user-settings/ssh-settings/ssh-user-settings-fingerprints.service'; + +describe('SshUserSettingsFingerprintsComponent', () => { + let fixture: ComponentFixture; + let comp: SshUserSettingsFingerprintsComponent; + const mockFingerprints: { [key: string]: string } = { + RSA: 'SHA512:abcde123', + }; + + let fingerPintsServiceMock: { + getSshFingerprints: jest.Mock; + }; + let translateService: TranslateService; + + beforeEach(async () => { + fingerPintsServiceMock = { + getSshFingerprints: jest.fn(), + }; + jest.spyOn(console, 'error').mockImplementation(() => {}); + await TestBed.configureTestingModule({ + imports: [ArtemisTestModule], + declarations: [SshUserSettingsFingerprintsComponent, TranslatePipeMock], + providers: [{ provide: SshUserSettingsFingerprintsService, useValue: fingerPintsServiceMock }], + }).compileComponents(); + fixture = TestBed.createComponent(SshUserSettingsFingerprintsComponent); + comp = fixture.componentInstance; + translateService = TestBed.inject(TranslateService); + translateService.currentLang = 'en'; + + fingerPintsServiceMock.getSshFingerprints.mockImplementation(() => Promise.resolve(mockFingerprints)); + }); + + it('should display fingerprints', async () => { + await comp.ngOnInit(); + await fixture.whenStable(); + + expect(fingerPintsServiceMock.getSshFingerprints).toHaveBeenCalled(); + }); +}); diff --git a/src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts b/src/test/javascript/spec/component/account/ssh/ssh-user-settings-key-details.component.spec.ts similarity index 95% rename from src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts rename to src/test/javascript/spec/component/account/ssh/ssh-user-settings-key-details.component.spec.ts index 1b96b78f3a32..638116e428cb 100644 --- a/src/test/javascript/spec/component/account/ssh-user-settings-key-details.component.spec.ts +++ b/src/test/javascript/spec/component/account/ssh/ssh-user-settings-key-details.component.spec.ts @@ -3,13 +3,13 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AccountService } from 'app/core/auth/account.service'; import { of, throwError } from 'rxjs'; import { ActivatedRoute, Router } from '@angular/router'; -import { ArtemisTestModule } from '../../test.module'; -import { MockNgbModalService } from '../../helpers/mocks/service/mock-ngb-modal.service'; -import { MockTranslateService, TranslatePipeMock } from '../../helpers/mocks/service/mock-translate.service'; +import { ArtemisTestModule } from '../../../test.module'; +import { MockNgbModalService } from '../../../helpers/mocks/service/mock-ngb-modal.service'; +import { MockTranslateService, TranslatePipeMock } from '../../../helpers/mocks/service/mock-translate.service'; import { TranslateService } from '@ngx-translate/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { SshUserSettingsKeyDetailsComponent } from 'app/shared/user-settings/ssh-settings/details/ssh-user-settings-key-details.component'; -import { MockActivatedRoute } from '../../helpers/mocks/activated-route/mock-activated-route'; +import { MockActivatedRoute } from '../../../helpers/mocks/activated-route/mock-activated-route'; import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; import dayjs from 'dayjs/esm'; import { AlertService } from 'app/core/util/alert.service'; diff --git a/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts b/src/test/javascript/spec/component/account/ssh/ssh-user-settings.component.spec.ts similarity index 95% rename from src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts rename to src/test/javascript/spec/component/account/ssh/ssh-user-settings.component.spec.ts index 07a46ad53d4f..83500a6a421e 100644 --- a/src/test/javascript/spec/component/account/ssh-user-settings.component.spec.ts +++ b/src/test/javascript/spec/component/account/ssh/ssh-user-settings.component.spec.ts @@ -2,8 +2,8 @@ import { HttpResponse } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AccountService } from 'app/core/auth/account.service'; import { of, throwError } from 'rxjs'; -import { ArtemisTestModule } from '../../test.module'; -import { MockTranslateService, TranslatePipeMock } from '../../helpers/mocks/service/mock-translate.service'; +import { ArtemisTestModule } from '../../../test.module'; +import { MockTranslateService, TranslatePipeMock } from '../../../helpers/mocks/service/mock-translate.service'; import { TranslateService } from '@ngx-translate/core'; import { SshUserSettingsComponent } from 'app/shared/user-settings/ssh-settings/ssh-user-settings.component'; import { UserSshPublicKey } from 'app/entities/programming/user-ssh-public-key.model'; diff --git a/src/test/javascript/spec/service/ssh-user-settings-fingerprints.service.spec.ts b/src/test/javascript/spec/service/ssh-user-settings-fingerprints.service.spec.ts new file mode 100644 index 000000000000..36d3d3ddf841 --- /dev/null +++ b/src/test/javascript/spec/service/ssh-user-settings-fingerprints.service.spec.ts @@ -0,0 +1,29 @@ +import { TestBed, fakeAsync } from '@angular/core/testing'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { provideHttpClient } from '@angular/common/http'; +import { SshUserSettingsFingerprintsService } from 'app/shared/user-settings/ssh-settings/ssh-user-settings-fingerprints.service'; + +describe('SshUserSettingsFingerprintsService', () => { + let sshFingerprintsService: SshUserSettingsFingerprintsService; + let httpMock: HttpTestingController; + + const getUserUrl = 'api/ssh-fingerprints'; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideHttpClient(), provideHttpClientTesting()], + }); + sshFingerprintsService = TestBed.inject(SshUserSettingsFingerprintsService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + jest.restoreAllMocks(); + }); + + it('should get SSH fingerprints', fakeAsync(() => { + sshFingerprintsService.getSshFingerprints(); + httpMock.expectOne({ method: 'GET', url: getUserUrl }); + })); +});