Kohana[v2.3]: AUTH, A2 & ACL

I have been using Kohana recently for a project I am working on. This project requires user registration and authentication, thus I found that there is a set of modules from Wouter that were created specifically for that. A thread in the Kohana Forum can be found here.

Those modules are:
A1: A simple & safe Authentication Library
ACL: Zend_ACL completely rewritten & improved for Kohana use.
A2: Object level authorization library

Even thought those modules work fine with each other, the first module, A1, can be easily replaced by any other module that provides the authentication, like the AUTH module from Kohana itself, and I will guess that maybe OpenID might do the trick here.

But in this post what I want to share is a modified version of the nice demo provided by Wouter on how to use those modules. My demo makes use of the AUTH module for authentication. So, let’s go to the explanation.
Try the online demo Here

The module name is auth-a2-acl-demo, thus we need to add it to our config.php file (located at application/config/config.php).

1
2
3
4
MODPATH.'a2',      // Wouter's A2 module
MODPATH.'acl',      // Wouter's ACL module
MODPATH.'auth-a2-acl-demo',      // Nano's demo
MODPATH.'auth',      // Kohana Authentication

Notice that I put the demo module before the Kohana AUTH in order to have the User Model with higher priority and have the one in the demo being loaded instead of the default from the Kohana that does nothing. The main reason to do this is because our User Model implements Acl_Role_Interface (used on A2 and ACL modules).

Next we can dump the module auth-a2-acl-demo into the modules folder.

The file structure is as follows:

- config
   - auth-a2-acl-demo.php (Do not change its name unless you change its reference inside the controller too)
- controllers
   - authdemo.php (Change the name if you want but make sure you change the name of the class also. The links created internally will be updated accordingly)
-  models
   - user.php (Overwrites the default one from AUTH)
   - blog.php (For this demo, taken from original demo)

Now let’s start with the configuration file:

1
2
3
4
5
6
7
8
9
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
61
62
63
64
65
66
67
68
69
70
71
<?php
 
/*
 * The Authentication library to use
 * Make sure that the library supports:
 * 1) A get_user method that returns FALSE when no user is logged in
 *      and a user object that implements Acl_Role_Interface when a user is logged in
 * 2) A static instance method to instantiate a Authentication object
 *
 * array(CLASS_NAME,array $arguments)
 */
$config['a1'] = array('AUTH'); // For Kohana's AUTH, simply use array('AUTH');
 
/*
 * The ACL Roles (String IDs are fine, use of ACL_Role_Interface objects also possible)
 * Use: ROLE => PARENT(S) (make sure parent is defined as role itself before you use it as a parent)
 */
$config['roles'] = array
(
    'guest'  =>  NULL,
    'author' =>  'guest',
    'admin'  =>  'author' 
);
 
/*
 * The name of the guest role
 * Used when no user is logged in.
 */
$config['guest_role'] = 'guest';
 
/*
 * The ACL Resources (String IDs are fine, use of ACL_Resource_Interface objects also possible)
 * Use: ROLE => PARENT (make sure parent is defined as resource itself before you use it as a parent)
 */
$config['resources'] = array
(
    'blog'   =>  NULL
);
 
/*
 * The ACL Rules (Again, string IDs are fine, use of ACL_Role/Resource_Interface objects also possible)
 * Split in allow rules and deny rules, one sub-array per rule:
     array( ROLES, RESOURCES, PRIVILEGES, ASSERTION)
 *
 * Assertions are defined as follows :
          array(CLASS_NAME,$argument) // (only assertion objects that support (at most) 1 argument are supported
                                      //  if you need to give your assertion object several arguments, use an array)
 */
$config['rules'] = array
(
    'allow' => array
    (
        // guest can read blog
        array('guest','blog','read'),
 
        // authors can add blogs
        array('author','blog','add'),
 
        // authors can edit their own blogs (and only their own blogs)
        array('author','blog','edit',array('Acl_Assert_Argument',array('primary_key_value'=>'user_id'))),
        array('author','blog','delete',array('Acl_Assert_Argument',array('primary_key_value'=>'user_id'))),
 
        // administrators can do everything
        array('admin','blog','delete'),
        array('admin','blog','edit'),
    ),
    'deny' => array
    (
          // no deny rules in this example
    )
);

Notice the line that defines the a1 property [line 12]:
$config[‘a1’] = array(‘AUTH’);
In the original demo Wouter had A1 there.

Then we have 3 roles:

  • Guest: Any visitor
  • Author: The one that writes the post (Its parent is the Guest)
  • Admin: The superuser (its parent is the Author)

And there is only 1 resource: blog.

Personally I see the roles-parents relationship more of a hierarchy where the later defined role supersedes the previous defined one; but it works anyways.

