I have been wanting to start blogging about Salesforce and my experiences in Salesforce for quite a long time. I always would get caught up trying to figure out what my first post would be, thinking it had to be something HUGE and EPIC. I finally came to the realization I just needed to go for it and stop waiting. So here it is.
Quite a few times I have been asked to set up Account Contact Roles within an org. More than once I have been asked if I could restrict by profile who can create/edit/delete certain roles. How many times have you heard complaints from one department that someone from another department changed a role that they 'had no business' changing.
The scenario seems simple enough.
***This scenario is 100% arbitrary for the example, no promises the roles/profiles make sense
Let's say I have 3 Account Contact Roles,
- Buyer Role
- Technical Role
- Executive Role
Lets assume that the business rules require that anyone can view these roles, but only certain profiles can create, edit, or delete these roles. Lets say I have the following 3 profiles
- Sales User
- Support User
- Marketing User
The rules say that
- Only Sales User profile can Create/Edit/Delete Buyer Roles
- Only Support User profile can Create/Edit/Delete Technical Roles
- Only Marketing User profile can Create/Edit/Delete Executive Roles
This seems like a fairly reasonable request, but with a little research and playing around you realize that this can't be done with standard Account Contact Roles. Wait, I can write some triggers to control this functionality.
Wait, WHAT??? Contact roles don't support triggers either. This is hopeless.......What to do????
I want to premise this solution by saying that
I in no way think this is the only or the best solution to this problem. In fact, there are likely 10+ other ways to solve this problem. This just happens to be the way that I solved it. In fact, I will show you a very elegant way that I stumbled across and think its a pretty slick solution.
http://www.youtube.com/watch?v=7QAWIPw5VIk&feature=youtu.be
The solution I came up with is a bit different. I decided to create a custom object to emulate the Standard Contact Role Object. By doing this, I can use triggers to control permissions.
I created a very simple custom object, Custom Contact Role. I kept it very simple, it has only 3 fields.
- Account - Master-Detail to Account
- Contact - Master-Detial to Contact
- Role - Pick-list
Now that I have my Custom object, I can write a trigger that controls the permissions on each Role. So I could just write a trigger that checks which role is being created, and which profile the current user has, and throw an error if its not he correct profile.
trigger CustomContactRoleTrigger on Custom_Contact_Role__c (before insert) {
map<Id,Profile> profileMap = new map<Id,Profile>([Select Id, Name From Profile]);
for(Custom_Contact_Role__c cRole : trigger.new){
if(cRole.Role__c == 'Buyer' && profileMap.get(Userinfo.getProfileId()).Name != 'Sales User'){
cRole.Role__c.addError('You do not have permission to create this Role');
}
}
}
BOOM! Great it works. Now what if the business rules change? What if in a few weeks they decide that another profile can create the Buyer role? That change would require you (or another developer type) to go in an update the code. What if the business wants an admin to be able to make these changes on the fly? This isn't possible with the current construct as any change in the business rules would require a developer to update the code. BUMMER.
So how do we fix that? How do we make it a bit more flexible to allow an admin to maintain this?
I thought using a hierarchy custom setting would be perfect for this. So I create a new hierarchy Custom setting. You can do this by navigating to
Setup -> Develop -> Custom Settings.
I won't get into custom settings and why they are awesome, but if you have never used them before or are curious as to why they are so awesome, here are 2 excellent posts on the 2 kinds of custom settings from Jeff Douglas. They are older, but still extremely relevant. I highly recommend following his blog as well, it's truly excellent.
http://blog.jeffdouglas.com/2010/01/07/using-list-custom-settings-in-salesforce-com/
http://blog.jeffdouglas.com/2010/02/08/using-hierarchy-custom-settings-in-salesforce-com/
I call my new hierarchical custom setting
Contact Role Permissions. I create a custom checkbox field on the setting for each role in my Role pick-list. So in this example I would need 3 custom fields, which match exactly to the roles in my pick-list.
- Buyer
- Technical
- Executive
Here's what my new custom setting looks like
Now that I have this configured, I can start adding new data sets to it by clicking the manage button. The first one I want to add is for the Org Wide permissions. We want to set all fields to false for the org wide values. This way, the default is to not allow anyone to update custom contact roles. We can add permissions to those who need it.
Now is where we can start adding profile specific permissions. Lets create new data sets for the 3 profiles we mentioned above. Lets also add a set for system administrators as we want them to be able to have permissions for all roles.
Here I create the System Admin record with access to change all three roles
So then we create the records for all three profiles, giving them the specific permission the business is asking for.
So now that we have all the custom settings as we need them, we just need to make some changes to my trigger. First I move the logic from the trigger itself and put it in a helper class.
public with sharing class CustomContactRoleTriggerHelper {
public void CustomRoleBeforeInsert(List<Custom_Contact_Role__c> newRoles){
checkPermissions(newRoles, null, 'create');
}
public void CustomRoleBeforeUpdate(List<Custom_Contact_Role__c> newRoles, List<Custom_Contact_Role__c> oldRoles, map<Id,Custom_Contact_Role__c> newMap, map<Id,Custom_Contact_Role__c> oldMap){
checkPermissions(newRoles, oldMap, 'update');
}
public void CustomRoleBeforeDelete(List<Custom_Contact_Role__c> newRoles, List<Custom_Contact_Role__c> oldRoles, map<Id,Custom_Contact_Role__c> newMap, map<Id,Custom_Contact_Role__c> oldMap){
checkPermissions(oldRoles, oldMap, 'delete');
}
private void checkPermissions(List<Custom_Contact_Role__c> rolesToCheck, map<Id,Custom_Contact_Role__c> oldMap, string actionDone){
Contact_Role_Permissions__c perm = Contact_Role_Permissions__c.getInstance(UserInfo.getUserId());
for(Custom_Contact_Role__c r : rolesToCheck){
if(actionDone == 'create'){
if(r.Role__c == 'Buyer' && !perm.Buyer__c || r.Role__c == 'Technical' && !perm.Technical__c || r.Role__c == 'Executive' && !perm.Executive__c){
r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + r.Role__c + '\'');
}
}else If(actionDone == 'update'){
if(oldMap.get(r.Id).Role__c == 'Buyer' && !perm.Buyer__c || oldMap.get(r.Id).Role__c == 'Technical' && !perm.Technical__c || oldMap.get(r.Id).Role__c == 'Executive' && !perm.Executive__c){
r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + oldMap.get(r.Id).Role__c + '\'');
}else if(r.Role__c == 'Buyer' && !perm.Buyer__c) || r.Role__c == 'Technical' && !perm.Technical__c) || r.Role__c == 'Executive' && !perm.Executive__c){
r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles to the role of \'' + r.Role__c + '\'');
}
}else If(actionDone == 'delete'){
if(oldMap.get(r.Id).Role__c == 'Buyer' && !perm.Buyer__c) || oldMap.get(r.Id).Role__c == 'Technical' && !perm.Technical__c) || oldMap.get(r.Id).Role__c == 'Executive' && !perm.Executive__c){
r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + oldMap.get(r.Id).Role__c + '\'');
}
}
}
}
}
So now we have a trigger, that actually uses our custom setting to check if the current user has permission to make the change to the contact role they are attempting.
This code grabs our custom setting record using the current users user Id.
Contact_Role_Permissions__c perm = Contact_Role_Permissions__c.getInstance(UserInfo.getUserId());
If the user is trying to create or insert a new custom contact role, we just need to check that the role they are trying to create, is a role they have permission to work with. If they do, no action, basically let the record be created without intervening. If they do not have permission, we add an error to the page.
if(actionDone == 'create'){
if(r.Role__c == 'Buyer' && !perm.Buyer__c || r.Role__c == 'Technical' && !perm.Technical__c || r.Role__c == 'Executive' && !perm.Executive__c){
r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + r.Role__c + '\'');
}
}
Here is an example if I am a Marketing User and try to create a new contact role with the role of 'Buyer'.
We do basically the same thing with updates, but for updates you have to check two scenarios. You need to check that you have the permission on the role you are changing the record to, but also the role that you are changing it from. For instance, I may have permission on the 'Technical' role, but does that mean I can change an existing record with a role of 'Technical' to 'Buyer'? I think not.
else If(actionDone == 'update'){
if(oldMap.get(r.Id).Role__c == 'Buyer' && !perm.Buyer__c || oldMap.get(r.Id).Role__c == 'Technical' && !perm.Technical__c || oldMap.get(r.Id).Role__c == 'Executive' && !perm.Executive__c){
r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + oldMap.get(r.Id).Role__c + '\'');
}else if(r.Role__c == 'Buyer' && !perm.Buyer__c) || r.Role__c == 'Technical' && !perm.Technical__c) || r.Role__c == 'Executive' && !perm.Executive__c){
r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles to the role of \'' + r.Role__c + '\'');
}
}
We do this by first using the map of old values and checking that the user has permissions to the role we are changing the record from. Then similar to insert we make sure the user has permission to the role we are changing it to.
Here is an example of a user having permission to the 'Buyer' role that we are changing it to, but doesn't have permission to the 'Technical' role we are changing it from.
For the delete, you obviously can only check the old values, as there are no new values.
else If(actionDone == 'delete'){
if(oldMap.get(r.Id).Role__c == 'Buyer' && !perm.Buyer__c) || oldMap.get(r.Id).Role__c == 'Technical' && !perm.Technical__c) || oldMap.get(r.Id).Role__c == 'Executive' && !perm.Executive__c){
r.Role__c.addError('You do not have permission to ' + actionDone + ' contact roles with the role of \'' + oldMap.get(r.Id).Role__c + '\'');
}
}
The only caveat with the delete is how the error is rendered on the page. For the insert and update, the error is the usual Salesforce red 'Error: Invalid Data' at the top of the page. When you sue addError() with delete in a trigger though, the error does not render this way. It is rendered on a separate page. Below is an example of a user that does not have permission on the 'Technical' role trying to delete a contact role of 'Technical'.
Its obvious a bit different, but the only way around it is overriding the standard page with Visualforce, and I really didn't think it was all that necessary.
So now we have it. We have a custom contact role that we can control who can create, edit, and delete. We also made it a bit more flexible. Remember how our first trigger required recoding if we decided there was another profile that we wanted to allow access to a certain role? That is no longer a problem. An admin can simply add a new value to our hierarchical custom setting. Below is me adding a new record to allow the Customer Advocate profile to have permission on the 'Buyer' and 'Technical' Role.
And that's all that needs to be done. Now the Customer Advocate profile can create, edit, and delete custom contact roles with the role of 'Buyer' and 'Technical'.
We now are able to give the business this functionality they have been asking for.
There is actually a few further steps we can take this to make it
even more flexible and dynamic, but I think I will take that step in my next post.
Hope you all found this useful and helpful.