Access Control Lists (ACL)


Access Control Lists - 图1

Overview

Phalcon\Acl provides an easy and lightweight management of ACLs as well as the permissions attached to them. Access Control Lists (ACL) allow an application to control access to its areas and the underlying objects from requests.

In short, ACLs have two objects: The object that needs access, and the object that we need access to. In the programming world, these are usually referred to as Roles and Components. In the Phalcon world, we use the terminology Role and Component.

Use Case

An accounting application needs to have different groups of users have access to various areas of the application.

Role

  • Administrator Access
  • Accounting Department Access
  • Manager Access
  • Guest Access

Component

  • Login page
  • Admin page
  • Invoices page
  • Reports page

As seen above in the use case, an Role is defined as who needs to access a particular Component i.e. an area of the application. A Component is defined as the area of the application that needs to be accessed.

Using the Phalcon\Acl component, we can tie those two together, and strengthen the security of our application, allowing only specific roles to be bound to specific components.

Creating an ACL

Phalcon\Acl uses adapters to store and work with roles and components. The only adapter available right now is Phalcon\Acl\Adapter\Memory. Having the adapter use the memory, significantly increases the speed that the ACL is accessed but also comes with drawbacks. The main drawback is that memory is not persistent, so the developer will need to implement a storing strategy for the ACL data, so that the ACL is not generated at every request. This could easily lead to delays and unnecessary processing, especially if the ACL is quite big and/or stored in a database or file system.

Phalcon also offers an easy way for developers to build their own adapters by implementing the Phalcon\Acl\AdapterInterface interface.

In action

The Phalcon\Acl constructor takes as its first parameter an adapter used to retrieve the information related to the control list.

  1. <?php
  2. use Phalcon\Acl\Adapter\Memory as AclList;
  3. $acl = new AclList();

There are two self explanatory actions that the Phalcon\Acl provides:

  • Phalcon\Acl\Enum::ALLOW
  • Phalcon\Acl\Enum::DENYThe default action is Phalcon\Acl\Enum::DENY for any Role or Component. This is on purpose to ensure that only the developer or application allows access to specific components and not the ACL component itself.
  1. <?php
  2. use Phalcon\Acl\Enum;
  3. use Phalcon\Acl\Adapter\Memory as AclList;
  4. $acl = new AclList();
  5. $acl->setDefaultAction(Enum::ALLOW);

Adding Roles

As mentioned above, a Phalcon\Acl\Role is an object that can or cannot access a set of Component in the access list.

There are two ways of adding roles to our list.

  • by using a Phalcon\Acl\Role object or
  • using a string, representing the name of the roleTo see this in action, using the example outlined above, we will add the relevant Phalcon\Acl\Role objects in our list.

Role objects. The first parameter is the name of the role, the second the description

  1. <?php
  2. use Phalcon\Acl\Adapter\Memory as AclList;
  3. use Phalcon\Acl\Role;
  4. $acl = new AclList();
  5. $roleAdmins = new Role('admins', 'Administrator Access');
  6. $roleAccounting = new Role('accounting', 'Accounting Department Access');
  7. $acl->addRole($roleAdmins);
  8. $acl->addRole($roleAccounting);

Strings. Add the role with just the name directly to the ACL:

  1. <?php
  2. use Phalcon\Acl\Adapter\Memory as AclList;
  3. $acl = new AclList();
  4. $acl->addRole('manager');
  5. $acl->addRole('guest');

Adding Components

A Component is the area of the application where access is controlled. In a MVC application, this would be a Controller. Although not mandatory, the Phalcon\Acl\Component class can be used to define components in the application. Also it is important to add related actions to a component so that the ACL can understand what it should control.

There are two ways of adding components to our list.

  • by using a Phalcon\Acl\Component object or
  • using a string, representing the name of the roleSimilar to the addRole, addComponent requires a name for the component and an optional description.

Component objects. The first parameter is the name of the component, the second the description

  1. <?php
  2. use Phalcon\Acl\Adapter\Memory as AclList;
  3. use Phalcon\Acl\Component;
  4. $acl = new AclList();
  5. $admin = new Component('admin', 'Administration Pages');
  6. $reports = new Component('reports', 'Reports Pages');
  7. $acl->addComponent(
  8. $admin,
  9. [
  10. 'dashboard',
  11. 'users',
  12. ]
  13. );
  14. $acl->addComponent(
  15. $reports,
  16. [
  17. 'list',
  18. 'add',
  19. ]
  20. );

