Usage

Restrict access to resources

The first thing to do is to create some access rules.

For example, let's say you are creating a blog engine and you want to define who can access the articles. You'll have the following objects:

  • users accounts are the security identities
  • articles are the resources to which you want to restrict access
  • there will be several roles, like an "article editor", an administrator, …
  • each role will be able to do specific actions on the articles (edit, delete, …)

1. Define the security identity

As we said, users are the security identities (they could also be customers, clients, usergroups, profiles, accounts …).

Here is an example of a simple user class that implements the SecurityIdentityInterface:

/**
 * @ORM\Entity
 */
class User implements SecurityIdentityInterface
{
    use SecurityIdentityTrait;

    /**
     * @ORM\Id @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @var Role[]
     * @ORM\OneToMany(targetEntity="MyCLabs\ACL\Model\Role", mappedBy="securityIdentity",
     *     cascade={"persist", "remove"}, orphanRemoval=true)
     */
    protected $roles;

    public function __construct()
    {
        $this->roles = new ArrayCollection();
    }

    public function getId()
    {
        return $this->id;
    }
}

As you can see we used the SecurityIdentityTrait to implement methods required by the interface, but we still need to declare the $roles association to map it with Doctrine.

2. Mark an entity as a resource

If you want to restrict access to an entity (e.g. the article #42), you need to make it implement the EntityResource interface:

class Article implements EntityResource
{
    // ...

    public function getId()
    {
        return $this->id;
    }
}

You can also optionally add an association to the roles that apply on this resource. Such association is very useful so that the roles (and their authorizations) are deleted in cascade when the resource is deleted:

class Article implements EntityResource
{
    // ...

    /**
     * @ORM\OneToMany(targetEntity="ArticleEditorRole", mappedBy="article", cascade={"remove"})
     */
    protected $roles;

    public function __construct()
    {
        $this->roles = new ArrayCollection();
    }
}

This association can also be useful if you need to find all the "editors" of an article for example.

Here the association targets ArticleEditorRole, but if you have several roles that apply to articles, you might want to have an abstract BaseArticleRole that you can reference in your Doctrine association.

3. Create a new role

The role gives the authorizations.

To create a new role, extend the MyCLabs\ACL\Model\Role abstract class:

/**
 * @ORM\Entity(readOnly=true)
 */
class ArticleEditorRole extends Role
{
    /**
     * @ORM\ManyToOne(targetEntity="Article", inversedBy="roles")
     */
    protected $article;

    public function __construct(User $user, Article $article)
    {
        $this->article = $article;

        parent::__construct($user);
    }

    public function createAuthorizations(ACL $acl)
    {
        $acl->allow(
            $this,
            new Actions([ Actions::VIEW, Actions::EDIT ]),
            $this->article
        );
    }
}

The authorizations given by the role are created in the createAuthorizations() method.

For creating an authorization, you need to call $acl->allow() with:

  • the role (the role contains the user)
  • the actions that are included in the authorization
  • the resource (here the article)

The resource can be either an entity instance (as shown above) or an entity classname, which will give access to all entities of that type:

/**
 * @ORM\Entity(readOnly=true)
 */
class AdministratorRole extends Role
{
    public function createAuthorizations(ACL $acl)
    {
        // Administrators are able to do everything on ALL the articles
        $acl->allow(
            $this,
            Actions::all(),
            new ClassResource('My\Model\Article')
        );
    }
}

Authorizations that are granted on class resources (i.e. all entities of that class) are cascaded automatically to each sub-resource (i.e. all entities). Read the documentation about authorization cascading to learn more.

Another common use case for class resources are object creation. For example, you want an article editor to be able to create new articles. You can do this in the ArticleEditorRole:

        $acl->allow(
            $this,
            new Actions([ Actions::CREATE ]),
            new ClassResource('My\Model\Article')
        );

4. Grant roles to users

Now that everything is defined, we can grant users some roles very simply:

$acl->grant($user, new ArticleEditorRole($user, $article));

Here, the ACL will add the role to the user and use the role to automatically generate and persist the related authorizations.

Check access

Now that accesses are defined, we can test the authorizations:

$acl->grant($user, new ArticleEditorRole($user, $article));

echo $acl->isAllowed($user, Actions::VIEW, $article);   // true
echo $acl->isAllowed($user, Actions::EDIT, $article);   // true
echo $acl->isAllowed($user, Actions::DELETE, $article); // false

// If you added the CREATE authorization on the class resource:
$allArticles = new ClassResource('My\Model\Article');
echo $acl->isAllowed($user, Actions::CREATE, $allArticles); // true

Note: You should never test if a user has a role to check access. This practice, called "implicit" access control, makes your access rules hardcoded and very likely to fail or break on change. Instead, it is recommended that you use "explicit" access control using authorizations on resources. Read more about this in this excellent article about RBAC. This is in part for that reason that testing if a user has a role is not that practical with this library. It's not a main feature, because it shouldn't be used a lot. Use isAllowed() instead.

Filter at query level

If you fetch entities from the database and then test isAllowed() onto each entity, this is inefficient:

  • you will load all the entities, even the one the user can't access
  • you will issue one query for each isAllowed() call

There is a much more efficient solution: filtering entities at the query level (i.e. SQL level).

For this, move on to the Filtering queries documentation.