diff --git a/docs/AuthorizationPlugin.md b/docs/AuthorizationPlugin.md index 54f63bf4..5f582f0a 100644 --- a/docs/AuthorizationPlugin.md +++ b/docs/AuthorizationPlugin.md @@ -12,4 +12,24 @@ $this->loadComponent('TinyAuth.Authorization', [ For all the rest just follow the plugin's documentation. +For your resolver you need to use this map inside `Application::getAuthorizationService()`: +```php +use TinyAuth\Policy\RequestPolicy; + +/** + * @param \Psr\Http\Message\ServerRequestInterface $request + * @param \Psr\Http\Message\ResponseInterface $response + * + * @return \Authorization\AuthorizationServiceInterface + */ +public function getAuthorizationService(ServerRequestInterface $request, ResponseInterface $response) { + $map = [ + ServerRequest::class => new RequestPolicy(), + ]; + $resolver = new MapResolver($map); + + return new AuthorizationService($resolver); +} +``` + Then you use the [Authorization documention](Authorization.md) to set up roles and fill your INI config file. diff --git a/src/Auth/AclTrait.php b/src/Auth/AclTrait.php index 9dff0711..fa209bfc 100644 --- a/src/Auth/AclTrait.php +++ b/src/Auth/AclTrait.php @@ -7,6 +7,7 @@ use Cake\ORM\TableRegistry; use Cake\Utility\Hash; use InvalidArgumentException; +use RuntimeException; use TinyAuth\Auth\AclAdapter\AclAdapterInterface; use TinyAuth\Utility\Cache; use TinyAuth\Utility\Utility; @@ -391,7 +392,7 @@ protected function _constructIniKey($params) { * Will look for a roles array in * Configure first, tries database roles table next. * - * @return array List with all available roles + * @return int[] List with all available roles * @throws \Cake\Core\Exception\Exception */ protected function _getAvailableRoles() { @@ -410,13 +411,23 @@ protected function _getAvailableRoles() { $key = $this->getConfig('superAdmin') ?: 'superadmin'; $roles[$key] = $this->getConfig('superAdminRole'); } + + if (!$roles) { + throw new Exception('Invalid roles config: No roles found in config.'); + } + return $roles; } - $rolesTable = TableRegistry::get($rolesTableKey); - $result = $rolesTable->find()->formatResults(function (ResultSetInterface $results) { - return $results->combine($this->getConfig('aliasColumn'), 'id'); - }); + try { + $rolesTable = TableRegistry::get($rolesTableKey); + $result = $rolesTable->find()->formatResults(function (ResultSetInterface $results) { + return $results->combine($this->getConfig('aliasColumn'), 'id'); + }); + } catch (RuntimeException $e) { + throw new Exception('Invalid roles config: DB table `' . $rolesTableKey . '` cannot be found/accessed (`' . $e->getMessage() . '`).', null, $e); + } + $roles = $result->toArray(); if ($this->getConfig('superAdminRole')) { @@ -448,7 +459,12 @@ protected function _getAvailableRoles() { protected function _getUserRoles($user) { // Single-role from session if (!$this->getConfig('multiRole')) { - if (!array_key_exists($this->getConfig('roleColumn'), $user)) { + $roleColumn = $this->getConfig('roleColumn'); + if (!$roleColumn) { + throw new Exception('Invalid TinyAuth config, `roleColumn` config missing.'); + } + + if (!array_key_exists($roleColumn, $user)) { throw new Exception(sprintf('Missing TinyAuth role id field (%s) in user session', 'Auth.User.' . $this->getConfig('roleColumn'))); } if (!isset($user[$this->getConfig('roleColumn')])) { @@ -529,10 +545,12 @@ protected function _getRolesFromDb($pivotTableName, $id) { } /** - * @param array $roles - * @return array + * Returns current roles as [alias => id] pairs. + * + * @param int[] $roles + * @return int[] */ - protected function _mapped($roles) { + protected function _mapped(array $roles) { $availableRoles = $this->_getAvailableRoles(); $array = []; diff --git a/src/Controller/Component/AuthComponent.php b/src/Controller/Component/AuthComponent.php index a13d980d..3268e648 100644 --- a/src/Controller/Component/AuthComponent.php +++ b/src/Controller/Component/AuthComponent.php @@ -4,7 +4,6 @@ use Cake\Controller\ComponentRegistry; use Cake\Controller\Component\AuthComponent as CakeAuthComponent; -use Cake\Event\Event; use RuntimeException; use TinyAuth\Auth\AclTrait; use TinyAuth\Auth\AllowTrait; @@ -60,18 +59,6 @@ public function implementedEvents() { ] + parent::implementedEvents(); } - /** - * @param \Cake\Event\Event $event - * @return \Cake\Http\Response|null - */ - public function beforeRender(Event $event) { - /** @var \Cake\Controller\Controller $controller */ - $controller = $event->getSubject(); - - $authUser = (array)$this->user(); - $controller->set('_authUser', $authUser); - } - /** * @param array $params * @return void diff --git a/src/Controller/Component/AuthorizationComponent.php b/src/Controller/Component/AuthorizationComponent.php index 664cea9f..2969b590 100644 --- a/src/Controller/Component/AuthorizationComponent.php +++ b/src/Controller/Component/AuthorizationComponent.php @@ -4,7 +4,6 @@ use Authorization\Controller\Component\AuthorizationComponent as CakeAuthorizationComponent; use Cake\Controller\ComponentRegistry; -use Cake\Event\Event; use RuntimeException; use TinyAuth\Auth\AclTrait; use TinyAuth\Utility\Config; @@ -36,25 +35,26 @@ public function __construct(ComponentRegistry $registry, array $config = []) { } /** - * Callback for Controller.startup event. + * Action authorization handler. * - * @param \Cake\Event\Event $event Event instance. - * @return \Cake\Http\Response|null + * Checks identity and model authorization. + * + * @return void */ - public function startup(Event $event) { - $this->_prepareAuthorization($event); + public function authorizeAction() { + $request = $this->getController()->getRequest(); - return null; - } + $action = $request->getParam('action'); + $skipAuthorization = $this->checkAction($action, 'skipAuthorization'); + if ($skipAuthorization) { + $this->skipAuthorization(); - /** - * @param \Cake\Event\Event $event - * @return void - */ - protected function _prepareAuthorization(Event $event) { - //TODO 'skipAuthorization' from allow ini config - //TODO 'authorizeModel' and maybe 'actionMap' - //TODO rest + return; + } + + $this->authorize($request, 'access'); + + parent::authorizeAction(); } } diff --git a/src/Policy/RequestPolicy.php b/src/Policy/RequestPolicy.php new file mode 100644 index 00000000..58bc9f3b --- /dev/null +++ b/src/Policy/RequestPolicy.php @@ -0,0 +1,47 @@ +setConfig($config); + } + + /** + * Method to check if the request can be accessed + * + * @param \Authorization\IdentityInterface|null $identity Identity + * @param \Cake\Http\ServerRequest $request Request + * @return bool + */ + public function canAccess($identity, ServerRequest $request) { + $params = $request->getAttribute('params'); + $user = $identity->getOriginalData(); + + return $this->_checkUser((array)$user, $params); + } + +} diff --git a/src/Utility/TinyAuth.php b/src/Utility/TinyAuth.php index 7853c4bb..5be95fa7 100644 --- a/src/Utility/TinyAuth.php +++ b/src/Utility/TinyAuth.php @@ -25,7 +25,7 @@ public function __construct(array $config = []) { } /** - * @return string[] + * @return int[] */ public function getAvailableRoles() { $roles = $this->_getAvailableRoles(); diff --git a/tests/TestCase/Controller/Component/AuthorizationComponentTest.php b/tests/TestCase/Controller/Component/AuthorizationComponentTest.php index ad9a6eb0..58d1422d 100644 --- a/tests/TestCase/Controller/Component/AuthorizationComponentTest.php +++ b/tests/TestCase/Controller/Component/AuthorizationComponentTest.php @@ -2,10 +2,11 @@ namespace TinyAuth\Test\TestCase\Controller\Component; +use Authorization\AuthorizationService; use Cake\Controller\ComponentRegistry; use Cake\Controller\Controller; +use Cake\Core\Configure; use Cake\Core\Plugin; -use Cake\Event\Event; use Cake\Http\ServerRequest; use Cake\TestSuite\TestCase; use TinyAuth\Controller\Component\AuthorizationComponent; @@ -26,8 +27,14 @@ class AuthorizationComponentTest extends TestCase { * @return void */ public function setUp() { + Configure::write('Roles', [ + 'user' => ROLE_USER, + 'moderator' => ROLE_MODERATOR, + 'admin' => ROLE_ADMIN + ]); + $this->componentConfig = [ - 'allowFilePath' => Plugin::path('TinyAuth') . 'tests' . DS . 'test_files' . DS, + 'aclFilePath' => Plugin::path('TinyAuth') . 'tests' . DS . 'test_files' . DS, 'autoClearCache' => true, ]; } @@ -43,17 +50,24 @@ public function testValid() { '_ext' => null, 'pass' => [1] ]]); + $authorization = $this->getMockBuilder(AuthorizationService::class)->disableOriginalConstructor()->getMock(); + $authorization->expects($this->once()) + ->method('can') + ->willReturn(true); + + $request = $request->withAttribute('authorization', $authorization); $controller = $this->getControllerMock($request); $registry = new ComponentRegistry($controller); $this->component = new AuthorizationComponent($registry, $this->componentConfig); - $config = []; - $this->component->initialize($config); + $this->component->authorizeAction(); + + $request = $this->component->getController()->getRequest(); + /** @var \Authorization\AuthorizationService $service */ + $service = $request->getAttribute('authorization'); - $event = new Event('Controller.startup', $controller); - $response = $this->component->startup($event); - $this->assertNull($response); + $this->assertInstanceOf(AuthorizationService::class, $service); } /** diff --git a/tests/TestCase/Policy/RequestPolicyTest.php b/tests/TestCase/Policy/RequestPolicyTest.php new file mode 100644 index 00000000..fe965952 --- /dev/null +++ b/tests/TestCase/Policy/RequestPolicyTest.php @@ -0,0 +1,90 @@ + ROLE_USER, + 'moderator' => ROLE_MODERATOR, + 'admin' => ROLE_ADMIN + ]); + + $this->config = [ + 'aclFilePath' => Plugin::path('TinyAuth') . 'tests' . DS . 'test_files' . DS, + 'autoClearCache' => true, + ]; + + $this->policy = new RequestPolicy($this->config); + } + + /** + * @return void + */ + public function testPolicyCanAccessSuccess() { + $request = new ServerRequest(['params' => [ + 'controller' => 'Tags', + 'action' => 'delete', + 'plugin' => null, + ]]); + + $identityArray = [ + 'id' => 1, + 'role_id' => ROLE_ADMIN, + ]; + $service = $this->getService(); + $identity = new IdentityDecorator($service, $identityArray); + $result = $this->policy->canAccess($identity, $request); + $this->assertTrue($result); + } + + /** + * @return void + */ + public function testPolicyCanAccessFail() { + $request = new ServerRequest(['params' => [ + 'controller' => 'Tags', + 'action' => 'edit', + 'plugin' => null, + ]]); + + $identityArray = [ + 'id' => 1, + 'role_id' => ROLE_ADMIN, + ]; + $service = $this->getService(); + $identity = new IdentityDecorator($service, $identityArray); + $result = $this->policy->canAccess($identity, $request); + $this->assertFalse($result); + } + + /** + * @return \Authorization\AuthorizationService|\PHPUnit\Framework\MockObject\MockObject + */ + protected function getService() { + return $this->getMockBuilder(AuthorizationService::class)->disableOriginalConstructor()->getMock(); + } + +} diff --git a/tests/TestCase/Utility/TinyAuthTest.php b/tests/TestCase/Utility/TinyAuthTest.php new file mode 100644 index 00000000..5ab31986 --- /dev/null +++ b/tests/TestCase/Utility/TinyAuthTest.php @@ -0,0 +1,34 @@ + 'DatabaseRoles', + ]; + + $result = (new TinyAuth($config))->getAvailableRoles(); + $expected = [ + 'user' => 11, + 'moderator' => 12, + 'admin' => 13 + ]; + $this->assertEquals($expected, $result); + } + +}