Strings. Add the component with just the name directly to the ACL:

  1. <?php
  2. use Phalcon\Acl\Adapter\Memory as AclList;
  3. $acl = new AclList();
  4. $acl->addComponent(
  5. 'admin',
  6. [
  7. 'dashboard',
  8. 'users',
  9. ]
  10. );
  11. $acl->addComponent(
  12. 'reports',
  13. [
  14. 'list',
  15. 'add',
  16. ]
  17. );

Defining Access Controls

After both the Roles and Components have been defined, we need to tie them together so that the access list can be created. This is the most important step in the role since a small mistake here can allow access to roles for components that the developer does not intend to. As mentioned earlier, the default access action for Phalcon\Acl is Phalcon\Acl\Enum::DENY, following the whitelist approach.

To tie Roles and Components together we use the allow() and deny() methods exposed by the Phalcon\Acl\Memory class.

  1. <?php
  2. use Phalcon\Acl\Adapter\Memory as AclList;
  3. use Phalcon\Acl\Role;
  4. use Phalcon\Acl\Component;
  5. $acl = new AclList();
  6. /**
  7. * Add the roles
  8. */
  9. $acl->addRole('manager');
  10. $acl->addRole('accounting');
  11. $acl->addRole('guest');
  12. /**
  13. * Add the Components
  14. */
  15. $acl->addComponent(
  16. 'admin',
  17. [
  18. 'dashboard',
  19. 'users',
  20. 'view',
  21. ]
  22. );
  23. $acl->addComponent(
  24. 'reports',
  25. [
  26. 'list',
  27. 'add',
  28. 'view',
  29. ]
  30. );
  31. $acl->addComponent(
  32. 'session',
  33. [
  34. 'login',
  35. 'logout',
  36. ]
  37. );
  38. /**
  39. * Now tie them all together
  40. */
  41. $acl->allow('manager', 'admin', 'users');
  42. $acl->allow('manager', 'reports', ['list', 'add']);
  43. $acl->allow('*', 'session', '*');
  44. $acl->allow('*', '*', 'view');
  45. $acl->deny('guest', '*', 'view');

What the above lines tell us:

  1. $acl->allow('manager', 'admin', 'users');

For the manager role, allow access to the admin component and users action. To bring this into perspective with a MVC application, the above line says that the group manager is allowed to access the admin controller and users action.

  1. $acl->allow('manager', 'reports', ['list', 'add']);

You can also pass an array as the action parameter when invoking the allow() command. The above means that for the manager role, allow access to the reports component and list and add actions. Again to bring this into perspective with a MVC application, the above line says that the group manager is allowed to access the reports controller and list and add actions.

  1. $acl->allow('*', 'session', '*');

Wildcards can also be used to do mass matching for roles, components or actions. In the above example, we allow every role to access every action in the session component. This command will give access to the manager, accounting and guest roles, access to the session component and to the login and logout actions.

  1. $acl->allow('*', '*', 'view');

Similarly the above gives access to any role, any component that has the view action. In a MVC application, the above is the equivalent of allowing any group to access any controller that exposes a viewAction.

Please be VERY careful when using the * wildcard. It is very easy to make a mistake and the wildcard, although it seems convenient, it may allow users to access areas of your application that they are not supposed to. The best way to be 100% sure is to write tests specifically to test the permissions and the ACL. These can be done in the unit test suite by instantiating the component and then checking the isAllowed() if it is true or false.

Codeception is the chosen testing framework for Phalcon and there are plenty of tests in our github repository (tests folder) to offer guidance and ideas.

  1. $acl->deny('guest', '*', 'view');

For the guest role, we deny access to all components with the view action. Despite the fact that the default access level is Acl\Enum::DENY in our example above, we specifically allowed the view action to all roles and components. This includes the guest role. We want to allow the guest role access only to the session component and the login and logout actions, since guests are not logged into our application.

  1. $acl->allow('*', '*', 'view');

This gives access to the view access to everyone, but we want the guest role to be excluded from that so the following line does what we need.

  1. $acl->deny('guest', '*', 'view');

Querying

