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