CodeLive: Introducing the Minimum Access Profile

Did you know Apex executes in System mode and that if developers do not follow best practices this can unintentionally expose records and fields a user doesn’t have access to? Last week on CodeLive we worked with a couple of new features that deal specifically with this issue. Specifically the Summer ’20 Minimum Access Profile; and the Spring ’20 feature Security.stripInaccessible(). Missed it live? That’s ok, you can view it on demand here at Trailhead live. In the meantime, let’s recap!

What’s the issue?

Consider this unit test:

@isTest
static void testMinAccessProfileInSystemModeLeaksData(){
   // Unit test data created in an @testSetup method
   // This minAccess user is assigned the Minimum Access Profile
   User minAccess = TestFactory.createMinAccessUser(true);
    Test.startTest();
    List<Campaign> campaigns;
    System.runAs(minAccess){
        campaigns = [SELECT Name, BudgetedCost, ActualCost From Campaign];
    }
    Test.stopTest();
    System.assertEquals(150, campaigns.size(), 'expected to find 200 campaigns');
    for (Campaign cmp : campaigns) {
        System.assertEquals(200, cmp.BudgetedCost, 'expected to see 200 for the budgeted cost');
       System.assertEquals(299, cmp.ActualCost, 'expected to see 200 for the actual cost');
    }
}

As I read this test, the query is executed as a user with the Minimum Access Profile – a profile that by definition does not have campaign object access. Nor should it have access to the BudgetedCost or ActualCost fields. Yet this test passes! Why? It passes because the code above does not follow best practices, and because Apex executes in System mode. Apex that follows best practices must work to prevent data leaks. You can do this in a number of ways, as detailed by this Trailhead module. Additionally, libraries like fflib, ESAPI, and DMLManager all handle data leak mitigation for you. Recent additions to Apex, like WITH SECURITY_ENFORCED and stripInaccessible() provide quick, inline options for preventing data leaks. Adding the ‘WITH SECURITY_ENFORCED’ clause causes this test to fail with an insufficient permissions exception.

So how do we handle this?

As developers it’s important we test our code in a variety of permission scenarios. We can’t simply assume that a user has access to the particular records or fields that will be needed by our code after the query occurs. If the data is to be displayed to the user, we must ensure we’re not leaking privileged information. This is where Permissions Testing comes to light!

Access is paramount.

Sooner or later in your Salesforce career you’ll face the challenge of allowing a specific subset of your users to access data you otherwise want restricted. That might be specific fields – say Personally Identifiable Information. Historically, Salesforce admins created a custom Profiles and removed access to objects down to a minimal level, and then use permission sets to add access. Enter the Minimum Access Profile. If you missed the blurb in the release notes, the Minimum Access Profile probably sounds a bit anticlimactic. After all, it does exactly what the name says – provides a profile with the least access possible. However, this is one of those situations where the tool itself (the profile) is less important than what you can do with it! Assigning users the Minimum Access Profile means you’re forced to use the finer-grained, easier to deploy permission sets.

Permissions testing?

On stream we worked with the Minimum Access Profile, and the new Security.StripInaccessable() method. These pair nicely as each demonstrates how the other works. Together, they also illustrate the basic pattern for permission testing. Permission testing executes your code as a specific user – ensuring that fields, objects and apps are, or are not, accessible. More importantly, permissions testing helps you reason about your code, ensuring that it behaves as you expect when a user does, or does not have access. stripInaccessible() allows developers to ensure record(s) only contain the fields the user can access.

How to write Permission Tests?

The basic pattern for Permission Testing is pretty straight forward:

  1. Create your test data
  2. Create a test user
  3. Run your code as the test user
  4. Assert as relevant.

The nature of permission tests means you’ll likely be creating multiple tests – each with a different set of profiles or permission sets assigned. On stream we created two basic tests. The first executes the code as the a user with the Minimum Access Profile assigned. This is a negative permission test because we expect this test to fail – because the minimum access profile doesn’t give the user access to the object. The second test uses a permission set to give the user access to a specific field. This test, we expect to pass – but only when we try to access that specific field! Both of these tests exercise some code that relies on stripInacessible() to remove fields from the returned records. Using stripInaccessible() allows us to quickly, and easily test the permissions of a user.

Security.stripInaccessible() vs. WITH SECURITY_ENFORCED

At first glance, Security.stripInaccessible() and WITH SECURITY_ENFORCED seem to be doing the same thing. The truth is, there is some functional overlap. However, stripInaccessible() is capable of running against any set of records – including records you’re about to update, or insert. Specifically, stripInaccessible() can be used before DML to ensure the records aren’t updating records or fields they don’t have access to, a trick WITH SECURITY_ENFORCED can’t manage.

So what do I do now?

That’s the big question isn’t it? I think the there are two very important takeaways here. First, start migrating users, code and architecture to permission sets. Do it now, before someone adds another profile. Secondly, It’s important that developers write Permissions based tests – not only to show the business we’re not leaking data, but also to ensure our code handles a lack of data properly. Remember, you can tune into the on demand version of this CodeLive episode hereand don’t forget to join us every Thursday at 10am Pacific/1pm Eastern on Trailhead Live!