Once the list has been defined, we can query it to check if a particular role has access to a particular component and action. To do so, we need to use the isAllowed() method.

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Acl\Adapter\Memory as AclList;
  4. use Phalcon\Acl\Role;
  5. use Phalcon\Acl\Component;
  6. $acl = new AclList();
  7. /**
  8. * Setup the ACL
  9. */
  10. $acl->addRole('manager');
  11. $acl->addRole('accounting');
  12. $acl->addRole('guest');
  13. $acl->addComponent(
  14. 'admin',
  15. [
  16. 'dashboard',
  17. 'users',
  18. 'view',
  19. ]
  20. );
  21. $acl->addComponent(
  22. 'reports',
  23. [
  24. 'list',
  25. 'add',
  26. 'view',
  27. ]
  28. );
  29. $acl->addComponent(
  30. 'session',
  31. [
  32. 'login',
  33. 'logout',
  34. ]
  35. );
  36. $acl->allow('manager', 'admin', 'users');
  37. $acl->allow('manager', 'reports', ['list', 'add']);
  38. $acl->allow('*', 'session', '*');
  39. $acl->allow('*', '*', 'view');
  40. $acl->deny('guest', '*', 'view');
  41. // ....
  42. // true - defined explicitly
  43. $acl->isAllowed('manager', 'admin', 'dashboard');
  44. // true - defined with wildcard
  45. $acl->isAllowed('manager', 'session', 'login');
  46. // true - defined with wildcard
  47. $acl->isAllowed('accounting', 'reports', 'view');
  48. // false - defined explicitly
  49. $acl->isAllowed('guest', 'reports', 'view');
  50. // false - default access level
  51. $acl->isAllowed('guest', 'reports', 'add');

Function based access

Depending on the needs of your application, you might need another layer of calculations to allow or deny access to users through the ACL. The method isAllowed() accepts a 4th parameter which is a callable such as an anonymous function.

To take advantage of this functionality, you will need to define your function when calling the allow() method for the role and component you need. Assume that we need to allow access to all manager roles to the admin component except if their name is ‘Bob’ (Poor Bob!). To achieve this we will register an anonymous function that will check this condition.

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Acl\Adapter\Memory as AclList;
  4. use Phalcon\Acl\Role;
  5. use Phalcon\Acl\Component;
  6. $acl = new AclList();
  7. /**
  8. * Setup the ACL
  9. */
  10. $acl->addRole('manager');
  11. $acl->addComponent(
  12. 'admin',
  13. [
  14. 'dashboard',
  15. 'users',
  16. 'view',
  17. ]
  18. );
  19. // Set access level for role into components with custom function
  20. $acl->allow(
  21. 'manager',
  22. 'admin',
  23. 'dashboard',
  24. function ($name) {
  25. return boolval('Bob' !== $name);
  26. }
  27. );

Now that the callable is defined in the ACL, we will need to call the isAllowed() method with an array as the fourth parameter:

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Acl\Adapter\Memory as AclList;
  4. use Phalcon\Acl\Role;
  5. use Phalcon\Acl\Component;
  6. $acl = new AclList();
  7. /**
  8. * Setup the ACL
  9. */
  10. $acl->addRole('manager');
  11. $acl->addComponent(
  12. 'admin',
  13. [
  14. 'dashboard',
  15. 'users',
  16. 'view',
  17. ]
  18. );
  19. // Set access level for role into components with custom function
  20. $acl->allow(
  21. 'manager',
  22. 'admin',
  23. 'dashboard',
  24. function ($name) {
  25. return boolval('Bob' !== $name);
  26. }
  27. );
  28. // Returns true
  29. $acl->isAllowed(
  30. 'manager',
  31. 'admin',
  32. 'dashboard',
  33. [
  34. 'name' => 'John',
  35. ]
  36. );
  37. // Returns false
  38. $acl->isAllowed(
  39. 'manager',
  40. 'admin',
  41. 'dashboard',
  42. [
  43. 'name' => 'Bob',
  44. ]
  45. );

The fourth parameter must be an array. Each array element represents a parameter that your anonymous function accepts. The key of the element is the name of the parameter, while the value is what will be passed as the value of that the parameter of to the function.

