Kohana 3: AUTH, A2 & ACL

Some time ago I wrote a blog entry a modified demo of Wouter’s ACL & A2 using the AUTH module. At that time I used Kohana 2.3.4.

Now, this entry is to explain was needs to be changed to use the AUTH module instead of the A1 module for the demo of Wouter for the Kohana 3 release. At the same time, it can help you to understand what needs to be changed if you want to provide your own authentication module. The original announcement of Wouter’s module can be find at this Kohana forum discussion.

Keep reading for the explanation

Step 1
You need to install configure your KO3 application:
application/bootstrap.php

...
/**
 * Enable modules. Modules are referenced by a relative or absolute path.
 */
Kohana::modules(array(
     'A2'         => MODPATH.'A2',  // Wouter's module
     'ACL'        => MODPATH.'ACL',  // Wouter's module
     'A2ACLdemo'  => MODPATH.'A2ACLdemo',  // Nano's module
     'auth'       => MODPATH.'auth',       // Basic authentication
     'database'   => MODPATH.'database',   // Database access
     'orm'        => MODPATH.'orm',        // Object Relationship Mapping
));

Notice that A2ACLdemo is placed before the auth module, this is important to apply the right priorities for the Kohana cascading filesystem. The user model is the one that will be kept for our demo. More in the next sections.

Step 2
Setup your configuration files properly. In my case, the config files are placed at /application/config/ and they are for database (database credentials) and auth (for the authentication).

Step 3
Download my demo. Unzip it and place the folder contents in a folder under the /modules folder. Make sure the folder name matches the one put in the bootstrap.php file; if you don’t change the name the code snippet shown above should work.

Step 4
The files in the module are as follow:

- classes
  - controller
    - a2demo.php (The modified controller)
    - acldemo.php (NOT USED for this demo)
  - model
    - blog.php (Used, but not modified)
    - user.php (The modified model)
  - zend (NOT USED for this demo)
- config
  - a2-demo (The modified configuration file)

Step 5
Now, let’s explain each modified file starting by the configuration file (a2-demo)

