└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Apigility and ZfcRbac integration 2 | 3 | 4 | You have created API application with Apigility, integrated OAuth2 authentication and now you want to add roles? Tough luck, you wouldn't find any tutorial how to do it. Till now. 5 | 6 | ## Requirements 7 | 8 | - Working Apigility API 9 | - Doctrine ORM 10 | - Working authentication process using Doctrine ORM 11 | 12 | ## How it works? 13 | 14 | We use zf-mvc-auth to handle OAuth2 authentication. We inject our listener to post authentication event, so 15 | after successful authentication we query DB and get user's role instead of ID. 16 | 17 | In ZfcRbac configuration we point to our IdentityProvider that will translate zf-mvc-auth Identity into 18 | ZfcRbac Identity. 19 | 20 | We add alias `ZF\MvcAuth\Authorization\AuthorizationInterface` to our Authorization, so it's method isAuthorized 21 | is called instead of Acl. 22 | 23 | ## Setup 24 | 25 | This setup assumes that your module is called "YourApp". Please change accordingly. 26 | 27 | Install ZfcRbac module. 28 | This downloads module and copies it into /vendor/zf-commons/zfc-rbac/ 29 | 30 | ```sh 31 | $ php composer.phar require zf-commons/zfc-rbac:~2.4 32 | ``` 33 | 34 | Add module to /config/application.config.php. 35 | This enables module to be used by ZF2. 36 | 37 | ```php 38 | return array( 39 | 'modules' => array( 40 | // other modules ie. Doctrine 41 | 'ZfcRbac', 42 | // other modules ie. Application, ZF\\Apigility 43 | ) 44 | ); 45 | ``` 46 | 47 | Copy /vendor/zf-commons/zfc-rbac/config/zfc_rbac.global.php.dist to /config/autoload/zfc_rbac.global.php. 48 | This will be the base of configuration you will use in next step. 49 | 50 | Set following values in /config/autoload/zfc_rbac.global.php. 51 | This will enable authorization only in places specified by code. If you want to block whole controller, read about guards. 52 | 53 | ```php 54 | return array( 55 | 'zfc_rbac' => array( 56 | 'identity_provider' => 'YourApp\\Rbac\\IdentityProvider', 57 | 'guest_role' => 'guest', 58 | 'guards' => array(), 59 | 'protection_policy' => \ZfcRbac\Guard\GuardInterface::POLICY_ALLOW, 60 | ) 61 | ); 62 | ``` 63 | 64 | Set role tree in /config/autoload/zfc_rbac.global.php (same file, update it). 65 | This defines that your application have three roles: admin, user and guest. 66 | User have permissions "canDoFoo", "canDoBar". 67 | Admin have all user's permission ("canDoFoo", "canDoBar") and their own "canDoBaz". 68 | 69 | ```php 70 | return array( 71 | 'zfc_rbac' => array( 72 | // our previous settings are here 73 | 'role_provider' => array( 74 | 'ZfcRbac\Role\InMemoryRoleProvider' => array( 75 | 'admin' => array( 76 | 'children' => array('user'), 77 | 'permissions' => array( 78 | 'canDoBaz', 79 | ), 80 | ), 81 | 'user' => array( 82 | 'children' => array('guest'), 83 | 'permissions' => array( 84 | 'canDoFoo', 85 | 'canDoBar', 86 | ), 87 | ), 88 | 'guest' => array(), 89 | ), 90 | ), 91 | ) 92 | ); 93 | ``` 94 | 95 | Set REST guard in /config/autoload/zfc_rbac.global.php (same file, update it). 96 | It is similar to zf-mvc-auth/authorization config option, instead of boolean options (true: require authorization, 97 | false: allow guest) it uses boolean+array (true: always allow, false: never allow, array: allow only those with selected 98 | permission). 99 | 100 | ```php 101 | 'rest_guard' => [ 102 | 'YourApp\\V1\\Rest\\Foo\\Controller' => [ 103 | 'entity' => [ 104 | 'GET' => true, // everyone can use GET /foo/:id 105 | 'POST' => false, // nobody can use POST /foo/:id 106 | 'PATCH' => ['canDoFoo'], // only admin or user can use PATCH /foo/:id 107 | 'PUT' => ['canDoFoo', 'canDoBar'], // only roles that have BOTH permissions (admin/user) can use PUT /foo/:id 108 | 'DELETE' => ['canDoFoo'], 109 | ], 110 | 'collection' => [ 111 | 'GET' => true, // everyone can use GET /foo 112 | 'POST' => ['canDoFoo'], // only admin or user can use POST /foo 113 | 'PATCH' => false, // nobody can use PATCH /foo 114 | 'PUT' => false, 115 | 'DELETE' => ['canDoBaz'], // only admin can use DELETE /foo 116 | ], 117 | ], 118 | ], 119 | ``` 120 | 121 | Remove 'zf-mvc-auth/authorization' branch from /module/YourApp/config/module.config.php - it's no longer used. 122 | 123 | 124 | In /module/YourApp/config/module.config.php add following: 125 | 126 | ```php 127 | return array( 128 | 'service_manager' => array( 129 | 'aliases' => array( 130 | 'ZF\MvcAuth\Authorization\AuthorizationInterface' => 'YourApp\\Rbac\\Authorization', 131 | ), 132 | 'factories' => array( 133 | 'YourApp\\Rbac\\IdentityProvider' => 'YourApp\\Rbac\\IdentityProviderFactory', 134 | 'YourApp\\Rbac\\AuthenticationListener' => 'YourApp\\Rbac\\AuthenticationListenerFactory', 135 | 'YourApp\\Rbac\\Authorization' => 'YourApp\\Rbac\\AuthorizationFactory', 136 | ), 137 | ), 138 | ); 139 | ``` 140 | 141 | Create /module/YourApp/Rbac/IdentityProviderFactory.php. 142 | This will create IdentityProvider service used by ZfcRbac and include OAuth2 identity resolved by token. 143 | 144 | ```php 145 | namespace YourApp\Rbac; 146 | 147 | use \Zend\ServiceManager\ServiceManager; 148 | 149 | class IdentityProviderFactory 150 | { 151 | public function __invoke(ServiceManager $services) 152 | { 153 | /** @var \Zend\Authentication\AuthenticationService $authenticationProvider */ 154 | $authenticationProvider = $services->get('authentication'); 155 | 156 | $identityProvider = new IdentityProvider(); 157 | $identityProvider->setAuthenticationProvider($authenticationProvider); 158 | return $identityProvider; 159 | } 160 | } 161 | ``` 162 | 163 | Create /module/YourApp/Rbac/IdentityProvider.php. 164 | GetIdentity function will be executed by ZfcRbac. Since it wants something different than ZF\MvcAuth\Identity, 165 | we have to translate. We take existing Identity, get userId (weirdly called getRoleId), then we check OAuthUserEntity 166 | where we store users and their roles. Then we return YourApp\Rbac\Identity with a role. 167 | 168 | ```php 169 | namespace YourApp\Rbac; 170 | 171 | use ZfcRbac\Identity\IdentityProviderInterface; 172 | use Zend\Authentication\AuthenticationService; 173 | 174 | /** 175 | * Class IdentityProvider provides Identity object required by RBAC. 176 | * We return custom Identity because we connect OAuth2 authentication (returning userId) and RBAC authorization (requiring roles) 177 | * 178 | * @package YourApp\Rbac 179 | */ 180 | class IdentityProvider implements IdentityProviderInterface 181 | { 182 | /** @var Identity $rbacIdentity */ 183 | private $rbacIdentity = null; 184 | 185 | /* @var \Zend\Authentication\AuthenticationService $authenticationProvider */ 186 | private $authenticationProvider; 187 | 188 | public function setAuthenticationProvider(AuthenticationService $authenticationProvider) 189 | { 190 | $this->authenticationProvider = $authenticationProvider; 191 | return $this; 192 | } 193 | 194 | /** 195 | * Checks if user is authenticated. If yes, checks db for user's role and returns Identity. 196 | * 197 | * @return Identity 198 | */ 199 | public function getIdentity() 200 | { 201 | if ($this->rbacIdentity === null) 202 | { 203 | $this->rbacIdentity = new Identity(); 204 | 205 | $mvcIdentity = $this->authenticationProvider->getIdentity(); 206 | $role = $mvcIdentity->getRoleId(); 207 | $this->rbacIdentity 208 | ->setRoles($role); 209 | } 210 | 211 | return $this->rbacIdentity; 212 | } 213 | } 214 | 215 | ``` 216 | 217 | Create /module/YourApp/Rbac/Identity.php. 218 | This creates Identity class used by ZfcRbac. 219 | 220 | ```php 221 | namespace YourApp\Rbac; 222 | 223 | use ZfcRbac\Identity\IdentityInterface; 224 | 225 | class Identity implements IdentityInterface 226 | { 227 | private $roles = array(); 228 | 229 | public function setRoles($roles) 230 | { 231 | if (!is_array($roles)) { 232 | $roles = array($roles); 233 | } 234 | $this->roles = $roles; 235 | return $this; 236 | } 237 | 238 | /** 239 | * Get the list of roles of this identity 240 | * 241 | * @return string[]|\Rbac\Role\RoleInterface[] 242 | */ 243 | public function getRoles() 244 | { 245 | return $this->roles; 246 | } 247 | } 248 | ``` 249 | 250 | .Update your OAuth2 users table (oauth_users) and entity (YourApp\OAuth\OAuthUserEntity). 251 | Add `role` VARCHAR(20) field to oauth_users table. 252 | Add getRole function to OAuthUserEntity (or whatever you called it). 253 | We also store role/permission constants here. 254 | 255 | ```php 256 | namespace YourApp\OAuth; 257 | 258 | use Doctrine\ORM\Mapping as ORM; 259 | use Zend\Crypt\Password\Bcrypt; 260 | 261 | /** 262 | * Class OAuthUserEntity 263 | * 264 | * @package YourApp\OAuth 265 | * @ORM\Entity() 266 | * @ORM\Table(name="oauth_users") 267 | */ 268 | class OAuthUserEntity 269 | { 270 | // role tree is in /config/autoload/zfc_rbac.global.php 271 | const ROLE_ADMIN = 'admin'; 272 | const ROLE_USER = 'user'; 273 | const ROLE_GUEST = 'guest'; 274 | 275 | const PERMISSION_CAN_DO_FOO = 'canDoFoo'; 276 | const PERMISSION_CAN_DO_BAR = 'canDoBar'; 277 | const PERMISSION_CAN_DO_BAZ = 'canDoBaz'; 278 | 279 | /** 280 | * @ORM\Id 281 | * @ORM\GeneratedValue(strategy="AUTO") 282 | * @ORM\Column(type="integer") 283 | * @var int 284 | */ 285 | protected $user_id; 286 | 287 | /** 288 | * @ORM\Column(type="string",length=255) 289 | * @var string 290 | */ 291 | protected $username; 292 | 293 | /** 294 | * @ORM\Column(type="string",length=255) 295 | * @var string 296 | */ 297 | protected $password; 298 | 299 | /** 300 | * @ORM\Column(type="string",length=20) 301 | * @var string 302 | */ 303 | protected $role; 304 | 305 | public function getUserId() 306 | { 307 | return $this->user_id; 308 | } 309 | 310 | public function setUsername($username) 311 | { 312 | $this->username = $username; 313 | return $this; 314 | } 315 | 316 | public function setPassword($password) 317 | { 318 | $this->password = (new Bcrypt())->create($password); 319 | return $this; 320 | } 321 | 322 | public function setRole($role) 323 | { 324 | $this->role = $role; 325 | return $this; 326 | } 327 | 328 | public function getRole() 329 | { 330 | return $this->role; 331 | } 332 | } 333 | ``` 334 | 335 | Create /module/YourApp/Rbac/AuthenticationListenerFactory.php. 336 | This will inject Doctrine's entity manager in our listener. 337 | 338 | ```php 339 | namespace YourApp\Rbac; 340 | 341 | use \Zend\ServiceManager\ServiceManager; 342 | 343 | class AuthenticationListenerFactory 344 | { 345 | public function __invoke(ServiceManager $services) 346 | { 347 | /** @var \Doctrine\ORM\EntityManager $entityManager */ 348 | $entityManager = $services->get('Doctrine\ORM\EntityManager'); 349 | $authenticationListener = new AuthenticationListener(); 350 | $authenticationListener->setEntityManager($entityManager); 351 | return $authenticationListener; 352 | } 353 | } 354 | ``` 355 | 356 | Create /module/YourApp/Rbac/AuthenticationListener.php. 357 | This will overwrite user's ID with name of their role. 358 | 359 | ```php 360 | namespace YourApp\Rbac; 361 | 362 | use ZF\MvcAuth\MvcAuthEvent; 363 | use ZF\MvcAuth\Identity\AuthenticatedIdentity; 364 | use Doctrine\ORM\EntityManager; 365 | use YourApp\OAuth\OAuthUserEntity; 366 | 367 | class AuthenticationListener 368 | { 369 | /** @var EntityManager */ 370 | private $entityManager; 371 | 372 | public function setEntityManager(EntityManager $entityManager) 373 | { 374 | $this->entityManager = $entityManager; 375 | } 376 | 377 | public function __invoke(MvcAuthEvent $mvcAuthEvent) 378 | { 379 | $identity = $mvcAuthEvent->getIdentity(); 380 | if ($identity instanceof AuthenticatedIdentity) 381 | { 382 | $userId = $identity->getRoleId(); 383 | /** @var OAuthUserEntity $oauthUserEntity */ 384 | $oauthUserEntity = $this->entityManager->find('YourApp\OAuth\OAuthUserEntity', $userId); 385 | 386 | $identity->setName($oauthUserEntity->getRole()); 387 | } 388 | return $identity; 389 | 390 | } 391 | } 392 | 393 | ``` 394 | 395 | Add post authentication event in bootstrap in /module/YourApp/Module.php. 396 | 397 | ```php 398 | class Module implements ApigilityProviderInterface 399 | { 400 | public function onBootstrap(EventInterface $e) 401 | { 402 | /** @var Application $application */ 403 | $application = $e->getParam('application'); 404 | $eventManager = $application->getEventManager(); 405 | $moduleRouteListener = new ModuleRouteListener(); 406 | $moduleRouteListener->attach($eventManager); 407 | $eventManager->attach(MvcAuthEvent::EVENT_AUTHENTICATION_POST, $sm->get('YourApp\\Rbac\\AuthenticationListener'), 100); 408 | } 409 | } 410 | ``` 411 | 412 | Create /modules/YourApp/Rbac/AuthorizationFactory.php 413 | This injects ZfcRbac into our authorization and reads it's config. 414 | 415 | ```php 416 | namespace YourApp\Rbac; 417 | 418 | use \Zend\ServiceManager\ServiceManager; 419 | 420 | class AuthorizationFactory 421 | { 422 | public function __invoke(ServiceManager $services) 423 | { 424 | /** @var \ZfcRbac\Service\AuthorizationService $authorizationService */ 425 | $authorizationService = $services->get('ZfcRbac\Service\AuthorizationService'); 426 | 427 | $config = $services->get('config'); 428 | $rbacConfig = $config['zfc_rbac']; 429 | $authorization = new Authorization(); 430 | $authorization->setConfig($rbacConfig); 431 | $authorization->setAuthorizationService($authorizationService); 432 | return $authorization; 433 | } 434 | } 435 | 436 | ``` 437 | 438 | Create /modules/YourApp/Rbac/Authorization.php 439 | This enables REST guards. 440 | 441 | ```php 442 | namespace YourApp\Rbac; 443 | 444 | use ZF\MvcAuth\Authorization\AuthorizationInterface; 445 | use ZF\MvcAuth\Identity\IdentityInterface; 446 | use ZfcRbac\Service\AuthorizationService; 447 | 448 | class Authorization implements AuthorizationInterface 449 | { 450 | /** @var AuthorizationService */ 451 | private $authorizationService; 452 | private $config = []; 453 | 454 | public function setAuthorizationService(AuthorizationService $authorizationService) 455 | { 456 | $this->authorizationService = $authorizationService; 457 | } 458 | 459 | public function setConfig(array $config) 460 | { 461 | $this->config = $config; 462 | } 463 | 464 | 465 | 466 | /** 467 | * Whether or not the given identity has the given privilege on the given resource. 468 | * 469 | * @param IdentityInterface $identity 470 | * @param mixed $resource 471 | * @param mixed $privilege 472 | * @return bool 473 | */ 474 | public function isAuthorized(IdentityInterface $identity, $resource, $privilege) 475 | { 476 | $restGuard = $this->config['rest_guard']; 477 | list($controller, $group) = explode('::', $resource); 478 | if (isset($restGuard[$controller][$group][$privilege])) { 479 | $result = $restGuard[$controller][$group][$privilege]; 480 | if (is_array($result)) { 481 | $and = true; 482 | foreach ($result as $r) { 483 | $and = $and && $this->authorizationService->isGranted($r); 484 | } 485 | $result = $and; 486 | } 487 | return $result; 488 | } 489 | 490 | return true; 491 | } 492 | 493 | } 494 | ``` 495 | 496 | # If you want to check permissions in resource... 497 | 498 | 499 | Add service in resource factory ie. /modules/YourApp/V1/Rest/Foo/FooResourceFactory.php. 500 | 501 | ```php 502 | namespace YourApp\V1\Rest\Foo; 503 | 504 | class FooResourceFactory 505 | { 506 | /** 507 | * @param \Zend\ServiceManager\ServiceManager $services 508 | * 509 | * @return PluginResource 510 | */ 511 | public function __invoke($services) 512 | { 513 | /** @var \Doctrine\ORM\EntityManagerInterface $entityManager */ 514 | $entityManager = $services->get('Doctrine\ORM\EntityManager'); 515 | /** @var \ZfcRbac\Service\AuthorizationService $authorizationService */ 516 | $authorizationService = $services->get('ZfcRbac\Service\AuthorizationService'); 517 | 518 | $fooResource = new FooResource(); 519 | $fooResource->setEntityManager($entityManager); 520 | $fooResource->setAuthorizationService($authorizationService); 521 | 522 | return $fooResource; 523 | } 524 | } 525 | ``` 526 | 527 | Use authorization service in resource ie. /modules/YourApp/V1/Rest/Foo/FooResource.php. 528 | 529 | ```php 530 | namespace YourApp\V1\Rest\Foo; 531 | 532 | use ZF\ApiProblem\ApiProblem; 533 | use ZfcRbac\Service\AuthorizationService; 534 | 535 | class FooResource extends AbstractResourceListener 536 | { 537 | /** @var AuthorizationService */ 538 | protected $authorizationService; 539 | 540 | public function setAuthorizationService(AuthorizationService $authorizationService) 541 | { 542 | $this->authorizationService = $authorizationService; 543 | return $this; 544 | } 545 | 546 | public function create() 547 | { 548 | $authResult = $this->authorizationService->isGranted(OAuthUserEntity::PERMISSION_CAN_DO_FOO); 549 | if (!$authResult) { 550 | return new ApiProblem(403, 'You don\'t have a permission to do create Foo.'); 551 | } 552 | // you have permission, create foo 553 | } 554 | } 555 | ``` 556 | 557 | And that's all. 558 | --------------------------------------------------------------------------------