You can also omit to pass the fourth parameter to isAllowed() if you wish. The default action for a call to isAllowed() without the last parameter is Acl\Enum::DENY. To change this behavior, you can make a call to setNoArgumentsDefaultAction():

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Acl\Adapter\Memory as AclList;
  4. use Phalcon\Acl\Role;
  5. use Phalcon\Acl\Component;
  6. $acl = new AclList();
  7. /**
  8. * Setup the ACL
  9. */
  10. $acl->addRole('manager');
  11. $acl->addComponent(
  12. 'admin',
  13. [
  14. 'dashboard',
  15. 'users',
  16. 'view',
  17. ]
  18. );
  19. // Set access level for role into components with custom function
  20. $acl->allow(
  21. 'manager',
  22. 'admin',
  23. 'dashboard',
  24. function ($name) {
  25. return boolval('Bob' !== $name);
  26. }
  27. );
  28. // Returns false
  29. $acl->isAllowed('manager', 'admin', 'dashboard');
  30. $acl->setNoArgumentsDefaultAction(
  31. Acl\Enum::ALLOW
  32. );
  33. // Returns true
  34. $acl->isAllowed('manager', 'admin', 'dashboard');

Objects as role name and component name

Phalcon allows developers to define their own role and component objects. These objects must implement the supplied interfaces:

Role

We can implement the Phalcon\Acl\RoleAware in our custom class with its own logic. The example below shows a new role object called ManagerRole:

  1. <?php
  2. use Phalcon\Acl\RoleAware;
  3. // Create our class which will be used as roleName
  4. class ManagerRole implements RoleAware
  5. {
  6. protected $id;
  7. protected $roleName;
  8. public function __construct($id, $roleName)
  9. {
  10. $this->id = $id;
  11. $this->roleName = $roleName;
  12. }
  13. public function getId()
  14. {
  15. return $this->id;
  16. }
  17. // Implemented function from RoleAware Interface
  18. public function getRoleName()
  19. {
  20. return $this->roleName;
  21. }
  22. }

Component

We can implement the Phalcon\Acl\ComponentAware in our custom class with its own logic. The example below shows a new role object called ReportsComponent:

  1. <?php
  2. use Phalcon\Acl\ComponentAware;
  3. // Create our class which will be used as componentName
  4. class ReportsComponent implements ComponentAware
  5. {
  6. protected $id;
  7. protected $componentName;
  8. protected $userId;
  9. public function __construct($id, $componentName, $userId)
  10. {
  11. $this->id = $id;
  12. $this->componentName = $componentName;
  13. $this->userId = $userId;
  14. }
  15. public function getId()
  16. {
  17. return $this->id;
  18. }
  19. public function getUserId()
  20. {
  21. return $this->userId;
  22. }
  23. // Implemented function from ComponentAware Interface
  24. public function getComponentName()
  25. {
  26. return $this->componentName;
  27. }
  28. }

ACL

These objects can now be used in our ACL.

  1. <?php
  2. use ManagerRole;
  3. use Phalcon\Acl;
  4. use Phalcon\Acl\Adapter\Memory as AclList;
  5. use Phalcon\Acl\Role;
  6. use Phalcon\Acl\Component;
  7. use ReportsComponent;
  8. $acl = new AclList();
  9. /**
  10. * Add the roles
  11. */
  12. $acl->addRole('manager');
  13. /**
  14. * Add the Components
  15. */
  16. $acl->addComponent(
  17. 'reports',
  18. [
  19. 'list',
  20. 'add',
  21. 'view',
  22. ]
  23. );
  24. /**
  25. * Now tie them all together with a custom function. The ManagerRole and
  26. * ModelSbject parameters are necessary for the custom function to work
  27. */
  28. $acl->allow(
  29. 'manager',
  30. 'reports',
  31. 'list',
  32. function (ManagerRole $manager, ModelComponent $model) {
  33. return boolval($manager->getId() === $model->getUserId());
  34. }
  35. );
  36. // Create the custom objects
  37. $levelOne = new ManagerRole(1, 'manager-1');
  38. $levelTwo = new ManagerRole(2, 'manager');
  39. $admin = new ManagerRole(3, 'manager');
  40. // id - name - userId
  41. $reports = new ModelComponent(2, 'reports', 2);
  42. // Check whether our user objects have access
  43. // Returns false
  44. $acl->isAllowed($levelOne, $reports, 'list');
  45. // Returns true
  46. $acl->isAllowed($levelTwo, $reports, 'list');
  47. // Returns false
  48. $acl->isAllowed($admin, $reports, 'list');

The second call for $levelTwo evaluates true since the getUserId() returns 2 which in turn is evaluated in our custom function. Also note that in the custom function for allow() the objects are automatically bound, providing all the data necessary for the custom function to work. The custom function can accept any number of additional parameters. The order of the parameters defined in the function() constructor does not matter, because the objects will be automatically discovered and bound.

Roles Inheritance