<!--?php return array( /* * 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) */ 'lib' =&gt; array(
        'class'  =&gt; 'AUTH', // (or AUTH)
        'params' =&gt; array('auth')
      ),
 
	'exception' =&gt; FALSE,
 
	/*
	 * The ACL Roles (String IDs are fine, use of ACL_Role_Interface objects also possible)
	 * Use: ROLE =&gt; PARENT(S) (make sure parent is defined as role itself before you use it as a parent)
	 */
	'roles' =&gt; array
	(
	    'author' =&gt; 'guest',
            'admin'  =&gt; 'author'
	),
 
	/*
	 * The name of the guest role 
	 * Used when no user is logged in.
	 */
	'guest_role' =&gt; 'guest',
 
	/*
	 * The ACL Resources (String IDs are fine, use of ACL_Resource_Interface objects also possible)
	 * Use: ROLE =&gt; PARENT (make sure parent is defined as resource itself before you use it as a parent)
	 */
	'resources' =&gt; array
	(
		'blog' =&gt; 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)
	 */
	'rules' =&gt; array
	(
		'allow' =&gt; array
		(
			// guest can read blog
			'guest1' =&gt; array(
				'role'      =&gt; 'guest',
				'resource'  =&gt; 'blog',
				'privilege' =&gt; 'read'
			),
 
			// authors can add blogs
			'author1' =&gt; array(
				'role'      =&gt; 'author',
				'resource'  =&gt; 'blog',
				'privilege' =&gt; 'add'
			),
 
			// authors can edit &amp; delete their own blogs (and only their own blogs)
			'author2' =&gt; array(
				'role'      =&gt; 'author',
				'resource'  =&gt; 'blog',
				'privilege' =&gt; array('edit','delete'),
				'assertion' =&gt; array('Acl_Assert_Argument',array('id'=&gt;'user_id'))
			),
 
			// administrators can delete and edit everything 
			'admin1' =&gt; array(
				'role'      =&gt; 'admin',
				'resource'  =&gt; 'blog',
				'privilege' =&gt; array('delete', 'edit')
			),
		),
		'deny' =&gt; array
		(
			  // no deny rules in this example
		)
	)
);
&lt;/pre&gt;
&lt;p&gt;The changes include setting AUTH as the authentication module to be used (in contrast to A1 for Wouter's demo). The other only difference I think I have compared to the original demo is that I have the admins the option to also edit posts (blogs).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 6&lt;/strong&gt;&lt;br ?-->
Now is the turn of the controller. Notice that if you have Kohana configured with the default route, then you don't need to have any special route, the module will work out of the box by calling the http://www.mysite.com/a2demo.
In my case, I have the kohana pointing to the ko3 folder http://www.nanodocumet.com/ko3/a2demo, by setting that in the bootstrap.php file by setting <a href="http://v3.kohanaphp.com/guide/api/Kohana#init">the base_url entry of the Kohana::init</a> and setting the right <a href="http://kerkness.ca/wiki/doku.php?id=removing_the_index.php">RewriteBase entry</a>.
&lt;?php
 
class Controller_A2demo extends Controller {
 
	private $_name;
 
        public function before()
	{
	        $this-&gt;a2 = A2::instance('a2-demo');
		$this-&gt;a1 = $this-&gt;a2-&gt;a1;
		$this-&gt;_name = Request::instance()-&gt;controller;
 
		$this-&gt;user = $this-&gt;a2-&gt;get_user();
 
		echo 'This demo uses KO3. For demo using Kohana 2.3.4 go &lt;a href="http://www.nanodocumet.com/kohana/authdemo/"&gt;here&lt;/a&gt;';
		echo '&lt;div style="position:absolute;top:0px;right:10px;background-color:#f0f0f0;font-weight:bold;padding:5px;"&gt;',
                     html::anchor($this-&gt;_name.'/','index'),'-',
		     html::anchor(Request::instance()-&gt;uri.'?profiler=true','Show profiler'),'-',
		     html::anchor($this-&gt;_name.'/db','DB'),'&lt;/div&gt;';
	}
 
	public function after()
	{
	    // Showing the profiler if using debug mode
	    if (!empty($_GET['profiler']))
	        echo View::factory('profiler/stats');	    
	}
 
	public function action_index()
	{
        $blogs = ORM::factory('blog')-&gt;find_all();
 
        // show user info
        echo $this-&gt;user_info();
 
        // show blogs
        echo '&lt;hr /&gt;';
 
        if(count($blogs) === 0)
        {
            echo 'No blogs yet&lt;br /&gt;';
        }
        else
        {
            foreach($blogs as $blog)
            {
                echo $blog-&gt;text . ' by &lt;strong&gt;&lt;i&gt;' . $blog-&gt;user-&gt;username . '&lt;/i&gt;&lt;/strong&gt;&lt;br /&gt;';
                $e = "";
                $d = "";
                if ($this-&gt;user) {
                    $e = ($this-&gt;a2-&gt;allowed($blog,'edit'))? html::anchor($this-&gt;_name.'/edit/'.$blog-&gt;id,'Edit') : "";
                    $d = ($this-&gt;a2-&gt;allowed($blog,'delete'))? ' - '.html::anchor($this-&gt;_name.'/delete/'.$blog-&gt;id,'Delete') : "";
                }
                echo $e,$d,'&lt;hr /&gt;';
            }
        }
        if ($this-&gt;user){
          echo html::anchor($this-&gt;_name.'/add','Add');
        }
	}
 
	private function user_info()
	{
        $s = "Use admin:admin (username:password) for Admin role. Use author:author (username:password) for Author role &lt;br /&gt;";
        if( $this-&gt;user )
        {
            $s .=  '&lt;b&gt;&lt;i&gt;'.$this-&gt;user-&gt;username.' &lt;/i&gt;&lt;/b&gt; ' . html::anchor($this-&gt;_name.'/logout','Logout');
        }
        else
        {
            $s .= '&lt;b&gt;Guest&lt;/b&gt; ' . html::anchor($this-&gt;_name.'/login','Login') . ' - ' . html::anchor($this-&gt;_name.'/create','Create account');
        }
 
		return '&lt;div style="width:95%;padding:5px;background-color:#AFB6FF;"&gt;' . $s . '&lt;/div&gt;';
	}
 
	public function action_create()
	{
	    if($this-&gt;user) //cannot create new accounts when a user is logged in
			$this-&gt;action_index();
 
		$post = $_POST;
 
		// Create a new user
		$user = ORM::factory('user')
			-&gt;values($post);
 
		if ($user-&gt;check())
		{
		       // Saving login role and the one coming from dropdown menu
                       $user-&gt;save();
                       $user-&gt;add('roles', ORM::factory('role', array('name' =&gt; 'login')));
                       $user-&gt;add('roles', ORM::factory('role', array('id' =&gt; $post['role'])));
 
			// user  created, show login form
			$this-&gt;action_login();
		}
		else
		{
		    //show form
                   echo form::open();
                   echo 'username:' . form::input('username') . '&lt;br /&gt;';
                   echo 'password:' . form::password('password') . '&lt;br /&gt;';
                   echo 'password confirm:' . form::password('password_confirm') . '&lt;br /&gt;';
                   echo 'email:' . form::input('email') . '&lt;br /&gt;';
                   echo 'role:' . form::select('role',array('3'=&gt;'author','2'=&gt;'admin')) . '&lt;br /&gt;';
                   echo form::submit('create','create');
                   echo form::close();
 
                  echo Kohana::debug($user-&gt;validate()-&gt;errors());
		}
	}
 
	public function action_login()
	{
		if($this-&gt;user) //cannot create new accounts when a user is logged in
			return $this-&gt;action_index();
 
		$post = Validate::factory($_POST)
			-&gt;filter(TRUE,'trim')
			-&gt;rule('username', 'not_empty')
			-&gt;rule('username', 'min_length', array(4))
			-&gt;rule('username', 'max_length', array(127))
			-&gt;rule('password', 'not_empty');
 
		if($post-&gt;check())
		{
			if($this-&gt;a1-&gt;login($post['username'],$post['password'], isset($_POST['remember']) ? (bool) $_POST['remember'] : FALSE))
			{
				$this-&gt;request-&gt;redirect($this-&gt;_name.'/index' );
			}
		}
 
		//show form
		echo form::open();
		echo 'username:' . form::input('username') . '&lt;br&gt;';
		echo 'password:' . form::password('password') . '&lt;br&gt;';
		echo 'remember me:' . form::checkbox('remember',TRUE) . '&lt;br&gt;';
		echo form::submit('login','login');
		echo form::close();
		echo Kohana::debug($post-&gt;errors());
 
	}
 
	public function action_logout()
	{
		$this-&gt;a1-&gt;logout();
		$this-&gt;user = NULL;
		return $this-&gt;action_index();
	}
 
	public function action_add()
	{
		if(!$this-&gt;a2-&gt;allowed('blog','add'))
		{
			echo '&lt;b&gt;You are not allowed to add blogs&lt;/b&gt;&lt;br&gt;';
			return $this-&gt;action_index();
		}
 
		$blog = ORM::factory('blog');
 
		$this-&gt;editor($blog);
	}
 
	public function action_edit($blog_id)
	{
		$blog = ORM::factory('blog',$blog_id);
 
		// NOTE the use of the actual blog object in the allowed method call!
		if(!$this-&gt;a2-&gt;allowed($blog,'edit')) 
		{
			echo '&lt;b&gt;You are not allowed to edit this blog&lt;/b&gt;&lt;br&gt;';
			return $this-&gt;action_index();
		}
 
		$this-&gt;editor($blog);
	}
 
	private function editor($blog)
	{
		if(count($_POST))
		{
			$blog-&gt;values($_POST);
 
			if($blog-&gt;check())
			{
				// Check if user_id is not empty, that means an admin is editing
				$blog-&gt;user_id = (empty($blog-&gt;user_id))? $this-&gt;a2-&gt;get_user()-&gt;id : $blog-&gt;user_id;
				$blog-&gt;save();
				return $this-&gt;action_index();
			}
		}
 
		//show form
		echo form::open();
		echo 'text:' . form::textarea('text',$blog-&gt;text) . '&lt;br&gt;';
		echo form::submit('post','post');
		echo form::close();
		echo Kohana::debug($blog-&gt;validate()-&gt;errors());
 
	}
 
	public function action_delete($blog_id)
	{
		$blog = ORM::factory('blog',$blog_id);
 
		// NOTE the use of the actual blog object in the allowed method call!
		if(!$this-&gt;a2-&gt;allowed($blog,'delete')) 
		{
			echo '&lt;b&gt;You are not allowed to delete this blog&lt;/b&gt;&lt;br&gt;';
		}
		else
		{
			$blog-&gt;delete();
		}
 
		$this-&gt;action_index();
	}
 
	public function action_db()
	{
		echo '&lt;b&gt;Mysql DB structure&lt;/b&gt;&lt;hr&gt;';
 
		echo "&lt;pre&gt;
        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&gt;";
	}
 
}

The most important information from this controller is how to make the demo to create users under AUTH module. That is achieved by adding the proper roles to the roles_users table (the relationship information is set at the model). After saving the newly created user (line 91) we need to add the roles: “login” role and the role from the select box (lines 92 and 93). I have to admit that I have not taken a look of the AUTH module for KO3 module fully, thus, I am not sure if the login role is still required. The other role (author or admin) will allow the users to do the different actions set at the config file.

Step 7
The last file is the user model.

&lt;?php class Model_User extends Model_Auth_User implements Acl_Role_Interface { public function get_role_id() { $roles = array(); // We need to explicitly call find_all() to get the roles. Lazy loading... $my_roles = $this-&gt;roles-&gt;find_all();
        foreach ($my_roles as $role)
        {
            // Now grab the names of the roles. Used on the ACL module
            $roles[] = $role-&gt;name;
        }
        return $roles;
    }
 
} // End User Model

The 2 big changes are the class name and the get_role_id function. The class name extends the Model_Auth_User instead of Model_A1_ORM_User. The get_role_id function is needed to have the ACL working properly and it needs to return the names of the roles for the given user that need to match the roles defined in the configuration file (see Step 5). Notice that those roles are the ones defined in the database, thus, make sure that when you are developing your application, you keep the configuration file and role values in the database in sync.

Notice that I didn’t have to define any of the relationships with other tables or validation rules or callback functions as done in original Wouter’s user model; the reason behind it is that this user model extends from the Model_Auth_User (defined at modules/auth/classes/model/auth/user.php) which includes many of these features (unique username and email, change password functionality, etc). Feel free to replace any method or add your own for the user model if the ones provided at Model_Auth_User are not for you.

Well, I think, that’s it. Play with it and enjoy!



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

12 Responses to “Kohana 3: AUTH, A2 & ACL

  • 1
    Igor D.
    November 16th, 2009 18:43

    Hi. I am active user of Kohana 2x where I developed my own Auth system. Recently I decided to cross over to Kohana 3x. I like it very much and I also like your solution but for the time being I cannot get it to work. I downloaded your demo files and placed it in kohana 3.0.1.2 modules, I also downloaded A2 and ACL from Git hub here http://github.com/Wouterrr/ and placed them in modules and changed bootstrap as you suggested.
    This is the error:
    ErrorException [ Warning ]: Missing argument 1 for Acl_Assert_Argument::__construct(), called in D:\HTDOCS\ko3\modules\A2\classes\a2.php on line 138 and defined

    Is there any chance you can upload full KO3 installation somewhere with functional A2ACLdemo module for me/us to make quick start with it? It will be much appreciated!
    Thank you.

    Response: Have you played around with the configuration file? It seems that this rule is not being applied correctly:
    ‘assertion’ => array(‘Acl_Assert_Argument’,array(‘id’=>’user_id’))
    I will recommend you post a discussion at the Kohana Forum: http://forum.kohanaphp.com.

  • 2
    Igor D.
    November 16th, 2009 18:46

    Note – this is what I get when I call “a2demo” controller. I have created database, config files are in place and everything should be ok. My best guess is that I am not using correct A2 and ACL modules…

  • 3
    Igor D.
    November 17th, 2009 00:58

    Well, I have tried without much results – anyway it was late night – I will try more when I get back to work. At my work we are using KO2 in production environment and I will play with KO3 and ACL and see what happens.
    As I can see you have modified original sources and you have announced that you will post it on GCode when you have time to do so. Should I post to that part of the topic or make new one?
    Again, and I know I am stubborn, is there any chance you can upload fully functional KO3 installation with functional ACL and A2Demo please, please, please 🙂

  • 4
    chibo xl
    December 7th, 2009 03:43

    Hi,i was wondering whether you can demonstrate this.A registered user who has not yet logged in,is accessing a page as guest but wait,he wants to access an page that needs authentication.So i want a situation where by i will return the user(guest) to the page he was now that he is authenticated.

  • 5
    Pappleton
    January 3rd, 2010 14:33

    I can confirm this does work. I had to jump some hurdles. I’m not sure if they were all necessary but here they are I hope they can act as a check list:

    1. You have to download all the WouterACL A2, A1 files from git
    2. Download Nano’s ACLA2Demo files form the original.
    3. Cut and paste the changes from this blog – the extends have changed.
    4. Ensure the database tables are set up with the right data and columns.
    5 Copy all config files to the application/config
    6. Read Nano’s text carefully eg get modules in right order.

    There did seem to be a problem with the config/a2-demo.php

    I removed this line (and the comma before it:

    ‘assertion’ => array(‘Acl_Assert_Argument’,array(‘id’=>’user_id’))

    (not sure if this is a my problem or kohana problem or an example problem but it seemed to at least get the example working)

    Great work By Nano and the Kohana team, this is all new to me too so apologies if I stated the obvious.

    [Nano]: Thanks for the write-up, sure will help other people too.

  • 6
    akbass
    January 9th, 2010 06:14

    Nano,

    this looks like a great set of tools to use. I am trying to get this to work as described, but I continually get the error “Undefined index: password_confirm” when I try to create a user. any Idea what may be going on?

    thanks!

    [Nano said: It seems that when you are creating the user you don’t have an input called password_confirm (password type). Do you have any sample code? The best place to answer your question will be at the kohana forums]

  • 7
    zdl
    January 29th, 2010 23:32

    how can i get the result like this:
    username must not be empty
    but not the result like your array.

  • 8
    Juice
    February 12th, 2010 07:01

    Oh, I’m fool!!!!
    “right order”!
    damn!
    I had error Missing argument 1 for Acl_Assert_Argument too.
    I have bypassed all forums, but could not find the ready answer anywhere =)
    But now, as it seems to me, I have found the reason. Faster the working day would be completed and i will check up this guess at home =))
    Forgive for my bad English =)

  • 9
    Zack Live
    February 26th, 2010 21:40

    Say Hello to Kohana 3…

    Kohana 3 has been released for a while. This is a redesign version of Kohana. Kohana 3 and 2 will both be in active, if you are new to Kohana, I suggest you to start with Kohana 3. The most important difference between these two version is that Kohana …

  • 10
    fabian
    March 7th, 2010 02:57

    Hi,
    I was wondering if this example really makes use of A2. The config sounds like using AUTH???

    ‘lib’ => array(
    ‘class’ => ‘AUTH’, // (or AUTH)
    ‘params’ => array(‘auth’)

    Response [Nano] The example is to use AUTH instead of A2, for A2 use the original demo

  • 11
    jig
    April 18th, 2010 23:26

    I am using above given code with kohana but i am getting below given error

    Fatal error: Cannot redeclare class Controller_A2demo in E:\Apache\www\sw\Kohana\application\controllers\a2demo.php on line 290

  • 12
    Pedma
    June 25th, 2010 08:03

    Hi,

    Nice tutorial, Thanks. I have problem of creating new account.

    – I have downloaded the A1, A2, ACL, A2ACLdemo and put them to …/www/modules directory.
    – put database.php & auth.php to …./application/config
    – edit database.php to match database user, passwd etc
    – create the database, which exist in file a2demo.php
    – I can see : Login – Create Account Page
    – As I tried to create account, it goes back to the same page. And in Database, I didn’t see any data in table ‘users’
    – Then I see at your demo site : http://www.nanodocumet.com/ko3/a2demo/create
    There is an EMAIL form at your demo page.
    – At my local server, there is NO email form.

    May be it’s the problem ?

    How to solve this ?

    Thanks
    – pedma –



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.