46

Basic deal is, we have a custom built "kickstart" for our projects. For this we are looking at redoing the user control. I know there are a lot of questions out there about general rbac, but I cannot find any on hierarchical rbac?

Our requirements are:

  • Roles can be assigned to group permissions
  • If the role does not have a permission entry then it is automatically denied
  • A user can be given overriding permissions
  • A users overriding permissions is either a grant or deny
  • If a user is explicitly denied a permission no matter what roles say "granted" the override wins.
  • Users can have multiple roles
  • Roles can have hierarchy
  • Roles can inherit from other roles (e.g. A "Forum Super Moderator" role is a "Forum Moderator", and a "System Maintainer", and the "Forum Moderator" role already inherits from the "Forum User" role )
  • Roles that inherit from another role that deny or grant a privilege override their child permission
  • Permissions are grouped by "module" (e.g. a "Blog" module can have an "edit entry" permission, and a "Forum" module can have an "edit entry" permission and they will not clash)
  • There is a "Everything and Anything" permission that automatically grants full access

So, with those requirements out of the way, here's how I am thinking of doing it.

Table: Users

id            | int     | unique id

Table: Roles

id            | int     | unique id
--------------|---------------------------------------------
title         | varchar | human readable name

Table: Permissions

id            | int     | unique id
--------------|---------------------------------------------
module        | varchar | module name
--------------|---------------------------------------------
title         | varchar | human readable name
--------------|---------------------------------------------
key           | varchar | key name used in functions

Table: Role_User

role_id       | int     | id from roles table
--------------|---------------------------------------------
user_id       | int     | id from users table

Table: Permission_Role

id            | int     | unique id
--------------|---------------------------------------------
permission_id | int     | id from permissions table
--------------|---------------------------------------------
role_id       | int     | id from roles table
--------------|---------------------------------------------
grant         | tinyint | 0 = deny, 1 = grant

Table: Permission_User

id            | int     | unique id
--------------|---------------------------------------------
permission_id | int     | id from permissions table
--------------|---------------------------------------------
user_id       | int     | id from users table
--------------|---------------------------------------------
grant         | tinyint | 0 = deny, 1 = grant

Well, actually that's half of it, that part I am sure about, the part I am getting stuck on is the hierarchical roles.

So, how do I design this? My idea is that to save on the database queries I am just going to build the permission matrix on login and save it to session so the queries don't have to be too simple as they are only run once for each login.

The issue I see is that, I am going to need to know the hierarchy of the roles so I can resolve the inherited roles permissions before I resolve the inheriting.

The user permissions is the easy part, the per-user permissions are essentially the finally resolved group.

Hailwood
  • 89,623
  • 107
  • 270
  • 423
  • Is there a reason why `user` have no `role`, but have `permission`? It's a "permisson model", not "role model", isn't it? With this approach, roles are not used anywhere. – BlitZ Apr 22 '13 at 05:06
  • Whoops, Forgot to add that table in, edited! – Hailwood Apr 22 '13 at 05:11
  • 6
    Still, I think there is no reason for `user` to have `permission`. It might have `role`s with `permission`s, but not `permission`s as they are. This thing ruining logic (IMHO). – BlitZ Apr 22 '13 at 05:15
  • 1
    @CORRUPT, A user themselves can also be given permissions, these permissions override any roles they are in. It saves having to create whole new roles with one permission just because you want User X to be able to do one extra action. Or if the user has been doing something they shouldn't you can quickly deny it for that user. Otherwise you would have to inherit the highest level role and then deny there which can get messy. – Hailwood Apr 22 '13 at 05:20
  • 2
    It's pretty much like saying that each user is their own role that automatically inherits from every other role the user has been given without actually having to create a group for the user. Does that make sense? – Hailwood Apr 22 '13 at 05:22
  • Tweaking the requirements such that records never deny, only additively grant permissions will simplify everything - forces better modelling of role hierarchy, especially the most restricted role, and simplifies permission lookup query (to an EXISTS). Denying everything by default and building on that is a [recommended approach](https://owasp.org/Top10/A01_2021-Broken_Access_Control). – Epigene Oct 13 '22 at 10:31

1 Answers1

64

There is a way to implement role inheritance by using recursive relation on table Roles, by making role reference to another record:

1:n inheritance

This relation will add 1 : n inheritance within Roles record. You might obtain whole hierarchy tree with this stored function:

CREATE FUNCTION `getHierarchy`(`aRole` BIGINT UNSIGNED)
RETURNS VARCHAR(1024)
NOT DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE `aResult` VARCHAR(1024) DEFAULT NULL;
DECLARE `aParent` BIGINT UNSIGNED;

SET `aParent` = (SELECT `parent` FROM `Roles` WHERE `id` = `aRole`);

WHILE NOT `aParent` IS NULL DO

    SET `aResult` = CONCAT_WS(',', `aResult`, `aParent`);
    SET `aParent` = (SELECT `parent` FROM `Roles` WHERE `id` = `aParent`);

END WHILE;

RETURN IFNULL(`aResult`, '');
END

Then, you might obtain all granted permissions with something like this:

SELECT
    `permission_id`
FROM
    `Permission_Role`
WHERE
    FIND_IN_SET(`role_id`, `getHierarchy`({$role}))
    AND
    grant;

If it's not enough, then you might do another table for inheritance:

n:m inheritance

But, in this case, needed another hierarchy obtainment algorithm.


To resolve overriding issue you will have to get role permissions and user permissions. Then, write user permissions over roles permissions to session.


Also, I suggest to remove grant columns in Permission_Role and Permission_User. There is no need to map every permission for each of them. Just enough to use EXISTS queries: if there is a record, then permission granted, else - it's not. If you need to retrieve all permissions and statuses, you might use LEFT JOINs.

BlitZ
  • 12,038
  • 3
  • 49
  • 68
  • This sounds good, I will look into it, I am definitely looking at having each role inheriting multiple other roles, so will have to work out the other algorithm, also wondering how to do it if we cannot create stored procedures? Point with grant or deny is not to list every permission, point for grant or deny is that If a role inherits another role, if the inherited role has been granted the permission, you can explicitly deny it in the inheriting role to deny the permission. – Hailwood Apr 22 '13 at 08:19
  • To the above comment, we can delete the record to deny the permission explicitly, isn't it? – coretechie Aug 04 '22 at 03:07
  • 1
    @coretechie Yes. Or, you can set `grant` to `false`. Also note, that some time had passed since this answer and MySQL finally got recusive query support. It now may be more optimal to select data with recursion. No stored procedures are required for such logic as of now. – BlitZ Aug 10 '22 at 12:51
  • Has anybody implemented this? GitHub? – Chef Gladiator Apr 27 '23 at 05:21
  • @ChefGladiator It was fine solution at the time, but these days MySQL [supports recursive queries](https://dev.mysql.com/doc/refman/8.0/en/with.html#common-table-expressions-recursive-hierarchy-traversal). I would suggest to look there for more robust approach without stored procedure complications. Unsure about ready-to-use components. I guess some of them depends on target ecosystem (php, python, etc). Consider checking packages with RBAC functionality. There are plenty of them. – BlitZ Apr 27 '23 at 05:26