In this post I’ll describe some of the CRM Security Model internals. I’ll describe how roles and privileges are used under the covers, how security is enforced for different operations, and some of the internal structures that make it possible.
Overview
Just in case we need a quick refresher on the basics…
The CRM security model uses Windows-based authentication (Kerberos/NTLM), and internally-driven authorization. It’s basically a role-based security model, where a role is conceptually some class of person (Marketing Professional, System Administrator, etc.). CRM supports object-level (i.e. row-level) security, with support for sharing and assignment. Depending on your role and sharing information, you can have access to an object even if you’re not the owner.
Implementation
In v1.0 and v1.2, CRM used windows security descriptors and the windows AccessCheck function (actually AuthzAccessCheck) to actually validate a user’s access. A security descriptor is the same thing controls your rights on files, directories, etc. A CRM security descriptor would contain information about what users, teams, and roles had what specific rights on an object, and it would be stored in the row of the object. Users, teams, and roles would all be represented by some AD object (user object or an AD group). To check access, we would retrieve the security descriptor and call the AccessCheck function using the token of the user who was logged into CRM, which would contain the groups of all the roles and teams of which they were a member.
In v3.0, we moved to a database-driven security model since it gave us many benefits over the security descriptor model.
Roles and Privileges
A privilege is conceptually a right on an action. For example, example, holding the “Read Account” privilege gives you the right to try to read an account. Without the privilege, we don’t even bother to show the Account entity in the UI. We call this the PrivilegeCheck. What actually controls your access to a particular object instance (or row), is the depth of privilege and the relative depth of the business unit to which you belong to the business unit to which the object belongs (unless it’s an Organization-Owned object). We call this part the AccessCheck. Here’s an example:
If Bob has the “Read Account” privilege at the Parent:Child depth, he can read account A, but not Accounts B or C. If he didn’t have the privilege at all, Accounts wouldn’t even show up in the UI.
Depth is the pie-pieces you see in the Role Editor UI. So a role is really a collection of {Privilege, Depth} pairs, and a full security check = PrivilegeCheck + AccessCheck (AccessCheck implicitly checks privileges, too, but a PrivilegeCheck is more efficient since we don’t need any object information to deny someone who doesn’t even have the basic privilege).
Sharing and Assignment
CRM also supports sharing and assignment. Assignment is obviously just changing the owner of an object. Sharing provides the ability to override AccessCheck-restrictions, but PrivilegeCheck-restrictions still apply. So in the example above, if Account B was shared with Bob with Read rights, he could read B, but still not C. If Bob didn’t hold the read privilege at all, he still couldn’t read A, B, or C, even with the sharing.
PrivilegeCheck and AccessCheck
Okay, what actually happens with security when you view a grid, update an Account, etc.? Let’s start by going through the flow of updating an Account. I’ll look at it from an SDK perspective, since the UI actually does some additional stuff. Here’s the flow:
After the privilege check, we continue on with other business logic like basic validation of input against business rules or other entity-/operation-specific stuff. For the access check, we need to retrieve some security information from the row of the Account. In particular, we need to know the Primary Key, OwnerId, and OwningBusinessUnit. Once we have that, we can do the access check:
Let’s go through the steps here:
1. If the user is the owner of the record, the depth stuff doesn’t matter (remember, we’ve already gotten past the PrivilegeCheck part), so they have access.
2. Next, we need to determine the minimum privilege depth required:
a. If the user is in the same business unit as the object, the user needs “Business” depth (half a pie from the UI).
b. If the object is in one of the child business units of the user’s, the user needs “Parent:Child” depth (3/4 pie from the UI).
c. Otherwise, the user will need “Organization” depth to read the object (for example, if it’s in a parent or sibling business unit).
We can do all these checks from cache lookups: the user’s business and the organization’s business hierarchy are cached, as well.
3. Then we find out if the user has all the required privileges at the minimum required depth. Included in the privilege information we cache for the user is the maximum depth at which they hold the privilege.
4. If the user doesn’t get access through the owner checks or role-based checks, we check sharing next. CRM stores all sharing information in a table called PrincipalObjectAccess which maps users and teams to objects that have been shared to them explicitly or via cascaded operations.
5. If the user still doesn’t have access by this point, we return an Access-Denied error.
So basically that’s the security part of what happens when you use the Update API or when you click “Save” on a form (though we wouldn’t even render the “Save” button if you didn’t hold the privilege).
What about when you view a grid? Well, we don’t want to retrieve a bunch of records and then do an access check row-by-row in the middle-tier. We want to be able to do the security checks in SQL so we can get good paging performance and not retrieve more records than are necessary to satisfy the user’s request.
So we invert the process slightly. Instead of determining a minimum “Read” privilege depth by looking at the relative positions of the user and the object in the business-hierarchy, we get the maximum “Read” privilege depth on the entities being queried and use that to limit the query to those business units the user will be able to read. We also do the ownership check and sharing checks as part of the query as well (we still do a PrivilegeCheck for “Read” on the entities being queried first, of course):
SELECT top 51 AccountId
FROM Account
WHERE
— user is the current user
(Account.OwningUser = <current_user>)
OR
— user gets access via role access (this example is checking Parent:Child)
(Account.OwningBusinessUnit in
(SELECT SubBusinessId
FROM BusinessUnitMap
WHERE BusinessUnitId = <current_user_business_unit>))
OR
— user gets access via sharing
(AccountId in (SELECT ObjectId
FROM PrincipalObjectAccess poa
JOIN SystemUserPrincipals sup on poa.PrincipalId = sup.PrincipalId
WHERE sup.SystemUserId = <current_user> AND
poa.ObjectTypeCode = <entity_object_type> AND
((poa.AccessRightsMask | poa.InheritedAccessRightsMask)&1) = 1)))
This is a simplified query for a user who has the “Read” account privilege at “Parent:Child” depth. The user can get access through ownership, role access, or sharing. The ownership check is pretty straightforward. For the role check, we need to look at objects in the user’s business or child businesses because they have “Parent:Child” access. We use a utility table, BusinessUnitMap, that denormalizes the business hierarchy into a flat list for each business unit.
For the sharing check, we need to retrieve the entity rows where the PrincipalObjectAccess table has entries for the entity with read access (the “& 1” part) where the Principal is the user or any teams of which the user is a member. We use the utility SystemUserPrincipals table to get a flat list of all the principals of which the user is a member (including a self-mapping to the user). The “top 51” just means we are retrieving the first page of a grid with 50 records per page. The extra record is used to let the caller know whether there are more records or not.
Wrapping Up
I hope you found this brief look under the covers interesting. If you have any questions, comments, requests on the CRM Security Model, we’d love to hear from you — please add a comment to the post and I’ll be happy to respond.