Commit 72b7d701 authored by François Agneray's avatar François Agneray
Browse files

Merge branch '58-prise-en-compte-des-privileges' into 'develop'

Resolve "Prise en compte des privileges"

Closes #58

See merge request !55
parents 41c8352b 7c0c15ee
Pipeline #3712 failed with stages
in 24 seconds
...@@ -121,7 +121,7 @@ $container->set('App\Action\DatasetFamilyListAction', function (ContainerInterfa ...@@ -121,7 +121,7 @@ $container->set('App\Action\DatasetFamilyListAction', function (ContainerInterfa
}); });
$container->set('App\Action\DatasetListByInstanceAction', function (ContainerInterface $c) { $container->set('App\Action\DatasetListByInstanceAction', function (ContainerInterface $c) {
return new App\Action\DatasetListByInstanceAction($c->get('em')); return new App\Action\DatasetListByInstanceAction($c->get('em'), $c->get(SETTINGS)['token']);
}); });
$container->set('App\Action\DatasetFamilyAction', function (ContainerInterface $c) { $container->set('App\Action\DatasetFamilyAction', function (ContainerInterface $c) {
...@@ -180,10 +180,11 @@ $container->set('App\Action\SearchAction', function (ContainerInterface $c) { ...@@ -180,10 +180,11 @@ $container->set('App\Action\SearchAction', function (ContainerInterface $c) {
return new App\Action\SearchAction( return new App\Action\SearchAction(
$c->get('em'), $c->get('em'),
new App\Utils\DBALConnectionFactory(), new App\Utils\DBALConnectionFactory(),
new App\Utils\Operator\OperatorFactory() new App\Utils\Operator\OperatorFactory(),
$c->get(SETTINGS)['token']
); );
}); });
$container->set('App\Action\DownloadFileAction', function (ContainerInterface $c) { $container->set('App\Action\DownloadFileAction', function (ContainerInterface $c) {
return new App\Action\DownloadFileAction($c->get('em')); return new App\Action\DownloadFileAction($c->get('em'), $c->get(SETTINGS)['token']);
}); });
...@@ -22,6 +22,9 @@ $app->group('', function (RouteCollectorProxy $group) { ...@@ -22,6 +22,9 @@ $app->group('', function (RouteCollectorProxy $group) {
$group->map([OPTIONS, GET, POST], '/database', App\Action\DatabaseListAction::class); $group->map([OPTIONS, GET, POST], '/database', App\Action\DatabaseListAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/database/{id}', App\Action\DatabaseAction::class); $group->map([OPTIONS, GET, PUT, DELETE], '/database/{id}', App\Action\DatabaseAction::class);
$group->map([OPTIONS, GET], '/database/{id}/table', App\Action\TableListAction::class); $group->map([OPTIONS, GET], '/database/{id}/table', App\Action\TableListAction::class);
})->add(new App\Middleware\AdminMiddleware($container->get(SETTINGS)['token'], false));
$app->group('', function (RouteCollectorProxy $group) {
$group->map([OPTIONS, GET, POST], '/project', App\Action\ProjectListAction::class); $group->map([OPTIONS, GET, POST], '/project', App\Action\ProjectListAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/project/{name}', App\Action\ProjectAction::class); $group->map([OPTIONS, GET, PUT, DELETE], '/project/{name}', App\Action\ProjectAction::class);
$group->map([OPTIONS, GET, POST], '/instance', App\Action\InstanceListAction::class); $group->map([OPTIONS, GET, POST], '/instance', App\Action\InstanceListAction::class);
...@@ -51,7 +54,7 @@ $app->group('', function (RouteCollectorProxy $group) { ...@@ -51,7 +54,7 @@ $app->group('', function (RouteCollectorProxy $group) {
'/dataset/{name}/attribute/{id}/distinct', '/dataset/{name}/attribute/{id}/distinct',
App\Action\AttributeDistinctAction::class App\Action\AttributeDistinctAction::class
); );
})->add(new App\Middleware\AdminMiddleware($container->get(SETTINGS)['token'])); })->add(new App\Middleware\AdminMiddleware($container->get(SETTINGS)['token'], true));
$app->get('/search/{dname}', App\Action\SearchAction::class); $app->get('/search/{dname}', App\Action\SearchAction::class);
$app->get('/download-file/{dname}/[{fpath:.*}]', App\Action\DownloadFileAction::class); $app->get('/download-file/{dname}/[{fpath:.*}]', App\Action\DownloadFileAction::class);
...@@ -13,6 +13,9 @@ declare(strict_types=1); ...@@ -13,6 +13,9 @@ declare(strict_types=1);
namespace App\Action; namespace App\Action;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpUnauthorizedException;
use Slim\Exception\HttpForbiddenException;
/** /**
* Centralize access to the database and some common functions * Centralize access to the database and some common functions
...@@ -49,4 +52,35 @@ abstract class AbstractAction ...@@ -49,4 +52,35 @@ abstract class AbstractAction
{ {
return !isset($parsedBody[$field]); return !isset($parsedBody[$field]);
} }
/**
* @param Request $request
* @param string $datasetName
* @param string $adminRole
*/
protected function verifyDatasetAuthorization(Request $request, string $datasetName, string $adminRole)
{
$token = $request->getAttribute('token');
if (!$token) {
// The user is not connected (401)
throw new HttpUnauthorizedException($request);
}
$roles = $token->getClaim('realm_access')->roles;
if (!in_array($adminRole, $roles)) {
$qb = $this->em->createQueryBuilder();
$qb->select('d.name')
->from('App\Entity\Group', 'g')
->join('g.datasets', 'd')
->where($qb->expr()->in('g.role', $roles))
->andWhere($qb->expr()->eq('d.name', ':dname'));
$qb->setParameter('dname', $datasetName);
$r = $qb->getQuery()->getResult();
if (count($r) < 1) {
throw new HttpForbiddenException(
$request,
'You do not have the permission to access the dataset : ' . $datasetName
);
}
}
}
} }
...@@ -14,10 +14,30 @@ namespace App\Action; ...@@ -14,10 +14,30 @@ namespace App\Action;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Doctrine\ORM\EntityManagerInterface;
use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpNotFoundException;
final class DatasetListByInstanceAction extends AbstractAction 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)
{
parent::__construct($em);
$this->settings = $settings;
}
/** /**
* `GET` Returns a list of all datasets for a given instance * `GET` Returns a list of all datasets for a given instance
* *
...@@ -44,12 +64,35 @@ final class DatasetListByInstanceAction extends AbstractAction ...@@ -44,12 +64,35 @@ final class DatasetListByInstanceAction extends AbstractAction
} }
if ($request->getMethod() === GET) { if ($request->getMethod() === GET) {
$token = $request->getAttribute('token');
$qb = $this->em->createQueryBuilder(); $qb = $this->em->createQueryBuilder();
$qb->select('d') $qb->select('d')
->from('App\Entity\Dataset', 'd') ->from('App\Entity\Dataset', 'd')
->join('d.datasetFamily', 'f') ->join('d.datasetFamily', 'f')
->where($qb->expr()->eq('IDENTITY(f.instance)', ':instanceName')) ->where($qb->expr()->eq('IDENTITY(f.instance)', ':instanceName'));
->setParameter('instanceName', $instance->getName());
if (boolval($this->settings['enabled'])) {
if (!$token) {
// If user is not connected return public datasets
$qb->andWhere($qb->expr()->eq('d.public', 'true'));
} else {
$roles = $token->getClaim('realm_access')->roles;
if (!in_array($this->settings['admin_role'], $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\Group', '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(); $datasets = $qb->getQuery()->getResult();
$payload = json_encode($datasets); $payload = json_encode($datasets);
} }
......
...@@ -14,11 +14,31 @@ namespace App\Action; ...@@ -14,11 +14,31 @@ namespace App\Action;
use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ResponseInterface as Response;
use Doctrine\ORM\EntityManagerInterface;
use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpNotFoundException;
use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\Factory\Psr17Factory;
final class DownloadFileAction extends AbstractAction final class DownloadFileAction 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 the file found * `GET` Returns the file found
* *
...@@ -45,6 +65,11 @@ final class DownloadFileAction extends AbstractAction ...@@ -45,6 +65,11 @@ final class DownloadFileAction extends AbstractAction
); );
} }
// If dataset is private and authorization enabled
if (!$dataset->getPublic() && boolval($this->settings['enabled'])) {
$this->verifyDatasetAuthorization($request, $dataset->getName(), $this->settings['admin_role']);
}
// Search the file // Search the file
$filePath = realpath($dataset->getDataPath()) . DIRECTORY_SEPARATOR . $args['fpath']; $filePath = realpath($dataset->getDataPath()) . DIRECTORY_SEPARATOR . $args['fpath'];
......
...@@ -37,20 +37,30 @@ final class SearchAction extends AbstractAction ...@@ -37,20 +37,30 @@ final class SearchAction extends AbstractAction
private $connectionFactory; private $connectionFactory;
private $operatorFactory; private $operatorFactory;
/**
* Contains settings to handle Json Web Token
*
* @var array
*/
private $settings;
/** /**
* Create the classe before call __invoke to execute the action * Create the classe before call __invoke to execute the action
* *
* @param EntityManagerInterface $em Doctrine Entity Manager Interface * @param EntityManagerInterface $em Doctrine Entity Manager Interface
* @param DBALConnectionFactory $connectionFactory Factory used to construct connection to business database * @param DBALConnectionFactory $connectionFactory Factory used to construct connection to business database
* @param array $settings Settings about token
*/ */
public function __construct( public function __construct(
EntityManagerInterface $em, EntityManagerInterface $em,
DBALConnectionFactory $connectionFactory, DBALConnectionFactory $connectionFactory,
OperatorFactory $operatorFactory OperatorFactory $operatorFactory,
array $settings
) { ) {
parent::__construct($em); parent::__construct($em);
$this->connectionFactory = $connectionFactory; $this->connectionFactory = $connectionFactory;
$this->operatorFactory = $operatorFactory; $this->operatorFactory = $operatorFactory;
$this->settings = $settings;
} }
/** /**
...@@ -79,6 +89,11 @@ final class SearchAction extends AbstractAction ...@@ -79,6 +89,11 @@ final class SearchAction extends AbstractAction
); );
} }
// If dataset is private and authorization enabled
if (!$dataset->getPublic() && boolval($this->settings['enabled'])) {
$this->verifyDatasetAuthorization($request, $dataset->getName(), $this->settings['admin_role']);
}
// Create query builder with from clause using dataset information // Create query builder with from clause using dataset information
$connection = $this->connectionFactory->create($dataset->getProject()->getDatabase()); $connection = $this->connectionFactory->create($dataset->getProject()->getDatabase());
$queryBuilder = $connection->createQueryBuilder(); $queryBuilder = $connection->createQueryBuilder();
......
...@@ -27,23 +27,33 @@ final class AdminMiddleware implements MiddlewareInterface ...@@ -27,23 +27,33 @@ final class AdminMiddleware implements MiddlewareInterface
*/ */
private $settings; private $settings;
/**
* If read === true method GET is authorized without permission
* If read === false method GET need token and permission
*
* @var bool
*/
private $read;
/** /**
* Create the classe before call process to execute this middleware * Create the classe before call process to execute this middleware
* *
* @param array $settings Settings about token * @param array $settings Settings about token
* @param bool $read If true GET is authorized without token
*/ */
public function __construct(array $settings) public function __construct(array $settings, bool $read)
{ {
$this->settings = $settings; $this->settings = $settings;
$this->read = $read;
} }
public function process(Request $request, RequestHandler $handler): Response public function process(Request $request, RequestHandler $handler): Response
{ {
if ( if ($request->getMethod() === OPTIONS || !boolval($this->settings['enabled'])) {
$request->getMethod() === OPTIONS return $handler->handle($request);
|| $request->getMethod() === GET }
|| $this->settings['enabled'] === 0
) { if ($this->read === true && $request->getMethod() === GET) {
return $handler->handle($request); return $handler->handle($request);
} }
......
...@@ -62,7 +62,7 @@ final class AuthorizationMiddleware implements MiddlewareInterface ...@@ -62,7 +62,7 @@ final class AuthorizationMiddleware implements MiddlewareInterface
if ( if (
$request->getMethod() === OPTIONS $request->getMethod() === OPTIONS
|| !$request->hasHeader('Authorization') || !$request->hasHeader('Authorization')
|| $this->settings['enabled'] === 0 || !boolval($this->settings['enabled'])
) { ) {
return $handler->handle($request); return $handler->handle($request);
} }
...@@ -94,19 +94,6 @@ final class AuthorizationMiddleware implements MiddlewareInterface ...@@ -94,19 +94,6 @@ final class AuthorizationMiddleware implements MiddlewareInterface
return $handler->handle($request->withAttribute('token', $token)); return $handler->handle($request->withAttribute('token', $token));
} }
// private function getPublicKey(string $issuer, string $kid): string
// {
// $urlOpenIdConfiguration = $issuer . '/.well-known/openid-configuration';
// $openIdConfiguration = json_decode(file_get_contents($urlOpenIdConfiguration), true);
// $jwksUri = $openIdConfiguration['jwks_uri'];
// $jwks = json_decode(file_get_contents($jwksUri), true);
// foreach ($jwks['keys'] as $jwk) {
// if ($jwk['kid'] === $kid) {
// return $jwk['x5c'];
// }
// }
// }
private function getUnauthorizedResponse(string $message) private function getUnauthorizedResponse(string $message)
{ {
$resonse = new NyholmResponse(); $resonse = new NyholmResponse();
......
...@@ -31,8 +31,12 @@ final class DatasetListByInstanceActionTest extends TestCase ...@@ -31,8 +31,12 @@ final class DatasetListByInstanceActionTest extends TestCase
protected function setUp(): void protected function setUp(): void
{ {
$settings = array(
'enabled' => '0',
'admin_role' => 'anis_admin'
);
$this->entityManager = EntityManagerBuilder::getInstance(); $this->entityManager = EntityManagerBuilder::getInstance();
$this->action = new \App\Action\DatasetListByInstanceAction($this->entityManager); $this->action = new \App\Action\DatasetListByInstanceAction($this->entityManager, $settings);
} }
public function testOptionsHttpMethod(): void public function testOptionsHttpMethod(): void
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment