From 5e02f8911ce06f9953edec89fdddfe4c4efdca5b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fran=C3=A7ois=20Agneray?= <francois.agneray@lam.fr>
Date: Mon, 25 Apr 2022 11:50:23 +0200
Subject: [PATCH] Instance and dataset private => done

---
 .../home/components/welcome.component.html    |  3 +-
 client/src/app/instance/instance.component.ts | 28 +++++++++++++-
 .../dataset/dataset-card.component.html       |  7 +++-
 .../dataset/dataset-card.component.ts         | 38 ++++++++++++++++++-
 .../dataset/dataset-tabs.component.html       |  8 +++-
 .../dataset/dataset-tabs.component.ts         |  7 +++-
 .../search/containers/dataset.component.html  | 13 +++++--
 .../search/containers/dataset.component.ts    | 32 +++++++++++++++-
 .../shared/components/navbar.component.html   |  3 +-
 .../src/app/shared/pipes/auth-image.pipe.ts   |  5 ++-
 docker-compose.yml                            |  2 +-
 .../Action/DatasetListByInstanceAction.php    | 23 -----------
 server/src/Action/InstanceListAction.php      | 30 ---------------
 13 files changed, 131 insertions(+), 68 deletions(-)

diff --git a/client/src/app/instance/home/components/welcome.component.html b/client/src/app/instance/home/components/welcome.component.html
index aa8010a9..f59854e3 100644
--- a/client/src/app/instance/home/components/welcome.component.html
+++ b/client/src/app/instance/home/components/welcome.component.html
@@ -1,6 +1,7 @@
 <div class="row align-items-center jumbotron">
     <div class="col-6 col-md-4 order-md-2 mx-auto text-center">
-        <img class="img-fluid mb-3 mb-md-0" [src]="getLogoSrc() | authImage | async" alt="Instance logo">
+        <img *ngIf="instance.public" class="img-fluid mb-3 mb-md-0" [src]="getLogoSrc()" alt="Instance logo">
+        <img *ngIf="!instance.public" class="img-fluid mb-3 mb-md-0" [src]="getLogoSrc() | authImage | async" alt="Instance logo">
     </div>
     <div class="col-md-8 order-md-1 text-justify pr-md-5" [innerHtml]="instance.home_component_config.home_component_text"></div>
 </div>
diff --git a/client/src/app/instance/instance.component.ts b/client/src/app/instance/instance.component.ts
index 2b397dd5..6f7540f8 100644
--- a/client/src/app/instance/instance.component.ts
+++ b/client/src/app/instance/instance.component.ts
@@ -8,6 +8,7 @@
  */
 
 import { Component, OnDestroy, OnInit } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
 
 import { Store } from '@ngrx/store';
 import { Observable, Subscription } from 'rxjs';
@@ -20,6 +21,7 @@ import * as authSelector from 'src/app/auth/auth.selector';
 import * as datasetActions from 'src/app/metamodel/actions/dataset.actions';
 import * as datasetFamilyActions from 'src/app/metamodel/actions/dataset-family.actions';
 import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector';
+import * as datasetGroupActions from 'src/app/metamodel/actions/dataset-group.actions';
 import { AppConfigService } from 'src/app/app-config.service';
 
 /**
@@ -47,7 +49,7 @@ export class InstanceComponent implements OnInit, OnDestroy {
     public url: Observable<string>;
     public instanceSubscription: Subscription;
 
-    constructor(private store: Store<{ }>, private config: AppConfigService) {
+    constructor(private store: Store<{ }>, private config: AppConfigService, private http: HttpClient) {
         this.instance = store.select(instanceSelector.selectInstanceByRouteName);
         this.isAuthenticated = store.select(authSelector.selectIsAuthenticated);
         this.userProfile = store.select(authSelector.selectUserProfile);
@@ -60,6 +62,7 @@ export class InstanceComponent implements OnInit, OnDestroy {
         // This micro task prevent the expression has changed after view init error
         Promise.resolve(null).then(() => this.store.dispatch(datasetFamilyActions.loadDatasetFamilyList()));
         Promise.resolve(null).then(() => this.store.dispatch(datasetActions.loadDatasetList()));
+        Promise.resolve(null).then(() => this.store.dispatch(datasetGroupActions.loadDatasetGroupList()));
         this.instanceSubscription = this.instance.subscribe(instance => {
             if (instance) {
                 if (instance.search_by_criteria_allowed) {
@@ -72,7 +75,7 @@ export class InstanceComponent implements OnInit, OnDestroy {
                     this.links.push({ label: instance.documentation_label, icon: 'fas fa-question', routerLink: 'documentation' });
                 }
                 if (instance.design_favicon !== '') {
-                    this.favIcon.href = `${this.config.apiUrl}/instance/${instance.name}/file-explorer${instance.design_favicon}`;
+                    this.setFaviconHref(instance);
                 }
                 this.title.innerHTML = instance.label;
                 this.body.style.backgroundColor = instance.design_background_color;
@@ -80,6 +83,22 @@ export class InstanceComponent implements OnInit, OnDestroy {
         })
     }
 
+    setFaviconHref(instance: Instance) {
+        const src = `${this.config.apiUrl}/instance/${instance.name}/file-explorer${instance.design_favicon}`;
+        if (instance.public) {
+            this.favIcon.href = src;
+        } else {
+            this.http.get(src, { responseType: 'blob' }).subscribe(data => {
+                const reader = new FileReader();
+                reader.readAsDataURL(data);
+                reader.onloadend = () => {
+                    const base64data = reader.result;                
+                    this.favIcon.href = base64data as string;
+                }
+            });
+        }
+    }
+
     /**
      * Returns application base href.
      *
@@ -98,6 +117,11 @@ export class InstanceComponent implements OnInit, OnDestroy {
         return this.config.authenticationEnabled;
     }
 
+    /**
+     * Returns admin roles list
+     * 
+     * @returns string[]
+     */
     getAdminRoles(): string[] {
         return this.config.adminRoles;
     }
