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