Building Salesforce Sharing Model Into Einstein Analytics DataFlow Part II

This is the second part of a two part series on building the Salesforce sharing model into your Einstein Analytics DataFlow. I would recommend that you check out Part I here before reading further.

In the first part of this series I laid out a method to add all of the information that you would need to recreate most of the Salesforce security model into your DataFlow. The focus of this post will be to take this model and write a security predicate that you can apply to a dataset to control row level access.

So how’s it done?

I could just paste the entire logical statement in one blob of text for you to copy, but I don’t think it will be as helpful as walking through it and making sure you understand what each part is doing. But if you just want the statement in its entirety I will include it at the bottom of the post.

The first statement in the predicate will deal with the case of an individual user receiving access to the Opportunity record. The first part being the user ID’s that have access to the record and the second part being the managing roles for all those users who also inherit access.

'UserShare.User.Id' == "$User.Id" || 'UserShare.User.Role.Roles' == "$User.UserRoleId"

Then we need to build in the check for if the user trying to access the record has been given access because of their role, or one of the roles below them has been given access to the opportunity record directly via a sharing rule.

'RoleShare.Group.RelatedId' == "$User.UserRoleId" || 'RoleShare.Role.Roles' == "$User.UserRoleId"

Next, dealing with record access through group sharing. Specifically, users who are within a group being granted record access. This will deal with sharing being granted to the managers as well.

'GroupMembersWithBosses.GroupHaveBosses.User.Role.Roles' == "$User.UserRoleId" || 'GroupMembersWithBosses.GroupHaveBosses.User.Id' == "$User.Id"

Next the case where access was granted through group sharing with a user and they don’t want it shared with their manager.

'GroupMemberNoBoss.GroupMemberWithoutBosses.User.Id' == "$User.Id"

Then we move on to the users who gain access to a record because of a role within a group. The first part of this statement will be when access is granted to the role as well as the managing roles above it. The second part of the statement will be for when they are granted access to a role, but access is not given to the managing roles.

'GroupMemberRolesWithBosses.GroupMemberRoles.SubGroup.Role.Roles' == "$User.UserRoleId"  || 'GroupMemberRolesNoBosses.GroupMemberRoles.SubGroup.Role.Id' == "$User.UserRoleId"

All of the Salesforce sharing cases, as specified in part I of this series of posts, will be covered in the above predicate statements. Again, this was built specifically around Opportunity access but the overall algorithm can be applied to any object you wish to control sharing for in this manner.

And finally, here is the security predicate in its entirety.

'GroupMembersWithBosses.GroupHaveBosses.User.Role.Roles' == "$User.UserRoleId"
|| 'GroupMembersWithBosses.GroupHaveBosses.User.Id' == "$User.Id"
|| 'GroupMemberNoBoss.GroupMemberWithoutBosses.User.Id' == "$User.Id"
|| 'UserShare.User.Id' == "$User.Id"
|| 'UserShare.User.Role.Roles' == "$User.UserRoleId"
|| 'RoleShare.Group.RelatedId' == "$User.UserRoleId"
|| 'RoleShare.Role.Roles' == "$User.UserRoleId"
|| 'GroupMemberRolesWithBosses.GroupMemberRoles.SubGroup.Role.Roles' == "$User.UserRoleId"
|| 'GroupMemberRolesNoBosses.GroupMemberRoles.SubGroup.Role.Id' == "$User.UserRoleId"

Happy Coding!

Building Salesforce Sharing Model Into Einstein Analytics DataFlow Part I

This is the first part of a two part series on building the Salesforce sharing model into your Einstein Analytics DataFlow. You can check out Part II here.

We all love to think that what is out of the box is going to work without fail and that all will end up being as easy as it says in the marketing documents. But as you know, this is not always the case. One instance of this is the out of the box security that is available to users of Einstein Analytics. When you are working at an enterprise scale you will almost always exceed the limits specified for inherited sharing and then must fall back on security predicates. But what if you have sharing rules? A complex sharing model? What to do then? This post sets out to give a look into how to implement the Salesforce sharing model in your DataFlow using only standard objects. This will specifically be for Opportunity record access, but the algorithm can be expanded to mostly any object. I will be assuming a moderate technical skill level in this post.

Quickly, What Inspired This Post?

