Thursday, December 18, 2014

APACHE web server and SSL authentication

This article describes configuration techniques of module mod_ssl, which extends a functionality of Apache HTTPD to support SSL protocol. The article will deal with authentication of server (One-way SSL authentication), as well as it will also include authentication of clients by using certificates (Two-way SSL authentication).

1. Introduction

If you have decided to enable a SSL ( Secure Sockets Layer ) protocol on your web server it may be because you would like to extend its functionality to achieve an integrity and confidentiality for a data transferred on unsecured networks. However, this protocol with the combination of PKI ( Public Key Infrastructure ) principles can also along the side of integrity and confidentiality provide authentication between both sides involved in the client-server communication.
One-way SSL authentication allows a SSL client to confirm an identity of SSL server. However, SSL server cannot confirm an identity of SSL client. This kind of SSL authentication is used by HTTPS protocol and many public servers around the world this way provides services such as webmail or Internet banking. The SSL client authentication is done on a “application layer” of OSI model by the client entering an authentication credentials such as username and password or by using a grid card.

Two-way SSL authentication also known as mutual SSL authentication allows SSL client to confirm an identity of SSL server and SSL server can also confirm an identity of the SSL client. This type of authentication is called client authentication because SSL client shows its identity to SSL server with a use of the client certificate. Client authentication with a certificate can add yet another layer of security or even completely replace authentication method such us user name and password.

In this document, we will discuss configuration of both types of SSL authentication one-way SSL authentication and two-way SSL authentication.

2. Issuing OpenSSL certificates

This section briefly describes a procedure to create all required certificates using an openssl application. The whole process of issuing openssl certificates is simple. However, in case when a larger amount of issued certificates is required below described procedure would be inadequate, and therefore, I recommend for that case use OpenSSL's CA modul. Reader is expected to have a basic knowledge of PKI, and for that reason all steps will be described just briefly. Please follow this link if you wish to refresh your knowledge about Public key infrastructure.

All certificates will be issued by using OpenSSL application and openssl.cnf configuration file. Please save this file into a directory from which you would run all openssl commands. Please note that this configuration file is optional, and we use it just to make the whole process easier.
openssl.cnf:
[ req ]
default_md = sha1
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
countryName = Country
countryName_default = SK
countryName_min = 2
countryName_max = 2
localityName = Locality
localityName_default = Bratislava
organizationName = Organization
organizationName_default = Jariq.sk Enterprises
commonName = Common Name
commonName_max = 64

[ certauth ]
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
basicConstraints = CA:true
crlDistributionPoints = @crl

