285 lines
9.7 KiB
PHP

<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\MainBundle\Security\Authorization;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserACLAwareRepositoryInterface;
use Chill\MainBundle\Security\ParentRoleHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* Helper for authorizations.
*
* Provides methods for user and entities information.
*/
class AuthorizationHelper implements AuthorizationHelperInterface
{
public function __construct(
private readonly CenterResolverManagerInterface $centerResolverManager,
private readonly LoggerInterface $logger,
private readonly ScopeResolverDispatcher $scopeResolverDispatcher,
private readonly UserACLAwareRepositoryInterface $userACLAwareRepository,
private readonly ParentRoleHelper $parentRoleHelper
) {}
/**
* Filter an array of centers, return only center which are reachable.
*
* @param User $user The user
* @param array $centers a list of centers which are going to be filtered
*/
public function filterReachableCenters(User $user, array $centers, mixed $role): array
{
$results = [];
foreach ($centers as $center) {
if ($this->userCanReachCenter($user, $center)) {
$results[] = $center;
}
}
return $results;
}
/**
* @deprecated use UserACLAwareRepositoryInterface::findUsersByReachedACL instead
*
* @return User[]
*/
public function findUsersReaching(string $role, array|Center $center, array|Scope|null $scope = null, bool $onlyEnabled = true): array
{
return $this->userACLAwareRepository
->findUsersByReachedACL($role, $center, $scope, $onlyEnabled);
}
/**
* Return all the role which give access to the given role. Only the role
* which are registered into Chill are taken into account.
*
* @return string[] the role which give access to the given $role
*/
public function getParentRoles(string $role): array
{
trigger_deprecation('Chill\MainBundle', '2.0', 'use ParentRoleHelper::getParentRoles instead');
return $this->parentRoleHelper->getParentRoles($role);
}
/**
* Get reachable Centers for the given user, role,
* and optionally Scope.
*
* @return list<Center>
*/
public function getReachableCenters(UserInterface $user, string $role, ?Scope $scope = null): array
{
if (!$user instanceof User) {
return [];
}
/** @var array<string, Center> $centers */
$centers = [];
foreach ($user->getGroupCenters() as $groupCenter) {
$permissionGroup = $groupCenter->getPermissionsGroup();
// iterate on roleScopes
foreach ($permissionGroup->getRoleScopes() as $roleScope) {
// check that the role is in the reachable roles
if ($this->isRoleReached($role, $roleScope->getRole())) {
if (null === $scope) {
$centers[spl_object_hash($groupCenter->getCenter())] = $groupCenter->getCenter();
break;
}
if ($scope->getId() === $roleScope->getScope()->getId()) {
$centers[spl_object_hash($groupCenter->getCenter())] = $groupCenter->getCenter();
break;
}
}
}
}
return array_values($centers);
}
/**
* Return all reachable circle for a given user, center and role.
*
* @param Center|Center[] $center
*
* @return Scope[]
*/
public function getReachableCircles(UserInterface $user, string $role, array|Center $center)
{
$scopes = [];
if (is_iterable($center)) {
foreach ($center as $c) {
$scopes = \array_merge($scopes, $this->getReachableCircles($user, $role, $c));
}
return $scopes;
}
foreach ($user->getGroupCenters() as $groupCenter) {
if ($center->getId() === $groupCenter->getCenter()->getId()) {
// iterate on permissionGroup
$permissionGroup = $groupCenter->getPermissionsGroup();
// iterate on roleScopes
foreach ($permissionGroup->getRoleScopes() as $roleScope) {
// check that the role is in the reachable roles
if ($this->isRoleReached($role, $roleScope->getRole())) {
$scopes[] = $roleScope->getScope();
}
}
}
}
return $scopes;
}
/**
* Return all reachable scope for a given user, center and role.
*/
public function getReachableScopes(UserInterface $user, string $role, array|Center $center): array
{
return $this->getReachableCircles($user, $role, $center);
}
/**
* Determines if a user is active on this center.
*
* @param Center|Center[] $center May be an array of center
*/
public function userCanReachCenter(User $user, array|Center $center): bool
{
if ($center instanceof \Traversable) {
foreach ($center as $c) {
if ($c->userCanReachCenter($user, $c)) {
return true;
}
}
return false;
}
if ($center instanceof Center) {
foreach ($user->getGroupCenters() as $groupCenter) {
if ($center->getId() === $groupCenter->getCenter()->getId()) {
return true;
}
}
return false;
}
throw new \UnexpectedValueException(sprintf('The entity given is not an instance of %s or an array of centers, %s given', Center::class, gettype($center)));
}
/**
* Determines if the user has access to the given entity.
*
* if the entity implements Chill\MainBundle\Entity\HasScopeInterface,
* the scope is taken into account.
*
* @return bool true if the user has access
*/
public function userHasAccess(UserInterface $user, mixed $entity, string $attribute): bool
{
$centers = $this->centerResolverManager->resolveCenters($entity);
foreach ($centers as $c) {
if ($this->userHasAccessForCenter($user, $c, $entity, $attribute)) {
return true;
}
}
return false;
}
/**
* Test if a parent role may give access to a given child role.
*
* @param string $childRole The role we want to test if he is reachable
* @param string $parentRole The role which should give access to $childRole
*
* @return bool true if the child role is granted by parent role
*/
private function isRoleReached(string $childRole, string $parentRole)
{
return $this->parentRoleHelper->isRoleReached($childRole, $parentRole);
}
private function userHasAccessForCenter(User $user, Center $center, mixed $entity, $attribute): bool
{
if (!$this->userCanReachCenter($user, $center)) {
$this->logger->debug('user cannot reach center of entity', [
'center_name' => $center->getName(),
'user' => $user->getUsername(),
]);
return false;
}
foreach ($user->getGroupCenters() as $groupCenter) {
// filter on center
// in some case, the center can be the same, but have different object hashes,
// we cannot compare the objects: we must compare the ids here
if ($groupCenter->getCenter()->getId() === $center->getId()) {
$permissionGroup = $groupCenter->getPermissionsGroup();
// iterate on roleScopes
foreach ($permissionGroup->getRoleScopes() as $roleScope) {
// check that the role allow to reach the required role
if ($this->isRoleReached($attribute, $roleScope->getRole())) {
// if yes, we have a right on something...
// perform check on scope if necessary
if ($this->scopeResolverDispatcher->isConcerned($entity)) {// here, we should also check that the role need a scope
$scope = $this->scopeResolverDispatcher->resolveScope($entity);
if (null === $scope) {
return true;
}
if (is_iterable($scope)) {
foreach ($scope as $s) {
if ($roleScope->getScope()->getId() === $s->getId()) {
return true;
}
}
} else {
if ($roleScope->getScope() === $scope) {
return true;
}
}
} else {
return true;
}
}
}
}
}
$this->logger->debug('user can reach center entity, but not role', [
'username' => $user->getUsername(),
'center' => $center->getName(),
]);
return false;
}
}