From 532bf8c5a8c559986ea5019d867af4df18d62355 Mon Sep 17 00:00:00 2001 From: Philipp Gritsch <philipp.gritsch@uibk.ac.at> Date: Thu, 25 Jan 2024 10:56:55 +0100 Subject: [PATCH] closes #469 introduce check to only make button clickable if user has access to git repository --- .../uibk/gitsearch/service/GitlabService.java | 20 ++++++++++ .../gitsearch/web/rest/ExerciseResource.java | 35 ++++++++++++++++ .../exercise-details.component.ts | 40 +++++++++++++++---- .../exercise-body.component.html | 1 + .../app/exercise/service/exercise.service.ts | 31 +++++++------- .../shared/model/search/project-dto.model.ts | 1 + .../search/public-visibility-dto.model.ts | 3 ++ .../user-provided-metadata-dto.model.ts | 2 + 8 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 src/main/webapp/app/shared/model/search/public-visibility-dto.model.ts diff --git a/src/main/java/at/ac/uibk/gitsearch/service/GitlabService.java b/src/main/java/at/ac/uibk/gitsearch/service/GitlabService.java index 79f33294e..ee88f94db 100644 --- a/src/main/java/at/ac/uibk/gitsearch/service/GitlabService.java +++ b/src/main/java/at/ac/uibk/gitsearch/service/GitlabService.java @@ -375,6 +375,26 @@ public class GitlabService { return groupApi.getSubGroups(groupId); } + public boolean isMember(long gitProjectId, at.ac.uibk.gitsearch.domain.User user) throws GitLabApiException { + try { + org.gitlab4j.api.models.User gitUser = this.gitLabRepository.getAdminGitLabApi().getUserApi().getUserByEmail(user.getEmail()); + + if (gitUser == null) { + return false; + } + + return gitLabRepository.getAdminGitLabApi().getProjectApi().getMember(gitProjectId, gitUser.getId(), true) != null; + } catch (final GitLabApiException e) { + if (e.getHttpStatus() == 404) { + return false; + } + throw e; + } catch (IllegalArgumentException e) { + // email of user invalid + return false; + } + } + /** * Utility function used to set the default JGIT credentials to use the provided * OAuth2 token diff --git a/src/main/java/at/ac/uibk/gitsearch/web/rest/ExerciseResource.java b/src/main/java/at/ac/uibk/gitsearch/web/rest/ExerciseResource.java index ac296968d..37b922235 100644 --- a/src/main/java/at/ac/uibk/gitsearch/web/rest/ExerciseResource.java +++ b/src/main/java/at/ac/uibk/gitsearch/web/rest/ExerciseResource.java @@ -1,6 +1,7 @@ package at.ac.uibk.gitsearch.web.rest; import at.ac.uibk.gitsearch.domain.ChildInfo; +import at.ac.uibk.gitsearch.domain.User; import at.ac.uibk.gitsearch.es.model.ArtemisExerciseInfo; import at.ac.uibk.gitsearch.security.SecurityUtils; import at.ac.uibk.gitsearch.service.ArtemisImportError; @@ -9,6 +10,7 @@ import at.ac.uibk.gitsearch.service.GitlabService; import at.ac.uibk.gitsearch.service.SearchService; import at.ac.uibk.gitsearch.service.SearchService.ExtractionDepth; import at.ac.uibk.gitsearch.service.StatisticsService; +import at.ac.uibk.gitsearch.service.UserService; import at.ac.uibk.gitsearch.service.dto.StatisticsDTO; import at.ac.uibk.gitsearch.web.rest.utils.RestUtils; import at.ac.uibk.gitsearch.web.util.HeaderUtil; @@ -84,6 +86,10 @@ public class ExerciseResource { @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" }) private SearchService searchService; + @Autowired + @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" }) + private UserService userService; + @Autowired @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" }) private StatisticsService statisticsService; @@ -389,6 +395,35 @@ public class ExerciseResource { return ResponseEntity.ok(new URL(baseUrl + "/import/" + exerciseImportService.getTokenFromUrl(exerciseUrl)).toURI()); } + @GetMapping("/exercises/{id}/source-authorization") + @SuppressWarnings("PMD.AvoidCatchingGenericException") + public ResponseEntity<?> getMembers(@PathVariable("id") String exerciseId) { + try { + if (SecurityUtils.getCurrentUserLogin().isEmpty()) { + ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + + final Optional<SearchResultDTO> result = searchService.findExerciseById(ExerciseId.fromString(exerciseId)); + if (result.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + Optional<User> user = this.userService.getUserByLogin(SecurityUtils.getCurrentUserLogin().get()); + if (user.isEmpty()) { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } + + int gitProjectId = result.get().getProject().getProject_id(); + + return this.gitLabService.isMember(gitProjectId, user.get()) + ? ResponseEntity.ok().build() + : ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } catch (final Exception e) { + log.error("Error while getting /source-authorization for exercise {}", exerciseId, e); + return ResponseEntity.internalServerError().build(); + } + } + /** * GET /exercise/imported-exercise-info/{exerciseToken} : Used to retrieve the exercise's metadata * of a given exerciseToken diff --git a/src/main/webapp/app/exercise/exercise-details/exercise-details.component.ts b/src/main/webapp/app/exercise/exercise-details/exercise-details.component.ts index 7777f9764..bdad39843 100644 --- a/src/main/webapp/app/exercise/exercise-details/exercise-details.component.ts +++ b/src/main/webapp/app/exercise/exercise-details/exercise-details.component.ts @@ -80,12 +80,7 @@ export class ExerciseHeaderComponent { }) export class ExerciseBodyComponent implements OnInit, OnDestroy, AfterViewInit { @Output() exerciseChangedEvent = new EventEmitter<SearchResultDTO>(); - @Input() get referencedExercise(): SearchResultDTO | undefined { - return this.exercise; - } - set referencedExercise(exercise: SearchResultDTO | undefined) { - this.exercise = exercise as ExtendedSearchResultDTO; - } + gitlabIsAccessible = false; exercise: ExtendedSearchResultDTO | undefined; parent: SearchResultDTO | undefined; @@ -102,10 +97,21 @@ export class ExerciseBodyComponent implements OnInit, OnDestroy, AfterViewInit { treeIcon = faFolderOpen; faArrowRight = faArrowRight; - oerLink?: string; oerExerciseMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/; + @Input() get referencedExercise(): SearchResultDTO | undefined { + return this.exercise; + } + + set referencedExercise(exercise: SearchResultDTO | undefined) { + this.gitlabIsAccessible = false; + this.exercise = exercise as ExtendedSearchResultDTO; + if (exercise) { + this.updateGitIsAccessibleForUser(); + } + } + constructor( private reviewManagementService: ReviewManagementService, private accountService: AccountService, @@ -152,6 +158,26 @@ export class ExerciseBodyComponent implements OnInit, OnDestroy, AfterViewInit { return ''; } + updateGitIsAccessibleForUser(): void { + const projectIsPrivate: boolean = this.exercise?.project.visibilty == 'private'; + const exceptIsEmpty: boolean = + !this.exercise?.metadata.publicVisibility?.except || this.exercise?.metadata.publicVisibility?.except.length == 0; + + if (!projectIsPrivate && exceptIsEmpty) { + this.gitlabIsAccessible = true; + return; + } + + if (!this.isAuthenticated()) { + this.gitlabIsAccessible = false; + return; + } + + this.exerciseService + .hasUserAccessToGitlabRepo(this.exercise!.exerciseId) + .subscribe((accessible: boolean) => (this.gitlabIsAccessible = accessible)); + } + public startAction(action: PluginActionInfo, exercise: SearchResultDTO): void { const basketInfo: ShoppingBasketInfo = { plugin: action.plugin, diff --git a/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.html b/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.html index 52769fb60..5c720c3d4 100644 --- a/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.html +++ b/src/main/webapp/app/exercise/exercise-details/exerciseComponents/exercise-body.component.html @@ -182,6 +182,7 @@ jhiTranslate="exercise.details.allExercises" ></button> <button + [disabled]="!this.gitlabIsAccessible" type="button" class="btn btn-outline-secondary" style="display: block" diff --git a/src/main/webapp/app/exercise/service/exercise.service.ts b/src/main/webapp/app/exercise/service/exercise.service.ts index d459c217c..889dad964 100644 --- a/src/main/webapp/app/exercise/service/exercise.service.ts +++ b/src/main/webapp/app/exercise/service/exercise.service.ts @@ -1,12 +1,13 @@ -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { ApplicationConfigService } from 'app/core/config/application-config.service'; import { LikesService } from 'app/entities/likes/likes.service'; import { StatisticsService } from 'app/entities/statistics/statistics.service'; import { SearchService } from 'app/search/service/search-service'; import { ArtemisExerciseInfo } from 'app/shared/model/artemis-exercise-info.model'; +import { catchError, map } from 'rxjs/operators'; import { ChildInfo, ExtendedSearchResultDTO, hasChildren, SearchResultDTO } from 'app/shared/model/search/search-result-dto.model'; -import { BehaviorSubject, Observable } from 'rxjs'; +import { BehaviorSubject, Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class ExerciseService { @@ -36,6 +37,20 @@ export class ExerciseService { private exerciseCache: { [id: string]: BehaviorSubject<SearchResultDTO> } = {}; + public hasUserAccessToGitlabRepo(exerciseId: string): Observable<boolean> { + const endpoint: string = this.applicationConfigService.getEndpointFor( + SERVER_API_URL + 'api/exercises/' + encodeURIforExerciseId(exerciseId) + '/source-authorization' + ); + return this.http.get<any>(endpoint, { observe: 'response' }).pipe( + map((response: HttpResponse<any>) => { + return response.status === 200; + }), + catchError(() => { + return of(false); + }) + ); + } + public loadExercise(exerciseId: string): Observable<SearchResultDTO> { if (this.exerciseCache[exerciseId]) { return this.exerciseCache[exerciseId]; @@ -108,18 +123,6 @@ export class ExerciseService { }, error: () => console.warn('Could not load if user has liked or not'), }); - - /* - * the following is not relevant: is now reloaded directly from index - this.searchService.getStatisticsForExercise(exercise.exerciseId).subscribe({ - next: (data: Statistics) => { - result.views = data.views!; - result.downloads = data.downloads!; - result.badgeRewarded = data.badgeRewarded!; - }, - error: () => console.warn('Could not load exercise statistics'), - }); - */ } return result; } diff --git a/src/main/webapp/app/shared/model/search/project-dto.model.ts b/src/main/webapp/app/shared/model/search/project-dto.model.ts index 4b62548d9..713eb524e 100644 --- a/src/main/webapp/app/shared/model/search/project-dto.model.ts +++ b/src/main/webapp/app/shared/model/search/project-dto.model.ts @@ -5,5 +5,6 @@ export interface ProjectDTO { main_group: string; sub_group: string; url: string; + visibilty: string; last_activity_at: Date; } diff --git a/src/main/webapp/app/shared/model/search/public-visibility-dto.model.ts b/src/main/webapp/app/shared/model/search/public-visibility-dto.model.ts new file mode 100644 index 000000000..6ece5e058 --- /dev/null +++ b/src/main/webapp/app/shared/model/search/public-visibility-dto.model.ts @@ -0,0 +1,3 @@ +export interface PublicVisibilityDTOModel { + except: string[]; +} diff --git a/src/main/webapp/app/shared/model/search/user-provided-metadata-dto.model.ts b/src/main/webapp/app/shared/model/search/user-provided-metadata-dto.model.ts index bab099bfc..8d649b636 100644 --- a/src/main/webapp/app/shared/model/search/user-provided-metadata-dto.model.ts +++ b/src/main/webapp/app/shared/model/search/user-provided-metadata-dto.model.ts @@ -1,5 +1,6 @@ import { Person } from '../person.model'; import { IInteractivityType } from '../exercise.model'; +import { PublicVisibilityDTOModel } from './public-visibility-dto.model'; export interface UserProvidedMetadataDTO { assesses: string[]; @@ -33,4 +34,5 @@ export interface UserProvidedMetadataDTO { title: string; typicalAgeRange: string; version: string; + publicVisibility: PublicVisibilityDTOModel; } -- GitLab