Commit e976758f authored by Jordan Breton's avatar Jordan Breton

WIP Commands security

parent cdc068e4
<?php
namespace wfw\engine\core\command\security\rules;
use wfw\engine\core\command\ICommand;
/**
* Class NotCommandAccessRule
*
* @package wfw\engine\core\command\security\rules
*/
final class NotCommandAccessRule implements ICommandAccessRule {
/** @var ICommandAccessRule $_rule */
private $_rule;
/**
* NotCommandAccessRule constructor.
*
* @param ICommandAccessRule $rule Rule to negate
*/
public function __construct(ICommandAccessRule $rule) {
$this->_rule = $rule;
}
/**
* @param ICommand $cmd
* @return null|bool True if the command can be run, false otherwise. Null if not applicable
*/
public function checkCommand(ICommand $cmd): ?bool {
return !$this->_rule->checkCommand($cmd);
}
}
\ No newline at end of file
<?php
namespace wfw\engine\core\command\security\rules;
use wfw\engine\core\cache\ICacheSystem;
use wfw\engine\core\command\ICommand;
use wfw\engine\core\command\security\ICommandAccessRuleFactory;
use wfw\engine\package\users\data\model\IUserModelAccess;
use wfw\engine\package\users\domain\types\Admin;
use wfw\engine\package\users\domain\types\UserType;
/**
* Admin users have all permission, other user only the "any_user" permissions.
* Public commands are made using the "public" (no registration needed) array.
* Some commands can be attributed to the UserType::class => CommandClass[]
* Some specific rules can be attribued to each commands CommandClass => [ ICommandAccessRuleClass => [] ]
* AccessRules will be built and checked accordingly, or loaded from cache.
*
* Example :
* [
* "public" => [], //no public command
* "any_user" => [ ChangePassword::class => [ ICommandAccessRuleClass => []] ],
* Client::class => [ Unregister::class => [] ]
* ]
*/
final class UserTypeBasedCommandAccessRule implements ICommandAccessRule {
public const CACHE_KEY = self::class."/results";
public const ANY = "any user";
public const PUBLIC = "public command";
/** @var IUserModelAccess $_model */
private $_model;
/** @var array|mixed $_cache */
private $_cache;
/** @var ICommandAccessRuleFactory $_factory */
private $_factory;
/** @var ICacheSystem $_cacheSystem */
private $_cacheSystem;
/** @var ICommandAccessRule[][] $_permissions */
private $_permissions;
/**
* UserTypeBasedCommandAccessRule constructor.
*
* @param IUserModelAccess $model
* @param ICommandAccessRuleFactory $factory
* @param ICacheSystem $cache
* @param array $permissions
* @throws \InvalidArgumentException
*/
public function __construct(
IUserModelAccess $model,
ICommandAccessRuleFactory $factory,
ICacheSystem $cache,
array $permissions=[]
){
$this->_model = $model;
$this->_factory = $factory;
$this->_permissions = $this->checkPermissionsFormat($permissions);
$this->_cache = $cache->get(self::CACHE_KEY) ?? [];
$this->_cacheSystem = $cache;
}
/**
* @param array $permissions
* @return array
* @throws \InvalidArgumentException
*/
private function checkPermissionsFormat(array $permissions):array{
$res = $permissions;
foreach(array_keys($permissions) as $key){
if(!in_array($key,[self::PUBLIC,self::ANY])){
if(!is_a($key,UserType::class,true)) throw new \InvalidArgumentException(
"$key must implements ".UserType::class
);
}
}
foreach($permissions as $type=>$cmds){
foreach($cmds as $cmd=>$rules){
if(is_int($cmd)){
if(!is_string($rules)) throw new \InvalidArgumentException(
"In $type at offset $cmd : value must be a string"
);
$cmdClass = $rules;
} else $cmdClass = $cmd;
if(!is_a($cmdClass,ICommand::class,true)) throw new \InvalidArgumentException(
"$cmdClass must implements ".ICommand::class
);
if(is_array($rules)){
$rs = [];
foreach ($rules as $class=>$params){
if(is_int($class)){
if(is_string($params)) $rs[] = $this->_factory->create($params);
}else $rs[] = $this->_factory->create($params);
}
if(count($rs) === 0) $res[$type][$cmd] = new AllCommandsAllowed();
else if(count($rs) === 1) $res[$type][$cmd] = $rs[0];
else $res[$type][$cmd] = new AndCommandAccessRule(...$rs);
}else if(is_bool($rules)) $res[$type][$cmd] = $rules
? new AllCommandsAllowed()
: new AllCommandsDenied();
else $res[$type][$cmd] = new AllCommandsAllowed();
if(!is_array($rules)) throw new \InvalidArgumentException(
"$cmd rules must be an array"
);
}
}
return $res;
}
/**
* @param ICommand $cmd
* @return null|bool True if the command can be run, false otherwise. Null if not applicable
*/
public function checkCommand(ICommand $cmd): ?bool {
return $this->checkCommandFrom($cmd,$this->_permissions);
}
/**
* @param ICommand $cmd
* @param ICommandAccessRule[][] $permissions
* @return bool|null
*/
private function checkCommandFrom(ICommand $cmd,array $permissions): ?bool{
$cmdClass = get_class($cmd);
if(isset($permissions[self::PUBLIC][$cmdClass]))
return $permissions[self::PUBLIC][$cmdClass]->checkCommand($cmd);
/** @var ICommandAccessRule $cached */
$cached = $this->_cache[$cmd->getInitiatorId()][$cmdClass] ?? null;
if(!is_null($cached)) return $cached->checkCommand($cmd);
$user = $this->_model->getById($cmd->getInitiatorId());
if(!$user) return false;
if($user->getType() instanceof Admin) return $this->setCache(
$cmd->getInitiatorId(),
$cmdClass,new AllCommandsAllowed()
)->checkCommand($cmd);
$userTypesClasses = array_diff(
array_keys($permissions),
[self::PUBLIC,self::ANY]
);
foreach($userTypesClasses as $userType){
if (is_a($user->getType(), $userType)
&& isset($permissions[$userType][$cmdClass])
) return $this->setCache(
$cmd->getInitiatorId(),
$cmdClass,
$permissions[$userType][$cmdClass]
)->checkCommand($cmd);
}
//any check
if(isset($permissions[self::ANY][$cmdClass])) return $this->setCache(
$cmd->getInitiatorId(),
$cmdClass,
$permissions[self::ANY][$cmdClass]
)->checkCommand($cmd);
return $this->setCache(
$cmd->getInitiatorId(),
$cmdClass,
new AllCommandsDenied()
)->checkCommand($cmd);
}
/**
* Push the result into the cache and return the result
*
* @param string $userId User tested
* @param string $cmdClass Command tested
* @param ICommandAccessRule $res Rule to cache
* @return ICommandAccessRule $res
*/
private function setCache(string $userId,string $cmdClass, ICommandAccessRule $res):ICommandAccessRule{
if(!isset($this->_cache[$userId])) $this->_cache[$userId] = [$cmdClass=>$res];
else $this->_cache[$userId][$cmdClass]=$res;
$this->_cacheSystem->set(self::CACHE_KEY,$this->_cache);
return $res;
}
}
\ No newline at end of file
......@@ -3,9 +3,11 @@
namespace wfw\engine\core\security;
use wfw\engine\core\action\NotFoundHook;
use wfw\engine\core\command\security\rules\UserTypeBasedCommandAccessRule;
use wfw\engine\core\security\rules\RequireAuthentification;
use wfw\engine\core\security\rules\ValidToken;
use wfw\engine\package\general\security\GeneralAccessControlPolicies;
use wfw\engine\package\lang\security\LangAccessControlPolicies;
use wfw\engine\package\uploader\security\UploaderAccessControlPolicies;
use wfw\engine\package\users\security\UsersAccessControlPolicies;
......@@ -14,6 +16,52 @@ use wfw\engine\package\users\security\UsersAccessControlPolicies;
* if no security have been defined for a project.
*/
final class WFWDefaultSecurityPolicy extends SecurityPolicy {
/**
* @param array $policies
* @param bool $includeBase If true, will include default's WFW policies
* @param array $templateArray Template array to specify the query policy order
* @return array
*/
public static function queriesPolicy(
array $policies = [],
bool $includeBase=true,
array $templateArray=[
UserTypeBasedCommandAccessRule::class => []
]
): array {
$base = [];
if($includeBase) $base = array_merge(
UploaderAccessControlPolicies::queriesPolicy(),
UsersAccessControlPolicies::queriesPolicy(),
GeneralAccessControlPolicies::queriesPolicy(),
LangAccessControlPolicies::queriesPolicy()
);
return array_merge($templateArray,$base,$policies);
}
/**
* @param array $policies [(string|UserType::class)target => [ICommand:class]]
* @param bool $includeBase If true, will include default's WFW policies
* @param array $templateArray Template array to specify the command policies order
* @return array
*/
public static function commandsPolicy(
array $policies = [],
bool $includeBase=true,
array $templateArray=[
UserTypeBasedCommandAccessRule::class => []
]
): array {
$base = [];
if($includeBase) $base = array_merge(
UploaderAccessControlPolicies::commandsPolicy(),
UsersAccessControlPolicies::commandsPolicy(),
GeneralAccessControlPolicies::commandsPolicy(),
LangAccessControlPolicies::commandsPolicy()
);
return array_merge($templateArray,$base,$policies);
}
/**
* @param array $policies [AccessRuleClass=>params]
* @param bool $includeBase If true, will include default's WFW policies.
......@@ -34,6 +82,7 @@ final class WFWDefaultSecurityPolicy extends SecurityPolicy {
UploaderAccessControlPolicies::accessPolicy(),
UsersAccessControlPolicies::accessPolicy(),
GeneralAccessControlPolicies::accessPolicy(),
LangAccessControlPolicies::accessPolicy(),
[ ValidToken::class => [] ]
);
return array_merge($templateArray,$base,$policies);
......@@ -54,8 +103,10 @@ final class WFWDefaultSecurityPolicy extends SecurityPolicy {
): array {
$base = [];
if($includeBase) $base = array_merge(
UploaderAccessControlPolicies::hooksPolicy(),
UsersAccessControlPolicies::hooksPolicy(),
GeneralAccessControlPolicies::hooksPolicy()
GeneralAccessControlPolicies::hooksPolicy(),
LangAccessControlPolicies::hooksPolicy()
);
return array_merge($template,$base,$policies);
}
......
......@@ -2,9 +2,11 @@
namespace wfw\engine\package\lang\security;
use wfw\engine\core\security\SecurityPolicy;
/**
* Lang package access control policies
*/
final class LangAccessControlPolicies {
final class LangAccessControlPolicies extends SecurityPolicy {
public const DISABLE = ["^lang(/.*|)"];
}
\ No newline at end of file
......@@ -2,6 +2,7 @@
namespace wfw\engine\package\uploader\security;
use wfw\engine\core\command\security\rules\UserTypeBasedCommandAccessRule;
use wfw\engine\core\security\rules\RequireAuthentification;
use wfw\engine\core\security\SecurityPolicy;
......@@ -11,6 +12,15 @@ use wfw\engine\core\security\SecurityPolicy;
class UploaderAccessControlPolicies extends SecurityPolicy {
public const DISABLE = ["^uploader(/.*|)$"];
/**
* @return array
*/
public static function commandsPolicy(): array {
return [
UserTypeBasedCommandAccessRule::class => []
];
}
/**
* @return array [AccessRuleClass => params]
*/
......
......@@ -3,8 +3,17 @@
namespace wfw\engine\package\users\security;
use wfw\engine\core\action\NotFoundHook;
use wfw\engine\core\command\security\rules\UserTypeBasedCommandAccessRule;
use wfw\engine\core\security\rules\RequireAuthentification;
use wfw\engine\core\security\SecurityPolicy;
use wfw\engine\package\users\command\CancelPasswordRetrieving;
use wfw\engine\package\users\command\ChangePassword;
use wfw\engine\package\users\command\ChangeUserMail;
use wfw\engine\package\users\command\ConfirmUserMailChange;
use wfw\engine\package\users\command\ConfirmUserRegistration;
use wfw\engine\package\users\command\RegisterUser;
use wfw\engine\package\users\command\ResetPassword;
use wfw\engine\package\users\command\RetrievePassword;
/**
* Users package control access policies
......@@ -33,6 +42,27 @@ class UsersAccessControlPolicies extends SecurityPolicy{
];
}
/**
* @return array
*/
public static function commandsPolicy(): array {
return [
UserTypeBasedCommandAccessRule::class => [
UserTypeBasedCommandAccessRule::PUBLIC => [
RegisterUser::class,
ResetPassword::class,
RetrievePassword::class,
ConfirmUserRegistration::class
],
UserTypeBasedCommandAccessRule::ANY => [
ChangeUserMail::class,
ConfirmUserMailChange::class,
ChangePassword::class
]
]
];
}
/**
* @param bool $restrictMode
* @return array [HookClass => params ]
......
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