Commit 04281b5a authored by Jordan Breton's avatar Jordan Breton

WIP command & query embeded security (spliting app security level and domain security concerns)

parent bc935d9c
......@@ -15,8 +15,8 @@ final class MMDomainEventManager extends DomainEventObserver {
/**
* @param IDomainEvent $e
*/
public function dispatch(IDomainEvent $e): void {
parent::dispatch($e);
public function dispatchDomainEvent(IDomainEvent $e): void {
parent::dispatchDomainEvent($e);
foreach($this->getListeners() as $listened=>$listeners){
if($e instanceof $listened){
foreach($listeners as $listener){
......
......@@ -243,7 +243,7 @@ final class ModelManager implements IModelManager, ICrossModelAccess {
$this->_models[get_class($model)] = $model;
foreach($model->listenEvents() as $listened){
$this->_domainEventManager->addEventListener($listened,$model);
$this->_domainEventManager->addDomainEventListener($listened, $model);
}
}
}
......
......@@ -3,10 +3,13 @@
"engine" : {
"core" : {
"app" : {
"ACTION_HANDLER_NOT_FOUND" : "ActionHandler [[$]] doesn't exists !",
"ACTION_HANDLER_NOT_FOUND" : "ActionHandler [[$]] doesn't exists",
"INTERNAL_ERROR" : "Internal error : [$]",
"MUST_BE_LOGGED" : "Access denied : you must be connected !",
"404_NOT_FOUND" : "Page '[$]' not found !"
"MUST_BE_LOGGED" : "You must be connected.",
"ACCESS_DENIED" : "Access denied",
"INSUFFICIENT_PRIVILEGIES":"Insufficient user privileges.",
"BAD_CSRF_TOKEN" : "Invalid CSRF token.",
"404_NOT_FOUND" : "Page '[$]' not found"
}
},
"package" : {
......
......@@ -5,7 +5,10 @@
"app" : {
"ACTION_HANDLER_NOT_FOUND" : "L'ActionHandler [[$]] n'existe pas !",
"INTERNAL_ERROR" : "Erreur interne : [$]",
"MUST_BE_LOGGED" : "Accès refusé : vous devez être connecté pour effectuer cette action !",
"MUST_BE_LOGGED" : "Vous devez être connecté pour effectuer cette action !",
"ACCESS_DENIED" : "Accès refusé",
"INSUFFICIENT_PRIVILEGIES":"Privilèges utilisateur insuffisants.",
"BAD_CSRF_TOKEN" : "Token CSRF invalide.",
"404_NOT_FOUND" : "La page '[$]' n'existe pas !"
}
},
......
......@@ -42,7 +42,7 @@ final class WebApp {
) $layout = $cache->get($cacheKey);
else{
$translator = $this->_context->getTranslator();
$permission = $this->_context->getAccessControlCenter()->checkPermissions($action);
$permission = $this->_context->getAccessControlCenter()->checkAccessPermission($action);
$response = $this->_context->getActionHook()->hook($action,$permission);
if(is_null($response)){
if($permission->isGranted()){
......
......@@ -20,6 +20,10 @@ use wfw\engine\core\command\ICommandBus;
use wfw\engine\core\command\ICommandHandlerFactory;
use wfw\engine\core\command\ICommandInflector;
use wfw\engine\core\command\inflectors\NamespaceBasedInflector;
use wfw\engine\core\command\security\CommandAccessRuleFactory;
use wfw\engine\core\command\security\CommandSecurityCenter;
use wfw\engine\core\command\security\ICommandSecurityCenter;
use wfw\engine\core\command\security\rules\CommandAccessAccessRulesCollector;
use wfw\engine\core\command\SynchroneCommandBus;
use wfw\engine\core\data\DBAccess\NOSQLDB\msServer\IMSServerAccess;
use wfw\engine\core\data\DBAccess\NOSQLDB\msServer\MSServerWriterAccess;
......@@ -150,7 +154,7 @@ class DefaultContext implements IWebAppContext {
* @param null|String $ajaxViewPath Chemin d'accés à la vue ajax par défaut
* @param array $connections Connexions d'urls
* @param array $langs Langues disponibles
* @param array $accessRules Liste des régles d'accés
* @param array $securityRules 3 indxes : access, command, query
* @param array $hooks Liste des hooks
* @param array $diceRules Regles à ajouter à Dice
* @param array $globals Contient la variables globales de php aux index _GET,_POST,_FILES,_SERVER
......@@ -164,7 +168,7 @@ class DefaultContext implements IWebAppContext {
?string $ajaxViewPath = null,
array $connections = [],
array $langs = [],
array $accessRules = [],
array $securityRules = [],
array $hooks = [],
array $diceRules = [],
array $globals = [],
......@@ -176,6 +180,10 @@ class DefaultContext implements IWebAppContext {
$genericFactory = new DiceBasedFactory($this->_dice = $dice = new Dice());
$this->getErrorHandler()->handle();
//TODO : init if not set for commands and queries
$commandRules = $securityRules["command"] ?? [];
$accessRules = $securityRules["access"] ?? [];
$queryRules = $securityRules["query"] ?? [];
if(count($accessRules) === 0) $accessRules = [
RequireAuthentification::class => [
array_merge(
......@@ -250,12 +258,14 @@ class DefaultContext implements IWebAppContext {
$this->_translator = $translator = $this->initTranslator($langs,null);
$instance = $this;
$commandRulesCollector = new CommandAccessAccessRulesCollector(
new CommandAccessRuleFactory($genericFactory),$commandRules
);
$this->_dice->addRules([
'*' => [
'substitutions' => [
IConf::class => [ Dice::INSTANCE => function() use ($conf){ return $conf; } ],
/*IRouter::class => [ Dice::INSTANCE => function() use($router){return $router;}],*/
IMailProvider::class => [
Dice::INSTANCE => function() use($instance,$conf){
return $this->initMailProvider($conf);
......@@ -308,8 +318,12 @@ class DefaultContext implements IWebAppContext {
ICommandInflector::class => [
'instanceOf' => NamespaceBasedInflector::class, 'shared' => true
],
ICommandSecurityCenter::class => [
'instanceOf' => CommandSecurityCenter::class, 'shared'=>true,
'constructParams' => [ $commandRulesCollector->collect() ]
],
ICommandHandlerFactory::class => [
'instanceOf' => CommandHandlerFactory::class,'shared'=>true
'instanceOf' => CommandHandlerFactory::class, 'shared'=>true
],
IEventStore::class => [ 'instanceOf' => DBBasedEventStore::class ],
IDBAccess::class => [
......
......@@ -13,10 +13,10 @@ interface ICacheSystem {
public function get(string $key);
/**
* @param string[] $keys Clé des valeurs à chercher
* @return \Traversable
* @param string[] $keys Clés des valeurs à chercher
* @return iterable
*/
public function getAll(array $keys):\Traversable;
public function getAll(array $keys):iterable;
/**
* @param string[] $keys Clés des valeurs à supprimer du cache
......
......@@ -93,9 +93,9 @@ final class APCUBasedCache implements ICacheSystem{
* to APCUIterator::__construct()
* @see https://bugs.php.net/bug.php?id=78002
* @param string[] $keys Clé des valeurs à chercher
* @return \Traversable
* @return iterable
*/
public function getAll(array $keys): \Traversable {
public function getAll(array $keys): iterable {
foreach($keys as $k=>$key){
$keys[$k]="/".str_replace("/",'\/',"^$this->_namespace::$key")."/";
}
......
<?php
namespace wfw\engine\core\cache\systems;
use wfw\engine\core\cache\ICacheSystem;
/**
* Class MemoryCache
*
* @package wfw\engine\core\cache\systems
*/
final class MemoryCache implements ICacheSystem {
/** @var array $_data */
private $_data;
/** @var null|string $_namespace */
private $_namespace;
/**
* MemoryCache constructor.
*
* @param null|string $namespace
*/
public function __construct(?string $namespace=null) {
$this->_data = [];
$this->_namespace = $namespace;
}
/**
* Obtient la valeur d'une clé en cache
*
* @param string $key Clé de la valeur à rechercher
* @return mixed Valeur attribuée à la clé
*/
public function get(string $key) {
return $this->_data["$this->_namespace::$key"]["data"] ?? null;
}
/**
* @param string[] $keys Clé des valeurs à chercher
* @return iterable
*/
public function getAll(array $keys): iterable {
return array_intersect_key(
$this->_data,
array_flip(array_map(function($key){
return "$this->_namespace::$key";
},$keys)));
}
/**
* @param string[] $keys Clés des valeurs à supprimer du cache
*/
public function deleteAll(array $keys) {
foreach($keys as $key) if($this->contains($key))
unset($this->_data["$this->_namespace::$key"]);
}
/**
* Cache une variable
*
* @param string $key Clé de stockage
* @param mixed $data Donnée à stocker
* @param float $timeout Temps de stockage en secondes (0 : pas de temps de vie, existe jusqu'à la suppression manuelle)
* @return bool True si l'opération a réussi, false sinon
*/
public function set(string $key, $data, float $timeout = 0): bool {
$this->_data["$this->_namespace::$key"] = [
"data" => $data,
"timeout" => microtime(true) + $timeout
];
return true;
}
/**
* Mets à jour une donnée en cache
*
* @param string $key Clé de la donnée à changer
* @param mixed $data Nouvelle données
* @param float $timeout Temps de stockage en secondes (0 : pas de temps de vie, existe jusqu'à la suppression manuelle)
* @return bool True si l'opération a réussi, false sinon
*/
public function update(string $key, $data, float $timeout = 0): bool {
if($this->contains($key)) return $this->set($key,$data,$timeout);
else return false;
}
/**
* Supprime une donnée en cache
*
* @param string $key Clé de la donnée à supprimer du cache
* @return bool True si l'opération a réussi, false sinon
*/
public function delete(string $key): bool {
if($this->contains($key)){
unset($this->_data["$this->_namespace::$key"]);
return true;
}else return false;
}
/**
* Vide le cache
*
* @return bool True si l'opération a réussi, false sinon
*/
public function clear(): bool {
$this->_data = [];
return true;
}
/**
* Teste l'existence d'une clé de donnée en cache
*
* @param string $key Clé à tester
* @return bool True si la clé existe, false sinon
*/
public function contains(string $key): bool {
if(isset($this->_data["$this->_namespace::$key"])){
if(microtime(true) <= $this->_data["$this->_namespace::$key"]["timeout"]) return true;
else unset($this->_data["$this->_namespace::$key"]);
}
return false;
}
}
\ No newline at end of file
......@@ -62,9 +62,9 @@ class NoCache implements ICacheSystem{
/**
* @param string[] $keys Clé des valeurs à chercher
* @return \Traversable
* @return iterable
*/
public function getAll(array $keys): \Traversable {}
public function getAll(array $keys): iterable { return []; }
/**
* @param string[] $keys Clés des valeurs à supprimer du cache
......
......@@ -9,28 +9,38 @@ use wfw\engine\lib\PHP\types\UUID;
abstract class Command implements ICommand {
/** @var UUID */
private $_uuid;
/** @var null|string $_userId */
private $_userId;
/** @var float */
private $_generationDate;
/**
* Command constructor.
*
* @param null|string $userId
*/
public function __construct(){
public function __construct(?string $userId=null){
$this->_userId = $userId;
$this->_uuid = new UUID();
$this->_generationDate = microtime(true);
}
/**
* Retourne l'UUID de la commande
* @return UUID
* @return UUID Command ID
*/
public function getId():UUID{
return $this->_uuid;
}
/**
* Retourne la date de création de la commande
* @return float
* @return null|string User ID that try to execute the command
*/
public function getUserId(): ?string {
return $this->_userId;
}
/**
* @return float Creation date
*/
public function getGenerationDate():float{
return $this->_generationDate;
......
......@@ -25,11 +25,11 @@ final class CommandDispatcher implements ICommandDispatcher {
* @param ICommand $command Commande à dispatcher
* @throws NoCommandHandlerFound
*/
public function dispatch(ICommand $command): void {
public function dispatchCommand(ICommand $command): void {
$success = false;
foreach ($this->_dispatchers as $dispatcher){
try{
$dispatcher->dispatch($command);
$dispatcher->dispatchCommand($command);
$success = true;
break;
}catch(NoCommandHandlerFound $e){}
......
......@@ -26,7 +26,7 @@ final class CommandHandlerFactory implements ICommandHandlerFactory {
* @param array $params Paramètres de création
* @return ICommandHandler
*/
public function build(string $handlerClass,array $params=[]): ICommandHandler {
public function buildCommandHandler(string $handlerClass, array $params=[]): ICommandHandler {
return $this->_factory->create($handlerClass,$params,[ICommandHandler::class]);
}
}
\ No newline at end of file
......@@ -4,7 +4,7 @@ namespace wfw\engine\core\command;
use wfw\engine\lib\PHP\types\UUID;
/**
* Représente une commande pour un CommandHandler
* Command
*/
interface ICommand {
/**
......@@ -12,6 +12,11 @@ interface ICommand {
*/
public function getId():UUID;
/**
* @return null|string Uner that initates the command (if available)
*/
public function getUserId():?string;
/**
* @return float
*/
......
......@@ -12,5 +12,5 @@ interface ICommandBus {
*
* @return mixed
*/
public function execute(ICommand $command):void;
public function executeCommand(ICommand $command):void;
}
\ No newline at end of file
......@@ -11,5 +11,5 @@ interface ICommandDispatcher {
* @param ICommand $command Commande à dispatcher
* @throws NoCommandHandlerFound
*/
public function dispatch(ICommand $command):void;
public function dispatchCommand(ICommand $command):void;
}
\ No newline at end of file
......@@ -9,5 +9,5 @@ interface ICommandHandler {
* Traite la commande
* @param ICommand $command Commande à traiter
*/
public function handle(ICommand $command);
public function handleCommand(ICommand $command);
}
\ No newline at end of file
......@@ -12,5 +12,5 @@ interface ICommandHandlerFactory {
* @param array $params Paramètres de création
* @return ICommandHandler
*/
public function build(string $handlerClass,array $params=[]):ICommandHandler;
public function buildCommandHandler(string $handlerClass, array $params=[]):ICommandHandler;
}
\ No newline at end of file
......@@ -14,5 +14,5 @@ interface ICommandInflector {
* @return ICommandHandler[]
* @throws NoCommandHandlerFound
*/
public function resolveHandlers(ICommand $command):array;
public function resolveCommandHandlers(ICommand $command):array;
}
\ No newline at end of file
......@@ -22,10 +22,10 @@ final class InflectorBasedCommandDispatcher implements ICommandDispatcher {
* @param ICommand $command Commande à dispatcher
* @throws NoHandlerFound
*/
public function dispatch(ICommand $command): void {
$handlers = $this->_inflector->resolveHandlers($command);
public function dispatchCommand(ICommand $command): void {
$handlers = $this->_inflector->resolveCommandHandlers($command);
foreach($handlers as $handler){
$handler->handle($command);
$handler->handleCommand($command);
}
}
}
\ No newline at end of file
<?php
namespace wfw\engine\core\command;
use wfw\engine\core\command\security\errors\RejectedCommand;
use wfw\engine\core\command\security\ICommandSecurityCenter;
/**
* Traite les commandes de manières synchrone
*/
final class SynchroneCommandBus implements ICommandBus {
/**
* Inflecteur de commande (permet de savoir quel handler correspond à quelle commande)
* @var ICommandInflector $_inflector
*/
/** @var ICommandInflector $_inflector */
private $_inflector;
/** @var ICommandSecurityCenter $_security */
private $_security;
/**
* SynchroneCommandBus constructor.
*
* @param ICommandInflector $inflector Trouve le handler d'une commande
* @param ICommandInflector $inflector Trouve le handler d'une commande
* @param ICommandSecurityCenter $security
*/
public function __construct(ICommandInflector $inflector) {
public function __construct(ICommandInflector $inflector, ICommandSecurityCenter $security) {
$this->_inflector = $inflector;
$this->_security = $security;
}
/**
* Redirige la commande vers son handler et retourne le résultat du handler
* @param ICommand $command Commande à rediriger
*/
public function execute(ICommand $command):void {
$handlers = $this->_inflector->resolveHandlers($command);
public function executeCommand(ICommand $command):void {
if(!$this->_security->allowCommand($command)) throw new RejectedCommand(
"Access denied : ".get_class($command)." rejected by the security center."
);
$handlers = $this->_inflector->resolveCommandHandlers($command);
foreach($handlers as $handler){
$handler->handle($command);
$handler->handleCommand($command);
}
}
}
\ No newline at end of file
......@@ -35,7 +35,7 @@ final class NamespaceBasedInflector implements ICommandInflector {
* @return ICommandHandler[]
* @throws NoCommandHandlerFound
*/
public function resolveHandlers(ICommand $command): array {
public function resolveCommandHandlers(ICommand $command): array {
$handlers = $this->resolveHandlersFromCommandClass(get_class($command));
if(count($handlers)>0){
return $handlers;
......@@ -59,7 +59,7 @@ final class NamespaceBasedInflector implements ICommandInflector {
$className = array_pop($tmp);
try{
$res[] = $this->_factory->build(
$res[] = $this->_factory->buildCommandHandler(
implode('\\',$tmp).'\\handlers\\'.$className.'Handler'
);
}catch(\Exception $e){}
......
<?php
namespace wfw\engine\core\command\security;
use wfw\engine\core\app\factory\IGenericAppFactory;
use wfw\engine\core\command\security\rules\ICommandAccessRule;
/**
* Create access rules from a class and params with the app DIC.
*/
final class CommandAccessRuleFactory implements ICommandAccessRuleFactory {
/** @var IGenericAppFactory $_factory */
private $_factory;
/**
* CommandAccessRuleFactory constructor.
*
* @param IGenericAppFactory $factory
*/
public function __construct(IGenericAppFactory $factory) {
$this->_factory = $factory;
}
/**
* @param string $ruleClass
* @param array $params
* @return ICommandAccessRule
*/
public function create(string $ruleClass, array $params = []): ICommandAccessRule {
return $this->_factory->create($ruleClass,$params,[ICommandAccessRule::class]);
}
}
\ No newline at end of file
<?php
namespace wfw\engine\core\command\security;
use wfw\engine\core\command\ICommand;
use wfw\engine\core\command\security\rules\ICommandAccessRule;
/**
* Default security center. Will check if a command is allowed checking a CommandAccessRule.
*/
final class CommandSecurityCenter implements ICommandSecurityCenter {
/** @var ICommandAccessRule $_rule */
private $_rule;
/** @var bool $_ignoredAsTrue */
private $_ignoredAsTrue;
/**
* CommandSecurityCenter constructor.
*
* @param ICommandAccessRule $rule
* @param bool $ignoredAsTrue
*/
public function __construct(ICommandAccessRule $rule, bool $ignoredAsTrue = false) {
$this->_rule = $rule;
$this->_ignoredAsTrue = $ignoredAsTrue;
}
/**
* @param ICommand $cmd Command to run.
* @return bool True, the command is allowed for that user. False otherwise.
*/
public function allowCommand(ICommand $cmd): bool {
return $this->_rule->checkCommand($cmd) ?? $this->_ignoredAsTrue;
}
}
\ No newline at end of file
<?php
namespace wfw\engine\core\command\security;
use wfw\engine\core\command\security\rules\ICommandAccessRule;
/**
* Allow to create access rules
*/
interface ICommandAccessRuleFactory {
/**
* @param string $ruleClass
* @param array $params
* @return ICommandAccessRule
*/
public function create(string $ruleClass,array $params=[]):ICommandAccessRule;
}
\ No newline at end of file
<?php
namespace wfw\engine\core\command\security;
use wfw\engine\core\command\ICommand;
/**
* Will check if a user (or anonymous user) is allowed to run a command.
*/
interface ICommandSecurityCenter {
/**
* @param ICommand $cmd Command to run.
* @return bool True, the command is allowed for that user. False otherwise.
*/
public function allowCommand(ICommand $cmd):bool;
}
\ No newline at end of file
<?php
namespace wfw\engine\core\command\security\errors;
use wfw\engine\core\command\errors\CommandFailure;
/**
* Throwed when a command have been rejected (attempted to be runned by a non authorized user)
*/
class RejectedCommand extends CommandFailure {}
\ No newline at end of file
<?php
namespace wfw\engine\core\command\security\rules;
use wfw\engine\core\command\ICommand;
/**
* All commands are allowed.
*/
final class AllCommandsAllowed implements ICommandAccessRule{
/**
* @param ICommand $cmd
* @return null|bool True if the command can be run, false otherwise.
*/
public function checkCommand(ICommand $cmd): ?bool {
return true;
}
}
\ No newline at end of file
<?php
namespace wfw\engine\core\command\security\rules;
use wfw\engine\core\command\ICommand;
/**
* Will deny all commands.
*/
final class AllCommandsDenied implements ICommandAccessRule {
/**
* @param ICommand $cmd
* @return null|bool True if the command can be run, false otherwise.
*/
public function checkCommand(ICommand $cmd): ?bool {
return false;
}
}
\ No newline at end of file