Well, I work for a fortune 500 that is currently implementing the first phase of our roll-out of the Einstein Analytics Sales Analytics App. This means, among many other things, that we have to make sure that the information that each segment of our user base has access to fits into our sharing model. One method to easily determine the records your users should see in analytics is by using the inherited sharing feature that comes out of the box. But for us there was one big draw back, there was a limit. When your users can see more than 2,000 records in the object specified for sharing it can no longer be used. This presented a problem since we have users who can see 40K, 80K and even upwards of 150K Opportunity records at any given time. This meant that for anyone who was above a low level sales rep in the hierarchy would be unable to use inherited sharing…forced to rely on a hard coded security predicate to control sharing.

Knowing this left us with a few choices. Choice one, we can allow anyone higher in the hierarchy to see everyone’s information. Since we are dealing with an Org that has a very locked down security model this was instantly thrown out. The next option was to use the security predicates that were provided to you out of the box. This also posed a problem since they did not cover all of the individual sharing rules that our Org has defined. This left us only one option, build the entire Salesforce security model into the DataFlow so that it could be coded into the predicates and all will be well in the world. What follows is the outline of what you will need to take on such an endeavor.

Step One, What Are The Sharing Cases

There were a few different sharing cases that needed to be covered. Now I must warn, as of now not ALL of these scenarios are covered in this post (since I haven’t finished figuring out how to make them work), but most are. I will define the details next.

What is covered in this model:

  1. User is the owner of the record can view
  2. Managers of the record owner can view
  3. Records are shared directly with a individual user (team membership, individual record sharing, etc)
  4. Records are shared with an individual user and their manager
  5. Records are shared with an individual role
  6. Records are shared with an individual role as well as that roles managing roles
  7. Records are shared with a group containing users, only these users can view the records
  8. Records are shared with a group containing users, these users and their managers can see the users
  9. Records are shared with a group containing individual roles, records shared with only those roles
  10. Records are shared with a group containing individual roles, records are shared with those roles and the managing roles

What is not covered in this model:

  1. Records shared with role and that roles subordinate roles
  2. Records shared with a group that contains a role and its subordinates roles

Phew, glad that is out of the way. So as you can see, we are hitting the 90% mark here. So depending on the organization you may be able to get by with this. In our case, this was acceptable since our sharing did not utilize anything not covered…at least not as it pertains to analytical reporting.

Where to start?

First thing we need to do is tackle the cases of individual users being able to view a record (outside of a sharing rule). So digest the OpportunityShare object and augment that with User information to get the user role and managing roles for each User who can see an opportunity record.

"Extract_OpportunityShare": {
"action": "sfdcDigest",
"parameters": {
"fields": [
{
"name": "Id"
},
{
"name": "OpportunityAccessLevel"
},
{
"name": "OpportunityId"
},
{
"name": "UserOrGroupId"
}
],
"object": "OpportunityShare"
}
},
"AugmentShareUsers": {
 "action": "augment",
 "parameters": {
 "right_key": [
 "Id"
 ],
 "left": "Extract_OpportunityShare",
 "left_key": [
 "UserOrGroupId"
 ],
 "right_select": [
 "Name",
 "Id",
 "Role.Roles",
 "Role.RoleNames"
 ],
 "right": "User_with_Roles",
 "relationship": "User",
 "operation": "LookupSingleValue"
 }
 }

The AugmentShareUsers node will be utilizing the User_with_Roles node that should already be in your dataflow if you are using the standard Sales Analytics App. If you don’t have it, this is just all users by ID and the sales hierarchy flattened to include all users managing roles in a multi-value field.

Continue with identifying which OpportunityShare records are shared with a user and not a group and filter them out.

 "ComputeHasUsers": {
 "action": "computeExpression",
 "parameters": {
 "source": "AugmentShareUsers",
 "computedFields": [
 {
 "name": "HasUsers",
 "saqlExpression": "case when 'User.Name' is null then \"false\" else \"true\" end",
 "label": "HasUsers",
 "type": "Text"
 }
 ],
 "mergeWithSource": true
 }
 },
"Filter_HasUsers": {
 "action": "filter",
 "parameters": {
 "filter": "HasUsers:EQ:true",
 "source": "ComputeHasUsers"
 }
 }