The rules for access are as follow:
– Anybody can read
– Authors can add posts (blog resources). Because Author was the parent of Admin, this rule also infers that Admins can post blogs.
– Authors can only edit or delete their own posts (blogs).
– Admins can delete or edit any blog.

Now let’s take a look at the User Model:

1
2
3
4
5
6
7
8
9
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
<?php
 
class User_Model extends Auth_User_Model implements Acl_Role_Interface {
 
    public function get_role_id()
    {
        $roles = array();
        foreach ($this->roles as $role)
        {
            $roles[] = $role->name;
        }
        return $roles;
    }
 
    /**
     * Validates and optionally saves a new user record from an array.
     *
     * @param  array    values to check
     * @param  boolean  save the record when validation succeeds
     * @return boolean
     */
    public function validate(array & $array, $save = FALSE)
    {
        $array = Validation::factory($array)
            ->pre_filter('trim')
            ->add_rules('username', 'required', 'length[4,32]', 'chars[a-zA-Z0-9_.]', array($this, 'username_available'))
            ->add_rules('password', 'length[5,42]')
            ->add_rules('password_confirm', 'matches[password]')
            ->add_rules('email', 'length[7,127]');
            /* I didn't get how to validate the roles from the dropdown box to real roles */
            //->add_rules('roles','length[3,20]');
            //->add_rules('roles','User_Model::matches[2,3]');
            //->add_rules('role','matches[user,admin]');
 
        if ( ! $this->loaded)
        {
            // This user is new, the password must be provided
            $array->add_rules('password', 'required');
        }
 
        return ORM::validate($array, $save);
    }
 
    // A validation rule - $value should match one of the values in $args
    public static function matches($value,$args)
    {
        return in_array($value,$args);
    }
 
} // End User Model

From the Model above, what the function get_role_id does is to get all the roles from the given user in order to make all the tests for the rules (we have rules for reading, adding, editing and deleting). It is in the controller code where the tests for permissions are handled. This function is very different from the one provided in the original demo because the relationship for roles in AUTH is via a many-to-many relationship, while in the original demo is a column in the users table.

The rest is pretty much the same as original demo except that I have added a field for the email and I am not doing any validation for the roles because I failed in the attempt :). Also, my validation goes directly to ORM instead of doing a parent::validate (that should go to the Auth_User_Model in the AUTH module). The reason for this is because the validate function from Auth_User_Model does the same but it expects an array as first argument (same as our validate function) and we are sending a validation object.

The next file is the Blog Model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
 
class Blog_Model extends ORM implements Acl_Resource_Interface {
     // Relationships     protected $has_one = array('user');
     public function get_resource_id()     {
         return 'blog';
     }
     public function validate(array & $array, $save = FALSE)     {
         $array = Validation::factory($array)
                  ->pre_filter('trim')
                  ->add_rules('text','required');
 
        return parent::validate($array, $save);
    }
 
} // End Blog Model

Nothing really new here, the only change is the addition of the relationship with the User model, that way when we get each blog we can access the author (user) information transparently.

Finally, we have the controller itself. Here is the code:

1
2
3
4
5
6
7
8
9
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
<?php
 
class Authdemo_Controller extends Controller {
    private $_name;     
    public function __construct()     {
        parent::__construct();
        new Profiler;
        $this->a2 = A2::instance('auth-a2-acl-demo');
        $this->a1 = $this->a2->a1;
        $this->_name = Router::$controller;
        echo '<div style="position:absolute;top:0px;right:0px;background-color:#f0f0f0;font-weight:bold;padding:5px;">',html::anchor($this->_name.'/','index'),'-',html::anchor($this->_name.'/db','DB'),'</div>';
    }
 
    public function index()
    {
        $blogs = ORM::factory('blog')->find_all();
 
        // show user info
        echo $this->_user_info();
 
        // show blogs
        echo '<hr />';
        $user = $this->a2->get_user();
        if(count($blogs) === 0)
        {
            echo 'No blogs yet';
        }
        else
        {
            foreach($blogs as $blog)
            {
                echo $blog->text . ' by ' . $blog->user->username;
                $e = "";
                $d = "";
                if ($user) {
                    $e = ($this->a2->allowed($blog,'edit'))? html::anchor($this->_name.'/edit/'.$blog->id,'Edit') : "";
                    $d = ($this->a2->allowed($blog,'delete'))? ' - '.html::anchor($this->_name.'/delete/'.$blog->id,'Delete') : "";
                }
                echo $e,$d,'<hr />';
            }
        }
        if ($user){
          echo html::anchor($this->_name.'/add','Add');
        }
    }
 
