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.
In this content, we have discussed FLS enforcement in 3 ways. 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
Enjay Enterprise is an imaginary company. In their 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.
There is a custom profile named “Custom: Sales Profile”. As of now, “Custom: Sales Profile” users will only be able to CREATE, READ, EDIT “Test Object Parent” (Test_Object_Parent__c) and should have CREATE and READ permissions as object level access for the child object “Test Object Child” (Test_Object_Child__c).
Also, this profile should not have any access to a field called Rating (Rating__c).
Business wants that whenever a parent record will be created then one child record should also be created automatically populating few fields with specified static values. Business also wants only legitimate users to be able to do this. 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 fetchChildRecords.
- 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.fetchChildRecords(); }
<Apex Class Code>
/* Trigger name: TestObjectParentTrigger All methods with code will be executed enforcing Sharing Rules */ 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.Phone__c = '9096113718'; childObj.Email__c = '[email protected]'; childObj.Rating__c = 'Warm'; childObj.Test_Object_Parent__c = parentObj.Id; lstChildObj.add(childObj); } if(Schema.sObjectType.Test_Object_Child__c.isCreateable()){//Check CREATE accessibility before insert to avoid any exception //stripInaccessible removes inaccessible fields. But throws Exception 'No access to entity' if current user has no access to the object sobjectAccessDecision correctDesicion = Security.stripInaccessible(AccessType.CREATABLE,lstChildObj); //No Exceptions are thrown and no rating is set insert correctDesicion.getRecords(); } } /* Method fetchChildRecords() - 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 fetchChildRecords(){ //Option 1: Enforced no FLS checking in Query //In below for loop SOQL, Rating__c field will be fetched irrespective of current user's accessibility for(Test_Object_child__c dObj:[SELECT Id, Name, Email__c, Phone__c, Rating__c, Owner.Name FROM Test_Object_child__c]){ system.debug('---Id:'+dObj.Id+'---Name:'+dObj.Name+'---Email:'+dObj.Email__c+'---Phone:'+dObj.Phone__c +'---Rating:'+dObj.Rating__c+'---Owner Name:'+dObj.Owner.Name); } //Option 2: Enforced FLS checking of current user, using WITH USER_MODE clause in SOQL //In below for loop SOQL, WITH USER_MODE is added. Hence will throw Exception //System.QueryException: No such column 'Rating__c' on entity 'Test_Object_child__c'. Need to use try catch block. try{ for(Test_Object_child__c dObj:[SELECT Id, Name, Email__c, Phone__c, Rating__c, Owner.Name FROM Test_Object_child__c WITH USER_MODE]){ system.debug('---Id:'+dObj.Id+'---Name:'+dObj.Name+'---Email:'+dObj.Email__c+'---Phone:'+dObj.Phone__c +'---Rating:'+dObj.Rating__c+'---Owner Name:'+dObj.Owner.Name); } }catch(QueryException qe){ Map<String, Set<String>> mapInaccessibleFields = qe.getInaccessibleFields(); system.debug('---Query Exception: Missing Rating__c FLS: '+mapInaccessibleFields.get('Test_Object_child__c').contains('Rating__c')); } //Option 3: //AccessLevel.USER_MODE is used in Database.query(). It does not check FLS rather checks Object level access. So runs without error for(Test_Object_child__c dObj: Database.query('SELECT Id, Name, Email__c, Phone__c, Rating__c, Owner.Name FROM Test_Object_child__c',AccessLevel.USER_MODE)){ system.debug('---Id:'+dObj.Id+'---Name:'+dObj.Name+'---Email:'+dObj.Email__c+'---Phone:'+dObj.Phone__c +'---Rating:'+dObj.Rating__c+'---Owner Name:'+dObj.Owner.Name); } //Option 4: //stripInaccessible() is used in below block. sobjectAccessDecision correctDesicion = Security.stripInaccessible(AccessType.READABLE,[SELECT Id, Name, Email__c, Phone__c, Rating__c, Owner.Name FROM Test_Object_Child2__c]); for(sObject obj: correctDesicion.getRecords()){ Test_Object_Child2__c dObj = (Test_Object_Child2__c) obj; if(correctDesicion.getRemovedFields().get('Test_Object_Child2__c')!=null && correctDesicion.getRemovedFields().get('Test_Object_Child2__c').contains('Rating__c')){ system.debug('---Having no access to Rating__c'); system.debug('---Id:'+dObj.Id+ '---Name:'+dObj.Name+'---Email:'+dObj.Email__c+ '---Phone:'+dObj.Phone__c+ '---Owner Name:'+dObj.Owner.Name); }else{ system.debug('---Having all fields access'); system.debug('---Id:'+dObj.Id+ '---Name:'+dObj.Name+ '---Email:'+dObj.Email__c+ '---Phone:'+dObj.Phone__c+ '---Rating:'+dObj.Rating__c+'---Owner Name:'+dObj.Owner.Name); } } } }
Testing 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, Owner.name FROM Test_Object_Parent__c
SELECT Id, name, Email__c, Phone__c, Rating__c, owner.name FROM Test_Object_child__c
Above result shows that not only record has been created but also Rating__c field been populated with text “Warm” automatically from backend via trigger/class, as you as System Administrator has CRED access to the object with all fields access.
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, Email__c, Phone__c, Rating__c, owner.name FROM Test_Object_child__c
Above result shows that though record has been created but Rating__c field has not been populated with text “Warm” automatically from backend via trigger/class, as Maya as “Custom: Sales Profile” has CR access to the object and has all fields access except Rating__c.
Conclusion
FLS enforcement in DML using stripInaccessible
//Check CREATE accessibility before insert to avoid any exception if(Schema.sObjectType.Test_Object_Child__c.isCreateable()){ //stripInaccessible removes inaccessible fields. But throws Exception 'No access to entity' if current user has no access to the object sobjectAccessDecision correctDesicion = Security.stripInaccessible(AccessType.CREATABLE,lstChildObj); //No Exceptions are thrown and no rating is set insert correctDesicion.getRecords(); }
insertChildRecord method in TestObjectParentHelper class first creates record and place it in a List. But before Insert, it checks if the current user has at least CREATE access on the object or not to avoid ‘No access to entity’ exception. Then it uses stripInaccessible() to remove inaccessible fields from the record list accordingly, if required. Lastly it inserts record(s) with no exception and more correctly.
Consideration of FLS while fetching records using SOQL
There are 4 blocks for 4 types of fetching records in the method fetchChildRecords those explained below one by one.
Option 1: Enforced no FLS checking in Query
for(Test_Object_child__c dObj:[SELECT Id, Name, Email__c, Phone__c, Rating__c, Owner.Name FROM Test_Object_child__c]){ system.debug('---Id:'+ dObj.Id+ '---Name:'+ dObj.Name+'---Email:'+ dObj.Email__c+ '---Phone:'+ dObj.Phone__c+ '---Rating:'+ dObj.Rating__c+'---Owner Name:'+ dObj.Owner.Name); }
Simple SOQL query where FLS is not enforced but as this method belongs to the class which is having “with sharing” keyword, so records will be fetched as per current user’s record sharing access.
Option 2: Enforced FLS checking of current user, using WITH USER_MODE clause in SOQL
//Option 2: Enforced FLS checking of current user, using WITH USER_MODE clause in SOQL //In below for loop SOQL, WITH USER_MODE is added. Hence will throw Exception //System.QueryException: No such column 'Rating__c' on entity 'Test_Object_child__c'. Need to use try catch block. try{ for(Test_Object_child__c dObj:[SELECT Id, Name, Email__c, Phone__c, Rating__c, Owner.Name FROM Test_Object_child__c WITH USER_MODE]){ system.debug('---Id:'+ dObj.Id+ '---Name:'+dObj.Name+'---Email:'+dObj.Email__c+ '---Phone:'+dObj.Phone__c+ '---Rating:'+dObj.Rating__c+'---Owner Name:'+dObj.Owner.Name); } }catch(QueryException qe){ Map<String, Set<String>> mapInaccessibleFields = qe.getInaccessibleFields(); system.debug('---Query Exception: Missing Rating__c FLS:'+ mapInaccessibleFields.get('Test_Object_child__c').contains('Rating__c')); }
Here in the above code snippet, WITH USER_MODE is added to the SOQL query to enforce FLS. It throws exception System.QueryException: No such column ‘Rating__c’ on entity ‘Test_Object_child__c’. Hence to catch this exception we have to put the code block within try catch block. We can get list of inaccessible fields as per current user’s FLS using getInaccessibleFields() method from QueryException object.
Option 3: AccessLevel.USER_MODE used in Database.query(), does not check FLS but Object level access
//Option 3: //AccessLevel.USER_MODE is used in Database.query(). It does not check FLS rather checks Object level access. So runs without error. for(Test_Object_child__c dObj: Database.query('SELECT Id, Name, Email__c, Phone__c, Rating__c, Owner.Name FROM Test_Object_child__c', AccessLevel.USER_MODE)){ system.debug('---Id:'+dObj.Id+ '---Name:'+dObj.Name+'---Email:'+dObj.Email__c+ '---Phone:'+dObj.Phone__c+ '---Rating:'+dObj.Rating__c+ '---Owner Name:'+dObj.Owner.Name); }
Here in the above code snippet, AccessLevel.USER_MODE is used in Database.query() which actually does not check FLS rather check Object Level access. For more robust implementation, put the for block within one if block like if(Schema.sObjectType.Test_Object_Child__c. isAccessible())
Option 4: stripInaccessible to check FLS in SOQL
//Option 4: //stripInaccessible() is used in below block. sobjectAccessDecision correctDesicion = Security.stripInaccessible(AccessType.READABLE,[SELECT Id, Name, Email__c, Phone__c, Rating__c, Owner.Name FROM Test_Object_Child2__c]); for(sObject obj: correctDesicion.getRecords()){ Test_Object_Child2__c dObj = (Test_Object_Child2__c) obj; if(correctDesicion.getRemovedFields().get('Test_Object_Child2__c')!=null && correctDesicion.getRemovedFields().get('Test_Object_Child2__c').contains('Rating__c')){ system.debug('---Having no access to Rating__c'); system.debug('---Id:'+dObj.Id+ '---Name:'+dObj.Name+'---Email:'+dObj.Email__c+ '---Phone:'+dObj.Phone__c+ '---Owner Name:'+dObj.Owner.Name); }else{ system.debug('---Having all fields access'); system.debug('---Id:'+dObj.Id+ '---Name:'+dObj.Name+'---Email:'+dObj.Email__c+ '---Phone:'+dObj.Phone__c+ '---Rating:'+dObj.Rating__c+'---Owner Name:'+dObj.Owner.Name); } }
In the above code snippet, stripInaccessible is used to check FLS as per the current user’s access and remove them from the SOQL. So be careful while coding. First condition is added to the if condition as because in case of any user like system administrator having all field access, it will stop throwing null pointer exception for the part correctDesicion.getRemovedFields().get(‘Test_Object_Child2__c’).