From 2386c8ad3b48881fbec158fcb12565634ffcb7f4 Mon Sep 17 00:00:00 2001 From: Philipp Gritsch <philipp.gritsch@uibk.ac.at> Date: Wed, 17 Jan 2024 19:15:02 +0100 Subject: [PATCH] closes #469 introduce check to only make button clickable if user has access to git repository --- .vscode/launch.json | 1 + .../EduSharingApiConfiguration.java | 15 +++++++ .../edu_sharing/EduSharingConnector.java | 14 ++++++ .../uibk/gitsearch/service/GitlabService.java | 20 +++++++++ .../gitsearch/web/rest/ExerciseResource.java | 44 +++++++++++++++++++ .../exercise-details.component.ts | 38 +++++++++++++--- .../exercise-body.component.html | 1 + .../markDownViewer.component.ts | 2 +- .../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 + 12 files changed, 151 insertions(+), 21 deletions(-) create mode 100644 src/main/java/at/ac/uibk/gitsearch/edu_sharing/EduSharingApiConfiguration.java create mode 100644 src/main/java/at/ac/uibk/gitsearch/edu_sharing/EduSharingConnector.java create mode 100644 src/main/webapp/app/shared/model/search/public-visibility-dto.model.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index b6709cd8c..192db7b06 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,7 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { "name": "Launch jest explicitly", "program": "${workspaceFolder}/node_modules/jest/bin/jest.js", diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/EduSharingApiConfiguration.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/EduSharingApiConfiguration.java new file mode 100644 index 000000000..85a7475b6 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/EduSharingApiConfiguration.java @@ -0,0 +1,15 @@ +package at.ac.uibk.gitsearch.edu_sharing; + +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class EduSharingApiConfiguration { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplateBuilder().build(); + } +} diff --git a/src/main/java/at/ac/uibk/gitsearch/edu_sharing/EduSharingConnector.java b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/EduSharingConnector.java new file mode 100644 index 000000000..114d7c852 --- /dev/null +++ b/src/main/java/at/ac/uibk/gitsearch/edu_sharing/EduSharingConnector.java @@ -0,0 +1,14 @@ +package at.ac.uibk.gitsearch.edu_sharing; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class EduSharingConnector { + + @Autowired + private RestTemplate restTemplate; + + public void sendToApi() {} +} 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 f558766a8..4d3576ff7 100644 --- a/src/main/java/at/ac/uibk/gitsearch/service/GitlabService.java +++ b/src/main/java/at/ac/uibk/gitsearch/service/GitlabService.java @@ -376,6 +376,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 82e7681a3..90535e691 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,5 +1,6 @@ package at.ac.uibk.gitsearch.web.rest; +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; @@ -8,6 +9,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; @@ -82,6 +84,14 @@ public class ExerciseResource { @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" }) private SearchService searchService; + @Autowired + @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" }) + private GitlabService gitlabService; + + @Autowired + @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" }) + private UserService userService; + @Autowired @SuppressWarnings({ "PMD.ImmutableField", "PMD.AvoidDuplicateLiterals" }) private StatisticsService statisticsService; @@ -348,6 +358,40 @@ public class ExerciseResource { return ResponseEntity.ok(new URL(baseUrl + "/import/" + exerciseImportService.getTokenFromUrl(exerciseUrl)).toURI()); } + @GetMapping("/exercises/**") + public ResponseEntity test() { + System.out.println("hello"); + return ResponseEntity.status(HttpStatus.OK).build(); + } + + @GetMapping("/exercises/{id}/source-authorization") + 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 a3b0f3c52..4738648a1 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 @@ -74,12 +74,7 @@ export class ExerciseHeaderComponent { }) export class ExerciseBodyComponent implements OnInit, OnDestroy { @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; bookmarked = false; @@ -92,6 +87,17 @@ export class ExerciseBodyComponent implements OnInit, OnDestroy { likeSubscription?: Subscription; authenticated = false; + @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(); + } + } + treeIcon = faFolder; oerLink?: string; oerExerciseMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/; @@ -128,6 +134,26 @@ export class ExerciseBodyComponent implements OnInit, OnDestroy { ); } + 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 749278916..f1582e368 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 @@ -168,6 +168,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/markDownViewer/markDownViewer.component.ts b/src/main/webapp/app/exercise/markDownViewer/markDownViewer.component.ts index 66f251d7b..4b05b550f 100644 --- a/src/main/webapp/app/exercise/markDownViewer/markDownViewer.component.ts +++ b/src/main/webapp/app/exercise/markDownViewer/markDownViewer.component.ts @@ -19,7 +19,7 @@ export class MarkDownViewerComponent implements OnInit { } set exercise(exercise: SearchResultDTO | undefined) { this.myExercise = exercise; - this.loadVisible(); + //this.loadVisible(); } /* diff --git a/src/main/webapp/app/exercise/service/exercise.service.ts b/src/main/webapp/app/exercise/service/exercise.service.ts index d27dc0e58..2d4a0a634 100644 --- a/src/main/webapp/app/exercise/service/exercise.service.ts +++ b/src/main/webapp/app/exercise/service/exercise.service.ts @@ -1,4 +1,4 @@ -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'; @@ -6,7 +6,8 @@ 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 { ExtendedSearchResultDTO, SearchResultDTO } from 'app/shared/model/search/search-result-dto.model'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class ExerciseService { @@ -33,6 +34,20 @@ export class ExerciseService { }); } + 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> { return this.http.get<SearchResultDTO>(this.exerciseUrl + encodeURIforExerciseId(exerciseId)); } @@ -65,18 +80,6 @@ export class ExerciseService { }, error: () => console.warn('Could not load if user has liked or not'), }); - - /* - * 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