
After writing the last article about the
implementation of the system of separation of access rights in a web application , a lot of interesting comments appeared. They mostly argued that it could be made even better.
In fact, the system is not optimized now and cannot be used on servers with high attendance (since the previous article was written more for review).
Let's try to fix it.
In this article I will review:
1. Bit fields optimization
2. Serialize with denormalization of DB tables
3. You will learn how a system similar to Zend ACL works.
')
Where did we leave off
We stopped at the fact that for each object in the database we have the rights for a specific action by the user or group.
It clearly looks like a two-dimensional table (action, group):
| message_view | message_create | message_delete | message_edit |
User21 | + | + | | + |
---|
Ban | - | - | - | - |
---|
Users | + | | | |
---|
Admin | + | + | + | + |
---|
The first thing that came to mind was the possibility of combining bits for each user into groups.
The transformation is the first. The use of bit masks.
We can have as many users as we want. And the number of actions with the object is constant.
Consequently, it is possible to record all actions for a specific user in the form of a binary mask in the database at once.
Instead of pluses there will be 1, no action 0.
What do we win?
A variety of actions in a web application usually does not exceed more than 64. Therefore, for most cases, 64 bits will be more than enough. (8 bytes)
For comparison, each cell of such a database will occupy 40 bytes. This is the same as if we keep each action separately, only in this case, we will have several entries per user. Face savings :)
The new table will now look like this:
| allow | disallow |
---|
User21 | 1101 | 0000 |
---|
Ban | 0000 | 1111 |
---|
Users | 1000 | 0000 |
---|
Admin | 1111 | 0000 |
---|
It became less obvious for a person, but more clearly for a computer.
Now our (described in the last article) database looks like:
rights_action |
---|
ObjectRightsID: int (pk) | UserGroupID: int (fk) | Allow: VARBINARY (4) | Disallow: VARBINARY (4) |
---|
rights_group (sets groups to users) |
---|
UserRightsID: int (pk) | UserGroupID: INT |
---|
When combining the rights of sub-objects with global categories of objects, one can use simple OR (A | B for allow) and SUB (A &! B for disallow) operations.
Now let's deal with the interface problems for the programmer so that the bits in our program are not confused.
We introduce a bit definition for the action:
public $actions=array();
/* SetAction
@param {string} ActionName -
@param {int} bitNumber -
@return {Object ObjRights} -
*/
public function SetAction($ActionName, $bitNumber){
//
if (array_search($bitNumber, $ this ->actions))
return false ;
//
if (isset($ this ->actions[$ActionName]))
return false ;
//
$clone = clone $ this ;
$clone->actions[$ActionName]=$bitNumber;
return $clone;
}
Let's do a little class refactoring from the last article. First we divide user classes and actions. This is necessary because our actions should not be connected to the user, but they will be tied to objects. (For example, message objects will have actions of reading, deleting, editing. Objects of user accounts - actions of merging, viewing, etc.)
What to do so that the bits of the action do not overlap?
In the case when each action was determined not by the number of bits, but by a string (for example, 'message_view'), everything was completely clear to programmers. There was a certain agreement (for example, to define actions.
Name of the object: name of the action ), which gave clarity to which object it belonged and multiple options for the absence of intersection.
To maintain compatibility in the database, you can use either a separate common database.
Object type-> action-> bit number , or agree to use one file for all.
I will give an example of such a file:
//
$MessageRights= new ObjRights();
//
$MessageRights=$MessageRights->SetAction( 'message_view' ,1)->SetAction( 'message_read' ,2)->SetAction( 'message_edit' ,3)->SetAction( 'message_delete' ,4)->SetAction( 'message_create' ,5);
//
$MessageRights=$MessageRights->SetAction( 'comment_view' ,6)->SetAction( 'comment_create' ,7)->SetAction( 'comment_delete' ,8);
//... , .
//
$UserRights= new ObjRights();
$UserRights=$UserRights->SetAction( 'user_edit' ,1)->SetAction( 'user_delete' ,2)->SetAction( 'user_create' ,3);
Now it's time to deal with our users and get a ready, working program.
In the case of users, nothing changes. The only change is that we will need to use the user rights object for different classes separately.
//
class UsrRights{
public $groupID=array();
function __construct($rightsID){
// () ,
$result=mysql_query( "SELECT `group_rights`.groupID FROM `group_rights` WHERE `group_rights`.rightsID = $rightsID" );
$ this ->groupID = array();
while ($t=mysql_fetch_assoc($result)){
// ID
$ this ->groupID[] = $t[ 'groupID' ];
}
mysql_free_result($result);
}
}
//
class ObjRights{
public $actions=array();
public $groupallow=array(),$groupdisallow=array();
/* SetAction
@param {string} ActionName -
@param {int} bitNumber -
@return {Object ObjRights} -
*/
public function SetAction($ActionName, $bitNumber){
//
if (array_search($bitNumber, $ this ->actions))
return false ;
//
if (isset($ this ->actions[$ActionName]))
return false ;
//
$clone = clone $ this ;
$clone->actions[$ActionName]=$bitNumber;
return $clone;
}
/* include_right
@param {int} RightsID -
@return {Object ObjRights} -
*/
public function include_right($RightsID){
$clone=clone $ this ;
$result=mysql_query( "SELECT * FROM `action_rights` WHERE `action_rights`.rightsID = $RightsID" );
while ($t=mysql_fetch_assoc($result)){
//
$clone->calculate_allow($t[ 'groupID' ],$t[ 'allow' ],$t[ 'disallow' ]);
}
mysql_free_result($result);
return $clone;
}
/* calculate_allow
,
@param {int} GroupID -
@param {string} AllowMask -
@param {string} DisallowMask -
*/
private function calculate_allow($GroupID, $AllowMask, $DisallowMask){
if (isset($ this ->groupallow[$GroupID])){
// - -
$len=min(strlen($ this ->groupallow[$GroupID]),strlen($AllowMask));
for ($i=0;$i<$len;$i++)
//Allow |= Mask
$ this ->groupallow[$GroupID]{$i}=chr(ord($ this ->groupallow[$GroupID]{$i})|ord($AllowMask{$i}));
// - -
$len=min(strlen($ this ->groupdisallow[$GroupID]),strlen($DisallowMask));
for ($i=0;$i<$len;$i++)
//Disallow |= Mask
$ this ->groupdisallow[$GroupID]{$i}=chr(ord($ this ->groupdisallow[$GroupID]{$i})|ord($DisallowMask{$i}));
} else {
$ this ->groupallow[$GroupID]=$AllowMask;
$ this ->groupdisallow[$GroupID]=$DisallowMask;
}
}
/* isAllow
@param {Object UsrRights} UserRights - ,
@param {string} ActionName -
@return {bool} - ?
*/
public function isAllow($UserRights, $ActionName){
// ?
if (!isset($ this ->actions[$ActionName]))
return false ;
//
$Actionbit=$ this ->actions[$ActionName];
foreach ($UserRights->groupID as $grpname){
//
if ($ this ->checkgrp($grpname,$Actionbit))
// ,
return true ;
}
return false ;
}
/* checkgrp
@param {int} GroupID -
@param {int} bit -
@return {bool} -
*/
private function checkgrp($GroupID, $bit){
// ?
if (isset($ this ->groupallow[$GroupID])){
// Allow & NOT Disallow & bit
if ((ord($ this ->groupallow[$GroupID]{$bit>>3})&(~ord($ this ->groupdisallow[$GroupID]{$bit>>3}))&(1<<($bit&7)))!=0){
return true ;
}
}
return false ;
}
}
* This source code was highlighted with Source Code Highlighter .
Using this library is easy. You must first allocate the permissible rights to the object classes (example above), and then initializing the user (me, his identifier for groups of rights), check him with the necessary rights in the object:
//
$MessageRights = new ObjRights();
$MessageRights = $MessageRights->SetAction( 'message_view' ,0)->
SetAction( 'message_read' ,1)->
SetAction( 'message_edit' ,2)->
SetAction( 'message_delete' ,3)->
SetAction( 'message_create' ,4);
//...
// ()
$CurrentUserRights = new UseRights($CurrentUser->rightsID);
//...
// , .
$PageRights = $MessageRights->include_right($MainPage->rightsID);
//, ?
if ($PageRights->isAllow($CurrentUserRights, 'message_view' )){
//, . ?
//
foreach ($MainPage->Messages as $msg){
// (parent), (child)
$MsgRights = $PageRights->include_right($msg->rightsID);
//
if ($MsgRights->isAllow($CurrentUserRights, 'message_view' )){
// , ?
if ($MsgRights->isAllow($CurrentUserRights, 'message_edit' ))
$msg->editable_flag = 1;
// ?
if ($MsgRights->isAllow($CurrentUserRights, 'message_delete' ))
$msg->delete_flag = 1;
DrawMessage($msg);
}
}
}
As a result, compared to the example from the first article, we have several times accelerated access to the database and reduced the number of hits.
But is it even faster?
Yes, you can make a few more code optimizations, but I will not, so as not to lose visibility (and so the code has come over too much :)).
We will just go another way, improving the algorithm ...
The transformation is the second. Getting ready PHP objects from the database.
Now we will talk about how builders of sites with high user traffic optimize their code.
The creators of PHP have created two functions with which you can save and get ready-made structures (arrays, objects) data.
These are the functions serialize and unserialize.
And how will they help us?
Now in our code, when selecting each message (object) that has (unchanged) its rights, there is an additional selection from the rights_action table. For each object - this sample is the same until we decide to change it (adding new rights to the object). In one change of the rights of our object, there are more than a thousand / millions of readings.
Why do several unnecessary conversions to the format we need? This is how each reading occurs. Thus, this can be done only once (when generating / changing rights) and save the finished result. To save the result and restore, the functions serialize / unserialize will help us.
We will finalize our library:
public function include_right($grp){
// (child)
$clone=clone $ this ;
// ,
$result=unserialize($grp);
// array('GroupID'=>array('allow_bits','disallow_bits'),'GroupID2'=>array('allow_bits','disallow_bits'),...)
foreach ($result as $groupID=>$allow)
// ,
$clone->include_action($groupID,$allow[0],$allow[1]);
return $clone;
}
It became easier and clearer!
True in this case, we have the problem of changing the rights (for each mask). Let's try to write functions for this:
public function export_object_rights(){
return serialize($this->selfrights);
}
public function allow_group($GroupID, $ActionName){
if (!isset($ this ->actions[$ActionName]))
return false ;
$bit=$ this ->actions[$ActionName];
if (isset($ this ->groupallow[$GroupID])){
$ this ->groupallow[$GroupID]{$bit>>3}|=(1<<($bit&8));
} else {
$ this ->groupallow[$GroupID]{$bit>>3}=(1<<($bit&8));
for ($i=0;$i<($bit>>3);$i++)
$ this ->groupallow[$GroupID]{$i}=chr(0);
}
if (isset($ this ->selfrights[$GroupID])){
$ this ->selfrights[$GroupID][0]{$bit>>3}|=(1<<($bit&8));
} else {
$ this ->selfrights[$GroupID]=array();
$ this ->selfrights[$GroupID][0]{$bit>>3}=(1<<($bit&8));
for ($i=0;$i<($bit>>3);$i++)
$ this ->selfrights[$GroupID][0]{$i}=chr(0);
$ this ->selfrights[$GroupID][1]= "" ;
}
}
public function reset_group($GroupID, $ActionName){
if (!isset($ this ->actions[$ActionName]))
return false ;
$bit=$ this ->actions[$ActionName];
if (isset($ this ->groupallow[$GroupID])){
$ this ->groupallow[$GroupID]{$bit>>3}&=255^(1<<($bit&8));
}
if (isset($ this ->groupdisallow[$GroupID])){
$ this ->groupdisallow[$GroupID]{$bit>>3}&=255^(1<<($bit&8));
}
if (isset($ this ->selfrights[$GroupID])){
$ this ->selfrights[$GroupID][0]{$bit>>3}&=255^(1<<($bit&8));
$ this ->selfrights[$GroupID][1]{$bit>>3}&=255^(1<<($bit&8));
$nul= true ;
for ($i=0;$i<strlen(selfrights[$GroupID][0]);$i++)
if (selfrights[$GroupID][0]{$i})
$nul= false ;
for ($i=0;$i<strlen(selfrights[$GroupID][1]);$i++)
if (selfrights[$GroupID][1]{$i})
$nul= false ;
if ($nul)
unset(selfrights[$GroupID]);
}
}
public function disallow_group($GroupID, $ActionName){
if (!isset($ this ->actions[$ActionName]))
return false ;
$bit=$ this ->actions[$ActionName];
if (isset($ this ->groupdisallow[$GroupID])){
$ this ->groupdisallow[$GroupID]{$bit>>3}|=(1<<($bit&8));
} else {
$ this ->groupdisallow[$GroupID]{$bit>>3}=(1<<($bit&8));
for ($i=0;$i<($bit>>3);$i++)
$ this ->groupallow[$GroupID]{$i}=chr(0);
}
if (isset($ this ->selfrights[$GroupID])){
$ this ->selfrights[$GroupID][1]{$bit>>3}|=(1<<($bit&8));
} else {
$ this ->selfrights[$GroupID]=array();
$ this ->selfrights[$GroupID][1]{$bit>>3}=(1<<($bit&8));
for ($i=0;$i<($bit>>3);$i++)
$ this ->selfrights[$GroupID][1]{$i}=chr(0);
$ this ->selfrights[$GroupID][0]= "" ;
}
}
* This source code was highlighted with Source Code Highlighter .
Eerie looks, is not it true-Lee? I didn’t even check this code :), I was just scared - everything is so creepy. Such code in red and white tells us that something in the architecture is wrong.
I propose to cut off unnecessary and unnecessary tails! Make the code readable and more versatile.
Mode extra tails!
Our structure now looks like this:
array (/ * GroupID * / 1 => array (allow => 0b100101000100100010 / * bits * /, disallow => 0b100010010000010001010), 4 => ...)
In this form, it serializes to it and it is written to the object.
But, firstly, it is difficult to work with actions (you have to add new ones by editing the PHP file).
Secondly, the search for bits is not so easy. And let's try to return to the original version, discussed in the previous article.
That is, now the access rights will look like this:
array (/ * GroupID * / 1 => array (/ * Actions * / 'message_read' => 1 / * + * /, 'message_edit' => 0 / * - * /), 4 => ...)
Thereby:
1) The search for the desired action is done by one command $ array [GroupID] [ActionName]. (I am sure that this will help speed up the search process)
2) No need to add action. All of them will be stored in our facility.
3) Sampling will occur in one cycle, only by user groups.
4) User groups themselves can take on a human appearance.
The only visual negative - the structure will take up more space. Poorly? Yes, but in optimization it is always the case - either the load on the CPU, or the amount of memory. Yes, and the memory of the database it does not eat much, so we proceed.
//
class ObjRights{
public $groups=array();
public $selfgroups=array();
/* */
/* include_right
@param {serialize array} RightsID -
@return {Object ObjRights} -
*/
public function include_right($RightsID){
$clone=clone $ this ;
// SQL-,
$clone->selfgroups = unserialize($RightsID);
foreach ($clone->selfgroups as $groupID=>$actions){
if (isset($clone->groups[$groupID])){
// ,
foreach ($actions as $actname=>$allow){
if (isset($clone->groups[$groupID][$actname]))
// 1 - allow, 0 - disallow
$clone->groups[$groupID][$actname]&=$allow;
else
$clone->groups[$groupID][$actname]=$allow;
}
} else
// ,
$clone->groups[$groupID]=$actions;
}
return $clone;
}
/* isAllow
@param {array} UserRights - ,
@param {string} ActionName -
@return {bool} - ?
*/
public function isAllow($UserRights, $ActionName){
foreach ($UserRights as $groupname){
//
if (isset ($ this ->groups[$groupname]) &&
isset ($ this ->groups[$groupname][$ActionName]) &&
$ this ->groups[$groupname][$ActionName])
return true ;
}
return false ;
}
/* export_rights
. (serialize array)
@return {string} - serialize array
*/
public function export_rights(){
return serialize($ this ->selfgroups);
}
/* */
/* allow
@param {int} GroupID -
@param {string} ActionName -
*/
public function allow($GroupID, $ActionName){
if (!isset($ this ->selfgroups[$GroupID]))
$ this ->selfgroups[$GroupID]=array();
$ this ->selfgroups[$GroupID][$ActionName] = 1;
}
/* disallow
@param {int} GroupID -
@param {string} ActionName -
*/
public function disallow($GroupID, $ActionName){
if (!isset($ this ->selfgroups[$GroupID]))
$ this ->selfgroups[$GroupID]=array();
$ this ->selfgroups[$GroupID][$ActionName] = 0;
}
/* reset
@param {int} GroupID -
@param {string} ActionName -
*/
public function reset($GroupID, $ActionName){
if (isset($ this ->selfgroups[$GroupID]) && isset($ this ->selfgroups[$GroupID][$ActionName]))
unset($ this ->selfgroups[$GroupID][$ActionName]);
if ($ this ->selfgroups[$GroupID] === array())
unset ($ this ->selfgroups[$GroupID]);
}
}
* This source code was highlighted with Source Code Highlighter .
All these records will be stored in the database, right in a separate field of the table object. That is, like this:
page |
---|
pageID | page_name | page_rights (rights in the serialize form) |
---|
messages |
---|
messID | pageID | message_rights (rights in the serialize form) | message_header | message_text |
---|
What else can you do?
For fans of JSON, you can use instead of serialize / unserialize => json_encode / json_decode, which in theory should work faster and take up less space in the database.
For lovers of more humane group names, nothing is required in order to change the whole GroupID from int to string.
You can use $ _SESSION, if the user has logged in, to memorize all his rights $ User-> current_user-> rights (and other user information), in order not to access the database and not to perform unnecessary actions. The only drawback is that in order for the user to change access rights, he will need to log in.
Working example
Bitmasks:
Working exampletest0.phprights0.phpSerialize:
Working exampletest.phprights.phpAfterword
Zend offers a very similar mechanism for working with rights (the latest example of Serialize).
You have already learned the basics here (how it functions and where your legs grow from).
Zend_Acl offers a similar interaction description library. Who exactly how to use Zend_Acl can read on the
Internet . I think that after this article you will quickly understand.
ps thanks to everyone who left comments on the last article on this topic. Without you there would not be this article.