[ server ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
nsCertType = server
crlDistributionPoints = @crl

[ client ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment, dataEncipherment
extendedKeyUsage = clientAuth
nsCertType = client
crlDistributionPoints = @crl

[ crl ]
URI=http://testca.local/ca.crl
As a first step you need to generate self-signed certificate CA. Once prompted for value of “Common Name” insert string “Test CA”:
# openssl req -config ./openssl.cnf -newkey rsa:2048 -nodes \ 
 -keyform PEM -keyout ca.key -x509 -days 3650 -extensions certauth -outform PEM -out ca.cer
If you have not encountered any complications running the above command you would find in your current directory a file “ca.key” with private key of certificate authority (CA) and ca.cer with its self-signed certificate.
In the next step you need to generate private SSL key for the server:
 # openssl genrsa -out server.key 2048
To generate Certificate Signing Request in PKCS#10 format you would use a following command as a common name you can specify its hostname – for example “localhost”.
# openssl req -config ./openssl.cnf -new -key server.key -out server.req
With self-signed certificate authority issue server certificate with serial number 100:
# openssl x509 -req -in server.req -CA ca.cer -CAkey ca.key \ 
 -set_serial 100 -extfile openssl.cnf -extensions server -days 365 -outform PEM -out server.cer
New file server.key contains server's private key and file server.cer is a certificate itself. Certificate Signing Request file server.req is not needed any more so it can be removed.
# rm server.req
Generete private key for SSL client:
# openssl genrsa -out client.key 2048
As for the server also for client you need to generate Certificate Signing Request and as a Common Name, I have used string: “Jaroslav Imrich”.
# openssl req -config ./openssl.cnf -new -key client.key -out client.req
With your self-signed Certificate Authority, issue a client certificate with serial number 101:
# openssl x509 -req -in client.req -CA ca.cer -CAkey ca.key \ 
-set_serial 101 -extfile openssl.cnf -extensions client -days 365 -outform PEM -out client.cer
Save client's private key and certificate in a PKCS#12 format. This certificate will be secured by a password and this password will be used in the following sections to import the certificate into the web browser's certificate manager:
# openssl pkcs12 -export -inkey client.key -in client.cer -out client.p12
File “client.p12” contains a private key and the client's certificate, therefore files “client.key”, “client.cer” and “client.req” are no longer needed, so these files can be deleted.
# rm client.key client.cer client.req

3. One-way SSL authentication

Once the server's private key and certificate are ready, you can begin with SSL configuration of Apache web server. In many cases, this process is comprised of 2 steps – enabling mod_ssl and creating virtual host for port 443/TCP.
Enabling mod_ssl is very easy, all you need to do is to open httpd.conf file and remove comment mark from line:
  LoadModule ssl_module modules/mod_ssl.so
Just because the server will serve the HTTPS requests on port 443 in is important to enable port 433/TCP in the apaches's configuration file by adding a line:
Listen 443
Definition of a virtual host can be also defined in “httpd.conf” file and should look as the one below:
<VirtualHost _default_:443>
        ServerAdmin webmaster@localhost

        DocumentRoot /var/www
        
                Options FollowSymLinks
                AllowOverride None
        
        
                Options Indexes FollowSymLinks MultiViews
                AllowOverride None
                Order allow,deny
                allow from all
        

        ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
        
                AllowOverride None
                Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
                Order allow,deny
                Allow from all

        LogLevel warn
        ErrorLog /var/log/apache2/error.log
        CustomLog /var/log/apache2/ssl_access.log combined

        SSLEngine on
        SSLCertificateFile    /etc/apache2/ssl/server.cer
        SSLCertificateKeyFile /etc/apache2/ssl/server.key

        BrowserMatch ".*MSIE.*" 
                nokeepalive ssl-unclean-shutdown 
                downgrade-1.0 force-response-1.0
</VirtualHost>
In the example above directive “SSLEngine on” enables SSL support virtual host. Directive “SSLCertificateFile” defines a full path of the server's certificate and finally directive “SSLCertificateKeyFile” defines a full path to server's private key. If the private key is secured by password this password will be only needed when starting apache web server.
Any changes to https.conf file such as the changes above require a web server restart. If you encounter some problems during the restart it is likely that this is due to configuration errors in your https.conf file. The actual error should appear in deamon's error log.
Testing of a functionality of our new configuration can be done by using a web browser. The fist attempt to for connection most certainly displays an error message, that the attempt to verify server's certificate failed because, the issuer of the certificate is unknown.
The certificate is not trusted because the issuer certificate is unknown
Importing CA's certificate into the web browser's using its Certificate manager will solve this problem. To add a certificate into a Mozilla Firefox browser navigate to “Preferences > Advanced > Encryption > View certificates > Authorities” and during the import tick the box which says: “This certificate can identify web sites”.
Next attempt to connect the web server should be successful.
SSL server verified certificate
If you want to avoid the need of importing a CA's certificate into the web browser, you can buy server certificate from some commercial authority, which certificates are distributed by the web browser.

4. Two-way SSL authentication

If you have decided that you will require certificate authentication from every client, all you need to do is to add following lines into a virtual host configuration file:
SSLVerifyClient require
SSLVerifyDepth 10
SSLCACertificateFile /etc/apache2/ssl/ca.cer
“SSLVerifyClient require” directive ensures that clients which do not provide a valid certificate from some of the trusted Certificate authorities would not be able to communicate with SSL server. Some CA rely on another CA, which may rely yet on another and so on. Directive “SSLVerifyDepth 10” specifies how far down in the chain of CA reliance, the server will accept CA signed certificate as valid. If, for instance, SSLVerifyDepth directive will hold value 1 then the client's certificate must be signed directly by your trusted CA. In this article, the client's certificate is signed directly by CA and therefore the only sensible value for SSLVerifyDepth directive is 1. Last directive “SSLCACertificateFile” specifies a full path to a Certificate Authority certificate by which a client's certificate was signed.
Do not forget to restart your apache web server after any change made to its configuration files:
# apachectl graceful
If you try to connect to the SSL server without a client certificate an error message will pop up:
SSL peer was unable to negotiate an acceptable set of security parameters.
All what needs to be done is to import previously created a client certificate in PKCS#12 form into to firefox's certificate manager under “Your Certificates” section. This task can be done by navigating to menu then “Preferences > Advanced > Encryption > View certificates > Your certificates”. During the import, you will be asked to enter a password which had been set during the creation of the certificate. Depending on the browser version you use, you may also need to set main password for software token, which is used by the browser to safely store certificates.
Firefox SSL certificate manager
If you make another attempt to connect to the SSL server, browser will automatically pop-up an appropriate certificate for SSL server authentication.
select ssl certificate to by used with ssl connection
After the selection of a valid certificate, the connection to the SSL server will be granted.
SSL server verified certificate

5. Another advantages of SSL authentication

Values from a client certificate can be used by web application for precise identification of the user. It is easy as to use a directive “SSLOptions +StdEnvVars” and mode_ssl will provide information taken from a client certificate as well as a certificate itself to the given web application.

This operation will take a lot of server's run-time, and therefore, it is recommended to use this functionality on for files with certain extension or for files within certain directory as it is shown in the following example:
<FilesMatch ".(cgi|shtml|phtml|php)$">
 SSLOptions +StdEnvVars
</FilesMatch>

<Directory /usr/lib/cgi-bin>
 SSLOptions +StdEnvVars
</Directory>
List of the available variables can be found in a module mod_ssl documentation. Accessing variables provided my mod_ssl is language specific. However, for the sake of completeness, here is a sample of CGI script written in perl which will display a “Common Name” of the client:
#!/usr/bin/perl

use strict;

print "Content-type: text/htmln";
print "n";
print $ENV{"SSL_CLIENT_S_DN_CN"}
Here is an output of the script after its execution by the SSL web server:
mod_ssl - information taken from the client certificate
Mod_ssl also supports a use of above mentioned variables directly from the server's configuration. This way you can restrict an access to some resources for employees of a certain company:
<Location /private/>
 SSLRequire %{SSL_CLIENT_S_DN_O} eq “Jariq.sk Enterprises”
 </Location>
These variables can be also used in conjunction with configuration directive “CustomLog” to enable logging a client's access details . More information can be found in the official mod_ssl documentation.

6. Conclusion

If you have not heard about Two-way SSL authentication yet, it is likely that after reading this article you asked yourself why is this type of SSL authentication not used often in the production environment. The answer is simple – cryptic operations used during SSL connections are difficult to process in regard to the web server resources. It is possible to boost web server performance by so called SSL accelerators ( cards containing a processor optimized for cryptic operations). However, in many cases SSL accelerators are more expensive than the server itself and therefore, Two-way SSL authentication is not attractive to use in the web server environment.

7. Linux Apache2 specific notes:

openning a port 443 is not required, if a configuration file /etc/apache2/ports.conf has defined an IfModule mod_ssl.c directive:
<IfModule mod_ssl.c>
 Listen 443
 </IfModule>
Enabling ssl module can be done by:
 a2enmod ssl
If directive IfModule mod_ssl.c in /etc/apache2/ports.conf is defined command a2enmod ssl will also automatically enable listenning on port 443.
Definition of virtual host file needs a slight change:
 BrowserMatch “.*MSIE.*” \
 nokeepalive ssl-unclean-shutdown \
 downgrade-1.0 force-response-1.0

Wednesday, December 3, 2014

How to Use CakePHP's Access Control Lists

The ACL lets you create a hierarchy of users with their respective roles. Here's a quick example.
  • Super Users
    • User #1
  • Admins
    • User #2
    • User #3
  • Users
    • User #4
    • User #5
    • User #6
    • ....
In this tutorial, we will be setting up an ACL for a simple blog. If you haven't yet checked out Getting Started With CakePHP (and Part 2) here on Nettuts+, please do so and then return, as we will be taking for granted the framework basics.
With this hierarchy, we can assign several permissions for each role:
  • The Super Users can create, read, update and delete Posts and Users.
  • The Admins can create, read, update and delete Posts.
  • The Users can create and read Posts.
  • Everyone else can just read Posts.
Each permission will be given to the group, not to the user; so if user #6 is promoted to Admin, he will be checked against the group permission --not his. These roles and child nodes (users) are called Access Requests Objects, or AROs.
Now, on the other side, we have the Access Control Objects, or ACOs. These are the objects to be controlled. Above I mentioned Posts and Users. Normally, these objects are directly linked with the models, so if we have a Post model, we will need an ACO for this model.
Each ACO has four basic permissions: create, read, update and delete. You can remember them with the keyword CRUD. There's a fifth permission, the asterisk, that is a shortcut for full access.
We will be using just two ACOs for this tutorial: Post and User, but you can create as many as you need.
Let's proceed to create the database tables. You can find this code in db_acl.sql inside your app's config/sql directory.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
CREATE TABLE acos (
  id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  parent_id INTEGER(10) DEFAULT NULL,
  model VARCHAR(255) DEFAULT '',
  foreign_key INTEGER(10) UNSIGNED DEFAULT NULL,
  alias VARCHAR(255) DEFAULT '',
  lft INTEGER(10) DEFAULT NULL,
  rght INTEGER(10) DEFAULT NULL,
  PRIMARY KEY  (id)
);
  
CREATE TABLE aros_acos (
  id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  aro_id INTEGER(10) UNSIGNED NOT NULL,
  aco_id INTEGER(10) UNSIGNED NOT NULL,
  _create CHAR(2) NOT NULL DEFAULT 0,
  _read CHAR(2) NOT NULL DEFAULT 0,
  _update CHAR(2) NOT NULL DEFAULT 0,
  _delete CHAR(2) NOT NULL DEFAULT 0,
  PRIMARY KEY(id)
);
  
CREATE TABLE aros (
  id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  parent_id INTEGER(10) DEFAULT NULL,
  model VARCHAR(255) DEFAULT '',
  foreign_key INTEGER(10) UNSIGNED DEFAULT NULL,
  alias VARCHAR(255) DEFAULT '',
  lft INTEGER(10) DEFAULT NULL,
  rght INTEGER(10) DEFAULT NULL,
  PRIMARY KEY  (id)
);
We can begin now creating ARO and ACO nodes, but hey, we don't have users! We'll have to create a basic authentication system.
As this tutorial is intended for CakePHP developers with a basic to moderate knowledge of the framework, I will be supplying the code and a brief explanation. However, the authentication system is not the goal of this tutorial.
The MySQL table:
1
2
3
4
5
CREATE TABLE users (
  id INTEGER(10) UNSIGNED AUTO_INCREMENT KEY,
  username TEXT,
  password TEXT
);
The User model (models/user.php)
1
2
3
4
5
<?php
class User extends AppModel {
    var $name = 'User';
}
?>
The Users controller (controllers/users_controller.php)
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
class UsersController extends AppController {
    var $name = 'Users';
    var $components = array('Auth');
  
    function beforeFilter(){
        $this->Auth->userModel = 'User';
        $this->Auth->allow('*');
    }
  
    function register(){
        if(!empty($this->data)){
            // Here you should validate the username (min length, max length, to not include special chars, not existing already, etc)
            // As well as the password
            if($this->User->validates()){
                $this->User->save($this->data);
                // Let's read the data we just inserted
                $data = $this->User->read();
                // Use it to authenticate the user
                $this->Auth->login($data);
                // Then redirect
                $this->redirect('/');
            }
        }
    }
  
    function login(){
        if(!empty($this->data)){
            // If the username/password match
            if($this->Auth->login($this->data)){
                $this->redirect('/');
            } else {
                $this->User->invalidate('username', 'Username and password combination is incorrect!');
            }
        }
    }
  
    function logout(){
        $this->Auth->logout();
        $this->redirect('/');
    }
}
?>
Since we have new elements, let's review them. First, we are setting a $components variable. This variable includes all the components in the array. We will be needing the Auth component, which is a core component, as are HTML and Form helpers, but since it isn't included by default by Cake, we will have to include it manually.
The Auth component handles some basic authentication mechanics: it helps us to login a user and it handles an authenticated user's session for us, as well as handling the log out and basic authorization for guests. Also, it hashes the password automatically. I'll be explaining how to call each function in the following paragraphs.
Next, we are creating a function called beforeFilter. This is a callback function and it lets us set some actions before all the controller logic is processed. The Auth component requires us to specify a model to be used, in this case the User. Then, by default, it will deny all the access to users not logged in. We'll have to overwrite this behaviour with allow() which requires one parameter. That parameter can be an asterisk, specifying that all methods inside said controller can be accessed by unauthenticated users. Or, it can be passed an array with the functions that can be accessed by unauthenticated users. In this case, since we have just three functions, the following lines are the same thing.
1
2
$this->Auth->allow('*');
$this->Auth->allow(array('register', 'login', 'logout'));
For the login() function, the Auth component will handle all the login mechanics for us. We will just have to supply the function with an array with two keys: the username and the password. These keys can be changed, but by default, both username and password fields will be matched against the database and will return true if the user has been authenticated.
Finally, the controller's login function will try to match a username/password combination against the database.
Please note that this code is very basic. You'll need to validate the username characters, if the username exists, the minimum length for the password, and so on.
The Register view (views/users/register.ctp)
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<h2>Register your account</h2>
  
<form method="POST" action="<?=$this->here; ?>">
  
<p>
    Username
    <?=$form->text('User.username'); ?>
</p>
<p>
    Password
    <?=$form->password('User.password'); ?>
</p>
  
<?=$form->submit('Register'); ?>
</form>
The Login view (views/users/login.ctp)
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<h2>Log in to your account</h2>
  
<form method="POST" action="<?=$this->here; ?>">
  
<?=$form->error('User.username'); ?>
<p>
    Username
    <?=$form->text('User.username'); ?>
</p>
<p>
    Password
    <?=$form->password('User.password'); ?>
  
</p>
<?=$form->submit('Log in'); ?>
</form>
Open /users/register in your web browser and register a new account. I suggest admin as username and 123 as password and if your session expires just go to /users/login and enter the correct username/password combination you just created.
We're not even working with ACL, but we already can deny posting, editing and deleting posts. Open your Posts controller and add the Auth component.
1
var $components = array('Auth');
Now go to /posts in your web browser. If you're logged in, you should see the posts, but if you're not, you will be redirected to /users/login. By simply including the Auth component, all the actions are by default, denied for guests. We need to deny three actions for unauthorized users: create, edit and delete. In other terms, we'll have to allow index and view.
1
2
3
4
function beforeFilter(){
    $this->Auth->userModel = 'User';
    $this->Auth->allow(array('index', 'view'));
}
Go to edit or create a post; if you're not logged in, you should be redirected to /users/login. Everything seems to be working quite good, but what about the views? The edit and delete links are being shown to everybody. We should make a conditional.
But before going into that, let's see how Auth's user() function works. Copy and paste these lines into the index function.
1
2
$user = $this->Auth->user();
pr($user);
Open your /posts in your browser and, if logged in, the pr() will throw something like this.
1
2
3
4
5
6
7
8
Array
(
    [User] => Array
        (
            [id] => 1
            [username] => admin
        )
)
The user() function returns an array just as a model would do. If we had more than three fields (password is not included), they will be shown in the array. If you're not logged in, the array will be empty, so you can know a user is logged in if Auth's user() array is not empty.
Now, Auth is a component, meant to be used in a controller. We need to know from a view if a user is logged in, preferably via a helper. How can we use a component inside a helper? CakePHP is so awesome and flexible that it is possible.
01
02
03
04
05
06
07
08
09
10
11
12
<?
class AccessHelper extends Helper{
    var $helpers = array("Session");
  
    function isLoggedin(){
        App::import('Component', 'Auth');
        $auth = new AuthComponent();
        $auth->Session = $this->Session;
        $user = $auth->user();
        return !empty($user);
    }
?>
Save this snippet in views/helpers as access.php. Now let's see the code, line by line. First, we are setting up a $helpers var. Helpers can include other helpers, just like $components can. The Session component is required for the Auth component, but we have no access to this component inside a helper. Fortunately we have a Session helper, that will help us.
Next, we create a function and use App::import which will let us import an element that normally we wouldn't have access to. The next line creates the Auth component in a $auth variable, and now a little dirty hack; since the Auth component reads the session to know if we are logged or not, it requires the Session component, but as we are importing it from a place it shouldn't belong to, we'll have to give it a new Session object. Finally, we are using user() and setting it to $user and returning true if the variable is not empty, otherwise false.
Let's get back to the posts controller and proceed to add the helper.
1
var $helpers = array('Access');
The access helper is now accessible from the view. Open index.ctp in views/posts and replace this line.
1
<small><a href="/posts/edit/<? echo $post['Post']['id'] ?>">edit</a> | <? echo $html->link('delete', '/posts/delete/'.$post['Post']['id'], NULL, 'Are you sure?'); ?></small>
With this one.
1
<? if($access->isLoggedin()): ?><small><a href="/posts/edit/<? echo $post['Post']['id'] ?>">edit</a> | <? echo $html->link('delete', '/posts/delete/'.$post['Post']['id'], NULL, 'Are you sure?'); ?></small><? endif; ?>
Go back to your web browser, reload the index page and if you're logged in, you'll see the edit and delete links for each post. Otherwise, you'll see nothing.
While this would be enough if you have an app with one or two users, it is not enough if you have registrations open.
Open the Users controller and add the ACL component.
1
var $components = array('Auth', 'Acl');
Next, let's create a function to install the ACOs and AROs nodes.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function install(){    
    if($this->Acl->Aro->findByAlias("Admin")){
        $this->redirect('/');
    }
    $aro = new aro();
  
    $aro->create();
    $aro->save(array(
        'model' => 'User',
        'foreign_key' => null,
        'parent_id' => null,
        'alias' => 'Super'
    ));
  
    $aro->create();
    $aro->save(array(
        'model' => 'User',
        'foreign_key' => null,
        'parent_id' => null,
        'alias' => 'Admin'
    ));
  
    $aro->create();
    $aro->save(array(
        'model' => 'User',
        'foreign_key' => null,
        'parent_id' => null,
        'alias' => 'User'
    ));
  
    $aro->create();
    $aro->save(array(
        'model' => 'User',
        'foreign_key' => null,
        'parent_id' => null,
        'alias' => 'Suspended'
    ));
  
    $aco = new Aco();
    $aco->create();
    $aco->save(array(
        'model' => 'User',
        'foreign_key' => null,
        'parent_id' => null,
        'alias' => 'User'
    ));
  
    $aco->create();
    $aco->save(array(
       'model' => 'Post',
       'foreign_key' => null,
       'parent_id' => null,
       'alias' => 'Post'
    ));
  
    $this->Acl->allow('Super', 'Post', '*');
    $this->Acl->allow('Super', 'User', '*');
    $this->Acl->allow('Admin', 'Post', '*');
    $this->Acl->allow('User', 'Post', array('create'));
}
By importing the ACL component, we can access the ACOs and AROs model. We begin by creating an ARO, which is the requester for the objects; in other words, who will be accessing the objects. That would be the user (hence the User string in model). We are creating different roles; Super, Admin, User and Suspended.
Next, we do the same with ACOs, since we just have two objects to manage (Posts and Users), we'll create two, one for each.
Now, a quick note. The array that we are saving for both ACOs and AROs have four fields: the model name, foreign key (which is useful if you want to give access to one ARO, like a post he created), parent id (which will be used later and is the basic parent-child nodes relationship; we will be creating users below these roles), and the alias (which is a quick form to find the objects).
Finally, we are using ACL's allow() function. The first parameter is the ACO alias; second, the ARO alias, and third, the permissions given to said ARO. A Superuser has full access to the Post and User models, an Admin has full access to the Post model, and a User can just create posts.
At the function's beginning, I declared a conditional to check if the Admin role exists in ACOs. You don't want to install the same thing more than once, do you? It would mess up the database quite badly.
Open /users/install in your web browser, and, since we don't have a view, CakePHP will throw an error, but just check the MySQL dump. All the relationships have been successfully created, it's time to work with the child nodes.
Let's clean the users table. Open phpMyAdmin, select your database, the users table and click empty. We'll get back to the register() function on the Users Controller. Just below this line:
1
$this->Auth->login($data);
Paste this code:
01
02
03
04
05
06
07
08
09
10
11
// Set the user roles
$aro = new Aro();
$parent = $aro->findByAlias($this->User->find('count') > 1 ? 'User' : 'Super');
  
$aro->create();
$aro->save(array(
     'model'        => 'User',
     'foreign_key'    => $this->User->id,
     'parent_id'    => $parent['Aro']['id'],
     'alias'        => 'User::'.$this->User->id
));
In the first line we create a new ARO object. Then we'll get the parent node in which the user will be created. If there's a record in the database, we'll set it to the User ARO, else, the first user should be the Super.
Then we'll save a new ARO; the model is the one we are working currently on, User, the foreign_key is the last record's id we just created. The parent_id is the node we began with; we'll just pass it the id and finally the alias. It is a good idea to call it Model_name, then a separator ::, and then the identifier. It will be much easier to find it.
Now we're done. Create four users: superuser, adminuser, normaluser and suspendeduser. I suggest the same username and password for testing purposes. Don't forget that, until this point, only the superuser has a role of Super; all the remaining will be Users!
Because ACL is a component, it is accessible only within the controller. Later, it will be also in the view; but first things first. Include the ACL component in the Posts controller, as we did in the Users controller. Now you can begin checking permissions. Let's go to the edit function and do a quick test. In the first line of said method, add this.
1
2
$user = $this->Auth->user();
if(!$this->Acl->check('User::'.$user['User']['id'], 'Post', 'update')) die('you are not authorized');
ACL's check() function needs three parameters: the ARO, the ACO and the action. Go and edit a post, and, if you're not logged in as the super user, the script will die. Go to /users/login and access as the Super user and go back to editing. You should be able to edit the post. Check below the MySQL dump to see the magic. Four database queries: that sure isn't scalable.
We have two issues. First, two lines for the permissions check. Second, the ACLs aren't being cached. And don't forget all the logged users can see the edit link, even if just the Super user can use it.
Let's create a new component. Let's call it Access.
01
02
03
04
05
06
07
08
09
10
<?php
class AccessComponent extends Object{
    var $components = array('Acl', 'Auth');
    var $user;
  
    function startup(){
        $this->user = $this->Auth->user();
    }
}
?>
Save it in controllers/components as access.php. The class's $user var will be initiated as the component is loaded, and startup() is a callback function, so it's now set in the class. Now let's create our check() function.
1
2
3
4
5
6
7
function check($aco, $action='*'){
    if(!empty($this->user) && $this->Acl->check('User::'.$this->user['User']['id'], $aco, $action)){
        return true;
    } else {
        return false;
    }
}
Our check() method will need just two parameters: the ACO and the action (which is optional). The ARO will be the current user for each session. The action parameter will default to *, which is full access for the ARO. The function begins by checking if the $this->user is not empty (which really tells us if a user is logged in) and then we go to ACL. We have already covered this.
We can now include the Access component in our Posts controller and check the permissions with just one line.
1
if(!$this->Access->check('Post', 'update')) die('you are not authorized');
The same result is achieved with less code, but the error message is ugly. You'd better replace the die() with the CakePHP error handler:
1
$this->cakeError('error404');
This might be ugly, but it works. We'll have to create a helper that loads a component with a custom method for use in the helper.
Add this function in the Access component (controllers/components/access.php).
1
2
3
4
5
function checkHelper($aro, $aco, $action = "*"){
    App::import('Component', 'Acl');
    $acl = new AclComponent();
    return $acl->check($aro, $aco, $action);
}
Now,let's rewrite the access helper (views/helpers/access.php).
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
class AccessHelper extends Helper{
    var $helpers = array("Session");
    var $Access;
    var $Auth;
    var $user;
  
    function beforeRender(){
        App::import('Component', 'Access');
        $this->Access = new AccessComponent();
  
        App::import('Component', 'Auth');
        $this->Auth = new AuthComponent();
        $this->Auth->Session = $this->Session;
  
        $this->user = $this->Auth->user();
    }
  
    function check($aco, $action='*'){
        if(empty($this->user)) return false;
        return $this->Access->checkHelper('User::'.$this->user['User']['id'], $aco, $action);
    }
  
    function isLoggedin(){
        return !empty($this->user);
    }
}
?>
The beforeRender() method is a callback, similar to the component's startup(). We are loading two components and since these will be used in most functions, it's a good idea to start all at once, than to manually start them each time the method is called.
Now on your index.ctp view in views/posts you can replace this line.
1
<small><a href="/posts/edit/<? echo $post['Post']['id'] ?>">edit</a> | <? echo $html->link('delete', '/posts/delete/'.$post['Post']['id'], NULL, 'Are you sure?'); ?></small>
With this one.
1
<? if($access->check('Post')): ?><small><a href="/posts/edit/<? echo $post['Post']['id'] ?>">edit</a> | <? echo $html->link('delete', '/posts/delete/'.$post['Post']['id'], NULL, 'Are you sure?'); ?></small><? endif; ?>
Just don't forget to check permissions, both in the views and the controllers!
You can access the user data for a logged user with the user() method in the Auth component. Then you can access the array and get the info you want. But there must be a better way. Let's add the following function in the Access component.
1
2
3
function getmy($what){
    return !empty($this->user) && isset($this->user['User'][$what]) ? $this->user['User'][$what] : false;
}
This is quite useful when you need to save a Post with a user_id relationship.
1
$this->data['Post']['user_id'] = $this->Access->getmy('id');
And in the view, we can do something similar with the helper.
1
2
3
function getmy($what){
    return !empty($this->user) && isset($this->user['User'][$what]) ? $this->user['User'][$what] : false;
}
In your template file you can do something like below to greet a user by his username.
1
Welcome <?=$access->isLoggedIn() ? $access->getmy('username') : 'Guest'; ?>
Let's say we need to switch roles for the user #4: he needs to be a Super User. So, User's id is 4 and Aro's id is 1.
1
2
$user_id = 4;
$user_new_group = 1;
Now we need to find the User's Aro in order to modify its parent Aro.
1
2
3
4
5
6
7
8
9
$aro_user $this->Acl->Aro->find('first',
    array(
        'conditions' => array(
            'Aro.parent_id !=' => NULL,
            'Aro.model' => 'User',
            'Aro.foreign_key' => $user_id
        )
    )
);
Finally, if the $aro_user variable is not empty, let's update the Aro.parent_id field.
1
2
3
4
5
if(!empty($aro_user)){
    $data['id'] = $aro_user['Aro']['id'];
    $data['parent_id'] = $user_new_group;
    $this->Acl->Aro->save($data);
}
Please note that you have to validate both the user's id and the new aro's id. If one of these do not exist, it will create a mess in your ACL tables.