Security.stripInaccessible() Bug with Protected Custom Settings
This post is my attempt to reach out to Salesforce with a full narration of a bug in Security.stripInaccessible() method with protected custom settings only. Also, this could help any ISV Partners using Security.stripInaccessible() with protected custom settings and running into issues.
Here is a quick summary of the issue:
Create a Protected Custom setting either of Hierarchy or List Type
As part of Security Review Compliance, one will either check for CRUD/FLS via Apex Describe Calls or use the newly released Security.stripInaccessible() Apex utility.
Add a class like Foo.cls that uses Security.stripInaccessible() to verify Custom Setting access in any Aura, LWC, Batch, or other Apex code.
Do a managed release or beta package upload, i.e. with a package prefix
Install the managed package in a client org.
In the client org, invoke Foo.cls, and you will get a FALSE Exception from Security.stripInaccessible() despite having READ/WRITE access on the protected custom settings.
Please Note: This issue is not reproducible in the packaging org, it only comes in the client/target org after installing the package.
The following video explains the situation 👇🏻
How to reproduce this error?
Install Managed Package
All the source code demonstrated in the above video can be installed via this package link:
https://login.salesforce.com/packaging/installPackage.apexp?p0=04t2x000003D7Rb&isdtp=p1
Or deploy this video’s source from GitHub to a new Developer Edition org, and upload a managed package as released/beta after registering a prefix. You can check out this repository, and use normal SFDX development tooling/flows to deploy this code to your own org.
Setup a Target Org
Assign the Accessor permission set to get the required permissions on Classes and Custom Settings.
Create a class with the following source code; in case you deployed the Github source to your own org, replace the “striptest” package prefix in the following code, with your own namespace prefix.
public class AccessorPlayGround {
public static void populate() {
//populate sample
datastriptest.Accessor.populateAll();
}
public static void assertAccess() {
// Generate classic CRUD FLS report
String accessReport = striptest.Accessor.generateClassicAccessReport();
System.debug (accessReport);
try {
striptest.Accessor.accessProtectedHierarchy(AccessType.READABLE);
System.debug (' ProtectedHierarchy is READABLE');
} catch (Exception ex) {
System.debug (' ProtectedHierarchy ' + ex.getMessage() + '\n' + ex.getStackTraceString());
}
try {
striptest.Accessor.accessProtectedList(AccessType.READABLE);
System.debug (' ProtectedList is READABLE');
} catch (Exception ex) {
System.debug (' ProtectedList ' + ex.getMessage() + '\n' + ex.getStackTraceString());
}
try {
striptest.Accessor.accessPublicHierarchy(AccessType.READABLE);
System.debug (' PublicHierarchy is READABLE');
} catch (Exception ex) {
System.debug (' PublicHierarchy ' + ex.getMessage() + '\n' + ex.getStackTraceString());
}
try {
striptest.Accessor.accessPublicList(AccessType.READABLE);
System.debug (' PublicList is READABLE');
} catch (Exception ex) {
System.debug (' PublicList ' + ex.getMessage() + '\n' + ex.getStackTraceString());
}
}
}
Execute the following code to start observing the debug logs for exception traces regarding Protected Custom Settings.
AccessorPlayGround.assertAccess();
If you have configured permission sets correctly, the user_debug output in debug logs for the above code line should have something to the following: Observe the bug in Security.stripInaccessible() around Protected Custom Settings only. This is a bug because the CRUD/FLS permissions are given for UPDATE to all 4 custom settings; only the public custom settings work correctly.
00:46:05.2 (147600827)|USER_DEBUG|[10]|DEBUG|Access Report for Object: striptest__Protected_Hierarchy__cObject : striptest__Protected_Hierarchy__c is Accessible: true, Createable: true, Updateable: trueField : Record ID is Accessible: true, Createable: false, Updateable: falseField : Deleted is Accessible: true, Createable: false, Updateable: falseField : Name is Accessible: true, Createable: true, Updateable: trueField : Location is Accessible: true, Createable: true, Updateable: trueField : Created Date is Accessible: true, Createable: true, Updateable: falseField : Created By ID is Accessible: true, Createable: true, Updateable: falseField : Last Modified Date is Accessible: true, Createable: true, Updateable: falseField : Last Modified By ID is Accessible: true, Createable: true, Updateable: falseField : System Modstamp is Accessible: true, Createable: false, Updateable: falseField : Last Viewed Date is Accessible: true, Createable: false, Updateable: falseField : Last Referenced Date is Accessible: true, Createable: false, Updateable: falseField : value is Accessible: true, Createable: true, Updateable: trueAccess Report for Object: striptest__Public_Hierarchy__cObject : striptest__Public_Hierarchy__c is Accessible: true, Createable: true, Updateable: trueField : Record ID is Accessible: true, Createable: false, Updateable: falseField : Deleted is Accessible: true, Createable: false, Updateable: falseField : Name is Accessible: true, Createable: true, Updateable: trueField : Location is Accessible: true, Createable: true, Updateable: trueField : Created Date is Accessible: true, Createable: true, Updateable: falseField : Created By ID is Accessible: true, Createable: true, Updateable: falseField : Last Modified Date is Accessible: true, Createable: true, Updateable: falseField : Last Modified By ID is Accessible: true, Createable: true, Updateable: falseField : System Modstamp is Accessible: true, Createable: false, Updateable: falseField : Last Viewed Date is Accessible: true, Createable: false, Updateable: falseField : Last Referenced Date is Accessible: true, Createable: false, Updateable: falseField : Value is Accessible: true, Createable: true, Updateable: trueAccess Report for Object: striptest__Protected_List__cObject : striptest__Protected_List__c is Accessible: true, Createable: true, Updateable: trueField : Record ID is Accessible: true, Createable: false, Updateable: falseField : Deleted is Accessible: true, Createable: false, Updateable: falseField : Name is Accessible: true, Createable: true, Updateable: trueField : Location is Accessible: true, Createable: true, Updateable: trueField : Created Date is Accessible: true, Createable: true, Updateable: falseField : Created By ID is Accessible: true, Createable: true, Updateable: falseField : Last Modified Date is Accessible: true, Createable: true, Updateable: falseField : Last Modified By ID is Accessible: true, Createable: true, Updateable: falseField : System Modstamp is Accessible: true, Createable: false, Updateable: falseField : Last Viewed Date is Accessible: true, Createable: false, Updateable: falseField : Last Referenced Date is Accessible: true, Createable: false, Updateable: falseField : Value is Accessible: true, Createable: true, Updateable: trueAccess Report for Object: striptest__Public_List__cObject : striptest__Public_List__c is Accessible: true, Createable: true, Updateable: trueField : Record ID is Accessible: true, Createable: false, Updateable: falseField : Deleted is Accessible: true, Createable: false, Updateable: falseField : Name is Accessible: true, Createable: true, Updateable: trueField : Location is Accessible: true, Createable: true, Updateable: trueField : Created Date is Accessible: true, Createable: true, Updateable: falseField : Created By ID is Accessible: true, Createable: true, Updateable: falseField : Last Modified Date is Accessible: true, Createable: true, Updateable: falseField : Last Modified By ID is Accessible: true, Createable: true, Updateable: falseField : System Modstamp is Accessible: true, Createable: false, Updateable: falseField : Last Viewed Date is Accessible: true, Createable: false, Updateable: falseField : Last Referenced Date is Accessible: true, Createable: false, Updateable: falseField : Value is Accessible: true, Createable: true, Updateable: true........00:46:05.2 (176462805)|USER_DEBUG|[16]|DEBUG| ProtectedHierarchy No access to entityClass.System.Security.stripInaccessible: line 15, column 1Class.System.Security.stripInaccessible: line 10, column 1Class.striptest.Accessor.assertAccess: line 177, column 1Class.striptest.Accessor.accessProtectedHierarchy: line 65, column 1Class.abhinav.AccessorPlayGround.assertAccess: line 13, column 1AnonymousBlock: line 1, column 1AnonymousBlock: line 1, column 1........00:46:05.2 (184833029)|USER_DEBUG|[23]|DEBUG| ProtectedList No access to entityClass.System.Security.stripInaccessible: line 15, column 1Class.System.Security.stripInaccessible: line 10, column 1Class.striptest.Accessor.assertAccess: line 177, column 1Class.striptest.Accessor.accessProtectedList: line 43, column 1Class.abhinav.AccessorPlayGround.assertAccess: line 20, column 1AnonymousBlock: line 1, column 1AnonymousBlock: line 1, column 1........00:46:05.2 (203847426)|USER_DEBUG|[28]|DEBUG| PublicHierarchy is READABLE........00:46:05.2 (215334265)|USER_DEBUG|[35]|DEBUG| PublicList is READABLE
Possible Workarounds?
Avoid Security.stripInaccessible() only for custom settings by checking it dynamically, as shown below:
public static void assertAccess(AccessType accessType, SObject[] records) {
Schema.DescribeSObjectResult dsr = records[0].getSObjectType().getDescribe();
String objectName = dsr.getLabel();
if (dsr.isCustomSetting()) {
/*Security.stripInaccessible(...) doesnt works correctly with protected custom settings*/
switch on accessType {
when READABLE {hasAccess = dsr.isAccessible();}
when CREATABLE {hasAccess = dsr.isCreateable();}
when UPDATABLE {hasAccess = dsr.isUpdateable();}
when UPSERTABLE {hasAccess = dsr.isUpdateable() && dsr.isCreateable();}
when else {hasAccess = false;}
}
if (hasAccess == false) {
String msg = String.format('"{0}" access missing on Object: "{1}"', new String[] { accessType.name(), objectName});
throw new YourException(msg);}
} else {
// Normal Objecttry {// Strip fields that are not updatableSObjectAccessDecision decision = Security.stripInaccessible(accessType, records);.. usual code
}
Managed Beta Upload Failure
With protected custom settings and this Github source, I ran into an issue of an internal Salesforce error while uploading a Managed Beta package. I managed to get out of this issue by doing a managed release upload. I know it might not be possible to always skip Beta packages. This issue happened with me and my peer developer as well, though after uploading a released package, the beta packages are uploading as well. 🙁
Update 24-July-2020
I shared the blog in a support case with Salesforce, and they acknowledged this problem as a known issue. If you are impacted by this issue as well, You can click on the button – “This issue affects me” on this article to get the latest updates on this issue.
References
Managed Package Source Code: https://github.com/abhinavguptas/sf-managed-package-custom-setting-stripInaccessible
YouTube Video Walkthrough: https://youtu.be/fKCly_WeBc8
Update 24-July-2020: Salesforce reported this as a Known Issue.