In previous articles, we have developed User management features, Page management features and App-wide User authentication. In this article, we will look at how we can implement role-based permissions, as well as user-specific permissions settings. To begin, we need to be able to define custom UserCategories, on a per-company basis.

This is the first time we will use namespacing to separate data between Companies. We have created a few namespaces like Users, Sessions and Companies, but we are yet to use them to create a secure way to store our data between Companies. We will use the Company KeyId as the name of each individual Company namespace. Each of our Users will have a CurrentCompany value set that contains this KeyId. When we set up our basic authentication in the article Adding Authentication – A SaaS Experience, we also ensured that the App class set a static $User variable when authenticating. This means that as soon as our App is instantiated, and our authentication method is called, we have access to the Company KeyId by calling App::$User->CurrentCompany.

With this in mind, we can simplify the process of obtaining a Gateway for our Stores, by adding a gateway that only has access to the Company namespace.

<?PHP	// container.class.php -- updates only --
	
	public static function newGateway($Namespace = null){
		if(is_null($Namespace)){
			return new GDSGatewayProtoBuf(null, App::$User->CurrentCompany);
		}
		return new GDSGatewayProtoBuf(null, $Namespace);
	}

?>

We have updated our newGateway() method in the container.class.php file. Now anytime we need access to Company specific data, we can call on our newGateway() method and we will be provided with a gateway already set to the Company namespace. If we do not pass a namespace to the method, our gateway will be restricted to the Company namespace. If we do provide a namespace, like “Session”, the gateway will be restricted to that namespace.

Next, we need to be able to create a UserCategory. We will need a UserCategory Entity type, as well as a UserCategoryStore. We don’t need anything too elaborate, simply a UserCategory Name, and a boolean to show if the UserCategory is Active.

<?PHP	// models/usercategorystore.class.php

class UserCategoryStore extends CacheStore {
	
	private $UserCategory;
	
	public function buildSchema(){
		$Schema = new GDSSchema('UserCategory');
		$Schema->addString('Name', true);
		$Schema->addBoolean('Active', true);
		return $Schema;
	}
	
	public function __construct($Gateway, $Cache){
		parent::__construct($this->buildSchema(), $Gateway, $Cache);
		$this->setEntityClass('UserCategory');
	}
	
	public function processUpdateForm(){
		if(!isset($_POST['UserCategoryKey'])){
			$this->UserCategory = new UserCategory();
		} else{
			$this->UserCategory = $this->fetchById($_POST['UserCategoryKey']);
			if(!$this->UserCategory){
				throw new UserCategoryException('Trying to process use category update form, but the UserCategoryKey provided returned no valid user category');
			}
		}
		
		$this->UserCategory->Name = $_POST['Name'];
		$this->UserCategory->Active = (bool) $_POST['Active'];
		
		$this->upsert($this->UserCategory);

		$_POST['UserCategoryKey'] = $this->UserCategory->getKeyId();

		$this->getUpdateForm(false);
		
		$this->UpdateForm->add_error('error', 'The user category '.$this->UserCategory->Name.' has been updated.');
		return $this->UpdateForm;
			
	}
		
	public function getUpdateForm($New = false){
		$Form = new AppForm('UpdateUserCategory');
		
		$UActive = true;
		$UName = '';
		
		if(!$New){
			if(isset($_POST['UserCategoryKey'])){
				$this->UserCategory = $this->fetchById($_POST['UserCategoryKey']);
				$UName = $this->UserCategory->Name;
				$UActive = $this->UserCategory->Active;
			} else{
				throw new UserCategoryException('Trying to generate common fields, but $_POST[CompanyKey] not set');
			}
		}
				
		$Form->add('label','labelName','Name','User Category Name: ');
		$Name = $Form->add('text','Name', $UName);
		$Name->set_rule(array(
			'required' => array('error','A user category name is required'),
		));
		
		$Form->add('label','labelActive','Active','Active: ');
		$ActiveAttributes = '';
		if($UActive or $New){
			$ActiveAttributes = array('checked' => 'checked');
		}
		$Active = $Form->add('checkbox','Active',true,$ActiveAttributes);

				
		$Form->add('submit','btnSubmit','Save User Category');
		
		$this->UpdateForm = $Form;
		return $Form;
	}
?>
<?PHP	// models/usercategory.class.php

class UserCategory extends GDSEntity {

}	
	
?>

With these items in place, we can now create a UserCategories Controller, and an AddUserCategoryView.

<?PHP	// controllers/usercategories.class.php

class UserCategories extends Controller{
	
	public function __construct(){
		$this->Model(App::newUserCategoryStore());
	}
	
	private function getUpdateForm($New = false){
		$UpdateForm = $this->Model->getUpdateForm($New);
		return $UpdateForm;
	}
	
	public function add(){
		$this->View(App::newAddUserCategoryView());
		$this->View->Model($this->Model);
		$this->View->setForm($this->getUpdateForm(true));
		$this->View->render();
	}
		
	public function index($Params){
		echo '<h1>User Categories Dashboard</h1>';
	}
	
}
	
	
?>
<?PHP	// views/addusercategoryview.class.php

class AddUserCategoryView extends View{
	
	private $Form;
	private $Content;
	
	public function __construct(){
		$this->setContent();
	}
	
	public function setForm($Form){
		$this->Form = $Form;
	}
	
	public function render(){
		echo $this->Content;
		if($this->Form->validate()){
			$this->Form = $this->Model->processUpdateForm();
		}
		$this->Form->render('*horizontal');		
	}
	
	public function setContent(){
		$Content = '<h1>Add a New User Category</h1>';
		$this->Content = $Content;
	}
	
	
}
	
?>

After updating the container.class.php file with the new dependencies, we can run composer dump to update our autoload file and run our new form.

We are also going to need a UserCategory Select form in order to choose a UserCategory to use when defining permissions. We can create one in our UserCategoryStore as we did with our UserStore.

<?PHP	// models/usercategorystore.class.php -- additions only --
	
	public function getUserCategorySelectForm($Active = true){
		$Form = new AppForm('SelectUserCategory');
		
		$Form->add('label', 'labelUserCategoryKey','UserCategoryKey','User Category: ');
		$UserCategoryKey = $Form->add('select','UserCategoryKey');
		
		$Options = $this->getUserCategoryOptions($this->getUserCategories($Active));
		
		$UserCategoryKey->add_options($Options);
		
		$Form->add('submit','btnSubmit','Select User Category');
		
		return $Form;
	}
	
	public function getUserCategories($Active = true){
		$Categories = $this->fetchAll("SELECT * FROM UserCategory WHERE Active = @Active", ['Active' => $Active]);
		return $Categories;
	}
	
	public function getUserCategoryOptions($Categories){
		$Options = [];
		if(!is_array($Categories)){
			throw new UserCategoryException('Trying to get User Category Options, but $Categories provided is not an array of categories.');
		}
		
		foreach($Categories AS $Category){
			$Options[$Category->getKeyId()] = $Category->Name;
		}
		
		return $Options;
	}
?>

To test our new select form, I have added an EditUserCategoryView, and an edit() method in the UserCategories Controller to allow us to edit UserCategories using the new select form and our existing update form.

<?PHP	// views/editusercategoryview.class.php

class EditUserCategoryView extends View{
	
	private $Content;
	private $SelectForm;
	private $EditForm;
	
	public function __construct(){
		$this->setContent();
	}
	
	public function render(){
		if(!isset($_POST['UserCategoryKey'])){
			echo $this->Content;
			$this->setSelectForm();
			$this->SelectForm->render('*horizontal');
		} else{
			
			$this->setEditForm();
			
			if($this->EditForm->validate()){
				$this->EditForm = $this->Model->processUpdateForm();
			}
			echo $this->Content;
			
			$this->EditForm->render('*horizontal');
		}
	}
	
	public function setContent(){
		$Content = '<h1>Edit a User Category</h1>';
		$Content .= '<p>Select a user category to edit';
		$this->Content = $Content;
	}
	
	public function setSelectForm(){
		$this->SelectForm = $this->Model->getUserCategorySelectForm();
	}
	
	public function setEditForm(){
		$this->EditForm = $this->Model->getUpdateForm(false);
	}
	
}
	
?>
<?PHP	// controllers/usercategories.class.php -- additions only --
	public function edit(){
		$this->View(App::newEditUserCategoryView());
		$this->View->Model($this->Model);
		$this->View->render();
	}
?>

Our next task, now that we are able to manage our UserCategories, is to create a way to assign permissions to the UserCategories. To manage user category permissions, we will need a CategoryPermission Entity type and a CategoryPermissionStore.

<?PHP	// models/categorypermissionstore.class.php

class CategoryPermissionStore extends CacheStore {
	
	private $Company;
	
	public function buildSchema(){
		$Schema = new GDSSchema('CategoryPermission');
		$Schema->addString('UserCategoryKey', true);
		$Schema->addString('PageKey',true);
		return $Schema;
	}
	
