Access controle model

Concepts

Every time an entity is created, viewed or updated, the software check if the user has the permission to make this action. The decision is made with three parameters :

  • the type of entity ;
  • the entity’s center ;
  • the entity’scope

The user must be granted access to the action on this particular entity, with this scope and center.

TL;DR

Resolve scope and center

In a service, resolve the center and scope of an entity

use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;


class MyService {
   private ScopeResolverDispatcher $scopeResolverDispatcher;
   private CenterResolverDispatcher $centerResolverDispatcher;

   public function myFunction($entity) {
      /** @var null|Center[]|Center $center */
      $center = $this->centerResolverDispatcher->resolveCenter($entity);
      // $center may be null, an array of center, or an instance of Center

      if ($this->scopeResolverDispatcher->isConcerned($entity) {
         /** @var null|Scope[]|Scope */
         $scope = $this-scopeResolverDispatcher->resolveScope($entity);
         // $scope may be null, an array of Scope, or an instance of Scope
      }

   }

}

In twig template, resolve the center:

{# resolve a center #}

{% if person|chill_resolve_center is not null%}

   {% if person|chill_resolve_center is iterable %}
      {% set centers = person|chill_resolve_center %}
   {% else %}
      {% set centers = [ person|chill_resolve_center ] %}
   {% endif %}

   <span class="open_sansbold">
      {{ 'Center'|trans|upper}} :
   </span>
   {% for c in centers %}
      {{  c.name|upper }}
      {% if not loop.last %}, {% endif %}
   {% endfor %}
{%- endif -%}

In twig template, resolve the scope:

{% if entity|chill_is_scope_concerned %}

   {% if entity|chill_resolve_scope is iterable %}
      {% set scopes = entity|chill_resolve_scope %}
   {% else %}
      {% set scopes = [ entity|chill_resolve_scope ] %}
   {% endif %}

   <span>Scopes&nbsp;:</span>
   {% for s in scopes %}
      {{  c.name|localize_translatable_string }}
      {% if not loop.last %}, {% endif %}
   {% endfor %}
{%- endif -%}

Build a Voter

<?php

namespace Chill\DocStoreBundle\Security\Authorization;

use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Security\Core\Security;

class PersonDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
    // roles should be stored into constants:

    const CREATE = 'CHILL_PERSON_DOCUMENT_CREATE';
    const SEE    = 'CHILL_PERSON_DOCUMENT_SEE';
    const SEE_DETAILS = 'CHILL_PERSON_DOCUMENT_SEE_DETAILS';
    const UPDATE = 'CHILL_PERSON_DOCUMENT_UPDATE';
    const DELETE = 'CHILL_PERSON_DOCUMENT_DELETE';

    protected Security $security;
    protected VoterHelperInterface $voterHelper;

    public function __construct(
        Security $security,
        VoterHelperFactoryInterface $voterHelperFactory
    ) {
        $this->security = $security;

        // we build here a voter helper. This will ease the operations below.
        // when the authorization model is changed, it will be easy to make a different implementation
        // of the helper, instead of writing all Voters

        $this->voterHelper = $voterHelperFactory
            // create a builder with some context
            ->generate(self::class)
            // add the support of given roles for given class:
            ->addCheckFor(Person::class, [self::SEE, self::CREATE])
            ->addCheckFor(PersonDocument::class, $this->getRoles())
            ->build();
    }


    protected function supports($attribute, $subject)
    {
        return $this->voterHelper->supports($attribute, $subject);
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        // basic check
        if (!$token->getUser() instanceof User) {
            return false;
        }

        // we first check the acl for associated elements.
        // here, we must be able to see the person associated to the document:
        if ($subject instanceof PersonDocument
            && !$this->security->isGranted(PersonVoter::SEE, $subject->getPerson())) {

            // not possible to see the associated person ? Then, not possible to see the document!
            return false;
        }

        // the voter helper will implements the logic:
        return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
    }

    // all the method below are used to register roles into the admin part
    public function getRoles()
    {
        return [
            self::CREATE,
            self::SEE,
            self::SEE_DETAILS,
            self::UPDATE,
            self::DELETE
        ];
    }

    public function getRolesWithoutScope()
    {
        return array();
    }


    public function getRolesWithHierarchy()
    {
        return ['PersonDocument' => $this->getRoles() ];
    }
}

From an user point of view

The software is design to allow fine tuned access rights for complicated installation and team structure. The administrators may also decide that every user has the right to see all resources, where team have a more simple structure.

Here is an overview of the model.

Chill can be multi-center

Chill is designed to be installed once for social center who work with multiple teams separated, or for social services’s federation who would like to share the same installation of the software for all their members.

This was required for cost reduction, but also to ease the generation of statistics agregated across federation’s members, or from the central unit of the social center with multiple teams.

Otherwise, it is not required to create multiple center: Chill can also work for one center.

Obviously, users working in the different centers are not allowed to see the entities (_persons_, _reports_, _activities_) of other centers. But users may be attached to multiple centers: consequently they will be able to see the entities of the multiple centers they are attached to.

Inside center, scope divide team

Users are attached to one or more center and, inside to those center, there may exists differents scopes. The aim of those _scopes_ is to divide the whole team of social worker amongst different departement, for instance: the social team, the psychologist team, the nurse team, the administrative team, … Each team is granted of different rights amongst scope. For instance, the social team may not see the _activities_ of the psychologist team. The administrative team may see the date & time’s activities, but is not allowed to see the detail of those entities (the personal notes, …).

The administrator is responsible of creating those scopes and team. He may also decide to not define a division inside his team: he creates only one scope and all entities will belong to this scope, all users will be able to see all entities.

As entities have only one scopes, if some entities must be shared across two different teams, the administrator will have to create a scope shared by two different team, and add the ability to create, view, or update this scope to those team.

Example: if some activities must be seen and updated between nurses and psychologists, the administrator will create a scope “nurse and psy” and add the ability for both team “nurse” and “psychologist” to “create”, “see”, and “update” the activities belonging to scope “nurse and psy”.

Where does the scope and center comes from ?

Most often, scope and center comes from user’s input:

