PradoSoft

Database authentication tutorial

From PRADO Wiki

We are going to change how PRADO autentification/authorization scheme works. Instead of getting user/role permissions from static .xml files we are going to make our permission engine database driven and so easily modifiable and dynamic.

We will use following conseptions:

  • User - actual user that belongs to some group.
  • Group - a group of users.
  • Page - page path for PageService.
  • Accessor - class property which uses getXXX, setXXX methods.

Our engine will have:

  • Global authorization rules.
  • Per page authorization.

Contents

Intro

Basically we are going to alter 3 classes:

  • TUserManager - normally this holds user database, but we are going to ditch that and use our DB.
  • TUser - a user instance which is saved in session.
  • TAuthManager - we're going to extend this one to provide custom authorization.

For sake of convenience I'm prefixing all my classes with 'M' which stands for My.

Every class which I'm writing is being put under protected/Engine which resolves to the Application.Engine namespace.

SQL

We'll use following SQL.

User table - all user information will be held here:

 
CREATE TABLE `users` (
  `id` smallint(5) UNSIGNED NOT NULL AUTO_INCREMENT,
  `group_id` smallint(5) UNSIGNED NOT NULL,
  `person_id` smallint(5) UNSIGNED DEFAULT NULL,
  `name` varchar(32) collate utf8_unicode_ci NOT NULL,
  `password` varchar(64) collate utf8_unicode_ci DEFAULT NULL,
  KEY `id` (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=3 ;

Group table:

 
CREATE TABLE `groups` (
  `id` smallint(5) UNSIGNED NOT NULL AUTO_INCREMENT,
  `name` varchar(32) collate utf8_unicode_ci NOT NULL,
  PRIMARY KEY  (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=3 ;

Permissions table:

 
CREATE TABLE `permissions` (
  `id` smallint(5) UNSIGNED NOT NULL AUTO_INCREMENT,
  `page` varchar(64) collate utf8_unicode_ci DEFAULT NULL,
  `selector` enum('user_id','group_id') collate utf8_unicode_ci DEFAULT NULL,
  `value` smallint(5) UNSIGNED DEFAULT NULL,
  `allowed` enum('','1') collate utf8_unicode_ci NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `selector` (`selector`,`value`,`page`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=27 ;

Few notes about this one. page can be NULL - rule becomes global then. allowed is boolean value, '' means FALSE, '1' means TRUE (try '(string)FALSE, (string)TRUE' yourself).

TUserManager

Our class will have 4 accessors:

  • DBHandler - application module which will provide database access. I'm using PEAR_DB for that.
  • UserTable - DB table with user data.
  • GroupTable - DB table with group data.
  • GuestName - username of guest user (default: Guest).

I'm putting commented code down there, you should read it (and comments too :P).

 
<?php
/**
 * @author Artūras Šlajus <x11@arturaz.afraid.org>
 * @license http://creativecommons.org/licenses/LGPL/2.1/ CC-GNU LGPL
 */
 
// We'll need our user class later.
Prado::using('Application.Engine.MTUser');
 
// Every user manager should implement IUserManager.
class MTUserManager extends TModule implements IUserManager {
	
	// This basically checks if all needed properties are set and calls 
	// parent init method.
	public function init($config) {		
		if (! $this->_DBHandler)
			throw new TConfigurationException(
				'You must set DBHandler property!'
			);
		if (! $this->_UserTable)
			throw new TConfigurationException(
				'You must set UserTable property!'
			);
		if (! $this->_GroupTable)
			throw new TConfigurationException(
				'You must set GroupTable property!'
			);
		
		parent::init($config);
	}
 
	// validateUser() authentificates given $name and $password against DB.
 
	/**
	 * @see TUserManager::validateUser()
	 */
	public function validateUser($name, $password) {
		// NULL or empty values are not allowed
		if (is_null($password) || $password === '')
			return FALSE;
			
		// MD5 and cleartext are not safe! :-)
		$password = sha1($password);		
		$db = $this->Application->getModule($this->_DBHandler);
		
	        // We get our data from DB.
		$row = $db->getRow(
			'SELECT `name`, `password` ' .
			'FROM `!` ' .
			'WHERE `name`=? AND `password`=?',
			array(
				$this->_UserTable,
				$name,
				$password
			)
		);
		
		// NULL means we have disabled account
		if (is_null($row['password']))
			return FALSE;
		
		// Additional checking (in case of SQL injection)
		return ($row['name'] == $name && $row['password'] == $password);
	}
 
	// After validateUser() this method is called by TAuthManager.
	// It simply instantiates and returns instance of IUser.
	/**
	 * @see TUserManager::getUser()
	 */
	public function getUser($name = NULL) {
		// If no username was given then it must be guest.
		if (is_null($name)) {
			$user = new MTUser($this);
			$user->IsGuest = TRUE;
			return $user;
		}
		else {
			$db = $this->Application->getModule($this->_DBHandler);
			
			// Get data from DB.
			$row = $db->getRow(
				'SELECT ' .
					'`u`.`id`,' .
					'`u`.`group_id`,' .
					'`g`.`name` ' .
					
				'FROM `!` as `u`  ' .
								
				'LEFT JOIN ' .
					'`!` as `g` ' .
				'ON ' .
					'`u`.`group_id`=`g`.`id` ' .
					
				'WHERE `u`.`name`=? ',				
				array(
					$this->_UserTable,
					$this->_GroupTable,
					$name
				)
			);
 
			// If we have such user in DB then create new User and
			// return it.
			if ($row) {
				$user = new MTUser($this);
				$user->IsGuest = FALSE;
				$user->Name = $name;
				$user->Group = $row['name'];
				$user->UserID = $row['id'];
				$user->GroupID = $row['group_id'];
				return $user;
			}
			else
				return NULL;
		}
	}
	
	// Simply replace $user with guest user instance.
 
	/**
	 * @see TUserManager::switchToGuest()
	 */
	public function switchToGuest($user) {
		$user = $this->getUser();
	}	
	
	// Just accessors below. One thing to mention: setDBHandler checks if
	// passed module name really represents application module.
 
	private $_DBHandler;
	/**
	 * @return string DB handler ID
	 */
	public function getDBHandler() {
		return $this->_DBHandler;
	}
	/**
	 * @param string DB handler ID
	 */
	public function setDBHandler($DBHandler) {
		$DBHandler = TPropertyValue::ensureString($DBHandler);
		
		if (is_null($this->Application->getModule($DBHandler)))
			throw new TConfigurationException(
				"No module with such ID: $DBHandler"
			);
		
		$this->_DBHandler = $DBHandler;
	}
	
	private $_UserTable;
	/**
	 * @return string user table
	 */
	public function getUserTable() {
		return $this->_UserTable;
	}
	/**
	 * @param string user table
	 */
	public function setUserTable($UserTable) {
		$this->_UserTable = TPropertyValue::ensureString($UserTable);
	}
	
	private $_GroupTable;
	/**
	 * @return string group table
	 */
	public function getGroupTable() {
		return $this->_GroupTable;
	}
	/**
	 * @param string group table
	 */
	public function setGroupTable($GroupTable) {
		$this->_GroupTable = TPropertyValue::ensureString($GroupTable);
	}
	
	/**
	 * @var String Default value for GuestName
	 */
	private $_GuestName = 'Guest';
	
	/**
	 * @return String GuestName
	 */
	public function getGuestName() {
		return $this->_GuestName;
	}
	/**
	 * @param String GuestName
	 */
	public function setGuestName($value) {
		$this->_GuestName = TPropertyValue::ensureString($value);
	}
	
	
}
?>

TUser

Next target - TUser. It will hold all the information about current user in session.

Basically TUser is all about accessors and serializing. We have 5 accessors here:

  • Name - human-readable user name.
  • Group - human-readable group name.
  • IsGuest - boolean value for identifying is this user guest or not?
  • UserID - numeric ID value from DB.
  • GroupID - same as above.
 
<?php
/**
 * @author Artūras Šlajus <x11@arturaz.afraid.org>
 * @license http://creativecommons.org/licenses/LGPL/2.1/ CC-GNU LGPL
 */
 
class MTUser extends TComponent implements IUser {
	/**
	 * @var TUserManager user manager
	 */
	private $_Manager;
	
	/**
	 * Constructor.
	 * @param TUserManager user manager
	 */
	public function __construct($Manager=NULL) {
		$this->_Manager = $Manager;
	}
 
	/**
	 * @return TUserManager user manager
	 */
	public function getManager() {
		return $this->_Manager;
	}
	
	// Following 2 functions makes most work in this class. They are 
	// responsible of saving and restoring user instance to/from session.
 
	/**
	 * @return string user data that is serialized and will be stored in session
	 */
	public function saveToString() {
		return serialize(
			array(
				$this->_Name,				
				$this->_Group,
				$this->_IsGuest,
				$this->_UserID,
				$this->_GroupID
			)
		);
	}
 
	/**
	 * @param string user data that is serialized and restored from session
	 * @return IUser the user object
	 */
	public function loadFromString($data) {
		if (!empty($data)) {
			$array = unserialize($data);
			$this->_Name = $array[0];			
			$this->_Group = $array[1];
			$this->_IsGuest = $array[2];
			$this->_UserID = $array[3];
			$this->_GroupID = $array[4];			
		}
		return $this;
	}
	
	// Fake methods! We don't really need these but IUser insists on 'em.
	public function getRoles() {}
	public function setRoles($value) {}	
	public function isInRole($value) {}
 
	// Just accessors below.
 
	/**
	 * @var String Default value for Name
	 */
	private $_Name = NULL;
	
	/**
	 * @return String Name
	 */
	public function getName() {
		return $this->_Name;
	}
	/**
	 * @param String Name
	 */
	public function setName($value) {
		$this->_Name = TPropertyValue::ensureString($value);
	}
	
	/**
	 * @var String Default value for Group
	 */
	private $_Group = NULL;
	
	/**
	 * @return String Group
	 */
	public function getGroup() {
		return $this->_Group;
	}
	/**
	 * @param String Group
	 */
	public function setGroup($value) {
		$this->_Group = TPropertyValue::ensureString($value);
	}
	
	/**
	 * @var Integer Default value for UserID
	 */
	private $_UserID = NULL;
	
	/**
	 * @return Integer UserID
	 */
	public function getUserID() {
		return $this->_UserID;
	}
	/**
	 * @param Integer UserID
	 */
	public function setUserID($value) {
		$this->_UserID = TPropertyValue::ensureInteger($value);
	}
	
	/**
	 * @var Integer Default value for GroupID
	 */
	private $_GroupID = NULL;
	
	/**
	 * @return Integer GroupID
	 */
	public function getGroupID() {
		return $this->_GroupID;
	}
	/**
	 * @param Integer GroupID
	 */
	public function setGroupID($value) {
		$this->_GroupID = TPropertyValue::ensureInteger($value);
	}
	
	/**
	 * @var Boolean Default value for IsGuest
	 */
	private $_IsGuest = TRUE;
	
	/**
	 * @return Boolean IsGuest
	 */
	public function getIsGuest() {
		return $this->_IsGuest;
	}
	/**
	 * @param Boolean IsGuest
	 */
	public function setIsGuest($value) {
		$this->_IsGuest = TPropertyValue::ensureBoolean($value);
	}
 
}
 
?>

TAuthManager

And finally - core of our engine - AuthManager.

Again, 3 accessors here, 1 event handler and init method:

  • DBHandler - see TUserManager.
  • PermissionsTable - DB table with permissions.
  • Default - should we allow or deny access to pages by default?

The real work is being done by OnAuthorize event. We're going to extend TAuthManager because all the methods there works perfictly for us.

 
<?php
/**
 * @author Artūras Šlajus <x11@arturaz.afraid.org>
 * @license http://creativecommons.org/licenses/LGPL/2.1/ CC-GNU LGPL
 */
 
Prado::using('System.Security.TAuthManager');
 
/**
 * MTAuthManager class.
 */
class MTAuthManager extends TAuthManager {
 
	public function init($config) {		
		if (! $this->_DBHandler)
			throw new TConfigurationException(
				'You must set DBHandler property!'
			);
		if (! $this->_PermissionsTable)
			throw new TConfigurationException(
				'You must set PermissionsTable property!'
			);
		
		parent::init($config);
	}
	
	/**
	 * Authorize with database
	 */
	public function OnAuthorize($param) {
		$app = $this->getApplication();
		$db = $app->Modules[$this->_DBHandler];
		
		// Get our authorization data from DB.
		// This query gets single column, 'allowed' in array, orders it
		// so 'page'=NULL values would be in end and returns
		// it.
		$res = $db->getCol(
			'SELECT `allowed` FROM `!` WHERE ' .
				'(`page`=? OR `page` IS NULL) ' .
				"AND (" .
					"(`selector`='user_id' AND `value`=?) " .
					"OR (`selector`='group_id' AND `value`=?) " .
				")" .
			'ORDER BY `page` DESC',
			0,
			array(
				$this->_PermissionsTable,
				$app->getPageService()->getRequestedPagePath(),
				$app->getUser()->getUserID(),
				$app->getUser()->getGroupID(),
			)
		);
		
		// If there were no results
		if (! $res)
			// And default is deny
			if (! $this->_Default)
				$this->DenyRequest();
		else
			// Traverse results
			foreach ($res as $allowed)
				// If we get deny here
				if (! $allowed)
					$this->DenyRequest();
	}
	
	/**
	 * Deny request.
	 */
	private function DenyRequest() {
		$this->getApplication()->getResponse()->setStatusCode(401);
		$this->getApplication()->completeRequest();
	}
	
	/**
	 * @var String Default value for DBHandler
	 */
	private $_DBHandler = NULL;
	
	/**
	 * @return String DBHandler
	 */
	public function getDBHandler() {
		return $this->_DBHandler;
	}
	/**
	 * @param String DBHandler
	 */
	public function setDBHandler($DBHandler) {
		$DBHandler = TPropertyValue::ensureString($DBHandler);
		
		if (is_null($this->Application->getModule($DBHandler)))
			throw new TConfigurationException(
				"No module with such ID: $DBHandler"
			);
		
		$this->_DBHandler = $DBHandler;
	}
 
	/**
	 * @var String Default value for PermissionsTable
	 */
	private $_PermissionsTable = NULL;
	
	/**
	 * @return String PermissionsTable
	 */
	public function getPermissionsTable() {
		return $this->_PermissionsTable;
	}
	/**
	 * @param String PermissionsTable
	 */
	public function setPermissionsTable($value) {
		$this->_PermissionsTable = TPropertyValue::ensureString($value);
	}
	
	/**
	 * @var Boolean Default value for Default
	 */
	private $_Default = FALSE;
	
	/**
	 * @return Boolean Default
	 */
	public function getDefault() {
		return $this->_Default;
	}
	/**
	 * @param Boolean Default
	 */
	public function setDefault($value) {
		$this->_Default = TPropertyValue::ensureBoolean($value);
	}
	
	
}
 
?>

Application configuration

Only thing left for now is to plug this thing into our application:

 
<?xml version="1.0" encoding="utf-8" ?>
<application id="App" mode="Debug">
	<modules>
		<!-- Database module -->
		<module id="DB" class="Application.Engine.MTPearDB"
			DSNFile="Application.DSN" Persistent="True" 
			FetchMode="DB_FETCHMODE_ASSOC" Names="UTF8" />
		<!-- Auth manager, must be named 'Auth' -->
		<module id="Auth" class="Application.Engine.MTAuthManager"
			UserManager="Users" LoginPage="Home"
			DBHandler="DB" PermissionsTable="permissions"
			Default="TRUE" />
		<!-- User manager. -->
		<module id="Users" class="Application.Engine.MTUserManager"
			DBHandler="DB" UserTable="users" GroupTable="groups" />
	</modules>
</application>

Voila! It should work now :)

Credits

Written by Artūras Šlajus.

Personal tools
Your user name:

Your password:

MediaWiki