In previous posts, we saw how CakeDC Users plugin can help you to build an application that manages everything related to users: registration, social login, permissions, etc. Recently it has been noted by the team that there are some use cases where a deeper control of permissions is needed - more than is offered in RBAC. Today we’ll go into this using the ACL approach.
ACL or Access Control List, refers to the application using a detailed list of objects to decide who can access what. It can be as detailed as particular users and rows through to specifying which action can be performed (i.e user XX has permissions to edit articles but does not have permissions to delete articles).
One of the big features of ACL is that both the accessed objects; and objects who ask for access, can be organized in trees.
There’s a good explanation of how ACL works in the CakePHP 2.x version of the Book.
ACL does not form part of CakePHP core V 3.0 and can be accessed through the use of the cakephp/acl plugin.
Let’s just refresh the key concepts of ACL:
-
ACL: Access Control List (the whole paradigm)
-
ACO: Access Control Object (a thing that is wanted), e.g. an action in a controller: creating an article
-
ARO: Access Request Object (a thing that wants to use stuff), e.g. a user or a group of users
-
Permission: relation between an ACO and an ARO
For the purpose of this article - we shall use this use case: You are using
CakeDC/users plugin and now want to implement ACL in your application.
Installation
Starting with a brand new CakePHP app:
composer selfupdate && composer create-project --prefer-dist cakephp/app acl_app_demo && cd acl_app_demo
We are going to use
CakeDC/users and
cakephp/acl plugins. In a single step we can install them with composer:
composer require cakedc/users cakephp/acl
Create a DB and set its name and credentials in the
config/app.php
file of the just created app (in the
Datasources/default
section). This command can help you out if you are using MySQL:
mysql -u root -p -e "create user acl_demo; create database acl_demo; grant all privileges on acl_demo.* to acl_demo;"
Plugins will be loaded always with the app. Let’s set them on the bootstrap file:
bin/cake plugin load -br CakeDC/Users
bin/cake plugin load -b Acl
Now let’s insert a line in
bootstrap.php
before Users plugin loading, so
cakedc/users
will read the configuration from the
config/users.php
file of our app.
Configure::write('Users.config', ['users']);
This file does not exist yet. The plugin provides a default file which is very good to start with. Just copy it to your app running:
cp -i vendor/cakedc/users/config/users.php config/
Also, let’s copy the permissions file the same way to avoid warnings in our log files:
cp -i vendor/cakedc/users/config/permissions.php config/
We need to change
cakedc/users
config: remove RBAC, add ACL. In
cakephp/acl
there’s
ActionsAuthorize
&
CrudAuthorize
. We’ll start just using
ActionsAuthorize
. We will tell
ActionsAuthorize
that actions will be under the
'controllers/'
node and that the users entity will be
MyUsers
(an override of the Users entity from the plugin).
Edit the
Auth/authorize
section of
config/users.php
so that it sets:
'authorize' => [
'CakeDC/Auth.Superuser',
'Acl.Actions' => [
'actionPath' => 'controllers/',
'userModel' => 'MyUsers',
],
],
Add calls to load components both from Acl & Users plugin in the
initialize()
method in
AppController
:
class AppController extends Controller
{
public function initialize()
{
parent::initialize();
// (...)
$this->loadComponent('Acl', [
'className' => 'Acl.Acl'
]);
$this->loadComponent('CakeDC/Users.UsersAuth');
// (...)
}
// (...)
}
Database tables
Some tables are required in the database to let the plugins work. Those are created automatically just by running their own migrations:
bin/cake migrations migrate -p CakeDC/Users
bin/cake migrations migrate -p Acl
One table from the Acl plugin needs to be fixed because Users migration creates
users.id
as
UUID (CHAR(36))
and Acl migrations creates AROs foreing keys as
int(11)
. Types must match. Let’s fix it adapting the aros table field:
ALTER TABLE aros CHANGE foreign_key foreign_key CHAR(36) NULL DEFAULT NULL;
Now, it’s time to set our own tables as needed for our app. Let’s suppose we are developing a CMS app as specified in the
CMS Tutorial from the CakePHP book.
Based on the tutorial, we can create a simplified articles table:
CREATE TABLE articles (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id CHAR(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL,
title VARCHAR(255) NOT NULL,
body TEXT,
published BOOLEAN DEFAULT FALSE,
created DATETIME,
modified DATETIME,
FOREIGN KEY user_key (user_id) REFERENCES users(id)
);
Note: Specify CHARACTER SET and COLLATE for user_id only if the table CHARACTER SET and COLLATE of the table differ from users.id (than may happen running migrations). They must match.
Roles will be dynamic: admin will be allowed to manage them. That means that they has to be stored in a table.
CREATE TABLE roles (
id CHAR(36) NOT NULL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
created DATETIME,
modified DATETIME
);
Association between users and roles bill be
belongsTo
, so we’ll need a foreign key in the users table instead of a role
varchar
field:
ALTER TABLE users
ADD role_id CHAR(36) CHARACTER SET latin1 COLLATE latin1_swedish_ci NULL DEFAULT NULL AFTER role,
ADD INDEX role_id (role_id),
ADD FOREIGN KEY (role_id) REFERENCES roles(id);
ALTER TABLE users
DROP role;
Baking
Time to think about what will be ACOs and AROs. In most cases, Users will be the only AROs. To do that, we need to link the
Users
entity and table to the ACL plugin. In this case that we are using CakeDC/users plugin, we first need to extend the plugin as it is explained in the
docs. We will also add the behavior and
parentNode()
as shown in the
cakephp/acl readme file, so at the end we’ll need to create those files:
src/Model/Entity/MyUser.php
:
<?php
namespace App\Model\Entity;
use CakeDC\Users\Model\Entity\User;
/**
* Application specific User Entity with non plugin conform field(s)
*/
class MyUser extends User
{
public function parentNode() {
return ['Roles' => ['id' => $this->role_id]];
}
}
src/Model/Table/MyUsersTable.php
:
<?php
namespace App\Model\Table;
use CakeDC\Users\Model\Table\UsersTable;
class MyUsersTable extends UsersTable
{
public function initialize(array $config)
{
parent::initialize($config);
$this->addBehavior('Acl.Acl', ['requester']);
$this->belongsTo('Roles');
$this->hasMany('Articles');
}
}
Run
bin/cake bake controller MyUsers
(beware of case)
Then, edit the top of
src/Controller/MyUsersController.php
as:
<?php
namespace App\Controller;
use App\Controller\AppController;
use CakeDC\Users\Controller\Traits\LinkSocialTrait;
use CakeDC\Users\Controller\Traits\LoginTrait;
use CakeDC\Users\Controller\Traits\ProfileTrait;
use CakeDC\Users\Controller\Traits\ReCaptchaTrait;
use CakeDC\Users\Controller\Traits\RegisterTrait;
use CakeDC\Users\Controller\Traits\SimpleCrudTrait;
use CakeDC\Users\Controller\Traits\SocialTrait;
class MyUsersController extends AppController
{
use LinkSocialTrait;
use LoginTrait;
use ProfileTrait;
use ReCaptchaTrait;
use RegisterTrait;
use SimpleCrudTrait;
use SocialTrait;
// CRUD methods ...
To generate the template files for
MyUsers
we can run:
bin/cake bake template MyUsers
Next, just let Cake bake all objects for articles and roles:
bin/cake bake all Articles
bin/cake bake all Roles
Add behavior to their tables.
ArticlesTable
will act as
controlled
because it will represent ACOs:
class ArticlesTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
// (...)
$this->addBehavior('Acl.Acl', ['controlled']);
// (...)
The case of
RolesTable
will be similar but it will act as
requester
, as it will represent AROs:
class RolesTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
// (...)
$this->addBehavior('Acl.Acl', ['requester']);
// (...)
Create the
parentNode()
method in both entities:
Article
and
Role
.
public function parentNode() {
return null;
}
Testing
Ok, time to test the whole system! At this point, the app should be ready to use. At least, for an administrator. Let’s quickly create one: it is as easy as running
bin/cake users add_superuser
. New credentials will appear on screen.
When accessing our app in the URL that we installed it, a login form will appear. Log as the just created admin.
First, let’s create some roles. Go to
/roles
in your app’s URL. Then, click on "New Role". Create the roles:
Then, we can create two users an author and a reader. Head to
/my-users
and add them. Remember to select the Active checkbox and the proper role in the dropdown menu.
Because MyUsers has the AclBehavior, AROs has been automatically created while creating users, along with the created roles. Check it out with
bin/cake acl view aro
Aro tree:
---------------------------------------------------------------
[1] Roles.24c5646d-133d-496d-846b-af951ddc60f3
[4] MyUsers.7c1ba036-f04b-4f7b-bc91-b468aa0b7c55
[2] Roles.5b221256-0ca8-4021-b262-c6d279f192ad
[3] Roles.25908824-15e7-4693-b340-238973f77b59
[5] MyUsers.f512fcbe-af31-49ab-a5f6-94d25189dc78
---------------------------------------------------------------
Imagine that we decided that authors will be able to write new articles and readers will be able to view them. First, let’s create the root node for all controllers:
bin/cake acl create aco root controllers
Then, let’s inform ACL that there are such things as articles:
bin/cake acl create aco controllers Articles
Now, we will tell that there are 5 actions related to Articles:
bin/cake acl create aco Articles index
bin/cake acl create aco Articles view
bin/cake acl create aco Articles add
bin/cake acl create aco Articles edit
bin/cake acl create aco Articles delete
We can see the first branch of the ACOs tree here:
bin/cake acl view aco
Aco tree:
---------------------------------------------------------------
[1] controllers
[2] Articles
[3] index
[4] view
[5] add
[6] edit
[7] delete
---------------------------------------------------------------
ACL knows that articles can be added, so let’s tell who can do that. We can check which
aro.id
belongs to role
Author
with:
mysql> select id from roles where name like 'Author';
+--------------------------------------+
| id |
+--------------------------------------+
| 24c5646d-133d-496d-846b-af951ddc60f3 |
+--------------------------------------+
1 row in set (0.00 sec)
And the same with the Reader role::
mysql> select id from roles where name like 'Reader';
+--------------------------------------+
| id |
+--------------------------------------+
| 25908824-15e7-4693-b340-238973f77b59 |
+--------------------------------------+
1 row in set (0.00 sec)
So, if we look up this id in the
bin/cake acl view aro
output, it turns out that
aro.id
1 is
Author
and that
aro.id
3 is
Reader
.
If we want to let authors (ARO 1) add articles (ACO 5), we must grant permission to Articles/add to editors by running:
bin/cake acl grant 1 5
And we'll grant readers (ARO 3) view articles (ACO 4) with:
bin/cake acl grant 3 4
Don't forget to grant access to Articles/index for all roles, or nobody would access
/articles
:
bin/cake acl grant 1 3
bin/cake acl grant 2 3
bin/cake acl grant 3 3
Note: Obviously, it would be easier to set a "super role" which includes the 3 roles and grant access to index to it, but we don't want to add too many steps in this tutorial. You can try it for yourself.
Then,
aros_acos
table becomes:
mysql> select * from aros_acos;
+----+--------+--------+---------+-------+---------+---------+
| id | aro_id | aco_id | _create | _read | _update | _delete |
+----+--------+--------+---------+-------+---------+---------+
| 1 | 1 | 5 | 1 | 1 | 1 | 1 |
| 2 | 3 | 4 | 1 | 1 | 1 | 1 |
| 3 | 1 | 3 | 1 | 1 | 1 | 1 |
| 4 | 2 | 3 | 1 | 1 | 1 | 1 |
| 5 | 3 | 3 | 1 | 1 | 1 | 1 |
+----+--------+--------+---------+-------+---------+---------+
5 rows in set (0.00 sec)
Let’s create a new article as the first user. To do that:
- Log out (we are still logged in as superadmin) going to
/logout
- Log in as the first created user
- Go to
/articles
- Create an article
Right now, author can add an article but not view it, since we only set the add permission. Check it out clicking in
View
next to the article.
Log in as a reader to check how the reader can really view the article.
Obviously, more than a couple of permissions have to be grant in a big app. This tutorial served just as an example to start.
Last words
That's all for now related to the use of ACL in a webapp made with
CakePHP. A lot more can be done with ACL. Next step would be to use CrudAuthorize to specify which CRUD permissions are granted for any ARO to any ACO.
Keep visiting the blog for new articles!
This tutorial has been tested with:
- CakePHP 3.5.10
- CakeDC/users 6.0.0
- cakephp/acl 0.2.6
An example app with the steps followed in this tutorial is available in this
GitHub repo.
Please let us know if you use it, we are always improving on them - And happy to get issues and pull requests for our open source plugins. As part of our open source work in CakeDC, we maintain many open source plugins as well as contribute to the CakePHP Community.
Reference