A Better Login System

A Better Login System

Mar 26th in PHP by Andrew Steenbuck

Net.tuts+ has published several great tutorials on user login systems. Most tutorials only deal with authenticating the user, which allows for two levels of security: logged in and not logged in. For many sites, a finer degree of control is needed to control where users can go and what they can do. Creating an access control list (ACL) system will give you the flexibility for granular permissions.

PG

Author: Andrew Steenbuck

This is a NETTUTS contributor who has published 2 tutorial(s) so far here. Their bio is coming soon!

Introduction

Final Product

Imagine you are running a great tutorial site that lets users learn about a wide variety of web development techniques. In addition to your normal readers, you have some premium subscription members, as well contributing authors and administrators.

Your problem

You want to restrict users' to only specific pages that their particular account allows access to.

The solution

Implementing an access control list will allow you a great deal of control over what users can and cannot access on your site.

If you view the demo, available with the downloadable source code, you will be greeted with an index page that tests the ACL for each user. You can select different links at the bottom to view the ACL for the different users. If you click on the 'Admin Screen' link near the top, you can view a sample of the admin interface that allows you to manage the users, roles, and permissions. NOTE: The admin system will perform a database restore every 30 minutes to make sure everything stays on the up and up. The download files also implement the ACL security on the admin site, so if user number one doesn't have the 'access admin' permission, you won't be able to access the admin site.

This system will enable you to create different groups of users (i.e. guests, premium members, contributors, and admins). We will be able to set unique permissions for each group, as well as for individual users. Let's get started by setting up our MySQL database.

Step 1: Create the Database

Our ACL will be stored in a relational database using six tables (including the table for users). You should already have a database set up in your host environment. We will create the following table structure:

Database tables

The code to create the database is available in the source files (install.sql), and there is also another file (sampleData.sql) that will create 4 sample users, along with several roles and permissions for you to test with. Simply open the files with you favorite text editor, and copy/paste the code into the SQL panel in phpMyAdmin.

Step 2: Database Include

We need to create an include file so that we may connect to our database. Create a file called assets/php/database.php and add the following code to it (replace the variable values with the information appropriate for your hosting situation):

<?php
session_start();
ob_start();
$hasDB = false;
$server = 'localhost';
$user = 'root';
$pass = 'mysql';
$db = 'acl_test';
$link = mysql_connect($server,$user,$pass);
if (!is_resource($link)) {   
    $hasDB = false;
    die("Could not connect to the MySQL server at localhost.");
} else {   
    $hasDB = true;
    mysql_select_db($db);
}
?>

On the first line of the code, we call session_start(); we will not actually use the session variables but you will need it as part of the user login system. Then, we call ob_start() to create an output buffer. Typically, when PHP generates the page, it is sent to the browser as it is generating. By using ob_start(), the page and headers aren't sent to the browser until they've loaded completely, or until we call ob_end_flush(). By buffering the page, we are able to redirect using PHP at any point on the page, instead of just at the top. After the headers are sent, our only redirect option is with JavaScript. An enterprising hacker could easily turn JavaScript off, and then see our unsecured page in all it's glory. This one line allows us to deny the user access at any point in the page if needed.

Lines 4-8 set up our variables. $hasDB is a boolean used to determine if we are connected. $server, $user, $pass, and $db are the connection arguments for the server. Line 9 connects to the server, while line 10 determines if the connection was successful. If it was, we select the database to use; if it wasn't, we display an error message using die().

Step 3: Create the ACL Class

This step is fairly long, as we are creating the ACL class that will form the basis of our system. I apologize in advance for the length of this step.

ACL

Our ACL system will be object-oriented, so let's start creating the class file. We start by adding the class definition, variable definitions, and the constructor to the file /assets/php/class.acl.php:

<?php
class ACL
{
    var $perms = array();        //Array : Stores the permissions for the user
    var $userID = 0;            //Integer : Stores the ID of the current user
    var $userRoles = array();    //Array : Stores the roles of the current user
    
    function __constructor($userID = '')
    {
        if ($userID != '')
        {
            $this->userID = floatval($userID);
        } else {
            $this->userID = floatval($_SESSION['userID']);
        }
        $this->userRoles = $this->getUserRoles('ids');
        $this->buildACL();
    }
    function ACL($userID='')
	{
		$this->__constructor($userID);
	}

Analysis

After creating the class definition, we create the three class variables to store the information that will be used in the generation of the ACL.

The Constructor Method

The __constructor() function is used to initialize the object when we want to load an ACL. It is called automatically when we call new ACL();. It is then passed a single, optional argument of the user to load the ACL for. Inside the constructor, we check to see if a user ID was passed in. If no ID was passed, we assume that we will load the ACL for the currently logged in user; so we read in the session variable for that. Alternatively, if we pass in a user ID, it allows us to read and edit the ACL for a user other than the one logged in (useful for your admin page).

After we read in the user ID, we call getUserRoles() to generate an array of the roles the user is assigned to and store it in the $userRoles class variable. At the end of the constructor, we call buildACL() to generate the actual ACL. The method named ACL() is a crutch for PHP4 installs. When you call new ACL() in PHP5, the PHP interpreter runs the __constructor() method. However, when you run the same code in PHP4, the interpreter runs ACL(). By providing a method named the same as the class, we make the class PHP4 compatible.

Any time we create a new ACL object by passing in a user ID, that object will hold the permissions for the user who was passed in.

Helper Methods

Now, lets add some more helper methods to the same class file. These methods will provide support to the other methods by performing specialized tasks:

    function getUserRoles()
    {
        $strSQL = "SELECT * FROM `user_roles` WHERE `userID` = " . floatval($this->userID) . " ORDER BY `addDate` ASC";
        $data = mysql_query($strSQL);
        $resp = array();
        while($row = mysql_fetch_array($data))
        {
            $resp[] = $row['roleID'];
        }
        return $resp;
    }
    function getAllRoles($format='ids')
    {
        $format = strtolower($format);
        $strSQL = "SELECT * FROM `roles` ORDER BY `roleName` ASC";
        $data = mysql_query($strSQL);
        $resp = array();
        while($row = mysql_fetch_array($data))
        {
            if ($format == 'full')
            {
                $resp[] = array("ID" => $row['ID'],"Name" => $row['roleName']);
            } else {
                $resp[] = $row['ID'];
            }
        }
        return $resp;
    }
    function buildACL()
    {
        //first, get the rules for the user's role
        if (count($this->userRoles) > 0)
        {
            $this->perms = array_merge($this->perms,$this->getRolePerms($this->userRoles));
        }
        //then, get the individual user permissions
        $this->perms = array_merge($this->perms,$this->getUserPerms($this->userID));
    }
    function getPermKeyFromID($permID)
    {
        $strSQL = "SELECT `permKey` FROM `permissions` WHERE `ID` = " . floatval($permID) . " LIMIT 1";
        $data = mysql_query($strSQL);
        $row = mysql_fetch_array($data);
        return $row[0];
    }
    function getPermNameFromID($permID)
    {
        $strSQL = "SELECT `permName` FROM `permissions` WHERE `ID` = " . floatval($permID) . " LIMIT 1";
        $data = mysql_query($strSQL);
        $row = mysql_fetch_array($data);
        return $row[0];
    }
    function getRoleNameFromID($roleID)
    {
        $strSQL = "SELECT `roleName` FROM `roles` WHERE `ID` = " . floatval($roleID) . " LIMIT 1";
        $data = mysql_query($strSQL);
        $row = mysql_fetch_array($data);
        return $row[0];
    }
    function getUsername($userID)
    {
        $strSQL = "SELECT `username` FROM `users` WHERE `ID` = " . floatval($userID) . " LIMIT 1";
        $data = mysql_query($strSQL);
        $row = mysql_fetch_array($data);
        return $row[0];
    }

getUserRoles()

getUserRoles() will return an array of roles the current user it is assigned to. First, we will build the appropriate SQL statement and execute it. Using while(), we loop through all of the matching results, and finally return an array of the IDs. Likewise, getAllRoles() will return all of the available roles (not just the ones the user is assigned to). Based on the value of the argument, $format, it will return an array of IDs for all the roles, or an array of associative array with the ID and name of each role. This allows our function to do double duty. If we want to use the array of user roles in MySQL, we need an array of role IDs; but if we want to display the roles on our page, it would be helpful to have one array with all the info in it.

buildACL

buildACL() generates the permissions array for the user, and is the heart of the system. First, we check to see if the user is assigned to any roles. If they are, we use array_merge() to combine the existing permissions array with the new array returned from the call to getRolePerms() (which gets all the permissions for all the roles the user is assigned to). Then we do the same for the individual user permissions, this time calling getUserPerms(). It is important that we read the user perms second because array_merge() overwrites duplicate keys. Reading the user permissions second ensures that the individual permissions will override any permissions inherited from the user's roles.

All of the functions getPermKeyFromID(), getPermNameFromID(), getRoleNameFromID() and getUsername() are simply "lookup" functions. They allow us to pass in an ID and return the appropriate text value. You can see that we build the SQL statement, then execute it and return the result. Next we will add in the two functions which will pull the permissions from the database.

