diff --git a/client/src/app/admin/admin-auth.guard.ts b/client/src/app/admin/admin-auth.guard.ts index c352533d5c62b05448cda4c70c661c5b5e6e394c..07ce850f03c648dd4bac50f338939dfd80f80f2e 100644 --- a/client/src/app/admin/admin-auth.guard.ts +++ b/client/src/app/admin/admin-auth.guard.ts @@ -14,7 +14,6 @@ import { Store, select } from '@ngrx/store'; import { combineLatest, Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import * as authActions from 'src/app/auth/auth.actions'; import * as authSelector from 'src/app/auth/auth.selector'; import { AppConfigService } from 'src/app/app-config.service'; @@ -39,19 +38,18 @@ export class AdminAuthGuard implements CanActivate { return true; } - // Force the user to log in if currently unauthenticated. + // If user is authenticated and authorized so admin changes to true + let admin = false; if (!isAuthenticated) { - this.store.dispatch(authActions.login()); - return false; + for (let i = 0; i < this.config.adminRoles.length; i++) { + admin = userRoles.includes(this.config.adminRoles[i]); + if (admin) break; + } } - // If authenticated but not admin go to unauthorized page. - let admin = false; - for (let i = 0; i < this.config.adminRoles.length; i++) { - admin = userRoles.includes(this.config.adminRoles[i]); - if (admin) break; - } + // If user is not authorized to continue go to unauthorized page if (!admin) { + sessionStorage.setItem('redirect_uri', window.location.toString()); this.router.navigateByUrl('/unauthorized'); return false; } diff --git a/client/src/app/admin/admin.component.ts b/client/src/app/admin/admin.component.ts index f3c4d66c3bdca98dccc1c01fc543a0902d613035..039226dc1b6765eea135fbc8e40352065f56df14 100644 --- a/client/src/app/admin/admin.component.ts +++ b/client/src/app/admin/admin.component.ts @@ -74,7 +74,7 @@ export class AdminComponent implements OnInit { } login(): void { - this.store.dispatch(authActions.login()); + this.store.dispatch(authActions.login({ redirectUri: window.location.toString() })); } logout(): void { diff --git a/client/src/app/auth/auth.actions.ts b/client/src/app/auth/auth.actions.ts index 429574a4716d4c1db8670e584eed302f148fa37f..5c13f24ab497f02efd0a9c06c0c2d917f3b71384 100644 --- a/client/src/app/auth/auth.actions.ts +++ b/client/src/app/auth/auth.actions.ts @@ -11,7 +11,7 @@ import { createAction, props } from '@ngrx/store'; import { UserProfile } from './user-profile.model'; -export const login = createAction('[Auth] Login'); +export const login = createAction('[Auth] Login', props<{ redirectUri: string }>()); export const logout = createAction('[Auth] Logout'); export const authSuccess = createAction('[Auth] Auth Success'); export const loadUserProfileSuccess = createAction('[Auth] Load User Profile Success', props<{ userProfile: UserProfile }>()); diff --git a/client/src/app/auth/auth.effects.ts b/client/src/app/auth/auth.effects.ts index 5622d78917dd0483eabb6ac1471499babe8daa54..f519f277f0ebc2a056229ce5b171df257c9a16dd 100644 --- a/client/src/app/auth/auth.effects.ts +++ b/client/src/app/auth/auth.effects.ts @@ -22,10 +22,7 @@ export class AuthEffects { login$ = createEffect(() => this.actions$.pipe( ofType(authActions.login), - tap(_ => { - let redirectUri = window.location.toString() - this.keycloak.login({ redirectUri }); - }) + tap(action => this.keycloak.login({ redirectUri: action.redirectUri })) ), { dispatch: false } ); diff --git a/client/src/app/auth/init.keycloak.ts b/client/src/app/auth/init.keycloak.ts index 34ee8d9f7490c1d5f046dd676996fa9d50e97894..d7ca4a22b596d999eff7a8b400d94136c10adb66 100644 --- a/client/src/app/auth/init.keycloak.ts +++ b/client/src/app/auth/init.keycloak.ts @@ -28,7 +28,7 @@ export function initializeKeycloak(keycloak: KeycloakService, store: Store<{ }>, store.dispatch(keycloakActions.authSuccess()); } if (event.type === KeycloakEventType.OnAuthRefreshError) { - store.dispatch(keycloakActions.login()); + store.dispatch(keycloakActions.login({ redirectUri: window.location.toString() })); } }) diff --git a/client/src/app/core/containers/unauthorized.component.html b/client/src/app/core/containers/unauthorized.component.html index 0f725cf52e46f708e499b2b941f5c0a26066a944..415ff094da3c5719714f681efed872846155c15a 100644 --- a/client/src/app/core/containers/unauthorized.component.html +++ b/client/src/app/core/containers/unauthorized.component.html @@ -3,10 +3,27 @@ <div class="text-center"> <img class="mb-4" src="assets/cesam_anis80.png" alt="ANIS logo"> - <p> + <p *ngIf="(isAuthenticated | async)"> You are not authorized to navigate to this interface (403).<br /> Please contact the administrator to increase your access rights. </p> + + <p *ngIf="!(isAuthenticated | async)"> + You are not logged in and therefore you are not allowed to continue on this page (401).<br /> + If you want to continue please sign in. + </p> + + <p> + <a routerLink="/" role="button" class="btn btn-primary"> + <span class="fa-solid fa-circle-left"></span> + Go back to portal + </a> + + <a *ngIf="!(isAuthenticated | async)" (click)="login()" role="button" class="btn btn-warning"> + <span class="fa-solid fa-circle-up"></span> + Sign in + </a> + </p> </div> </div> </main> diff --git a/client/src/app/core/containers/unauthorized.component.ts b/client/src/app/core/containers/unauthorized.component.ts index ad4d069214ca321fbb10a2b87fd2a3df7268f776..c98dc4d05a45b66f5772660acb016033776d93e6 100644 --- a/client/src/app/core/containers/unauthorized.component.ts +++ b/client/src/app/core/containers/unauthorized.component.ts @@ -9,6 +9,12 @@ import { Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Store } from '@ngrx/store'; + +import * as authActions from 'src/app/auth/auth.actions'; +import * as authSelector from 'src/app/auth/auth.selector'; + /** * @class * @classdesc Unauthorized container. @@ -17,4 +23,18 @@ import { Component } from '@angular/core'; selector: 'app-unauthorized', templateUrl: 'unauthorized.component.html' }) -export class UnauthorizedComponent { } +export class UnauthorizedComponent { + public isAuthenticated: Observable<boolean>; + + constructor(private store: Store<{ }>) { + this.isAuthenticated = this.store.select(authSelector.selectIsAuthenticated); + } + + login() { + let redirectUri = sessionStorage.getItem('redirect_uri'); + if (!redirectUri) { + redirectUri = '/'; + } + this.store.dispatch(authActions.login({ redirectUri })); + } +} diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index 76633d13679147649ab2094c02e23956de6c4893..2445f41b5950eb5ea5866cd58d68c2e33ca0d8c4 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts @@ -15,10 +15,12 @@ import { ToastrModule } from 'ngx-toastr'; import { AppComponent } from './containers/app.component'; import { NotFoundPageComponent } from './containers/not-found-page.component'; +import { UnauthorizedComponent } from './containers/unauthorized.component'; export const COMPONENTS = [ AppComponent, - NotFoundPageComponent + NotFoundPageComponent, + UnauthorizedComponent ]; /** diff --git a/client/src/app/instance/instance-auth.guard.ts b/client/src/app/instance/instance-auth.guard.ts index cc3bd6d531b6df6ecb4a1ff2c077978536eb3721..59fd9a2c03ac12bb4ed174ac0b38f7e17fc639aa 100644 --- a/client/src/app/instance/instance-auth.guard.ts +++ b/client/src/app/instance/instance-auth.guard.ts @@ -16,7 +16,6 @@ import { switchMap, map, skipWhile } from 'rxjs/operators'; import * as instanceSelector from 'src/app/metamodel/selectors/instance.selector'; import * as instanceGroupSelector from 'src/app/metamodel/selectors/instance-group.selector'; -import * as authActions from 'src/app/auth/auth.actions'; import * as authSelector from 'src/app/auth/auth.selector'; import { AppConfigService } from 'src/app/app-config.service'; import { isAdmin } from 'src/app/shared/utils'; @@ -49,19 +48,18 @@ export class InstanceAuthGuard implements CanActivate { return true; } - // Force the user to log in if currently unauthenticated. + // If user is authenticated and authorized so accessible changes to true + let accessible = false; if (!isAuthenticated) { - this.store.dispatch(authActions.login()); - return false; + accessible = instanceGroupList + .filter(instanceGroup => instanceGroup.instances.includes(instance.name)) + .filter(instanceGroup => userRoles.includes(instanceGroup.role)) + .length > 0; } - - // If authenticated but not authorized go to unauthorized page. - let accessible = instanceGroupList - .filter(instanceGroup => instanceGroup.instances.includes(instance.name)) - .filter(instanceGroup => userRoles.includes(instanceGroup.role)) - .length > 0; + // If user is not authorized to continue go to unauthorized page if (!accessible) { + sessionStorage.setItem('redirect_uri', window.location.toString()); this.router.navigateByUrl('/unauthorized'); return false; } diff --git a/client/src/app/instance/instance.component.ts b/client/src/app/instance/instance.component.ts index 6f7540f87db40bf25bd0713419523d708a1640d7..dc692bd80d951ea5908d6529174138ef802a78ad 100644 --- a/client/src/app/instance/instance.component.ts +++ b/client/src/app/instance/instance.component.ts @@ -139,7 +139,7 @@ export class InstanceComponent implements OnInit, OnDestroy { * Dispatches action to log in. */ login(): void { - this.store.dispatch(authActions.login()); + this.store.dispatch(authActions.login({ redirectUri: window.location.toString() })); } /** diff --git a/client/src/app/instance/search/search-auth.guard.ts b/client/src/app/instance/search/search-auth.guard.ts index 4e07f4dc6bd82dab2c965d7bae46a5eb3ea33ffb..591165ba38306129f28b13760beec6c9b345b8a6 100644 --- a/client/src/app/instance/search/search-auth.guard.ts +++ b/client/src/app/instance/search/search-auth.guard.ts @@ -11,14 +11,13 @@ import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { Store, select } from '@ngrx/store'; -import { combineLatest, Observable, of } from 'rxjs'; +import { combineLatest, Observable } from 'rxjs'; import { switchMap, map, skipWhile } from 'rxjs/operators'; import * as datasetSelector from 'src/app/metamodel/selectors/dataset.selector'; import * as datasetActions from 'src/app/metamodel/actions/dataset.actions'; import * as datasetGroupSelector from 'src/app/metamodel/selectors/dataset-group.selector'; import * as datasetGroupActions from 'src/app/metamodel/actions/dataset-group.actions'; -import * as authActions from 'src/app/auth/auth.actions'; import * as authSelector from 'src/app/auth/auth.selector'; import { AppConfigService } from 'src/app/app-config.service'; import { isAdmin } from 'src/app/shared/utils'; @@ -62,20 +61,19 @@ export class SearchAuthGuard implements CanActivate { || (isAuthenticated && isAdmin(this.config.adminRoles, userRoles))) { return true; } - - // Force the user to log in if currently unauthenticated. + + // If user is authenticated and authorized so accessible changes to true + let accessible = false if (!isAuthenticated) { - this.store.dispatch(authActions.login()); - return false; + accessible = datasetGroupList + .filter(datasetGroup => datasetGroup.datasets.includes(dataset.name)) + .filter(datasetGroup => userRoles.includes(datasetGroup.role)) + .length > 0; } - - // If authenticated but not authorized go to unauthorized page. - let accessible = datasetGroupList - .filter(datasetGroup => datasetGroup.datasets.includes(dataset.name)) - .filter(datasetGroup => userRoles.includes(datasetGroup.role)) - .length > 0; + // If user is not authorized to continue go to unauthorized page if (!accessible) { + sessionStorage.setItem('redirect_uri', window.location.toString()); this.router.navigateByUrl('/unauthorized'); return false; } diff --git a/client/src/app/portal/containers/portal-home.component.ts b/client/src/app/portal/containers/portal-home.component.ts index e9f442615c9ccff98850aaf40b032242fc2b71de..ceb30652b6bd4251dfe7971389d5f02bb1ca515c 100644 --- a/client/src/app/portal/containers/portal-home.component.ts +++ b/client/src/app/portal/containers/portal-home.component.ts @@ -100,7 +100,7 @@ export class PortalHomeComponent implements OnInit { * Dispatches action to log in. */ login(): void { - this.store.dispatch(authActions.login()); + this.store.dispatch(authActions.login({ redirectUri: window.location.toString() })); } /** diff --git a/server/app/dependencies.php b/server/app/dependencies.php index 6cffc41a97fe67cc767ef5713abcf5d078b1caad..f210549049573958cede080d8ffb581d3e325e36 100644 --- a/server/app/dependencies.php +++ b/server/app/dependencies.php @@ -130,7 +130,7 @@ $container->set('App\Action\DatasetGroupAction', function (ContainerInterface $c }); $container->set('App\Action\InstanceListAction', function (ContainerInterface $c) { - return new App\Action\InstanceListAction($c->get('em'), $c->get(SETTINGS)['token']); + return new App\Action\InstanceListAction($c->get('em')); }); $container->set('App\Action\InstanceAction', function (ContainerInterface $c) { @@ -146,7 +146,7 @@ $container->set('App\Action\DatasetFamilyListAction', function (ContainerInterfa }); $container->set('App\Action\DatasetListByInstanceAction', function (ContainerInterface $c) { - return new App\Action\DatasetListByInstanceAction($c->get('em'), $c->get(SETTINGS)['token']); + return new App\Action\DatasetListByInstanceAction($c->get('em')); }); $container->set('App\Action\DatasetFamilyAction', function (ContainerInterface $c) { diff --git a/server/src/Action/DatasetListByInstanceAction.php b/server/src/Action/DatasetListByInstanceAction.php index 414ad6f48d4936381da1ed1bf1b1b7d6b4651086..30d5184ff4eb512921ff8e3d9920eed80066607c 100644 --- a/server/src/Action/DatasetListByInstanceAction.php +++ b/server/src/Action/DatasetListByInstanceAction.php @@ -23,23 +23,14 @@ use Slim\Exception\HttpNotFoundException; */ final class DatasetListByInstanceAction extends AbstractAction { - /** - * Contains settings to handle Json Web Token - * - * @var array - */ - private $settings; - /** * Create the classe before call __invoke to execute the action * * @param EntityManagerInterface $em Doctrine Entity Manager Interface - * @param array $settings Settings about token */ - public function __construct(EntityManagerInterface $em, array $settings) + public function __construct(EntityManagerInterface $em) { parent::__construct($em); - $this->settings = $settings; } /** diff --git a/server/src/Action/InstanceListAction.php b/server/src/Action/InstanceListAction.php index f5a3a43306afd59dcba50554cb90de38ae32a494..f27cb9d5e48059f293899b75c760a3669f7b1ea7 100644 --- a/server/src/Action/InstanceListAction.php +++ b/server/src/Action/InstanceListAction.php @@ -24,23 +24,14 @@ use App\Entity\Instance; */ final class InstanceListAction extends AbstractAction { - /** - * Contains settings to handle Json Web Token - * - * @var array - */ - private $settings; - /** * Create the classe before call __invoke to execute the action * * @param EntityManagerInterface $em Doctrine Entity Manager Interface - * @param array $settings Settings about token */ - public function __construct(EntityManagerInterface $em, array $settings) + public function __construct(EntityManagerInterface $em) { parent::__construct($em); - $this->settings = $settings; } /** diff --git a/server/tests/Action/InstanceListActionTest.php b/server/tests/Action/InstanceListActionTest.php index bd1b515b9b660db6eaa940cb6a358935ec20dd33..0e646684e61ec2cfeb04ad266010455a32939b8e 100644 --- a/server/tests/Action/InstanceListActionTest.php +++ b/server/tests/Action/InstanceListActionTest.php @@ -17,9 +17,7 @@ use Nyholm\Psr7\ServerRequest; use Nyholm\Psr7\Response; use Slim\Exception\HttpBadRequestException; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\QueryBuilder; -use Doctrine\ORM\AbstractQuery; -use Doctrine\ORM\Query\Expr; +use Doctrine\Persistence\ObjectRepository; final class InstanceListActionTest extends TestCase { @@ -28,12 +26,8 @@ final class InstanceListActionTest extends TestCase protected function setUp(): void { - $settings = array( - 'enabled' => '0', - 'admin_role' => 'anis_admin' - ); $this->entityManager = $this->createMock(EntityManager::class); - $this->action = new \App\Action\InstanceListAction($this->entityManager, $settings); + $this->action = new \App\Action\InstanceListAction($this->entityManager); } public function testOptionsHttpMethod(): void @@ -45,18 +39,9 @@ final class InstanceListActionTest extends TestCase public function testGetAllInstances(): void { - $expr = $this->getExprMock(); - $query = $this->getAbstractQueryMock(); - $query->expects($this->once())->method('getResult'); - - $queryBuilder = $this->getQueryBuilderMock(); - $queryBuilder->method('select')->willReturn($queryBuilder); - $queryBuilder->method('from')->willReturn($queryBuilder); - $queryBuilder->method('join')->willReturn($queryBuilder); - $queryBuilder->method('expr')->willReturn($expr); - $queryBuilder->expects($this->once())->method('getQuery')->willReturn($query); - - $this->entityManager->method('createQueryBuilder')->willReturn($queryBuilder); + $repository = $this->getObjectRepositoryMock(); + $repository->expects($this->once())->method('findAll'); + $this->entityManager->method('getRepository')->with('App\Entity\Instance')->willReturn($repository); $request = $this->getRequest('GET'); ($this->action)($request, new Response(), array()); @@ -117,26 +102,10 @@ final class InstanceListActionTest extends TestCase } /** - * @return Expr|\PHPUnit\Framework\MockObject\MockObject - */ - private function getExprMock() - { - return $this->createMock(Expr::class); - } - - /** - * @return AbstractQuery|\PHPUnit\Framework\MockObject\MockObject - */ - private function getAbstractQueryMock() - { - return $this->createMock(AbstractQuery::class); - } - - /** - * @return QueryBuilder|\PHPUnit\Framework\MockObject\MockObject + * @return ObjectRepository|\PHPUnit\Framework\MockObject\MockObject */ - private function getQueryBuilderMock() + private function getObjectRepositoryMock() { - return $this->createMock(QueryBuilder::class); + return $this->createMock(ObjectRepository::class); } }