├── templates └── bake │ ├── element │ ├── table_methods.twig │ └── entity_methods.twig │ └── policy.twig ├── src ├── Exception │ ├── Exception.php │ ├── AuthorizationRequiredException.php │ ├── MissingIdentityException.php │ └── ForbiddenException.php ├── Policy │ ├── Exception │ │ ├── MissingMethodException.php │ │ └── MissingPolicyException.php │ ├── ResultInterface.php │ ├── RequestPolicyInterface.php │ ├── ResolverInterface.php │ ├── BeforeScopeInterface.php │ ├── BeforePolicyInterface.php │ ├── Result.php │ ├── ResolverCollection.php │ ├── MapResolver.php │ └── OrmResolver.php ├── AuthorizationServiceProviderInterface.php ├── Middleware │ ├── UnauthorizedHandler │ │ ├── ExceptionHandler.php │ │ ├── HandlerInterface.php │ │ ├── HandlerFactory.php │ │ ├── UnauthorizedHandlerTrait.php │ │ ├── CakeRedirectHandler.php │ │ └── RedirectHandler.php │ ├── RequestAuthorizationMiddleware.php │ └── AuthorizationMiddleware.php ├── AuthorizationPlugin.php ├── Identity.php ├── IdentityInterface.php ├── AuthorizationServiceInterface.php ├── Command │ └── PolicyCommand.php ├── IdentityDecorator.php ├── AuthorizationService.php └── Controller │ └── Component │ └── AuthorizationComponent.php ├── LICENSE.txt ├── readme.md └── composer.json /templates/bake/element/table_methods.twig: -------------------------------------------------------------------------------- 1 | /** 2 | * Apply user access controls to a query for index actions 3 | * 4 | * @param \Authorization\IdentityInterface $user The user. 5 | * @param \Cake\ORM\Query\SelectQuery $query The query to apply authorization conditions to. 6 | * @return \Cake\ORM\Query\SelectQuery 7 | */ 8 | public function scopeIndex(IdentityInterface $user, SelectQuery $query): SelectQuery 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/Exception/Exception.php: -------------------------------------------------------------------------------- 1 | add('bake policy', PolicyCommand::class); 38 | } 39 | 40 | return $commands; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Policy/BeforeScopeInterface.php: -------------------------------------------------------------------------------- 1 | $options Options array. 34 | * @return \Psr\Http\Message\ResponseInterface 35 | */ 36 | public function handle( 37 | Exception $exception, 38 | ServerRequestInterface $request, 39 | array $options = [], 40 | ): ResponseInterface; 41 | } 42 | -------------------------------------------------------------------------------- /src/Policy/BeforePolicyInterface.php: -------------------------------------------------------------------------------- 1 | status = $status; 47 | if ($reason !== null) { 48 | $this->reason = $reason; 49 | } 50 | } 51 | 52 | /** 53 | * @inheritDoc 54 | */ 55 | public function getReason(): ?string 56 | { 57 | return $this->reason; 58 | } 59 | 60 | /** 61 | * @inheritDoc 62 | */ 63 | public function getStatus(): bool 64 | { 65 | return $this->status; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Middleware/UnauthorizedHandler/HandlerFactory.php: -------------------------------------------------------------------------------- 1 | $handler, 42 | ]; 43 | } 44 | if (!isset($handler['className'])) { 45 | throw new RuntimeException('Missing `className` key from handler config.'); 46 | } 47 | 48 | $unauthorizedHandler = HandlerFactory::create($handler['className']); 49 | 50 | return $unauthorizedHandler->handle( 51 | $exception, 52 | $request, 53 | $handler, 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Identity.php: -------------------------------------------------------------------------------- 1 | authorization = $service; 44 | $this->identity = $identity; 45 | } 46 | 47 | /** 48 | * Get the primary key/id field for the identity. 49 | * 50 | * @return array|string|int|null 51 | */ 52 | public function getIdentifier(): string|int|array|null 53 | { 54 | return $this->identity->getIdentifier(); 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | public function getOriginalData(): ArrayAccess|array 61 | { 62 | return $this->identity->getOriginalData(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Exception/ForbiddenException.php: -------------------------------------------------------------------------------- 1 | result = $result; 57 | 58 | parent::__construct($message, $code, $previous); 59 | } 60 | 61 | /** 62 | * Returns policy check result if passed to the exception. 63 | * 64 | * @return \Authorization\Policy\ResultInterface|null 65 | */ 66 | public function getResult(): ?ResultInterface 67 | { 68 | return $this->result; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Policy/Exception/MissingPolicyException.php: -------------------------------------------------------------------------------- 1 | getRepository() && 45 | $resource->getRepository() instanceof RepositoryInterface 46 | ) { 47 | $repositoryClass = get_class($resource->getRepository()); 48 | $resource = sprintf($this->_messageTemplate, $resourceClass); 49 | $queryMessage = ' This resource looks like a `Query`. If you are using `OrmResolver`, ' . 50 | 'you should create a new policy class for your `%s` class in `src/Policy/`.'; 51 | $resource .= sprintf($queryMessage, $repositoryClass); 52 | } else { 53 | $resource = [$resourceClass]; 54 | } 55 | } 56 | 57 | parent::__construct($resource, $code, $previous); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/IdentityInterface.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | interface IdentityInterface extends ArrayAccess 32 | { 33 | /** 34 | * Check whether the current identity can perform an action. 35 | * 36 | * @param string $action The action/operation being performed. 37 | * @param mixed $resource The resource being operated on. 38 | * @return bool 39 | */ 40 | public function can(string $action, mixed $resource): bool; 41 | 42 | /** 43 | * Check whether the current identity can perform an action. 44 | * 45 | * @param string $action The action/operation being performed. 46 | * @param mixed $resource The resource being operated on. 47 | * @return \Authorization\Policy\ResultInterface 48 | */ 49 | public function canResult(string $action, mixed $resource): ResultInterface; 50 | 51 | /** 52 | * Apply authorization scope conditions/restrictions. 53 | * 54 | * @param string $action The action/operation being performed. 55 | * @param mixed $resource The resource being operated on. 56 | * @param mixed $optionalArgs Multiple additional arguments which are passed to the scope 57 | * @return mixed The modified resource. 58 | */ 59 | public function applyScope(string $action, mixed $resource, mixed ...$optionalArgs): mixed; 60 | 61 | /** 62 | * Get the decorated identity 63 | * 64 | * If the decorated identity implements `getOriginalData()` 65 | * that method should be invoked to expose the original data. 66 | * 67 | * @return \ArrayAccess|array 68 | */ 69 | public function getOriginalData(): ArrayAccess|array; 70 | } 71 | -------------------------------------------------------------------------------- /src/Middleware/UnauthorizedHandler/CakeRedirectHandler.php: -------------------------------------------------------------------------------- 1 | [ 36 | MissingIdentityException::class, 37 | ], 38 | 'url' => [ 39 | 'controller' => 'Users', 40 | 'action' => 'login', 41 | ], 42 | 'queryParam' => 'redirect', 43 | 'statusCode' => 302, 44 | 'allowedRedirectExtensions' => true, 45 | ]; 46 | 47 | /** 48 | * Constructor. 49 | * 50 | * @throws \RuntimeException When `Cake\Routing\Router` class cannot be found. 51 | */ 52 | public function __construct() 53 | { 54 | if (!class_exists(Router::class)) { 55 | $message = sprintf( 56 | 'Class `%s` does not exist. ' . 57 | 'Make sure you are using a full CakePHP framework ' . 58 | 'and have autoloading configured properly.', 59 | Router::class, 60 | ); 61 | throw new RuntimeException($message); 62 | } 63 | } 64 | 65 | /** 66 | * @inheritDoc 67 | */ 68 | protected function getUrl(ServerRequestInterface $request, array $options): string 69 | { 70 | $url = $options['url']; 71 | if ($options['queryParam'] !== null) { 72 | $uri = $request->getUri(); 73 | $redirect = $uri->getPath(); 74 | if ($uri->getQuery()) { 75 | $redirect .= '?' . $uri->getQuery(); 76 | } 77 | 78 | $url['?'][$options['queryParam']] = $redirect; 79 | } 80 | 81 | return Router::url($url); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakephp/authorization", 3 | "description": "Authorization abstraction layer plugin for CakePHP", 4 | "license": "MIT", 5 | "type": "cakephp-plugin", 6 | "keywords": [ 7 | "auth", 8 | "authorization", 9 | "access", 10 | "cakephp" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "CakePHP Community", 15 | "homepage": "https://github.com/cakephp/authorization/graphs/contributors" 16 | } 17 | ], 18 | "support": { 19 | "issues": "https://github.com/cakephp/authorization/issues", 20 | "forum": "https://stackoverflow.com/tags/cakephp", 21 | "irc": "irc://irc.freenode.org/cakephp", 22 | "source": "https://github.com/cakephp/authorization", 23 | "docs": "https://cakephp.org/authorization/2/en/" 24 | }, 25 | "require": { 26 | "php": ">=8.1", 27 | "cakephp/http": "^5.1", 28 | "psr/http-client": "^1.0", 29 | "psr/http-message": "^1.1 || ^2.0", 30 | "psr/http-server-handler": "^1.0", 31 | "psr/http-server-middleware": "^1.0" 32 | }, 33 | "require-dev": { 34 | "cakephp/authentication": "^3.0", 35 | "cakephp/bake": "^3.2", 36 | "cakephp/cakephp": "^5.1", 37 | "cakephp/cakephp-codesniffer": "^5.1", 38 | "phpunit/phpunit": "^10.5.5 || ^11.1.3" 39 | }, 40 | "suggest": { 41 | "cakephp/http": "To use \"RequestPolicyInterface\" (Not needed separately if using full CakePHP framework).", 42 | "cakephp/orm": "To use \"OrmResolver\" (Not needed separately if using full CakePHP framework)." 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Authorization\\": "src/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Authorization\\Test\\": "tests/", 52 | "OverridePlugin\\": "tests/test_app/Plugin/OverridePlugin/src/", 53 | "TestApp\\": "tests/test_app/TestApp/", 54 | "TestPlugin\\": "tests/test_app/Plugin/TestPlugin/src/" 55 | } 56 | }, 57 | "config": { 58 | "allow-plugins": { 59 | "cakephp/plugin-installer": true, 60 | "dealerdirect/phpcodesniffer-composer-installer": true 61 | }, 62 | "sort-packages": true 63 | }, 64 | "scripts": { 65 | "check": [ 66 | "@cs-check", 67 | "@stan", 68 | "@test" 69 | ], 70 | "cs-check": "phpcs --colors -p src/ tests/", 71 | "cs-fix": "phpcbf --colors -p src/ tests/", 72 | "phpstan": "tools/phpstan analyse", 73 | "stan": "@phpstan", 74 | "stan-baseline": "tools/phpstan --generate-baseline", 75 | "stan-setup": "phive install", 76 | "test": "phpunit", 77 | "test-coverage": "phpunit --coverage-clover=clover.xml" 78 | }, 79 | "minimum-stability": "dev", 80 | "prefer-stable": true 81 | } 82 | -------------------------------------------------------------------------------- /src/Policy/ResolverCollection.php: -------------------------------------------------------------------------------- 1 | ServicePolicy::class 37 | * ]) 38 | * ]); 39 | * 40 | * $service = new AuthorizationService($collection); 41 | * ``` 42 | */ 43 | class ResolverCollection implements ResolverInterface 44 | { 45 | /** 46 | * Policy resolver instances. 47 | * 48 | * @var array<\Authorization\Policy\ResolverInterface> 49 | */ 50 | protected array $resolvers = []; 51 | 52 | /** 53 | * Constructor. Takes an array of policy resolver instances. 54 | * 55 | * @param array<\Authorization\Policy\ResolverInterface> $resolvers An array of policy resolver instances. 56 | */ 57 | public function __construct(array $resolvers = []) 58 | { 59 | foreach ($resolvers as $resolver) { 60 | $this->add($resolver); 61 | } 62 | } 63 | 64 | /** 65 | * Adds a resolver to the collection. 66 | * 67 | * @param \Authorization\Policy\ResolverInterface $resolver Resolver instance. 68 | * @return $this 69 | */ 70 | public function add(ResolverInterface $resolver) 71 | { 72 | $this->resolvers[] = $resolver; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * @inheritDoc 79 | */ 80 | public function getPolicy(mixed $resource): mixed 81 | { 82 | foreach ($this->resolvers as $resolver) { 83 | try { 84 | return $resolver->getPolicy($resource); 85 | } catch (MissingPolicyException) { 86 | } 87 | } 88 | 89 | throw new MissingPolicyException($resource); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/AuthorizationServiceInterface.php: -------------------------------------------------------------------------------- 1 | \App\Policy\ResourcePolicy::class, 50 | * \App\Service\Resource2::class => $policyObject, 51 | * \App\Service\Resource3::class => function() {}, 52 | * ] 53 | * ``` 54 | * 55 | * @param array $map Resource class name to policy map. 56 | * @param \Cake\Core\ContainerInterface|null $container The DIC instance from the application 57 | */ 58 | public function __construct(array $map = [], ?ContainerInterface $container = null) 59 | { 60 | $this->container = $container; 61 | foreach ($map as $resourceClass => $policy) { 62 | $this->map($resourceClass, $policy); 63 | } 64 | } 65 | 66 | /** 67 | * Maps a resource class to the policy class name. 68 | * 69 | * @param string $resourceClass A resource class name. 70 | * @param callable|object|string $policy A policy class name, an object or a callable factory. 71 | * @return $this 72 | * @throws \InvalidArgumentException When a resource class does not exist or policy is invalid. 73 | */ 74 | public function map(string $resourceClass, string|object|callable $policy) 75 | { 76 | if (!class_exists($resourceClass)) { 77 | $message = sprintf('Resource class `%s` does not exist.', $resourceClass); 78 | throw new InvalidArgumentException($message); 79 | } 80 | 81 | if (is_string($policy) && !class_exists($policy)) { 82 | $message = sprintf('Policy class `%s` does not exist.', $policy); 83 | throw new InvalidArgumentException($message); 84 | } 85 | 86 | $this->map[$resourceClass] = $policy; 87 | 88 | return $this; 89 | } 90 | 91 | /** 92 | * {@inheritDoc} 93 | * 94 | * @throws \InvalidArgumentException When a resource is not an object. 95 | * @throws \Authorization\Policy\Exception\MissingPolicyException When a policy for a resource has not been defined. 96 | */ 97 | public function getPolicy($resource): mixed 98 | { 99 | if (!is_object($resource)) { 100 | $message = sprintf('Resource must be an object, `%s` given.', gettype($resource)); 101 | throw new InvalidArgumentException($message); 102 | } 103 | 104 | $class = get_class($resource); 105 | 106 | if (!isset($this->map[$class])) { 107 | throw new MissingPolicyException($resource); 108 | } 109 | 110 | $policy = $this->map[$class]; 111 | 112 | if (is_callable($policy)) { 113 | return $policy($resource, $this); 114 | } 115 | 116 | if (is_object($policy)) { 117 | return $policy; 118 | } 119 | 120 | if ($this->container && $this->container->has($policy)) { 121 | return $this->container->get($policy); 122 | } 123 | 124 | return new $policy(); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Middleware/RequestAuthorizationMiddleware.php: -------------------------------------------------------------------------------- 1 | 'authorization', 52 | 'identityAttribute' => 'identity', 53 | 'method' => 'access', 54 | 'unauthorizedHandler' => 'Authorization.Exception', 55 | ]; 56 | 57 | /** 58 | * Constructor 59 | * 60 | * @param array $config Configuration options 61 | */ 62 | public function __construct(array $config = []) 63 | { 64 | $this->setConfig($config); 65 | } 66 | 67 | /** 68 | * Gets the authorization service from the request attribute 69 | * 70 | * @param \Psr\Http\Message\ServerRequestInterface $request Server request. 71 | * @return \Authorization\AuthorizationServiceInterface 72 | */ 73 | protected function getServiceFromRequest(ServerRequestInterface $request): AuthorizationServiceInterface 74 | { 75 | $serviceAttribute = $this->getConfig('authorizationAttribute'); 76 | $service = $request->getAttribute($serviceAttribute); 77 | 78 | if (!$service instanceof AuthorizationServiceInterface) { 79 | $errorMessage = self::class . ' could not find the authorization service in the request attribute. ' . 80 | 'Make sure you added the AuthorizationMiddleware before this middleware or that you ' . 81 | 'somehow else added the service to the requests `' . $serviceAttribute . '` attribute.'; 82 | 83 | throw new RuntimeException($errorMessage); 84 | } 85 | 86 | return $service; 87 | } 88 | 89 | /** 90 | * Callable implementation for the middleware stack. 91 | * 92 | * @param \Psr\Http\Message\ServerRequestInterface $request The request. 93 | * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler. 94 | * @return \Psr\Http\Message\ResponseInterface A response. 95 | */ 96 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 97 | { 98 | $service = $this->getServiceFromRequest($request); 99 | $identity = $request->getAttribute($this->getConfig('identityAttribute')); 100 | 101 | $result = $service->canResult($identity, $this->getConfig('method'), $request); 102 | try { 103 | if (!$result->getStatus()) { 104 | throw new ForbiddenException($result, [$this->getConfig('method'), $request->getRequestTarget()]); 105 | } 106 | } catch (Exception $exception) { 107 | return $this->handleException($exception, $request, $this->getConfig('unauthorizedHandler')); 108 | } 109 | 110 | return $handler->handle($request); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Command/PolicyCommand.php: -------------------------------------------------------------------------------- 1 | type === 'table') { 57 | $name .= 'Table'; 58 | } 59 | 60 | return $name . 'Policy.php'; 61 | } 62 | 63 | /** 64 | * @inheritDoc 65 | */ 66 | public function template(): string 67 | { 68 | return 'Authorization.policy'; 69 | } 70 | 71 | /** 72 | * @inheritDoc 73 | */ 74 | public function templateData(Arguments $arguments): array 75 | { 76 | $data = parent::templateData($arguments); 77 | 78 | $name = $arguments->getArgument('name'); 79 | if (!$name) { 80 | throw new RuntimeException('You must specify name of policy to create.'); 81 | } 82 | 83 | $name = $this->_getName($name); 84 | $type = $this->type = (string)$arguments->getOption('type'); 85 | 86 | $suffix = ''; 87 | if ($type === 'table') { 88 | $suffix = 'Table'; 89 | } 90 | 91 | $imports = []; 92 | $className = $data['namespace'] . '\\' . $name; 93 | if ($type === 'table') { 94 | $className = "{$data['namespace']}\Model\\Table\\{$name}{$suffix}"; 95 | $imports[] = 'Cake\ORM\Query\SelectQuery'; 96 | } elseif ($type === 'entity') { 97 | $className = "{$data['namespace']}\Model\\Entity\\{$name}"; 98 | $imports[] = $className; 99 | } 100 | $imports[] = 'Authorization\\IdentityInterface'; 101 | 102 | $variable = Inflector::variable($name); 103 | if ($variable === 'user') { 104 | $variable = 'resource'; 105 | } 106 | 107 | $vars = [ 108 | 'name' => $name, 109 | 'type' => $type, 110 | 'suffix' => $suffix, 111 | 'variable_name' => $variable, 112 | 'classname' => $className, 113 | 'imports' => $imports, 114 | ]; 115 | 116 | return $vars + $data; 117 | } 118 | 119 | /** 120 | * Gets the option parser instance and configures it. 121 | * 122 | * @param \Cake\Console\ConsoleOptionParser $parser The parser to update. 123 | * @return \Cake\Console\ConsoleOptionParser 124 | */ 125 | public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser 126 | { 127 | $parser = $this->_setCommonOptions($parser); 128 | 129 | return $parser 130 | ->setDescription('Bake policy classes for various supported object types.') 131 | ->addArgument('name', [ 132 | 'help' => 'The name of the policy class to create.', 133 | ]) 134 | ->addOption('type', [ 135 | 'help' => 'The object type to bake a policy for. If only one argument is used, type will be object.', 136 | 'default' => 'entity', 137 | 'choices' => ['table', 'entity', 'object'], 138 | 'required' => true, 139 | ]); 140 | } 141 | 142 | /** 143 | * Do nothing (for now) 144 | * 145 | * @param string $className The class to bake a test for. 146 | * @param \Cake\Console\Arguments $args The arguments object 147 | * @param \Cake\Console\ConsoleIo $io The consoleio object 148 | * @return void 149 | */ 150 | public function bakeTest(string $className, Arguments $args, ConsoleIo $io): void 151 | { 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Middleware/UnauthorizedHandler/RedirectHandler.php: -------------------------------------------------------------------------------- 1 | [ 44 | MissingIdentityException::class, 45 | ], 46 | 'url' => '/login', 47 | 'queryParam' => 'redirect', 48 | 'statusCode' => 302, 49 | 'allowedRedirectExtensions' => true, 50 | ]; 51 | 52 | /** 53 | * {@inheritDoc} 54 | * 55 | * Return a response with a location header set if an exception matches. 56 | */ 57 | public function handle( 58 | Exception $exception, 59 | ServerRequestInterface $request, 60 | array $options = [], 61 | ): ResponseInterface { 62 | $options += $this->defaultOptions; 63 | 64 | if (!$this->redirectAllowed($request, $options) || !$this->checkException($exception, $options['exceptions'])) { 65 | throw $exception; 66 | } 67 | 68 | $url = $this->getUrl($request, $options); 69 | 70 | $response = new Response(); 71 | 72 | return $response 73 | ->withHeader('Location', $url) 74 | ->withStatus($options['statusCode']); 75 | } 76 | 77 | /** 78 | * Checks if an exception matches one of the classes. 79 | * 80 | * @param \Authorization\Exception\Exception $exception Exception instance. 81 | * @param array<\Exception> $exceptions A list of exception classes. 82 | * @return bool 83 | */ 84 | protected function checkException(Exception $exception, array $exceptions): bool 85 | { 86 | foreach ($exceptions as $class) { 87 | if ($exception instanceof $class) { 88 | return true; 89 | } 90 | } 91 | 92 | return false; 93 | } 94 | 95 | /** 96 | * Returns the url for the Location header. 97 | * 98 | * @param \Psr\Http\Message\ServerRequestInterface $request Server request. 99 | * @param array $options Options. 100 | * @return string 101 | */ 102 | protected function getUrl(ServerRequestInterface $request, array $options): string 103 | { 104 | $url = $options['url']; 105 | if ($options['queryParam'] !== null && $request->getMethod() === 'GET') { 106 | $uri = $request->getUri(); 107 | $redirect = $uri->getPath(); 108 | if ($uri->getQuery()) { 109 | $redirect .= '?' . $uri->getQuery(); 110 | } 111 | $query = urlencode($options['queryParam']) . '=' . urlencode($redirect); 112 | if (str_contains($url, '?')) { 113 | $query = '&' . $query; 114 | } else { 115 | $query = '?' . $query; 116 | } 117 | 118 | $url .= $query; 119 | } 120 | 121 | return $url; 122 | } 123 | 124 | /** 125 | * @param \Psr\Http\Message\ServerRequestInterface $request 126 | * @param array $options 127 | * @return bool 128 | */ 129 | protected function redirectAllowed(ServerRequestInterface $request, array $options): bool 130 | { 131 | $extensions = $options['allowedRedirectExtensions'] ?? true; 132 | if ($extensions === false) { 133 | return false; 134 | } 135 | if ($extensions === true) { 136 | return true; 137 | } 138 | 139 | /** @var \Cake\Http\ServerRequest $request */ 140 | $currentExtension = $request->getParam('_ext'); 141 | if (!$currentExtension) { 142 | return true; 143 | } 144 | 145 | return in_array($currentExtension, (array)$extensions, true); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/IdentityDecorator.php: -------------------------------------------------------------------------------- 1 | authorization = $service; 56 | $this->identity = $identity; 57 | } 58 | 59 | /** 60 | * @inheritDoc 61 | */ 62 | public function can(string $action, mixed $resource): bool 63 | { 64 | return $this->authorization->can($this, $action, $resource); 65 | } 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | public function canResult(string $action, mixed $resource): ResultInterface 71 | { 72 | return $this->authorization->canResult($this, $action, $resource); 73 | } 74 | 75 | /** 76 | * @inheritDoc 77 | */ 78 | public function applyScope(string $action, mixed $resource, mixed ...$optionalArgs): mixed 79 | { 80 | return $this->authorization->applyScope($this, $action, $resource, ...$optionalArgs); 81 | } 82 | 83 | /** 84 | * @inheritDoc 85 | */ 86 | public function getOriginalData(): ArrayAccess|array 87 | { 88 | if ( 89 | $this->identity 90 | && !is_array($this->identity) 91 | && method_exists($this->identity, 'getOriginalData') 92 | ) { 93 | return $this->identity->getOriginalData(); 94 | } 95 | 96 | return $this->identity; 97 | } 98 | 99 | /** 100 | * Delegate unknown methods to decorated identity. 101 | * 102 | * @param string $method The method being invoked. 103 | * @param array $args The arguments for the method. 104 | * @return mixed 105 | */ 106 | public function __call(string $method, array $args): mixed 107 | { 108 | if (!is_object($this->identity)) { 109 | throw new BadMethodCallException("Cannot call `{$method}`. Identity data is not an object."); 110 | } 111 | $call = [$this->identity, $method]; 112 | 113 | /** @phpstan-ignore callable.nonCallable */ 114 | return $call(...$args); 115 | } 116 | 117 | /** 118 | * Delegate property access to decorated identity. 119 | * 120 | * @param string $property The property to read. 121 | * @return mixed 122 | */ 123 | public function __get(string $property): mixed 124 | { 125 | return $this->identity->{$property}; 126 | } 127 | 128 | /** 129 | * Delegate property isset to decorated identity. 130 | * 131 | * @param string $property The property to read. 132 | * @return bool 133 | */ 134 | public function __isset(string $property): bool 135 | { 136 | return isset($this->identity->{$property}); 137 | } 138 | 139 | /** 140 | * Whether a offset exists 141 | * 142 | * @link https://secure.php.net/manual/en/arrayaccess.offsetexists.php 143 | * @param mixed $offset Offset 144 | * @return bool 145 | */ 146 | public function offsetExists(mixed $offset): bool 147 | { 148 | return isset($this->identity[$offset]); 149 | } 150 | 151 | /** 152 | * Offset to retrieve 153 | * 154 | * @link https://secure.php.net/manual/en/arrayaccess.offsetget.php 155 | * @param mixed $offset Offset 156 | * @return mixed 157 | */ 158 | public function offsetGet(mixed $offset): mixed 159 | { 160 | if (isset($this->identity[$offset])) { 161 | return $this->identity[$offset]; 162 | } 163 | 164 | return null; 165 | } 166 | 167 | /** 168 | * Offset to set 169 | * 170 | * @link https://secure.php.net/manual/en/arrayaccess.offsetset.php 171 | * @param mixed $offset The offset to assign the value to. 172 | * @param mixed $value Value 173 | * @return void 174 | */ 175 | public function offsetSet(mixed $offset, mixed $value): void 176 | { 177 | $this->identity[$offset] = $value; 178 | } 179 | 180 | /** 181 | * Offset to unset 182 | * 183 | * @link https://secure.php.net/manual/en/arrayaccess.offsetunset.php 184 | * @param mixed $offset Offset 185 | * @return void 186 | */ 187 | public function offsetUnset(mixed $offset): void 188 | { 189 | unset($this->identity[$offset]); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/AuthorizationService.php: -------------------------------------------------------------------------------- 1 | resolver = $resolver; 50 | } 51 | 52 | /** 53 | * @inheritDoc 54 | */ 55 | public function can(?IdentityInterface $user, string $action, $resource, ...$optionalArgs): bool 56 | { 57 | $result = $this->performCheck($user, $action, $resource, ...$optionalArgs); 58 | 59 | return is_bool($result) ? $result : $result->getStatus(); 60 | } 61 | 62 | /** 63 | * @inheritDoc 64 | */ 65 | public function canResult(?IdentityInterface $user, string $action, $resource, ...$optionalArgs): ResultInterface 66 | { 67 | $result = $this->performCheck($user, $action, $resource, ...$optionalArgs); 68 | 69 | return is_bool($result) ? new Result($result) : $result; 70 | } 71 | 72 | /** 73 | * Check whether the provided user can perform an action on a resource. 74 | * 75 | * @param \Authorization\IdentityInterface|null $user The user to check permissions for. 76 | * @param string $action The action/operation being performed. 77 | * @param mixed $resource The resource being operated on. 78 | * @param mixed $optionalArgs Multiple additional arguments which are passed on 79 | * @return \Authorization\Policy\ResultInterface|bool 80 | */ 81 | protected function performCheck( 82 | ?IdentityInterface $user, 83 | string $action, 84 | mixed $resource, 85 | mixed ...$optionalArgs, 86 | ): bool|ResultInterface { 87 | $this->authorizationChecked = true; 88 | $policy = $this->resolver->getPolicy($resource); 89 | 90 | if ($policy instanceof BeforePolicyInterface) { 91 | $result = $policy->before($user, $resource, $action); 92 | 93 | if ($result !== null) { 94 | return $result; 95 | } 96 | } 97 | 98 | $handler = $this->getCanHandler($policy, $action); 99 | $result = $handler($user, $resource, ...$optionalArgs); 100 | 101 | assert( 102 | is_bool($result) || $result instanceof ResultInterface, 103 | new Exception(sprintf( 104 | 'Authorization check method must return `%s` or `bool`.', 105 | ResultInterface::class, 106 | )), 107 | ); 108 | 109 | return $result; 110 | } 111 | 112 | /** 113 | * @inheritDoc 114 | */ 115 | public function applyScope(?IdentityInterface $user, string $action, mixed $resource, mixed ...$optionalArgs): mixed 116 | { 117 | $this->authorizationChecked = true; 118 | $policy = $this->resolver->getPolicy($resource); 119 | 120 | if ($policy instanceof BeforeScopeInterface) { 121 | $result = $policy->beforeScope($user, $resource, $action); 122 | 123 | if ($result !== null) { 124 | return $result; 125 | } 126 | } 127 | 128 | $handler = $this->getScopeHandler($policy, $action); 129 | 130 | return $handler($user, $resource, ...$optionalArgs); 131 | } 132 | 133 | /** 134 | * Returns a policy action handler. 135 | * 136 | * @param mixed $policy Policy object. 137 | * @param string $action Action name. 138 | * @return \Closure 139 | * @throws \Authorization\Policy\Exception\MissingMethodException 140 | */ 141 | protected function getCanHandler(mixed $policy, string $action): Closure 142 | { 143 | $method = 'can' . ucfirst($action); 144 | 145 | assert( 146 | method_exists($policy, $method) || method_exists($policy, '__call'), 147 | new MissingMethodException([$method, $action, get_class($policy)]), 148 | ); 149 | 150 | return [$policy, $method](...); 151 | } 152 | 153 | /** 154 | * Returns a policy scope action handler. 155 | * 156 | * @param mixed $policy Policy object. 157 | * @param string $action Action name. 158 | * @return \Closure 159 | * @throws \Authorization\Policy\Exception\MissingMethodException 160 | */ 161 | protected function getScopeHandler(mixed $policy, string $action): Closure 162 | { 163 | $method = 'scope' . ucfirst($action); 164 | 165 | assert( 166 | method_exists($policy, $method) || method_exists($policy, '__call'), 167 | new MissingMethodException([$method, $action, get_class($policy)]), 168 | ); 169 | 170 | return [$policy, $method](...); 171 | } 172 | 173 | /** 174 | * @inheritDoc 175 | */ 176 | public function authorizationChecked(): bool 177 | { 178 | return $this->authorizationChecked; 179 | } 180 | 181 | /** 182 | * @inheritDoc 183 | */ 184 | public function skipAuthorization() 185 | { 186 | $this->authorizationChecked = true; 187 | 188 | return $this; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Policy/OrmResolver.php: -------------------------------------------------------------------------------- 1 | 44 | */ 45 | protected array $overrides = []; 46 | 47 | /** 48 | * The DIC instance from the application 49 | * 50 | * @var \Cake\Core\ContainerInterface|null 51 | */ 52 | protected ?ContainerInterface $container; 53 | 54 | /** 55 | * Constructor 56 | * 57 | * @param string $appNamespace The application namespace 58 | * @param array $overrides A list of plugin name overrides. 59 | * @param \Cake\Core\ContainerInterface|null $container The DIC instance from the application 60 | */ 61 | public function __construct( 62 | string $appNamespace = 'App', 63 | array $overrides = [], 64 | ?ContainerInterface $container = null, 65 | ) { 66 | $this->appNamespace = $appNamespace; 67 | $this->overrides = $overrides; 68 | $this->container = $container; 69 | } 70 | 71 | /** 72 | * Get a policy for an ORM Table, Entity or Query. 73 | * 74 | * @param mixed $resource The resource. 75 | * @return mixed 76 | * @throws \Authorization\Policy\Exception\MissingPolicyException When a policy for the 77 | * resource has not been defined or cannot be resolved. 78 | */ 79 | public function getPolicy(mixed $resource): mixed 80 | { 81 | if ($resource instanceof EntityInterface) { 82 | return $this->getEntityPolicy($resource); 83 | } 84 | if ($resource instanceof RepositoryInterface) { 85 | return $this->getRepositoryPolicy($resource); 86 | } 87 | if ($resource instanceof QueryInterface) { 88 | $repo = $resource->getRepository(); 89 | if ($repo === null) { 90 | throw new RuntimeException('No repository set for the query.'); 91 | } 92 | 93 | return $this->getRepositoryPolicy($repo); 94 | } 95 | 96 | throw new MissingPolicyException([get_debug_type($resource)]); 97 | } 98 | 99 | /** 100 | * Get a policy for an entity 101 | * 102 | * @param \Cake\Datasource\EntityInterface $entity The entity to get a policy for 103 | * @return mixed 104 | */ 105 | protected function getEntityPolicy(EntityInterface $entity): mixed 106 | { 107 | $class = get_class($entity); 108 | $entityNamespace = '\Model\Entity\\'; 109 | $namespace = str_replace('\\', '/', substr($class, 0, (int)strpos($class, $entityNamespace))); 110 | $name = substr($class, strpos($class, $entityNamespace) + strlen($entityNamespace)); 111 | 112 | return $this->findPolicy($class, $name, $namespace); 113 | } 114 | 115 | /** 116 | * Get a policy for a table 117 | * 118 | * @param \Cake\Datasource\RepositoryInterface $table The table/repository to get a policy for. 119 | * @return mixed 120 | */ 121 | protected function getRepositoryPolicy(RepositoryInterface $table): mixed 122 | { 123 | $class = get_class($table); 124 | $tableNamespace = '\Model\Table\\'; 125 | $namespace = str_replace('\\', '/', substr($class, 0, (int)strpos($class, $tableNamespace))); 126 | $name = substr($class, strpos($class, $tableNamespace) + strlen($tableNamespace)); 127 | 128 | return $this->findPolicy($class, $name, $namespace); 129 | } 130 | 131 | /** 132 | * Locate a policy class using conventions 133 | * 134 | * @param string $class The full class name. 135 | * @param string $name The name suffix of the resource. 136 | * @param string $namespace The namespace to find the policy in. 137 | * @throws \Authorization\Policy\Exception\MissingPolicyException When a policy for the 138 | * resource has not been defined. 139 | * @return mixed 140 | */ 141 | protected function findPolicy(string $class, string $name, string $namespace): mixed 142 | { 143 | $namespace = $this->getNamespace($namespace); 144 | $policyClass = null; 145 | 146 | // plugin entities can have application overrides defined. 147 | if ($namespace !== $this->appNamespace) { 148 | $policyClass = App::className($name, 'Policy\\' . $namespace, 'Policy'); 149 | } 150 | 151 | // Check the application/plugin. 152 | if ($policyClass === null) { 153 | $policyClass = App::className($namespace . '.' . $name, 'Policy', 'Policy'); 154 | } 155 | 156 | if ($policyClass === null) { 157 | throw new MissingPolicyException([$class]); 158 | } 159 | 160 | if ($this->container && $this->container->has($policyClass)) { 161 | $policy = $this->container->get($policyClass); 162 | } else { 163 | $policy = new $policyClass(); 164 | } 165 | 166 | return $policy; 167 | } 168 | 169 | /** 170 | * Returns plugin namespace override if exists. 171 | * 172 | * @param string $namespace The namespace to find the policy in. 173 | * @return string 174 | */ 175 | protected function getNamespace(string $namespace): string 176 | { 177 | if (isset($this->overrides[$namespace])) { 178 | return $this->overrides[$namespace]; 179 | } 180 | 181 | return $namespace; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/Middleware/AuthorizationMiddleware.php: -------------------------------------------------------------------------------- 1 | null, 65 | 'identityAttribute' => 'identity', 66 | 'requireAuthorizationCheck' => true, 67 | 'unauthorizedHandler' => 'Authorization.Exception', 68 | ]; 69 | 70 | /** 71 | * Authorization service or application instance. 72 | * 73 | * @var \Authorization\AuthorizationServiceInterface|\Authorization\AuthorizationServiceProviderInterface 74 | */ 75 | protected AuthorizationServiceInterface|AuthorizationServiceProviderInterface $subject; 76 | 77 | /** 78 | * The container instance from the application 79 | * 80 | * @var \Cake\Core\ContainerInterface|null 81 | */ 82 | protected ?ContainerInterface $container = null; 83 | 84 | /** 85 | * Constructor. 86 | * 87 | * @param \Authorization\AuthorizationServiceInterface|\Authorization\AuthorizationServiceProviderInterface $subject Authorization service or provider instance. 88 | * @param array $config Config array. 89 | * @param \Cake\Core\ContainerInterface|null $container The container instance from the application 90 | * @throws \InvalidArgumentException 91 | */ 92 | public function __construct( 93 | AuthorizationServiceInterface|AuthorizationServiceProviderInterface $subject, 94 | array $config = [], 95 | ?ContainerInterface $container = null, 96 | ) { 97 | if ($this->_defaultConfig['identityDecorator'] === null) { 98 | $this->_defaultConfig['identityDecorator'] = interface_exists(AuthenIdentityInterface::class) 99 | ? Identity::class 100 | : IdentityDecorator::class; 101 | } 102 | 103 | $this->subject = $subject; 104 | $this->container = $container; 105 | $this->setConfig($config); 106 | } 107 | 108 | /** 109 | * Callable implementation for the middleware stack. 110 | * 111 | * @param \Psr\Http\Message\ServerRequestInterface $request The request. 112 | * @param \Psr\Http\Server\RequestHandlerInterface $handler The request handler. 113 | * @return \Psr\Http\Message\ResponseInterface A response. 114 | */ 115 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 116 | { 117 | $service = $this->getAuthorizationService($request); 118 | $request = $request->withAttribute('authorization', $service); 119 | 120 | if ($this->subject instanceof ContainerApplicationInterface) { 121 | $container = $this->subject->getContainer(); 122 | $container->add(AuthorizationService::class, $service); 123 | } elseif ($this->container) { 124 | $this->container->add(AuthorizationService::class, $service); 125 | } 126 | 127 | $attribute = $this->getConfig('identityAttribute'); 128 | $identity = $request->getAttribute($attribute); 129 | 130 | if ($identity !== null) { 131 | $identity = $this->buildIdentity($service, $identity); 132 | $request = $request->withAttribute($attribute, $identity); 133 | } 134 | 135 | try { 136 | $response = $handler->handle($request); 137 | 138 | if ($this->getConfig('requireAuthorizationCheck') && !$service->authorizationChecked()) { 139 | throw new AuthorizationRequiredException(['url' => $request->getRequestTarget()]); 140 | } 141 | } catch (Exception $exception) { 142 | $response = $this->handleException( 143 | $exception, 144 | $request, 145 | $this->getConfig('unauthorizedHandler'), 146 | ); 147 | } 148 | 149 | return $response; 150 | } 151 | 152 | /** 153 | * Returns AuthorizationServiceInterface instance. 154 | * 155 | * @param \Psr\Http\Message\ServerRequestInterface $request Server request. 156 | * @return \Authorization\AuthorizationServiceInterface 157 | * @throws \RuntimeException When authorization method has not been defined. 158 | */ 159 | protected function getAuthorizationService( 160 | ServerRequestInterface $request, 161 | ): AuthorizationServiceInterface { 162 | $service = $this->subject; 163 | if ($this->subject instanceof AuthorizationServiceProviderInterface) { 164 | $service = $this->subject->getAuthorizationService($request); 165 | } 166 | 167 | if (!$service instanceof AuthorizationServiceInterface) { 168 | throw new RuntimeException(sprintf( 169 | 'Invalid service returned from the provider. `%s` does not implement `%s`.', 170 | get_debug_type($service), 171 | AuthorizationServiceInterface::class, 172 | )); 173 | } 174 | 175 | return $service; 176 | } 177 | 178 | /** 179 | * Builds the identity object. 180 | * 181 | * @param \Authorization\AuthorizationServiceInterface $service Authorization service. 182 | * @param \ArrayAccess|array $identity Identity data 183 | * @return \Authorization\IdentityInterface 184 | */ 185 | protected function buildIdentity( 186 | AuthorizationServiceInterface $service, 187 | ArrayAccess|array $identity, 188 | ): IdentityInterface { 189 | $class = $this->getConfig('identityDecorator'); 190 | 191 | if (is_callable($class)) { 192 | $identity = $class($service, $identity); 193 | } else { 194 | if (!$identity instanceof IdentityInterface) { 195 | $identity = new $class($service, $identity); 196 | } 197 | } 198 | 199 | if (!$identity instanceof IdentityInterface) { 200 | throw new RuntimeException(sprintf( 201 | 'Invalid identity returned by decorator. `%s` does not implement `%s`.', 202 | get_debug_type($identity), 203 | IdentityInterface::class, 204 | )); 205 | } 206 | 207 | return $identity; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/Controller/Component/AuthorizationComponent.php: -------------------------------------------------------------------------------- 1 | 42 | */ 43 | protected array $_defaultConfig = [ 44 | 'identityAttribute' => 'identity', 45 | 'serviceAttribute' => 'authorization', 46 | 'authorizationEvent' => 'Controller.startup', 47 | 'skipAuthorization' => [], 48 | 'authorizeModel' => [], 49 | 'actionMap' => [], 50 | ]; 51 | 52 | /** 53 | * Check the policy for $resource, raising an exception on error. 54 | * 55 | * If $action is left undefined, the current controller action will 56 | * be used. 57 | * 58 | * @param mixed $resource The resource to check authorization on. 59 | * @param string|null $action The action to check authorization for. 60 | * @return void 61 | * @throws \Authorization\Exception\ForbiddenException when policy check fails. 62 | */ 63 | public function authorize(mixed $resource, ?string $action = null): void 64 | { 65 | if ($action === null) { 66 | $request = $this->getController()->getRequest(); 67 | $action = $this->getDefaultAction($request); 68 | } 69 | 70 | $result = $this->canResult($resource, $action); 71 | if ($result->getStatus()) { 72 | return; 73 | } 74 | 75 | if (is_object($resource)) { 76 | $name = get_class($resource); 77 | } elseif (is_string($resource)) { 78 | $name = $resource; 79 | } else { 80 | $name = gettype($resource); 81 | } 82 | throw new ForbiddenException($result, [$action, $name]); 83 | } 84 | 85 | /** 86 | * Check the policy for $resource, returns true if the action is allowed 87 | * 88 | * If $action is left undefined, the current controller action will 89 | * be used. 90 | * 91 | * @param mixed $resource The resource to check authorization on. 92 | * @param string|null $action The action to check authorization for. 93 | * @return bool 94 | */ 95 | public function can(mixed $resource, ?string $action = null): bool 96 | { 97 | return $this->performCheck($resource, $action); 98 | } 99 | 100 | /** 101 | * Check the policy for $resource, returns true if the action is allowed 102 | * 103 | * If $action is left undefined, the current controller action will 104 | * be used. 105 | * 106 | * @param mixed $resource The resource to check authorization on. 107 | * @param string|null $action The action to check authorization for. 108 | * @return \Authorization\Policy\ResultInterface 109 | */ 110 | public function canResult(mixed $resource, ?string $action = null): ResultInterface 111 | { 112 | return $this->performCheck($resource, $action, 'canResult'); 113 | } 114 | 115 | /** 116 | * Check the policy for $resource. 117 | * 118 | * @param mixed $resource The resource to check authorization on. 119 | * @param string|null $action The action to check authorization for. 120 | * @param string $method The method to use, either "can" or "canResult". 121 | * @return \Authorization\Policy\ResultInterface|bool 122 | */ 123 | protected function performCheck( 124 | mixed $resource, 125 | ?string $action = null, 126 | string $method = 'can', 127 | ): ResultInterface|bool { 128 | $request = $this->getController()->getRequest(); 129 | if ($action === null) { 130 | $action = $this->getDefaultAction($request); 131 | } 132 | 133 | $identity = $this->getIdentity($request); 134 | if ($identity === null) { 135 | return $this->getService($request)->{$method}(null, $action, $resource); 136 | } 137 | 138 | return $identity->{$method}($action, $resource); 139 | } 140 | 141 | /** 142 | * Applies a scope for $resource. 143 | * 144 | * If $action is left undefined, the current controller action will 145 | * be used. 146 | * 147 | * @param mixed $resource The resource to apply a scope to. 148 | * @param string|null $action The action to apply a scope for. 149 | * @param mixed $optionalArgs Multiple additional arguments which are passed to the scope 150 | * @return mixed 151 | */ 152 | public function applyScope(mixed $resource, ?string $action = null, mixed ...$optionalArgs): mixed 153 | { 154 | $request = $this->getController()->getRequest(); 155 | if ($action === null) { 156 | $action = $this->getDefaultAction($request); 157 | } 158 | $identity = $this->getIdentity($request); 159 | if ($identity === null) { 160 | return $this->getService($request)->applyScope(null, $action, $resource); 161 | } 162 | 163 | return $identity->applyScope($action, $resource, ...$optionalArgs); 164 | } 165 | 166 | /** 167 | * Skips the authorization check. 168 | * 169 | * @return $this 170 | */ 171 | public function skipAuthorization() 172 | { 173 | $request = $this->getController()->getRequest(); 174 | $service = $this->getService($request); 175 | 176 | $service->skipAuthorization(); 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * Allows to map controller action to another authorization policy action. 183 | * 184 | * For instance you may want to authorize `add` action with `create` authorization policy. 185 | * 186 | * @param string $controllerAction Controller action. 187 | * @param string $policyAction Policy action. 188 | * @return $this 189 | */ 190 | public function mapAction(string $controllerAction, string $policyAction) 191 | { 192 | $this->_config['actionMap'][$controllerAction] = $policyAction; 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * Allows to map controller actions to policy actions. 199 | * 200 | * @param array $actions Map of controller action to policy action. 201 | * @param bool $overwrite Set to true to override configuration. False will merge with current configuration. 202 | * @return $this 203 | */ 204 | public function mapActions(array $actions, bool $overwrite = false) 205 | { 206 | $this->setConfig('actionMap', $actions, !$overwrite); 207 | 208 | return $this; 209 | } 210 | 211 | /** 212 | * Adds an action to automatic model authorization checks. 213 | * 214 | * @param string ...$actions Controller action to authorize against table policy. 215 | * @return $this 216 | */ 217 | public function authorizeModel(string ...$actions) 218 | { 219 | $this->_config['authorizeModel'] = array_merge($this->_config['authorizeModel'], $actions); 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * Get the authorization service from a request. 226 | * 227 | * @param \Psr\Http\Message\ServerRequestInterface $request The request 228 | * @return \Authorization\AuthorizationServiceInterface 229 | * @throws \InvalidArgumentException When invalid authorization service encountered. 230 | */ 231 | protected function getService(ServerRequestInterface $request): AuthorizationServiceInterface 232 | { 233 | $serviceAttribute = $this->getConfig('serviceAttribute'); 234 | $service = $request->getAttribute($serviceAttribute); 235 | if (!$service instanceof AuthorizationServiceInterface) { 236 | $type = is_object($service) ? get_class($service) : gettype($service); 237 | throw new InvalidArgumentException(sprintf( 238 | 'Expected that `%s` would be an instance of %s, but got %s', 239 | $serviceAttribute, 240 | AuthorizationServiceInterface::class, 241 | $type, 242 | )); 243 | } 244 | 245 | return $service; 246 | } 247 | 248 | /** 249 | * Get the identity from a request. 250 | * 251 | * @param \Psr\Http\Message\ServerRequestInterface $request The request 252 | * @return \Authorization\IdentityInterface|null 253 | * @throws \Authorization\Exception\MissingIdentityException When identity is not present in a request. 254 | * @throws \InvalidArgumentException When invalid identity encountered. 255 | */ 256 | protected function getIdentity(ServerRequestInterface $request): ?IdentityInterface 257 | { 258 | $identityAttribute = $this->getConfig('identityAttribute'); 259 | $identity = $request->getAttribute($identityAttribute); 260 | if ($identity === null) { 261 | return $identity; 262 | } 263 | if (!$identity instanceof IdentityInterface) { 264 | $type = is_object($identity) ? get_class($identity) : gettype($identity); 265 | throw new InvalidArgumentException(sprintf( 266 | 'Expected that `%s` would be an instance of %s, but got %s', 267 | $identityAttribute, 268 | IdentityInterface::class, 269 | $type, 270 | )); 271 | } 272 | 273 | return $identity; 274 | } 275 | 276 | /** 277 | * Action authorization handler. 278 | * 279 | * Checks identity and model authorization. 280 | * 281 | * @return void 282 | */ 283 | public function authorizeAction(): void 284 | { 285 | $request = $this->getController()->getRequest(); 286 | $action = $request->getParam('action'); 287 | 288 | $skipAuthorization = $this->checkAction($action, 'skipAuthorization'); 289 | if ($skipAuthorization) { 290 | $this->skipAuthorization(); 291 | 292 | return; 293 | } 294 | 295 | $authorizeModel = $this->checkAction($action, 'authorizeModel'); 296 | if ($authorizeModel) { 297 | $this->authorize($this->getController()->fetchTable()); 298 | } 299 | } 300 | 301 | /** 302 | * Checks whether an action should be authorized according to the config key provided. 303 | * 304 | * @param string $action Action name. 305 | * @param string $configKey Configuration key with actions. 306 | * @return bool 307 | */ 308 | protected function checkAction(string $action, string $configKey): bool 309 | { 310 | $actions = (array)$this->getConfig($configKey); 311 | 312 | return in_array($action, $actions, true); 313 | } 314 | 315 | /** 316 | * Returns authorization action name for a controller action resolved from the request. 317 | * 318 | * @param \Cake\Http\ServerRequest $request Server request. 319 | * @return string 320 | * @throws \UnexpectedValueException When invalid action type encountered. 321 | */ 322 | protected function getDefaultAction(ServerRequest $request): string 323 | { 324 | $action = $request->getParam('action'); 325 | $name = $this->getConfig('actionMap.' . $action); 326 | 327 | if ($name === null) { 328 | return $action; 329 | } 330 | if (!is_string($name)) { 331 | $type = is_object($name) ? get_class($name) : gettype($name); 332 | $message = sprintf('Invalid action type for `%s`. Expected `string` or `null`, got `%s`.', $action, $type); 333 | throw new UnexpectedValueException($message); 334 | } 335 | 336 | return $name; 337 | } 338 | 339 | /** 340 | * Returns model authorization handler if model authorization is enabled. 341 | * 342 | * @return array 343 | */ 344 | public function implementedEvents(): array 345 | { 346 | return [ 347 | (string)$this->getConfig('authorizationEvent') => 'authorizeAction', 348 | ]; 349 | } 350 | } 351 | --------------------------------------------------------------------------------