📣 Attention Salesforce Certified Trailblazers! Link your Trailhead and Webassessor accounts and maintain your credentials by December 14th. Learn more.
close
trailhead

Explore Custom Transaction Security Policies

Learning Objectives

Warning

Warning

Be careful—Transaction Security is a powerful feature. An incorrect Login Event policy that blocks for its real-time action locks you out of your org. To prevent this from happening in an org you care about, create a new Trailhead Playground for this module. Yes, we really mean a brand new Trailhead Playground.

After completing this unit, you’ll be able to:
  • Modify the generated Apex to implement your specific needs.
  • Write tests for your Apex classes.

Swimming in the Sea of Apex

At this point, you’ve delved into the depths of Transaction Security and seen how Apex makes it work. Now it’s time to pump up that Apex so that it does exactly what you want it to do. We’ll dive into the code and make changes to fine-tune a policy condition.

Customize the Apex for the Policy

Let’s modify the Block Android policy to block only older versions of the operating system. You might want to block older versions because of their security vulnerabilities. We make the policy more selective by changing the Apex code that Transaction Security generated.

We begin by editing the generated Apex code that implements the Block Android policy. In the list of policies, click BlockAndroidPolicyCondition. The Apex Class page opens and shows the code. Now click Edit to open the editor.

Editing window for the BlockAndroid policy.

You probably recognize this class from the earlier unit about Apex. If you like, you can go back to it for a refresher. We’re going to modify this code and also add some error checking to ensure that we’re looking at a login event..

Because Transaction Security examines all events, we want to make sure we’re working with a login event. After we get the LoginHistory object eObj from the event, we check whether the event is a login. The changes are noted with the comment “Addition”.
global class BlockAndroidPolicyCondition implements TxnSecurity.PolicyCondition {
  public boolean evaluate(TxnSecurity.Event e) {
    LoginHistory eObj = [SELECT Platform FROM LoginHistory
                         WHERE Id = :e.data.get('LoginHistoryId')];
    if (eObj != null) {  // Addition
      if(eObj.Platform == 'Android 9') {
        return true;
      }
    }  // Addition
    return false;
  }
}
This new test makes sure that there’s something in eObj, that is, that eObj is not equal to null. If eObj is null, we skip the test for the operating system, because we can’t get a platform value from eObj if it’s empty. We also cleaned up the indenting slightly to make the whole thing easier to read.
Next, we fine-tune the check for the OS and block any version of Android that’s release 5 or earlier, instead of just checking for release 9. The new test is marked with “Changed” comments.
global class BlockAndroidPolicyCondition implements TxnSecurity.PolicyCondition {
  public boolean evaluate(TxnSecurity.Event e) {
    LoginHistory eObj = [SELECT Platform FROM LoginHistory
                         WHERE Id = :e.data.get('LoginHistoryId')];
    if (eObj != null) {
      if (eObj.Platform.contains('Android') &&         // Changed
          eObj.Platform.compareTo('Android 5') < 0) {  // Changed
        return true;
      }
    }
    return false;
  }
}
Now, instead of only looking to see if the platform is Android, we also check the Platform value in eObj to see if it contains the string “Android”. If it does, we check if the version is 5 or less (older versions have lower version numbers). If that’s the case, the test is true, and True is returned, triggering the policy.
Note

Note

Here’s the description of what’s going on in that second if statement. The contains() part in Platform.contains('Android') checks whether the word “Android” is anywhere in the platform value. After all, the platform could also be something like “Windows”. If contains() doesn’t find Android, False is returned, and the if statement immediately stops processing. If the contains() function does find Android, it returns True, and we keep going.

The compareTo() function is what you expect: It compares the contents of Platform with the string we provide (“Android 5” here). If the number is the same or less, True is returned. And how can one string be less than another? Like so: “Android 3” is less than “Android 5”. That’s how we catch all older versions. The && symbol means “and,” so both the contains() and compareTo() functions have to return True for the entire test to be true.

Click Save to update the policy with your new code. When you save the code, it’s checked to make sure that its syntax is correct. If something was entered incorrectly, you get a bright red error containing the line and column number for the mistyped part. Fix that by comparing it to the example here, and try saving again.

Test the Apex for the Policy

You’ve learned about Apex programs and customized a class. The last step to becoming an ace Apex programmer is to test your code. When you start writing your own code, you should also write tests to make sure that your code works as you expect. You do that with test classes.

Begin by editing the Block Android policy’s code that you customized. You don’t change the original BlockAndroidPolicyCondition class. Instead, you add test code. Test classes aren’t specific to Transaction Security. All good Apex programmers use them to make sure that their code does what they expect it to, and to find errors before their users and customers find them. We’re going to jump right to the end and show you what test code looks like. Then we go through it so you understand what it does.

