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