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.
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).
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:
<!--?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 ) ); </pre> <p>Notice the line that defines the a1 property [line 12]:<br ?--><strong>$config['a1'] = array('AUTH'); </strong> 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:
‘; } public function index() { $blogs = ORM::factory(‘blog’)->find_all(); // show user info echo $this->_user_info(); // show blogs echo ‘
‘; $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,’
‘; } } if ($user){ echo html::anchor($this->_name.’/add’,’Add’); } } private function _user_info() { if (($user = $this->a2->get_user())) { $s = ‘‘.$user->username.’ ‘ . html::anchor($this->_name.’/logout’,’Logout’); } else { $s = ‘Guest ‘ . html::anchor($this->_name.’/login’,’Login’) . ‘ – ‘ . html::anchor($this->_name.’/create’,’Create account’); } return ‘
‘; } 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 ‘You are not allowed to add blogs‘; 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 ‘You are not allowed to edit this blog‘; 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 ‘You are not allowed to delete this blog‘; } else { $blog->delete(); } $this->index(); } public function db() { echo ‘Mysql DB structure
‘; echo “<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; </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: Real NAT on Windows XP »
Next Post: Kohana 3: AUTH, A2 & ACL »
- How to connect to Mysql using SSH in Windows
- Announcing a new PHP DICOM toolkit: Nanodicom
- Nuevo Portal LoteriaFutbol.com: La Polla del Mundial Sudáfrica 2010.
- Kohana 3: AUTH, A2 & ACL
- Kohana[v2.3]: AUTH, A2 & ACL
- Real NAT on Windows XP
- New home for this package
- File_DICOM.php update
- The unofficial File_DICOM.php v 0.3
- DICOM php
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!
October 11th, 2009 19:45
Just to say thanks for the post.
Not much documentation around for ACL.
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.
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..