Skip to main content
Version: 4.0

A Real World Example

In this example we are going to create a very little real world application. We will create a controller PostController that interacts with a service called PostService. For the sake of simplicity we will only cover the delete-methods of both parts.

Let's start by creating a controller that has the PostService as dependency:

class PostController extends \Laminas\Mvc\Controller\AbstractActionController
{
protected PostService $postService;

public function __construct(PostService $postService)
{
$this->postService = $postService;
}

// addAction(), editAction(), etc...

public function deleteAction()
{
$id = $this->params()->fromRoute('id');

$this->postService->deletePost($id);

return $this->redirect()->toRoute('posts');
}
}

Since we have a dependency, let's inject it using the ControllerManager, we will do this inside our Module class

class Module
{
public function getConfig()
{
return [
'controllers' => [
'factories' => [
'PostController' => function ($container) {
return new PostController(
$container->get('PostService')
);
},
],
],
];
}
}

Now that we have this in place let us quickly define our PostService. We will be using a Service that makes use of Doctrine, so we require a Doctrine\Persistence\ObjectManager as dependency.

use Doctrine\Persistence\ObjectManager;

class PostService
{
protected $objectManager;

public function __construct(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}

public function deletePost($id)
{
$post = $this->objectManager->find('Post', $id);
$this->objectManager->remove($post);
$this->objectManager->flush();
}
}

And for this one, too, let's quickly create the factory, again within our Module class.

class Module
{
// getAutoloaderConfig(), getConfig(), etc...

public function getServiceConfig()
{
return [
'factories' => [
'PostService' => function($container) {
return new PostService(
$container->get('doctrine.entitymanager.orm_default')
);
}
]
];
}
}

With this set up we can now cover some best practices.

Best practices

Ideally, you should not protect your applications using only guards (Route or Controller guards). This leaves your application open for some undesired side-effects. As a best practice you should protect all your services or controllers by injecting the authorization service. But let's go step by step:

Assuming the application example above we can easily use LmcRbacMvc to protect our route using the following guard:

return [
'lmc_rbac' => [
'guards' => [
\Lmc\Rbac\Mvc\Guard\RouteGuard::class => [
'post/delete' => ['admin']
]
]
]
];

Now, any users that do not have the "admin" role will receive a 403 error (unauthorized) when trying to access the "post/delete" route. However, this does not prevent the service (which should contain the actual logic in a properly design application) to be injected and used elsewhere in your code. For instance:

class PostController
{
protected $postService;

public function createAction()
{
// this action may have been reached through the "forward" method, hence bypassing guards
$this->postService->deletePost('2');
}
}

You see the issue!

The solution is to inject the AuthorizationService into your services and check for the permissions before doing anything wrong. So let's modify our previously created PostService class

use Doctrine\Persistence\ObjectManager;
use Lmc\Rbac\Mvc\Service\AuthorizationService;

class PostService
{
protected $objectManager;

protected $authorizationService;

public function __construct(
ObjectManager $objectManager,
AuthorizationService $authorizationService
) {
$this->objectManager = $objectManager;
$this->authorizationService = $authorizationService;
}

public function deletePost($id)
{
// First check permission
if (!$this->authorizationService->isGranted('deletePost')) {
throw new UnauthorizedException('You are not allowed !');
}

$post = $this->objectManager->find('Post', $id);
$this->objectManager->remove($post);
$this->objectManager->flush();
}
}

Since we now have an additional dependency we should inject it through our factory, again within our Module class.

class Module
{
// getAutoloaderConfig(), getConfig(), etc...

public function getServiceConfig()
{
return [
'factories' => [
'PostService' => function($sm) {
return new PostService(
$sm->get('doctrine.entitymanager.orm_default'),
$sm->get('Lmc\Rbac\Mvc\Service\AuthorizationService') // This is new!
);
}
]
];
}
}

Alternatively, you can also protect your controllers using the isGranted helper (you do not need to inject the AuthorizationService then):

class PostController
{
protected $postService;

public function createAction()
{
if (!$this->isGranted('deletePost')) {
throw new UnauthorizedException('You are not allowed !');
}

$this->postService->deletePost('2');
}
}

While protecting services is the more defensive way (because services are usually the last part of the logic flow), it is often complicated to deal with. If your application is architectured correctly, it is often simpler to protect your controllers.

When using guards then?

In fact, you should see guards as a very efficient way to quickly reject access to a hierarchy of routes or a whole controller. For instance, assuming you have the following route config:

return [
'router' => [
'routes' => [
'admin' => [
'type' => 'Literal',
'options' => [
'route' => '/admin'
],
'may_terminate' => true,
'child_routes' => [
'users' => [
'type' => 'Literal',
'options' => [
'route' => '/users'
]
],
'invoices' => [
'type' => 'Literal',
'options' => [
'route' => '/invoices'
]
]
]
]
]
]
};

You can quickly reject access to all admin routes using the following guard:

return [
'lmc_rbac' => [
'guards' => [
'Lmc\Rbac\Mvc\Guard\RouteGuard' => [
'admin*' => ['admin']
]
]
]
];

A Real World Application Part 2 - Only delete your own Posts

If you jumped straight to this section please notice that we assume you have the knowledge that we presented in the previous example. In here we will cover a very common use case. Users of our Application should only have delete permissions to their own content. So let's quickly refresh our PostService class:

use Doctrine\Persistence\ObjectManager;

class PostService
{
protected $objectManager;

protected $authorizationService;

public function __construct(
ObjectManager $objectManager,
AuthorizationService $authorizationService
) {
$this->objectManager = $objectManager;
$this->authorizationService = $authorizationService;
}

public function deletePost($id)
{
// First check permission
if (!$this->authorizationService->isGranted('deletePost')) {
throw new UnauthorizedException('You are not allowed !');
}

$post = $this->objectManager->find('Post', $id);
$this->objectManager->remove($post);
$this->objectManager->flush();
}
}

As we can see, we check within our Service if the User of our Application is allowed to delete the post with a check against the deletePost permission. Now how can we achieve that only a user who is the owner of the Post to be able to delete his own post, but other users can't? We do not want to change our Service with more complex logic because this is not the task of such service. The Permission-System should handle this. And we can, for this we have the AssertionPluginManager and here is how to do it:

First of all we need to write an Assertion. The Assertion will return a boolean statement about the current identity being the owner of the post.

namespace Your\Namespace;

use Lmc\Rbac\Mvc\Assertion\AssertionInterface;
use Lmc\Rbac\Mvc\Service\AuthorizationService;

class MustBeAuthorAssertion implements AssertionInterface
{
/**
* Check if this assertion is true
*
* @param AuthorizationService $authorization
* @param mixed $post
*
* @return bool
*/
public function assert(AuthorizationService $authorization, $post = null)
{
return $authorization->getIdentity() === $post->getAuthor();
}
}

This simple MustBeAuthorAssertion will check against the current $authorization if it equals the identity of the current context Author. The second parameter is called the "context". A context can be anything (an object, a scalar, an array...) and only makes sense in the context of the assertion.

Imagine a user calls http://my.dom/post/delete/42, so obviously he wants to delete the Post-Entity with ID#42. In this case Entity#42 is our Context! If you're wondering how the context gets there, bare with me. We will get to this later.

Now that we have written the Assertion, we want to make sure that this assertion will always be called, whenever we check for the deletePost permission. We don't want others to delete our previous content! For this we have the so called assertion_map. In this map we glue assertions and permissions together.

// config/autoload/lmc_rbac.global.php or wherever your LmcRbac configuration file is
return [
'lmc_rbac' => [
'assertion_map' => [
'deletePost' => 'Your\Namespace\MustBeAuthorAssertion'
]
]
];

Now, whenever some test the deletePost permission, it will automatically call the MustBeAuthorAssertion from the AssertionPluginManager. This plugin manager is configured to automatically add unknown classes to an invokable. However, some assertions may need dependencies. You can manually configure the assertion plugin manager as shown below:

// config/autoload/lmc_rbac.global.php or wherever your LmcRbac configuration file is
return [
'lmc_rbac' => [
// ... other rbac stuff
'assertion_manager' => [
'factories' => [
'AssertionWithDependency' => 'Your\Namespace\AssertionWithDependencyFactory'
]
]
]
];

Now we need to remember about the context. Somehow we need to let the AssertionPluginManager know about our context. This is done by simply passing it to the isGranted() method. For this we need to modify our Service one last time.

use Doctrine\Persistence\ObjectManager;

class PostService
{
protected $objectManager;

protected $authorizationService;

public function __construct(
ObjectManager $objectManager,
AuthorizationService $autorizationService
) {
$this->objectManager = $objectManager;
$this->authorizationService = $autorizationService;
}

public function deletePost($id)
{
// Note, we now need to query for the post of interest first!
$post = $this->objectManager->find('Post', $id);

// Check the permission now with a given context
if (!$this->authorizationService->isGranted('deletePost', $post)) {
throw new UnauthorizedException('You are not allowed !');
}

$this->objectManager->remove($post);
$this->objectManager->flush();
}
}

And there you have it. The context is injected into the isGranted() method and now the AssertionPluginManager knows about it and can do its thing. Note that in reality, after you have queried for the $post you would check if $post is actually a real post. Because if it is an empty return value then you should throw an exception earlier without needing to check against the permission.

A Real World Application Part 3 - Admins can delete everything

Often, you want users with a specific role to be able to have full access to everything. For instance, admins could delete all the posts, even if they don't own it.

However, with the previous assertion, even if the admin has the permission deletePost, it won't work because the assertion will evaluate to false.

Actually, the answer is quite simple: deleting my own posts and deleting others' posts should be treated like two different permissions (it makes sense if you think about it). Therefore, admins will have the permission deleteOthersPost (as well as the permission deletePost, because admin could write posts, too).

The assertion must therefore be modified like this:

namespace Your\Namespace;

use Lmc\Rbac\Mvc\Assertion\AssertionInterface;
use Lmc\Rbac\Mvc\Service\AuthorizationService;

class MustBeAuthorAssertion implements AssertionInterface
{
/**
* Check if this assertion is true
*
* @param AuthorizationService $authorization
* @param mixed $context
*
* @return bool
*/
public function assert(AuthorizationService $authorization, $context = null)
{
if ($authorization->getIdentity() === $context->getAuthor()) {
return true;
}

return $authorization->isGranted('deleteOthersPost');
}
}

A Real World Application Part 4 - Checking permissions in the view

If some part of the view needs to be protected, you can use the shipped isGranted view helper.

For example, lets's say that only users with the permissions post.manage will have a menu item to acces the adminsitration panel :

In your template post-index.phtml

<ul class="nav">
<li><a href="/">Home</a></li>
<li><a href="/posts/list">View posts</a></li>
<?php if ($this->isGranted('post.manage'): ?>
<li><a href="/posts/admin">Manage posts</a></li>
<?php endif ?>
</ul>

You can even protect your menu item regarding a role, by using the hasRole view helper :

<ul class="nav">
<li><a href="/">Home</a></li>
<li><a href="/posts/list">View posts</a></li>
<?php if ($this->hasRole('admin'): ?>
<li><a href="/posts/admin">Manage posts</a></li>
<?php endif ?>
</ul>

In this last example, the menu item will be hidden for users who don't have the admin role.

Using LmcRbacMvc with Doctrine ORM

First your User entity class must implement Lmc\Rbac\Mvc\Identity\IdentityInterface :

use LmccUser\Entity\User as LmcUserEntity;
use Lmc\Rbac\Mvc\Identity\IdentityInterface;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;

/**
* @ORM\Entity
* @ORM\Table(name="user")
*/
class User extends LmcUserEntity implements IdentityInterface
{
/**
* @var Collection
* @ORM\ManyToMany(targetEntity="HierarchicalRole")
*/
private $roles;

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

/**
* {@inheritDoc}
*/
public function getRoles()
{
return $this->roles->toArray();
}

/**
* Set the list of roles
* @param Collection $roles
*/
public function setRoles(Collection $roles)
{
$this->roles->clear();
foreach ($roles as $role) {
$this->roles[] = $role;
}
}

/**
* Add one role to roles list
* @param \Rbac\Role\RoleInterface $role
*/
public function addRole(RoleInterface $role)
{
$this->roles[] = $role;
}
}

For this example we will use a more complex situation by using Rbac\Role\HierarchicalRoleInterface so the second step is to create HierarchicalRole entity class

class HierarchicalRole implements HierarchicalRoleInterface
{
/**
* @var HierarchicalRoleInterface[]|\Doctrine\Common\Collections\Collection
*
* @ORM\ManyToMany(targetEntity="HierarchicalRole")
*/
protected $children;

/**
* @var PermissionInterface[]|\Doctrine\Common\Collections\Collection
*
* @ORM\ManyToMany(targetEntity="Permission", indexBy="name", fetch="EAGER", cascade={"persist"})
*/
protected $permissions;

/**
* Init the Doctrine collection
*/
public function __construct()
{
$this->children = new ArrayCollection();
$this->permissions = new ArrayCollection();
}

/**
* {@inheritDoc}
*/
public function addChild(HierarchicalRoleInterface $child)
{
$this->children[] = $child;
}

/*
* Set the list of permission
* @param Collection $permissions
*/
public function setPermissions(Collection $permissions)
{
$this->permissions->clear();
foreach ($permissions as $permission) {
$this->permissions[] = $permission;
}
}

/**
* {@inheritDoc}
*/
public function addPermission($permission)
{
if (is_string($permission)) {
$permission = new Permission($permission);
}

$this->permissions[(string) $permission] = $permission;
}

/**
* {@inheritDoc}
*/
public function hasPermission($permission)
{
// This can be a performance problem if your role has a lot of permissions. Please refer
// to the cookbook to an elegant way to solve this issue

return isset($this->permissions[(string) $permission]);
}

/**
* {@inheritDoc}
*/
public function getChildren()
{
return $this->children->toArray();
}

/**
* {@inheritDoc}
*/
public function hasChildren()
{
return !$this->children->isEmpty();
}
}

And the last step is to create a Permission entity class which is a very simple entity class. You don't have to do specific things!

You can find all entity examples in this folder : Example

You need one more configuration step. Indeed, how can the RoleProvider retrieve your role and permissions? For this you need to configure LmcRbacMvc\Role\ObjectRepositoryRoleProvider in your lmc_rbac.global.php file :

        /**
* Configuration for role provider
*/
'role_provider' => [
'LmcRbacMvc\Role\ObjectRepositoryRoleProvider' => [
'object_manager' => 'doctrine.entitymanager.orm_default', // alias for doctrine ObjectManager
'class_name' => 'User\Entity\HierarchicalRole', // FQCN for your role entity class
'role_name_property' => 'name', // Name to show
],
],

Using DoctrineORM with LmcRbacMvc is very simple. You need to be aware of performance where there is a lot of permissions for roles.

How to deal with roles with lot of permissions?

In very complex applications, your roles may have dozens of permissions. In the [/data/FlatRole.php.dist] entity we provide, we configure the permissions association so that whenever a role is loaded, all of its permissions are also loaded in one query (notice the fetch="EAGER"):

/**
* @ORM\ManyToMany(targetEntity="Permission", indexBy="name", fetch="EAGER")
*/
protected $permissions;

The hasPermission method is therefore really simple:

public function hasPermission($permission)
{
return isset($this->permissions[(string) $permission]);
}

However, with a lot of permissions, this method will quickly kill your database. What you can do is modfiy the Doctrine mapping so that the collection is not actually loaded:

/**
* @ORM\ManyToMany(targetEntity="Permission", indexBy="name", fetch="LAZY")
*/
protected $permissions;

Then, modify the hasPermission method to use the Criteria API. The Criteria API is a Doctrine 2.2+ API that allows your application to efficiently filter a collection without loading the whole collection:

use Doctrine\Common\Collections\Criteria;

public function hasPermission($permission)
{
$criteria = Criteria::create()->where(Criteria::expr()->eq('name', (string) $permission));
$result = $this->permissions->matching($criteria);

return count($result) > 0;
}

NOTE: This is only supported starting from Doctrine ORM 2.5!