StripInaccessible: The great new way to enforce security in Apex

Mehdi Maujood
Salesforce Zolo
Published in
6 min readJun 5, 2019

Summer ’19 brings us a great new update to enforce security in Apex through the Security and SObjectAccessDecision classes. Put simply, the new feature (which is still in pilot phase as of this writing) allows us to clear field values on lists of sObjects that the user doesn’t have access to. In this post, we will dive into this new feature, code some experiments to see how exactly it works and explore different scenarios that the release notes didn’t cover.

We’ll start with a little background on Apex and Security which you can skip if you’re comfortable with the idea of the system execution context and the difference between field/object-level and record-level security in Apex.

Apex and Security: Background

The System Context

Any time you attempt to access data in Apex (through a SOQL query, for example), you get to query all fields on all objects. You also get to create and update records for objects that the current user doesn’t have access to. Not just fields and objects, you can even query all records regardless of the currently logged in user. This is the magic (or horror, in case you discovered this the hard way) of the system context.

There are exceptions to that rule. A Standard Controller in a Visualforce page will respect the user’s permissions, executing on what we can call the user context. That is why if you’re creating a custom Visualforce edit page or a Visualforce record page, Salesforce will thankfully ensure that an oversight from you doesn’t end up broadcasting confidential information to users. The Salesforce community has done a great job of documenting the different cases in which Apex executes in either the system context or the user context.

Object-level vs Record-level security

When we talk about security in Apex, there are generally two distinct kinds we consider: record-level security and object/field-level security.

The world of record-level security is relatively simple: we can use with sharing, without sharing and inherited sharing keywords to enforce the record-level security we need for Apex scripts running in the system context. Field-level and object-level security, however, are more complicated to deal with. By default, this kind of security is not enforced at all. While there are ways to enforce field-level security in Apex, updates in the Summer ’19 release give us a far cleaner alternative to the existing methods.

Strip inaccessible fields

The release notes for the new feature provide the following sample code:

List<Opportunity> opportunities = new List<Opportunity>{
new Opportunity(Name='Opportunity1'),
new Opportunity(Name='Opportunity2', Probability=95)
};

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

// Print stripped records
for (SObject strippedOpportunity : decision.getRecords()) {
System.debug(strippedOpportunity);
}

The idea is simple: fields that the user does not have access to will be unset. Emphasis on the word unset because this is different from setting a field’s value to null — A null value is saved to the database in case of a DML operation while a value of “unset” will be ignored.

The code passes the value AccessType.CREATABLE to the stripInaccessible function to indicate that fields that the user cannot create should be stripped, but we can also pass AccessType.UPDATABLE and AccessType.READABLE to strip out fields that the current user cannot update or read.

There are a few questions you could be asking at this point. What happens if the user doesn’t have access to the entire object? What if we query data and include parent fields? Will the parent fields be stripped as well? What happens if we include child objects in a sub-query? The release notes don’t mention all of those, so let’s dive in and find out!

Case 1: No access to sObject at all

For my first experiment, I removed all access for my test user for the Account sObject. I then had the following Apex executed in the system context while logged in as the test user:

List<Account> accounts = [SELECT Id, Name, AccountNumber, Phone FROM Account];

The query would, of course, return all Account records with every field with Name, AccountNumber and Phone fields set regardless of the test user’s permissions. That is how the system context works.

I then had this account list passed through the following function to get rid of fields that the user does not have read access to:

private List<Account> secureAccounts(List<Account> accounts) {
SObjectAccessDecision decision = Security.stripInaccessible(
AccessType.READABLE,
accounts);
return (List<Account>) decision.getRecords();
}

This is what I got back from the call to stripInaccessible:

System.NoAccessException: No access to entity: Account

stripInaccessible does not throw an exception if the user doesn’t have access to certain fields, but it looks like it does throw an exception if the user doesn’t have access to the sObject to begin with. This behavior makes sense because stripInaccessible is not supposed to remove any records from the passed list (remember that this function is not supposed to enforce record-level security), and a list of Accounts with null in every field is not a sensible thing for the function to return either.

enforceRootObjectCRUD

The stripInaccessible function has an optional parameter called enforceRootObjectCRUD which is true by default. If we pass false instead of the default value, however, we can avoid the exception and get back a completely useless list with null for every field.

Completely useless for now, but we’ll see it can make sense for some other cases we’ll explore soon.

Case 2: No access to fields on parent sObject

For this experiment, I queried Contacts and included information from the Account sObject:

List<Contact> contactsWithAccount = [SELECT Id, LastName, Phone, Account.Id, Account.Name, Account.Phone FROM Contact];

My test user did not have access to the Phone fields on either the Contact sObject or the Account sObject. The result was pretty much what I hoped for: the Phone and Account.Phone fields were null after being passed through stripInaccessible. It’s good to see that this feature strips out fields from parent sObjects as well.

Case 3: No access to parent sObject

For this experiment, I ran the exact same query as above but revoked all access to the Account sObject for my test user. The result? All the account fields were set to null. I was afraid that contactsWithAccount[0].Account itself would be set to null resulting in a NullPointerException upon trying to access contactsWithAccount[0].Account.Name, but was greeted with pleasant design choice of setting only the fields to null.

Interestingly, this time there was no System.NoAccessException.

Case 4: No access to object at all, but access to parent sObject

This experiment uses the same query I’ve used for the last two experiments:

List<Contact> contactsWithAccount = [SELECT Id, LastName, Phone, Account.Id, Account.Name, Account.Phone FROM Contact];

This time I tried something more interesting. I revoked my test users’ access to the Contact sObject, but left them read access for Account. The result was what I expected: System.NoAccessException.

And this is where the formerly uselessenforceRootObjectCRUD comes in.

When I set enforceRootObjectCRUD to false, the stripInaccessible function returned the records with null for Contact fields (as the user had no access to the Contact sObject) but had values for the Account fields.

Case 5: Subqueries

To experiment with lists that include child records, I tried the following list of sObjects:

List<Account> accountsWithContacts = 
[SELECT Id, Name, Phone,
(SELECT Id, LastName, Phone FROM Account.Contacts)
FROM Account];

Once again, I passed it through stripInaccessible to remove all fields that the user did not have read access to. I tried removing read access for the Phone field on Contacts, and tried removing access completely for the Contact sObject. The result in both cases was the same: nothing is removed from the child records.

It looks like stripInaccessible removes field values only on the original sObject list, but does not recursively remove field values on records obtained through subqueries. This is not a bad design choice — it gives us the flexibility to not have child records sanitized if we don’t need them sanitized. We always have the option of making a second call to stripInaccessible on the child list if we need to.

I did, however, receive feedback that the ability to strip fields from results of subqueries is on the roadmap and may be there in the beta release.

Conclusion

Overall, the feature looks very promising. Field and object-level security has never been fun to implement in Apex and this feature looks like it will make many use cases much easier to implement. I am looking forward to seeing this get past the pilot phase into a beta release and then generally available!

I do want to give a special thanks to Chris Peterson for giving some detailed feedback on this article and correcting me in a few places.

--

--

Mehdi Maujood
Salesforce Zolo

Software engineer, Salesforce enthusiast, Pluralsight author