Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrated code lifecycle: Improve SSH handling #8772

Merged
merged 23 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
acb8d5b
ssh usability improvements
icelord42 Jun 9, 2024
60e56ce
fixed tests and link
icelord42 Jun 10, 2024
e7142b6
only display warning when no SSH key is present
icelord42 Jun 11, 2024
a65466c
code rabbitai change requests
icelord42 Jun 11, 2024
16b8cc2
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 11, 2024
541ca07
code rabbitai change requests
icelord42 Jun 11, 2024
60929c7
Update src/main/webapp/app/core/auth/account.service.ts
SimonEntholzer Jun 11, 2024
2c396d4
use documentation button
icelord42 Jun 11, 2024
76b805d
Merge branch 'feature/ssh-usability-improvements' of github.com:ls1in…
icelord42 Jun 11, 2024
af7118b
Update src/main/webapp/app/shared/user-settings/ssh-settings/ssh-user…
SimonEntholzer Jun 12, 2024
a5b2106
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 12, 2024
b2fc3ef
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 16, 2024
ecdc10f
adapted code to change requests
icelord42 Jun 16, 2024
5f4b12c
Fixed test and made error message from server generic for security
icelord42 Jun 17, 2024
0e11e66
Update src/main/webapp/app/shared/components/clone-repo-button/clone-…
SimonEntholzer Jun 17, 2024
eaeb1d7
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 17, 2024
21d4a9c
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 20, 2024
1fccb5a
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 21, 2024
7121f24
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 22, 2024
c549a66
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 23, 2024
90af7a9
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 23, 2024
1d29461
Merge branch 'develop' into feature/ssh-usability-improvements
SimonEntholzer Jun 26, 2024
51f7c99
removed unused import form merge conflict
icelord42 Jun 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -37,6 +39,7 @@
import de.tum.in.www1.artemis.service.dto.UserInitializationDTO;
import de.tum.in.www1.artemis.service.user.UserCreationService;
import de.tum.in.www1.artemis.service.user.UserService;
import de.tum.in.www1.artemis.web.rest.util.HeaderUtil;
import tech.jhipster.web.util.PaginationUtil;

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

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

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

private final UserService userService;
Expand Down Expand Up @@ -184,13 +190,38 @@ public ResponseEntity<Void> setIrisAcceptedToTimestamp() {
@PutMapping("users/sshpublickey")
@EnforceAtLeastStudent
public ResponseEntity<Void> addSshPublicKey(@RequestBody String sshPublicKey) throws GeneralSecurityException, IOException {

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

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

log.debug("Successfully deleted SSH key of user {}", user.getLogin());
return ResponseEntity.ok().build();
}
}
2 changes: 2 additions & 0 deletions src/main/webapp/app/app.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export const PROFILE_LOCALVC = 'localvc';

export const PROFILE_LOCALCI = 'localci';

export const PROFILE_GITLAB = 'gitlab';

export const PROFILE_AEOLUS = 'aeolus';

export const PROFILE_IRIS = 'iris';
Expand Down
15 changes: 15 additions & 0 deletions src/main/webapp/app/core/auth/account.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,10 +327,25 @@ export class AccountService implements IAccountService {
this.prefilledUsernameValue = prefilledUsername;
}

/**
* Sends the added SSH key to the server
*
* @param sshPublicKey
*/
addSshPublicKey(sshPublicKey: string): Observable<void> {
if (this.userIdentity) {
this.userIdentity.sshPublicKey = sshPublicKey;
}
return this.http.put<void>('api/users/sshpublickey', sshPublicKey);
}