  • when person is created, Chill asks the associated center to the user. Then, every entity associated to the user (Activity, …) is associated to this center;
  • when an entity is created, Chill asks the associated scope.

The UI check the model before adding those input into form. If the user hae access to only one center or scope, this scope or center is filled automatically, and the UI does not ask the user. Most of the times, the user does not see “Pick a scope” and “Pick a center” inputs.

Scope and Center are associated to entities through ManyToOne properties, which are then mapped to FOREIGN KEY in tables, …

But sometimes, this implementation does not fits the needs:

  • persons are associated to center geographically: the address of each person contains lat/lon coordinates, and the center is resolved from this coordinated;
  • some would like to associated persons to multiple center, or one center;
  • entities are associated to scope through the job reached by “creator” (an user);
  • some would like not to use scope at all;

For this reasons, associated center and scopes must be resolved programmatically. The default implementation rely on the model association, as described above. But it becomes possible to change the behaviour on different implementations.

Is my entity “concerned” by scopes ?

Some entities are concerned by scope, some not.

This is also programmatically resolved.

The concepts translated into code

../_images/access_control_model.png

Schema of the access control model

Chill handle entities, like persons, reports (associated to persons), activities (also associated to _persons), … On creation, those entities are linked to one center and, eventually, to one scope. They implements the interface HasCenterInterface.

Note

Somes entities are linked to a center through the entity they are associated with. For instance, activities or reports are associated to a person, and the person is associated to a center. The report’s center is always the person’s center.

Entities may be associated with a scope. In this case, they implement the HasScopeInterface.

Note

Currently, only the person entity is not associated with a scope.

At each step of his lifetime (creation, view of the entity and eventually of his details, update and, eventually, deletion), the right of the user are checked. To decide wether the user is granted right to execute the action, the software must decide with those elements :