Now we can make the first connection back up to our Opportunity data to gain information about each individual user that can see the Opportunity record and the managing roles that can see the record as a consequence.

"AugmentOpportunityShare": {
 "action": "augment",
 "parameters": {
 "right_key": [
 "OpportunityId"
 ],
 "left": "Join_OpportunityOwner",
 "left_key": [
 "Id"
 ],
 "right_select": [
 "User.Id",
 "User.Name",
 "User.Role.RoleNames",
 "User.Role.Roles"
 ],
 "right": "Filter_HasUsers",
 "relationship": "UserShare",
 "operation": "LookupMultiValue"
 }
 }

In the code above, the Join_OpportunityOwner node is just the most recent node containing all the data for Opportunity rows. The left key is the primary key for an opportunity record.

Next we need to move into the scenarios involving the sharing of records with an individual role. In the OpportunityShare object this takes the form of a group assigned to an opportunity with the type of “role”.  We need to extract the Group object and then augment that up with the OpportunityShare records to get information about each group.

"Extract_Groups": {
 "action": "sfdcDigest",
 "parameters": {
 "fields": [
 {
 "name": "Id"
 },
 {
 "name": "Type"
 },
 {
 "name": "RelatedId"
 },
 {
 "name": "DoesIncludeBosses"
 }
 ],
 "object": "Group"
 }
 },
"AugmentShareGroups": {
 "action": "augment",
 "parameters": {
 "right_key": [
 "Id"
 ],
 "left": "Extract_OpportunityShare",
 "left_key": [
 "UserOrGroupId"
 ],
 "right_select": [
 "Type",
 "Id",
 "RelatedId",
 "DoesIncludeBosses"
 ],
 "right": "Extract_Groups",
 "relationship": "Group",
 "operation": "LookupSingleValue"
 }
 }

Now that we have the info about all the groups given access to the opportunity we need to limit this to only the group record that are a result of the opportunity record being shared with a role. Then get all of the managing roles that will also need to have access to the records as a result.

"Filter_HasRoles": {
 "action": "filter",
 "parameters": {
 "filter": "Group.Type:EQ:Role",
 "source": "AugmentShareGroups"
 }
 },
"AugmentShareRoles": {
 "action": "augment",
 "parameters": {
 "right_key": [
 "Id"
 ],
 "left": "Filter_HasRoles",
 "left_key": [
 "Group.RelatedId"
 ],
 "right_select": [
 "Roles",
 "RoleNames",
 "Id"
 ],
 "right": "Flatten_Hierarchy_Grouping",
 "relationship": "Role",
 "operation": "LookupSingleValue"
 }
 }

The Flatten_Hierarchy_Grouping node is another one that is included in the out-of-the-box sales app. This contains the roles by ID and each of its managing roles in the “Roles” multi-field.

Having this information we can now augment it with the opportunity records to have a multi-field of which roles and managing roles can see each opportunity record due to role sharing. Notice that the master node in this object (the left one) is the node we previously created to join the users who can see the opportunity record and their managers.

"AugmentOpportunityRoles": {
 "action": "augment",
 "parameters": {
 "right_key": [
 "OpportunityId"
 ],
 "left": "AugmentOpportunityShare",
 "left_key": [
 "Id"
 ],
 "right_select": [
 "Group.RelatedId",
 "Role.RoleNames",
 "Role.Roles",
 "Group.DoesIncludeBosses",
 "Role.Id"
 ],
 "right": "AugmentShareRoles",
 "relationship": "RoleShare",
 "operation": "LookupMultiValue"
 }
 }

OK, we are getting closer…

Now we move on to the next set of cases, those where the opportunity records are being shared with real group. To identify these we need to go back to the AugmentShareGroups node and only pull the records that apply to this scenario.

"Filter_HasGroups": {
 "action": "filter",
 "parameters": {
 "filter": "Group.Type:EQ:Regular",
 "source": "AugmentShareGroups"
 }
 }

Now we start getting into some sub-cases. Specifically, we need to identify which users are within each of these groups so we can give them access. But there are also cases where the group dictates that these users’ managers should also see the records. So to do this we need to filter the records into these two scenarios.

