I'm designing a very simple RBAC (Role Based Access Control) system in my PHP project and after giving it some thought I've came up with a solution, but without knowing much about building business systems I'm not sure if there are, or would be, any major design flaws with my solution.
So basically, I want to give a user a set of 'roles'. I will use these roles to allow or deny access to certain functionality on the application.
Here is the roles table:
# Roles
- id [auto-increment, unsigned]
- role [string, length:50]
# User_Roles
- user_id [FK:user_id, unsigned]
- role_id [FK:roles_id, unsigned]
Note: user_id and role_id to be unique index
The problem that concerned me was there was no information in the database about what the role actually does. But then I started to think if that was important. Because if the roles had meaningful names, and a structure, then you could query the user_roles table to get all the users roles and then within code something like:
# Fetch user with ID 1 from database
$user = User::find(1);
# Fetch the roles the user has from the database
# #returns : Array of roles
$userRoles = $user->roles()
# $userRoles = ['newmember', 'member.post', 'member.chat']
# Can the user send a message?
if(in_array('member.message', $userRoles)
{
# User can send a message
}
else
{
# User can not send a message
}
The roles can be managed, and can have whatever meaning they like within the organisation. I'm just concerned that the roles in the database have no meaning, I can't help but think there's probably a better way to achieve this.
Would my solution be feasible long term?
Thanks
Here's my approach to such RBAC systems, first I'd split the application into modules, at least logically.
Think of the modules as entities/resources in your application on which certain actions can be performed on. A resource could be a User, a Membership, a Product, ..
Let's assume we're building a site like Stackoverflow and we have the following modules:
Questions (Q&A section)
Chat
Each of these modules register themselves in the applications database table called modules which could look like:
# Modules
- id [an application-wide unique module identifier, provided by the module]
- name [string, human readable]
Each of these modules come with a set of actions which are pre-defined by the module, e.g actions to create a question, kick users from chat rooms etc. These actions get installed along with the modules into the applications database table called 'actions', which could look like:
# Actions
- module_id [reference to the modules table, is the namespace for the token]
- action [string / int, action identifier provided by the module, unique in the scope of the module]
- name [string, human readable name]
- Primary Key: [module_id, action]
Let's define the modules and actions for our Stackoverflow clone now:
Modules
+------------------------------------------+
| ID | Name |
+------------------------------------------+
| questions | Question and Answer Module |
| chat | Chat Module |
+------------------------------------------+
Along with the modules, the actions will be installed:
Actions
+-----------------------------------------------+
| Module | Action | Name |
+-----------------------------------------------+
| questions | read | Read Questions |
| questions | create | Create Questions |
| questions | edit | Edit Questions |
| questions | delete | Delete Questions |
| questions | vote | Vote on Questions |
| | | |
| chat | join | Join the Chat |
| chat | kick | Kick users |
| chat | create | Create Chatrooms |
+-----------------------------------------------+
The important thing here is that you can not modify the entries in these tables directly as admin of the systems so there's no GUI
to add/remove actions etc.
The next step is to create some roles for our system, this is done over the admin's user interface, roles can be defined arbitrarly.
Let's start off with some basic roles:
Q&A User:
- can read questions
- can create questions
Q&A Moderator:
- can read questions
- can create questions
- can vote on questiosn
- can edit questions
Q&A Admin:
- can read questions
- can create questions
- can vote on questiosn
- can edit questions
- can delete questions
Chat User:
- can join the chat
Chat Moderator:
- can join the chat
- can kick users from the chat
Chat Admin:
- can join the chat
- can kick users from the chat
- can create chat rooms
First, the roles are created in the roles table:
# Roles
- id [auto-increment, unsigned]
- name [string, length:50]
Populated with our custom definitions:
Roles
+-----------------------+
| ID | Name |
+-----------------------+
| 1 | Q&A User |
| 2 | Q&A Moderator |
| 3 | Q&A Admin |
| 4 | Chat User |
| 5 | Chat Moderator |
| 6 | Chat Admin |
+-----------------------+
In our super fancy admin UI we now have a sidebar with a list of all installed modules and their associated actions. Since
our typical admin is super lazy and doesn't know anything about programming he can now conveniently assign actions to each
role with drag&drop, i.e assigning permissions to roles.
These assigned permissions are stored in our mapping table Roles_Actions:
# Roles_Actions
- role_id
- module_id
- action
PK: [role_id, module_id, action]
The populated table:
Roles_Actions
+--------------------------------+
| Role ID | Module ID | Action |
+--------------------------------+
| 1 | questions | read |
| 1 | questions | create |
| 2 | questions | read |
| 2 | questions | create |
| 2 | questions | vote |
| 2 | questions | edit |
...
| 6 | chat | join |
| 6 | chat | kick |
| 6 | chat | create |
+--------------------------------+
Now we have to assign the roles to the users in the systems, let's say we have four users initially:
- Chuck Norris which is a Q&A Admin and also a Chat Admin (UID = 1)
- Bruce Willis which is a Q&A Moderator and a Chat User (UID = 2)
- Dilbert which is a Q&A Moderator and a Chat Moderator (UID = 3)
# User_Roles
- user_id [FK:user_id, unsigned]
- role_id [FK:roles_id, unsigned]
The populated Table:
Users_Roles
+---------------------------------+
| User ID | Role ID |
+---------------------------------+
| 1 | 3 (= Q&A Admin) |
| 1 | 6 (= Chat Admin) |
| 2 | 2 (= Q&A Moderator) |
| 2 | 4 (= Chat User) |
| 3 | 2 (= Q&A Moderator) |
| 3 | 5 (= Chat Moderator) |
+---------------------------------+
So one user can have multiple roles, and the permissions (module/action pairs) are then merged together in the application layer.
If you want to go one step further you could also provide some sort of inheritance in the role model, e.g the Q&A Moderator
could be defined as child of Q&A User, inheriting all permissions from the Q&A User and extending it with moderator rights.
So how could the authorization look like on the application layer?
Let's assume Dilbert which is a Q&A Moderator and Chat Moderator logs into the system, then you collect all his roles and all
permissions assigned to those roles. Next you start to build a permission tree and remove all duplicate permissions, the permission
tree could be represented as an associative array like:
Dilbert's permission tree:
$dilberts_permissions = array(
"questions" => array("read", "create", "vote", "edit")
"chat" => array("join", "kick")
)
For convenience we create a simple helper function like:
function has_permission($path, $tree) {
list($module, $action) = explode('.', $path);
return (array_key_exists($module, $tree) && in_array($action, $tree[$module]));
}
In our code we can now check if Dilbert is allowed to do certain things with a syntax like:
has_permission("questions.create", $dilberts_permissions); // => TRUE
has_permission("questions.delete", $dilberts_permissions); // => FALSE
has_permission("chat.join", $dilberts_permissions); // => TRUE
has_permission("chat.create", $dilberts_permissions); // => FALSE
Related
I'm trying to implement RBAC, but a User can have multiple roles, each under a particular context.
The context is based on 3 factors, Company, Product and Region.
So User A could be an Admin of Company 1, and also be a Viewer of Company 2 > Product 3 > Region 4
What is the best approach for this kind of RBAC set up?
I'm hand rolling this at the moment - so looking for the best way to structure the DB tables that would allow for this level of granular access.
Unfortunately this is on a legacy application with runs PHP CodeIgniter / mySQL - so any modern existing libs are probably not compatible.
UPDATE
I have this so far. Permissions table maps to Roles, Roles are then assigned to users, and given context.
account_id | role_id | company_id | product_id | region_id |
------------------------------------------------------------
1 | 1 | | | |
2 | 2 | 1 | | |
2 | 3 | 2 | 1 | |
3 | 4 | 3 | 1 | 2 |
3 | 4 | 3 | 1 | 3 |
3 | 4 | 3 | 1 | 4 |
The SQL for this looks a bit like this...
SELECT DISTINCT account_id
FROM user_roles ur, role_permissions rp
JOIN permissions p ON rp.permission_id=p.id
WHERE (
#correct user that has a role with the permission we want
ur.account_id = 1
AND rp.permission_id IN (1,2,3)
AND ur.role_id = rp.role_id
#if the user is sys admin they can see everything
AND (
ur.role_id = 1
#if the user is company admin
OR ( ur.role_id = 2 AND ur.company_id=#cid )
#if the user is product admin
OR ( ur.role_id = 3 AND ur.company_id=#cid AND ur.product_id=#pid )
#if the user is any other role then the need explicit access
OR ( ur.role_id > 3 AND ur.company_id=#cid AND ur.product_id=#pid AND ur.country_code=#cc )
)
);
This works, but I'm sure there must be a better way to do this.
The next issue is how to then apply a hierarchy to the roles?
So a 'Admin' of a company > product > region can only create users, of their role or lower?
I would have to look up the role they have assigned at the context we are in, and then return every role who has a lower rank than the users one?
Could this context be better placed as a string? '1.3.gb' / '1.2' ? And then when looking up the role, I form the context string first?
User table can have a cross reference to a roll table, each roll has a specific role identification number with a can_write, can_read column (ect...)
Your heircachy could look something like this:
Users:
uid(int) auto_increment primary
role_id(int)
Roles:
rid(int) auto_increment primary
description(varchar)
// any access values ie: can_read(int)
Then you could set-up a sub roles table which allows viewings on regions.
Some example code for the accessing of these tables:
$stmp = (new PDO(...))->Prepare("SELECT role_id FROM Users WHERE uid = :userid");
$stmp->bindValue(":userid", $id); // I'd suggest storing a login key using sessions
$stmp->execute();
$user_permissions = $stmp->Fetch()['role_id'];
// Reference the Roles table for permission lists
$stmp = (new PDO(...))->Prepare("SELECT * FROM Roles WHERE rid = :roleid");
$stmp->bindValue(":roleid", $user_permissions);
[...]
Hope this helped.
I'm not sure why I'm struggling with this it seems like a very simple concept. So my struggling makes me think that perhaps my data modeling needs another component...
I'm using Laravel 5 and am trying to define some model relationships. BelongsTo,HasA, etc. Before I can write the code, I need to at least conceptually understand what type of relationship I'm creating.
I have an application to where users can send people referral links, if a person clicks on the link and signs up, their user record makes note of the code that referred them. This way I can trace back and see who referred a particular user. But a referral is NOT necessary to sign up
Tables:
USERS
+----+-------------+
| id | referral_id |
+----+-------------+
| 1 | 1 |
| 2 | null |
| 3 | 2 |
+----+-------------+
REFERRALS
+----+---------------+---------+
| id | referral_code | user_id |
+----+---------------+---------+
| 1 | 12345 | 2 |
| 2 | 54321 | 2 |
| 3 | 99999 | 2 |
+----+---------------+---------+
USERS.REFERRAL_ID references REFERRALS.ID
and
REFERRALS.USER_ID references USERS.ID
But what kind of relationships are these?
The only one that seems obvious to me is that REFERRALS.USER_ID belongs to USERS.
But what about USERS.REFERRAL_ID, saying it belongsTo Referrals doesn't feel right, as that record isn't required and I don't feel like it 'owns' the user by any means. Saying it hasA referral doesn't feel correct either, as again the user doesn't own or even require the referral.
I guess what is confusing me is that REFERRALS is an optional entity.
How should I conceptualize the relationship between USERS.REFERRAL_ID and REFERRALS.ID?
Is it bad to have this sort of "circular reference"? Would I be better off creating a pivot table?
No need to add any reference to the Referrals table in the User table, you already have that relation defined in the referral table ( user_id column )
Further reading: https://en.wikipedia.org/wiki/Database_normalization
The Relationship is
USER has many REFERRALS
REFERRAL belongs to USER ( inviter )
REFERRAL belongs to USER ( invitee )
Modify your REFERRALS table
+----+---------------+---------+------------+
| id | referral_code | user_id | invitee_id |
+----+---------------+---------+------------+
| 1 | 12345 | 2 | 1 |
| 2 | 54321 | 1 | null |
| 3 | 99999 | 3 | 1 |
+----+---------------+---------+------------+
user_id is the id of the user that sends the invitation
invitee_id is the id of the user that accepts and registers
invitee_id column is nullable() and will contain the id of the invitee from users table when they join.
Think of it as a JOIN table between inviter and invitee.
I am looking for some general information in regards to User Table Design.
I have an old table design for 'users', which I need to update but not breaking the entire site's structure.
Current Table Design
UserID | Email | FirstName | Last Name | ...
1 | a#a.com | John | Doe | ...
2 | b#b.com | Jane | Doe | ...
I need to be able to create "Primary" users, as well as "Assitant" users.
Now I believe I should have a few tables designed:
Users
Accounts
Users > Accounts - (Relationships & Permissions)
IE: of users > accounts
TableID | UserID | AccountID | PERM
1 | 1 | 1 | 001
So I guess my question is. Is there a better way to do this? Specifically if there is a current design being used?
Hope this makes sense. Any direction in this would be greatly appreciated.
Here's an example where you'd have a table for each group, plus a users table. You can filter the users by group using a JOIN. Personally I don't love this. If anyone else has a better suggestion, I'd like to hear it.
http://sqlfiddle.com/#!9/993dd/1
Im currently creating a role System for a web Project (PHP/SQL)
To start me of I will have ~5 fixed roles which can be assigned to users.
And then I got a lot of different privileges.
Now I have to create a table to assign different privileges to different roles and I wonder whats the best way to do so:
1) Table with columns like that:
id | privilege | sysadmin | editor | ... | guest
0 | db_view | True | False | ... | False
1 | page_edit | True | True | ... | False
...
This seems the best solution as Long as there are only five fixed different roles, but is it still practicable when I open the role system to user defined roles in a future version.
2) Table with columns like that:
id | role | db_view* | page_edit | ... | usermanagement
0 | sysadmin | True | True | ... | True
1 | Editor | False | True | ... | False
...
At the end this table will have a great amount of columns - is that a good idea?
2) Table with columns like that:
id | role | privilege | Value
0 | sysadmin | page_edit | True
1 | Editor | page_edit | True
2 | sysadmin | dbview | True
3 | Editor | dbview | False
...
Here it should probably be enough to just create a dataset for privilges who are true. But it would still require a big table.
Is there another way? What would be the clearest, most flexible way to create that table?
Which way do you learn when you study database design?
Thanks in advance for any suggestions or questions!
This seems like a classic case for a many to many relationship:
TblPrivileges
-------------
Privilege_Id (primary key)
Privilege_name
Other privileges related data
TblRoles
--------
Role_Id (primary key)
Role_Name
Other role related data
TblPrivilegesToRoles
--------------------
PTR_Privilege_Id (reference privilege id in tblPrivileges)
PTR_Role_Id (reference role id in tblRoles)
In table TblPrivilegesToRoles the primary key should be both columns.
I have created a privilege system for my application which allows/disallows access to specific pages based on user input.
The table looks something like this:
page_id | client_id | sys_group_no | name | friendly_name | viewable |
1 | 4 | 1 | home | Home | true |
2 | 4 | 1 | admin| Admin Home | false |
So if the user in client_id 4 is of group 1 they are NOT allowed to view 'Admin Home' it isn't actually quite this simple but for the sake of this question we can pretend.
The problem is as maintenance goes on this table get out of date quickly, and when you have a few thousand rows, constantly checking the table against the actual page names (using scandir() and array_diff()) will be expensive. Is there a different paradigm for checking this kind of integrity other than direct comparison? - For instance would hashing my $page_array and comparing it be a better approach?