    function getRolePerms($role)
    {
        if (is_array($role))
        {
            $roleSQL = "SELECT * FROM `role_perms` WHERE `roleID` IN (" . implode(",",$role) . ") ORDER BY `ID` ASC";
        } else {
            $roleSQL = "SELECT * FROM `role_perms` WHERE `roleID` = " . floatval($role) . " ORDER BY `ID` ASC";
        }
        $data = mysql_query($roleSQL);
        $perms = array();
        while($row = mysql_fetch_assoc($data))
        {
            $pK = strtolower($this->getPermKeyFromID($row['permID']));
            if ($pK == '') { continue; }
            if ($row['value'] === '1') {
                $hP = true;
            } else {
                $hP = false;
            }
            $perms[$pK] = array('perm' => $pK,'inheritted' => true,'value' => $hP,'Name' => $this->getPermNameFromID($row['permID']),'ID' => $row['permID']);
        }
        return $perms;
    }
    
    function getUserPerms($userID)
    {
        $strSQL = "SELECT * FROM `user_perms` WHERE `userID` = " . floatval($userID) . " ORDER BY `addDate` ASC";
        $data = mysql_query($strSQL);
        $perms = array();
        while($row = mysql_fetch_assoc($data))
        {
            $pK = strtolower($this->getPermKeyFromID($row['permID']));
            if ($pK == '') { continue; }
            if ($row['value'] == '1') {
                $hP = true;
            } else {
                $hP = false;
            }
            $perms[$pK] = array('perm' => $pK,'inheritted' => false,'value' => $hP,'Name' => $this->getPermNameFromID($row['permID']),'ID' => $row['permID']);
        }
        return $perms;
    }
    function getAllPerms($format='ids')
    {
        $format = strtolower($format);
        $strSQL = "SELECT * FROM `permissions` ORDER BY `permName` ASC";
        $data = mysql_query($strSQL);
        $resp = array();
        while($row = mysql_fetch_assoc($data))
        {
            if ($format == 'full')
            {
                $resp[$row['permKey']] = array('ID' => $row['ID'], 'Name' => $row['permName'], 'Key' => $row['permKey']);
            } else {
                $resp[] = $row['ID'];
            }
        }
        return $resp;
    }

These functions are essentially identical except for the tables they pull from. The single argument is the ID for the roles/users you want to pull. The roles function can be passed an array or an integer, while the user function can only be passed an integer. By using is_array(), we determine how to treat the argument for the role permission function. If it is an array, we use implode() to create a comma-separated-list. In either case, we use that value in the SQL. Then, we create a new empty array called $perms - this will store the permissions locally in the function.

Inside the while() loop, we perform several functions. First we generate the variable $pK, which we will use as the name of the array key. Because we will be looking for this value to determine if the user has a specific permission, it is important that we have it in a uniform format, which is why we are using strtolower(). If the key value is blank, we skip to the next iteration using continue;. Next, we look at $row['value'] to set an implicit boolean value for the permission. This ensures that only an actual value of '1' in the table will equate with true (i.e. the user has the permission), and is important for security. Otherwise we set the permission to false. At the end of the function, we create an array with several named keys so we can get all of the information about a permission. That array is assign to a new named key in the $perms array we created earlier. Note that we use $pK to create an appropriately named index. Finally we return the array.

You can see that in the returned array, there is an index name 'inherited'. This has a special significance for the ACL. If a user receives a permission because it belongs to a role the user is assigned to, it is said to be inherited. If the permissions is assigned to the user manually, it is not inherited.

In getAllPerms(), we build a list of all available permissions. Similar to getAllRoles() we can pass in a format argument to determine how the results will be returned. Now for the last part of the class:

    function userHasRole($roleID)
    {
        foreach($this->userRoles as $k => $v)
        {
            if (floatval($v) === floatval($roleID))
            {
                return true;
            }
        }
        return false;
    }
    