Here’s the BlockAndroidPolicyCondition that you’ve already created.
global class BlockAndroidPolicyCondition implements TxnSecurity.PolicyCondition {
  public boolean evaluate(TxnSecurity.Event e) {
    LoginHistory eObj = [SELECT Platform FROM LoginHistory
                         WHERE Id = :e.data.get('LoginHistoryId')];
    if (eObj != null) {
      if (eObj.Platform.contains('Android') &&
          eObj.Platform.compareTo('Android 5') < 0) {
        return true;
      }
    }
    return false;
  }
}
There are two test classes. The first class checks that the policy is triggered when the OS is Android 5 or earlier.
@isTest
public class TestBlockAndroid {
  public static testMethod void testIsAndroid() {

    /* Create a history object that has Platform = Android 4. */
    LoginHistory loginHistoryObj = new LoginHistory();
    loginHistoryObj.Platform = 'Android 4';

    /* Create a map for the event we’re going to build. */
    Map<String, String> eventData = new Map<String, String>();

    /* Insert the LoginHistoryId into the event data map. */
    insert loginHistoryObj;
    eventData.put('LoginHistoryId', loginHistoryObj.id);

    /* We’re not going to cause a real event in the org.
       Instead, we’re going to create a Transaction Security
       event object and “feed” it to the Policy Engine. */
    /* You can find more about TxnSecurity.Event in the 
       Apex Developer Guide. */
    TxnSecurity.Event e = new TxnSecurity.Event(
      '00Dxxx123123123', /* organizationId */
      '005xxx123123123', /* userId */
      'AuthSession', /* entityName */
      'Login', /* action */
      'LoginHistory', /* resourceType */
      '01pR00000009D2H', /* entityId */
      Datetime.newInstance(2016, 2, 15), /* timeStamp */
      eventData; ) /* data - Map with info about this event. */
        /* The only info in the map is the login history, and
           the only info in the login history is the Platform. */

    /* We are unit testing a PolicyCondition that triggers
       when a login is from Android OS version 5 or older. */
    BlockAndroidPolicyCondition condition =
      new BlockAndroidPolicyCondition();

    /* Assert that the condition is triggered by evaluating
       the event e. The Transaction Security PolicyCondition
       interface returns True if the policy is triggered. */
    System.assertEquals(true, condition.evaluate(e));
  }
}
Scary-looking? Let’s break it down. If you read the comments (the parts inside the /* and */), you can see the steps. We start by creating a fake login history object for the login condition we want to test. The only thing we put in the login history object is that the Platform value is Android 4. That value triggers the policy. Next we create a map object and add the login history object to the map. Transaction Security events all have a map of data about the event.
Note

Note

If you’re not familiar with Apex maps, they are collections of key-value pairs where each unique key maps to a single value. For example, the following table represents a map of countries and currencies.

Country (Key) 'United States' 'Japan' 'France' 'England' 'Canada'
Currency (Value) 'Dollar' 'Yen' 'Euro' 'Pound' 'Dollar'
We use the eventDatamap to map LoginHistoryId (the key) to loginHistoryObj.id (the value). The login history is the only data item we’re concerned with here, so that’s all we put in the map.

Next, we create a Transaction Security event. We make up values for its parts so that it’s the type of event we need for the test. Finally, we create a BlockAndroidPolicyCondition object, condition, and use the System.assertEquals() function to apply the condition to the event. If all goes well, the condition is true, and the test passes. If not, either the Apex code for the policy or the test class has a mistake, and we have to hunt for it.

The second class checks that the policy is not triggered if the OS is later than Android 5. This test works the same way as the first test, with two differences. We check for Android 6, which is okay to use according to our policy. The condition is supposed to return False at the end, because Android 6 is allowed.
@isTest
public class TestBlockAndroid {
  public static testMethod void testIsNotAndroid() {

    /* Create a history object that has Platform = Android 6. */
    LoginHistory loginHistoryObj = new LoginHistory();
    loginHistoryObj.Platform = 'Android 6';

    /* Create a map for the event we’re going to build. */
    Map<String, String> eventData = new Map<String, String>();

    /* Insert the LoginHistoryId into the event data map. */
    insert loginHistoryObj;
    eventData.put('LoginHistoryId', loginHistoryObj.id);

    /* We’re not going to cause a real event in the org.
       Instead, we’re going to create a Transaction Security
       event object and “feed” it to the Policy Engine. */
    /* You can find more about TxnSecurity.Event in the 
       Apex Developer Guide. */
    TxnSecurity.Event e = new TxnSecurity.Event(
      '00Dxxx123123123', /* organizationId */
      '005xxx123123123', /* userId */
      'AuthSession', /* entityName */
      'Login', /* action */
      'LoginHistory', /* resourceName */
      '01pR00000009D2H', /* entityId */
      Datetime.newInstance(2016, 2, 15), /* timeStamp */
      eventData; ) /* data - Map with info about this event. */
        /* The only info in the map is the login history, and
           the only info in the login history is the Platform. */

    /* We are unit testing a PolicyCondition that triggers
       when a login is from an Android OS version newer than 5. */
    BlockAndroidPolicyCondition condition =
      new BlockAndroidPolicyCondition();

    /* Assert that the condition is NOT triggered by evaluating
       the event e. The Transaction Security PolicyCondition
       interface returns False if the policy is not triggered. */
    System.assertEquals(false, condition.evaluate(e));
  }
}

Click Save to save the test classes with the policy. As always, if something isn’t entered correctly, you get a bright red error containing the line and column number. Fix any errors and try saving again. Use this example as a reference.

Now we have tests to check that our custom policy works correctly. We’ve created a positive test to make sure that our policy is triggered when the event criteria are met, and we created a negative test to confirm that the policy isn’t triggered when it shouldn’t be.

retargeting