diff --git a/scripts/install/setSimpleAccess.php b/config/default/FuncAccessControl.conf.php old mode 100755 new mode 100644 similarity index 50% rename from scripts/install/setSimpleAccess.php rename to config/default/FuncAccessControl.conf.php index fbfe779cc0..116218cbd4 --- a/scripts/install/setSimpleAccess.php +++ b/config/default/FuncAccessControl.conf.php @@ -15,22 +15,9 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * - * Copyright (c) 2013 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); - * - * + * Copyright (c) 2020 (original work) Open Assessment Technologies SA; */ -use oat\tao\model\accessControl\func\AccessRule; -use oat\tao\model\accessControl\func\AclProxy as FuncProxy; -use oat\tao\model\accessControl\data\implementation\FreeAccess; - -$impl = new oat\tao\model\accessControl\func\implementation\SimpleAccess(); +use oat\tao\model\accessControl\func\implementation\CacheOnly; -$exts = common_ext_ExtensionsManager::singleton()->getInstalledExtensions(); -foreach ($exts as $extension) { - foreach ($extension->getManifest()->getAclTable() as $tableEntry) { - $rule = new AccessRule($tableEntry[0], $tableEntry[1], $tableEntry[2]); - $impl->applyRule($rule); - } -} -FuncProxy::setImplementation($impl); +return new CacheOnly(); diff --git a/manifest.php b/manifest.php index ebe9734f36..2e519391a7 100755 --- a/manifest.php +++ b/manifest.php @@ -1,7 +1,5 @@ 'TAO Base', 'description' => 'TAO meta-extension', 'license' => 'GPL-2.0', - 'version' => '45.5.0', + 'version' => '45.6.0', 'author' => 'Open Assessment Technologies, CRP Henri Tudor', 'requires' => [ 'generis' => '>=13.0.0', @@ -110,7 +109,6 @@ ], 'php' => [ __DIR__ . '/scripts/install/addFileUploadSource.php', - __DIR__ . '/scripts/install/setSimpleAccess.php', SetServiceFileStorage::class, SetServiceState::class, __DIR__ . '/scripts/install/setJsConfig.php', @@ -195,10 +193,10 @@ [AccessRule::GRANT, TaoRoles::REST_PUBLISHER, ['ext' => 'tao','mod' => 'TaskQueue', 'act' => 'get']], [AccessRule::GRANT, TaoRoles::REST_PUBLISHER, ['ext' => 'tao','mod' => 'TaskQueue', 'act' => 'getStatus']], [AccessRule::GRANT, TaoRoles::SYSTEM_ADMINISTRATOR, ['ext' => 'tao','mod' => 'ExtensionsManager']], - [AccessRule::GRANT, TaoRoles::LOCK_MANAGER, 'tao_actions_Lock@forceRelease'], - [AccessRule::GRANT, TaoRoles::PROPERTY_MANAGER, 'tao_actions_PropertiesAuthoring'], + [AccessRule::GRANT, TaoRoles::LOCK_MANAGER, tao_actions_Lock::class.'@forceRelease'], + [AccessRule::GRANT, TaoRoles::PROPERTY_MANAGER, tao_actions_PropertiesAuthoring::class], [AccessRule::GRANT, TaoRoles::SYSTEM_ADMINISTRATOR, Users::class], - [AccessRule::GRANT, TaoRoles::GLOBAL_MANAGER, Users::class], + [AccessRule::GRANT, TaoRoles::GLOBAL_MANAGER, Users::class], ], 'routes' => [ '/tao/api' => ['class' => ApiRoute::class], diff --git a/models/classes/accessControl/func/AccessRule.php b/models/classes/accessControl/func/AccessRule.php index 1427c26842..596a9d63c1 100644 --- a/models/classes/accessControl/func/AccessRule.php +++ b/models/classes/accessControl/func/AccessRule.php @@ -22,6 +22,7 @@ namespace oat\tao\model\accessControl\func; use core_kernel_classes_Resource; +use common_exception_InconsistentData; /** * An access rule gramnting or denying access to a functionality @@ -32,21 +33,40 @@ */ class AccessRule { - const GRANT = 'grant'; - const DENY = 'deny'; + public const GRANT = 'grant'; + public const DENY = 'deny'; + public const SCOPE_EXTENSION = 'ext'; + public const SCOPE_CONTROLLER = 'mod'; + public const SCOPE_ACTION = 'act'; + + /** @var string */ private $grantDeny; + /** @var string */ private $role; + /** @var string */ private $mask; - - public function __construct($mode, $roleUri, $mask) + /** @var string */ + private $scope; + /** @var string */ + private $extension; + /** @var string */ + private $controller; + /** @var string */ + private $action; + + /** + * @throws common_exception_InconsistentData + */ + public function __construct(string $mode, string $roleUri, $mask) { $this->grantDeny = $mode; - $this->role = new core_kernel_classes_Resource($roleUri); + $this->role = $roleUri; $this->mask = $mask; + $this->parseMask(); } /** @@ -63,16 +83,101 @@ public function isGrant() * @return core_kernel_classes_Resource */ public function getRole() + { + return new core_kernel_classes_Resource($this->role); + } + + public function getRoleId() { return $this->role; } - + /** - * Returns the filter of the rule - * @return array + * @deprecated please used the preparsed extension, controller, action */ public function getMask() { return $this->mask; } + + public function getScope(): string + { + return $this->scope; + } + + public function getAction(): ?string + { + return $this->action; + } + + public function getController(): ?string + { + return $this->controller; + } + + public function getExtensionId(): ?string + { + return $this->extension; + } + + private function parseMask(): void + { + if (is_string($this->mask)) { + $this->parseStringMask($this->mask); + } elseif (is_array($this->mask)) { /// array masks + $this->parseArrayMask($this->mask); + } else { + throw new \common_exception_InconsistentData('Invalid AccessRule mask ' . gettype($this->mask)); + } + } + + private function parseStringMask(string $mask): void + { + $controller = $mask; + $action = null; + if (strpos($mask, '@') !== false) { + [$controller, $action] = explode('@', $mask, 2); + } + if (class_exists($controller)) { + $this->scope = is_null($action) ? self::SCOPE_CONTROLLER : self::SCOPE_ACTION; + $this->controller = $controller; + $this->action = $action; + } else { + throw new \common_exception_InconsistentData('Invalid AccessRule mask ' . $mask); + } + } + + private function parseArrayMask(array $mask): void + { + $legacy = $this->checkLegacyMask($mask); + if (!is_null($legacy)) { + $this->parseStringMask($legacy); + } elseif (isset($mask['act'], $mask['mod'], $mask['ext'])) { + $this->scope = self::SCOPE_ACTION; + $this->controller = FuncHelper::getClassName($mask['ext'], $mask['mod']); + $this->action = $mask['act']; + } elseif (isset($mask['mod'], $mask['ext'])) { + $this->scope = self::SCOPE_CONTROLLER; + $this->controller = FuncHelper::getClassName($mask['ext'], $mask['mod']); + } elseif (isset($mask['ext'])) { + $this->scope = self::SCOPE_EXTENSION; + $this->extension = $mask['ext']; + } else { + throw new \common_exception_InconsistentData('Invalid AccessRule mask ' . implode(',', array_keys($mask))); + } + } + + /** + * Legacy notation, should not be used, but we still need to support it + */ + private function checkLegacyMask(array $mask): ?string + { + if (isset($mask['controller'])) { + return $mask['controller']; + } elseif (isset($mask['act']) && !isset($mask['controller']) && strpos($mask['act'], '@') !== false) { + return $mask['act']; + } else { + return null; + } + } } diff --git a/models/classes/accessControl/func/AclModel.php b/models/classes/accessControl/func/AclModel.php new file mode 100644 index 0000000000..62c7f715d4 --- /dev/null +++ b/models/classes/accessControl/func/AclModel.php @@ -0,0 +1,120 @@ + + */ +class AclModel +{ + private $actionRules = []; + private $controllerRules = []; + private $extensionRules = []; + + public function applyRule(AccessRule $rule): void + { + switch ($rule->getScope()) { + case AccessRule::SCOPE_ACTION: + $this->addAction($rule); + break; + case AccessRule::SCOPE_CONTROLLER: + $this->addController($rule); + break; + case AccessRule::SCOPE_EXTENSION: + $this->addExtension($rule); + break; + } + } + + public function getControllerAcl(string $controllerName, string $extensionId): ControllerAccessRight + { + $controller = new ControllerAccessRight($controllerName, $extensionId); + $this->applyExtensionRules($controller); + $this->applyControllerRules($controller); + $this->applyActionRules($controller); + return $controller; + } + + private function addAction(AccessRule $rule): void + { + $action = $rule->getAction(); + $controller = $rule->getController(); + if (!isset($this->actionRules[$controller])) { + $this->actionRules[$controller] = []; + } + $this->actionRules[$controller][] = [$rule->getRoleId(), $action]; + } + + private function addController(AccessRule $rule): void + { + $controller = $rule->getController(); + if (!isset($this->controllerRules[$controller])) { + $this->controllerRules[$controller] = []; + } + $this->controllerRules[$controller][] = $rule->getRoleId(); + } + + private function addExtension(AccessRule $rule): void + { + $extensionName = $rule->getExtensionId(); + if (!isset($this->extensionRules[$extensionName])) { + $this->extensionRules[$extensionName] = []; + } + $this->extensionRules[$extensionName][] = $rule->getRoleId(); + } + + private function applyActionRules(ControllerAccessRight $controller): ControllerAccessRight + { + if (isset($this->actionRules[$controller->getClassName()])) { + foreach ($this->actionRules[$controller->getClassName()] as $pair) { + $controller->addActionAccess($pair[0], $pair[1]); + } + } + return $controller; + } + + private function applyControllerRules(ControllerAccessRight $controller): ControllerAccessRight + { + if (isset($this->controllerRules[$controller->getClassName()])) { + foreach ($this->controllerRules[$controller->getClassName()] as $roleId) { + $controller->addFullAccess($roleId); + } + } + return $controller; + } + + private function applyExtensionRules(ControllerAccessRight $controller): ControllerAccessRight + { + $extensionId = $controller->getExtensionId(); + if (isset($this->extensionRules[$extensionId])) { + foreach ($this->extensionRules[$extensionId] as $roleId) { + $controller->addFullAccess($roleId); + } + } + return $controller; + } +} diff --git a/models/classes/accessControl/func/AclModelFactory.php b/models/classes/accessControl/func/AclModelFactory.php new file mode 100644 index 0000000000..df230aea23 --- /dev/null +++ b/models/classes/accessControl/func/AclModelFactory.php @@ -0,0 +1,53 @@ + + */ +class AclModelFactory extends ConfigurableService +{ + public function buildModel(): AclModel + { + $aclModel = new AclModel(); + foreach ($this->getExtensionManager()->getInstalledExtensions() as $ext) { + foreach ($ext->getManifest()->getAclTable() as $tableEntry) { + $rule = new AccessRule($tableEntry[0], $tableEntry[1], $tableEntry[2]); + $aclModel->applyRule($rule); + } + } + return $aclModel; + } + + private function getExtensionManager(): common_ext_ExtensionsManager + { + return $this->getServiceLocator()->get(common_ext_ExtensionsManager::SERVICE_ID); + } +} diff --git a/models/classes/accessControl/func/AclProxy.php b/models/classes/accessControl/func/AclProxy.php index 54d9254548..4929f3373b 100644 --- a/models/classes/accessControl/func/AclProxy.php +++ b/models/classes/accessControl/func/AclProxy.php @@ -22,8 +22,6 @@ namespace oat\tao\model\accessControl\func; use oat\tao\model\accessControl\AccessControl; -use common_ext_ExtensionsManager; -use common_Logger; use oat\oatbox\user\User; use oat\oatbox\service\ServiceManager; @@ -37,12 +35,8 @@ */ class AclProxy implements AccessControl { - const SERVICE_ID = 'tao/FuncAccessControl'; - - const CONFIG_KEY_IMPLEMENTATION = 'FuncAccessControl'; - - const FALLBACK_IMPLEMENTATION_CLASS = 'oat\tao\model\accessControl\func\implementation\NoAccess'; - + public const SERVICE_ID = 'tao/FuncAccessControl'; + /** * @return FuncAccessControl */ diff --git a/models/classes/accessControl/func/ControllerAccessRight.php b/models/classes/accessControl/func/ControllerAccessRight.php new file mode 100644 index 0000000000..551e51b676 --- /dev/null +++ b/models/classes/accessControl/func/ControllerAccessRight.php @@ -0,0 +1,104 @@ + + * @package tao + + */ +class ControllerAccessRight implements \JsonSerializable +{ + private $className; + private $extensionId; + + private $rights_actions = []; + private $rights_full = []; + + public static function fromJson(string $json): self + { + $data = json_decode($json, true); + $controller = new self($data['c'], $data['e']); + $controller->rights_actions = $data['a']; + $controller->rights_full = $data['f']; + return $controller; + } + + public function __construct(string $className, string $extensionId) + { + $this->className = $className; + $this->extensionId = $extensionId; + } + + public function addFullAccess($role): void + { + $this->rights_full[] = $role; + } + + public function addActionAccess($role, $action): void + { + if (!isset($this->rights_actions[$action])) { + $this->rights_actions[$action] = []; + } + $this->rights_actions[$action][] = $role; + } + + public function getClassName(): string + { + return $this->className; + } + + public function getExtensionId(): string + { + return $this->extensionId; + } + + /** + * @return array list of roles that have access to this action + */ + public function getAllowedRoles(string $action): array + { + $allowed = $this->rights_full; + if (isset($this->rights_actions[$action])) { + $allowed = array_merge($allowed, $this->rights_actions[$action]); + } + return $allowed; + } + + public function jsonSerialize() + { + return [ + 'c' => $this->className, + 'e' => $this->extensionId, + 'a' => $this->rights_actions, + 'f' => $this->rights_full, + ]; + } +} diff --git a/models/classes/accessControl/func/FuncAccessControl.php b/models/classes/accessControl/func/FuncAccessControl.php index 066ba04387..14d2546956 100644 --- a/models/classes/accessControl/func/FuncAccessControl.php +++ b/models/classes/accessControl/func/FuncAccessControl.php @@ -28,7 +28,8 @@ */ interface FuncAccessControl { - + public const SERVICE_ID = 'tao/FuncAccessControl'; + /** * Returns whenever or not a user has access to a specified call * diff --git a/models/classes/accessControl/func/implementation/CacheOnly.php b/models/classes/accessControl/func/implementation/CacheOnly.php new file mode 100644 index 0000000000..c6372076b4 --- /dev/null +++ b/models/classes/accessControl/func/implementation/CacheOnly.php @@ -0,0 +1,171 @@ + + */ +class CacheOnly extends ConfigurableService implements FuncAccessControl, AccessControl +{ + private const CACHE_PREFIX = 'funcacl::'; + + /** + * (non-PHPdoc) + * @see AccessControl::hasAccess() + */ + public function hasAccess(User $user, $controller, $action, $parameters) + { + return $this->accessPossible($user, $controller, $action); + } + + /** + * (non-PHPdoc) + * @see FuncAccessControl::accessPossible() + */ + public function accessPossible(User $user, $controllerName, $action) + { + $userRoles = $user->getRoles(); + try { + $controllerAccess = $this->getController($controllerName); + $allowedRoles = $controllerAccess->getAllowedRoles($action); + $accessAllowed = count(array_intersect($userRoles, $allowedRoles)) > 0; + } catch (\ReflectionException $e) { + $this->logInfo('Unknown controller ' . $controllerName); + $accessAllowed = false; + } catch (\common_cache_NotFoundException $e) { + $this->logInfo('Unknown controller ' . $controllerName); + $accessAllowed = false; + } + + return (bool) $accessAllowed; + } + + /** + * (non-PHPdoc) + * @see FuncAccessControl::applyRule() + */ + public function applyRule(AccessRule $rule) + { + // nothing to do + } + + /** + * (non-PHPdoc) + * @see FuncAccessControl::revokeRule() + */ + public function revokeRule(AccessRule $rule) + { + // nothing to do + } + + public function buildCache(): void + { + $factory = $this->getServiceLocator()->get(AclModelFactory::class); + $this->cacheModel($factory->buildModel()); + } + + /** + * Returns the access rights of a controller, either read from cache + * or triggers a regeneration ofthe cache + */ + protected function getController($controllerName): ControllerAccessRight + { + $cache = $this->getCache()->get(self::CACHE_PREFIX . $controllerName); + if (is_null($cache)) { + if (!$this->getControllerMapFactory()->isControllerClassNameValid($controllerName)) { + // do not rebuild cache if controller is invalid, to prevent CPU consumtion attacks + // return empty controller instead + return new ControllerAccessRight($controllerName); + } + // as we need to parse all manifests, it is easier to write whole cache in one go + $this->buildCache(); + $cache = $this->getCache()->get(self::CACHE_PREFIX . $controllerName); + } + return ControllerAccessRight::fromJson($cache); + } + + protected function buildModel(): AclModel + { + $aclModel = new AclModel(); + foreach ($this->getExtensionManager()->getInstalledExtensions() as $ext) { + foreach ($ext->getManifest()->getAclTable() as $tableEntry) { + $rule = new AccessRule($tableEntry[0], $tableEntry[1], $tableEntry[2]); + $aclModel->applyRule($rule); + } + } + return $aclModel; + } + + /** + * Cache the acl model, ensuring to write all controllers, + * not just controllers with access rights to prevent + * unncesessary regeneration of the cache + */ + protected function cacheModel(AclModel $aclModel): void + { + $controllerFactory = $this->getControllerMapFactory(); + foreach ($this->getExtensionManager()->getInstalledExtensions() as $ext) { + foreach ($controllerFactory->getControllers($ext->getId()) as $controller) { + $controllerName = $controller->getClassName(); + $this->cacheController($aclModel->getControllerAcl($controllerName, $ext->getId())); + } + } + } + + private function cacheController(ControllerAccessRight $controller): void + { + $data = json_encode($controller); + $this->getCache()->set(self::CACHE_PREFIX . $controller->getClassName(), $data); + } + + private function getExtensionManager(): common_ext_ExtensionsManager + { + return $this->getServiceLocator()->get(common_ext_ExtensionsManager::SERVICE_ID); + } + + private function getControllerMapFactory(): Factory + { + return new Factory(); + } + + private function getCache(): SimpleCache + { + return $this->getServiceLocator()->get(SimpleCache::SERVICE_ID); + } +} diff --git a/models/classes/accessControl/func/implementation/SimpleAccess.php b/models/classes/accessControl/func/implementation/SimpleAccess.php index ec574c715a..c124a6ba71 100644 --- a/models/classes/accessControl/func/implementation/SimpleAccess.php +++ b/models/classes/accessControl/func/implementation/SimpleAccess.php @@ -32,20 +32,13 @@ use oat\oatbox\service\ConfigurableService; /** - * Simple ACL Implementation deciding whenever or not to allow access - * strictly by the BASEUSER role and a whitelist - * - * Not to be used in production, since testtakers cann access the backoffice - * - * @access public - * @author Joel Bout, - * @package tao - + * DO NOT USE, will leave the system vunerable to exploits + * @deprecated */ class SimpleAccess extends ConfigurableService implements FuncAccessControl { - const WHITELIST_KEY = 'SimpleAclWhitelist'; + private const WHITELIST_KEY = 'SimpleAclWhitelist'; private $controllers = []; diff --git a/models/classes/controllerMap/Factory.php b/models/classes/controllerMap/Factory.php index 8107c2f1ea..553731c1cc 100644 --- a/models/classes/controllerMap/Factory.php +++ b/models/classes/controllerMap/Factory.php @@ -107,7 +107,6 @@ private function getControllerClasses(\common_ext_Extension $extension) } if (!empty($namespaces)) { common_Logger::t('Namespace found in routes for extension ' . $extension->getId()); - $classes = []; $recDir = new RecursiveDirectoryIterator($extension->getDir()); $recIt = new RecursiveIteratorIterator($recDir); $regexIt = new RegexIterator($recIt, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH); @@ -148,14 +147,17 @@ private function getControllerClasses(\common_ext_Extension $extension) * * @return bool */ - private function isControllerClassNameValid($controllerClassName) + public function isControllerClassNameValid($controllerClassName) { $returnValue = true; if (!class_exists($controllerClassName)) { common_Logger::w($controllerClassName . ' not found'); $returnValue = false; - } elseif (!is_subclass_of($controllerClassName, 'Module') && !is_subclass_of($controllerClassName, Controller::class)) { + } elseif ( + !is_subclass_of($controllerClassName, 'Module') + && !is_subclass_of($controllerClassName, Controller::class) + ) { common_Logger::w($controllerClassName . ' is not a valid Controller.'); $returnValue = false; } else { diff --git a/test/unit/models/classes/accessControl/func/AccessRuleTest.php b/test/unit/models/classes/accessControl/func/AccessRuleTest.php new file mode 100644 index 0000000000..4a32673186 --- /dev/null +++ b/test/unit/models/classes/accessControl/func/AccessRuleTest.php @@ -0,0 +1,96 @@ +expectException(\common_exception_InconsistentData::class); + } + $accessRule = new AccessRule(AccessRule::GRANT, 'fakeRole', $mask); + $this->assertEquals($expected['scope'], $accessRule->getScope()); + $this->assertEquals($expected['ext'] ?? null, $accessRule->getExtensionId()); + $this->assertEquals($expected['ctrl'] ?? null, $accessRule->getController()); + $this->assertEquals($expected['act'] ?? null, $accessRule->getAction()); + } + + public function dataProviderMask(): array + { + return [ + 'invalid string mask' => [ + 'mask' => 'NotAmasK' + ], + 'valid controller string mask' => [ + 'mask' => \tao_actions_Main::class, + 'expected' => [ + 'scope' => AccessRule::SCOPE_CONTROLLER, + 'ctrl' => \tao_actions_Main::class + ], + ], + 'valid action string mask' => [ + 'mask' => \tao_actions_Main::class.'@index', + 'expected' => [ + 'scope' => AccessRule::SCOPE_ACTION, + 'ctrl' => \tao_actions_Main::class, + 'act' => 'index' + ], + ], + 'invalid array mask' => [ + 'mask' => ['a' => 'b'], + ], + 'valid extension array mask' => [ + 'mask' => [ + 'ext' => 'tao' + ], + 'expected' => [ + 'scope' => AccessRule::SCOPE_EXTENSION, + 'ext' => 'tao' + ], + ], + 'valid controller string mask' => [ + 'mask' => \tao_actions_Main::class, + 'expected' => [ + 'scope' => AccessRule::SCOPE_CONTROLLER, + 'ctrl' => \tao_actions_Main::class + ], + ], + 'valid action string mask' => [ + 'mask' => \tao_actions_Main::class.'@index', + 'expected' => [ + 'scope' => AccessRule::SCOPE_ACTION, + 'ctrl' => \tao_actions_Main::class, + 'act' => 'index' + ], + ], + ]; + } +} diff --git a/test/unit/models/classes/accessControl/func/AclModelTest.php b/test/unit/models/classes/accessControl/func/AclModelTest.php new file mode 100644 index 0000000000..ea9aed2ab3 --- /dev/null +++ b/test/unit/models/classes/accessControl/func/AclModelTest.php @@ -0,0 +1,66 @@ +applyRule($this->mockAccessRule('role0', AccessRule::SCOPE_EXTENSION, ['ext' => 'ext1'])); + $model->applyRule($this->mockAccessRule('role1', AccessRule::SCOPE_CONTROLLER, ['ctrl' => 'sample1'])); + $model->applyRule($this->mockAccessRule('role1', AccessRule::SCOPE_CONTROLLER, ['ctrl' => 'sample2'])); + $model->applyRule($this->mockAccessRule('role2', AccessRule::SCOPE_ACTION, ['ctrl' => 'sample1', 'act' => 'action1'])); + $model->applyRule($this->mockAccessRule('role3', AccessRule::SCOPE_ACTION, ['ctrl' => 'sample1', 'act' => 'action2'])); + + $controller = $model->getControllerAcl('sample1', 'ext1'); + $this->assertEquals(['role0', 'role1', 'role2'], $controller->getAllowedRoles('action1')); + $this->assertEquals(['role0', 'role1', 'role3'], $controller->getAllowedRoles('action2')); + $this->assertEquals(['role0', 'role1'], $controller->getAllowedRoles('action3')); + + $controller2 = $model->getControllerAcl('sample2', 'ext2'); + $this->assertEquals(['role1'], $controller2->getAllowedRoles('action1')); + + $controller3 = $model->getControllerAcl('sample3', 'ext3'); + $this->assertEquals([], $controller3->getAllowedRoles('action1')); + } + + private function mockAccessRule(string $role, string $scope, array $data): AccessRule + { + $prophet = $this->prophesize(AccessRule::class); + $prophet->isGrant()->willReturn(true); + $prophet->getRoleId()->willReturn($role); + $prophet->getScope()->willReturn($scope); + if (isset($data['act'])) { + $prophet->getAction()->willReturn($data['act']); + } + if (isset($data['ctrl'])) { + $prophet->getController()->willReturn($data['ctrl']); + } + if (isset($data['ext'])) { + $prophet->getExtensionId()->willReturn($data['ext']); + } + return $prophet->reveal(); + } +}