    private function _user_info()
    {
        if (($user = $this->a2->get_user()))
        {
            $s =  '<b><i>'.$user->username.' </i></b> ' . html::anchor($this->_name.'/logout','Logout');
        }
        else
        {
            $s = '<b>Guest</b> ' . html::anchor($this->_name.'/login','Login') . ' - ' . html::anchor($this->_name.'/create','Create account');
        }
 
        return '<div style="width:100%;padding:5px;background-color:#AFB6FF;">' . $s . '</div>';
    }
 
    public function create()
    {
        if ($this->a2->logged_in()) //cannot create new accounts when a user is logged in
        {
            $this->index();
        }
 
        // Create a new user
        $user = ORM::factory('user');
        $post = $_POST;
 
        // Do not auto-save, we need to add the roles properly
        if ($user->validate($post))
        {
            // Saving login role and the one coming from dropdown menu
            $user->roles = array(1, $post->role);
            $user->save();
            // user  created, show login form
            $this->login();
        }
        else
        {
           $errors = $post->errors();
           if (count($errors) > 0) {
                echo 'Validation errors were found';
                foreach ($errors as $key => $val)
                {
                echo $key.' failed rule '.$val;
                }
           }
            //show form
            echo form::open();
            echo 'username:' . form::input('username');
            echo 'password:' . form::password('password');
            echo 'password confirm:' . form::password('password_confirm');
            echo 'email:' . form::input('email');
            echo 'role:' . form::dropdown('role',array('3'=>'author','2'=>'admin'));
            echo form::submit(array('value'=>'create'));
            echo form::close();
        }
 
        echo Kohana::debug($post->as_array(),$user->as_array());
    }
 
    public function login()
    {
        if($this->a2->logged_in()) //cannot create new accounts when a user is logged in
        {
            return $this->index();
        }   
 
        $post = Validation::factory($_POST)
            ->pre_filter('trim')
            ->add_rules('username', 'required', 'length[4,127]')
            ->add_rules('password', 'required');
 
        if($post->validate())
        {
            if($this->a1->login($post['username'],$post['password'], isset($_POST['remember']) ? (bool) $_POST['remember'] : FALSE))
            {
                url::redirect( $this->_name.'/index' );
            }
        }
 
        //show form
        echo form::open();
        echo 'username:' . form::input('username');
        echo 'password:' . form::password('password');
        echo 'remember me:' . form::checkbox('remember',TRUE);
        echo form::submit(array('value'=>'login'));
        echo form::close();
    }
 
    public function logout()
    {
        $this->a1->logout();
        return $this->index();
    }
 
    public function add()
    {
        if(!$this->a2->allowed('blog','add'))
        {
            echo '<b>You are not allowed to add blogs</b>';
            return $this->index();
        }
 
        $blog = ORM::factory('blog');
        $this->_editor($blog);
    }
 
    public function edit($blog_id = "")
    {
        $blog = ORM::factory('blog',$blog_id);
 
        // NOTE the use of the actual blog object in the allowed method call!
        if(!$this->a2->allowed($blog,'edit'))
        {
            echo '<b>You are not allowed to edit this blog</b>';
            return $this->index();
        }
 
        $this->_editor($blog);
    }
 
    private function _editor($blog)
    {
        if($blog->validate($_POST))
        {
            $blog->user_id = (empty($blog->user_id))? $this->a2->get_user()->id : $blog->user_id;
            $blog->save();
            return $this->index();
        }
 
        //show form
        echo form::open();
        echo 'text:' . form::textarea('text',$blog->text);
        echo form::submit(array('value'=>'post'));
        echo form::close();
    }
 
    public function delete($blog_id = "")
    {
        $blog = ORM::factory('blog',$blog_id);
 
        // NOTE the use of the actual blog object in the allowed method call!
        if(!$this->a2->allowed($blog,'delete'))
        {
            echo '<b>You are not allowed to delete this blog</b>';
        }
        else
        {
            $blog->delete();
        }
 
        $this->index();
    }
 
