How to implement permissions

Declare permission schema

An extension needs to declare all permission components and instance templates in composer.json. This is required to let the permissions system know them which helps supporting site administrators looking up components and instances.

The following example shows how this is done:

{
    "name": "acme/person-bundle",
    ...
    "extra": {
        "zikula": {
            ...
            "securityschema": {
                "AcmePersonBundle::": "::",
                "AcmePersonBundle:Person:": "Person ID::",
                "AcmePersonBundle:Address:": "Address ID::",
                ...
            }
        }
    }
}

Each entry in the securityschema array consists of a component (key) and a template for the instances (value).

The extension author is completely free in deciding which components and instances are supported.

Basic usage

Typically required permissions are checked for using the PermissionApi.

In controllers

The following code shows a possible example how to use this in a controller method:

namespace Acme\PersonBundle\Controller;

use Acme\PersonBundle\Entity\PersonEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Zikula\PermissionsBundle\Api\ApiInterface\PermissionApiInterface;

class PersonController extends AbstractController
{
    /**
     * Modify a person.
     *
     * @throws AccessDeniedException Thrown if the user hasn't permissions to edit the person
     */
    #[Route('/admin/edit/{personid}', name: 'acmepersonbundle_person_edit', requirements: ['personid' => "^[1-9]\d*$"])]
    #[Theme('admin')]
    public function edit(
        Request $request,
        PersonEntity $person,
        PermissionApiInterface $permissionApi
    ): Response {
        if (!$permissionApi->hasPermission('AcmePersonBundle::', $person->getId() . '::', ACCESS_EDIT)) {
            throw new AccessDeniedException();
        }

        // ...
    }
}

Direct usage

Of course the PermissionApi can also be injected as a service into any class if desired.

namespace Acme\PersonBundle\Helper;

use Acme\PersonBundle\Entity\PersonEntity;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Zikula\PermissionsBundle\Api\ApiInterface\PermissionApiInterface;

class MyService
{
    public function __construct(private readonly PermissionApiInterface $permissionApi)
    {
    }

    public function processPerson(PersonEntity $person)
    {
        if (!$this->permissionApi->hasPermission('AcmePersonBundle::', $person->getId() . '::', ACCESS_EDIT)) {
            throw new AccessDeniedException();
        }
    }
}

Using an annotation

Controllers may also use a PermissionCheck Annotation to perform permission checks in a declarative way.

The controller example from above would look like this then:

namespace Acme\PersonBundle\Controller;

use Acme\PersonBundle\Entity\PersonEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Zikula\PermissionsBundle\Annotation\PermissionCheck;

class PersonController extends AbstractController
{
    #[Route('/admin/edit/{personid}', name: 'acmepersonbundle_person_edit', requirements: ['personid' => "^[1-9]\d*$"])]
    #[PermissionCheck(['AcmePersonBundle::', '$personid::', 'edit'])]
    #[Theme('admin')]
    public function edit(Request $request, PersonEntity $person): Response
    {
        // ...
    }
}

Note this is limited to one permission check for each method. It is not possible to have multiple occurrences in a method's doc block. If more complex evaluations are required, permission API should be used instead (see above).

It is also possible to use the annotation on class-level. But it is not allowed to use it for a class and for it's methods concurrently.

Example for a class-level use case:

namespace Acme\PersonBundle\Controller;

use Acme\PersonBundle\Entity\PersonEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Zikula\PermissionsBundle\Annotation\PermissionCheck;

#[PermissionCheck('admin')]
class ConfigController extends AbstractController
{
    public function doSomething(Request $request)
    {
        // ...
    }

    public function somethingElse(Request $request)
    {
        // ...
    }
}

For more details see the PermissionCheck Annotation document.

Twig templates

You can use hasPermission inside templates similarly as in PHP. The only difference is that the permission level constants need to be declared as strings.

Example:

{% if hasPermission('AcmePersonBundle::', person.id ~ '::', 'ACCESS_READ') %}
    <h3>{{ person.name }}</h3>
{% endif %}

Special aspects

Check permissions for specific users

By default permission checks are always executed for the current user. Internally the PermissionApi uses the CurrentUserApi for that.

In order to explicitly perform a permission check for a specific user, it is possible to assign the corresponding user ID as the fourth parameter for the hasPermission() method of PermissionApi.

Note the annotation-based checks are always done for the current user.

Permissions for own data

The permissions system does not have a default way to express ownerships. The reason for this is that extensions may implement arbitrary logic and rules for when some data is considered as owned by a user.

Common approaches for handling such requirements are for example:

  • Use a separate permission component, like AcmeRecipesBundle:Own:(Ingredients|Recipes).
  • Do a comparison of the current user's ID with the owner ID.
  • Use dedicated configuration options, for example user group selectors, to store a group ID which may do additional things (independent of permissions).
  • Combine these steps.

Category-based permissions

If an extension utilises the side-wide category system. it could become helpful to be able to filter data based on permissions for categories.

For further information about this please refer to the Categories docs, particularly CategoryPermissionApi.