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

Merge branch '60-integration-de-keycloack' into 'develop'

Resolve "Intégration de keycloack"

Closes #60

See merge request !53
parents 41f4acb9 6f7ddad8
Pipeline #3419 passed with stages
in 1 minute and 56 seconds
......@@ -76,14 +76,6 @@ $container->set('App\Action\ProjectAction', function (ContainerInterface $c) {
return new App\Action\ProjectAction($c->get('em'));
});
$container->set('App\Action\UserListAction', function (ContainerInterface $c) {
return new App\Action\UserListAction($c->get('em'));
});
$container->set('App\Action\UserAction', function (ContainerInterface $c) {
return new App\Action\UserAction($c->get('em'));
});
$container->set('App\Action\GroupListAction', function (ContainerInterface $c) {
return new App\Action\GroupListAction($c->get('em'));
});
......@@ -179,8 +171,3 @@ $container->set('App\Action\SearchAction', function (ContainerInterface $c) {
$container->set('App\Action\DownloadFileAction', function (ContainerInterface $c) {
return new App\Action\DownloadFileAction($c->get('em'));
});
// Middlewares
$container->set('App\Middleware\AuthorizationMiddleware', function (ContainerInterface $c) {
return new App\Middleware\AuthorizationMiddleware($c->get('em'), $c->get(SETTINGS)['token_options']);
});
......@@ -14,3 +14,4 @@ $app->add(new App\Middleware\JsonBodyParserMiddleware());
$app->add(new App\Middleware\ContentTypeJsonMiddleware());
$app->add(new App\Middleware\CorsMiddleware());
$app->add(new App\Middleware\MetamodelSqlLoggerMiddleware($container->get('logger'), $container->get('em')));
$app->add(new App\Middleware\AuthorizationMiddleware($container->get(SETTINGS)['token']));
......@@ -13,31 +13,41 @@ declare(strict_types=1);
use Slim\Routing\RouteCollectorProxy;
$app->get('/', App\Action\RootAction::class);
$app->map([OPTIONS, GET, POST], '/database', App\Action\DatabaseListAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/database/{id}', App\Action\DatabaseAction::class);
$app->map([OPTIONS, GET], '/database/{id}/table', App\Action\TableListAction::class);
$app->map([OPTIONS, GET, POST], '/project', App\Action\ProjectListAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/project/{name}', App\Action\ProjectAction::class);
$app->map([OPTIONS, GET, POST], '/user', App\Action\UserListAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/user/{email}', App\Action\UserAction::class);
$app->map([OPTIONS, GET, POST], '/group', App\Action\GroupListAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/group/{id}', App\Action\GroupAction::class);
$app->map([OPTIONS, GET, POST], '/instance', App\Action\InstanceListAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/instance/{name}', App\Action\InstanceAction::class);
$app->map([OPTIONS, GET, POST], '/instance/{name}/dataset-family', App\Action\DatasetFamilyListAction::class);
$app->map([OPTIONS, GET], '/instance/{name}/dataset', App\Action\DatasetListByInstanceAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/dataset-family/{id}', App\Action\DatasetFamilyAction::class);
$app->map([OPTIONS, GET, POST], '/dataset-family/{id}/dataset', App\Action\DatasetListAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}', App\Action\DatasetAction::class);
$app->map([OPTIONS, GET, POST], '/dataset/{name}/criteria-family', App\Action\CriteriaFamilyListAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/criteria-family/{id}', App\Action\CriteriaFamilyAction::class);
$app->map([OPTIONS, GET, POST], '/dataset/{name}/output-family', App\Action\OutputFamilyListAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/output-family/{id}', App\Action\OutputFamilyAction::class);
$app->map([OPTIONS, GET], '/dataset/{name}/output-category', App\Action\OutputCategoryListByDatasetAction::class);
$app->map([OPTIONS, GET, POST], '/output-family/{id}/output-category', App\Action\OutputCategoryListAction::class);
$app->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class);
$app->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class);
$app->map([OPTIONS, GET, PUT], '/dataset/{name}/attribute/{id}', App\Action\AttributeAction::class);
$app->map([OPTIONS, GET, PUT], '/dataset/{name}/attribute/{id}/distinct', App\Action\AttributeDistinctAction::class);
$app->group('', function (RouteCollectorProxy $group) {
$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], '/database/{id}/table', App\Action\TableListAction::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, POST], '/group', App\Action\GroupListAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/group/{id}', App\Action\GroupAction::class);
$group->map([OPTIONS, GET, POST], '/instance', App\Action\InstanceListAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/instance/{name}', App\Action\InstanceAction::class);
$group->map([OPTIONS, GET, POST], '/instance/{name}/dataset-family', App\Action\DatasetFamilyListAction::class);
$group->map([OPTIONS, GET], '/instance/{name}/dataset', App\Action\DatasetListByInstanceAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/dataset-family/{id}', App\Action\DatasetFamilyAction::class);
$group->map([OPTIONS, GET, POST], '/dataset-family/{id}/dataset', App\Action\DatasetListAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/dataset/{name}', App\Action\DatasetAction::class);
$group->map([OPTIONS, GET, POST], '/dataset/{name}/criteria-family', App\Action\CriteriaFamilyListAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/criteria-family/{id}', App\Action\CriteriaFamilyAction::class);
$group->map([OPTIONS, GET, POST], '/dataset/{name}/output-family', App\Action\OutputFamilyListAction::class);
$group->map([OPTIONS, GET, PUT, DELETE], '/output-family/{id}', App\Action\OutputFamilyAction::class);
$group->map([OPTIONS, GET], '/dataset/{name}/output-category', App\Action\OutputCategoryListByDatasetAction::class);
$group->map(
[OPTIONS, GET, POST],
'/output-family/{id}/output-category',
App\Action\OutputCategoryListAction::class
);
$group->map([OPTIONS, GET, PUT, DELETE], '/output-category/{id}', App\Action\OutputCategoryAction::class);
$group->map([OPTIONS, GET], '/dataset/{name}/attribute', App\Action\AttributeListAction::class);
$group->map([OPTIONS, GET, PUT], '/dataset/{name}/attribute/{id}', App\Action\AttributeAction::class);
$group->map(
[OPTIONS, GET, PUT],
'/dataset/{name}/attribute/{id}/distinct',
App\Action\AttributeDistinctAction::class
);
})->add(new App\Middleware\AdminMiddleware());
$app->get('/search/{dname}', App\Action\SearchAction::class);
$app->get('/download-file/{dname}/[{fpath:.*}]', App\Action\DownloadFileAction::class);
......@@ -29,5 +29,9 @@ return [
'name' => getenv('LOGGER_NAME'),
'path' => getenv('LOGGER_PATH'),
'level' => getenv('LOGGER_LEVEL')
],
'token' => [
'issuer' => getenv('TOKEN_ISSUER'),
'public_key_file' => getenv('TOKEN_PUBLIC_KEY_FILE')
]
];
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "41b0ad136ffb7b6f8f4c93825e72e5be",
"content-hash": "9f060118c163df9ee6fc4a2bb7d5c005",
"packages": [
{
"name": "doctrine/annotations",
......@@ -900,6 +900,7 @@
"keywords": [
"reflection"
],
"abandoned": "roave/better-reflection",
"time": "2018-06-14T14:45:07+00:00"
},
{
......@@ -958,8 +959,74 @@
"serialize",
"tokenizer"
],
"abandoned": "opis/closure",
"time": "2018-03-21T22:21:57+00:00"
},
{
"name": "lcobucci/jwt",
"version": "3.3.3",
"source": {
"type": "git",
"url": "https://github.com/lcobucci/jwt.git",
"reference": "c1123697f6a2ec29162b82f170dd4a491f524773"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lcobucci/jwt/zipball/c1123697f6a2ec29162b82f170dd4a491f524773",
"reference": "c1123697f6a2ec29162b82f170dd4a491f524773",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-openssl": "*",
"php": "^5.6 || ^7.0"
},
"require-dev": {
"mikey179/vfsstream": "~1.5",
"phpmd/phpmd": "~2.2",
"phpunit/php-invoker": "~1.1",
"phpunit/phpunit": "^5.7 || ^7.3",
"squizlabs/php_codesniffer": "~2.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1-dev"
}
},
"autoload": {
"psr-4": {
"Lcobucci\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Luís Otávio Cobucci Oblonczyk",
"email": "lcobucci@gmail.com",
"role": "Developer"
}
],
"description": "A simple library to work with JSON Web Token and JSON Web Signature",
"keywords": [
"JWS",
"jwt"
],
"funding": [
{
"url": "https://github.com/lcobucci",
"type": "github"
},
{
"url": "https://www.patreon.com/lcobucci",
"type": "patreon"
}
],
"time": "2020-08-20T13:22:28+00:00"
},
{
"name": "monolog/monolog",
"version": "2.0.1",
......@@ -3620,5 +3687,6 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
"platform-dev": [],
"plugin-api-version": "1.1.0"
}
......@@ -3,8 +3,10 @@ set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER anis LOGIN PASSWORD 'anis';
CREATE USER keycloak LOGIN PASSWORD 'keycloak';
CREATE DATABASE anis_metamodel;
CREATE DATABASE anis_test;
CREATE DATABASE keycloakdb;
GRANT ALL PRIVILEGES ON DATABASE anis_metamodel TO anis;
GRANT ALL PRIVILEGES ON DATABASE anis_test TO anis;
EOSQL
......
This diff is collapsed.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAh4woW+Tg4L3iGG9osCLi
1TD6vX/fAqX3iMfe9hi+M269FcGCl/1+Ls4gwLs/TqqFpWmg3T3bLuShHkabeGi5
CKr8hrHJMPA0NjPHuH1RUL/QyQpPgKQkmKxyUH9i3hdQtB2REcVZBmB5+JRIcUeh
cTLkpOWrRz9cquv8R7N8xD6OpdLWSFItYnOrKlR4II6EQaY0PBDyNQElXIiqTMLt
aTbXVn1suzT0NUwDTIvcxKpqTCEyM3meuIFsc+ISjff12WY5rLWoadVZHLwkfe/9
7zF3UYPb3ddZkQ/W3jQXYYMVgMHOfskXjstqH9XPkez4ovJUHukPYKsvvWWqiDCo
rwIDAQAB
-----END PUBLIC KEY-----
......@@ -17,6 +17,8 @@ services:
LOGGER_NAME: "anis-metamodel"
LOGGER_PATH: "php://stderr"
LOGGER_LEVEL: "debug"
TOKEN_ISSUER: http://localhost:8180/auth/realms/anis
TOKEN_PUBLIC_KEY_FILE: /data/public_key
ports:
- 8080:80
volumes:
......@@ -24,6 +26,25 @@ services:
- ./conf-dev/dev-php.ini:/usr/local/etc/php/conf.d/dev-php.ini
- ./conf-dev/vhost.conf:/etc/apache2/sites-available/000-default.conf
- ./conf-dev/data-dev:/data/ASPIC
- ./conf-dev/public_key:/data/public_key
keycloak:
image: jboss/keycloak
environment:
DB_VENDOR: POSTGRES
DB_ADDR: db
DB_DATABASE: keycloakdb
DB_USER: keycloak
DB_PASSWORD: keycloak
DB_SCHEMA: public
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
KEYCLOAK_IMPORT: /tmp/keycloak-anis-realm.json
ports:
- 8180:8180
volumes:
- ./conf-dev/keycloak-anis-realm.json:/tmp/keycloak-anis-realm.json
command: -b 0.0.0.0 -Djboss.http.port=8180
db:
image: postgres
......@@ -44,5 +65,10 @@ services:
ports:
- 8083:8080
mailer:
image: djfarrelly/maildev
ports:
- 1080:80
volumes:
pgdata:
\ No newline at end of file
<?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\Action;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Exception\HttpBadRequestException;
use Slim\Exception\HttpNotFoundException;
use App\Entity\User;
final class UserAction extends AbstractAction
{
/**
* `GET` Returns the user found
* `PUT` Full update the user and returns the new version
* `DELETE` Delete the user found and return a confirmation message
*
* @param ServerRequestInterface $request PSR-7 This object represents the HTTP request
* @param ResponseInterface $response PSR-7 This object represents the HTTP response
* @param string[] $args This table contains information transmitted in the URL (see routes.php)
*
* @return ResponseInterface
*/
public function __invoke(Request $request, Response $response, array $args): Response
{
if ($request->getMethod() === OPTIONS) {
return $response->withHeader('Access-Control-Allow-Methods', 'GET, PUT, DELETE, OPTIONS');
}
// Search the correct user with primary key
$user = $this->em->find('App\Entity\User', $args['email']);
// If user is not found 404
if (is_null($user)) {
throw new HttpNotFoundException(
$request,
'User with email ' . $args['email'] . ' is not found'
);
}
if ($request->getMethod() === GET) {
$payload = json_encode($user);
}
if ($request->getMethod() === PUT) {
$parsedBody = $request->getParsedBody();
// If mandatories empty fields 400
foreach (array('role') as $a) {
if ($this->isEmptyField($a, $parsedBody)) {
throw new HttpBadRequestException(
$request,
'Param ' . $a . ' needed to edit the user'
);
}
}
$this->editUser($user, $parsedBody);
$payload = json_encode($user);
}
if ($request->getMethod() === DELETE) {
$email = $user->getEmail();
$this->em->remove($user);
$this->em->flush();
$payload = json_encode(array('message' => 'User with email ' . $email . ' is removed!'));
}
$response->getBody()->write($payload);
return $response;
}
/**
* Update user object with setters
*
* @param User $user The user to update
* @param array $parsedBody Contains the new values ​​of the user
*/
private function editUser(User $user, array $parsedBody): void
{
$user->setRole($parsedBody['role']);
$this->em->flush();
}
}
<?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\Action;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Exception\HttpBadRequestException;
use App\Entity\User;
final class UserListAction extends AbstractAction
{
/**
* `GET` Returns a list of all users listed in the metamodel
* `POST` Add a new user
*
* @param ServerRequestInterface $request PSR-7 This object represents the HTTP request
* @param ResponseInterface $response PSR-7 This object represents the HTTP response
* @param string[] $args This table contains information transmitted in the URL (see routes.php)
*
* @return ResponseInterface
*/
public function __invoke(Request $request, Response $response, array $args): Response
{
if ($request->getMethod() === OPTIONS) {
return $response->withHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
}
if ($request->getMethod() === GET) {
$users = $this->em->getRepository('App\Entity\User')->findAll();
$payload = json_encode($users);
}
if ($request->getMethod() === POST) {
$parsedBody = $request->getParsedBody();
// To work this action needs user information
foreach (array('email') as $a) {
if ($this->isEmptyField($a, $parsedBody)) {
throw new HttpBadRequestException(
$request,
'Param ' . $a . ' needed to add a new user'
);
}
}
$user = $this->postUser($parsedBody);
$payload = json_encode($user);
$response = $response->withStatus(201);
}
$response->getBody()->write($payload);
return $response;
}
/**
* Add a new user into the metamodel
*
* @param array $parsedBody Contains the values ​​of the new user
*/
private function postUser(array $parsedBody): User
{
$user = new User($parsedBody['email']);
$user->setRole('user');
$this->em->persist($user);
$this->em->flush();
return $user;
}
}
......@@ -37,18 +37,6 @@ class Group implements \JsonSerializable
*/
protected $label;
/**
* Many Groups have Many Users.
*
* @ManyToMany(targetEntity="User", inversedBy="groups")
* @JoinTable(
* name="groups_users",
* joinColumns={@JoinColumn(name="group_id", referencedColumnName="id")},
* inverseJoinColumns={@JoinColumn(name="user_email", referencedColumnName="email")}
* )
*/
protected $users;
/**
* Many Groups have Many Datasets privileges.
*
......@@ -61,9 +49,8 @@ class Group implements \JsonSerializable
*/
protected $datasets;
public function __construct(array $users, array $datasets)
public function __construct(array $datasets)
{
$this->users = new ArrayCollection($users);
$this->datasets = new ArrayCollection($datasets);
}
......@@ -82,16 +69,6 @@ class Group implements \JsonSerializable
$this->label = $label;
}
public function getUsers()
{
return $this->users;
}
public function setUsers($users)
{
$this->users = $users;
}
public function getDatasets()
{
return $this->datasets;
......@@ -104,11 +81,6 @@ class Group implements \JsonSerializable
public function jsonSerialize()
{
$users = array();
foreach ($this->getUsers() as $user) {
$users[] = $user->getEmail();
}
$datasets = array();
foreach ($this->getDatasets() as $dataset) {
$datasets[] = $dataset->getName();
......@@ -117,7 +89,6 @@ class Group implements \JsonSerializable
return [
'id' => $this->getId(),
'label' => $this->getLabel(),
'users' => $users,
'datasets' => $datasets
];
}
......
<?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\Entity;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Entity
* @Table(name="anis_user")
*/
class User implements \JsonSerializable
{
/**
* @var string
*
* @Id
* @Column(type="string", nullable=false)
*/
protected $email;
/**
* @var string
*
* @Column(type="string", nullable=false)
*/
protected $role;
/**
* Many Users have Many Groups.
*
* @ManyToMany(targetEntity="Group", mappedBy="users")
*/
protected $groups;
public function __construct($email)
{
$this->email = $email;
$this->groups = new ArrayCollection();
}
public function getEmail()
{
return $this->email;
}
public function getRole()
{
return $this->role;
}
public function setRole($role)
{
$this->role = $role;
}
public function getGroups()
{
return $this->groups;
}
public function jsonSerialize()
{
$groups = array();
foreach ($this->getGroups() as $group) {
$groups[] = $group->getId();
}
return [
'email' => $this->getEmail(),
'role' => $this->getRole(),
'groups' => $groups
];