Learn MOAR in Spring ’20 with Field Level Security in Apex

Discover Spring ’20 Release features! We are sharing release highlights for Developers (and Admins), curated and published by Salesforce product experts, as part of Learn MOAR. Complete the trailmix by March 31, 2020 to receive a special community badge and unlock a $10 contribution to FIRST®.

Controlling access to data based on user permissions is critical for any application. On Salesforce, you can control who sees what using Sharing rules, field and object permissions. However, it is important to note that Apex generally runs in system context, therefore the current user’s permissions, field-level security, and sharing rules aren’t taken into account during code execution, which can compromise the integrity of your data.

Using the with sharing keywords when declaring a class enforces Sharing Rules, but not object and field-level permissions. Below are some ways in which you can enforce object-level and field-level permissions in Apex.

Using Schema Methods

This way of determining field level access has been around for quite sometime now. You can call the isAccessible, isCreateable, or isUpdateable methods of Schema.DescribeFieldResult to verify whether the current user has read, create, or update access for a field.

For example, if you want to check if the Email field on the Contact Object is accessible/readable by the logged in user, you enclose the SOQL query inside an if block that checks for field access using the Schema methods described above.

if (Schema.sObjectType.Contact.fields.Email.isAccessible()) {
   Contact c = [SELECT Email FROM Contact WHERE Id= :Id];
}

Imagine that you have a bunch of fields in your query, and you have inner queries. This if statement becomes complex to maintain, and increases the cyclomatic complexity of the code.

Using WITH SECURITY_ENFORCED clause (Generally Available)

Generally Available starting Spring ‘20, the WITH SECURITY_ENFORCED clause can be used in SOQL queries to enforce field and object level security permissions in Apex code, including subqueries and cross-object relationships. Field-level permissions are checked for all the fields that are retrieved in the SELECT clause(s) of the query. Since this clause only works inside an SOQL query, it’s only useful when you want to check for read access on a field.

Here is an example:

try{
    List<Account> act1 = [SELECT Id, Name, (SELECT LastName FROM Contacts)
        FROM Account WHERE Name like 'Acme' WITH SECURITY_ENFORCED];
} catch(System.QueryException){
    //TODO: Handle Errors
}

The above query will return the Id and Name of Accounts, and the LastName of the related contacts, only if the user has read access to all of these three fields. If the user doesn’t have access to at least one of these fields, the query throws a System.QueryException exception, and no results are returned. As a best practice, SOQL queries that use this clause, have to be enclosed in a try/catch block, so that errors can be gracefully handled.

However, it is important to note that this clause doesn’t verify field-level security for fields used in the WHERE clause of the query. For example, if a user doesn’t have access a custom field called Picture_URL__c on the Contact object, the below query doesn’t throw an error, and the results are returned as usual.

List<Contact> contacts = [SELECT Id, Name, BirthDate 
                               FROM Contact 
                               WHERE Picture_URL__c != null WITH SECURITY_ENFORCED];

Using stripInaccessible Method (Generally Available)

Generally Available starting Spring ‘20, you can now use the stripInaccessible method from the new Security class to enforce field and object level security in Apex. Like the name suggests, this method can be used to strip the fields from sObject lists to which the user doesn’t have appropriate access, depending on the operation being performed.

Here is the method signature:

stripInaccessible(System.AccessType accessCheckType, List<SObject> sourceRecords, [Boolean enforceRootObjectCRUD])

  • accessCheckType: This parameter defines the type of field-level access check to be performed. It accepts System.AccessType enum values: CREATABLE, READABLE, UPDATABLE, UPSERTABLE.
  • sourceRecords: A list of sObjects to be checked for fields that aren’t accessible in the context of the current user’s operation.
  • enforceRootObjectCRUD: An optional parameter that indicates whether object-level access check has to be performed. If set to true, and the user doesn’t have the necessary CRUD permissions on the object, this method throws an exception. It defaults to true.

This method returns an object of type SObjectAccessDecision. You use the getRecords() method to access the list of sObjects which are stripped of fields that fail the field-level security checks for the current user. For error handling purposes, you can use the getRemovedFields() method to access a map of sObject types and their corresponding inaccessible fields.

Here is an example of a DML operation, where the current user doesn’t have access to a custom field Picture_URL__c on the Contact Object:

List<Contact> contacts = new List<Contact>{
    new Contact(FirstName='Jane', LastName='Doe', Picture_URL__c='someurl'),
    new Contact(FirstName='John', LastName='Doe', Picture_URL__c='someurl'),
};

// Strip fields that are not creatable
SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.CREATABLE,
    contacts);

//DML 
try{
    insert decision.getRecords();
}catch(NoAccessException e){
    //TODO: Handle Error if the user lacks create permission on the Object
}

// OPTIONAL: Print removed fields
System.debug(decision.getRemovedFields());

The DML operation written above runs successfully without exceptions, but the Picture URL field on the inserted records would be blank because the current user doesn’t have appropriate permissions on it, therefore the value has been stripped off. However, if the user lacked the create permission on the Contact object itself, the DML statement would throw an exception.

Here is another example of the method’s usage in a query operation, where the current user doesn’t have access to a custom field Picture_URL__c on the Contact Object.

Security.SObjectAccessDecision securityDecision = Security.stripInaccessible(
AccessType.READABLE,
[SELECT Name, Picture_URL__c from Contact];
);

for (Contact c : securityDecision.getRecords()) {
   system.debug(c.Name); //Prints: Jane, John
   system.debug(c.Picture_URL__c); //Prints: null, null
}

Inaccessible fields are removed from the query result, therefore those fields would return a NULL value.

If you are using the stripInaccessible method on a list of sObject records that have already been retrieved by a query, remember to use the getRecords() method to access the list of records with inaccessible fields removed. The original list of records is not updated by the stripInaccessible method and would still contain the values of inaccessible fields.

List<Contacts> contacts = [SELECT Name, Picture_URL__c from Contact];
Security.SObjectAccessDecision securityDecision = Security.stripInaccessible(
    AccessType.READABLE, contacts
);

system.debug(contacts); //Insecure access
system.debug(securityDecision.getRecords()); //Secure access

To sum up, this method can be used to:

  • Strip fields from query results that the user doesn’t have read access to.
  • Remove inaccessible fields before a DML operation without causing an exception.
  • Sanitize sObjects that have been deserialized from an untrusted source.

Summary

Even though ways to check and enforce field and object-level security were around for quite sometime, these new techniques make the code less verbose and more efficient. The WITH SECURITY_ENFORCED clause can be used directly in an SOQL query to check for read access on fields, and the query will throw an exception if a single field isn’t accessible. On the other hand stripInaccessible method can be used in read, create, update and upsert operations to strip the fields from sObject Lists that are inaccessible.

Now that you’ve seen how easy it is to enforce Field Level Security in Apex, check out the rest of our documentation and get hands on today!

 

About the Author:

Aditya Naag Topalli is a 10x Certified Senior Developer Evangelist at Salesforce. He focuses on Lightning Web Components, Einstein Platform Services, and integrations. He writes technical content and speaks frequently at webinars and conferences around the world. Follow him on Twitter @adityanaag