Wednesday, December 2, 2015

PHP Restrict Objects Access based on Security Profiles

When you are building a web application or a web site and you want to restrict access to pages, links, buttons, etc. you can take many approaches, one approach may be to hide the objects you do not want the user to have access to, another approach is to gray/disable objects that are restricted. Both approaches have their advantages and disadvantages. I know customers that do not like things to disappear, they like more a homogeneous interface and some other customers do not like to see options that are not available. So which approach is the best, it depends on your customer and on your application design.

No matter what approach you use, you need to be able to uniquely identify such objects, that way you may create different profiles with different accesses to different elements or objects, but what happens if you have hundreds or thousands objects that you want to control its access.

You can give an ID to each object and when a user logs in, it will load all the allowed IDs into a memory array and you will have to query that array for every restricted object. That may be a lot of memory. Another approach is to query the database for each of the restricted objects, but again you may need to query the database several times on each page or create an array at the beginning and query it for each object.

The approach I like is to uniquely identify each restricted object using a bit, this means that for each byte I can store 8 different objects, so to uniquely identify 80 objects I only need 10 bytes, for 500 objects I need 63 bytes (500 objects/8 bits in a byte) and for 1000 I need 125 bytes.

The object IDs will be defined using a number that is divided in two parts, the first one will identify the byte position within a string and the second one will identify the bit position within that byte. The first one can be any number from 0 till whatever, depending on the number of objects we have and the second one will go from 1 till 8, the number of bits in a byte. The below code defines 3 objects, the first byte (0) defines the position in the security string and the second byte(1,2,3) defines the bit position within that byte.


 const MAIN_PAGE = "01"; // bits: 00000001 
 const MENU_ENTRY_DETAILS = "02";  // bits: 00000010 
 const MENU_ENTRY_UPDATE = "03";  // bits: 00000100

Each user will have a string that will represent its access, that string will contain all the bits that represent all the objects the user has access to. This string is usually build based on the security profile the user has in the database. 

If the user only has access to the first two elements, its security string will be defined as:

 $security = 0b00000011;   

if we want the user to have access to the second and third the security string will be defined as:


 $security = 0b00000110;   

and full access will be:


 $security = 0b00000111;   


Let's do an example to depict this approach, we will set it to hold a maximum of 80 objects (10 bytes). The below code define the restricted objects:


 const MAIN_PAGE = "01";  
 const MENU_ENTRY_DETAILS = "02";  
 const MENU_ENTRY_UPDATE = "03";  
 const MENU_ENTRY_ADD = "04";  
 const PAGE_ORDER_DETAILS = "05";  
 const PAGE_ORDER_DETAILS_EDIT = "06";  
 const PAGE_ORDER_DETAILS_ADD  = "07";  
 const MAIN_PAGE_CHANGE_PASSWORD = "08";  
 const PAGE_USERS_VIEW = "11";  
 const PAGE_USERS_ADD = "12";  
 const PAGE_USERS_EDIT = "13";  
 const PAGE_USER_DELETE = "14";  
 const EXTERNAL_LINKS = "15";  
 const MAIN_PAGE_END_SESSION = "16";  
 const MAIN_PAGE_NEW_SESSION = "17";  


As you can see each one has two digits (in this example), the first digit represents its position within the security string and the second one represents the bit position within a byte, that way the last digits goes from 1 till 8.

When the user logs in, we need to call a function that returns its security string based on its security profile, the next function will do the job for our example:


1:       public static function getSecurityString() {  
2:            $security="";  
3:            $permissions = array_fill(0,self::M_SECURITY_STRING_LEN,0);  
4:            /* begin: this should be ontained from a DB or other source*/  
5:            $objects = array();  
6:            $objects[0] = "01"; //main page  
7:            $objects[1] = "02"; //menu entry details  
8:            $objects[2] = "05"; //page order details  
9:            $objects[3] = "06"; //page order details edit   
10:           $objects[4] = "07"; //page order details add   
11:           $objects[5] = "08"; //main page change password  
12:           $objects[6] = "11"; //page users view  
13:            /* end: this should be ontained from a DB */  
14:            for ($count=0;$count<count($objects);$count++) {  
15:                 $pos = substr($objects[$count],0,strlen($objects[$count])-1);  
16:                 $value = substr($objects[$count],strlen($objects[$count])-1) - 1;  
17:                 if ($pos < self::M_SECURITY_STRING_LEN) {  
18:                      $permissions[$pos] = $permissions[$pos] | (1 << $value);   
19:                 } else {  
20:                      //this should not happen, raise an error  
21:                 }  
22:            }  
23:            for ($count=0; $count < self::M_SECURITY_STRING_LEN; $count++) {  
24:                 $security = $security . pack("C1",$permissions[$count]);  
25:            }  
26:            return $security;  
27:       }  

Lets talk about the above code:

Line 3: initialized the security string with zeros. (no access)
Lines 4-13: Define the objects the user has access to, this will usually come from an external source such as a database. It is hard coded here for simplicity.
Line 14: Executes for every object
Line 15: Extracts the byte position from the security string
Line 16: Extracts the bit position within the byte.
Line 18: Sets the bit from the right byte of the security string
Line 23: Executes for every byte of the security string
Line 24: Builds the security string attaching all the bytes.

Once we have the security string, we can store it on a variable or session variable.


 $secStr = Security::getSecurityString();  

Now for each restricted object you will have code similar to the below one:


1:  if (Security::isAllowed($secStr,Security::PAGE_USER_DELETE)) {  
2:       echo "access is granted";  
3:       //execute you restricted code  
4:  } else {  
5:       echo "access is denied";  
6:       //execute the code when access is denied or hide/gray the elements  
7:  }  

The function isAllowed is defined as follows:


1:  public static function isAllowed($_security, $_ID) {  
2:            $pos=0;  
3:            $value=0;  
4:            $secValue=0;  
5:            $secString=0;  
6:            $pos = substr($_ID,0,strlen($_ID)-1);  
7:            $value = substr($_ID,strlen($_ID)-1) - 1;  
8:            $secArray = unpack('C1', $_security{$pos});  
9:            $secString = $secArray[1];  
10:            $secValue = (1 << $value);  
11:            if ( ($secString & $secValue) != 0) {  
12:                 return true;  
13:            } else {  
14:                 echo "Access Denied to object: " . $_ID;  
15:            }  
16:            return false;  
17:       }  

Line 6: Retrieves, from the element ID, the byte position within the security string.
Line 7: Retrieves, from the element ID,  the bit position within the byte.
Line 8-9: From the security string, retrieves the byte based on the previously obtained position. 
Line 10: Based on the bit position obtained in line 7, we create a byte with that bit set.
Line 11: Compares the bit set in the security string with the one of the element ID.
Line 12: If the value is not zero is allowed.

You need to add some validations in these functions that were omitted for simplicity, for example, in the isAllowed function, you need to make sure that the position is not bigger than the length of the security string.

The definition of the Security class is as follows:


1:  class Security {  
2:       private $m_security="";  
3:       const M_SECURITY_STRING_LEN = 10;  
4:       const TOTAL_OBJECTS = 15;  
5:       const MAIN_PAGE = "01";  
6:       const MENU_ENTRY_DETAILS = "02";  
7:       const MENU_ENTRY_UPDATE = "03";  
8:       const MENU_ENTRY_ADD = "04";  
9:       const PAGE_ORDER_DETAILS = "05";  
10:       const PAGE_ORDER_DETAILS_EDIT = "06";  
11:       const PAGE_ORDER_DETAILS_ADD  = "07";  
12:       const MAIN_PAGE_CHANGE_PASSWORD = "08";  
13:       const PAGE_USERS_VIEW = "11";  
14:       const PAGE_USERS_ADD = "12";  
15:       const PAGE_USERS_EDIT = "13";  
16:       const PAGE_USER_DELETE = "14";  
17:       const EXTERNAL_LINKS = "15";  
18:       const MAIN_PAGE_END_SESSION = "16";  
19:       const MAIN_PAGE_NEW_SESSION = "17";  
20:       public static function getSecurityString() {  
21:            $security="";  
22:            $permissions = array_fill(0,self::M_SECURITY_STRING_LEN,0);  
23:            /* begin: this should be ontained from a DB or other source*/  
24:            $objects = array();  
25:            $objects[0] = "01"; //main page  
26:            $objects[1] = "02"; //menu entry details  
27:            $objects[2] = "05"; //page order details  
28:            $objects[3] = "06"; //page order details edit   
29:            $objects[4] = "07"; //page order details add   
30:            $objects[5] = "08"; //main page change password  
31:            $objects[6] = "11"; //page users view  
32:            /* end: this should be ontained from a DB */  
33:            for ($count=0;$count<count($objects);$count++) {  
34:                 $pos = substr($objects[$count],0,strlen($objects[$count])-1);  
35:                 $value = substr($objects[$count],strlen($objects[$count])-1) - 1;  
36:                 if ($pos < self::M_SECURITY_STRING_LEN) {  
37:                      $permissions[$pos] = $permissions[$pos] | (1 << $value);   
38:                 } else {  
39:                      //this should not happen, raise an error  
40:                 }  
41:            }  
42:            for ($count=0; $count < self::M_SECURITY_STRING_LEN; $count++) {  
43:                 $security = $security . pack("C1",$permissions[$count]);  
44:            }  
45:            return $security;  
46:       }  
47:       public static function isAllowed($_security, $_ID) {  
48:            $pos=0;  
49:            $value=0;  
50:            $secValue=0;  
51:            $secString=0;  
52:            $pos = substr($_ID,0,strlen($_ID)-1);  
53:            $value = substr($_ID,strlen($_ID)-1) - 1;  
54:            $secArray = unpack('C1', $_security{$pos});  
55:            $secString = $secArray[1];  
56:            $secValue = (1 << $value);  
57:            if ( ($secString & $secValue) != 0) {  
58:                 return true;  
59:            } else {  
60:                 echo "Access Denied to object: " . $_ID;  
61:            }  
62:            return false;  
63:       }  
64:  }  

With this approach, you can restrict access to many objects using little memory and I/O. I have use it in a Java environment and it works very good. This PHP version has not been tested, so please let me know if you find issues or improvements.


No comments:

Post a Comment