Symfony: Roles and Permissions in configuration https://justsomegeek.com/2018/04/15/symfony-roles-and-permissions-in-configuration/
<?php | |
namespace ACME\Auth\DependencyInjection; | |
use Symfony\Component\DependencyInjection\Extension\Extension; | |
use Symfony\Component\DependencyInjection\ContainerBuilder; | |
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; | |
use Symfony\Component\Config\FileLocator; | |
class AuthExtension extends Extension | |
{ | |
protected const CONFIGURATION_RESOURCES = [ | |
'controllers.yml', | |
'repositories.yml', | |
'voters.yml', | |
]; | |
protected const CONFIGURATION_PARAMS = [ | |
'auth.roles' => 'roles', | |
'auth.permissions' => 'permissions', | |
]; | |
public function load(array $configs, ContainerBuilder $container): void | |
{ | |
$config = $this->processConfiguration(new Configuration(), $configs); | |
foreach (self::CONFIGURATION_PARAMS as $name => $value) { | |
$container->setParameter($name, $config[$value]); | |
} | |
$loader = new YamlFileLoader( | |
$container, | |
new FileLocator( | |
sprintf( | |
'%s/../Resources/config', | |
__DIR__ | |
) | |
) | |
); | |
foreach (self::CONFIGURATION_RESOURCES as $value) { | |
$loader->load($value); | |
} | |
} | |
} |
<?php | |
namespace ACME\Auth\DependencyInjection; | |
use Symfony\Component\Config\Definition\Builder\TreeBuilder; | |
use Symfony\Component\Config\Definition\ConfigurationInterface; | |
class Configuration implements ConfigurationInterface | |
{ | |
public function getConfigTreeBuilder() | |
{ | |
$treeBuilder = new TreeBuilder(); | |
$rootNode = $treeBuilder->root('auth'); | |
$rootNode->children() | |
->arrayNode('roles') | |
->prototype('array') | |
->prototype('scalar')->end() | |
->end() | |
->end(); | |
$rootNode->children() | |
->arrayNode('permissions') | |
->prototype('array') | |
->prototype('array') | |
->prototype('scalar')->end() | |
->end() | |
->end() | |
->end(); | |
return $treeBuilder; | |
} | |
} |
<?php | |
namespace ACME\Auth\Security; | |
use AppBundle\Entity\ApiEntity; | |
use ACME\Auth\Repository\RoleRepository; | |
use ReflectionClass; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Authorization\Voter\Voter; | |
class EntityVoter extends Voter | |
{ | |
/** @var RoleRepository */ | |
private $roleRepository; | |
public function __construct(RoleRepository $roleRepository) | |
{ | |
$this->roleRepository = $roleRepository; | |
} | |
protected function supports($attribute, $subject): bool | |
{ | |
$supports = false; | |
// Vote on Api Entity objects. | |
if ($subject && is_object($subject) && in_array(ApiEntity::class, class_implements($subject))) { | |
$supports = true; | |
} elseif (class_exists($subject) && strpos($subject, 'Entity') !== false) { | |
// Vote on api entity class name as string. | |
$supports = true; | |
} | |
return $supports; | |
} | |
/** | |
* @inheritdoc | |
*/ | |
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool | |
{ | |
$vote = false; | |
$permissions = $this->roleRepository->getUserPermissionsSummary($token->getUser()); | |
$reflect = new ReflectionClass($subject); | |
$entityClass = $reflect->getShortName(); | |
if (isset($permissions[$entityClass]) && in_array($attribute, $permissions[$entityClass])) { | |
$vote = true; | |
} | |
return $vote; | |
} | |
} |
auth: | |
permissions: | |
login: | |
# A route name | |
users_get_current: | |
- read | |
# An entity, this gives read-only access | |
Record: | |
- read | |
dashboard_everyone: | |
# Route names again | |
reporting.device: | |
- read | |
reporting.system.status: | |
- read | |
statistics: | |
# Route name again | |
statistics.daily: | |
- read | |
manage_users: | |
# Full access to an entity | |
User: | |
- read | |
- create | |
- update | |
- delete |
<?php | |
namespace ACME\Auth\Repository; | |
use Symfony\Component\Security\Core\User\UserInterface; | |
class RoleRepository | |
{ | |
/** @var array */ | |
private $rolesPermissions; | |
/** | |
* @param array $roles | |
* @param array $permissions | |
* | |
* @return RoleRepository | |
*/ | |
public function setRolesPermissions(array $roles, array $permissions): RoleRepository | |
{ | |
// The naming here is a bit ew, but the general idea is to iterate over roles and expand permissions groups | |
// and other roles into a list of permissions. | |
$rolesAndPermissions = []; | |
foreach ($roles as $role => $roleSpec) { | |
$rolePerms = []; | |
foreach ($roleSpec as $perm) { | |
if (substr($perm, 0, 5) === 'ROLE_') { | |
$rolePerms = $this->arrayMergeRecursiveUnique($rolesAndPermissions[$perm], $rolePerms); | |
} else { | |
// The order of these two arrays here is important, we need this roles permissions to overwrite | |
// already included other role permissions if there are any. | |
$rolePerms = $this->arrayMergeRecursiveUnique($rolePerms, $permissions[$perm]); | |
} | |
} | |
$rolesAndPermissions[$role] = $rolePerms; | |
} | |
$this->rolesPermissions = $rolesAndPermissions; | |
return $this; | |
} | |
/** | |
* Get all defined roles. | |
* | |
* @return array | |
*/ | |
public function getRoles(): array | |
{ | |
$roles = array_keys($this->rolesPermissions); | |
return $roles; | |
} | |
/** | |
* Get all defined roles and permissions. | |
* | |
* @return array | |
*/ | |
public function getRolesPermissions(): array | |
{ | |
$roles = $this->rolesPermissions; | |
return $roles; | |
} | |
/** | |
* Get all user roles. | |
* | |
* @param UserInterface $user | |
* @return array | |
*/ | |
public function getUserRoles(UserInterface $user): array | |
{ | |
$roles = array_keys( | |
array_intersect_key( | |
$this->rolesPermissions, | |
array_combine( | |
$user->getRoles(), | |
$user->getRoles() | |
) | |
) | |
); | |
return $roles; | |
} | |
/** | |
* Get all user roles and permissions. | |
* | |
* @param UserInterface $user | |
* @return array | |
*/ | |
public function getUserRolesPermissions(UserInterface $user): array | |
{ | |
$roles = array_intersect_key( | |
$this->rolesPermissions, | |
array_combine( | |
$user->getRoles(), | |
$user->getRoles() | |
) | |
); | |
return $roles; | |
} | |
/** | |
* Get all user permissions as a flat array of all entities and permissions. | |
* | |
* @param UserInterface $user | |
* @return array | |
*/ | |
public function getUserPermissionsSummary(UserInterface $user): array | |
{ | |
$roles = $this->getUserRolesPermissions($user); | |
$summary = []; | |
// Iterate all entities in each role and add them into a summary array. | |
foreach ($roles as $role) { | |
foreach ($role as $entity => $operations) { | |
if (isset($summary[$entity])) { | |
$summary[$entity] = array_values(array_unique(array_merge($summary[$entity], $operations))); | |
} else { | |
$summary[$entity] = $operations; | |
} | |
} | |
} | |
return $summary; | |
} | |
/** | |
* Custom method to mimick array_merge_recursive behaviour but with | |
* outputting unique values only. | |
*/ | |
private function arrayMergeRecursiveUnique(array $array1, array $array2) | |
{ | |
$merged = $array1; | |
foreach ($array2 as $key => & $value) { | |
if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { | |
$merged[$key] = $this->arrayMergeRecursiveUnique($merged[$key], $value); | |
} elseif (is_numeric($key)) { | |
if (!in_array($value, $merged)) { | |
$merged[] = $value; | |
} | |
} else { | |
$merged[$key] = $value; | |
} | |
} | |
return $merged; | |
} | |
} |
<?php | |
namespace ACME\Auth\Security; | |
use ACME\Auth\Repository\RoleRepository; | |
use Symfony\Bundle\FrameworkBundle\Routing\Router; | |
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |
use Symfony\Component\Security\Core\Authorization\Voter\Voter; | |
class RouteVoter extends Voter | |
{ | |
/** @var RoleRepository */ | |
private $roleRepository; | |
/** @var Router */ | |
private $router; | |
public function __construct(RoleRepository $roleRepository, Router $router) | |
{ | |
$this->roleRepository = $roleRepository; | |
$this->router = $router; | |
} | |
/** @var array */ | |
private $matchingRoutes = [ | |
'reporting', | |
'auth', | |
'statistics', | |
'users', | |
]; | |
protected function supports($attribute, $subject): bool | |
{ | |
$supports = false; | |
// Only vote on selected routes. | |
if (is_string($subject)) { | |
foreach ($this->matchingRoutes as $route) { | |
if (strpos($subject, $route) !== false) { | |
$supports = true; | |
} | |
} | |
} | |
return $supports; | |
} | |
/** | |
* @inheritdoc | |
*/ | |
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool | |
{ | |
$vote = false; | |
$permissions = $this->roleRepository->getUserPermissionsSummary($token->getUser()); | |
if (isset($permissions[$subject]) && in_array($attribute, $permissions[$subject])) { | |
$vote = true; | |
} | |
return $vote; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment