Overview

Our Access Control List (ACL) design focuses on the following problem: we need a protection mechanism that can provide or restrict access to various operations (such as view, edit, create) on various objects within the system (contacts, groups, or more abstract things such as location types and custom fields). Access must be defined at three levels: all contacts within the domain, contacts within a group, and single contacts.

As in the 1.x scheme (based on Drupal roles), protection must be enforcable by - but not restricted to - filter clauses in SQL queries. For a good overview and idea of ACL based systems the phpGACL manual is a good read and the package worth installing. (we would have used it if it had support for dynamic sql)

Design

Schema

civicrm_acl

Column

Type

Description

deny

boolean

Does this ACL entry grant (0) or restrict (1) access?

operation

enum VIEW, EDIT, CREATE, DELETE, GRANT, REVOKE

What type of operation does this ACL entry govern?

entity_table

string

The table of the object(s) possessing this ACL entry. Possible values are Contact, Group, ACL Group, and Domain.

entity_id

int unsigned

The ID of the object possessing this ACL entry.

object_table

string

The table being governed by this ACL entry

object_id

int unsigned

The ID of the object being governed. If NULL, the ACL entry refers to all objects within object_table.

acl_table

string

For GRANT/REVOKE operations, this field determines if it refers to a single ACL entry, or an entire ACL Group.

acl_id

int unsigned

ID of the ACL or ACL group being GRANTED/REVOKED.

civicrm_acl_group

Column

Type

Description

domain_id

int unsigned

Foreign Key to civicrm_domain.id

is_active

boolean

Is this ACL Group active?

title

string

The name of this ACL Group

civicrm_acl_group_join

Column

Type

Description

acl_group_id

int unsigned

Foreign Key to civicrm_acl_group.id.

entity_table

string

Which table we're joining to (Contact, Group or Domain).

entity_id

int unsigned

ID of the object being joined.

Algorithms

permissionClause

$clause = permissionClause($tables, $operation, $object_table, $object_id, $acl_id = null, $acl_group = false);

When performing an operation that requires permission, the clause returned by permissionClause should be ANDed with the rest of the WHERE clause. The function will hit the civicrm_acl table and find all the ACLs with the correct operation type that the user/contact has access to. The returned clause is then of the form

((allow_clause_1OR allow_clause_2 OR ... OR allow_clause_n) AND NOT (deny_clause_1 OR deny_clause_2 OR .. deny_clause_m)).

This clause, anded with the existing WHERE, will filter out any result rows that the user does not have access to. If there are no matching ACLs in the database, the algorithm will return 0: no possible permission.

Since the schema allows for ACLs to be granted not only to contacts, but groups of contacts, there are a few details to be aware of. First, there is the issue of conflicting ALLOW and DENY permissions. Assume that a contact belongs to a group, and there is some conflicting overlap in the group's ACL's and those of the contact. There are three interesting cases:

  1. ALLOW and DENY permissions exist directly linked to the contact. I consider this an error; permission should not be granted. It will be the administrator's responsibility to correct it.
  2. ALLOW is linked to the contact, DENY is linked to the group. Since contacts are more specific than groups, permission should be granted. This means that the clause described above will need to be slightly more structured:
    ((allow_clause_1 OR ...) AND NOT (NOT (contact_allow_1 OR
    contact_allow_2 OR ... ) AND (deny_clause_1 OR deny_clause_2 OR ...)))
    
    In other words, permission is granted if it's allowed at either level, and it's not denied (unless explicitly overridden).
  3. ALLOW is linked to the group, DENY is linked to the contact. Contact permission overrides group permission, so this will be handled
    correctly by the clauses described above.

The second issue is that group permissions can only be accessed by static members, since there is no efficient way to determine which
smart groups a contact belongs to. I can't think of a good reason to grant permissions to a smart group, so I don't think this is such a
bad thing.

getACLs

$rules = getACLs($contact_id, $group_id, $aclGroups)

All of the parameters to this function are optional. It returns an array of all ACL rules owned by the input contact or group (or both) IDs. If the $aclGroups parameter is set to true, rules granted through ACL Group ownership are also included. The other use cases are enumerated below.

  1. $contact_id = null and $group_id = null
    If both parameters are null, the returned ACL array contains those rules owned by all contacts within the domain.
  2. $contact_id != null and $group_id = null
    If only the $contact_id is set, the returned ACL array contains only the rules owned directly by the contact. Note that this does not include those granted through group membership; to find ACLs granted through all of the contact's group memberships, use getGroupACLs().
  3. $contact_id != null and $group_id != null
    If both parameters are set, the returned ACL array contains only the rules owned by the contact through membership to the specified group.
  4. $contact_id = null and $group_id != null
    If only the $group_id parameter is set, the returned ACL array contains the rules owned by that group.

For the sake of security, ACL objects (DAO/BAO) should never be exposed outside of the BAO code. The ACL array returned by this function returns the rules in a sanitized associative array format.

getGroupACLs

$rules = getGroupACLs($contact_id, $aclGroups)

This function returns all ACL rules owned by the specified contact through any of his group memberships. To find ACLs given through membership to a specific group, use getACLs().

Again, the $aclGroups boolean parameter defines whether to include rules owned through ACL Groups.

The returned array is of the same format as in getACLs().

getAllByContact

$rules = getAllByContact($contact_id)

This function returns all ACL rules owned by the specified contact, through any possible ownership means. This includes all of the following rules:

Returned values are in the array format defined in getACLs().

Inheritence and Granting

Given our need for scalability and speed, dynamic inheritence of permissions would be incredibly difficult. Instead, I think we should go with a static inheritence scheme where specific ACLs can be granted (possibly in a restricted form) from one contact to another.

This makes the most sense if we consider the object to be some set of contacts. Instead of linking directly to the contact rows (which would explode the ACL table), or to a group of contacts (not semantically equivalent), we can repurpose the saved search table. If the set of contacts happens to be a group, we can store that in the form values of the saved search.

If we use saved searches for ACL objects, then they can be easily restricted when granted from one user to another. Say we have a user with permission to Edit anyone in California, and he wants to grant a subset of that permission to another user (say, everyone in California with the "Volunteer" tag). Then in the granting process (possibly a multi-page wizard, but i think it'll be simple enough for one page), the saved search can be edited, but only in such a way that the
existing form values are preserved, and ORs can only exist in previously unrestricted clauses. This way, any result set from a granted permission must be a subset of the original.

Of course, we don't want a user to be able to grant any permission to anyone. Since this is a special operation, the "object" fields of the GRANT ACL entry refer to the destination/recipient of the permission specified by the acl_id column.

With this granting mechanism, if the administrator has full permissions to all possible objects, any subset of permission can be constructed statically, and without too much tedious work.

Grouping and Roles

One convenient aspect of Drupal's role system is its ability to group permissions under a name. We can accomplish this in the present schema by overloading the idea of group. A group can be created for the sole purpose of permissioning, and contacts can be added to the group in the same way that a user can be flagged for a role.

However, this makes granting permissions tedious, as each indivdual ACL rule would have to be transferred. Instead, we will provide the concept of an ACL Group. Like individual rules, ACL Groups can be owned by a Contact, a Group of contacts, or everyone. In turn, the ACL rules will be owned by the ACL Group via the entity_table and entity_id mechanisms. This makes it easy for an administrator to modify batches of ACL rules, or for a user to GRANT several related rules to another user in one operation.

Use Cases and Additional Implementation Suggestions

Locality-based Permissioning

Neil Adair submitted this suggestion for solving permission-row explosion which results from the common requirement for locality-based permissioning (e.g. access control rules for access to xxx smart groups whose membership is based on country,city,state...):

"If a "search" field can be designated in CRM Profile similar to "match" field and any search by a
user/admin is constrained by matching the field content of the user with
the search results. For example if "State" is set as the "search" field
in CRM Profile then a user in "Ontario" will only have results from
Ontario returned by any search."

NOTES: Probably need a clearer name for this Profile field property (something like "permission control"??). This type of access control behavior could be triggered for specific roles/users by assigning a new access control item (e.g. "limit access by shared permission control values").

I've thought this through some more and extended it (see next section). Neil Adair

CRM access control

Fine-grained permissions are an essential feature for CRM. The problem with comprehensive permissions is administration. The admin overhead is so high that users are often granted permissions they don't want or need or they share accounts. Data security, user tracking, and productivity are all compromised.

The following is a suggested approach to providing comprehensive acccess control to CRM with a minimum of administration required.

Role - Drupal access control roles
sets CRM permissions

Administer CRM
Access CRM Profile
Access CRM
Add Contacts
Edit Contacts
View Contacts

Profile - CRM Profile with ability to apply different profile to each role and set access fields

Currently CRM Profile can be set for "Public" or "User and User Admin" only. If it were extended to apply a profile for each role and to set an "access" field similar to "match" field setting, administration can be simplified. Profile is used to set the CRM fields visible to a role and the field to use (if any) to control access to contacts for that role (in addition to its' other functions).

Groups - dynamic
Users in a role which has the access field set in its' profile will have all searches restricted to contacts who match the content in the users access field. For example if "state" is the access field then users can only access contacts in their state. Note that roles with no access field set can view/edit all contacts, roles with "email" set as access can only view their own contact data. This is not limited to geographic groups as any field can be set as the access field, a custom "group" field set as the access field can be used to form groups.

This scheme embeds the access rules in the profiles configuration and requires only the assignment of roles to users. Administration of a website requires setting roles for users and this scheme simply adds a few additional roles, no other tasks.

Another advantage of controlling access in this way is leveraging of saved searches. A saved search for "all members" will return all members in different states, districts, or other divisions depending on the user who runs it. This can significantly reduce the number of saved searches and the ease of selecting the correct one.

Use Case from Joe Murray (relative to Membership Mgmt Spec...)

I want to be able to set up something that defines permissions for any riding membership secretary, and then have a table of roles that indicates that contact A is a riding membership secretary for riding X, where riding X is a group. I see Membership related roles as being created using whatever access control functionality is provided generally.

Use Case from Hari Gopalan

I am setting up civicrm for a non profit with thousands of centers across the world. I need to give access to individuals to be able to update the information about their centers which are Organization records in civiCRM..

I created a relationship between a record of type "Individual" and "Organization"

Now, how do I go about letting these individuals who are in charge of a center maintain this organization's details.

Use Case from Rob Thorne

I am looking at using a hook to make assigning users to ACLs dynamic.   Here's the basic model:

First, CiviCRM gets the set of ACLs that apply to this user.

Then CiviCRM checks for a hook  (say, in Drupal parlance,  a "hook_civicrm_access') that has a signature like this:

function hook_civicrm_access($contact_id, $grant_type, $view_only = TRUE){

where

@param int  $contact_id is the CID of the requestor

  @param string $grant_type  is  one of  "groups",   'acls',  and perhaps  'contacts'.

  @param boolean $view_only  (just view access, or view and edit.  This only makes sense for groups or contacts, since ACLs have their own notion of this. 

  @returns  array of id  (group_ids for groups,  acl_ids for acls,  contact_ids for 'contacts'

This has the advantage letting the user framework decide at run time what access make sense (say,  by checking organic group membership or some other UF side construct).  This hook would only need to be called once during a request, and any processing of the lists into WHERE clauses or such would only need to be done once as well.