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.

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:

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).

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.

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: 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. Open /admin/users.php and add the following code: 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':

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:

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. Assign Roles Our assign roles form will end up looking like this: Add this code right below the previous code block: 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: 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.

Saving the Data

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.

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:

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 -->:

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

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:

Saving the Data

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

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.

List Permissions

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

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:

Edit Permission

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

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.

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.

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:

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

Step 8: Implementing the ACL on Your Site

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:

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:

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.