Implement bitmask or relational ACL in PHP

2.2k views Asked by At

I think PHP people are familiar with the E_ALL and various other bitmask constants from the error_reporting() function. They are number constants, example: E_ALL means 32676 and E_NOTICE means 8. I can say that I want all errors but notice shown, and I do this by passing E_ALL & ~E_NOTICE as the argument of error_reporting(). But essentially, I tell it 32759 which is 32767 - 8.

These bitmasks are taking their values from the output set of f(x) = 2^x function, and doing add and subtract arithmetics on these values, we can finetune what errors to get.

I am thinking about a more configurable access control thing to be implemented into my framework-thingy. For this, I wish to set the user's bitmask would have the same method of adding number values together, the problem is that I don't know how to pull this off, how to check against the requested and the present value: Does the user have rights to access foobar?.

And the other problem is scalability. I might only have 31 unique bits (as 2^32 reaches the too big and unmaintainable state), and it would be hard to migrate if I need to undergo (not really planned right now) this barrier? My other thought for access control would be setting up a table which links together the user.id and the integer value of the access bit he has.

To wrap it up, Which of the following two options is a better solution?:

  • Use one field in the user's table to store bitmasks, and then fetch the requested bit from it when needed.
  • Have a set of access flags defined in a table and have a relation table which links the user to the access, exploiting the awesomeness of relational databases to our needs.

I was researching about the second method, but I don't know which one would be the best to use.

2

There are 2 answers

0
Whisperity On BEST ANSWER

Bitmasks

The original problem with bitmasks is that they go against the conventions of data modelling (expressed in another answer here, with further reading here and here). A 4-byte signed integer might only contain 31 different values (represented in integer as 2 147 483 648) and calculation is hard with them. There have been various questions on Stack Overflow before discussing this topic, which I used to get a sense of implementation of how bistmasks would work.

Testing also revealed that working with bitmasks is hard. It needs a sense to understand the bitwise operators, and migration becomes basically impossible. (By impossible I mean that, at first, implementing bitmasks seemed a good thing to do, but after all it turned out it requires too much expends compared to the benefit it could give in return.) Take a basic portal-like website... I mean no. Take Stack Overflow as an example, and how much unique privileges it has. I have actually tried to make a count but lost myself in complexity, but the amount we would need is way close, if not already over the said barrier of 31 unique values. There is a very likely chance that an update on the project which alters bitmask meanings result in a long need of recalculation, and it is a lot more error prone if confronted by database errors.

While I don't have the exact, pinpoint timing figures, using bitmasks felt slower than ACL. Comparing database sizes, memory and storage footprint is bigger and we have less chance to exploit the relational database and indexing capabilities. For user permissions on a website, bitmasks are a no-go, if we have other methods we might use.

There are various systems where bitmasks do works, namely MaNGOS (from which I originally came up with the idea), where item_template and various other template tables define flags which makes use of bitmasks. But the bottom line is that in these tables, the values are unlikely to ever change, the calculation is read-only, contrary to websites where users arbitrarily obtain and lose privileges.

Example code

define('U_NIL', 0);
define('U_EXEC', 1);
define('U_WRIT', 2);
define('U_READ', 4);

$our_perm = 7;
$req_perm = U_EXEC | U_WRIT | U_READ;
var_dump( ($our_perm & $req_perm) == $req_perm ); # Will be bool(true)

$our_perm = 3;
var_dump( ... The same thing ...); # Will be bool(false)

$our_perm = U_READ;
$req_perm = U_READ | U_WRIT;
var_dumo(...); # Will be bool(false)

$our_perm = U_READ | U_WRIT | U_EXEC;
$req_perm = U_READ;
var_dump(...); # Will be bool(true)

Etcetera.

I will spare you the lines of code because various other questions describe the method aptly, in a way I will never be able to describe it. Bitmasks seem to be nice, bitmasks seem to be exotic, but there is no point letting them settle in production.

ACL

The other option which was described in the original question was to exploit the relational database and the SQL language to set up the permissions in the database. We will need to create two more tables into our database:

CREATE TABLE `perm_relation` (
    `row_id` int(10) NOT NULL AUTO_INCREMENT,
    `user_id` int(10) NOT NULL,
    `perm_id` int(10) NOT NULL,
    PRIMARY KEY (`row_id`),
    UNIQUE `permission` (`user_id`, `perm_id`)
) ENGINE=InnoDB;

CREATE TABLE `permission` (
    `id` int(10) NOT NULL AUTO_INCREMENT,
    `name` varchar(64) NOT NULL,
    PRIMARY KEY (`row_id`)
) ENGINE=InnoDB;

perm_relation.perm_id will be the foreign key pointing to permission.id, and perm_relation.user_id will be our relation to users.id. After this, it's only a matter of how we put together our programming logic.

SELECT row_id FROM perm_relation
WHERE perm_id = (SELECT id FROM permission WHERE name = "U_SUPERADMIN")
AND user_id = CURRENT_USER_ID;

Using this method we bind towards greater compatibility, quicker execution and easier migration. It takes only a neck of time to add new permissions, both as defining new ones and grating some to arbitrary users. Deletion is just as easy and it takes only a small SQL query to optimize our tables to remove orphan entires (for example: users having permissions given to them without related permission defined in the system).

Because we are implementing this system into a PHP environment, we can assume that there will be some sort of administrative page with many features relying on this permission list. An example of listing users filtered by the permission they have would be one example. In this context, ACL is a lot better than bitmasks, because we can take further exploits of JOIN statements... and even beyond.

The conclusion is that bitmasks and the relational pattern has different purposes. Bitmasks are too much and very bulk for a user permission system, while relational tables would be an overkill in the MaNGOS example mentioned above.

0
Knarf On

You should never have a big integer in a database representing several diffrent values. One value per field, thats the first law of data moddeling (or something).

In other words, you should have a second table with say; an userID field and and the accessbit.

userPriviliges:

  • userID, int, primary
  • privilige, int