  • the entity’s type;
  • the entity’s center ;
  • the entity’s scope, if it exists,
  • and, obviously, a string representing the action

All those action are executed through symfony voters and helpers.

How to check authorization ?

Just use the symfony way-of-doing, but do not forget to associate the entity you want to check access. For instance, in controller :

class MyController extends Controller
{

   public function viewAction($entity)
   {
      $this->denyAccessUnlessGranted('CHILL_ENTITY_SEE', $entity);

      //... go on with this action
   }
}

And in template :

{{ if is_granted('CHILL_ENTITY_SEE', entity) %}print something{% endif %}

Retrieving reachable scopes and centers for a user

The class :class:`Chill\\MainBundle\\Security\\Authorization\\AuthorizationHelperInterface` helps you to get centers and scope reachable by a user.

Those methods are intentionnaly build to give information about user rights:

  • getReachableCenters: to get reachable centers for a user
  • getReachableScopes : to get reachable scopes for a user

Adding your own roles

Extending Chill will requires you to define your own roles and rules for your entities. You will have to define your own voter to do so.

To create your own roles, you should:

Note

Both operation may be done through a simple class: you can implements :class:`Chill\\MainBundle\\Security\\ProvideRoleInterface` and :class:`Chill\\MainBundle\\Security\\AbstractChillVoter` on the same class. See live example: :class:`Chill\\ActivityBundle\\Security\\Authorization\\ActivityVoter`, and similar examples in the PersonBundle and ReportBundle.

See also

How to Use Voters to Check User Permissions

From the symfony cookbook

New in Symfony 2.6: Simpler Security Voters

From the symfony blog

Declare your role

To declare new role, implement the class :class:`Chill\\MainBundle\\Security\\ProvideRoleInterface`.

interface ProvideRoleInterface
{
    /**
     * return an array of role provided by the object
     *
     * @return string[] array of roles (as string)
     */
    public function getRoles();

    /**
     * return roles which doesn't need
     *
     * @return string[] array of roles without scopes
     */
    public function getRolesWithoutScope();
}

Then declare your service with a tag chill.role. Example :

your_service:
    class: Chill\YourBundle\Security\Authorization\YourVoter
    tags:
        - { name: chill.role }

Example of an implementation of :class:`Chill\\MainBundle\\Security\\ProvideRoleInterface`:

namespace Chill\PersonBundle\Security\Authorization;

use Chill\MainBundle\Security\ProvideRoleInterface;

class PersonVoter implements ProvideRoleInterface
{
    const CREATE = 'CHILL_PERSON_CREATE';
    const UPDATE = 'CHILL_PERSON_UPDATE';
    const SEE    = 'CHILL_PERSON_SEE';

    public function getRoles()
    {
        return array(self::CREATE, self::UPDATE, self::SEE);
    }

    public function getRolesWithoutScope()
    {
        return array(self::CREATE, self::UPDATE, self::SEE);
    }

}

Adding role hierarchy

You should prepend Symfony’s security component directly from your code.

namespace Chill\ReportBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface;
use Chill\MainBundle\DependencyInjection\MissingBundleException;

/**
 * This is the class that loads and manages your bundle configuration
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class ChillReportExtension extends Extension implements PrependExtensionInterface
{
    public function prepend(ContainerBuilder $container)
    {
        $this->prependRoleHierarchy($container);
    }

    protected function prependRoleHierarchy(ContainerBuilder $container)
    {
        $container->prependExtensionConfig('security', array(
           'role_hierarchy' => array(
              'CHILL_REPORT_UPDATE' => array('CHILL_REPORT_SEE'),
              'CHILL_REPORT_CREATE' => array('CHILL_REPORT_SEE')
           )
        ));
    }
}

Implement your voter

Most of the time, Voter will check that:

  1. The given role is reachable (= $attribute)
  2. for the given center,
  3. and, if any, for the given role
  4. if the entity is associated to another entity, this entity should be, at least, viewable by the user.

Thats what we call the “autorization logic”. But this logic may be replace by a new one, and developers should take care of it.

Then voter implementation should take care of:

  • check the access to associated entities. For instance, if an Activity is associated to a Person, the voter should first check that the user can show the associated Person;
  • as far as possible, delegates the check for associated center, scopes, and check for authorization using the authorization logic. VoterHelper will ease the most common operation of this logic.

This is an example of implementation:

<?php

namespace Chill\DocStoreBundle\Security\Authorization;

use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;

class PersonDocumentVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
    protected Security $security;
    protected VoterHelperInterface $voterHelper;

    public function __construct(
        Security $security,
        VoterHelperFactoryInterface $voterHelperFactory
    ) {
        $this->security = $security;

        // we build here a voter helper. This will ease the operations below.
        // when the authorization model is changed, it will be easy to make a different implementation
        // of the helper, instead of writing all Voters

        $this->voterHelper = $voterHelperFactory
            // create a builder with some context
            ->generate(self::class)
            // add the support of given roles for given class:
            ->addCheckFor(Person::class, [self::SEE, self::CREATE])
            ->addCheckFor(PersonDocument::class, $this->getRoles())
            ->build();
    }


    protected function supports($attribute, $subject)
    {
        return $this->voterHelper->supports($attribute, $subject);
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        // basic check
        if (!$token->getUser() instanceof User) {
            return false;
        }

        // we first check the acl for associated elements.
        // here, we must be able to see the person associated to the document:
        if ($subject instanceof PersonDocument
            && !$this->security->isGranted(PersonVoter::SEE, $subject->getPerson())) {

            // not possible to see the associated person ? Then, not possible to see the document!
            return false;
        }

        // the voter helper will implements the logic of checking:
        // 1. that the center is reachable
        // 2. for this given entity
        // 3. for this given scope
        // 4. and for the given role
        return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
    }

    public function getRoles()
    {
        // ...
    }

    public function getRolesWithoutScope()
    {
        // ...
    }


    public function getRolesWithHierarchy()
    {
        // ...
    }
}

Then, you will have to declare the service and tag it as a voter :

services:
    chill.report.security.authorization.report_voter:
        class: Chill\ReportBundle\Security\Authorization\ReportVoter
        arguments:
            - "@chill.main.security.authorization.helper"
        tags:
         - { name: security.voter }

How to resolve scope and center programmatically ?

In a service, resolve the center and scope of an entity

use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;


class MyService {
   private ScopeResolverDispatcher $scopeResolverDispatcher;
   private CenterResolverDispatcher $centerResolverDispatcher;

   public function myFunction($entity) {
      /** @var null|Center[]|Center $center */
      $center = $this->centerResolverDispatcher->resolveCenter($entity);
      // $center may be null, an array of center, or an instance of Center

      if ($this->scopeResolverDispatcher->isConcerned($entity) {
         /** @var null|Scope[]|Scope */
         $scope = $this-scopeResolverDispatcher->resolveScope($entity);
         // $scope may be null, an array of Scope, or an instance of Scope
      }

   }

}

In twig template, resolve the center:

{# resolve a center #}

{% if person|chill_resolve_center is not null%}

   {% if person|chill_resolve_center is iterable %}
      {% set centers = person|chill_resolve_center %}
   {% else %}
      {% set centers = [ person|chill_resolve_center ] %}
   {% endif %}

   <span class="open_sansbold">
      {{ 'Center'|trans|upper}} :
   </span>
   {% for c in centers %}
      {{  c.name|upper }}
      {% if not loop.last %}, {% endif %}
   {% endfor %}
{%- endif -%}

In twig template, resolve the scope:

{% if entity|chill_is_scope_concerned %}

   {% if entity|chill_resolve_scope is iterable %}
      {% set scopes = entity|chill_resolve_scope %}
   {% else %}
      {% set scopes = [ entity|chill_resolve_scope ] %}
   {% endif %}

   <span>Scopes&nbsp;:</span>
   {% for s in scopes %}
      {{  c.name|localize_translatable_string }}
      {% if not loop.last %}, {% endif %}
   {% endfor %}
{%- endif -%}

What is the default implementation of Scope and Center resolver ?

By default, the implementation rely on association into entities.

  • implements Chill\MainBundle\Entity\HasCenterInterface on entities which have one or any center;
  • implements Chill\MainBundle\Entity\HasCentersInterface on entities which have one, multiple or any centers;
  • implements Chill\MainBundle\Entity\HasScopeInterface on entities which have one or any scope;
  • implements Chill\MainBundle\Entity\HasScopesInterface on entities which have one or any scopes;

Then, the default implementation will resolve the center and scope based on the implementation in your model.

How to change the default behaviour ?

Implements those interface into services:

  • Chill\MainBundle\Security\Resolver\CenterResolverInterface;
  • Chill\MainBundle\Security\Resolver\ScopeResolverInterface;

Authorization into lists and index pages

Due to the fact that authorization model may be overriden, “list” and “index” pages should not rely on center and scope from controller. This must be delegated to dedicated service, which will be aware of the authorization model. We call them ACLAwareRepository. This service must implements an interface, in order to allow to change the implementation.

The controller must not performs any DQL or SQL query.

Example in a controller:

namespace Chill\TaskBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface;


final class SingleTaskController extends AbstractController
{

    private SingleTaskAclAwareRepositoryInterface $singleTaskAclAwareRepository;

    /**
     *
     * @Route(
     *  "/{_locale}/task/single-task/list",
     *  name="chill_task_singletask_list"
     * )
     */
    public function listAction(
        Request $request
    ) {
        $this->denyAccessUnlessGranted(TaskVoter::SHOW, null);

        $nb = $this->singleTaskAclAwareRepository->countByAllViewable(
            '', // search pattern
            [] // search flags
        );
        $paginator = $this->paginatorFactory->create($nb);

        if (0 < $nb) {
            $tasks = $this->singleTaskAclAwareRepository->findByAllViewable(
               '', // search pattern
               [] // search flags
                $paginator->getCurrentPageFirstItemNumber(),
                $paginator->getItemsPerPage(),
                // ordering:
                [
                    'startDate' => 'DESC',
                    'endDate' => 'DESC',
                ]
            );
        } else {
            $tasks = [];
        }

        return $this->render('@ChillTask/SingleTask/List/index.html.twig', [
                'tasks' => $tasks,
                'paginator' => $paginator,
                'filter_order' => $filterOrder
            ]);
    }
}

Writing ACLAwareRepository

The ACLAwareRepository should rely on interfaces

As described above, the ACLAwareRepository will perform the query for listing entities, and take care of authorization.

Those “ACLAwareRepositories” must be described into interfaces.

The service must rely on this interface, and not on the default implementation.

Example: at first, we design an interface for listing SingleTask entities:

<?php

namespace Chill\TaskBundle\Repository;

use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;

interface SingleTaskAclAwareRepositoryInterface
{

    /**
     * @return SingleTask[]|array
     */
    public function findByCurrentUsersTasks(?string $pattern = null, ?array $flags = [], ?int $start = 0, ?int $limit = 50, ?array $orderBy = []): array;

    public function countByCurrentUsersTasks(?string $pattern = null, ?array $flags = []): int;

    public function countByAllViewable(
        ?string $pattern = null,
        ?array $flags = []
    ): int;

    /**
     * @return SingleTask[]|array
     */
    public function findByAllViewable(
        ?string $pattern = null,
        ?array $flags = [],
        ?int $start = 0,
        ?int $limit = 50,
        ?array $orderBy = []
    ): array;
}

Implements this interface and register the interface as an alias for the implementation.

services:
    Chill\TaskBundle\Repository\SingleTaskAclAwareRepository:
        autowire: true
        autoconfigure: true

    Chill\TaskBundle\Repository\SingleTaskAclAwareRepositoryInterface: '@Chill\TaskBundle\Repository\SingleTaskAclAwareRepository'

Write the basic implementation for re-use: separate authorization logic and search logic

The logic of such repository may be separated into two logic:

  • the authorization logic (show only entities that the user is allowed to see);
  • the search logic (filter entities on some criterias).

This logic should be separated into your implementation.

Considering this simple interface:

interface MyEntityACLAwareRepositoryInterface {

    public function countByAuthorized(array $criterias): int;

    public function findByAuthorized(array $criteria, int $start, int $limit, array $orderBy): array;

}

The base implementation should separate the logic to allow an easy reuse. Here, the method buildQuery build a basic query without authorization logic, which can be re-used. The authorization logic is dedicated to a private method. For ease of user, the logic of adding ordering criterias and pagination parameters ($start and $limit) are also delegated to a public method.

namespace Chill\MyBundle\Repository;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;


final class MyEntityACLAwareRepository implements MyEntityACLAwareRepositoryInterface {

    private EntityManagerInterface $em;

    // constructor omitted

    public function countByAuthorized(array $criterias): int
    {
        $qb = $this->buildQuery($criterias);

        return $this->addAuthorizations($qb)->select("COUNT(e)")->getQuery()->getResult()->getSingleScalarResult();
    }

    public function findByAuthorized(array $criteria, int $start, int $limit, array $orderBy): array
    {
        $qb = $this->buildQuery($criterias);

        return $this->getResult($this->addAuthorizations($qb), $start, $limit, $orderBy);
    }

    public function getResult(QueryBuilder $qb, int $start, int $limit, array $orderBy): array
    {
        $qb
            ->setFirstResult($start)
            ->setMaxResults($limit)
            ;

         // add order by logic

         return $qb->getQuery()->getResult();
    }

    public function buildQuery(array $criterias): QueryBuilder
    {
        $qb = $this->em->createQueryBuilder();

        // implement you logic with search criteria here

        return $qb;
    }

    private function addAuthorizations(QueryBuilder $qb): QueryBuilder
    {
        // add authorization logic here
        return $qb;
    }

}

Once this logic is executed, it becomes easy to make a new implementation of the repository:

namespace Chill\MyOtherBundle\Repository;

use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Chill\MyBundle\Repository\MyEntityACLAwareRepository


final class AnotherEntityACLAwareRepository implements MyEntityACLAwareRepositoryInterface {

    private EntityManagerInterface $em;
    private \Chill\MyBundle\Repository\MyEntityACLAwareRepository $initial;

    public function __construct(
        EntityManagerInterface $em,
        \Chill\MyBundle\Repository\MyEntityACLAwareRepository $initial
    ) {
        $this->em = $em;
        $this->initial = $initial;
    }

    public function countByAuthorized(array $criterias): int
    {
        $qb = $this->initial->buildQuery($criterias);

        return $this->addAuthorizations($qb)->select("COUNT(e)")->getQuery()->getResult()->getSingleScalarResult();
    }

    public function findByAuthorized(array $criteria, int $start, int $limit, array $orderBy): array
    {
        $qb = $this->initial->buildQuery($criterias);

        return $this->initial->getResult($this->addAuthorizations($qb), $start, $limit, $orderBy);
    }

    private function addAuthorizations(QueryBuilder $qb): QueryBuilder
    {
        // add a different authorization logic here
        return $qb;
    }

}

Then, register this service and decorates the old one:

services:
    Chill\MyOtherBundle\Repository\AnotherEntityACLAwareRepository:
        autowire: true
        autoconfigure: true
        decorates: Chill\MyBundle\Repository\MyEntityACLAwareRepositoryInterface: