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 "<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 (3) or Leave A Trackback.
Post Info
This entry was posted on Monday, September 21st, 2009 and is filed under PHP, kohana.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 »
- 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
- Cookieless Authentication – Client side
- Cookieless authentication (part 2)



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.