Last active
March 1, 2019 10:31
-
-
Save m1n0/0af11b1bd888d3a8a466426a47c0fd6a to your computer and use it in GitHub Desktop.
Symfony: Roles and Permissions in configuration https://justsomegeek.com/2018/04/15/symfony-roles-and-permissions-in-configuration/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
auth: | |
roles: | |
ROLE_USER: | |
- login | |
- dashboard_everyone | |
- statistics | |
ROLE_FRONT_DESK: | |
- ROLE_USER | |
- manage_users |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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