Aller directement à la fin des métadonnées
Aller au début des métadonnées

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

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

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:
    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

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

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

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

  • owned by every contact in the domain,
  • owned by the given contact,
  • owned by the given contact through all group memberships,
  • all of the above, but through ACL Groups.

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.
 

Étiquette
  • Aucun
  1. Nov 23, 2005

    First, we definitely agree that the current access control model (via smart groups with specific search criteria) does not scale well.

    The approach specified by Neil seems like a good solution for organizations who need to segment access across a domain with a large number of distinct sets (e.g. the 308 federal election districts example).

    However, the solution presupposes that the Users being granted the permissions will always/mostly share the same value(s) for the properties which control the segmentation. For example, in Joe's use permissioning is based on a custom Riding/Constituency field (which is populated by postal code lookup). If all Riding Secretarys had their address IN the riding they are secretary for - this works beautifully. BUT is this generally the case? Exceptions would require either input of a 'fake' Riding value for these secretarys' contact records or creation of riding-specific permissions (e.g. similar to the existing smart groups model).

    Another example closer to my experience is the virtual phone-banking I did for the last election. Although my home and work address are in San Francisco, I was given/assigned "access" to contacts in Ohio. This seems like a pretty common scenario.

    For non-location-based segmentation - the requirement would be that permissioned user (worker/volunteer/etc.) records be populated with the same value(s) as the contacts they are granted access to.

    So...we'd like to get some dialog going about the 'fit' between current and projected access control/data scenarios and the proposed solution.

    1. Nov 23, 2005

      Neil Adair dit :

      Yes, access to locations other than one's own is an issue. The problem is that access for many agents can bear no relationship to their personal address, eg. parachute candidates. I was thinking of using Organizations as the access control point and individuals with a relationship with an organization would inherit the Org's access.

      While we have relatively stable permanent Electoral District Associations (with CEO, CFO etc.) campaigns are operated by separate transient orgs with different staff (official campaign period, candidate, official agent etc.). It makes for a lot of permissions administration in a short time. Access could be pre-configured for the Org which related users with the appropriate role can inherit. This would also solve the problem of users needing access to multiple groups as they could just be given a relationship with each.

      1. Nov 24, 2005

        Neil:

        This seems to muddle the issue up even more. So your latest comment suggests the following process:

        1. create a profile P (say constituency)

        2. For a logged in user U with role R with permission on profile P, get the value of constituency C from the relationship that this user has with an organization O and allow U to only see contacts who live in C

        The above seems a bit too hackish and fairly specific to your current case. I dont think thats a general permissioning model (triste)

        lobo

        1. Nov 24, 2005

          Neil Adair dit :

          I know, it is just one way to address the outside agent problem. I will probably create office based users, eg. 35066CFO is a user which has access permission to contacts in riding 35066. Having more than one user account with different permissions for some individuals is normally how we deal with this situation.

  2. Feb 28, 2006

    Rob Thorne dit :

    I'm looking at this from the perspective of a Drupal module coder trying to get at CRM data from Drupal module code.

    Dave G I believe gave the use case that covers our case: a permission/ACL that grants an access to a contact to a smart group who is not him/herself a member of the group. So the access is something like "READ/ADD NOTES OR ACTIONS TO RIDING 322". To be usable, this means that you have a "template" for producing an ACL to some specific riding. Everything is still static, but the admin UI give you help in creating ACLs that are similar to some related class of ACLs.

    One issue does not appear in the notes, if I read it right: the right of a piece of code to execute with the rights of some user which is different from the current user (~SUID, etc.) In Zope's model, you can attach these kinds of privileges to specific pieces of code. Obviously, if the permissions granted this way are not extremely granular, this is a huge risk, as SUID scripts are in Unix. But if we can identify certain common types of queries that are relatively safe and create a distinct and low level access, and make it easy to grant that access to code, you can do this in a controlled way. In other words, a sort of "sudo" for code that temporarily elevates access in a very limited way. Like code that accesses databases at run-time, the code gets what it needs to authenticate at run time (it does not ask the user), gets some kind of security cookie to give to a CRM API, and away you go.

    Example from the other day: before I add an anoymous contact to a subscription list, the code probably needs to test if a user of that email address exists. I'd agree that revealing to the anonymous user via UI whether a person of that email address has been added is a security leak (since the user isn't authenticated to the email address, and may just be impersonating the user to get info about the victim's activities), but the code still needs to make the test. There is no legitimate reason why code like this needs to actually see the contact record; only that such a contact exists. So the access is to assess group membership by some limited set of criteria, not access to contacts.

    Certainly, bad or malicious coding will expose sensitive info. But by creating richer primatives for the ACLs, we can make it possible to grant as little access as possible to code to limit potential damage.

    I also think that some kind of a "Permissions object" and API would be helpful: create a permissions object, add credentialling info to it (analogous to log in info for a SQL database), and pass the permissions back to a CRM API that "elevates" the code's privilege level accordingly.


Creative Commons License
Except where otherwise noted, content on this site is licensed under a Creative Commons Attribution-Share Alike 3.0 United States Licence.