ACL and permissions 27/07/2015
Let’s start with creating an empty project: composer create-project nette/web-project myweb
.
Out of the box, you can ask Nette Framework if the user has some roles and whether he is logged in or not:
<?php
namespace App\Presenters;
use Nette\Application\UI\Presenter;
class HomepagePresenter extends Presenter
{
public function actionDefault(): void
{
dump($this->user->isLoggedIn()); // false
dump($this->user->isInRole('admin')); // false
die;
}
}
But we would gone crazy if we have to ask for user’s role each and every time trying to find out user’s permission for particular action.
Nette provides really pleasant way how to deal with application’s ACL. It’s called Nette\Security\Permission
. :P
We may add some example roles and resources when creating an object of Permission
class:
<?php
namespace App\Security;
use Nette\Security\Permission;
class AuthorizatorFactory
{
private const ROLE_GUEST = 'guest';
private const ROLE_CLIENT = 'client';
public function create(): Permission
{
$acl = new Permission;
/**
* Roles 1*
*/
$acl->addRole(self::ROLE_GUEST);
$acl->addRole(self::ROLE_CLIENT, self::ROLE_GUEST); // 2*
/**
* Resoures 3*
*/
$acl->addResource('Front');
$acl->addResource('Front:Homepage', 'Front');
$acl->addResource('Front:About', 'Front');
$acl->addResource('Client');
$acl->addResource('Client:Sign', 'Client');
$acl->addResource('Client:Profile', 'Client');
/**
* Permissions 4*
*/
$acl->allow(self::ROLE_GUEST, 'Front');
$acl->allow(self::ROLE_GUEST, 'Client:Sign');
$acl->allow(self::ROLE_CLIENT, Permission::ALL, Permission::ALL);
return $acl;
}
}
What have we done in this step:
- Two user roles were created:
guest
: that is a default role name (of unsigned user) in Nette Frameworkclient
: role defined by us == logged user
- Role
client
inherits from theguest
role ($acl->addRole(self::ROLE_CLIENT, self::ROLE_GUEST);
) - There are two main resources (
Front
,Client
) and other that inherit from these two:Front:Homepage
Front:About
Client:Sign
Client:Profile
guest
role is able to look at everything inFront
resource ($acl->allow(self::ROLE_GUEST, 'Front');
) but also theClient:Sign
($acl->allow(self::ROLE_GUEST, 'Client:Sign');
) resource. TheClient:Sign is there because unsigned user has to be allowed to log in
. Roleclient
is allowed to do everything in the world ($acl->allow(self::ROLE_CLIENT, Permission::ALL, Permission::ALL);
).
Cool, we have a Permission object, what now? Let’s tell Nette that it can use a authorizator
:
services:
...
- App\Security\AuthorizatorFactory
-
class: Nette\Security\Permission
factory: @App\Security\AuthorizatorFactory::create
By default, Nette looks into DIC
and tries to find a service implementing Nette\Security\IAuthorizator
. The Nette\Security\Permission
class implements that interface and we have registered this class into DIC
so it will be automatically passed to an object of type Nette\Security\User
available in each Presenter
class.
Try it out in action:
<?php
namespace App\Presenters;
use Nette\Application\UI\Presenter;
class HomepagePresenter extends Presenter
{
public function actionDefault()
{
dump($this->user->isAllowed('Front:Homepage')); // => true
dump($this->user->isAllowed('Client')); // => false
dump($this->user->isAllowed('Client:Sign')); // => true
die;
}
}
Right. Now we have to automate the process a bit (as I said before - we don’t want to call $this->user->isAllowed('...')
each time manually).
I will create a AbstractPresenter
class for that purpose (it will be empty for now):
<?php
namespace App\Presenters;
use Nette\Application\UI\Presenter;
class AbstractPresenter extends Presenter
{
}
And some other presenters:
<?php
namespace App\Presenters;
/**
* @resource Front:Homepage
*/
class HomepagePresenter extends AbstractPresenter
{
}
<?php
namespace App\Presenters;
/**
* @resource Front:About
*/
class AboutPresenter extends AbstractPresenter
{
}
<?php
namespace App\Presenters;
/**
* @resource Client:Sign
*/
class SignPresenter extends AbstractPresenter
{
}
<?php
namespace App\Presenters;
/**
* @resource Client:Profile
*/
class ProfilePresenter extends AbstractPresenter
{
}
Do you see the annotation? That is our own way how to define a resource on presenter class. Beware! You can do it your own way, the annotation is just one among many other ways how to define presenter resource (You can use for example the Nette naming convention: $presenter->getName()
).
We may make the SignPresenter a little bit more functional..
<?php
namespace App\Presenters;
use App\Security\AuthorizatorFactory;
use Nette\Application\UI\Form;
use Nette\Security\Identity;
use Nette\Utils\ArrayHash;
/**
* @resource User:Sign
*/
class SignPresenter extends AbstractPresenter
{
/**
* @persistent
*/
public $backlink = '';
public function actionIn()
{
if ($this->user->isLoggedIn()) {
$this->redirect('Profile:');
}
}
public function actionOut()
{
$this->user->logout();
$this->flashMessage('You were signed out');
$this->redirect('in');
}
public function createComponentSignInForm()
{
$form = new Form;
$form->addText('username', 'Username:');
$form->addPassword('password', 'Password:');
$form->addSubmit('submit', 'Submit');
$form->onValidate[] = [$this, 'validateCreditians'];
$form->onSuccess[] = [$this, 'signInFormSucceeded'];
return $form;
}
public function validateCreditians(Form $form, ArrayHash $values)
{
if ($values->username !== 'franta' || $values->password !== 'heslo') {
$form->addError('Invalid credentials');
}
}
public function signInFormSucceeded(Form $form, ArrayHash $values)
{
$identity = new Identity(1, AuthorizatorFactory::ROLE_USER, [
'username' => $values->username
]);
$this->user->login($identity);
if ($this->backlink) {
$this->restoreRequest($this->backlink);
}
$this->redirect('Homepage:');
}
}
We can not forgot a tiny template where we put the sign form (file app/presenters/templates/Sing/in.latte
):
{block content}
{control signInForm}
{/block}
Cool. But we still didn’t automate the authorization process. We will start by implementing method ::checkRequirements()
in our AbstractPresenter
. Why exactly checkRequirements
? Becase it is meant for it (see Nette\Application\UI\Presenter::checkRequirements()
).
<?php
namespace App\Presenters;
use Nette\Application\UI\Presenter;
use Nette\Application\ForbiddenRequestException;
use Nette\Reflection;
class AbstractPresenter extends Presenter
{
/**
* Check authorization
* @return void
*/
public function checkRequirements($element)
{
if ($element instanceof Reflection\Method) {
/**
* Check permissions for Action/handle methods
*
* - Do not that (rely on presenter authorization)
*/
return;
} else {
/**
* Check permissions for presenter access
*/
$resource = $element->getAnnotation('resource'); // 1*
}
if (!$this->user->isAllowed($resource)) { // 2*
if (!$this->user->isLoggedIn()) {
$this->redirect('Sign:in', ['backlink' => $this->storeRequest()]);
} else {
throw new ForbiddenRequestException;
}
}
}
}
This method is called twice in the application’s lifecycle. Once with a Presenter
class reflection in the method argument and second with particular method reflection (::actionDefault
, ::actionOut
, …). We will ignore the call with action method reflection for now and focus on the call with presenter class reflection. Steps written in the snippet above:
- Find out the value of presenter annotation
@resource
- Is user allowed to access the resource? If so, continue executing script. Of not:
- When user is logged in, tell him he has no access to this resource (404)
- Unsigned user is redirected to login
Now the application is ready for testing. Open your browser and navigate to homepage url: /
. Great, you’re on homepage. Now try /profile
. Bam! The application will redirect you to /sign/in
and after submitting login form there will be another redirect - to the /profile
url - cool! Why is that?
First of all, Presenter
class can store the request in session and return unique hash: $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]);
. And after login we will simply restore the request by given backlink hash: $this->restoreRequest($this->backlink);
.
There is still some error about missing template in Client:Profile
, but I think you can manage to create it on your own. :)
And that’s it!
That was just an simple way how to implement ACL
in the Nette application, sure it can be done in more complex ways with more functioanality. For example:
- We can specify
privileges
in particular resources ($this->allow(self::ROLE_GUEST, 'Front', ['read', 'write]);
) and use these privileges in action methods annotation. We would check user’s permissions in the first if-clause ofAbstractPresenter::checkRequirements()
method - Getting presenter/method annotation could be more extensive - we mau implement some annotation inheritency for more modular applications
- Proper way how to implement dynamic ACL is again in the
AuthorizatorFactory
class - as it is a service registered inDIC
, just add dependencies (database probably) in the__constructor
method and use the database to load user’s permissions