	public function __construct($Gateway, $Cache){
		parent::__construct($this->buildSchema(), $Gateway, $Cache);
		$this->setEntityClass('CategoryPermission');
	}
		
}
	
?>
<?PHP	// models/categorypermission.class.php

class CategoryPermission extends GDSEntity {
	
}	

?>

We now need to come up with a way to assign permissions to a UserCategory. We can achieve this by adding a getUpdateForm() method to our CategoryPermissionsStore to generate a list of all available pages, and allowing us to select if the page is allowed or not.

<?PHP // models/categorypermissionstore.class.php -- additions only --
	
	public function getUpdateForm(){
		if(isset($_POST['UserCategoryKey'])){
			$UserCategoryKey = $_POST['UserCategoryKey'];
		} else{
			throw new UserCategoryException('Trying to generate permissions form but $_POST[UserCategoryKey] not set');
		}
		
		$PageStore = App::newPageStore();
		$Directory = $PageStore->getPageDirectory();
		$CurrentPermissions = $this->getCurrentPermissions($UserCategoryKey);
		
		$Form = new AppForm('CategoryPermissions');
		
		$i = 0;
		
		foreach($Directory AS $Page){
			$i++;
			
			$Spacing = $this->getDirectorySpacing($Page);			
			
			$Box = $Form->add('checkboxes','Checkbox'.$i, array($Page[0]->getKeyId() => $Page[0]->MenuTitle));
			$Box->set_attributes(array('style' => $Spacing), false);
			
			if(in_array($Page[0]->getKeyId(), $CurrentPermissions)){
				$Box->set_attributes(array('checked' => 'checked'), false);
			}
		}
		
		$Form->add('hidden','i',$i);
		$Form->add('hidden','UserCategoryKey', $UserCategoryKey);	
		$Form->add('submit', 'btnSubmit','Save Category Permissions');

		return $Form;
		
	}
	
	public function getCategoryPermissions($UserCategoryKey){
		$Permissions = $this->fetchAll("SELECT * FROM CategoryPermission WHERE UserCategoryKey = @UserCategoryKey", ['UserCategoryKey' => $UserCategoryKey]);
		if(!$Permissions){
			$Permissions = [];
		}
		return $Permissions;
	}
	
	public function getCurrentPermissions($UserCategoryKey){
		$Objects = array();
		$Permissions = array();
		$Objects = $this->getCategoryPermissions($UserCategoryKey);
		foreach($Objects AS $Object){
			$Permissions[] = $Object->PageKey;
		}
		return $Permissions;
	}
	
	public function getDirectorySpacing($Item){
		$Spacing = 'margin-left: 0px;';
		
		if($Item[1] == 2){
			$Spacing = 'margin-left: 30px;';
		}
		if($Item[1] == 3){
			$Spacing = 'margin-left: 60px;';
		}
		return $Spacing;
	}

In the getUpdateForm() method, we have fist ensured we have been provided with a UserCategoryKey, we then ask our App for a newPageStore, and use the PageStore to get a page directory. We will use the page directory to list all of the available pages in our app. We also need to get the current permissions for the selected UserCategory. To do this, we call a getCurrentPermissions() method and supply our UserCategoryKey. You will see the getCurrentPermissions() helper function listed below our getUpdateForm() method. Is simply uses a getCategoryPermisions() method to retrieve an array of permissions objects, and then creates an array of PageKey’s from the permissions objects. We can then use this to see if the category already has permissions for a particular PageKey.

Next, we set a counter, in this case we have called it $i. We then loop through each page of the directory, and use a helper method getDirectorySpacing to add some formatting to each item, here we are adding margins to the left of each item to show the nesting levels for each group of pages. We then all a checkbox to our form for the item we are currently working with, setting the checkbox’s name to Checkbox1,2,3… using our counter. We then set the value of the checkbox to the PageKey for the page, and use the Page’s MenuTitle as the label for the checkbox. The next line then adds the custom margin to the form element.

The next task is to see if the permission is already assigned to the UserCategory. We can do this by seeing if the current PageKey is in the array of CurrentPermissions we looked up earlier. If it is available in that array, the current UserCategory already has permission to view the Current Page, and as such, we can pre-tick the checkbox on the form.

We then need to provide some information on the form, so our application can handle the submission of the form correctly. In this case, we need to provide the final value of our $i counter, so we know the range of Checkbox names to work through. We also need to provide the UserCategoryKey, so we can continue working with the same UserCategory. We then add a submit button, and return the form.

We can then create a View for our permissions form, and add it to the UserCategories controller as editPermissions().

<?PHP	// views/editcategorypermissionsview.class.php

class EditCategoryPermissionsView extends View{
	
	private $Content;
	private $SelectForm;
	private $EditForm;
	private $CategoryStore;
	
	public function __construct(){
		$this->setContent();
		$this->CategoryStore = App::newUserCategoryStore();
	}
	
