Press ESC to close

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 can 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 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 repo, 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 correct, 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*/
        Boolean hasAccess = false;
        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 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

Leave a Reply

%d bloggers like this: