diff --git a/client/src/app/portal/containers/portal-home.component.html b/client/src/app/portal/containers/portal-home.component.html index ed9b40ec610802e661884e0970100380fe001a49..39a2bfe5fdb3827ba089f847134873c39e000d59 100644 --- a/client/src/app/portal/containers/portal-home.component.html +++ b/client/src/app/portal/containers/portal-home.component.html @@ -15,6 +15,15 @@ </header> <main role="main" class="container-fluid pb-4"> <div class="container"> + <ng-container *ngIf="(instanceList | async).length === 0"> + <div class="col-12 lead text-center font-weight-bold"> + Oops! No instances available... + <span *ngIf="!(isAuthenticated | async)"> + Try to sign in to access to protected instances. + </span> + </div> + </ng-container> + <div class="row justify-content-center"> <div class="col-auto mb-3" *ngFor="let instance of (instanceList | async)"> <app-instance-card diff --git a/docker-compose.yml b/docker-compose.yml index 06b8074d7e6be4d1320d0781535d17d99d006895..e980ced06a1ec80546a72808816c8113c60d616e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,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/app/dependencies.php b/server/app/dependencies.php index d59b3c2da15111ec36314616463425b2f03e2ce5..0bb8ed844292d1b02d28662c06b24b2034ba56be 100644 --- a/server/app/dependencies.php +++ b/server/app/dependencies.php @@ -138,7 +138,7 @@ $container->set('App\Action\GroupAction', function (ContainerInterface $c) { }); $container->set('App\Action\InstanceListAction', function (ContainerInterface $c) { - return new App\Action\InstanceListAction($c->get('em')); + return new App\Action\InstanceListAction($c->get('em'), $c->get(SETTINGS)['token']); }); $container->set('App\Action\InstanceAction', function (ContainerInterface $c) { diff --git a/server/src/Action/AbstractAction.php b/server/src/Action/AbstractAction.php index eea4104761773564e8603da0411d1bd001646a72..8bb4537f73a3ceab4a8f8f14dfd02334d8a72d60 100644 --- a/server/src/Action/AbstractAction.php +++ b/server/src/Action/AbstractAction.php @@ -85,6 +85,40 @@ abstract class AbstractAction } } + /** + * @param ServerRequestInterface $request PSR-7 This object represents the HTTP request + * @param string $instanceName + * @param array $adminRoles + */ + protected function verifyInstanceAuthorization( + ServerRequestInterface $request, + string $instanceName, + array $adminRoles + ) { + $token = $request->getAttribute('token'); + if (!$token) { + // The user is not connected (401) + throw new HttpUnauthorizedException($request); + } + $roles = $token->realm_access->roles; + if (!$this->isAdmin($adminRoles, $roles)) { + $qb = $this->em->createQueryBuilder(); + $qb->select('i.name') + ->from('App\Entity\InstanceGroup', 'ig') + ->join('ig.instances', 'i') + ->where($qb->expr()->in('ig.role', $roles)) + ->andWhere($qb->expr()->eq('i.name', ':iname')); + $qb->setParameter('iname', $instanceName); + $r = $qb->getQuery()->getResult(); + if (count($r) < 1) { + throw new HttpForbiddenException( + $request, + 'You do not have the permission to access the instance : ' . $instanceName + ); + } + } + } + protected function isAdmin(array $adminRoles, $roles) { $admin = false; diff --git a/server/src/Action/DatasetFileExplorerAction.php b/server/src/Action/DatasetFileExplorerAction.php index 5dd9412f01dfb0e3a028d24a384a158de9e01ea5..077b0be78ec5389c0ba9ae69aea09b322370154d 100644 --- a/server/src/Action/DatasetFileExplorerAction.php +++ b/server/src/Action/DatasetFileExplorerAction.php @@ -82,6 +82,16 @@ final class DatasetFileExplorerAction extends AbstractAction ); } + // If instance is private and authorization enabled + $instance = $dataset->getDatasetFamily()->getInstance(); + if (!$instance->getPublic() && boolval($this->settings['enabled'])) { + $this->verifyInstanceAuthorization( + $request, + $instance->getName(), + explode(',', $this->settings['admin_roles']) + ); + } + // If dataset is private and authorization enabled if (!$dataset->getPublic() && boolval($this->settings['enabled'])) { $this->verifyDatasetAuthorization( diff --git a/server/src/Action/DownloadArchiveAction.php b/server/src/Action/DownloadArchiveAction.php index 5be57fd8f40702566792e875566a4db79ce34563..68920a8bbad5875d15d99e74c5c935b6c3fdaad6 100644 --- a/server/src/Action/DownloadArchiveAction.php +++ b/server/src/Action/DownloadArchiveAction.php @@ -90,6 +90,16 @@ final class DownloadArchiveAction extends AbstractAction ); } + // If instance is private and authorization enabled + $instance = $dataset->getDatasetFamily()->getInstance(); + if (!$instance->getPublic() && boolval($this->settings['enabled'])) { + $this->verifyInstanceAuthorization( + $request, + $instance->getName(), + explode(',', $this->settings['admin_roles']) + ); + } + // If dataset is private and authorization enabled if (!$dataset->getPublic() && boolval($this->settings['enabled'])) { $this->verifyDatasetAuthorization( diff --git a/server/src/Action/DownloadFileAction.php b/server/src/Action/DownloadFileAction.php index ebce967ae377bf29c34933d53bb31218115d4b50..196a0ad330f94e244183001b50c464350519536c 100644 --- a/server/src/Action/DownloadFileAction.php +++ b/server/src/Action/DownloadFileAction.php @@ -82,6 +82,16 @@ final class DownloadFileAction extends AbstractAction ); } + // If instance is private and authorization enabled + $instance = $dataset->getDatasetFamily()->getInstance(); + if (!$instance->getPublic() && boolval($this->settings['enabled'])) { + $this->verifyInstanceAuthorization( + $request, + $instance->getName(), + explode(',', $this->settings['admin_roles']) + ); + } + // If dataset is private and authorization enabled if (!$dataset->getPublic() && boolval($this->settings['enabled'])) { $this->verifyDatasetAuthorization( diff --git a/server/src/Action/DownloadResultAction.php b/server/src/Action/DownloadResultAction.php index 6d5f7eb886cc4dfc0b9eef8c7432ebf6641ea007..41a996c7925976d00b187cfc9def5591bab759fb 100644 --- a/server/src/Action/DownloadResultAction.php +++ b/server/src/Action/DownloadResultAction.php @@ -90,6 +90,16 @@ final class DownloadResultAction extends AbstractAction ); } + // If instance is private and authorization enabled + $instance = $dataset->getDatasetFamily()->getInstance(); + if (!$instance->getPublic() && boolval($this->settings['enabled'])) { + $this->verifyInstanceAuthorization( + $request, + $instance->getName(), + explode(',', $this->settings['admin_roles']) + ); + } + // If dataset is private and authorization enabled if (!$dataset->getPublic() && boolval($this->settings['enabled'])) { $this->verifyDatasetAuthorization( diff --git a/server/src/Action/InstanceListAction.php b/server/src/Action/InstanceListAction.php index 9aa4c2a9c5666785b74fba44dd41a3d37c305b8a..1dc9f4cb11e8c1c66653c4489790ef7a5b3d3daf 100644 --- a/server/src/Action/InstanceListAction.php +++ b/server/src/Action/InstanceListAction.php @@ -14,6 +14,7 @@ namespace App\Action; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; +use Doctrine\ORM\EntityManagerInterface; use Slim\Exception\HttpBadRequestException; use App\Entity\Instance; @@ -23,6 +24,25 @@ 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) + { + parent::__construct($em); + $this->settings = $settings; + } + /** * `GET` Returns a list of all instances listed in the metamodel * `POST` Add a new instance @@ -43,7 +63,8 @@ final class InstanceListAction extends AbstractAction } if ($request->getMethod() === GET) { - $instances = $this->em->getRepository('App\Entity\Instance')->findAll(); + //$instances = $this->em->getRepository('App\Entity\Instance')->findAll(); + $instances = $this->getInstanceList($request->getAttribute('token')); $payload = json_encode($instances); } @@ -69,6 +90,35 @@ 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 * diff --git a/server/src/Action/SearchAction.php b/server/src/Action/SearchAction.php index 3050de339540c4e16c2e01d380b8bfbf6af4608c..52051a9ecb37702f490826198ed6b38549672a1c 100644 --- a/server/src/Action/SearchAction.php +++ b/server/src/Action/SearchAction.php @@ -105,6 +105,16 @@ final class SearchAction extends AbstractAction ); } + // If instance is private and authorization enabled + $instance = $dataset->getDatasetFamily()->getInstance(); + if (!$instance->getPublic() && boolval($this->settings['enabled'])) { + $this->verifyInstanceAuthorization( + $request, + $instance->getName(), + explode(',', $this->settings['admin_roles']) + ); + } + // If dataset is private and authorization enabled if (!$dataset->getPublic() && boolval($this->settings['enabled'])) { $this->verifyDatasetAuthorization( diff --git a/server/src/Action/StartTaskCreateArchiveAction.php b/server/src/Action/StartTaskCreateArchiveAction.php index e98bbf4df78319e5bcce27667c6a88fc8614863d..f9c0f7bff664c06f4d924850f1bee70fbf194173 100644 --- a/server/src/Action/StartTaskCreateArchiveAction.php +++ b/server/src/Action/StartTaskCreateArchiveAction.php @@ -87,8 +87,20 @@ final class StartTaskCreateArchiveAction extends AbstractAction ); } - // If dataset is private and authorization enabled $token = ''; + + // If instance is private and authorization enabled + $instance = $dataset->getDatasetFamily()->getInstance(); + if (!$instance->getPublic() && boolval($this->settings['enabled'])) { + $this->verifyInstanceAuthorization( + $request, + $instance->getName(), + explode(',', $this->settings['admin_roles']) + ); + $token = $request->getHeader('Authorization')[0]; + } + + // If dataset is private and authorization enabled if (!$dataset->getPublic() && boolval($this->settings['enabled'])) { $this->verifyDatasetAuthorization( $request, diff --git a/server/src/Action/StartTaskCreateResultAction.php b/server/src/Action/StartTaskCreateResultAction.php index 57737286dcdc43f28a833b3200aa673f244ca43b..fc1e34e044b412c3b9181e913a364a4d30fa42c5 100644 --- a/server/src/Action/StartTaskCreateResultAction.php +++ b/server/src/Action/StartTaskCreateResultAction.php @@ -87,8 +87,20 @@ final class StartTaskCreateResultAction extends AbstractAction ); } - // If dataset is private and authorization enabled $token = ''; + + // If instance is private and authorization enabled + $instance = $dataset->getDatasetFamily()->getInstance(); + if (!$instance->getPublic() && boolval($this->settings['enabled'])) { + $this->verifyInstanceAuthorization( + $request, + $instance->getName(), + explode(',', $this->settings['admin_roles']) + ); + $token = $request->getHeader('Authorization')[0]; + } + + // If dataset is private and authorization enabled if (!$dataset->getPublic() && boolval($this->settings['enabled'])) { $this->verifyDatasetAuthorization( $request, @@ -97,7 +109,7 @@ final class StartTaskCreateResultAction extends AbstractAction ); $token = $request->getHeader('Authorization')[0]; } - + $queryParams = $request->getQueryParams(); // The parameter "a" is mandatory diff --git a/server/tests/Action/InstanceActionTest.php b/server/tests/Action/InstanceActionTest.php index 4f67cde95120c746dd41c8dcb779d51dadac5d3d..2b79c1c9bb3fe89649f922165ede0d398c9fb557 100644 --- a/server/tests/Action/InstanceActionTest.php +++ b/server/tests/Action/InstanceActionTest.php @@ -82,6 +82,7 @@ final class InstanceActionTest extends TestCase 'description' => 'Test', 'display' => 10, 'data_path' => '/DEFAULT', + 'public' => true, 'portal_logo' => '', 'design_color' => '#7AC29A', 'design_background_color' => '#FFFFFF', diff --git a/server/tests/Action/InstanceListActionTest.php b/server/tests/Action/InstanceListActionTest.php index bfaa7b7b1355687ecd6186c4487a0d2eec6f2b06..d263fcddf670642c17f2e1ebad7a061b08a233be 100644 --- a/server/tests/Action/InstanceListActionTest.php +++ b/server/tests/Action/InstanceListActionTest.php @@ -17,7 +17,9 @@ use Nyholm\Psr7\ServerRequest; use Nyholm\Psr7\Response; use Slim\Exception\HttpBadRequestException; use Doctrine\ORM\EntityManager; -use Doctrine\Persistence\ObjectRepository; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\AbstractQuery; +use Doctrine\ORM\Query\Expr; final class InstanceListActionTest extends TestCase { @@ -26,8 +28,12 @@ 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); + $this->action = new \App\Action\InstanceListAction($this->entityManager, $settings); } public function testOptionsHttpMethod(): void @@ -39,9 +45,18 @@ final class InstanceListActionTest extends TestCase public function testGetAllInstances(): void { - $repository = $this->getObjectRepositoryMock(); - $repository->expects($this->once())->method('findAll'); - $this->entityManager->method('getRepository')->with('App\Entity\Instance')->willReturn($repository); + $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); $request = $this->getRequest('GET'); ($this->action)($request, new Response(), array()); @@ -66,6 +81,7 @@ final class InstanceListActionTest extends TestCase 'description' => 'Test', 'display' => 10, 'data_path' => '/DEFAULT', + 'public' => true, 'portal_logo' => '', 'design_color' => '#7AC29A', 'design_background_color' => '#FFFFFF', @@ -96,10 +112,26 @@ final class InstanceListActionTest extends TestCase } /** - * @return ObjectRepository|\PHPUnit\Framework\MockObject\MockObject + * @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 */ - private function getObjectRepositoryMock() + private function getQueryBuilderMock() { - return $this->createMock(ObjectRepository::class); + return $this->createMock(QueryBuilder::class); } }