To remove duplication and increase efficiency in your application, the ACL offers inheritance in roles. This means that you can define one Phalcon\Acl\Role as a base and after that inherit from it offering access to supersets or subsets of components. To use role inheritance, you need, you need to pass the inherited role as the second parameter of the method call, when adding that role in the list.

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Acl\Adapter\Memory as AclList;
  4. use Phalcon\Acl\Role;
  5. $acl = new AclList();
  6. /**
  7. * Create the roles
  8. */
  9. $manager = new Role('Managers');
  10. $accounting = new Role('Accounting Department');
  11. $guest = new Role('Guests');
  12. /**
  13. * Add the `guest` role to the ACL
  14. */
  15. $acl->addRole($guest);
  16. /**
  17. * Add the `accounting` inheriting from `guest`
  18. */
  19. $acl->addRole($accounting, $guest);
  20. /**
  21. * Add the `manager` inheriting from `accounting`
  22. */
  23. $acl->addRole($manager, $accounting);

Whatever access guests have will be propagated to accounting and in turn accounting will be propagated to manager

Setup relationships after adding roles

Based on the application design, you might prefer to add first all the roles and then define the relationship between them.

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Acl\Adapter\Memory as AclList;
  4. use Phalcon\Acl\Role;
  5. $acl = new AclList();
  6. /**
  7. * Create the roles
  8. */
  9. $manager = new Role('Managers');
  10. $accounting = new Role('Accounting Department');
  11. $guest = new Role('Guests');
  12. /**
  13. * Add all the roles
  14. */
  15. $acl->addRole($manager);
  16. $acl->addRole($accounting);
  17. $acl->addRole($guest);
  18. /**
  19. * Add the inheritance
  20. */
  21. $acl->addInherit($manager, $accounting);
  22. $acl->addInherit($accounting, $guest);

Serializing ACL lists

Phalcon\Acl can be serialized and stored in a cache system to improve efficiency. You can store the serialized object in APC, session, file system, database, Redis etc. This way you can retrieve the ACL quickly without having to read the underlying data that create the ACL nor will you have to compute the ACL in every request.

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Acl\Adapter\Memory as AclList;
  4. $aclFile = 'app/security/acl.cache';
  5. // Check whether ACL data already exist
  6. if (true !== is_file($aclFile)) {
  7. // The ACL does not exist - build it
  8. $acl = new AclList();
  9. // ... Define roles, components, access, etc
  10. // Store serialized list into plain file
  11. file_put_contents(
  12. $aclFile,
  13. serialize($acl)
  14. );
  15. } else {
  16. // Restore ACL object from serialized file
  17. $acl = unserialize(
  18. file_get_contents($aclFile)
  19. );
  20. }
  21. // Use ACL list as needed
  22. if (true === $acl->isAllowed('manager', 'admin', 'dashboard')) {
  23. echo 'Access granted!';
  24. } else {
  25. echo 'Access denied :(';
  26. }

It is a good practice to not use serialization of the ACL during development, to ensure that your ACL is built in every request, while other adapters or means of serializing and storing the ACL in production.

Events

Phalcon\Acl can work in conjunction with the EventsManager if present, to fire events to your application. Events are triggered using the type acl. Events that return false can stop the active role. The following events are available:

Event NameTriggeredCan stop role?
afterCheckAccessTriggered after checking if a role/component has accessNo
beforeCheckAccessTriggered before checking if a role/component has accessYes

The following example demonstrates how to attach listeners to the ACL:

  1. <?php
  2. use Phalcon\Acl;
  3. use Phalcon\Acl\Adapter\Memory as AclList;
  4. use Phalcon\Events\Event;
  5. use Phalcon\Events\Manager as EventsManager;
  6. // ...
  7. // Create an event manager
  8. $eventsManager = new EventsManager();
  9. // Attach a listener for type 'acl'
  10. $eventsManager->attach(
  11. 'acl:beforeCheckAccess',
  12. function (Event $event, $acl) {
  13. echo $acl->getActiveRole() . PHP_EOL;
  14. echo $acl->getActiveComponent() . PHP_EOL;
  15. echo $acl->getActiveAccess() . PHP_EOL;
  16. }
  17. );
  18. $acl = new AclList();
  19. // Setup the $acl
  20. // ...
  21. // Bind the eventsManager to the ACL component
  22. $acl->setEventsManager($eventsManager);

Implementing your own adapters

The Phalcon\Acl\AdapterInterface interface must be implemented in order to create your own ACL adapters or extend the existing ones.