    public function db()
    {
        echo '<b>Mysql DB structure</b><hr />';
 
        echo "&lt;pre>
        CREATE TABLE IF NOT EXISTS `roles` (
          `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
          `name` varchar(32) NOT NULL,
          `description` varchar(255) NOT NULL,
          PRIMARY KEY  (`id`),
          UNIQUE KEY `uniq_name` (`name`)
        ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
 
        INSERT INTO `roles` (`id`, `name`, `description`) VALUES(1, 'login', 'Login privileges, granted after account confirmation');
        INSERT INTO `roles` (`id`, `name`, `description`) VALUES(2, 'admin', 'Administrative user, has access to everything.');
        INSERT INTO `roles` (`id`, `name`, `description`) VALUES(3, 'author', 'Can create posts. Delete and edit own posts.');
 
        CREATE TABLE IF NOT EXISTS `roles_users` (
          `user_id` int(10) UNSIGNED NOT NULL,
          `role_id` int(10) UNSIGNED NOT NULL,
          PRIMARY KEY  (`user_id`,`role_id`),
          KEY `fk_role_id` (`role_id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
        CREATE TABLE IF NOT EXISTS `users` (
          `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
          `email` varchar(127) NOT NULL,
          `username` varchar(32) NOT NULL DEFAULT '',
          `password` char(50) NOT NULL,
          `logins` int(10) UNSIGNED NOT NULL DEFAULT '0',
          `last_login` int(10) UNSIGNED,
          PRIMARY KEY  (`id`),
          UNIQUE KEY `uniq_username` (`username`),
          UNIQUE KEY `uniq_email` (`email`)
        ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
 
        CREATE TABLE IF NOT EXISTS `user_tokens` (
          `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
          `user_id` int(11) UNSIGNED NOT NULL,
          `user_agent` varchar(40) NOT NULL,
          `token` varchar(32) NOT NULL,
          `created` int(10) UNSIGNED NOT NULL,
          `expires` int(10) UNSIGNED NOT NULL,
          PRIMARY KEY  (`id`),
          UNIQUE KEY `uniq_token` (`token`),
          KEY `fk_user_id` (`user_id`)
        ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
 
        ALTER TABLE `roles_users`
          ADD CONSTRAINT `roles_users_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE,
          ADD CONSTRAINT `roles_users_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE;
 
        ALTER TABLE `user_tokens`
          ADD CONSTRAINT `user_tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
 
        CREATE TABLE IF NOT EXISTS `blogs` (
          `id` int(12) unsigned NOT NULL auto_increment,
          `user_id` int(12) unsigned NOT NULL,
          `text` text NOT NULL,
          PRIMARY KEY  (`id`),
          KEY `user_id` (`user_id`)
        ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
 
        ALTER TABLE `blogs`
          ADD CONSTRAINT `blogs_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE;
        &lt;/pre>";
    }
 
}

The controller gets at the __construct method its own name so it can add it to all the anchors created and make it easier to change the name of the controller and reflect the links changes automatically. That’s done in this line
$this->_name = Router::$controller;

Another nice difference is that I checked if the rules for edit and delete are satisfactory for the given user. If they are, the anchors are showed properly underneath the blog.

Also, I added the username of the user (author) that published the post (blog). This was done because the Blog Model included the relationship has_one.

In addition to create users in the user table, AUTH requires the users to have the login role present in the many-to-many relationship between users and roles. For that I added the login role (id 1) to the user in addition to the value selected in the dropdown (id 2 or 3). In a production environment you might like to have an email sent to the user in order to validate the account, thus, when the account is created just add the user with a validation token and only when the verification step is done, add the login role for that user.

Because the _editor function is shared by add and edit methods, when editing the blogs I had to add a check to make sure no change of ownership of the blog happens.

Finally, at the end of the controller an updated DB schema is provided that uses the AUTH schema plus the blog table.

To play with the module go to http://localhost/index.php/authdemo (assuming you have Kohana installed in the root folder of your webserver) or try the online demo I have setup Here

Enjoy and thanks to Wouter and AUTH team for their great modules.



Thank you for reading this post. You can now Read Comments (4) or Leave A Trackback.

Post Info

This entry was posted on Monday, September 21st, 2009 and is filed under kohana, PHP.

You can follow any responses to this entry through the Comments Feed. You can Leave A Comment, or A Trackback.



Previous Post: »
Next Post: »

Read More

Related Reading:

4 Responses to “Kohana[v2.3]: AUTH, A2 & ACL

  • 1
    JustLikeIcarus
    September 23rd, 2009 22:44

    Wouter’s github page directed me here. You basically answered every question I had about implementing a2/acl with the AUTH module. Awesome work!

  • 2
    towfx
    October 11th, 2009 19:45

    Just to say thanks for the post.

    Not much documentation around for ACL.

  • 3
    ignas
    October 12th, 2009 00:48

    hi,

    I have an error :

    Please check the Kohana documentation for information about the following error.

    C:/xampp/htdocs/kohana/system/libraries/ORM.php [364]:

    The role property does not exist in the User_Model class.

    everythink i have done by your example, but this error kills my, dont know how to solve it.

    Thanks

    Response: Are you sure you have changed the Model? I remember I had that error a lot because the DB had the field role defined on it (from the A1 DB schema). Remember that the order of the modules is very important. Please check your models, and let me know if you still have the problem. Thanks, Nano.

  • 4
    joper
    September 21st, 2010 20:39

    Fatal error: Class ‘A2’ not found in C:\wamp\www\blog_2\application\controllers\authdemo.php on line 11

    how can i solve this?need response..thanks..



Leave a Reply

Note: Any comments are permitted only because the site owner is letting you post, and any comments will be removed for any reason at the absolute discretion of the site owner.