"Filter_GroupsHaveBosses": {
 "action": "filter",
 "parameters": {
 "filter": "Group.DoesIncludeBosses:EQ:true",
 "source": "Filter_HasGroups"
 }
 },
"Filter_GroupsDontHaveBosses": {
 "action": "filter",
 "parameters": {
 "filter": "Group.DoesIncludeBosses:EQ:false",
 "source": "Filter_HasGroups"
 }
 }

Now we need to take a quick side step and build up a dataset that gives us information about what members (users, roles and other groups) exist within each group. And for the users within groups, we want to get information about their role and managing roles. This will all be used in just a second. Lets build this up.

"Extract_GroupMember": {
 "action": "sfdcDigest",
 "parameters": {
 "fields": [
 {
 "name": "Id"
 },
 {
 "name": "GroupId"
 },
 {
 "name": "UserOrGroupId"
 }
 ],
 "object": "GroupMember"
 }
 },
"AugmentGroupMemberUser": {
 "action": "augment",
 "parameters": {
 "right_key": [
 "Id"
 ],
 "left": "Extract_GroupMember",
 "left_key": [
 "UserOrGroupId"
 ],
 "right_select": [
 "Name",
 "Role.Hierarchy_RoleNames",
 "Role.RoleNames",
 "Role.Roles",
 "UniqueUserName",
 "Id"
 ],
 "right": "User_with_Roles",
 "relationship": "User",
 "operation": "LookupSingleValue"
 }
 }

Again, we use the User_with_Roles node to get info about a user and the managing roles.

Now back to the fun stuff. We want to build up a couple of multi-fields that tell us the users and their managing roles that have access to opportunity records via group sharing. This will be for the case of group sharing users and also the managers of these users getting access to an opportunity record.

"AugmentMembersGroups": {
 "action": "augment",
 "parameters": {
 "right_key": [
 "GroupId"
 ],
 "left": "Filter_GroupsHaveBosses",
 "left_key": [
 "Group.Id"
 ],
 "right_select": [
 "User.Id",
 "User.Role.Roles",
 "User.Role.RoleNames"
 ],
 "right": "AugmentGroupMemberUser",
 "relationship": "GroupHaveBosses",
 "operation": "LookupMultiValue"
 }
 }

Sweet! We can now augment this up with the Opportunity dataset to let us know the users and managers who have access to that record because of being apart of a group those records have been shared with.

"AugmentOpportunityGroupHasBosses": {
 "action": "augment",
 "parameters": {
 "right_key": [
 "OpportunityId"
 ],
 "left": "AugmentOpportunityRoles",
 "left_key": [
 "Id"
 ],
 "right_select": [
 "UserOrGroupId",
 "GroupHaveBosses.User.Id",
 "GroupHaveBosses.User.Role.RoleNames",
 "GroupHaveBosses.User.Role.Roles"
 ],
 "right": "AugmentMembersGroups",
 "relationship": "GroupMembersWithBosses",
 "operation": "LookupMultiValue"
 }
 }

Now for the other end of this case, where we don’t want to share the records with the managers of users in the group. Just do another augment against the filter we previously created for this case and only care about getting the user ID’s and leave out the role information.

"AugmentGroupMembersUsersWithoutBosses": {
"action": "augment",
"parameters": {
"right_key": [
"GroupId"
],
"left": "Filter_GroupsDontHaveBosses",
"left_key": [
"Group.Id"
],
"right_select": [
"User.Id"
],
"right": "AugmentGroupMemberUser",
"relationship": "GroupMemberWithoutBosses",
"operation": "LookupMultiValue"
}
}

Now we can join this up to the Opportunity dataset to get a multi-field of all the users who can see the opportunity record due to it being shared with a group.

"AugmentOpportunityGroupNoBosses": {
"action": "augment",
"parameters": {
"right_key": [
"OpportunityId"
],
"left": "AugmentOpportunityGroupHasBosses",
"left_key": [
"Id"
],
"right_select": [
"Id",
"GroupMemberWithoutBosses.User.Id"
],
"right": "AugmentGroupMembersUsersWithoutBosses",
"relationship": "GroupMemberNoBoss",
"operation": "LookupMultiValue"
}
}