diff --git a/client/src/app/instance/search/components/dataset/dataset-card.component.html b/client/src/app/instance/search/components/dataset/dataset-card.component.html
index 175f9cd7..eebb222e 100644
--- a/client/src/app/instance/search/components/dataset/dataset-card.component.html
+++ b/client/src/app/instance/search/components/dataset/dataset-card.component.html
@@ -12,11 +12,16 @@
                 </div>
             </div>
             <div class="col-auto">
-                <button *ngIf="dataset.name !== datasetSelected"
+                <button *ngIf="dataset.name !== datasetSelected && isDatasetAccessible()"
                     (click)="selectDataset(dataset.name)"
                     class="btn btn-outline-secondary">
+                    <span *ngIf="!dataset.public" class="fa-solid fa-lock-open"></span>
+                    <span *ngIf="dataset.public" class="fa-solid fa-globe"></span>
                     Select
                 </button>
+                <button *ngIf="!isDatasetAccessible()" class="btn btn-outline-danger disabled" title="You are not authorized to access this dataset">
+                    <span class="fa-solid fa-lock"></span> Private
+                </button>
                 <span *ngIf="dataset.name === datasetSelected">
                     <span class="far fa-check-circle fa-2x text-success"></span>
                 </span>
diff --git a/client/src/app/instance/search/components/dataset/dataset-card.component.ts b/client/src/app/instance/search/components/dataset/dataset-card.component.ts
index f57589f6..c78f2780 100644
--- a/client/src/app/instance/search/components/dataset/dataset-card.component.ts
+++ b/client/src/app/instance/search/components/dataset/dataset-card.component.ts
@@ -10,7 +10,7 @@
 import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 import { Router } from '@angular/router';
 
