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

Merge branch 'authorization' into 'develop'

Authorization

See merge request !54
parents c47ecc89 3335003f
Pipeline #3561 passed with stages
in 3 minutes and 16 seconds
......@@ -47,7 +47,7 @@ $app->group('', function (RouteCollectorProxy $group) {
'/dataset/{name}/attribute/{id}/distinct',
App\Action\AttributeDistinctAction::class
);
})->add(new App\Middleware\AdminMiddleware());
})->add(new App\Middleware\AdminMiddleware($container->get(SETTINGS)['token']));
$app->get('/search/{dname}', App\Action\SearchAction::class);
$app->get('/download-file/{dname}/[{fpath:.*}]', App\Action\DownloadFileAction::class);
......@@ -31,7 +31,9 @@ return [
'level' => getenv('LOGGER_LEVEL')
],
'token' => [
'issuer' => getenv('TOKEN_ISSUER'),
'public_key_file' => getenv('TOKEN_PUBLIC_KEY_FILE')
//'issuer' => getenv('TOKEN_ISSUER'),
'enabled' => getenv('TOKEN_ENABLED'),
'public_key_file' => getenv('TOKEN_PUBLIC_KEY_FILE'),
'admin_role' => getenv('TOKEN_ADMIN_ROLE')
]
];
......@@ -17,8 +17,9 @@ services:
LOGGER_NAME: "anis-metamodel"
LOGGER_PATH: "php://stderr"
LOGGER_LEVEL: "debug"
TOKEN_ISSUER: http://localhost:8180/auth/realms/anis
TOKEN_ENABLED: 1
TOKEN_PUBLIC_KEY_FILE: /data/public_key
TOKEN_ADMIN_ROLE: anis_admin
ports:
- 8080:80
volumes:
......
......@@ -64,10 +64,10 @@ class Dataset extends \App\Entity\Dataset implements \Doctrine\ORM\Proxy\Proxy
public function __sleep()
{
if ($this->__isInitialized__) {
return ['__isInitialized__', 'name', 'tableRef', 'label', 'description', 'display', 'count', 'vo', 'dataPath', 'selectableRow', 'project', 'datasetFamily', 'attributes'];
return ['__isInitialized__', 'name', 'tableRef', 'label', 'description', 'display', 'count', 'vo', 'dataPath', 'config', 'public', 'project', 'datasetFamily', 'attributes'];
}
return ['__isInitialized__', 'name', 'tableRef', 'label', 'description', 'display', 'count', 'vo', 'dataPath', 'selectableRow', 'project', 'datasetFamily', 'attributes'];
return ['__isInitialized__', 'name', 'tableRef', 'label', 'description', 'display', 'count', 'vo', 'dataPath', 'config', 'public', 'project', 'datasetFamily', 'attributes'];
}
/**
......@@ -345,23 +345,45 @@ class Dataset extends \App\Entity\Dataset implements \Doctrine\ORM\Proxy\Proxy
/**
* {@inheritDoc}
*/
public function getSelectableRow()
public function getConfig()
{
$this->__initializer__ && $this->__initializer__->__invoke($this, 'getSelectableRow', []);
$this->__initializer__ && $this->__initializer__->__invoke($this, 'getConfig', []);
return parent::getSelectableRow();
return parent::getConfig();
}
/**
* {@inheritDoc}
*/
public function setSelectableRow($selectableRow)
public function setConfig($config)
{
$this->__initializer__ && $this->__initializer__->__invoke($this, 'setSelectableRow', [$selectableRow]);
$this->__initializer__ && $this->__initializer__->__invoke($this, 'setConfig', [$config]);
return parent::setSelectableRow($selectableRow);
return parent::setConfig($config);
}
/**
* {@inheritDoc}
*/
public function getPublic()
{
$this->__initializer__ && $this->__initializer__->__invoke($this, 'getPublic', []);
return parent::getPublic();
}
/**
* {@inheritDoc}
*/
public function setPublic($public)
{
$this->__initializer__ && $this->__initializer__->__invoke($this, 'setPublic', [$public]);
return parent::setPublic($public);
}
/**
......
......@@ -64,10 +64,10 @@ class Group extends \App\Entity\Group implements \Doctrine\ORM\Proxy\Proxy
public function __sleep()
{
if ($this->__isInitialized__) {
return ['__isInitialized__', 'id', 'label', 'datasetPrivileges'];
return ['__isInitialized__', 'id', 'label', 'datasets'];
}
return ['__isInitialized__', 'id', 'label', 'datasetPrivileges'];
return ['__isInitialized__', 'id', 'label', 'datasets'];
}
/**
......@@ -213,12 +213,23 @@ class Group extends \App\Entity\Group implements \Doctrine\ORM\Proxy\Proxy
/**
* {@inheritDoc}
*/
public function getDatasetPrivileges()
public function getDatasets()
{
$this->__initializer__ && $this->__initializer__->__invoke($this, 'getDatasetPrivileges', []);
$this->__initializer__ && $this->__initializer__->__invoke($this, 'getDatasets', []);
return parent::getDatasetPrivileges();
return parent::getDatasets();
}
/**
* {@inheritDoc}
*/
public function setDatasets($datasets)
{
$this->__initializer__ && $this->__initializer__->__invoke($this, 'setDatasets', [$datasets]);
return parent::setDatasets($datasets);
}
/**
......
......@@ -64,10 +64,10 @@ class Instance extends \App\Entity\Instance implements \Doctrine\ORM\Proxy\Proxy
public function __sleep()
{
if ($this->__isInitialized__) {
return ['__isInitialized__', 'name', 'label', 'clientUrl', 'datasetFamilies'];
return ['__isInitialized__', 'name', 'label', 'clientUrl', 'config', 'datasetFamilies'];
}
return ['__isInitialized__', 'name', 'label', 'clientUrl', 'datasetFamilies'];
return ['__isInitialized__', 'name', 'label', 'clientUrl', 'config', 'datasetFamilies'];
}
/**
......@@ -232,6 +232,28 @@ class Instance extends \App\Entity\Instance implements \Doctrine\ORM\Proxy\Proxy
return parent::setClientUrl($clientUrl);
}
/**
* {@inheritDoc}
*/
public function getConfig()
{
$this->__initializer__ && $this->__initializer__->__invoke($this, 'getConfig', []);
return parent::getConfig();
}
/**
* {@inheritDoc}
*/
public function setConfig($config)
{
$this->__initializer__ && $this->__initializer__->__invoke($this, 'setConfig', [$config]);
return parent::setConfig($config);
}
/**
* {@inheritDoc}
*/
......
......@@ -45,7 +45,7 @@ final class GroupListAction extends AbstractAction
$parsedBody = $request->getParsedBody();
// To work this action needs group information
foreach (array('label') as $a) {
foreach (array('label', 'datasets') as $a) {
if ($this->isEmptyField($a, $parsedBody)) {
throw new HttpBadRequestException(
$request,
......@@ -70,10 +70,9 @@ final class GroupListAction extends AbstractAction
*/
private function postGroup(array $parsedBody): Group
{
$group = new Group(
$this->getDatasets($parsedBody['datasets'])
);
$group = new Group();
$group->setLabel($parsedBody['label']);
$group->setDatasets($this->getDatasets($parsedBody['datasets']));
$this->em->persist($group);
$this->em->flush();
......
......@@ -12,9 +12,6 @@ declare(strict_types=1);
namespace App\Entity;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Entity
* @Table(name="anis_group")
......@@ -49,11 +46,6 @@ class Group implements \JsonSerializable
*/
protected $datasets;
public function __construct(array $datasets)
{
$this->datasets = new ArrayCollection($datasets);
}
public function getId()
{
return $this->id;
......
......@@ -20,20 +20,50 @@ use Nyholm\Psr7\Response as NyholmResponse;
final class AdminMiddleware implements MiddlewareInterface
{
/**
* Contains settings to handle Json Web Token
*
* @var array
*/
private $settings;
/**
* Create the classe before call process to execute this middleware
*
* @param array $settings Settings about token
*/
public function __construct(array $settings)
{
$this->settings = $settings;
}
public function process(Request $request, RequestHandler $handler): Response
{
if ($request->getMethod() === OPTIONS || $request->getMethod() === GET) {
if (
$request->getMethod() === OPTIONS
|| $request->getMethod() === GET
|| $this->settings['enabled'] === 0
) {
return $handler->handle($request);
}
$token = $request->getAttribute('token');
if (!$token) {
return (new NyholmResponse())->withStatus(401);
return $this->getResponse('HTTP 401: This url need a valid token', 401);
}
if (!in_array('anis_admin', $token->getClaim('realm_access')->roles)) {
return (new NyholmResponse())->withStatus(403);
if (!in_array($this->settings['admin_role'], $token->getClaim('realm_access')->roles)) {
return $this->getResponse('HTTP 403: This url need a higher level of permission', 403);
}
return $handler->handle($request);
}
private function getResponse(string $message, int $code)
{
$resonse = new NyholmResponse();
$resonse->getBody()->write(json_encode(array(
'message' => $message
)));
return $resonse->withStatus($code);
}
}
......@@ -15,6 +15,7 @@ namespace App\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Exception\HttpUnauthorizedException;
use Nyholm\Psr7\Response as NyholmResponse;
use Psr\Http\Server\MiddlewareInterface;
use Lcobucci\JWT\Parser;
......@@ -58,7 +59,11 @@ final class AuthorizationMiddleware implements MiddlewareInterface
*/
public function process(Request $request, RequestHandler $handler): Response
{
if ($request->getMethod() === OPTIONS || !$request->hasHeader('Authorization')) {
if (
$request->getMethod() === OPTIONS
|| !$request->hasHeader('Authorization')
|| $this->settings['enabled'] === 0
) {
return $handler->handle($request);
}
......@@ -66,7 +71,9 @@ final class AuthorizationMiddleware implements MiddlewareInterface
$bearer = $request->getHeader('Authorization');
$data = explode(' ', $bearer[0]);
if ($data[0] !== 'Bearer') {
return (new NyholmResponse())->withStatus(401);
return $this->getUnauthorizedResponse(
'HTTP 401: Authorization must contain a string with the following format -> Bearer JWT'
);
}
// Parse the JWT Token
......@@ -74,18 +81,38 @@ final class AuthorizationMiddleware implements MiddlewareInterface
// Validating token (verifying expiration date and issuer)
$data = new ValidationData();
// TODO: Ajouter une config pour vérifier ou non le issuer
// $data->setIssuer($this->settings['issuer']);
if (!$token->validate($data)) {
return (new NyholmResponse())->withStatus(401);
return $this->getUnauthorizedResponse('HTTP 401: Access Token is not valid or has expired');
}
// Test token signature with the public key
$publicKey = new Key('file://' . $this->settings['public_key_file']);
if (!$token->verify(new Sha256(), $publicKey)) {
return (new NyholmResponse())->withStatus(401);
return $this->getUnauthorizedResponse('HTTP 401: Access Token signature is not valid');
}
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)
{
$resonse = new NyholmResponse();
$resonse->getBody()->write(json_encode(array(
'message' => $message
)));
return $resonse->withStatus(401);
}
}
<?php
/*
* This file is part of Anis Server.
*
* (c) Laboratoire d'Astrophysique de Marseille / CNRS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace App\Tests\Action;
use PHPUnit\Framework\TestCase;
use Nyholm\Psr7\ServerRequest;
use Nyholm\Psr7\Response;
use Slim\Exception\HttpNotFoundException;
use Slim\Exception\HttpBadRequestException;
use App\tests\EntityManagerBuilder;
use App\Entity\Group;
use App\Entity\Database;
use App\Entity\Project;
use App\Entity\Instance;
use App\Entity\DatasetFamily;
use App\Entity\Dataset;
final class GroupActionTest extends TestCase
{
private $action;
private $entityManager;
protected function setUp(): void
{
$this->entityManager = EntityManagerBuilder::getInstance();
$this->action = new \App\Action\GroupAction($this->entityManager);
}
public function testOptionsHttpMethod(): void
{
$request = $this->getRequest('OPTIONS');
$response = ($this->action)($request, new Response(), array());
$this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, PUT, DELETE, OPTIONS');
}
public function testGroupIsNotFound(): void
{
$this->expectException(HttpNotFoundException::class);
$this->expectExceptionMessage('Group with id 1 is not found');
$request = $this->getRequest('GET');
$response = ($this->action)($request, new Response(), array('id' => 1));
$this->assertEquals(404, (int) $response->getStatusCode());
}
public function testGetAGroupById(): void
{
$group = $this->addAGroup();
$request = $this->getRequest('GET');
$response = ($this->action)($request, new Response(), array('id' => 1));
$this->assertSame(json_encode($group), (string) $response->getBody());
}
public function testEditAGroupEmptyLabelField(): void
{
$this->addAGroup();
$this->expectException(HttpBadRequestException::class);
$this->expectExceptionMessage('Param label needed to edit the group');
$request = $this->getRequest('PUT')->withParsedBody(array());
$response = ($this->action)($request, new Response(), array('id' => 1));
$this->assertEquals(400, (int) $response->getStatusCode());
}
public function testEditAGroupWithoutDataset(): void
{
$fields = array(
'label' => 'New_label',
'datasets' => array()
);
$this->addAGroup();
$request = $this->getRequest('PUT')->withParsedBody($fields);
$response = ($this->action)($request, new Response(), array('id' => 1));
$this->assertSame(json_encode(array_merge(['id' => 1], $fields)), (string) $response->getBody());
}
public function testEditAGroupWithDataset(): void
{
$dataset = $this->addADataset();
$fields = array(
'label' => 'New_label',
'datasets' => array($dataset->getName())
);
$this->addAGroup();
$request = $this->getRequest('PUT')->withParsedBody($fields);
$response = ($this->action)($request, new Response(), array('id' => 1));
$this->assertSame(json_encode(array_merge(['id' => 1], $fields)), (string) $response->getBody());
}
public function testDeleteAGroup(): void
{
$this->addAGroup();
$request = $this->getRequest('DELETE');
$response = ($this->action)($request, new Response(), array('id' => 1));
$this->assertSame(
json_encode(array('message' => 'Group with id 1 is removed!')),
(string) $response->getBody()
);
}
protected function tearDown(): void
{
$this->entityManager->getConnection()->close();
}
private function getRequest(string $method): ServerRequest
{
return new ServerRequest($method, '/group/1', array(
'Content-Type' => 'application/json'
));
}
private function addAGroup(): Group
{
$group = new Group();
$group->setLabel('Group1');
$group->setDatasets(array());
$this->entityManager->persist($group);
$this->entityManager->flush();
return $group;
}
private function addProject(): Project
{
$database = new Database();
$database->setLabel('Test1');
$database->setDbName('test1');
$database->setType('pgsql');
$database->setHost('db');
$database->setPort(5432);
$database->setLogin('test');
$database->setPassword('test');
$this->entityManager->persist($database);
$project = new Project('anis_project');
$project->setLabel('Test project');
$project->setDescription('Test description');
$project->setLink('http://test.com');
$project->setManager('User1');
$project->setDatabase($database);
$this->entityManager->persist($project);
$this->entityManager->flush();
return $project;
}
private function addInstance(): Instance
{
$instance = new Instance('aspic', 'Aspic');
$instance->setClientUrl('http://cesam.lam.fr/aspic');
$this->entityManager->persist($instance);
$this->entityManager->flush();
return $instance;
}
private function addDatasetFamily(): DatasetFamily
{
$instance = $this->addInstance();
$family = new DatasetFamily($instance);
$family->setLabel('Default dataset');
$family->setDisplay(10);
$this->entityManager->persist($family);
$this->entityManager->flush();
return $family;
}
private function addADataset(): Dataset
{
$project = $this->addProject();
$family = $this->addDatasetFamily();
$dataset = new Dataset('obs_cat');
$dataset->setTableRef('v_obs_cat');
$dataset->setLabel('Obscat label');
$dataset->setDescription('Obscat description');
$dataset->setDisplay(10);
$dataset->setCount(10000);
$dataset->setVo(false);
$dataset->setDataPath('/mnt/obs_cat');
$dataset->setPublic(true);
$dataset->setProject($project);
$dataset->setDatasetFamily($family);
$this->entityManager->persist($dataset);
$this->entityManager->flush();
return $dataset;
}
}
<?php
/*
* This file is part of Anis Server.
*
* (c) Laboratoire d'Astrophysique de Marseille / CNRS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace App\Tests\Action;
use PHPUnit\Framework\TestCase;
use Nyholm\Psr7\ServerRequest;
use Nyholm\Psr7\Response;
use Slim\Exception\HttpBadRequestException;
use App\tests\EntityManagerBuilder;
use App\Entity\Group;
use App\Entity\Database;
use App\Entity\Project;
use App\Entity\Instance;
use App\Entity\DatasetFamily;
use App\Entity\Dataset;
final class GroupListActionTest extends TestCase
{
private $action;
private $entityManager;
protected function setUp(): void
{
$this->entityManager = EntityManagerBuilder::getInstance();
$this->action = new \App\Action\GroupListAction($this->entityManager);
}
public function testOptionsHttpMethod(): void
{
$request = $this->getRequest('OPTIONS');
$response = ($this->action)($request, new Response(), array());
$this->assertSame($response->getHeaderLine('Access-Control-Allow-Methods'), 'GET, POST, OPTIONS');
}
public function testGetAllGroups(): void
{
$groups = $this->addGroups();