diff --git a/client/src/app/instance/home/components/welcome.component.html b/client/src/app/instance/home/components/welcome.component.html index aa8010a98ce902af1d3e51f6bfa576082b0e4216..f59854e39456e2bbbdb7dcaf1fcf902139b706ef 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 2b397dd57bf6fcf38d351b2db2ddc0b75a1264a7..6f7540f87db40bf25bd0713419523d708a1640d7 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 175f9cd70d8c214147737494260b2b778907994e..eebb222ef4271d8069adb962538442ef1363c253 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 f57589f6168301b8bafea3cd1d8a64cf45d95c57..c78f27805e1a4f28134484b19781bd78ca895d64 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 e66f010659f597af81fc32431652b24e539abffa..723befdae91f32e6260654aea7cb7828341005d6 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 c71df192afde90ade2409cf69e37e185144c5a5c..05c9f162a27a6a4531004827b7dbfff74ec1c857 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 d8857371271ca3f7c675a1c0aa58be9d7bf80636..d55db1a5a4e0badff8cf82fc0f5bac8c9067c402 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 93a9ac415acfba6458a90a089e9c310318337be6..7f24ff31fb884edf0baca82f354dcb01064422aa 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 98ece09533e1267a9e4c311fc772d2958325856b..347d4b7b7f1480d137d18442569f7f47c6f58426 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 6f31c30592d1e223f0120ddea5b618bff780db46..c479badc3229d193ff6bc0b7dba32ab2897e3fa6 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 3172ebe30d803d962f581ac165d6825ebd15c5cd..77088f75351fe2a0ec792528e547995a16365b89 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 626611a7bbf42a8927350f75a9afc2d99aa89273..414ad6f48d4936381da1ed1bf1b1b7d6b4651086 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 83f4420265228876d835ce4717f72df2a0a7f30e..f5a3a43306afd59dcba50554cb90de38ae32a494 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 *