    function hasPermission($permKey)
    {
        $permKey = strtolower($permKey);
        if (array_key_exists($permKey,$this->perms))
        {
            if ($this->perms[$permKey]['value'] === '1' || $this->perms[$permKey]['value'] === true)
            {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}
?>

These last two methods are very important for the functionality of the ACL. userHasRole() accepts the single argument of a role ID. By looping through all the elements in the $userRoles array, we can determine if the user is assigned to that role. If they are, we return true, or false otherwise. hasPermission() is the method we use to determine if a user can access something. We pass in the key for the permission we want to check. We make it uniform by converting it to lowercase, and see if there is an index with that name in the $perms array. If there is, we check to make sure that it is set to '1' and return true, or return false otherwise. This is the function we will use if we want to figure out if a user can do something.

Step 4: User Admin

The first part of our admin section will deal with managing users. We need to create four different interfaces to deal with the aspects of managing users: List the users so we can select one to edit, viewing a detail user listing, assign users to roles, and grant users permissions.

User Forms

Open /admin/users.php and add the following code:

<?php 
include("../assets/php/database.php"); 
include("../assets/php/class.acl.php");
$myACL = new ACL();
if ($myACL->hasPermission('access_admin') != true)
{
header("location: ../index.php");
} ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>ACL Test</title> <link href="../assets/css/styles.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="header"></div> <div id="adminButton"><a href="../">Main Screen</a> | <a href="index.php">Admin Home</a></div> <div id="page"> <!-- PAGE CONTENT --> </div> </body> </html>

As always, we need to include our database and ACL files, and set up the ACL object. Then we set up the security for the page. In this case, we are ensuring that the user has the permission 'access_admin'. If they don't, they are redirected.

NOTE: If you change the ACL permissions so that user #1 no longer has the 'access_admin' permission, you won't be able to access the admin site. Also, you must first go to /index.php before you go to any of the admin pages, as index.php sets the session variable assigning you userID #1.

Right now this is just the basic layout of the page. In the next steps, we will replace <!-- PAGE CONTENT --> above with some code to manage the users. We will use the querystring variable $action to determine which of the user interfaces we should display. There are four possible values that we will address: If it is null, we display a list of the current users. If it is set to 'user', we display the form for a single user. If it is set to 'roles', we display the form to assign a user. If it is set to 'perms', we display the form to give the user permissions.

List Users

Add this code inside the div with the id 'page':

<? if ($_GET['action'] == '' ) { ?>
    <h2>Select a User to Manage:</h2>
    <? 
    $strSQL = "SELECT * FROM `users` ORDER BY `Username` ASC";
    $data = mysql_query($strSQL);
    while ($row = mysql_fetch_assoc($data))
    {
        echo "<a href=\"?action=user&userID=" . $row['ID'] . "\">" . $row['username'] . "</a><br/ >";
    }
} ?>

The concept here is pretty simple. We build a SQL query, run it and loop through the results. For each user, we generate a link that will enable us to edit that particular user.

Edit Individual User

Now, add this code directly under the previous code block:

<?
if ($_GET['action'] == 'user' ) { 
    $userACL = new ACL($_GET['userID']);
?>
    <h2>Managing <?= $myACL->getUsername($_GET['userID']); ?>:</h2>
    ... Some form to edit user info ...
    <h3>Roles for user:   (<a href="users.php?action=roles&userID=<?= $_GET['userID']; ?>">Manage Roles</a>)</h3>
    <ul>
    <? $roles = $userACL->getUserRoles();
    foreach ($roles as $k => $v)
    {
        echo "<li>" . $userACL->getRoleNameFromID($v) . "</li>";
    }
    ?>
    </ul>
    <h3>Permissions for user:   (<a href="users.php?action=perms&userID=<?= $_GET['userID']; ?>">Manage Permissions</a>)</h3>
    <ul>
    <? $perms = $userACL->perms;
    foreach ($perms as $k => $v)
    {
        if ($v['value'] === false) { continue; }
        echo "<li>" . $v['Name'];
        if ($v['inheritted']) { echo "  (inheritted)"; }
        echo "</li>";
    }
    ?>
    </ul>
 <? } ?>

When we edit a user, we need to load the ACL for that user. This will enable us to see which roles and permissions they have. We start that by creating a new ACL object, and passing in the $userID from the querystring (this way we load that user's ACL, instead of the logged in user). After that is where your normal edit user form would go. Typical things would be text fields to edit username, password, etc. Below that we list the roles the user is assigned to, and also provide a link so we can assign the user to other roles. Lines 10-16 load all the roles that the user is assigned to, and prints them out as list items using foreach(). Then we list out the user's permissions in a similar fashion. We only print out the permissions that the user has, not ones that are set to false.

User Detail

Assign Roles

Our assign roles form will end up looking like this:

User Roles

Add this code right below the previous code block:

<? if ($_GET['action'] == 'roles') { ?>
 <h2>Manage User Roles: (<?= $myACL->getUsername($_GET['userID']); ?>)</h2>
 <form action="users.php" method="post">
    <table border="0" cellpadding="5" cellspacing="0">
    <tr><th></th><th>Member</th><th>Not Member</th></tr>
    <? 
    $roleACL = new ACL($_GET['userID']);
    $roles = $roleACL->getAllRoles('full');
    foreach ($roles as $k => $v)
    {
        echo "<tr><td><label>" . $v['Name'] . "</label></td>";
        echo "<td><input type=\"radio\" name=\"role_" . $v['ID'] . "\" id=\"role_" . $v['ID'] . "_1\" value=\"1\"";
        if ($roleACL->userHasRole($v['ID'])) { echo " checked=\"checked\""; }
        echo " /></td>";
        echo "<td><input type=\"radio\" name=\"role_" . $v['ID'] . "\" id=\"role_" . $v['ID'] . "_0\" value=\"0\"";
        if (!$roleACL->userHasRole($v['ID'])) { echo " checked=\"checked\""; }
        echo " /></td>";
        echo "</tr>";
    }
?>
    </table>
    <input type="hidden" name="action" value="saveRoles" />
    <input type="hidden" name="userID" value="<?= $_GET['userID']; ?>" />
    <input type="submit" name="Submit" value="Submit" />
</form>
<form action="users.php" method="post">
    <input type="button" name="Cancel" onclick="window.location='?action=user&userID=<?= $_GET['userID']; ?>'" value="Cancel" />
</form>
 <? } ?>

The first thing we have to do here is create a form and a table. The table will have 3 columns: one for the role, one for the member checkbox, and one for the non-member checkbox. After creating a new ACL object, we load an array of all the roles using getAllRoles(). That will allow us to display input elements for every role, not just the ones a user is assigned to.

Inside the foreach() loop, we do the following: We start a new row and print out a label with the name of the role. Then we print out a radio button input. The name and id of the radio buttons is made unique for each role by using the format "role_[roleID]" (i.e. role_0012). Lines 13 and 16 determine which of the radio buttons should be checked. The first one will be checked if the user is already assigned to the group, while the second one will be checked if they are not. Notice that one has a value of '1' (for assign), and the other has a value of '0' (for don't assign). Then we end the row.

After all that, we add in some hidden elements that tell us what we are saving, and what user ID to save. Then we add a submit and cancel button.

Assign Permissions

The assign permissions form is similar to the roles form, but with different inputs, so let's add this code:

<? if ($_GET['action'] == 'perms' ) { ?>
    <h2>Manage User Permissions: (<?= $myACL->getUsername($_GET['userID']); ?>)</h2>
    <form action="users.php" method="post">
        <table border="0" cellpadding="5" cellspacing="0">
        <tr><th></th><th></th></tr>
        <? 
        $userACL = new ACL($_GET['userID']);
        $rPerms = $userACL->perms;
        $aPerms = $userACL->getAllPerms('full');
        foreach ($aPerms as $k => $v)
        {
            echo "<tr><td>" . $v['Name'] . "</td>";
            echo "<td><select name=\"perm_" . $v['ID'] . "\">";
            echo "<option value=\"1\"";
            if ($rPerms[$v['Key']]['value'] === true && $rPerms[$v['Key']]['inheritted'] != true) { echo " selected=\"selected\""; }
            echo ">Allow</option>";
            echo "<option value=\"0\"";
            if ($rPerms[$v['Key']]['value'] === false && $rPerms[$v['Key']]['inheritted'] != true) { echo " selected=\"selected\""; }
            echo ">Deny</option>";
            echo "<option value=\"x\"";
            if ($rPerms[$v['Key']]['inheritted'] == true || !array_key_exists($v['Key'],$rPerms))
            {
                echo " selected=\"selected\"";
                if ($rPerms[$v['Key']]['value'] === true )
                {
                    $iVal = '(Allow)';
                } else {
                    $iVal = '(Deny)';
                }
            }
            echo ">Inherit $iVal</option>";
            echo "</select></td></tr>";
        }
    ?>
    </table>
    <input type="hidden" name="action" value="savePerms" />
    <input type="hidden" name="userID" value="<?= $_GET['userID']; ?>" />
    <input type="submit" name="Submit" value="Submit" />
<input type="button" name="Cancel" onclick="window.location='?action=user&userID=<?= $_GET['userID']; ?>'" value="Cancel" />
</form>
<? } ?>

Like the roles form, we start by adding a form and table, this time with 2 columns. Then we create the ACL object, pull the permissions array (line 8), and get an array of all the permissions (line 9). In the foreach() loop we print out a new row and the name of the permission. Then we start a select element. The select input will have 3 options: Allow, Deny and Inherit. We look at the value of $rPerms[$v['Key']]['value'] to see which option should be selected. Allow or Deny will not be selected if the permission value is inherited thanks to $rPerms[$v['Key']]['inheritted'] != true. If the permission is inherited, the Inherited option will be selected.

Line 23-32 enhance the inherit option. If the permission is inherited, it makes it selected. Then it determines the value of the inherited permission and sets the variable $iVal so we can use the text value in the option label on line 33. After ending the select input and the table, we add in the hidden inputs to set up the save options, and add submit and cancel buttons.

Once this code is run, we will end up with a row for each available permission, and a drop down indicating whether or not the user has it.

User Permissions

Saving the Data

Add this code to /admin/users.php right above the doc type tag:

<? if (isset($_POST['action']))
{
    switch($_POST['action'])
    {
        case 'saveRoles':
            $redir = "?action=user&userID=" . $_POST['userID'];
            foreach ($_POST as $k => $v)
            {
                if (substr($k,0,5) == "role_")
                {
                    $roleID = str_replace("role_","",$k);
                    if ($v == '0' || $v == 'x') {
                        $strSQL = sprintf("DELETE FROM `user_roles` WHERE `userID` = %u AND `roleID` = %u",$_POST['userID'],$roleID);
                    } else {
                        $strSQL = sprintf("REPLACE INTO `user_roles` SET `userID` = %u, `roleID` = %u, `addDate` = '%s'",$_POST['userID'],$roleID,date ("Y-m-d H:i:s"));
                    }
                    mysql_query($strSQL);
                }
            }
            
        break;
        case 'savePerms':
            $redir = "?action=user&userID=" . $_POST['userID'];
            foreach ($_POST as $k => $v)
            {
                if (substr($k,0,5) == "perm_")
                {
                    $permID = str_replace("perm_","",$k);
                    if ($v == 'x')
                    {
                        $strSQL = sprintf("DELETE FROM `user_perms` WHERE `userID` = %u AND `permID` = %u",$_POST['userID'],$permID);
                    } else {
                        $strSQL = sprintf("REPLACE INTO `user_perms` SET `userID` = %u, `permID` = %u, `value` = %u, `addDate` = '%s'",$_POST['userID'],$permID,$v,date ("Y-m-d H:i:s"));
                    }
                    mysql_query($strSQL);
                }
            }
        break;
    }
    header("location: users.php" . $redir);
}
?>

This code first checks to see if something has been submitted by looking at $_POST['action']. This is the value that was in one of the hidden form elements in the two forms we made.

If we just submitted the roles form, the following happens:

  1. We build a $redir querystring which is where we will be sent after the form processes.
  2. We loop through all of the $_POST variables.
  3. Using substr() we find out if the first 5 digits of the variable name are "role_". This way we only get the permission inputs in the following steps.
  4. If the value for the current input is equal to '0' or 'x' (i.e. we don't want the user to have this role), we perform the delete query. If we delete the role from the user_roles table, the user is no longer assigned to the role.
  5. If the value is not '0' or 'x' (line 14), we perform the replace query.
  6. For either query, we are using sprintf() for security (sprintf() forces variable typing and helps protect against SQL injection attacks more info).
  7. We execute the SQL using mysql_query().

Note on the replace query: The replace syntax is a special MySQL syntax that allows a seamless update or insert. By using the replace, it can save us from writing lots of PHP code. When we created the user_roles table, we created a unique index on the userID and roleID fields. When we execute the 'replace into' statement, it first looks in the table to see if inserting a new row would create a duplicate (i.e. a row with the same index values already exists). If there is a row that matches the indexes, it updates that row. If there isn't, it inserts a new row. For more info, see the MySQL developer site.

If we just submitted the permissions form, the process is the same, except we are looking for a different prefix on the input names, and using a different database table. Once any operations are done, we use header("location:...") to redirect back to the page we were on, and we append the $redir querystring variable we made.

Step 5: Roles Admin

Now that we have finished the forms to manage our users, we need to manage our roles. The roles will be more simple, there are only two actions: view a list of roles, or edit a role. Create /admin/roles.php with the following code:

<?php 
include("../assets/php/database.php"); 
include("../assets/php/class.acl.php");
$myACL = new ACL();
if ($myACL->hasPermission('access_admin') != true)
{
header("location: ../index.php");
} ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>ACL Test</title> <link href="../assets/css/styles.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="header"></div> <div id="adminButton"><a href="../">Main Screen</a> | <a href="index.php">Admin Home</a></div> <div id="page"> <!-- PAGE CONTENT --> </div> </body> </html>

List Roles

Like the users page, we start off with the includes, creating the ACL object, and the page format. Our default action (page loaded with no querystring) is to list the available roles, so insert this code in place of <!-- PAGE CONTENT -->:

<? if ($_GET['action'] == '') { ?>
    <h2>Select a Role to Manage:</h2>
    <? 
    $roles = $myACL->getAllRoles('full');
    foreach ($roles as $k => $v)
    {
        echo "<a href=\"?action=role&roleID=" . $v['ID'] . "\">" . $v['Name'] . "</a><br/ >";
    }
    if (count($roles) < 1)
    {
        echo "No roles yet.<br/ >";
    } ?>
    <input type="button" name="New" value="New Role" onclick="window.location='?action=role'" />
<? } ?>

First we check if the querystring var was empty. Then we store a list of all the available roles in $roles by using getAllRoles(). On each iteration of the foreach() loop, we make a link that will bring us to the form to edit an individual role. If there are no roles in the $roles array, we display a friendly message. Finally, we add in a button that will allow us to add a new role.

Edit Role

Add this code in /admin/roles.php under the previous block:

<? if ($_GET['action'] == 'role') { 
    if ($_GET['roleID'] == '') { 
    ?>
    <h2>New Role:</h2>
    <? } else { ?>
    <h2>Manage Role: (<?= $myACL->getRoleNameFromID($_GET['roleID']); ?>)</h2><? } ?>
    <form action="roles.php" method="post">
        <label for="roleName">Name:</label><input type="text" name="roleName" id="roleName" value="<?= $myACL->getRoleNameFromID($_GET['roleID']); ?>" />
        <table border="0" cellpadding="5" cellspacing="0">
        <tr><th></th><th>Allow</th><th>Deny</th><th>Ignore</th></tr>
        <? 
        $rPerms = $myACL->getRolePerms($_GET['roleID']);
        $aPerms = $myACL->getAllPerms('full');
        foreach ($aPerms as $k => $v)
        {
            echo "<tr><td><label>" . $v['Name'] . "</label></td>";
            echo "<td><input type=\"radio\" name=\"perm_" . $v['ID'] . "\" id=\"perm_" . $v['ID'] . "_1\" value=\"1\"";
            if ($rPerms[$v['Key']]['value'] === true && $_GET['roleID'] != '') { echo " checked=\"checked\""; }
            echo " /></td>";
            echo "<td><input type=\"radio\" name=\"perm_" . $v['ID'] . "\" id=\"perm_" . $v['ID'] . "_0\" value=\"0\"";
            if ($rPerms[$v['Key']]['value'] != true && $_GET['roleID'] != '') { echo " checked=\"checked\""; }
            echo " /></td>";
            echo "<td><input type=\"radio\" name=\"perm_" . $v['ID'] . "\" id=\"perm_" . $v['ID'] . "_X\" value=\"X\"";
            if ($_GET['roleID'] == '' || !array_key_exists($v['Key'],$rPerms)) { echo " checked=\"checked\""; }
            echo " /></td>";
            echo "</tr>";
        }
    ?>
    </table>
    <input type="hidden" name="action" value="saveRole" />
    <input type="hidden" name="roleID" value="<?= $_GET['roleID']; ?>" />
    <input type="submit" name="Submit" value="Submit" />
</form>
<form action="roles.php" method="post">
    <input type="hidden" name="action" value="delRole" />
    <input type="hidden" name="roleID" value="<?= $_GET['roleID']; ?>" />
    <input type="submit" name="Delete" value="Delete" />
</form>
<form action="roles.php" method="post">
    <input type="submit" name="Cancel" value="Cancel" />
</form>
<? } ?>

After checking to make sure the querystring variable is there, we see if a roleID was passed in the querystring. If one was, we assume that we are editing a role, if not we are creating one (we display a header as appropriate). Then we create a form. Inside the form, we need a text input for the name of our role, and a table to hold the permissions. The table has columns for the permission name, allow, deny, and ignore. Like we did while editing user permissions, we must loop through the array of all permissions (line 15, $myACL->getAllPerms('full'))

In each row, we print the permission name, and 3 radio buttons. The radios use the same nomenclature as the user form ("perm_[permID]"). 'Allow' or 'Deny' are selected depending on the value of the permission stored (thanks to lines 19 and 22). If you select 'ignore', no value is stored for that role/permission combo. Notice that the first two if() block have && $_GET['roleID'] != '' in them. This ensures that if no user ID is passed (that we are creating a new role), ignore is selected by default. Then we add the hidden inputs to set the save options, and close the form. We also add another form with hidden inuts to delete the role, and another form with a cancel button that will return us to the roles page.

If everything went according to plan, we should get the following when we try to edit the permissions for a role:

Role Permissions

Saving the Data

Insert this code in /admin/roles.php right before the doc type tag:

<? if (isset($_POST['action']))
{
	switch($_POST['action'])
	{
		case 'saveRole':
			$strSQL = sprintf("REPLACE INTO `roles` SET `ID` = %u, `roleName` = '%s'",$_POST['roleID'],$_POST['roleName']);
			mysql_query($strSQL);
			if (mysql_affected_rows() > 1)
			{
				$roleID = $_POST['roleID'];
			} else {
				$roleID = mysql_insert_id();
			}
			foreach ($_POST as $k => $v)
			{
				if (substr($k,0,5) == "perm_")
				{
					$permID = str_replace("perm_","",$k);
					if ($v == 'X')
					{
						$strSQL = sprintf("DELETE FROM `role_perms` WHERE `roleID` = %u AND `permID` = %u",$roleID,$permID);
						mysql_query($strSQL);
						continue;
					}
					$strSQL = sprintf("REPLACE INTO `role_perms` SET `roleID` = %u, `permID` = %u, `value` = %u, `addDate` = '%s'",$roleID,$permID,$v,date ("Y-m-d H:i:s"));
					mysql_query($strSQL);
				}
			}
			header("location: roles.php");
		break;
		case 'delRole':
			$strSQL = sprintf("DELETE FROM `roles` WHERE `ID` = %u LIMIT 1",$_POST['roleID']);
			mysql_query($strSQL);
			$strSQL = sprintf("DELETE FROM `user_roles` WHERE `roleID` = %u",$_POST['roleID']);
			mysql_query($strSQL);
			$strSQL = sprintf("DELETE FROM `role_perms` WHERE `roleID` = %u",$_POST['roleID']);
			mysql_query($strSQL);
			header("location: roles.php");
		break;
	}
}

?>

Like on the users page, we check to see if something was submitted via $_POST, and what the value of $_POST['action'] was. If we were saving a role, we do the following:

  1. Perform a replace query on the roles table. This will update/insert the role name. Lines 8-13 perform an important function for saving roles. If we are performing an update, we already have an ID for the role. However if we are inserting one, we don't know the role ID. When we perform the replace query, the number of rows affected are returned. If the number of rows affected was greater than 1, a row was updated, so we should use the role id from the form. If the rows affected was not greater than 1, the row was inserted, so we use mysql_insert_id() to get the ID for the last inserted row.
  2. Then we loop through the $_POST variables and line 16 ensures that we only process rows where the input name starts with "perm_".
  3. Line 18 gets the floatval() of the permission so we end up with just the integer ID of the perm (so we know which permission we are dealing with).
  4. if ($v == 'x') {...} will run if we selected 'Ignore' for a permission on the form. It will attempt to delete the row from the table where the row ID and permission ID are right. If this happens, we use continue; to go to the next variable.
  5. If we have gotten to this point, we assume that we want to add or update a permission for this role. So, we use the 'replace into' syntax that we used in the user form. It's important that we have the roleID and permID in there so the database can check for an existing row.
  6. Finally we execute the SQL and redirect to the roles page.

If we have submitted the delete form, we delete the role from the roles table. Then we also delete any records from the user_roles and role_perms tables that match the role ID so that we don't end up with users and permissions assigned to roles that don't exist. Then we redirect to the roles page.

Step 6: Permissions Admin

Like the roles admin, the permissions admin will have two functions: list the available permissions, and editing permissions. Start with this code in /admin/perms.php:

<?php 
include("../assets/php/database.php"); 
include("../assets/php/class.acl.php");
$myACL = new ACL();
if ($myACL->hasPermission('access_admin') != true)
{
header("location: ../index.php");
} ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>ACL Test</title> <link href="../assets/css/styles.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="header"></div> <div id="adminButton"><a href="../">Main Screen</a> | <a href="index.php">Admin Home</a></div> <div id="page"> <!-- PAGE CONTENT --> </div> </body> </html>

List Permissions

Place this code in the page div (in place of <!-- PAGE CONTENT -- >):

<? if ($_GET['action'] == '') { ?>
    <h2>Select a Permission to Manage:</h2>
    <? 
    $roles = $myACL->getAllPerms('full');
    foreach ($roles as $k => $v)
    {
        echo "<a href=\"?action=perm&permID=" . $v['ID'] . "\">" . $v['Name'] . "</a><br />";
    }
    if (count($roles) < 1)
    {
        echo "No permissions yet.<br />";
    } ?>
    <input type="button" name="New" value="New Permission" onclick="window.location='?action=perm'" />
<? } ?>

We will first use getAllPerms() to get an array of all the permissions. Then we will loop through it to build our list. Each iteration through the foreach() loop will generate a link that will direct us to the page to edit the given permission. If no permissions are present, we display a message saying so, and we end the form with a 'New Permission' button. And the result:

Permissions Form

Edit Permission

To edit/add an individual permission, we need to add this code immediately after the previous block:

<? if ($_GET['action'] == 'perm') { 
    if ($_GET['permID'] == '') { 
    ?>
    <h2>New Permission:</h2>
    <? } else { ?>
    <h2>Manage Permission: (<?= $myACL->getPermNameFromID($_GET['permID']); ?>)</h2><? } ?>
    <form action="perms.php" method="post">
        <label for="permName">Name:</label><input type="text" name="permName" id="permName" value="<?= $myACL->getPermNameFromID($_GET['permID']); ?>" maxlength="30" /><br />
        <label for="permKey">Key:</label><input type="text" name="permKey" id="permKey" value="<?= $myACL->getPermKeyFromID($_GET['permID']); ?>" maxlength="30" /><br />
    <input type="hidden" name="action" value="savePerm" />
    <input type="hidden" name="permID" value="<?= $_GET['permID']; ?>" />
    <input type="submit" name="Submit" value="Submit" />
</form>
<form action="perms.php" method="post">
     <input type="hidden" name="action" value="delPerm" />
     <input type="hidden" name="permID" value="<?= $_GET['permID']; ?>" />
    <input type="submit" name="Delete" value="Delete" />
</form>
<form action="perms.php" method="post">
    <input type="submit" name="Cancel" value="Cancel" />
</form>
<? } ?>

Like we did in the roles form, we check to see if a permission ID is provided in the querystring and display either an addition or update header based on that. We open a form tag, and add two text inputs: one for the permission name, the other for the permission key. The name is what will appear on forms, while the key is what will we used in scripts. The key should be pretty much the same as the name, except for it should not have spaces or symbols, and should be lower case. For both text fields, we provide default values if we are updating.

At the end of the form, we add the hidden inputs, and the submit button. Then we have the delete and cancel forms.

Save the Data

Finally, we need to save the permission form, so add this code to the top of /admin/perms.php right above the doc type.

if (isset($_POST['action']))
{
	switch($_POST['action'])
	{
		case 'savePerm':
			$strSQL = sprintf("REPLACE INTO `permissions` SET `ID` = %u, `permName` = '%s', `permKey` = '%s'",$_POST['permID'],$_POST['permName'],$_POST['permKey']);
			mysql_query($strSQL);
		break;
		case 'delPerm':
			$strSQL = sprintf("DELETE FROM `permissions` WHERE `ID` = %u LIMIT 1",$_POST['permID']);
			mysql_query($strSQL);
		break;
	}
	header("location: perms.php");
}

Like all the other submission scripts, we need to figure out what action was submitted. If we are saving a permission, we perform a replace into operation. This will either update or insert as appropriate. If we submitted the delete form, we perform the delete query. In either case, we will be redirected to perms.php.

Step 7: Admin hub

We need a jumping off point for our ACL admin. We'll just create something simple with links to the 3 pages. Here is a preview and the code for it:

<?php 
include("../assets/php/database.php"); 
include("../assets/php/class.acl.php");
$myACL = new ACL();
if ($myACL->hasPermission('access_admin') != true)
{
header("location: ../index.php");
} ?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>ACL Test</title> <link href="../assets/css/styles.css" rel="stylesheet" type="text/css" /> </head> <body> <div id="header"></div> <div id="adminButton"><a href="../">Main Screen</a></div> <div id="page"> <h2>Select an Admin Function:</h2> <a href="users.php">Manage Users</a><br /> <a href="roles.php">Manage Roles</a><br /> <a href="perms.php">Manage Permissions</a><br /> </div> </body> </html>

Pretty self-explanatory, we have 3 links to manage the 3 different aspects of your ACL.

Step 8: Implementing the ACL on Your Site

Index

Implementing your new ACL system on your site is fairly easy. Each page which you want to secure should have the database and ACL file included at the top. After that, you should create a new instance of the ACL object.

As an example, say you had set up a permission with the key 'access_admin' and wanted to use it to control access to the admin interface. At the top of your page you could use this script to check it:

<?php
include("assets/php/database.php"); 
include("assets/php/class.acl.php");
$myACL = new ACL();
if ($myACL->hasPermission('access_admin') != true)
{
    header("location: insufficientPermission.php");
}
?>

As you can see we create an ACL object. Since we are not passing in a user ID as an argument, the system will read the session variable $_SESSION['userID']. Then we use $myACL->hasPermission('access_admin') to check to see if the user has that permission. If they do not, they are redirected to insufficientPermission.php. This way they can't get in to secure areas that they don't have permissions for.

In the provided source files, I have provided an index file that provides a simple test of the ACL based on the example code above. The sample index displays a list of all the permissions, and icons representing whether or not the current user can access each. There is also a list of the users that allows you to change the user that the ACL is displayed for. Here is the code for the sample index:

<?php 
include("assets/php/database.php"); 
include("assets/php/class.acl.php");

$userID = $_GET['userID'];
$_SESSION['userID'] = 1;
$myACL = new ACL();
?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>ACL Test</title>
<link href="assets/css/styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div id="header"></div>
<div id="adminButton"><a href="admin/">Admin Screen</a></div>
<div id="page">
	<h2>Permissions for <?= $myACL->getUsername($userID); ?>:</h2>
	<? 
		$userACL = new ACL($userID);
		$aPerms = $userACL->getAllPerms('full');
		foreach ($aPerms as $k => $v)
		{
			echo "<strong>" . $v['Name'] . ": </strong>";
			echo "<img src=\"assets/img/";
			if ($userACL->hasPermission($v['Key']) === true)
			{
				echo "allow.png";
				$pVal = "Allow";
			} else {
				echo "deny.png";
				$pVal = "Deny";
			}
			echo "\" width=\"16\" height=\"16\" alt=\"$pVal\" /><br />";
		}
	?>
    <h3>Change User:</h3>
    <? 
		$strSQL = "SELECT * FROM `users` ORDER BY `Username` ASC";
		$data = mysql_query($strSQL);
		while ($row = mysql_fetch_assoc($data))
		{
			echo "<a href=\"?userID=" . $row['ID'] . "\">" . $row['username'] . "</a><br />";
		}
    ?>
</div>
</body>
</html>

Final Thoughts

When combined with a good user management platform, an ACL system is a great way to secure your web site. By following these steps, you should be able to create your own flexible security system. The admin system created here is a basic example of what you can create if you don't already have an admin system set up. It demonstrates all of the principles you need to effectively manage your ACL. On the other hand, if you already have created your own user management system, it should be fairly easy to take these techniques and implement them into your own project.

  • Subscribe to the NETTUTS RSS Feed for more daily web development tuts and articles.


Related Posts

Check out some more great tutorials and articles that you might like

Enjoy this Post?

Your vote will help us grow this site and provide even more awesomeness

Plus Members

Source Files, Bonus Tutorials and
More for $9 a month for all TUTS+
sites in one subscription.

Join Now

User Comments

( ADD YOURS )
  1. PG

    Stew Parkin March 26th

    Top Draw!!!

    ( Reply )
    1. PG

      bill March 27th

      *drawer*

      ( Reply )
  2. PG

    bodhi March 26th

    Thanks!!!

    ( Reply )
  3. PG

    Kyle March 26th

    Great Stuff!!

    ( Reply )
    1. PG

      Lawrence77 March 26th

      Great!
      I should/could translate this to my Fav. .NET and going to test something! :) :P

      ( Reply )
      1. PG

        Eric March 26th

        If you’re going to use .NET, try looking at the membership and role providers. They come with the framework.

        http://aspnet.4guysfromrolla.com/articles/120705-1.aspx

        http://msdn.microsoft.com/en-us/library/yh26yfzy.aspx

      2. PG

        lawrence77 March 27th

        Thanks Eric I like ur Links! :)
        As Like ASPX u also rocks with ur links :)

  4. PG

    Niklas Hansen March 26th

    Great tut

    ( Reply )
  5. PG

    Kevin Quillen March 26th

    Isn’t this kinda reinventing the wheel, in a sense?

    ( Reply )
    1. PG

      Jeffrey Way March 26th

      Don’t you want to know how the wheel works? Too much abstraction is never a good thing.

      ( Reply )
      1. PG

        Shane March 27th

        too much of anything is a bad thing :)

      2. PG

        lawrence77 March 27th

        Jeffrey I like this much, “Too much abstraction is never a good thing.” :) nice quote i save it in my harddisk! :)

      3. PG

        J Pluijmers April 2nd

        When you are starting off in the php language(or any programming language) its smart to start from scratch. However when you have already implemented over a dozen custom acl/auth systems. It is not a bad idea to abstract this part of your project and adopt a framework.

        Even if the framework solution is not exactly what you want you still save tons of time by extending the existing code rather than starting from scratch again.

        Its in my oppinion important to do this for your own sake. Because when you leave old problems and adventures behind you can focus on other mindbogling problems and learn from those.

    2. PG

      Sirwan March 26th

      agreed, i think we have too much moaners here.

      ( Reply )
      1. PG

        Michael March 26th

        or emo-nerz

        ;)

        Great tutorial. Great stuff for those that have spent alot of time using ‘packages’, and now find themselves wondering about creating things themselves.

        thanks

    3. PG

      Yoosuf March 26th

      @Kevin Quillen: don’t be to smart man, if you know or if you like to share post some nice Articles like this, else if you know just leave it, BTW if its a Plus one you can say like same old wine, but its a free one , keep your mouth shut!

      ( Reply )
  6. PG

    Philo March 26th

    Great Article! :)

    ( Reply )
  7. PG

    Ivan March 26th

    OMG, took me 5 min just to scroll through it, it’ll take me a week to digest it :) GREAT STUFF man!

    ( Reply )
    1. PG

      Lawrence77 March 26th

      yeah correct!
      Scrolling is always easy!
      normally i do that only! :) :P

      ( Reply )
      1. PG

        klaus March 27th

        U funny @Lawrence77 :-) :-))

        BTW Nice blog you got there mate, great stuff, love the post “World Without Engineers!” also funny.

      2. PG

        lawrence77 March 27th

        thanks mate! :)
        me too a Engineers! hahaha lol :)

      3. PG

        GQDEV April 3rd

        hahaaa

  8. PG

    Brenelz March 26th

    Nice tutorial… haven’t really needed this yet in my projects but could easily in the future.

    ( Reply )
  9. PG

    Om March 26th

    What a great tutorial. Totally beyond my level but nice to know its there in case I need it.

    ( Reply )
  10. PG

    Yoosuf March 26th

    its nice, i am planing to make similar thing with CI, Thank you Andrew Steenbuck, this is a nice flow of article!

    @Kevin Quillen: don’t be to smart man, if you know or if you like to share post some nice Articles like this, else if you know just leave it, BTW if its a Plus one you can say like same old wine, but its a free one , keep your mouth shut!

    ( Reply )
  11. PG

    Emil March 26th

    That is some great stuff…

    ( Reply )
  12. PG

    Eduardo March 26th

    awesome! pretty advanced ;)

    ( Reply )
  13. PG

    Meshach March 26th

    Woahh!! Wow!

    Very, very in depth article.

    Thanks!

    ( Reply )
  14. PG

    qalih March 26th

    Great stuff, same as Ivan, took me few minutes just to scroll through, will take a longer to understand and implement.

    @Yoosuf, cool it man. I thought it was a reasonable question to ask and a good response from JW.

    ( Reply )
  15. PG

    Jonathan Hedrén March 26th

    This is just what I’ve been waiting for! Thanks!

    ( Reply )
  16. PG

    David March 26th

    I primarily program in coldfusion, but articles with such detail are so helpful in presenting solution logic that could easily be applied in any language.

    Thanks!

    ( Reply )
  17. PG

    Fredybgonzález March 26th

    Great article! Thanks.

    ( Reply )
  18. PG

    Wuiqed March 26th

    Whoa, this is a little cooler than the simple bitfields I’ve been using to control permissions. Great article!

    ( Reply )
  19. PG

    crysfel March 26th

    this is to long…. but actually really easy ;) when i was at college i used to do this kind of thing, now i use the “Acegi Security framework” ;)

    ( Reply )
  20. PG

    Marcus March 26th

    Umm php5 won’t run __constructor() since the name for a constructor function in php5 is __construct(). Also php5 will run the ACL-function if you don’t have a __construct() function in it.

    I’ve just skimmed the article but there seems to be some more spelling errors in there. I don’t know if they affect the code but the above code does.

    ( Reply )
    1. PG

      Bernhard Häussner March 27th

      This error doesn’t affect the function of the code. But the __constructor() method is completely useless, since it is neither used by PHP5 nor by PHP4 as Marcus has explained correctly.

      I wonder why the author is explaining 2 paragraphs with the wrong __constructor() name.

      And PHP5 and PHP4 are not really compatible, since PHP4 uses & for referencing while PHP5 does referencing automatically and & will do something slightly different. This results in strange errors when running PHP5 code that is “PHP4-compatible” in PHP5 if you’re trying to use these &. Not compiling errors but sometimes really hard to find runtime errors, your code won’t do what you expect (and what is does in PHP4).

      Additionally sprintf() is not escaping SQL stuff, so an easy SQL-injection could be enough to make your ACLs useless.

      Sometimes just scrolling an bookmarking isn’t a good way to build projects…

      ( Reply )
  21. PG

    Zerbe March 26th

    Is it just me or do all these tutorials leave out the part about users requesting their passwords? Or allowing the user to reset passwords. I have not gone through this one in detail but along with the others that part always seems to be left out.

    ( Reply )
  22. PG

    flanders March 26th

    YEAH, very nice TUT! but i scroll 2min down:)

    ( Reply )
  23. PG

    Corey March 26th

    __constructor() should be __construct()

    ( Reply )
  24. PG

    Jordan Patterson March 26th

    Nice.
    A definite bookmark for later use.

    ( Reply )
  25. PG

    pixelsoul March 26th

    I think you guys really out did your self on this one. Nice work and really great quality tut.

    This is why I come here everyday.

    ( Reply )
    1. PG

      Dennison Uy March 28th

      This is why I come here everyday.

      +1

      ( Reply )
      1. PG

        Old Nub March 30th

        This is why I come here everyday.

        #2

        Great post!

  26. PG

    Jay March 26th

    Wow, that’s a hell of a tut.
    Thanks for the great depth and fantastic content!

    ( Reply )
  27. PG

    Randy March 26th

    Perfect timing on this for me! Thanks a million!

    ( Reply )
  28. PG

    chris March 26th

    Great tutorial. Perfectly timed for me too since i have to implement ACL in a project.

    THANK YOU!

    ( Reply )
  29. PG

    Younness March 26th

    Very nice tuts .. i really like the way he explains the methode .. and i say tht with 3 or more complete tutorials like this we will certainly be real webmasters ..
    thnx Andrew

    ( Reply )
  30. PG

    SX March 26th

    Just curious, how long does it take a PHP developer to code this from scratch?

    ( Reply )
    1. PG

      David Singer March 28th

      A couple hours I would guess for something this size.

      While it makes a great tutorial and an interesting read writing your own ACL class is kind of a pointless endeavor. Their are already thousands of existing ACL implementations that don’t suffer form the weak security, database limitations, and other errors that this implementation does. Most PHP frameworks also include this functionality by default.

      ( Reply )
      1. PG

        slier April 2nd

        -1 rep

      2. PG

        srinath April 11th

        Can you share any of those ACL implementations? Would be helpful.

        And nice article BTW.

    2. PG

      Andrew Steenbuck March 28th

      When I first made this implementation a few years ago it took me about a day to come up with the core logic for it.

      Thanks for the comments so far.

      ( Reply )
  31. PG

    techietim March 26th

    No mysqli or PDO? I’d really wish PHP developers would switch and stop using mysql_ functions.

    ( Reply )
  32. PG

    bse March 26th

    from django.contrib.auth import *

    ( Reply )
  33. PG

    Mark March 26th

    Wouldnt it be easier, and more beneficial for both the people who write code, and define the business logic if this class was just a bunch of boolean checks.

    For example - only level 2 admins can modify posts on sundays between 2pm and 4pm (server time) (write that into your acl)

    if(isAdamin2()->isSunday()->isAfterTime(1400)->isBeforeTime(1600)){
    //do something
    }

    ( Reply )
    1. PG

      Andrew Steenbuck March 28th

      Mark, you could do that, except for that I have never had success with method chaining in PHP4, which is my primary development environment.

      ( Reply )
  34. PG

    SX March 26th

    Just a caution, I just found out after some failed attempts running this code that short tags are used, so for anyone who doesn’t have it enabled change at the beginning of all scripts <? to <?php.
    In addition in various places on all pages you have an equal sign '<? = ' instead of the '<?php'. You have remove that. That’s all i think.

    ( Reply )
  35. PG

    why March 26th

    cakePHP.

    don’t reinvent the wheel when it’s already better than what you can make.

    but learning is good.

    ( Reply )
    1. PG

      Chris Simpson March 27th

      Yes, Most Frameworks have ACL classes, but this article is about how to write one and not saying that they dont exist elsewhere.

      ( Reply )
  36. PG

    Michael Rice March 26th

    This tutorial is outstanding.
    I’ve only started reading it, but will go through it in detail later!

    ( Reply )
  37. PG

    rizq March 26th

    Great Tutorial………..!!

    ( Reply )
  38. PG

    Otto March 26th

    GREAT!!!

    ( Reply )
  39. PG

    Rene March 26th

    this is just what I Asked in a previous post, the video would be great but thanks anyway this is awesome!

    ( Reply )
  40. PG

    w1sh March 26th

    Reading is hrad.

    ( Reply )
  41. PG

    Paul March 26th

    OMG.. just on time when i needed it…

    Great tut!

    ( Reply )
  42. PG

    Julius March 26th

    Nice tutorial!

    I have a suggestion with the way you code the class ACL. Basically when you read a class code with your eyes you start with the constructor function. If the constructor function contains methods within then we put the method definition next to the constructor function.

    EX.

    function __construct() {
    code…
    code…
    $this->method1();
    $this->method2();
    }

    function method1() {
    // statement
    $this->method3();
    }

    function method2() {
    // statement
    }

    function method3() {
    // statement
    }

    ( Reply )
  43. PG

    insic March 26th

    really nice tutorial.

    ( Reply )
    1. PG

      kettle March 27th

      can you give me your email please

      ( Reply )
      1. PG

        Chris Simpson March 27th

        lol.

      2. PG

        Rene March 27th

        I want her email too, give it to me?

      3. PG

        lawrence77 March 28th

        Finally Insic is not her!!! :)

      4. PG

        Juarez P. A. Filho March 28th

        It was really funny.

      5. PG

        Lawrence77 April 1st

        lol, Finally Insic is not a girl! ;)

    2. PG

      mfakira March 30th

      i hope that

      ( Reply )
    3. PG

      desu April 6th

      LOL, its funny lawrence :)

      ( Reply )
  44. PG

    Rashid Ahmed March 26th

    Great tutorial

    Thanks for sharing

    ( Reply )
  45. PG

    Rahul Chowdhury March 27th

    Wow, a lot of work is needed for this. By the way, it was a good tutorial.

    ( Reply )
  46. PG

    Lee March 27th

    IBM Lotus Domino comes with an extremely robust and intelligent security infrastructure built in which, from what I can tell, covers all the bases this article describes.

    Does anyone here have experience in developing Domino web applications?

    ( Reply )
  47. PG

    Martin Leblanc March 27th

    This is great! I was looking for exactly this. Thanks…
    [everything]tuts.com are keeping a high quality!

    ( Reply )
  48. PG

    DataMouse March 27th

    Awesome!
    Well done for sharing

    ( Reply )
  49. PG

    Ed Hardy March 27th

    Nice clean code to build on! TY!

    ( Reply )
  50. PG

    Nikhil - Powerusers March 27th

    I am going to try out this one.
    I think I found what were I looking for.

    ( Reply )
  51. PG

    Pedro March 27th

    Printing………. Thank you!

    ( Reply )
  52. PG

    Alexander Dombroff March 27th

    Definitely bookmarking this one!

    ( Reply )
  53. PG

    Fynn March 27th

    This Tutorial is truly amazing!

    ( Reply )
  54. PG

    arma9 March 27th

    Great stuff with a lot of information

    ( Reply )
  55. PG

    ChrisD March 27th

    Hey great tutorial.

    One problem… I added all the files to my WAMP folder, created the database but when I open index.php i get a page full of script with 2 errors stating:

    Notice: Undefined index: userID in C:\cefx\www\LeafTuts\index.php on line 5

    Fatal error: Class ‘ACL’ not found in C:\cefx\www\LeafTuts\index.php on line 7

    any help much appriciated :)

    ( Reply )
    1. PG

      SX March 27th

      Read my previous post.

      ( Reply )
      1. PG

        ChrisD March 28th

        Ahh yes that got rid of those… but now i just get lots of:

        ndefined index: action in C:\cefx\www\LeafTuts\admin\roles.php

        and in lots of other files :S

        help?

        Thanks

      2. PG

        Andrew Steenbuck March 29th

        Depending on your level of error reporting, it may be caused by lines like this: (admin/roles.php, line 61). Try changing to and see if that gets rid of your error.

      3. PG

        Andrew Steenbuck March 30th

        Sorry, I just noticed it stripped out my php code. Try changing the lines that read if ($_GET['action'] == ’someValue’) to if(isset($_GET['action']) && $_GET['action'] == ’someValue’). This problem may be caused by your error reporting level, as $_GET['action'] isn’t definded unless you are performing an action.

  56. PG

    Rik Girbes March 27th

    Amazing!!!

    I have nothing else to say then: WOW just fantastic!!!!

    Great!!!

    Thanks!!

    Keep up the good work!!!

    Rik

    ( Reply )
  57. PG

    Aaron Arnett March 27th

    Amazing! Great Tut! Keep up the great work!

    ( Reply )
  58. PG

    Abhisek March 27th

    Awesome!

    ( Reply )
  59. PG

    Art March 28th

    Awesome !! thanks :)

    ( Reply )
  60. PG

    Juarez P. A. Filho March 28th

    Most frameworks already have it implemented, but know how to build it is great. Thanks a lot.

    ( Reply )
  61. PG

    Gene March 28th

    I liked this one. Great read.

    Thumbs up!

    ( Reply )
  62. PG

    Angel March 29th

    Excellent, looking forward to implement it.

    ( Reply )
  63. PG

    Wouter Bulten March 29th

    Why don’t you use private/public in your class? It would really improve the usability..

    ( Reply )
  64. PG

    Danny March 29th

    Hi Nice tutorial and thanks for sharing this,

    I’m a php newbie but I have tried this code on php4 and php5 but I can’t switch to another user. Am I doing something wrong?

    Danny

    ( Reply )
    1. PG

      Andrew Steenbuck March 29th

      Hi Danny, How are you calling the ACL constructor, $myACL = new ACL(); or $myACL = new ACL(14); ? The first way will load the ACL for whatever user id is stored in $_SESSION['userID'] (which is set to 1 on line 6 of index.php). The second way will load the permissions for user #14 regardless of what user is logged in.

      ( Reply )
  65. PG

    jrosell March 29th

    Good post. I will enjoy to try if it works as expected.

    ( Reply )
  66. PG

    Tanax March 29th

    Cool system! Good tutorial! Messy code xD

    ( Reply )
  67. PG

    ThunderWolf March 29th

    This is AMAZING!

    ( Reply )
  68. PG

    Drazen March 29th

    Great tutorial, i really appreciate that.

    ( Reply )
  69. PG

    mfakira March 30th

    very nice tuts

    ( Reply )
  70. PG

    Kayla March 30th

    Great tutorial, I’ve been looking for something this clear with this subject for a long time!

    ( Reply )
  71. PG

    Timo March 30th

    Please stop using PHP4! The var keyword is deprecated and PHP4 is no longer maintained. It would be a nice start if anyone that writes a tutorial nowadays would use PHP5!

    Above from the messy code and the security flaws; the idea is well explained.

    ( Reply )
  72. PG

    Jeremy March 30th

    This is a very good and detailed article. It definitely provides good information for those who are trying to understand or implement ACLs or Authorization modules. Thanks.

    ( Reply )
  73. PG

    Gyorgy March 31st

    This particular implementation of ACL is way too complicated…

    ( Reply )
  74. PG

    Orhan SAGLAM March 31st

    Good Tutorial

    ( Reply )
  75. PG

    Fred Campbell March 31st

    Good grief! I’ve been troubled by this for months - thank you for sharing.

    ( Reply )
  76. PG

    idang April 1st

    at last i found it……thanx for this great tutorial

    ( Reply )
  77. PG

    DesignKings April 1st

    Good Tutorial . . .
    Amazing! Great Tut! Keep up the great work!

    ( Reply )
  78. PG

    Noneman April 1st

    Quite nice tutorial .. also this is not perl where you need {} in all things :)
    if ($pK == ”) { continue; }
    “to”
    if ( empty($pK) )
    continue;

    If company have LDAP/AD it’s always good to use those as backend and rely on those as login acl:s instead of creating new ones in sql.
    Also mysql -> mysqli so you can use mysql prepare method so sql injections are bit harder to get executed.

    ( Reply )
  79. PG

    ylcz April 2nd

    bookmark this nice tutorials!

    ( Reply )
  80. PG

    slier April 2nd

    What dose table users_perms for

    i dont see any purpose of its existence

    mind to explain?

    ( Reply )
    1. PG

      Andrew Steenbuck April 5th

      User_perms functions the same as group_perms, except for it can overide group permissions. Say user X is in the `user` group, but for some reason you want to give him some permission that the group as a whole doesn’t have. That unique permission would be stored in the user_perms table.

      The ACL looks in the group_perms table to get the permissions, then looks in the user_perms table to see if there are any overide permissions. Additionally, if a user isn’t a member of any groups, user_perms allows you to still give them permissions that are not bound to a group. Hope that explains it.

      ( Reply )
      1. PG

        slier April 28th

        thats clear everything
        thanks dude

  81. PG

    Mark.long April 3rd

    A wonderful tutorial

    ( Reply )
  82. PG

    Rishi April 5th

    Awesome man. Thanx

    ( Reply )
  83. PG

    Michael April 5th

    Brilliant tutorial!

    I’d be very keen to see a one-off example extension of this which shows, perhaps publishing an article and how this will interact with the ACL above.

    Well done, Net Tuts is turning into ESSENTIAL reading.

    Cheers

    ( Reply )
  84. PG

    Mark.long April 5th

    There may be a potential bug

    $roleperm = array(’access_admin’=>array(’perm’=>’access_admin’, ‘inheritted’=>true, ‘value’=>true, ‘Name’=>’Access Admin System’, ‘ID’=>1));

    $userperm = array(’95599′=>array(’perm’=>’95599′, ‘inheritted’=>true, ‘value’=>true, ‘Name’=>’Access ABC System’, ‘ID’=>2));

    var_dump(array_merge($roleperm, $userperm));
    /**
    Output: The numeric key has been renumbered!!
    array(2) {
    ["access_admin"]=>
    array(5) {
    ["perm"]=>string(12) “access_admin”
    ["inheritted"]=>bool(true)
    ["value"]=>bool(true)
    ["Name"]=>string(19) “Access Admin System”
    ["ID"]=>int(1)
    }
    [0]=>
    array(5) {
    ["perm"]=>string(5) “95599″
    ["inheritted"]=>bool(true)
    ["value"]=>bool(true)
    ["Name"]=>string(17) “Access ABC System”
    ["ID"]=>int(2)
    }
    }
    */

    ( Reply )
  85. PG

    Chris April 6th

    Wow, This really is a great tutorial. Thanks for taking the time to help out fellow developers! Kudos to you.

    ( Reply )
  86. PG

    Steven April 8th

    Umm….. the field ‘password’ is missing in the install.sql script. It only contains ‘ID’ and ‘username’.

    or have I missed something……

    ( Reply )
    1. PG

      Andrew Steenbuck April 9th

      The password field was intentionally not included, as the tutorial doesn’t deal with user authentication. It was assumed that you already had some sort of user table from another user authentication tutorial, so I only created a bare minimum table to experiment with permissions.

      ( Reply )
  87. PG

    Gav April 9th

    Brilliant tutorial mate!! Ignore the moaners & haters on here… if people are not happy go to another site, there are billions of other sites to look at!

    Fab tut though, nothing wrong with re-inventing the wheel if it helps me understand it better!

    Thanks again and keep it up - we ALL appreciate this!

    ( Reply )
  88. PG

    paul April 9th

    Notice: Undefined index: userID in C:\wamp\www\ACL\index.php on line 5

    Fatal error: Class ‘ACL’ not found in C:\wamp\www\ACL\index.php on line 7

    what do i do?

    ( Reply )
    1. PG

      Lars April 22nd

      I also get this error…
      Could the PHP version be the problem?

      ( Reply )
    2. PG

      Kevin Considine April 22nd

      I also have this error, i have php5.2.9-1

      ( Reply )
  89. PG

    Ben D. April 10th

    From a brief look at the code, it seems like there could be a lot more done by way of securing it against SQL injection, XSS, and CSRF attacks.

    There’s not much of a validation layer, for example. For the most part it looks like you accept user-supplied data straight from $_GET and $_POST. I saw the nod to SQL injection via sprintf() typing, and the frequent calls to floatval(). But is this enough?

    Looking at the “Save the Data” section, for example, there’s this code:

    case ’savePerm’:
    $strSQL = sprintf(”REPLACE INTO `permissions` SET `ID` = %u, `permName` = ‘%s’, `permKey` = ‘%s’”,$_POST['permID'],$_POST['permName'],$_POST['permKey']);
    mysql_query($strSQL);

    It seems that an ill-intentioned user could use an apostrophe in the value of ‘permKey’ to effectively convert that “REPLACE INTO” query into just about anything.

    I don’t mean to be pedantic, but if the tutorial is intended to teach readers how to implement a security module, it’s a little surprising not to see validation and parameter-binding, at minimum.

    ( Reply )
  90. PG

    EllisGL April 10th

    Now to integrate OpenID with it.

    ( Reply )
  91. PG

    Ejaz April 10th

    A great indepth article. I would be very happy if you write 2nd part of this article. About securing the code (e.g. from sql injection or related attacks.) and Integration of OpenID.

    A 5 star tutorial.

    ( Reply )
  92. PG

    INDK April 13th

    Mind Blowing Tutorial…!!!

    ( Reply )
  93. PG

    BillA April 20th

    Is there any plan to update this tutorial to make it a working code demo or will it remain as a theoretical exercise for users to play with?

    JW has plans to bring us all into Nettuts Plus but if the free stuff is going to be so highly manipulated (incomplete example, typos galore, missing code comments, no established plan or schema to explain this code integration into a practical web development) as this, the concern will be there about the value for money.

    ( Reply )
  94. PG

    Clifford April 20th

    WOW, really gone out of your way here. I really enjoy in-depth articles as such. Mind Blowing. Good job!

    ( Reply )
  95. PG

    BillA April 22nd

    Is there any method to get help in fixing some of the coding mistakes in this tutorial. I have documented and fixed several but would appreciate some assistance with the current error. If I could follow some of the logic possibly I could fix this and some of the remaining problems.

    Notice: Undefined index: action in \wamp\www\ACL\admin\users.php on line 62

    ( Reply )
  96. PG

    Bianchi Boy April 27th

    Very good tutorial, but surprised that you didn’t use mysqli or PDO. Is that code SQL Injection proof ?

    ( Reply )
  97. PG

    Ahmed Awawda May 5th

    Great job.

    ( Reply )
  98. PG

    ellisgl May 5th

    After reviewing the code and testing it, I have some sugguestions:
    1.) Don’t use short tags. They will die in PHP 6.
    2.) PHP 4 has been put to rest, I wouldn’t worry about supporting it.
    3.) There are several security issues.
    a.) Sessions are not secured, this is a simple simple fix to that issue - which it can be expanded even more to provide tighter security:
    // Start the session
    session_start();

    // Secure the session.
    if(isset($_SESSION['HTTP_USER_AGENT']))
    {
    if($_SESSION['HTTP_USER_AGENT'] !== md5($_SERVER['HTTP_USER_AGENT']))
    {
    session_regenerate_id();
    $_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
    }
    }
    else
    {
    $_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
    }

    b.) Put you access check before checking for a post and doing things to the DB. Your code had it after the post check.

    4.) Put the session and ob_start in another file instead of the db include file.
    5.) The db connection file could be written as:
    $hasDB = false;
    $server = ‘localhost’;
    $user = ‘root’;
    $pass = “”;
    $db = ‘acl’;
    $link = mysql_connect($server, $user, $pass) or die(’Could not connect to server: ‘.$server);
    $hasDB = true;

    mysql_select_db($db);

    This removes unneeded if/else statements.

    6.) There were several if($a != true), where if($a !== true) would be more desired. Also with string you can do the same thing, specially if they will be nothing more than a string.

    7.) “REPLACE INTO” can be dangerous since it delete the row then inserts it. Use “ON DUPLICATE KEY” stuff:
    $sql = sprintf(’INSERT INTO `permissions`
    SET `ID` = %u,
    `permName` = “%s”,
    `permKey` = “%s”
    ON DUPLICATE KEY
    UPDATE `ID` = `ID`,
    `permName` = `permName`
    `permKey = `permKey`’, $_POST['permID'], mysql_real_escape_string($_POST['permName']), mysql_real_escape_string($_POST['permKey']));

    7.) Use mysql_real_escape_string for an extra cya. Maybe also htmlentities along with that.

    ( Reply )
    1. PG

      zoman May 12th

      Thanks….

      ( Reply )
      1. PG

        ellisgl May 21st

        No problem. Just want to make an ACL isn’t giving access to just anyone ya know. =)

  99. PG

    pz4tfv May 16th

    huilo vagin 2

    ( Reply )
  100. PG

    xqyxuu May 16th

    message 20k-30k

    ( Reply )
  101. PG

    vk633v May 17th

    Hello, Very nice site. Universe help us, dont worry man.

    ( Reply )
  102. PG

    Martin May 17th

    I love it! I was wondering how the heck this ACL system is working and now I get it (I’ve read the whole thing!). I’m going to implement it into my CodeIgniter application. :)

    ( Reply )
    1. PG

      ellisgl May 21st

      Funny, I’m going to use it Kohana. =)

      ( Reply )
      1. PG

        ellisgl May 21st

        Of course with all my changes I’ve mentioned here and several others that I did not. A super tight login system that will integrate with it and some extra tables for quick and super quick look ups.

  103. PG

    chimad21 May 22nd

    great!!!!

    ( Reply )
  104. PG

    PHPnewb June 1st

    So how would one go about implementing a login page with this tutorial?

    ( Reply )
  105. PG

    NickHill June 10th

    after running install.php all i get is aload of random php code, any help will be helpful thanks

    ( Reply )
  106. PG

    Swany June 13th

    is there anybody on aol im that would want to chat with me about this tutorial?

    ( Reply )
  107. PG

    Mmahiya June 19th

    Very help full but its read to hard.!!!

    ( Reply )
  1. Arrow
    Gravatar

    Your Name
    June 19th