It seems to me that Discretionary Access Control (DAC) is most suited to your requirements. First of all its worth keeping access control policy in configuration and not in your application source code - coding to the activity. For example, (based on an example in the OWASP Top 10 Proactive Controls Project) instead of:
if (user.hasRole("ADMIN")) || (user.hasRole("MANAGER))
deleteContact();
Consider the following instead:
if (user.hasAccess("CONTACT_DELETE"))
deleteContact();
In your database you would configure which activity permissions are assigned to each role.
In terms of database table or entity relationships:
- A
security_activity
table will include one record for each functional activity.
- A
security_activity
may be assigned to zero or more security_role
, using a link table.
- A
security_role
may be assigned to zero or more security_user
, using a link table.
Let's now apply the above to your 3 scenarios:
Clerk:
- security_activity: id=1; name="FILE_CREATE"
- security_activity: id=2; name="FILE_READ"
- security_activity: id=3; name="FILE_UPDATE"
- security_activity: id=4; name="FILE_DELETE"
- security_role: id=1; name="Clerk"
- security_role_activity: id=1; role=1; activity=1
- security_role_activity: id=2; role=1; activity=2
- security_role_activity: id=3; role=1; activity=3
- security_role_activity: id=4; role=1; activity=4
- security_user: id=1; name="Andrew A."; job_title="Clerk"
- security_user_role: id=1; user=1; role=1
Contact Manager:
- security_activity: id=5; name="CONTACT_CREATE"
- security_activity: id=6; name="CONTACT_READ"
- security_activity: id=7; name="CONTACT_UPDATE"
- security_activity: id=8; name="CONTACT_DELETE"
- security_role: id=2; name="Contact Manager"
- security_role_activity: id=5; role=2; activity=5
- security_role_activity: id=6; role=2; activity=6
- security_role_activity: id=7; role=2; activity=7
- security_role_activity: id=8; role=2; activity=8
- security_user: id=2; name="Barry B."; job_title="Contact Manager"
- security_user_role: id=2; user=2; role=2
Admin:
- security_role: id=3; name="Admin"
- security_role_activity: id=9; role=3; activity=1
- security_role_activity: id=10; role=3; activity=2
- security_role_activity: id=11; role=3; activity=3
- security_role_activity: id=12; role=3; activity=4
- security_role_activity: id=13; role=3; activity=5
- security_role_activity: id=14; role=3; activity=6
- security_role_activity: id=15; role=3; activity=7
- security_role_activity: id=16; role=3; activity=8
- security_user: id=3; name="Catherine C."; job_title="Admin"
- security_user_role: id=3; user=3; role=3
"In addition, I'd like to take the roles one step further, and for example give them certain CRUD permissions for Files as well as Contacts, but have a scope within Files and within Contact (e.g. permissions apply to Files of type X, and Contacts of type Y)."
To achieve this you could create extra activity records, for example for type X files:
if ((user.hasAccess("FILE_DELETE")) && (user.hasAccess("FILE_TYPE_X")))
deleteFile();
And for type Y contacts (remember to assign these additional activities to the roles):
if ((user.hasAccess("CONTACT_DELETE")) && (user.hasAccess("CONTACT_TYPE_Y")))
deleteContact();
"Then other issues start arising: what if a certain role destroys a Contact, that is linked to a File that the role does not have access to? What if a role has access to a subset of Files, and a subset of Contacts, but one of the Files the role has access to, has Contacts linked to it that the role does not have access to?"
To solve this you will need some extra custom logic before you allow a file or contact to be deleted. I suggest you think it through as pseudo-code before starting implementation.
function deleteFile($iFileId) {
/* Check permissions */
if (!user.hasAccess("FILE_DELETE")) return false;
if (!user.hasAccess("FILE_TYPE_X")) return false;
/* Prevent deletion of file if there are contacts linked /*
$aContacts = (int)DB::GetValue(sprintf("SELECT COUNT(*) FROM business_contact WHERE file=%s;",
(int)$iFileId));
if ($iContacts > 0) return false;
/* Mark file as deleted and hide throughout application */
DB::Exec(sprintf("UPDATE business_file SET del=1 WHERE id=%s;", (int)$iFileId));
/* Mark linked contacts as removed links and hide throughout application */
DB::Exec(sprintf("UPDATE business_file_contact SET del=1 WHERE file=%s;", (int)$iContactId));
}
function deleteContact(iContactId) {
/* Check permissions */
if (!user.hasAccess("CONTACT_DELETE")) return false;
if (!user.hasAccess("CONTACT_TYPE_Y")) return false;
/* Prevent deletion if linked to file */
$iLinks = (int)DB::GetValue(sprintf(
"SELECT COUNT(*) FROM business_file_contact WHERE contact=%s;", (int)$iContactId));
if ($iLinks > 0) return false;
/* Mark contact as deleted and hide throughout application */
DB::Exec(sprintf("UPDATE business_contact SET del=1 WHERE id=%s;", (int)$iContactId));
}
"Is there an elegant way to solve this using the RBAC model? I've also looked into ARBAC, but it seems like overkill for a relatively simple application. To me, it seems that the root of all the issues is the many-to-many relationship between Files and Contacts. Are there specific ways to deal with a situation like this? I'm fairly new to this, and these permissions just landed in my lap for this project. I've looked into several Access Control models (even OrBAC), but none seem to be able to address this issue. Any help or hints in to the right direction would be greatly appreciated."
The above example will take you most of the way. It's quite normal to have many to many relationships in database structures, these are usually managed using a link table. Read up on the subject of Normalization and Entity Relationship Diagrams.
Should you need to add further controls regarding which records users have access to, then you could modify your hasAccess equivalent function to accept the record ID as a second parameter and apply whatever logic you need to in your hasAccess function, based on configured security policies in your database.
if (user.hasAccess("CONTACT_DELETE", 37991))
deleteAccount();
If record access will be limited by default, then you could have a column on your business_contact or business_file tables that holds a whitelist of users with access. If the files will be in folders and sub-folders etc, then perhaps you would have this column on your folder record instead. An asterisk in this column could be interpreted as 'allow all'. Remember to at least include in this list the creator's user ID!