	public function render(){
		if(!isset($_POST['UserCategoryKey'])){
			echo $this->Content;
			$this->setSelectForm();
			$this->SelectForm->render('*horizontal');
		} else{
			
			$this->setEditForm();
			
			if($this->EditForm->validate()){
				$this->Model->processUpdateForm();
				$this->setEditForm();
				$this->EditForm->add_error('error','Permissions Updated!');
			}
			echo $this->Content;
			$this->EditForm->render('*horizontal');
		}
	}
	
	public function setContent(){
		$Content = '<h1>Edit User Category Permissions</h1>';
		$Content .= '<p>Select a user category to edit';
		$this->Content = $Content;
	}
	
	public function setSelectForm(){
		$this->SelectForm = $this->CategoryStore->getUserCategorySelectForm();
	}
	
	public function setEditForm(){
		$this->EditForm = $this->Model->getUpdateForm();
	}
	
}
	
?>
<?PHP	// controllers/usercategories.class.php -- additions only --
	public function editPermissions(){
		$this->View(App::newEditCategoryPermissionsView());
		$this->View->Model(App::newCategoryPermissionStore());
		$this->View->render();
	}
?>

We should then be able to view our form at [appurl]/UserCategories/editPermissions after selecting a User Category.

Next, we need to process the results of the form, once it has ben submitted, and update the UserCategory permissions.

<?PHP	// models/categorypermissionstore.class.php -- additions only --
		
	public function processUpdateForm(){
		if(isset($_POST['i'])){
			$i = $_POST['i'];			
		} else{
			$i = 0;
		}
		
		$x = 1;
		$NewPermissions = array();
				
		while($x <= $i){
			if(isset($_POST['Checkbox'.$x])){
				$NewPermissions[] = $_POST['Checkbox'.$x];
			}
			$x++;
		}
				
		if(isset($_POST['UserCategoryKey'])){
			$OldPermissions = $this->getCurrentPermissions($_POST['UserCategoryKey']);		
		} else{
			throw new UserCategoryException('Trying to update category permissions, but $_POST[UserCategoryKey] not set');
		}

		$DeletePermissions = array_diff($OldPermissions, $NewPermissions);
		$AddPermissions = array_diff($NewPermissions, $OldPermissions);
		
		//Add New Permissions
		$this->upsert($this->generateNewPermissionObjects($AddPermissions));
		
		//Delete Permissions
		$this->delete($this->loadPermissionObjects($_POST['UserCategoryKey'], $DeletePermissions));		
	}
	
	public function generateNewPermissionObjects($PageIDs){
		$Objects = array();
		foreach($PageIDs AS $ID){
			$Object = new CategoryPermission();
			$Object->UserCategoryKey = $_POST['UserCategoryKey'];
			$Object->PageKey = $ID;
			$Objects[] = $Object;
		}
		return $Objects;
	}
	
	public function loadPermissionObjects($UserCategoryKey, $PageIDs){
		$Objects = array();
		
		foreach($PageIDs AS $ID){
			$Objects[] = $this->fetchOne("SELECT * FROM CategoryPermission WHERE UserCategoryKey = @UserCategoryKey AND PageKey = @PageKey", ['UserCategoryKey' => $UserCategoryKey, 'PageKey' => $ID]);			
		}
		return $Objects;
	}

Our processUpdateForm() method first obtains the counter we passed to it, $i, and also sets a new counter $x. We also create an empty array to hold the permissions items from our UpdateForm. Next, we use our $x counter, to loop through all of the Checkbox fields posted form the update form. We do this by searching Checkbox1, Checkbox2, Checkbox3…. until we reach Checkbox$i. If the value exists in our $_POST array, we store the value in our NewPermissions array.

Next, we check to ensure we have been sent a UserCategoryKey, and we find the current permissions for the UserCategory. We then create 2 arrays using the array_diff function. We Subtract our $NewPermissions from our $OldPermissions. Any that remain, we need to delete. We then Subtract our $OldPermissions from our $NewPermissions, to find any permissions that we need to add. We now have 2 lists, a list to delete, and a list to add.

To add the new permissions, we upsert an array of CategoryPermissions objects, which we obtain using the generatePermissionObjects() method, which simply creates an array of CategoryPermissions based on a list of PageId’s. To delete the permissions no longer required, we use the delete() method, and provide an array of permissions objects obtained by using the loadPermissionObjects() method, which looks up the permission objects for the PageIds we have identified as needing deletion.

If we now run the form again, we are now able to tick and untick each page and save the results. When the page loads again, you will see that the permissions are being saved correctly.

The features we have added so far allow us to create a UserCategory, and now save and edit a list of pages that the UserCategory is allowed to access. Later, we will modify our App’s authenticate() and routeRequest() methods to only allow access to pages if a UserCategory permission exists for the UserCategory of the logged in user. First, however we need to be able to assign UserCategories to Users.

<?PHP	//models/userstore.class.php -- updates only --
	
