Reading your question again I wonder if my answer gets to the heart of your issue.
I use very detailed User Role policies in an application. I have 5 different roles and they apply in very specific ways to each Controller’s actions.
My system is made up of 4 parts:
- A set of constants that name every role and sets of roles
- A method on the identity that, when given the value of one of the constants, reports whether the identity matches the role or is in the set
- A policy that routes to a concrete rule for the requested contoller
- Concrete rules that decide which roles can access which actions for a controller.
The concrete controller-access rules look like this:
Example Policy Code for a single controller
/**
* WarehouseWorkersController action access
*
* @param $identity UserIdentity
* @param $action string
* @return bool
*/
public function warehouseWorkers($identity, $action)
{
if ($identity->roleIs(RoleCon::SYSTEM_ADMIN)) { return true; }
switch ($action) {
case 'index':
case 'view':
// Warehouse admin or staff
$result = $identity->roleIs(RoleCon::WORKERS);
break;
case 'edit':
$result = $identity->roleIs(RoleCon::WAREHOUSE_WORKER);
break;
case 'add':
case 'delete':
// Warehouse admin
$result = $identity->roleIs(RoleCon::WAREHOUSE_ADMIN);
break;
default:
$result = false;
break;
}
return $result;
}
More about the `roleIs()` syntax in the policy code
I use an roleIs()
method on my Identity object which means I had to use a more advanced setup for Authentication/Authorization plugins to substitute my own Identity class.
The method looks like this:
/**
* Is the role of this person the $role or among the [$role]?
*
* @param string|array $role
* @return bool
*/
public function roleIs($role)
{
if (is_array($role)) {
return in_array($this->getRole(), $role);
}
return $role === $this->getRole();
}
With that in place I make constants for all my roles and groupings of roles. That’s what RoleCon is, a Class that holds many, many constants:
<?php
namespace App\Constants;
class RoleCon
{
const SYSTEM_ADMIN = 'admin';
const WAREHOUSE_ADMIN = 'warehouseAdmin';
const WAREHOUSE_STAFF = 'warehouseStaff';
const TENANT_ADMIN = 'tenantAdmin';
const TENANT_STAFF = 'tenantStaff';
const WAREHOUSE_ROBOT = 'warehouseRobot';
const TENANT_ROBOT = 'tenantRobot';
const WAREHOUSE_WORKER = [
self::WAREHOUSE_ADMIN => self::WAREHOUSE_ADMIN,
self::WAREHOUSE_STAFF => self::WAREHOUSE_STAFF
];
const WORKERS = [
self::TENANT_ADMIN => self::TENANT_ADMIN,
self::TENANT_STAFF => self::TENANT_STAFF,
self::WAREHOUSE_ADMIN => self::WAREHOUSE_ADMIN,
self::WAREHOUSE_STAFF => self::WAREHOUSE_STAFF
];
}
One request policy serves my entire app. It is this policy that decided which controller and action have been requested and it makes the final call to my methods (see example policy code above.
The master policy (router)
/**
* Method to check if the request can be accessed
*
* @param IdentityInterface|null $identity Identity
* @param ServerRequest $request Server Request
* @return bool|ResultInterface
*/
public function canAccess($identity, ServerRequest $request)
{
/* Ignore plugin requests */
if($request->getParam('plugin') == 'DebugKit') { return true; }
$method = lcfirst($request->getAttribute('params')['controller']);
/* Not logged-in has very few options (null identity throw errors otherwise) */
if(is_null($identity) && !in_array($method, ['pages', 'users'])) {
throw new UnauthenticatedException();
}
/* logged in users can check it all */
return $this->$method($identity, $request->getAttribute('params')['action']);
}