Hello Trailblazers,
Have you every faced this warning from PMD while writing your apex code which says:-
Validate CRUD permission before SOQL/DML operation (rule: Security-ApexCRUDViolation)
See: https://pmd.github.io/latest/pmd_rules_apex_security.html#apexcrudviolation
Have you every faced this warning from PMD while writing your apex code which says:-
Validate CRUD permission before SOQL/DML operation (rule: Security-ApexCRUDViolation)
See: https://pmd.github.io/latest/pmd_rules_apex_security.html#apexcrudviolation
Let's have a look at the code in which I am getting this warning. I have to display some contacts in my lightning component. I have created a controller class which will return me the list of contacts based on the limit and offset which are passed as parameters as shown below:-
I am getting the above warning to check CRUD permissions before SOQL from my PMD source code analyzer in VSCode. This warning basically suggests us to make sure that the current user has access to the object and fields that we're accessing or querying before executing the SOQL query. To remove this warning, we need to add a lot of code like:-
So my final code is shown below:-
And Finally..!! The warning is removed and we're done. But if we carefully have a look, we can see that we have to check for each and every field that we're querying so it can be a lot of code when we're querying a lot of fields or let's say when you have inner queries in an SOQL query, then it'll lead to a lot of extra code just to check permissions.
Thanks to Spring 20 Release, all this is history now 😎 and we can apply Object and Field Level Security in a single line without worrying about writing a lot of code. Let's see how..!!
WITH SECURITY_ENFORCED
This is the best way to make sure that the current user has the access to objects and fields when we're performing a READ operation or SOQL query. All you have to do is to add WITH SECURITY_ENFORCED clause in your SOQL query to enable object and field level permissions check.
Let's have a look at the updated code below:-
As you can see above, I have applied WITH SECURITY_ENFORCED after Contact in my query and that's all. This query will return a QueryException if we're querying a field which the current user doesn't have access to. To check that, I removed access from Phone field and called getContacts method from anonymous apex window. As you can see in the code, I am displaying the message from query exception using the getMessage() method. So, I got this message when I called the method after removing the access to the Phone field of Contact:- Insufficient permissions: secure query included inaccessible field.
So, in this way, we can perform a secure SOQL query and we don't have to check for any object or field level permissions. All that is already included in the query itself.
Some points to remember when using WITH SECURITY_ENFORCED clause:-
1. stripInaccessible(accessCheckType, sourceRecords, enforceRootObjectCRUD):- In this method, we have 3 parameters:-
2. stripInaccessible(accessCheckType, sourceRecords):- In this method, we have 2 parameters that are the same as described in above method. The only difference here is that while using this method, we're not checking for object level access.
I have updated my ContactsController class code and you can see that below:-
As you can see above, I have queried the list of contacts, displayed them and then checking the READ access on the list of contacts that I have queried. I have also specified the third parameter as true so that it'll check for the object accessibility as well. The stripInaccessible() method will return an object of SObjectAccessDecision class which is stored in the decision variable. This class has mainly 3 methods which we've called. The 3 methods are:-
I checked the debug messages and got messages in the debug as shown below :-
As you can see in the debug above, I have queried the list of contacts in which only the 3rd contact has a phone number. This 3rd contact will be at index 2 in the list. Therefore, getModifiedIndexes() is returning the index as 2 as only that contact is modified. The getRecords() method has returned the modified list of records in which you can see that the phone number is stripped from the 3rd record. Also, the getRemovedFields() method is returning the object name as key which is Contact in our case and the Set of fields that are removed which is Phone in our case.
I also tried removing the contact object access from the user and executed the method. I received the below NoAccessException in that case as shown below:-
System.NoAccessException: No access to entity Class.System.Security.stripInaccessible: line 15, column 1 Class.ContactsController.getContacts: line 15, column 1
Similarly, if you're trying to create/update a record, you'll get a NoAccessException in case you don't have access to that record. You can wrap this line in a try block and have a catch(NoAccessException e) in order to catch the NoAccessException. You can also use the stripInaccessible() method with two parameters so that you can strip the fields in list of records and have a noaccess exception check only covering the insert/update/upsert statement in a try catch block.
Note:- The stripInaccessible() method doesn't support AggregateResult SObject type. So, make sure you don't pass the records of type AggregateResult otherwise an exception will be thrown.
That's all for this post. If you liked this post, make sure to share it in your network and let me know your feedback in the comments down below.
Happy Trailblazing..!!
Let's have a look at the updated code below:-
As you can see above, I have applied WITH SECURITY_ENFORCED after Contact in my query and that's all. This query will return a QueryException if we're querying a field which the current user doesn't have access to. To check that, I removed access from Phone field and called getContacts method from anonymous apex window. As you can see in the code, I am displaying the message from query exception using the getMessage() method. So, I got this message when I called the method after removing the access to the Phone field of Contact:- Insufficient permissions: secure query included inaccessible field.
So, in this way, we can perform a secure SOQL query and we don't have to check for any object or field level permissions. All that is already included in the query itself.
Some points to remember when using WITH SECURITY_ENFORCED clause:-
- It's only applicable when you want to check for READ access.
- If we have a WHERE clause, we'll have WITH SECURITY_ENFORCED clause after the WHERE clause, otherwise it'll be after the FROM clause as shown in the query in the above code.
- It is always applied before any ORDER BY, LIMIT, OFFSET clauses as you can see above we have applied it before the LIMIT and OFFSET clause.
- It can also be applied in aggregate queries returning list of AggregateResult.
- Even if you have inner queries, you can have a single WITH SECURITY_ENFORCED in the outer query itself and it'll work for inner query fields.
- If you're accessing fields from parent object relationships WITH SECURITY_ENFORCED will work in that too, like:- if we are querying Account.Phone in our query above and we don't have access to phone field of account, it'll throw the same error. Make sure that it'll not work in some lookup fields like:- Owner, CreatedBy, LastModifiedBy.
- It'll not work if you have TYPEOF expressions in your queries.
- If you're creating an appexchange app, make sure the API version is 48.0 or later as this feature is Generally Available in this api version.
For more information on WITH SECURITY_ENFORCED have a look at the documentaton here:- https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_with_security_enforced.htm
stripInaccessible()
This method is mainly used to strip the fields from results of a query or subquery that the user doesn't have access to. The stripInaccessible method is defined in Security class and has two syntax as given below:-1. stripInaccessible(accessCheckType, sourceRecords, enforceRootObjectCRUD):- In this method, we have 3 parameters:-
- accessCheckType:- This is the access type parameter of the type AccessType enum. There are 4 values namely:- CREATABLE, READABLE, UPDATABLE and UPSERTABLE.
- sourceRecords:- This is basically the list of sobjects in which we have to remove fields that are inaccessible by the user.
- enforceRootObjectCRUD:- This is an optional boolean variable in which you specify wether you need to check for object level access or not. The default value of this parameter is true
2. stripInaccessible(accessCheckType, sourceRecords):- In this method, we have 2 parameters that are the same as described in above method. The only difference here is that while using this method, we're not checking for object level access.
I have updated my ContactsController class code and you can see that below:-
As you can see above, I have queried the list of contacts, displayed them and then checking the READ access on the list of contacts that I have queried. I have also specified the third parameter as true so that it'll check for the object accessibility as well. The stripInaccessible() method will return an object of SObjectAccessDecision class which is stored in the decision variable. This class has mainly 3 methods which we've called. The 3 methods are:-
- getModifiedIndexes() :- As you know that we pass a list of sobject in the second parameter. So, let's say we have 10 records out of which 5 of them have a value in Phone field which is inaccessible by the user. So, this method will return the indexes i.e. Set<Integer> i.e. the indexes in the list of sobjects on which those 5 records are present and have value in Phone field.
- getRecords() :- This method will return the modified list of sObjects after stripping the fields that are inaccessible by the user.
- getRemovedFields() :- This method will return a Map<String, Set<String>> in which the keys are basically the sObject names and values are Set<String> which are the field API names that are stripped from the records. This is mainly useful when you have subqueries, so that you can see which of the fields are stripped from each object.
I executed the above method in the anonymous window as shown below and I have removed access from Phone field of Contact.
As you can see in the debug above, I have queried the list of contacts in which only the 3rd contact has a phone number. This 3rd contact will be at index 2 in the list. Therefore, getModifiedIndexes() is returning the index as 2 as only that contact is modified. The getRecords() method has returned the modified list of records in which you can see that the phone number is stripped from the 3rd record. Also, the getRemovedFields() method is returning the object name as key which is Contact in our case and the Set of fields that are removed which is Phone in our case.
I also tried removing the contact object access from the user and executed the method. I received the below NoAccessException in that case as shown below:-
System.NoAccessException: No access to entity Class.System.Security.stripInaccessible: line 15, column 1 Class.ContactsController.getContacts: line 15, column 1
Similarly, if you're trying to create/update a record, you'll get a NoAccessException in case you don't have access to that record. You can wrap this line in a try block and have a catch(NoAccessException e) in order to catch the NoAccessException. You can also use the stripInaccessible() method with two parameters so that you can strip the fields in list of records and have a noaccess exception check only covering the insert/update/upsert statement in a try catch block.
Note:- The stripInaccessible() method doesn't support AggregateResult SObject type. So, make sure you don't pass the records of type AggregateResult otherwise an exception will be thrown.
That's all for this post. If you liked this post, make sure to share it in your network and let me know your feedback in the comments down below.
Happy Trailblazing..!!
Thanks a lot for the update. This really make a difference in coding and the way you explain the thing is really appreciable.
ReplyDeleteThank you so much. So happy to know that you liked it..!! Make sure to share it in your network too :-)
DeleteExcellent b helpful content Rahul, keep it up
ReplyDeleteGlad to know that you liked it buddy :-)
DeleteWow, great article Rahul!
ReplyDeleteHappy to know that you liked it buddy :-) Make sure to share it in your network too..!!
DeleteSuper COntent
ReplyDeleteThanks for the article. And appreciate for your time and effort to post this one it's helpful.
ReplyDeleteGood Work
ReplyDelete