	public function getCommonAddFields($Form, $New = true){
		$Form->add('label', 'labelUsername', 'Username', 'Username: ');
		if($New){
			$Username = $Form->add('text', 'Username');
			$Username->set_rule(array(
				'required' => array('error','Username is required!'),
			));
		} else{
			if(isset($_POST['UserKey'])){
				$this->User = $this->fetchById($_POST['UserKey']);
				if(!$this->User){
					throw new UserException('Trying to update a user, but the UserKey provided returned no valid user');					
				} else{
					$Username = $Form->add('text','Username', $this->User->Username, array('disabled' => 'disabled'));
				}

			} else{
				throw new UserException('Trying to update a user, but no UserKey has been supplied. $_POST[UserKey] not set.');
			}
		}
		
		if($New){
			$Form->add('label', 'labelPassword','Password','Password: ');
			$Password = $Form->add('password','Password');
			$Password->set_rule(array(
				'required' => array('error','Password is required!'),
				'length' => array(8,0,'error','Password must be at least 8 characters long.'),
			));
			
			$Form->add('label','labelConfirmPassword','ConfirmPassword','Confirm Password: ');
			$ConfirmPassword = $Form->add('password','ConfirmPassword');
			$ConfirmPassword->set_rule(array(
				'compare' => array('Password','error','Passwords do not match!'),
			));
		}
		
		$UName = '';
		$USurname = '';
		$UEmail = '';
		$UUserCategoryKey = '';
		
		if(isset($this->User)){
			$UName = $this->User->Name;
			$USurname = $this->User->Surname;
			$UEmail = $this->User->Email;
			$UUserCategoryKey = $this->User->UserCategoryKey;
		}
		
		$Form->add('label','labelName','Name','Name: ');
		$Name = $Form->add('text','Name', $UName);
		$Name->set_rule(array(
			'required' => array('error','Your name is required.'),
		));
		
		$Form->add('label','labelSurname','Surname','Surname: ');
		$Surname = $Form->add('text','Surname', $USurname);
		$Surname->set_rule(array(
			'required' => array('error','Your surname is required.'),
		));
		
		$Form->add('label','labelEmail','Email','Email: ');
		$Email = $Form->add('text','Email', $UEmail);
		$Email->set_rule(array(
			'required' => array('error','Your email is required.'),
			'email' => array('error','Your email address is not valid'),
		));
		
		$Form->add('label','labelUserCategoryKey','UserCategoryKey','User Category: ');
		$UserCategoryKey = $Form->add('select','UserCategoryKey', $UUserCategoryKey);
		
		$UserCategoryStore = App::newUserCategoryStore();
		$Cats = $UserCategoryStore->getUserCategories(true);
		$Options = $UserCategoryStore->getUserCategoryOptions($Cats);
		
		$UserCategoryKey->add_options($Options, false);
		$UserCategoryKey->set_rule(array(
			'required' => array('error','A User Category is Required'),
		));
		
		if(isset($this->User)){
			$Form->add('hidden','UserKey', $this->User->getKeyId());
		}
		
		return $Form;

	}
?>

We have updated our UserStore’s getCommonAddField() method to include a UserCategory select box. We have populated the options in the Select field by requesting a newUserCategoryStore from our App, and using the UserCategoryStore’s getUserCategories() and getUserCategoryOptions() methods. We now need to modify the schema and processUpdateForm() in our UserStore to save the new information.

<?PHP	//models/userstore.class.php -- updates only --
	
	public function buildSchema(){
		$Schema = new GDSSchema('User');
		$Schema->addString('Username', true);
		$Schema->addString('Password', false);
		$Schema->addString('Name', true);
		$Schema->addString('Surname',true);
		$Schema->addString('Email', true);
		$Schema->addString('CurrentCompany', true);
		$Schema->addBoolean('Active', true);
		$Schema->addString('UserCategoryKey', true);
		return $Schema;
	}

