We know that Apex by default does not enforce Object level permissions and field level permissions of the current running user. With Sharing or Without Sharing or Inherited Sharing never enforces Object level permissions and field level permissions of the current running user. To enforce Object level permission for current running user we can use sObject describe result methods Schema.DescribeSObjectResult. To enforce FLS for the current running user we can use field describe result methods Schema.DescribeFieldResult. In this way we can verify whether the logged in user has permissions to perform the DML or query.
By thoroughly reviewing the following content, we’ll gain clarity and ensure it sticks in our memory. Moreover, it will enable us to implement it with greater accuracy whenever similar tasks arise, thereby enhancing the robustness of functionality execution.
Problem Statement
In a project there are two custom objects – “Test Object Parent” (Test_Object_Parent__c) that is parent object and another object named “Test Object Child” (Test_Object_Child__c) is the child object. They are related by a Lookup field named “Test Object Parent” API as Test_Object_Parent__c. The OWD of both the objects is Private. Business wants that whenever a parent record will be created then one child record should also be created automatically. Business also wants only legitimate users to be able to do this. As of now, “Custom: Sales Profile” users will only be able to create, read, edit “Test Object Parent” (Test_Object_Parent__c) but should not have any object level access for the child object “Test Object Child” (Test_Object_Child__c). In future business may include and/or exclude other profiles for this function.
Below object level access details for Sales: Custom Profile user. Where you as System administrator have full access.
Solution Approach
Assumption: There are two profiles – “System Administrator” and another custom profile named “Custom: Sales Profile” already exist in the org. There is already a tab created called “Test Object Parent”.
Obviously, “System Administrator” profile should have all the accesses for both the objects.
- Write an Apex Trigger on parent object Test_Object_Parent__c and event is After Insert.
- Write an Apex Class with keyword “with sharing” having two methods. insertChildRecord and displayChildRecords.
- Call these methods from trigger.
- The profile “Sales: Custom Profile user” should have access to the class TestObjectParentHelper
<Apex Trigger Code>
trigger TestObjectParentTrigger on Test_Object_Parent__c (After Insert) { TestObjectParentHelper hlp = new TestObjectParentHelper(); hlp.insertChildRecord(Trigger.newMap); hlp.displayChildRecords(); }
<Apex Class Code>
/* Trigger name: TestObjectParentTrigger All methods with code will be executed enforcing record level Sharing Rules. On top of that it will implement Object Level Security. */ public with sharing class TestObjectParentHelper{ public void insertChildRecord(Map<Id,Test_Object_Parent__c> mapParentRecs){ List<Test_Object_Child__c> lstChildObj = new List<Test_Object_Child__c>(); for(Test_Object_Parent__c parentObj:mapParentRecs.values()){ Test_Object_Child__c childObj = new Test_Object_Child__c(); childObj.Name = 'Test Child Record from trigger'; childObj.Test_Object_Parent__c = parentObj.Id; lstChildObj.add(childObj); } //Option 1 //Below if condition to check current user's object level CREATE permission through profile, before creating record if(Schema.sObjectType.Test_Object_Child__c.isCreateable()){ insert lstChildObj; } } /* Method displayChildRecords() - Class having With Sharing keyword - Will fetch all records where owner is running user, nothing else as the class enforces Sharing Rule - Class having Without Sharing keyword - Will fetch all records irrespective of owner */ public void displayChildRecords(){ for(Test_Object_Child__c dObj:[SELECT Id, Name, Owner.Name FROM Test_Object_Child__c]){ system.debug('---Id:'+dObj.Id+'---Name:'+dObj.Name+'---Owner Name:'+dObj.Owner.Name); } } }
Testing steps
- Firstly, keep Option 1 section of Apex Class and then follow below steps
- Login to Salesforce org
- Click on “Test Object Parent” option from menu under Sales app.
- Click New to create a new Test Object Parent record providing value in Name field.
- Click on Save
Follow above testing steps by System Administrator user. Go to Developer Console->Query Editor and run below query.
SELECT Id, Name FROM Test_Object_Parent__c
SELECT Id, Name FROM Test_Object_Child__c
Follow above testing steps by Sales: Custom Profile user. Go back to your logged in org and go to Developer Console->Query Editor and run below query.
SELECT Id, Name FROM Test_Object_Parent__c
SELECT Id, Name FROM Test_Object_Child__c
(No record is created automatically from backend in case of User Maya[Profile: “Custom: Sales Profile”])
Further Testing Steps:
- Replace Option 1 by Option 2 in Apex Class and follow same testing steps.
- Next replace Option 2 by Option 3 in Apex Class and follow same testing steps.
//Option 2 //Below Database.Insert will check current user's object level permission before creating record List<Database.SaveResult> resList = Database.Insert(lstChildObj,false,AccessLevel.USER_MODE);
//Option 3 Id permissionSetId = [Select Id from PermissionSet where Name = 'AllowCreateToChildObj' limit 1].Id; Database.insert(lstChildObj, false, AccessLevel.User_mode.withPermissionSetId(permissionSetId));
Conclusion
Above test results show that in case of User Maya [Profile: “Custom: Sales Profile”] child record has not been created automatically from backend trigger/class code.
Option 1: This is because the below DML statement is used with user mode.
//Option 1 //Below if condition to check current user's object level CREATE permission through profile, before creating record if(Schema.sObjectType.Test_Object_Child__c.isCreateable()){ insert lstChildObj; }
Here, as per above code snippet, we have used isCreateable())because we need to do an Insert operation and if the current user has that CREATE permission or not. There are other methods to check other operations mentioned below.
isAccessible() – Use this when we need to check if the current user has READ permission or not.
isUpdateable() – Use this when we need to check if the current user has EDIT permission or not.
isDeletable() – Use this when we need to check if the current user has DELETE permission or not.
Option 2: This is because the below DML statement runs on user mode, implementing AccessLevel class.
//Option 2 //Below Database.Insert will check current user's object level permission before creating record List<Database.SaveResult> resList = Database.Insert(lstChildObj,false,AccessLevel.USER_MODE);
Remember, elevated access level is not considered here meaning it will check Object level permission only through current user’s profile access. There may be a possibility to have any permission set attached to the current user that elevates object level accesses. To take that access also in consideration, we need to use below code snippet.
//Option 3 Id permissionSetId = [Select Id from PermissionSet where Name = 'AllowCreateToChildObj' limit 1].Id; Database.insert(lstChildObj, false, AccessLevel.User_mode.withPermissionSetId(permissionSetId));
Here, you can access errors via SaveResult.getErrors().getFields()
Option 3: General Database operations (without using Database class) can specify either user or system mode using “as user” or “as system” respectively.
Here in below code snippet, “as user” is used to enforce User level execution mode.
//Option 3 try{ insert as User lstChildObj; }catch(SecurityException ex){ System.debug(‘---Exception:'+ex.getMessages()); }
If there was normal Insert DML statement without specifying any execution mode like “as user” or “as system”, then child record should be created automatically for any logged in user.
Remember, here in this case, exception may occur and need to catch through SecurityException