Skip to content

Instantly share code, notes, and snippets.

@m1n0
Last active March 1, 2019 10:31
Show Gist options
  • Save m1n0/0af11b1bd888d3a8a466426a47c0fd6a to your computer and use it in GitHub Desktop.
Save m1n0/0af11b1bd888d3a8a466426a47c0fd6a to your computer and use it in GitHub Desktop.
<?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;
}
}
auth:
roles:
ROLE_USER:
- login
- dashboard_everyone
- statistics
ROLE_FRONT_DESK:
- ROLE_USER
- manage_users
<?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