	public function processUpdateForm(){
		if(!isset($_POST['UserKey'])){
			$this->User = new User();
			$this->User->Username = $_POST['Username'];
			$this->User->Password = $this->generatePassword($_POST['Password']);
		} else{
			$this->User = $this->fetchById($_POST['UserKey']);
			if(!$this->User){
				throw new UserException('Trying to process user update form, but the UserKey provided returned no valid user');
			}
		}
		
		$this->User->Name = $_POST['Name'];
		$this->User->Surname = $_POST['Surname'];
		$this->User->Email = $_POST['Email'];
		$this->User->UserCategoryKey = $_POST['UserCategoryKey'];
		
		if(isset($_POST['CompanyKey'])){
			$this->setCurrentCompany($_POST['CompanyKey']);
		} else{
			if(isset(App::$User->CurrentCompany)){
				$this->setCurrentCompany(App::$User->CurrentCompany);
			}
		}
		$this->User->Active = true;
		
		$this->upsert($this->User);
		
		$_POST['UserKey'] = $this->User->getKeyId();
		
		$this->getUpdateForm(false);
		
		$this->UpdateForm->add_error('error', 'The user '.$this->User->Username.' has been updated.');
		return $this->UpdateForm;
			
	}
	

We can now assign Users a UserCategory, and we have set a rule to make this a mandatory field when creating a user. We have also added the functionality to set permissions for each UserCategory. In order to set permissions correctly, we first need a full list of Pages in our datastore, we need a UserCategory, we need to assign permissions to the UserCategory, and then we can assign a User to the UserCategory. When we implement the changes to our App’s authenticate() and routeRequest() methods, we will be able to control access to pages based on each User’s UserCategory. In the design of this App however, we also wanted to be able to manage permissions on a User by User basis. For example, if we had Managers and Workers UserCategories, and had users assigned to each category, we would also like to be able to give individual workers access to some of the Manager level items. This may be useful if you had an employee who was taking on some of the reporting roles for their manager, and needed access to only a select few items that the Manager level has access to. In order to achieve this, we need to create UserPermissions, in the same way as we created our CategoryPermissions.

<?PHP	// models/userpermissionstore.class.php

class UserPermissionStore extends CacheStore {
	
	private $Company;
	
	public function buildSchema(){
		$Schema = new GDSSchema('UserPermission');
		$Schema->addString('UserKey', true);
		$Schema->addString('PageKey',true);
		return $Schema;
	}
	
	public function __construct($Gateway, $Cache){
		parent::__construct($this->buildSchema(), $Gateway, $Cache);
		$this->setEntityClass('UserPermission');
	}
	
	public function getUpdateForm(){
		if(isset($_POST['UserKey'])){
			$UserKey = $_POST['UserKey'];
		} else{
			throw new UserException('Trying to generate permissions form but $_POST[UserKey] not set');
		}
		
		$PageStore = App::newPageStore();
		$Directory = $PageStore->getPageDirectory();
		$CurrentPermissions = $this->getCurrentPermissions($UserKey);
		
		//Get User
		$UserStore = App::newUserStore();
		$User = $UserStore->fetchById($UserKey);
		$UserCategoryKey = $User->UserCategoryKey;
		
		//Get UserCategoryPermissions
		$CategoryStore = App::newCategoryPermissionStore();
		$CategoryPermissions = $CategoryStore->getCurrentPermissions($UserCategoryKey);
		
		$Form = new AppForm('UserPermissions');
		
		$i = 0;
		
		foreach($Directory AS $Page){
			$i++;
			
			$Spacing = $this->getDirectorySpacing($Page);			
			
			$Box = $Form->add('checkboxes','Checkbox'.$i, array($Page[0]->getKeyId() => $Page[0]->MenuTitle));
			$Box->set_attributes(array('style' => $Spacing), false);
			
			if(in_array($Page[0]->getKeyId(), $CategoryPermissions)){
				$Box->set_attributes(array('checked' => 'checked'), false);
				$Box->set_attributes(array('disabled' => 'disabled'), false);
			}
			
			if(in_array($Page[0]->getKeyId(), $CurrentPermissions)){
				$Box->set_attributes(array('checked' => 'checked'), false);
			}
		}
		
		$Form->add('hidden','i',$i);
		$Form->add('hidden','UserKey', $UserKey);	
		$Form->add('submit', 'btnSubmit','Save User Permissions');

		return $Form;
		
	}
	
	public function processUpdateForm(){
		if(isset($_POST['i'])){
			$i = $_POST['i'];			
		} else{
			$i = 0;
		}
		
		$x = 1;
		$NewPermissions = array();
				
		while($x <= $i){
			if(isset($_POST['Checkbox'.$x])){
				$NewPermissions[] = $_POST['Checkbox'.$x];
			}
			$x++;
		}
				
		if(isset($_POST['UserKey'])){
			$OldPermissions = $this->getCurrentPermissions($_POST['UserKey']);		
		} else{
			throw new UserException('Trying to update user permissions, but $_POST[UserKey] not set');
		}

		$DeletePermissions = array_diff($OldPermissions, $NewPermissions);
		$AddPermissions = array_diff($NewPermissions, $OldPermissions);
		
		//Add New Permissions
		$this->upsert($this->generateNewPermissionObjects($AddPermissions));
		
		//Delete Permissions
		$this->delete($this->loadPermissionObjects($_POST['UserKey'], $DeletePermissions));		
	}
	