/**
* Sends a request to the server to delete the user's current SSH key
*/
deleteSshPublicKey(): Observable<void> {
if (this.userIdentity) {
this.userIdentity.sshPublicKey = undefined;
}
return this.http.delete<void>('api/users/sshpublickey');
}
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
container="body"
></button>
<ng-template #popContent>
@if (useSsh && !localVCEnabled) {
@if (useSsh && (!user.sshPublicKey || gitlabVCEnabled)) {
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
<div class="alert alert-warning" [innerHTML]="getSshKeyTip()"></div>
}
@if (participations && participations.length > 1) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { LocalStorageService } from 'ngx-webstorage';
import { ProgrammingExerciseStudentParticipation } from 'app/entities/participation/programming-exercise-student-participation.model';
import { ParticipationService } from 'app/exercises/shared/participation/participation.service';
import { Exercise } from 'app/entities/exercise.model';
import { PROFILE_LOCALVC } from 'app/app.constants';
import { PROFILE_GITLAB, PROFILE_LOCALVC } from 'app/app.constants';
import { isPracticeMode } from 'app/entities/participation/student-participation.model';
import { faDownload, faExternalLink } from '@fortawesome/free-solid-svg-icons';

Expand All @@ -33,13 +33,15 @@ export class CloneRepoButtonComponent implements OnInit, OnChanges {
exercise?: Exercise;

useSsh = false;
sshKeysUrl?: string;
setupSshKeysUrl?: string;
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
sshEnabled = false;
sshTemplateUrl?: string;
repositoryPassword?: string;
versionControlUrl: string;
versionControlAccessTokenRequired?: boolean;
localVCEnabled = false;
gitlabVCEnabled = false;

user: User;
cloneHeadline: string;
wasCopied = false;
Expand Down Expand Up @@ -67,14 +69,22 @@ export class CloneRepoButtonComponent implements OnInit, OnChanges {

// Get ssh information from the user
this.profileService.getProfileInfo().subscribe((profileInfo) => {
this.sshKeysUrl = profileInfo.sshKeysURL;
this.setupSshKeysUrl = profileInfo.sshKeysURL;
this.sshTemplateUrl = profileInfo.sshCloneURLTemplate;

SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
this.sshEnabled = !!this.sshTemplateUrl;
if (profileInfo.versionControlUrl) {
this.versionControlUrl = profileInfo.versionControlUrl;
}

this.versionControlAccessTokenRequired = profileInfo.versionControlAccessToken;
this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC);
this.gitlabVCEnabled = profileInfo.activeProfiles.includes(PROFILE_GITLAB);
if (this.localVCEnabled) {
this.setupSshKeysUrl = `${window.location.origin}/user-settings/sshSettings`;
} else {
this.setupSshKeysUrl = profileInfo.sshKeysURL;
}
});

this.useSsh = this.localStorage.retrieve('useSsh') || false;
Expand Down Expand Up @@ -170,7 +180,7 @@ export class CloneRepoButtonComponent implements OnInit, OnChanges {
* Inserts the correct link to the translated ssh tip.
*/
getSshKeyTip() {
return this.translateService.instant('artemisApp.exerciseActions.sshKeyTip').replace(/{link:(.*)}/, '<a href="' + this.sshKeysUrl + '" target="_blank">$1</a>');
return this.translateService.instant('artemisApp.exerciseActions.sshKeyTip').replace(/{link:(.*)}/, '<a href="' + this.setupSshKeysUrl + '" target="_blank">$1</a>');
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const DocumentationLinks = {
Quiz: 'exercises/quiz/',
Model: 'exercises/modeling/',
Programming: 'exercises/programming/',
SshSetup: 'exercises/programming.html#repository-access',
Text: 'exercises/textual/',
FileUpload: 'exercises/file-upload/',
Notifications: 'notifications/',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,30 +49,5 @@ <h1>
<dd>{{ currentUser.createdDate | artemisDate }}</dd>
</div>
}
@if (localVCEnabled) {
<div class="list-group-item">
<dt>
<!--Local Version Control SSH Key-->
<div class="row pb-1">
<div class="col">
{{ 'artemisApp.userSettings.accountInformationPage.sshKey' | artemisTranslate }}
</div>
<div class="col col-auto text-right">
<div class="btn-group" role="group" aria-label="Actions">
<button class="btn btn-sm btn-warning" (click)="editSshKey = !editSshKey">
<fa-icon [icon]="faEdit" />
</button>
<button class="btn btn-sm btn-success" (click)="saveSshKey()" [disabled]="!editSshKey">
<fa-icon [icon]="faSave" />
</button>
</div>
</div>
</div>
</dt>
<dd>
<textarea class="form-control" rows="5" [readonly]="!editSshKey" [(ngModel)]="sshKey"></textarea>
</dd>
</div>
}
</div>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ import { Component, OnInit } from '@angular/core';
import { User } from 'app/core/user/user.model';
import { AccountService } from 'app/core/auth/account.service';
import { Subscription, tap } from 'rxjs';
import { ProfileService } from 'app/shared/layouts/profiles/profile.service';
import { PROFILE_LOCALVC } from 'app/app.constants';
import { faEdit, faSave } from '@fortawesome/free-solid-svg-icons';

@Component({
selector: 'jhi-account-information',
Expand All @@ -13,38 +10,14 @@ import { faEdit, faSave } from '@fortawesome/free-solid-svg-icons';
})
export class AccountInformationComponent implements OnInit {
currentUser?: User;
localVCEnabled: boolean = false;
sshKey: string = '';
editSshKey = false;

faEdit = faEdit;
faSave = faSave;

private authStateSubscription: Subscription;

constructor(
private accountService: AccountService,
private profileService: ProfileService,
) {}
constructor(private accountService: AccountService) {}

ngOnInit() {
this.profileService.getProfileInfo().subscribe((profileInfo) => {
this.localVCEnabled = profileInfo.activeProfiles.includes(PROFILE_LOCALVC);
});

this.authStateSubscription = this.accountService
.getAuthenticationState()
.pipe(
tap((user: User) => {
this.sshKey = user.sshPublicKey || '';
return (this.currentUser = user);
}),
)
.pipe(tap((user: User) => (this.currentUser = user)))
SimonEntholzer marked this conversation as resolved.
Show resolved Hide resolved
.subscribe();
}

saveSshKey() {
this.editSshKey = false;
this.accountService.addSshPublicKey(this.sshKey).subscribe();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<h1 jhiTranslate="artemisApp.userSettings.sshSettings">
<!--SSH Settings-->
</h1>
@if (currentUser) {
<div class="list-group d-block">
<div class="list-group-item">
<dt>
<span jhiTranslate="artemisApp.userSettings.sshSettingsPage.sshSettingsInfoText"> </span>
<jhi-documentation-button [type]="documentationType" />
</dt>
</div>
@if (storedSshKey === '' && !editSshKey) {
<div class="list-group-item">
<jhi-button
[btnType]="ButtonType.PRIMARY"
[btnSize]="ButtonSize.SMALL"
[title]="'artemisApp.userSettings.sshSettingsPage.addNewSshKey'"
(onClick)="editSshKey = !editSshKey"
/>
</div>
}
@if (storedSshKey !== '' && !editSshKey) {
<dt class="list-group-item" jhiTranslate="artemisApp.userSettings.sshSettingsPage.sshKeyDisplayedInformation"></dt>
<div class="list-group-item">
{{ sshKey }}
</div>
<div class="list-group-item">
<div class="btn-group" role="group" aria-label="Actions">
<jhi-button
class="d-flex"
[btnType]="ButtonType.PRIMARY"
[btnSize]="ButtonSize.SMALL"
[icon]="faEdit"
[title]="'artemisApp.userSettings.sshSettingsPage.editExistingSshKey'"
(onClick)="editSshKey = !editSshKey"
/>
</div>
<div class="btn-group" role="group" aria-label="Actions">
<button
class="btn btn-md flex-grow-1 d-flex align-items-center"
jhiDeleteButton
[renderButtonText]="false"
(delete)="deleteSshKey()"
deleteQuestion="artemisApp.userSettings.sshSettingsPage.deleteSshKeyQuestion"
[dialogError]="dialogError$"
>
<fa-icon [icon]="faTrash" />
<div jhiTranslate="artemisApp.userSettings.sshSettingsPage.deleteSshKey" class="ms-2">Delete</div>
</button>
</div>
</div>
}
@if (editSshKey) {
<div class="list-group-item">
<div jhiTranslate="artemisApp.userSettings.sshSettingsPage.key"></div>
</div>
<div class="list-group-item">
<dd>
<textarea class="form-control" rows="10" [readonly]="!editSshKey" [(ngModel)]="sshKey"></textarea>
</dd>
<div class="col col-auto text-right">
<div class="btn-group" role="group" aria-label="Actions">
<jhi-button
[btnType]="ButtonType.PRIMARY"
[btnSize]="ButtonSize.SMALL"
[icon]="faSave"
[title]="'artemisApp.userSettings.sshSettingsPage.saveSshKey'"
(onClick)="saveSshKey()"
/>
</div>
<div class="btn-group" role="group" aria-label="Actions">
<jhi-button
[btnType]="ButtonType.PRIMARY"
[btnSize]="ButtonSize.SMALL"
[title]="'artemisApp.userSettings.sshSettingsPage.cancelSavingSshKey'"
(onClick)="cancelEditingSshKey()"
/>
</div>
</div>
</div>
}
</div>
}
Loading
Loading