-import { Dataset } from 'src/app/metamodel/models';
+import { Dataset, DatasetGroup } from 'src/app/metamodel/models';
 
 /**
  * @class
@@ -25,9 +25,45 @@ export class DatasetCardComponent {
     @Input() dataset: Dataset;
     @Input() instanceSelected: string;
     @Input() datasetSelected: string;
+    @Input() authenticationEnabled: boolean;
+    @Input() isAuthenticated: boolean;
+    @Input() userRoles: string[];
+    @Input() adminRoles: string[];
+    @Input() datasetGroupList: DatasetGroup[];
 
     constructor(private router: Router) { }
 
+    isDatasetAccessible() {
+        let accessible = true;
+
+        if (this.authenticationEnabled && !this.dataset.public && !this.isAdmin()) {
+            accessible = false;
+            if (this.isAuthenticated) {
+                accessible = this.datasetGroupList
+                    .filter(datasetGroup => datasetGroup.datasets.includes(this.dataset.name))
+                    .filter(datasetGroup => this.userRoles.includes(datasetGroup.role))
+                    .length > 0;
+            }
+        }
+
+        return accessible;
+    }
+
+    /**
+     * Returns true if user is admin
+     * 
+     * @returns boolean
+     */
+    isAdmin() {
+        let admin = false;
+        for (let i = 0; i < this.adminRoles.length; i++) {
+            admin = this.userRoles.includes(this.adminRoles[i]);
+            if (admin) break;
+        }
+
+        return admin;
+    }
+
     /**
      * Navigates to search form corresponding to the given dataset.
      *
diff --git a/client/src/app/instance/search/components/dataset/dataset-tabs.component.html b/client/src/app/instance/search/components/dataset/dataset-tabs.component.html
index e66f0106..723befda 100644
--- a/client/src/app/instance/search/components/dataset/dataset-tabs.component.html
+++ b/client/src/app/instance/search/components/dataset/dataset-tabs.component.html
@@ -17,7 +17,13 @@
                 <app-dataset-card
                     [dataset]="dataset" 
                     [instanceSelected]="instanceSelected"
-                    [datasetSelected]="datasetSelected">
+                    [datasetSelected]="datasetSelected"
+                    [datasetSelected]="datasetSelected"
+                    [authenticationEnabled]="authenticationEnabled"
+                    [isAuthenticated]="isAuthenticated"
+                    [userRoles]="userRoles"
+                    [adminRoles]="adminRoles"
+                    [datasetGroupList]="datasetGroupList">
                 </app-dataset-card>
                 <hr *ngIf="!isLast">
             </div>
diff --git a/client/src/app/instance/search/components/dataset/dataset-tabs.component.ts b/client/src/app/instance/search/components/dataset/dataset-tabs.component.ts
index c71df192..05c9f162 100644
--- a/client/src/app/instance/search/components/dataset/dataset-tabs.component.ts
+++ b/client/src/app/instance/search/components/dataset/dataset-tabs.component.ts
@@ -9,7 +9,7 @@
 
 import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
 
-import { Dataset, DatasetFamily } from 'src/app/metamodel/models';
+import { Dataset, DatasetFamily, DatasetGroup } from 'src/app/metamodel/models';
 
 /**
  * @class
@@ -26,4 +26,9 @@ export class DatasetTabsComponent {
     @Input() datasetFamilyList: DatasetFamily[];
     @Input() instanceSelected: string;
     @Input() datasetSelected: string;
+    @Input() authenticationEnabled: boolean;
+    @Input() isAuthenticated: boolean;
+    @Input() userRoles: string[];
+    @Input() adminRoles: string[];
+    @Input() datasetGroupList: DatasetGroup[];
 }
diff --git a/client/src/app/instance/search/containers/dataset.component.html b/client/src/app/instance/search/containers/dataset.component.html
index d8857371..d55db1a5 100644
--- a/client/src/app/instance/search/containers/dataset.component.html
+++ b/client/src/app/instance/search/containers/dataset.component.html
@@ -1,9 +1,11 @@
 <app-spinner *ngIf="(datasetFamilyListIsLoading | async)
-    || (datasetListIsLoading | async)">
+    || (datasetListIsLoading | async)
+    || (datasetGroupListIsLoading | async)">
 </app-spinner>
 
 <div *ngIf="(datasetFamilyListIsLoaded | async)
-    && (datasetListIsLoaded | async)" class="row mt-4">
+    && (datasetListIsLoaded | async)
+    && (datasetGroupListIsLoaded | async)" class="row mt-4">
     <ng-container *ngIf="(datasetList | async).length === 0">
         <div class="col-12 lead text-center">
             Oops! No dataset available...
@@ -18,7 +20,12 @@
                 [datasetList]="datasetList | async"
                 [datasetFamilyList]="datasetFamilyList | async"
                 [instanceSelected]="instanceSelected | async"
-                [datasetSelected]="datasetSelected | async">
+                [datasetSelected]="datasetSelected | async"
+                [authenticationEnabled]="getAuthenticationEnabled()"
+                [isAuthenticated]="isAuthenticated | async"
+                [userRoles]="userRoles | async"
+                [adminRoles]="getAdminRoles()"
+                [datasetGroupList]="datasetGroupList | async">
             </app-dataset-tabs>
         </div>
         <div class="col-12 col-md-4 col-lg-3 pt-2">
diff --git a/client/src/app/instance/search/containers/dataset.component.ts b/client/src/app/instance/search/containers/dataset.component.ts
index 93a9ac41..7f24ff31 100644
--- a/client/src/app/instance/search/containers/dataset.component.ts
+++ b/client/src/app/instance/search/containers/dataset.component.ts
@@ -13,10 +13,12 @@ import { Store } from '@ngrx/store';
 import { Observable, Subscription } from 'rxjs';
 
 import { AbstractSearchComponent } from './abstract-search.component';
-import { DatasetFamily } from 'src/app/metamodel/models';
+import { DatasetFamily, DatasetGroup } from 'src/app/metamodel/models';
 import * as searchActions from '../../store/actions/search.actions';
 import * as authSelector from 'src/app/auth/auth.selector';
 import * as datasetFamilySelector from 'src/app/metamodel/selectors/dataset-family.selector';
+import * as datasetGroupSelector from 'src/app/metamodel/selectors/dataset-group.selector';
+import { AppConfigService } from 'src/app/app-config.service';
 
 /**
  * @class
@@ -31,14 +33,22 @@ export class DatasetComponent extends AbstractSearchComponent {
     public datasetFamilyListIsLoading: Observable<boolean>;
     public datasetFamilyListIsLoaded: Observable<boolean>;
     public datasetFamilyList: Observable<DatasetFamily[]>;
+    public userRoles: Observable<string[]>;
+    public datasetGroupList: Observable<DatasetGroup[]>;
+    public datasetGroupListIsLoading: Observable<boolean>;
+    public datasetGroupListIsLoaded: Observable<boolean>;
     public datasetSelectedSubscription: Subscription;
 
-    constructor(protected store: Store<{ }>) {
+    constructor(protected store: Store<{ }>, private config: AppConfigService) {
         super(store);
         this.isAuthenticated = store.select(authSelector.selectIsAuthenticated);
         this.datasetFamilyListIsLoading = store.select(datasetFamilySelector.selectDatasetFamilyListIsLoading);
         this.datasetFamilyListIsLoaded = store.select(datasetFamilySelector.selectDatasetFamilyListIsLoaded);
         this.datasetFamilyList = store.select(datasetFamilySelector.selectAllDatasetFamilies);
+        this.userRoles = store.select(authSelector.selectUserRoles);
+        this.datasetGroupList = store.select(datasetGroupSelector.selectAllDatasetGroups);
+        this.datasetGroupListIsLoading = store.select(datasetGroupSelector.selectDatasetGroupListIsLoading);
+        this.datasetGroupListIsLoaded = store.select(datasetGroupSelector.selectDatasetGroupListIsLoaded);
     }
 
     ngOnInit(): void {
@@ -53,6 +63,24 @@ export class DatasetComponent extends AbstractSearchComponent {
         super.ngOnInit();
     }
 
+    /**
+     * Checks if authentication is enabled.
+     *
+     * @return boolean
+     */
+    getAuthenticationEnabled(): boolean {
+        return this.config.authenticationEnabled;
+    }
+
+    /**
+     * Returns admin roles list
+     * 
+     * @returns string[]
+     */
+    getAdminRoles(): string[] {
+        return this.config.adminRoles;
+    }
+
     ngOnDestroy(): void {
         if (this.datasetSelectedSubscription) this.datasetSelectedSubscription.unsubscribe();
         super.ngOnDestroy();
diff --git a/client/src/app/shared/components/navbar.component.html b/client/src/app/shared/components/navbar.component.html
index 98ece095..347d4b7b 100644
--- a/client/src/app/shared/components/navbar.component.html
+++ b/client/src/app/shared/components/navbar.component.html
@@ -5,7 +5,8 @@
     </a>
 
     <a *ngIf="instance" routerLink="/instance/{{ instance.name }}" class="navbar-brand">
-        <img [src]="getLogoHref() | authImage | async" alt="Instance logo" />
+        <img *ngIf="instance.public" [src]="getLogoHref()" alt="Instance logo" />
+        <img *ngIf="!instance.public" [src]="getLogoHref() | authImage | async" alt="Instance logo" />
     </a>
 
     <!-- Right Navigation -->
diff --git a/client/src/app/shared/pipes/auth-image.pipe.ts b/client/src/app/shared/pipes/auth-image.pipe.ts
index 6f31c305..c479badc 100644
--- a/client/src/app/shared/pipes/auth-image.pipe.ts
+++ b/client/src/app/shared/pipes/auth-image.pipe.ts
@@ -10,13 +10,16 @@
 import { Pipe, PipeTransform } from '@angular/core';
 import { HttpClient } from '@angular/common/http';
 
+import { lastValueFrom } from 'rxjs';
+
 @Pipe({ name: 'authImage' })
 export class AuthImagePipe implements PipeTransform {
     constructor(private http: HttpClient) { }
 
     async transform(src: string): Promise<string> {
         try {
-            const imageBlob = await this.http.get(src, { responseType: 'blob' }).toPromise();
+            const get$ = this.http.get(src, { responseType: 'blob' });
+            const imageBlob = await lastValueFrom(get$);
             const reader = new FileReader();
             return new Promise((resolve, reject) => {
                 reader.onloadend = () => resolve(reader.result as string);
diff --git a/docker-compose.yml b/docker-compose.yml
index 3172ebe3..77088f75 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -35,7 +35,7 @@ services:
             SSO_AUTH_URL: "http://localhost:8180/auth"
             SSO_REALM: "anis"
             SSO_CLIENT_ID: "anis-client"
-            TOKEN_ENABLED: 0
+            TOKEN_ENABLED: 1
             TOKEN_JWKS_URL: "http://keycloak:8180/auth/realms/anis/protocol/openid-connect/certs"
             TOKEN_ADMIN_ROLES: anis_admin,superuser
             RMQ_HOST: rmq
diff --git a/server/src/Action/DatasetListByInstanceAction.php b/server/src/Action/DatasetListByInstanceAction.php
index 626611a7..414ad6f4 100644
--- a/server/src/Action/DatasetListByInstanceAction.php
+++ b/server/src/Action/DatasetListByInstanceAction.php
@@ -71,35 +71,12 @@ final class DatasetListByInstanceAction extends AbstractAction
         }
 
         if ($request->getMethod() === GET) {
-            $token = $request->getAttribute('token');
-
             $qb = $this->em->createQueryBuilder();
             $qb->select('d')
                 ->from('App\Entity\Dataset', 'd')
                 ->join('d.datasetFamily', 'f')
                 ->where($qb->expr()->eq('IDENTITY(f.instance)', ':instanceName'));
 
-            if (boolval($this->settings['enabled'])) {
-                if (!$token) {
-                    // If user is not connected return public datasets
-                    $qb->andWhere($qb->expr()->eq('d.public', 'true'));
-                } else {
-                    $adminRoles = explode(',', $this->settings['admin_roles']);
-                    $roles = $token->realm_access->roles;
-                    if (!$this->isAdmin($adminRoles, $roles)) {
-                        // If user is not an admin return public datasets
-                        // And returns datasets from user's groups
-                        $qb->andWhere($qb->expr()->eq('d.public', 'true'));
-                        $qb2 = $this->em->createQueryBuilder();
-                        $qb2->select('d2.name')
-                            ->from('App\Entity\DatasetGroup', 'g')
-                            ->join('g.datasets', 'd2')
-                            ->where($qb2->expr()->in('g.role', $roles));
-                        $qb->orWhere($qb->expr()->in('d.name', $qb2->getDQL()));
-                    }
-                }
-            }
-
             $qb->setParameter('instanceName', $instance->getName());
             $datasets = $qb->getQuery()->getResult();
             $payload = json_encode($datasets);
diff --git a/server/src/Action/InstanceListAction.php b/server/src/Action/InstanceListAction.php
index 83f44202..f5a3a433 100644
--- a/server/src/Action/InstanceListAction.php
+++ b/server/src/Action/InstanceListAction.php
@@ -64,7 +64,6 @@ final class InstanceListAction extends AbstractAction
 
         if ($request->getMethod() === GET) {
             $instances = $this->em->getRepository('App\Entity\Instance')->findAll();
-            //$instances = $this->getInstanceList($request->getAttribute('token'));
             $payload = json_encode($instances);
         }
 
@@ -90,35 +89,6 @@ final class InstanceListAction extends AbstractAction
         return $response;
     }
 
-    // private function getInstanceList($token)
-    // {
-    //     $qb = $this->em->createQueryBuilder();
-    //     $qb->select('i')->from('App\Entity\Instance', 'i');
-
-    //     if (boolval($this->settings['enabled'])) {
-    //         if (!$token) {
-    //             // If user is not connected return public instances
-    //             $qb->andWhere($qb->expr()->eq('i.public', 'true'));
-    //         } else {
-    //             $adminRoles = explode(',', $this->settings['admin_roles']);
-    //             $roles = $token->realm_access->roles;
-    //             if (!$this->isAdmin($adminRoles, $roles)) {
-    //                 // If user is not an admin return public datasets
-    //                 // And returns datasets from user's groups
-    //                 $qb->andWhere($qb->expr()->eq('i.public', 'true'));
-    //                 $qb2 = $this->em->createQueryBuilder();
-    //                 $qb2->select('i2.name')
-    //                     ->from('App\Entity\InstanceGroup', 'ig')
-    //                     ->join('ig.instances', 'i2')
-    //                     ->where($qb2->expr()->in('ig.role', $roles));
-    //                 $qb->orWhere($qb->expr()->in('i.name', $qb2->getDQL()));
-    //             }
-    //         }
-    //     }
-
-    //     return $qb->getQuery()->getResult();
-    // }
-
     /**
      * Add a new instance into the metamodel
      *
-- 
GitLab