	public function generateNewPermissionObjects($PageIDs){
		$Objects = array();
		foreach($PageIDs AS $ID){
			$Object = new CategoryPermission();
			$Object->UserKey = $_POST['UserKey'];
			$Object->PageKey = $ID;
			$Objects[] = $Object;
		}
		return $Objects;
	}
	
	public function loadPermissionObjects($UserKey, $PageIDs){
		$Objects = array();
		
		foreach($PageIDs AS $ID){
			$Objects[] = $this->fetchOne("SELECT * FROM UserPermission WHERE UserKey = @UserKey AND PageKey = @PageKey", ['UserKey' => $UserKey, 'PageKey' => $ID]);			
		}
		return $Objects;
	}

	
	public function getUserPermissions($UserKey){
		$Permissions = $this->fetchAll("SELECT * FROM UserPermission WHERE UserKey = @UserKey", ['UserKey' => $UserKey]);
		if(!$Permissions){
			$Permissions = [];
		}
		return $Permissions;
	}
	
	public function getCurrentPermissions($UserKey){
		$Objects = array();
		$Permissions = array();
		$Objects = $this->getUserPermissions($UserKey);
		foreach($Objects AS $Object){
			$Permissions[] = $Object->PageKey;
		}
		return $Permissions;
	}
	
	public function getDirectorySpacing($Item){
		$Spacing = 'margin-left: 0px;';
		
		if($Item[1] == 2){
			$Spacing = 'margin-left: 30px;';
		}
		if($Item[1] == 3){
			$Spacing = 'margin-left: 60px;';
		}
		return $Spacing;
	}
		
}
	
?>
	
<?PHP	// models/userpermission.class.php

class UserPermission extends GDSEntity {
	
}	

?>
	
<?PHP	// controllers/users.class.php -- additions only --
	
	public function editPermissions(){
		$this->View(App::newEditUserPermissionsView());
		$this->View->Model(App::newUserPermissionStore());
		$this->View->render();
	}
?>
	
<?PHP	// views/edituserpermissionsview.class.php

class EditUserPermissionsView extends View{
	
	private $Content;
	private $SelectForm;
	private $EditForm;
	private $UserStore;
	
	public function __construct(){
		$this->setContent();
		$this->UserStore = App::newUserStore();
	}
	
	public function render(){
		if(!isset($_POST['UserKey'])){
			echo $this->Content;
			$this->setSelectForm();
			$this->SelectForm->render('*horizontal');
		} else{
			
			$this->setEditForm();
			
			if($this->EditForm->validate()){
				$this->Model->processUpdateForm();
				usleep(500000);
				$this->setEditForm();
				$this->EditForm->add_error('error','Permissions Updated!');
			}
			echo $this->Content;
			$this->EditForm->render('*horizontal');
		}
	}
	
	public function setContent(){
		$Content = '<h1>Edit User Permissions</h1>';
		$Content .= '<p>Select a user to edit';
		$this->Content = $Content;
	}
	
	public function setSelectForm(){
		$this->SelectForm = $this->UserStore->getUserSelectForm();
	}
	
	public function setEditForm(){
		$this->EditForm = $this->Model->getUpdateForm();
	}
	
}
	
?>

The code above, for UserPermissions is nearly identical in functionality to our CategoryPermissions, it has simply been customised to work for Users instead of UserCategories. The other change, is in the UpdateForm, where we also lookup the selected User’s UserCategory, and it’s current permissions. We then test to see if the User has access to a page via the UserCategory permissions, and if so, we tick, and then disable the checkbox. This will allow us to see the permissions a User already has by default due to their UserCategory, and will disable the checkboxes, as we only want to be able to add and remove permissions the UserCategory does not allow.

We now need to modify our authenticate() and routeRequest() methods to check whether the signed in user has access to the requested page, and if they do not, we need to redirect them to a suitable page.

<?PHP	// core/app.class.php -- updates only --
	