That takes care of all the stuff related to users within groups. Now we can move on to the last piece, individual roles that are included in groups. The first thing to do is go on another short jaunt to build up a dataset of the group members who have roles within them. Once we know that we can get some information about the applicable managing roles. All this will then be used in a brilliant fashion in just a sec, but first, the code to build this dataset.

"AugmentGroupMemberSubGroup": {
"action": "augment",
"parameters": {
"right_key": [
"Id"
],
"left": "Extract_GroupMember",
"left_key": [
"UserOrGroupId"
],
"right_select": [
"Id",
"RelatedId",
"Type"
],
"right": "Extract_Groups",
"relationship": "SubGroup",
"operation": "LookupSingleValue"
}
},
"AugmentShareSubGroupRoles": {
"action": "augment",
"parameters": {
"right_key": [
"Id"
],
"left": "AugmentGroupMemberSubGroup",
"left_key": [
"SubGroup.RelatedId"
],
"right_select": [
"RolePath",
"Roles",
"RoleNames",
"Id"
],
"right": "Flatten_Hierarchy_Grouping",
"relationship": "SubGroup.Role",
"operation": "LookupSingleValue"
}
},
"Filter_SubGroupHasRole": {
"action": "filter",
"parameters": {
"filter": "SubGroup.Type:EQ:Role",
"source": "AugmentShareSubGroupRoles"
}
}

We now have a node (Filter_SubGroupHasRole) that contains all of our group members who are a single role. This is useful since we can now go back to the set of groups that an opportunity record is being shared with and get all the roles that fall into that group. Lets start with the scenario where we don’t want to include the managing roles.

"AugmentGroupMemberRolesWithoutBosses": {
"action": "augment",
"parameters": {
"right_key": [
"GroupId"
],
"left": "Filter_GroupsDontHaveBosses",
"left_key": [
"Group.Id"
],
"right_select": [
"SubGroup.Role.Id"
],
"right": "Filter_SubGroupHasRole",
"relationship": "GroupMemberRoles",
"operation": "LookupMultiValue"
}
}

Really quick, lets just augment this with the Opportunity dataset so that we can get it out of the way. Then we will move on to the next scenario.

"AugmentOpportunityGroupRolesNoBosses": {
"action": "augment",
"parameters": {
"right_key": [
"OpportunityId"
],
"left": "AugmentOpportunityGroupNoBosses",
"left_key": [
"Id"
],
"right_select": [
"GroupMemberRoles.SubGroup.Role.Id"
],
"right": "AugmentGroupMemberRolesWithoutBosses",
"relationship": "GroupMemberRolesNoBosses",
"operation": "LookupMultiValue"
}
}

Aaaand, on to the next one…

Same idea as before, but now we are going to get not only the roles that are included in a group but also their managing roles. As a quick reminder, we are using the nodes that determine which groups allow and don’t allow managers to view the data shared with the group as the source for these. These are from earlier…just want to make sure you didn’t forget.

"AugmentGroupMemberRolesWithBosses": {
"action": "augment",
"parameters": {
"right_key": [
"GroupId"
],
"left": "Filter_GroupsHaveBosses",
"left_key": [
"Group.Id"
],
"right_select": [
"SubGroup.Role.Id",
"SubGroup.Role.Roles",
"SubGroup.Role.RoleNames"
],
"right": "Filter_SubGroupHasRole",
"relationship": "GroupMemberRoles",
"operation": "LookupMultiValue"
}
}

And finally, augment this information up with the Opportunity dataset.

"AugmentOpportunityGroupRolesWithBosses": {
"action": "augment",
"parameters": {
"right_key": [
"OpportunityId"
],
"left": "AugmentOpportunityGroupRolesNoBosses",
"left_key": [
"Id"
],
"right_select": [
"GroupMemberRoles.SubGroup.Role.Id",
"GroupMemberRoles.SubGroup.Role.Roles",
"GroupMemberRoles.SubGroup.Role.RoleNames"
],
"right": "AugmentGroupMemberRolesWithBosses",
"relationship": "GroupMemberRolesWithBosses",
"operation": "LookupMultiValue"
}
}

And there you have it. All the pieces you need to bring the salesforce sharing model into your DataFlow for use with security predicates. This example focused on Opportunity access but the same algorithm can be applied to almost any object you want.

Please check out Part II of this post for the details of setting up the security predicates to use this information.