Tuesday, May 21, 2013

User Hierarchy based Sharing

I have a client who has flattened out their role hierarchy to enable some other business processes within Salesforce.  So when it came for them to implement an object in Salesforce that required some hierarchy based sharing we had to build something custom based on the user record's manager hierarchy.  For example, if Joe owns a record, his manager, Jane, should have read access to the record.  Jane's manager Mike, should also have read access.  This access should continue to the top of the hierarchy.

In the code below, keep this in mind:

Joe -> Jane -> Mike -> Mary

In our org, the sensitive records that require Private org wide defaults give record owner's read-write access by virtue of their ownership of the record.  For all others to have read access, we have to create sharing records for the object.

A share is composed of the following elements:
  • ParentId - the record that you want to share with others
  • RowCause - the custom reason you are sharing this record
  • AccessLevel - the level of access being given (in our ex: read)
  • UserOrGroupId - the user (or group) who should have access

By creating a record with these required elements, these private records can be shared with those users (or groups) mentioned in the shares.

Programmatically, it is pretty easy to create a trigger on an object and create a related share for the object's owner.  Salesforce's documentation of apex managed sharing does an adequate job of this.  Where I scratched my head a little was figuring out how to share all the way up the hierarchy.  And do the sharing without hitting any governor limits.  I did some searching of blogs and found an interesting post by Jeff Douglas.  Jeff's solution is elegant, but I wanted to try another way myself.  In plain english here is what I wanted to do:

1. Create an object share record for the record's owner's manager
2. Then, create another object share for that manager's manager
3. and so on...
4. Insert list of object share records

It felt like a loop to me, so what I tried initially was to iterate through the object share collection, and for each record in the collection, create another share record, assign the manager, then add it to the collection and continue until a manager is no longer found for the user.  In this snippet, the collection of all users and their managers is held in a map:

         for(User u: [Select u.Id,u.ManagerId from User u])
        {
            mapUserManagers.put(u.Id,u.ManagerId);
        }

for(someObject__Share os: sharesToCreate)
        {
            if(mapUserManagers.get(os.UserOrGroupId) != null)
            {
                someObject__Share os_mgr = new someObject__Share ();
                os_mgr.AccessLevel = MGR_ACCESS;
                os_mgr.ParentId = es_mgr.ParentId;
                os_mgr.RowCause = MGR_ROW_CAUSE;
                os_mgr.UserOrGroupId = mapUserManagers.get(os_mgr.UserOrGroupId);
                sharesToCreate.add(os_mgr);
            }
        }

This approach throws an error:  "Cannot Modify a Collection While It Is Being Iterated"

Some folks over at stackexchange explain the issue with the index behind this loop.

So, I took a slightly different approach to the loop:

        allUsers = [Select u.Id,u.ManagerId from User u];
        for(User u: allUsers)
        {
            mapUserManagers.put(u.Id,u.ManagerId);
        }
        Id managerId;      
        for(SomeObject__c so: RecsToProcess)
        {
            managerId = mapUserManagers.get(so.OwnerId);
            do{
                SomeObject__Share sos = new SomeObject__Share ();
                sos.AccessLevel = SHARE_MGR_ACCESS;
                sos.ParentId = so.Id;
                sos.RowCause = SHARE_MGR_ROW_CAUSE;
                sos.UserOrGroupId = managerId;
                sharesToCreate.add(sos);
                managerId = mapUserManagers.get(managerId);
            } while (managerId !=null);
        }
             
        //allow for partial successes
        Database.SaveResult[] srList = Database.insert(sharesToCreate, false);

This compiles without issue and achieves the effect of creating a collection of share records for each of the managers with only 1 SOQL call.  It's another way to solve the hierarchy loop challenge.  Hope it helps you find a solution to your own problem.

Wednesday, May 15, 2013

Buyer Beware - Cont'd

The recent immigration reform bill that is being discussed has a lot of compromise built into it to allow highly skilled workers to more easily work in the US.  As I've previously posted, I've had some negative experiences finding "highly skilled workers".  In speaking to someone who places IT workers on H1B, he confirmed that there is a lot of bait and switch that occurs in the interviewing process.  Having already been told by a former coworker that the proxy-interviewing was occurring, I thought I had heard the worst of it.  But my headhunter source tells me that it's also common for someone who gets through the interviewing/bait and switch, who is inexperienced, to basically off-shore their work to cheaper labor back home.  I guess I shouldn't be surprised but I'm curious about whether any of you who follow this blog have an opinion on this.  Is this the tip of the iceberg?  Does it matter how the work gets done so long as it's done?

Upcoming Topics

I've been doing some interesting development in the last few months that I'll blog about when things slow down.  Some of the topics:

  • Replacing Salesforce's standard rich text editor with TinyMCE, CKEditor, Redactor, and more.
  • Spell checking and Salesforce
  • Wrappers