	private function authenticate(){
		if(isset($_SESSION['UserID'])){
			$Authenticated = $this->setCurrentUser($_SESSION['UserID']);
			if($Authenticated){
				//Login successful
				
				$Allowed = false;
				//Check Page Permissions
				if(!isset($this->URL[0])){
					$this->URL[0] = 'Dashboard';
				}
				
				if(!isset($this->URL[1])){
					$this->URL[1] = 'index';
				}
				$Allowed = $this->authenticatePage($this->URL[0], $this->URL[1]);
				
				//$Allowed = true;
				
				if(!$Allowed){
					$DashAllowed = false;
					$DashAllowed = $this->authenticatePage();
					if($DashAllowed){
						$this->URL[0] = 'Dashboard';
						$this->URL[1] = 'index';
					} else{
						$this->redirectToLogin();
					}
				}
				
			} else{
				$this->redirectToLogin();
			}
		} else{
			$this->redirectToLogin();
		}
	}
	
	private function authenticatePage($Controller = false, $Method = false){
		if(!$Controller){
			$Controller = 'Dashboard';
		}
		
		if(!$Method){
			$Method = 'index';
		}
		
		//Get UserPermissions
		$UserPermissionStore = self::newUserPermissionStore();
		$UserPermissions = $UserPermissionStore->getCurrentPermissions(self::$User->getKeyId());
				
		//Get UserCategoryPermissions
		$CategoryPermissions = $UserPermissionStore->getCategoryPermissions(self::$User->getKeyId());
		
		//Get the Requested PageKey
		$PageKey = $this->getPageKey($Controller,$Method);
		
		$Allowed = false;
		
		//Check CategoryPermissions
		if(in_array($PageKey, $CategoryPermissions)){
			$Allowed = true;
		}
		//Check UserPermissions
		if(in_array($PageKey, $UserPermissions)){
			$Allowed = true;
		}
		
		return $Allowed;
	}
	
	private function getPageKey($Controller = false, $Method = false){
		if(!$Controller){
			$Controller = $this->URL[0];
		}
		
		if(!$Method){
			$Method = $this->URL[1];
		}
		
		$PageStore = self::newPageStore();
		$PageKey = $PageStore->getPageKey($Controller, $Method);
		return $PageKey;
	}
?>

<?PHP	// models/pagestore.class.php -- updates only --
	
	private function findNewKeyId($Controller, $Method){
		$Page = $this->fetchOne("Select * FROM Page WHERE Controller = @Controller AND Method = @Method", ['Controller' => $Controller, 'Method' => $Method]);
		if(is_null($Page)){
			$PageKey = '';
		} else{
			$PageKey = $Page->getKeyId();				
		}
		return $PageKey;
	}
	
	public function getPageKey($Controller, $Method){
		$PageKey = $this->findNewKeyId($Controller, $Method);
		return $PageKey;
	}
?>

If we look within the authenticate() method, you will notice we have updated code within the if($Authenticated) block. We have now set a default $Allowed value of false, we then check that a valid page is set, and if not we provide the default Dashboard controller and index methods. Next, we call an authenticatePage() method and provide the Controller and the Method names that have been requested. If after running our authenticatePage() method, the value of $Allowed is still false, we then test if the user has access to the Dashboard, and if so, we modify the request to point to the dashboard. If the user does not have access to the Dashboard, we immediately redirect them back to the login page.

Our authenticatePage() method first ensures we have a valid Controller and Method set, then requests a newUserPermissionStore from the App. We can then use the UserPermissionStore methods to get the current UserPermissions and UserCategoryPermissions for the logged in user. We also find the page key for the Controller/Method combo using a new getPageKey() method. We then check to see if that PageKey is found within either the CategoryPermissions or UserPermissions arrays. If it is found, we set the $Allowed value to true. We then return either a true/false value depending on whether we are able to grant access to this page for the logged in user.

The getPageKey() method ensures a Controller and Method have been set. It then requests a PageStore from the App, and requests the PageKey for the Controller/Method combo by calling a getPageKey() method on the PageStore. We then return the PageKey we have found. To allow this to work correctly, I have modified the PageStore class, and added our getPageKey() method. It simply calls a findNewKeyId() method we had used previously to search the Datastore for the Controller/Method pair. I have modified the findNewKeyId() method to return an empty string if no valid Page is found. This allows our authenticatePage() method to redirect the user to the Dashboard if no valid page is found.

If we save all of the files changed, add the required classes to our dependency injection container, and run composer dump, you should now be able to experiment with the Category and User permissions features. If you try to access a page that does not exist, or that you have not been granted access for, you will be redirected to the Dashboard. If you grant yourself permission for the page, you should be able to use it as expected.

This was a very long and involved article, however the features implemented here are very powerful in allowing complete access control on a page-by-page bases for all users, and is infinitely customisable within each company in the system. I the next article, we will develop a navigation menu system that will allow users to navigate between pages within the application. The Menu will also take into account Category and User permissions, and only show those pages each individual user has access to.