A Recursive Headache: Custom Security Solution

This article is based on a presentation I did at a local Salesforce Dev User Group meetup.

Problem:
So this client paid me money to work for them for 6 months, which means, listen to requirements and do your best to implement. In this case, 30 objects assigned to accounts need to be shared to external users via a complicated security hierarchy based on the accounts in the hierarchy. The specific level of access should be able to be modified going forward and at least 6 levels of hierarchy schould be catered for... no problem right?

So optimal solution would allow an internal administrator user to control what a account contact user (I'll refer to these as external user) sees. For certain records, example 'rules and regulations' the external users BELOW the one in the hierarchy, should be able to have visibility access. For others, e.g. high sensitivity cases, the lower account should have complete access to enter/manage a case, but the higher accounts should not have any visibility of the records. This makes it very difficult to implement using the default security hierarchy.

The solution has to be scalable and due to the number of objects, of account hierarchy restructures and external users (thousands); trying to cram the sharing model per record by directly associating user access to a record would be nigh impossible.

Result:
Three apex sharing entries per object record created, aimed at parents, children and primary role of the associated account for the record. Recalculation only necessary on account change or during account hierarchy restructure. Access of the sharing entries determined by a plain English dictionary. Model extendable for customised sharing per object (e.g. sharing to users on the same level of hierarchy, to one below on hierarchy, to two accounts above).

Solution:
Use Public Groups created per Account to capture the directions of hierarchy and users associated with the Account itself:
Primary Public Group - 1 to 1 public group associated with the
Child Public Group - all accounts below this one in the hierarchy
Parent Public Group - all accounts above the current one
Direct Child Public Group - all accounts 1 level down from the current one

Custom Setting "SecurityDictionary__c" to control access to the objects e.g.
Name: ObjectA__c
Primary Access: Edit
Parent Access: Edit
Child Access: Read

Update the public groups across the hierarchy on update of account.

Including populating the Primary Group with the Public Role associated with the account, which is created quite late in the process.

On insertion or modification of associated records e.g. ObjectA__c scan the SecurityDictionary__c to find the required access.
Discussion:


The beauty of this solution is that memberships do not need to be updated as contacts are activated or deactivated, the sharing model will automatically cater for them due to the role association. Update of the account hierarchy does not impact the related records, only the public groups associated to the account need to be modified.

It also bypasses the 250 per object limit of Custom Rules and keeps license costs down avoiding rolling additional users into internal license holders as this was catered to multiple thousand users in a smaller organisation.

After having a chat with the good people at Good Day Sir. While solution sounded difficult, it was sound, and having a green-field org with my own framework meant the code could be structured to facilitate this function.

While I won't discuss these in THIS post. The framework in the organisation involves, separtion of concerns trigger framework as discussed in Advanced Apex Programming - Dan Appleman. Test data generation is done based on Test Data Builders (by Nat Pryce as implemented by James Hill) and Smart Factory courtesy of Matthew Botos. The code IS extendible and was rewritten for this article. It is catering in the system for a few additional public group requirements.

The account hierarchy traversal implementation is heavily relying on the structure provided here Managing Multiple Multilevel Acc. Hierarchies by Peter Frieberg

Creating Public Groups against the account is first step. Modified accounts are passed to a checker, a static variable is used to catch if this operation is currently running (as the modification of the Account to update it with another account will kick off a second update call).

Let's Code
There is a problem of mixed DML statements as public group modification will count is metadata and can't be done in same transaction, to bypass this queuables are kicked off for the modification, one for creation of the public group, another to update the account with the created group. Queueables are freaking awesome except... can't be nested when running in a test class. So logic is done in methods, and the queuable checks if a test is running. In a test, the methods are run one after the other manually, using Running as and IsTest to bypass the mixed DML issues.

The following piece of code takes a set of Account Id's, sees if they have all Public Group reference, if they do, kicks of a hierarchy recalc on the public group. If not, kicks off a queuable to create the missing public groups (primary, parent, child) and updates the Id's of the created groups back into the account.

That's a big chunk of code but results in a set of Public Groups for the Accounts as required

A trigger on the User, populates a field on the account with the Id of the External Role if one is created as a result of the user. This role is not deleted on the system and doesn't influence the public group process.

Next is a small utility method as I am about to make a LOT of comparisons and didn't want to retype the same comparison loop. The method takes 2 sets and returns a map, identifying existing and missing results in 3 categories (present in first, in second, in both)
Now comes the big chunk of code for the processing of the actual memberships for the public groups. The code is 90% implementation of Peter Friedberg's method, with the big difference being the processing of memberships (in this example only catering for the basics, but some custom groups can also be created by modifying the membership object and creating additional groups in the prior steps)

In short the code receives a set of accounts, finds the top accounts for the set (which may be in multiple hierarchies), traverses down the children tree, creating a "HierarchyNode" wrapper. After the tree is built, MemberManagement goes through every account, every public group and given the details in the tree, confirms or deletes present group memberships. Note the genius way Peter's code saves on queries by querying 4 levels at a time.
ALMOST THERE!

  • Make sure the object targeted is set to Private for external users (or no Share record for you)
  • Create a custom sharing reason for the object. (for this example "ExternalShareReason__c" seemed exciting and creative)
  • Create an entry for the "SecurityDictionary__c" for our object (ObjectA)
  • Create a Trigger on ObjectA__c update or insertion (checking if the account associated has been changed in the case of an update)


The following code expects a list of sObjects of ObjectA, it queries the associated accounts public groups, and the existing share records, then using the method discussed earlier, either insert or removes the specified records. The access is defined as 'None' (no access, doesn't create a sharing rule), 'Read' (View only access), 'Edit' (Read/Write access on the rule).
And that's about it! All the records can be modified without any recalculation (unless there's an account change) as the security is all based on the associated rule records and THOSE records do not change, since we have abstracted the sharing to a public group level!

I get very in depth with my test classes and focus on function based testing at a low level. The following test method should be extended to ensure records are not frivolously updated, or when the owning Account is reassigned:

The flexibility of public groups allowed for a scalability and flexibility. Future proofing the system by allowing for additional group types and independent sharing considerations per object, was invaluable in later stages where additional requirements included a custom 'manual sharing' implementation, multiple account ownership, 1 level down access etc.

Additional Notes:
  • Deleting shares is annoying, there are a few peripheral classes required for the mass recalculation of all sharing records.
  • Deleting Public Groups takes a VERY long time as they are removed a second at a time, that's worthy of its own post, but in short 2000 public groups means a 15 min DML process.
  • Same as best practice for batch executions, it's good to have minimum logic in the queueable method, in the case of a queueable it's almost mandatory to put into anther method as that will simplify the process when you are simulating the operation in a test class.


Thanks for reading!

Comments

Popular posts from this blog

Why Running 237km in Sahara and Being a Salesforce Dev is Exactly the Same

Magic Parents