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

Abhinav Gupta

First Indian Salesforce MVP, rewarded Eight times in a row, has been blogging about Salesforce, Cloud, AI, & Web3 since 2011. Founded 1st Salesforce Dreamin event in India, called “Jaipur Dev Fest”. A seasoned speaker at Dreamforce, Dreamin events, & local meets. Author of many popular GitHub repos featured in official Salesforce blogs, newsletters, and books.

https://abhinav.fyi
Previous
Previous

Salesforce Sustainability Cloud - Leading the World towards Eco-Friendly Culture

Next
Next

Salesforce “Inherited Sharing”, explained!