Thursday, January 16, 2020

Chrome update - SameSite Lax (Salesforce impact)



Chrome February 2020 update can break many integration which relies on cookies (which is heavily used in iframe based integration). Salesforce internally uses iframe to render VF pages on lightning, so that is broken as well as of now, till salesforce fixes it.

How does cookies work (especially in cross site or iframe - when multiple sites are involved)

E.g. let's say salesforce.com is hosting site, and inside the salesforce.com we are hosting facebook.com as an iframe.



1) user logs in to facebook.com, and upon login facebook saves the cookies about user session
2) user logs in to salesforce.com where we are hosting facebook.com iframe. When browser loads facebook in iframe, it passes the facebook cookies to facebook, so it is not challenged with username and password

 This is good thing, however in certain cases if salesforce.com has bad code and/or facebook.com has vulnerable code, then facebook.com is vulnerable to cross side scripting attack.

With chrome February update, all cookies will be treated with SameSite=Lax if SameSite is not specified. Which means, in this case, browser will not send the cookies to facebook.com if it is coming from salesforce.com.
If you open facebook.com on separate browser tab, it would pass the cookies but not if embedded in any other site.

Solution
Other site will need to store the cookies with SameSite=None in order to get the old behavior.


For demo purpose, here is the Heroku web server, which stores cookies in different fashion and displays the cookies
  • displayCookies.html  : displays cookies belongs to this site 
  • storeCookies.html - stores cookies with no SameSite information (meaning it would be treated with None before February, and Lax after February release)
  • storeCookiesNone.html - stores cookies with SameSite=None
  • storeCookiesLax.html - stores cookies with SameSite=Lax 
  • storeCookiesStrict.html - stores cookies with SameSite=Strict




On Salesforce I have visualforce page, which points to displayCookies.html





Default Chrome Behavior (before February 2020)




  • Scenario 1 : on browser go to storeCookies.html, then go to salesforce visual force page.  This is working as expected



  • Scenario 2 : on browser goto storeCookiesNone.html, then go to salesforce visual force page. This is also working as expected. as it is default behavior.




  • Scenario 3 : on browser go to storeCookiesLax.html, then go to salesforce visual force page. This is removing cookies from iframe, which could cause undesired behavior.




  • Scenario 4 : on browser goto storeCookiesStrict.html, then go to salesforce visual force page. This is removing cookies from iframe, which could cause undesired behavior.





Default Chrome Behavior (after February 2020)




  • Scenario 1 : on browser goto storeCookies.html, then go to salesforce visual force page. This is removing cookies from iframe, which could cause undesired behavior. This would be working before February update.

  • Scenario 2 : on browser goto storeCookiesNone.html, then go to salesforce visual force page. This is working as expected!

Server side code for this:

response.header("Set-Cookie","Heroku-Username=Anonymous-StoreCookie-None; Secure; SameSite=None");
response.header("Set-Cookie","Heroku-SessionId=SessionId-StoreCookie-None; Secure; SameSite=None");

  • Scenario 3 : on browser goto storeCookiesLax.html, then go to salesforce visual force page. This is removing cookies from iframe, which could cause undesired behavior.
similar to Scenario 1
  • Scenario 4 : on browser goto storeCookiesStrict.html, then go to salesforce visual force page.This is removing cookies from iframe, which could cause undesired behavior.
similar to Scenario 1

Saturday, December 28, 2019

Loopback calls - Local calls in salesforce




There are many situations when we have to make back to salesforce - e.g.

a) if we want to use tooling api or rest api for certain reason
b) if managed package is licensed to only certain user, and we want to proxy user
c) we want to perform action as some another user

Below solution is not highly advisable but it is hack that gets job done:

1) Named Credentials
Create named credential for the proxy user, this is the user which will be used to call web service




2) Generic HTTP Call out
Generic http callout method for calling out so we can reuse it multiple times


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public with sharing class HttpService {

    public static String callout(String endPoint, String httpMethod, Map<String, String> headers, String body, Integer timeout) {
        Http http = new Http();
        HttpRequest httpRequest = new HttpRequest();
        httpRequest.setTimeout(timeout);
        httpRequest.setEndpoint(endPoint);
        httpRequest.setMethod(httpMethod);
        for(String key : headers.keySet() ) {
            httpRequest.setHeader(key, headers.get(key) );
        }
        httpRequest.setBody(body);
        System.debug('HttpService.callout request : ' + httpRequest);
        HttpResponse httpResponse = http.send(httpRequest);
        String responseBody = httpResponse.getBody();
        System.debug('HttpService.callout responseBody : ' + responseBody);
        return responseBody;
    }


3) A demo web service 

This is demo rest service which just prints the current user in session - inside we can put any logic that needs to be executed as proxy user


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Created by Chintan Shah on 12/27/2019.
 */

@RestResource(urlMapping='/DemoRestService/*')
global with sharing class DemoRestService {

    @HttpGet
    global static String doGet() {
        System.debug('DemoRestService.doGet currentUser ' + UserInfo.getUserName() );
        return [ select id from Account limit 1].Name;
    }

    @HttpPost
    global static String doPost(String name) {
        System.debug('DemoRestService.doPost currentUser ' + UserInfo.getUserName() + ' name ' + name );
        return name;
    }

}

We will also need to setup security so that proxy user's profile has access to the Apex class


4) Get proxy user's session id
A service to get session id token using named credential, which can be used to call any rest service or soap service internally using the proxy user

To get token using rest service, you need to create connected app, but with SOAP that is not needed. Hence, I just use soap service to login as proxy user via named credentials and get the session id back. A little XML parsing doesn't hurt much either.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
    @TestVisible
    private static String getValueFromXMLString(string xmlString, string keyField){
        string valueFound = '';
        if( xmlString.contains('<' + keyField + '>') && xmlString.contains('</' + keyField + '>') ) {
            try{
                valueFound = xmlString.substring(xmlString.indexOf('<' + keyField + '>') + keyField.length() + 2, xmlString.indexOf('</' + keyField + '>'));
            } catch (exception e){
                System.debug('Error in getValueFromXMLString.  Details: ' + e.getMessage() + ' keyfield: ' + keyfield);
            }
        }
        return valueFound;
    }

    public static String getTokenNamedCredentials(String namedCredential, String version) {
        String body = '<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:urn="urn:partner.soap.sforce.com">'
                + '           <soapenv:Body> '
                + '              <urn:login> '
                + '                 <urn:username>{!$Credential.Username}</urn:username> '
                + '                 <urn:password>{!$Credential.Password}</urn:password> '
                + '              </urn:login> '
                + '           </soapenv:Body> '
                + '       </soapenv:Envelope>';
        String endPoint = 'callout:' + namedCredential + '/services/Soap/u/' + version ;
        Map<String, String> headers = new Map<String, String> {
            'SFDC_STACK_DEPTH' => '1',
            'SOAPAction' => 'DoesNotMatter',
            'Accept' => 'text/xml',
            'Content-type' => 'text/xml',
            'charset' => 'UTF-8'
        };
        String responseBody = callout(endPoint, 'POST', headers , body, 60000);
        System.debug('HttpService.getTokenNamedCredentials responseBody : ' + responseBody );
        String token = getValueFromXMLString( responseBody, 'sessionId');
        System.debug('HttpService.getTokenNamedCredentials token : ' + token );
        return token;
    }


5) Call Demo service 
Actually calling the demo service using the proxy user's session id. The code is quite simple here.

Line 8 gets session id for proxy user
Line 9 constructs the rest service url
Line 10 generates the payload
We do have to set the Authorization token in line 14 for header, and then callout is quite simple on line 18.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * Created by Chintan Shah on 12/27/2019.
 */

public with sharing class DemoRestServiceProxyClient {

    public static String callDemoServiceOverHttp() {
        String token = HttpService.getTokenNamedCredentials('LocalEntry','47.0');
        String endPoint = Url.getSalesforceBaseUrl().toExternalForm() + '/services/apexrest/DemoRestService';
        String body = JSON.serialize( new Map<String, Object> {
            'name' => 'Chintan'
        } );
        Map<String, String> headers = new Map<String, String> {
            'Authorization' => 'OAuth ' + token,
            'Content-Type' => 'application/json'
        };
        System.debug(' DemoRestServiceProxyClient.callDemoServiceOverHttp  endPoint ' + endPoint + ' token ' + token + ' body ' + body );
        String response = HttpService.callout(endPoint,  'POST', headers, body, 60000);
        System.debug(' DemoRestServiceProxyClient.callDemoServiceOverHttp response ' + response );
        return response;
    }

}


We can see the output. Salesforce doesn't print the session Id, but we can see the output of the call.


Monday, November 11, 2019

Generic Clone Record Component

In Salesforce classic, we can use URL hack to clone the record and pre populate the data. In lightning, URL hack doesn't work. However, we can use e.force:createRecord component to initialize the values for create record.

For cloning, we need to see previous records and sometime it is custom logic that we need to implement to initialize the value for cloning. Hence, we wrote generic component to take care of this. Currently it only supports fieldset.

We started with Base Clone Component (CloneBaseComponent)

CloneBaseComponent.cmp


1
2
3
4
5
6
<aura:component extensible="true" description="CloneComponentBase" controller="CloneComponentBaseController" implements="force:lightningQuickActionWithoutHeader,lightning:actionOverride,force:hasRecordId">
    <aura:attribute name="recordId" type="String"/>
    <aura:attribute name="sObjectApiName" type="String"/>
    <aura:attribute name="fieldSetName" type="String"/>
    <aura:handler name="init" value="{!this}" action="{!c.doInit}"/>
</aura:component>

CloneBaseComponentController.js


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 * Created by Spring7 on 9/19/2019.
 */

({
    doInit: function(component, event, helper) {
        var action = component.get('c.getRecord');
        var recordId = component.get('v.recordId');
        var fieldSetName = component.get('v.fieldSetName');
        var sObjectApiName = component.get('v.sObjectApiName');
        action.setParams({recordId: recordId,fieldSetName:fieldSetName,sObjectApiName:sObjectApiName});
        action.setCallback(this, function (response) {
            if(response.getState() === 'SUCCESS'){
                var defaultFieldValues = response.getReturnValue();
                helper.createRecord(component, defaultFieldValues);
                helper.closeQuickActionModal(component);
            }else if (response.getState() ==="ERROR") {
                var errors = response.getError();
                if (errors) {
                    if (errors[0] && errors[0].message) {
                        var toastEvent = $A.get("e.force:showToast");
                        toastEvent.setParams({
                            "title": "Error",
                            "message": errors[0].message
                        });
                        toastEvent.fire();
                        helper.closeQuickActionModal(component);
                    }
                } else {
                    console.log("Unknown error");
                }
            }
        });
        $A.enqueueAction(action);
    }
});

CloneBaseComponentHelper.js


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Created by Spring7 on 9/19/2019.
 */

({
    closeQuickActionModal : function(component){
        var dismissActionPanel = $A.get('e.force:closeQuickAction');
        dismissActionPanel.fire();
    },
    createRecord : function(component, defaultFieldValues) {
        delete defaultFieldValues['Id'];
        var createRecordEvent = $A.get('e.force:createRecord');
        var sObjectApiName = component.get("v.sObjectApiName");
        createRecordEvent.setParams({
            entityApiName: sObjectApiName,
            defaultFieldValues: defaultFieldValues,
        });
        createRecordEvent.fire();
    }
})

How to use it

In Salesforce, we can not pass parameter to component when it is bound to a button. Hence we created a new component with just a few lines, which extends the base component and tie to a button.

CloneAccount Component
It calls CloneBaseComponent with fieldset (CloneAccount) which will be used for copy


1
2
3
4
5
6
7
8
<!--
 - Created by Spring7 on 9/19/2019.
 -->

<aura:component description="CloneAccount" extends="c:CloneComponentBase" implements="force:lightningQuickActionWithoutHeader,lightning:actionOverride,force:hasRecordId">
    <aura:set attribute="sObjectApiName" value="Account">
    <aura:set attribute="fieldSetName" value="CloneAccount">
</aura:set></aura:set></aura:component>

CloneAccount Button

We will need to bind this component to button



CloneAccount FieldSet
We will need to create the field set which will be used by clone base component on which fields to be copied.



In UI, we can see fields are cloned now:




source code can be downloaded from unmanaged package or github

Sunday, November 10, 2019

Scheduling job every 5 min

In salesforce, with UI, we can schedule the job on daily bases, and using cron expression we can do hourly. There is no facility to run the job every minute, or every 5 minutes. However salesforce allows to start the job a few seconds from now or a few minutes from now. We can use that hack to run the job every 5 minutes.

E.g. below : during the execute call, we can reschedule the job, which will abort existing job and create new job based on frequency provided.


/**
 * Created by Chintan Shah on 11/10/2019.
 */

public with sharing class MasterScheduler implements Schedulable  {

    private static final Integer BATCH_FREQUENCY_IN_MINUTES = 5; // ideally it should goto custom settings.
    private static final String JOB_NAME = 'MasterScheduler';

    public void execute(SchedulableContext schedulableContext) {
        System.debug('MasterScheduler.execute : ');
        rescheduleJob( JOB_NAME, BATCH_FREQUENCY_IN_MINUTES );
    }

    public static void rescheduleJob(String jobName, Integer batchFrequencyMinutes) {
        System.debug('MasterScheduler.rescheduleJob : jobName ' + jobName + ' batchFrequencyMinutes ' + batchFrequencyMinutes );
        abortJobByName(jobName);
        Datetime dt = system.now().addMinutes(batchFrequencyMinutes);
        String cronExpression = '0 ' + dt.minute() + ' ' + dt.hour() + ' ' + dt.day() + ' ' + dt.month() + ' ? ' + ' ' + dt.year();
        System.debug('MasterScheduler.rescheduleJob : cronExpression ' + cronExpression );
        System.schedule(jobName, cronExpression, new MasterScheduler());
    }

    public static void abortJobByName(String jobName) {
        System.debug('MasterScheduler.abortJobByName : jobName ' + jobName );
        List<CronTrigger> cronTriggers = [select id, TimesTriggered, NextFireTime, CronExpression, PreviousFireTime, StartTime, EndTime from CronTrigger];
        for(CronTrigger cronTrigger : cronTriggers ) {
            abortJobById(cronTrigger.Id);
        }
    }

    public static void abortJobById(String jobId) {
        System.debug('MasterScheduler.abortJobById : jobId ' + jobId );
        System.abortJob(jobId);
    }


}


We can start the job from anonymous block:


new MasterScheduler().execute(null);

This will run the job every 5 minutes. This is not best practice as we might be over using salesforce cloud resources, but it can come handy for certain situations.

Sandbox Refresh

Usually there are a lot of things needs to be done when doing sandbox refresh, e.g. activating uses, changing email address of contacts or other objects, etc.. We can plugin the sandbox refresh script mentioned here. However, we still have to write a lot of code or use tools to achieve many things needed depending on fullcopy or dev refresh.

We wrote framework to simplify the refresh of the sandbox, where we can add multiple plugins, and also added static resource configuration file which can help with plug in configuration.

1
2
3
public interface SandboxRefreshPlugin {
    String execute(SandboxRefreshTemplate sandboxRefreshTemplate);
}

We also wrote multiple plugins to do the actual work, e.g.
  • SandboxRefreshProvisionPlugin
    • This will provision the users.
    • e.g. activate them, change their profiles
    • in static resource, it is under ProvisionUserRecords tag
  • SandboxRefreshDataCreationPlugin
    • This will create sample data after refresh (useful for dev sandbox)
    • In static resource it is under sObjectRecords records
  • SandboxRefreshInvalidateEmailPlugin
    • This will invalidate email addressed for specified fields
    • E.g. Contact or other email address that might be used for sending emails
    • In static resource, it is under inValidateEmailsObjects
  • SandboxRefreshEncryptFieldsDataPlugin
    • This will encrypt the data for specified field
    • E.g. some of the PII, PHI data needs to be encrypted
    • In static resource it is under encryptFieldsData
Configuration static resource would help the plugin to be more configurable. Hence, we created static resource file as below

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
{
  "ProvisionUserRecords": [
    {
      "userName": "test@test.com.cshah",
      "profile": "System Administrator",
      "isActive": true
    }
  ],
  "notificationEmails": "test@springsoa.com",
  "inValidateEmailsObjects": [
    {
      "sObjectName": "Contact",
      "emailFields": "Email"
    },
    {
      "sObjectName": "Lead",
      "emailFields": "Email"
    },
    {
      "sObjectName": "Case",
      "emailFields": "SuppliedEmail"
    }
  ],
  "encryptFieldsData": [
    {
      "sObjectName": "Contact",
      "sObjectFields": [
        {
          "fieldName": "AssistantPhone",
          "format": "999{N}{N}{N}{N}{N}{N}{N}"
        },
        {
          "fieldName": "Email",
          "format": "test@{S}{S}{S}{S}.com"
        }
      ]
    }
  ],
  "sObjectRecords": [
    {
      "sObjectName": "Account",
      "reference": "ISO_Account_1",
      "attributes": {
        "Name": "1 West Finance",
        "Type": "ISO"
      }
    },
    {
      "sObjectName": "Account",
      "reference": "Merchant_Account1",
      "attributes": {
        "Name": "The Carrington Group Limited",
        "Type": "Merchant"
      }
    },
    {
      "sObjectName": "Opportunity",
      "reference": "Opportunity1",
      "attributes": {
        "Name": "The Carrington Group Limited - 190529",
        "AccountId": "reference-Merchant_Account1",
        "StageName": "Approved",
        "CloseDate": "10/24/2018"
      }
    },
    {
      "sObjectName": "Account",
      "reference": "Merchant_Account2",
      "attributes": {
        "Name": "Otto Case",
        "Type": "Merchant"
      }
    },
    {
      "sObjectName": "Opportunity",
      "reference": "Opportunity2",
      "attributes": {
        "Name": "Otto Case - 190307",
        "AccountId": "reference-Merchant_Account2",
        "StageName": "Approved",
        "CloseDate": "10/24/2018"
      }
    }
  ]
}

Most of the script initiates the batch file, as the full copy data could be quite huge. Now all we need to do is to specify the class SandboxRefresh during the refresh of the sandbox.

Code can be installed using unmanaged package : https://login.salesforce.com/packaging/installPackage.apexp?p0=04t2E000003ZQyl  or can be downloaded from github https://github.com/springsoa/springsoa-salesforce



Saturday, November 9, 2019

Large Modal

When we create lightning component and associate with button, it has configurable height but width is always half of the screen.

E.g. lightning component


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--
 - Created by Chintan Shah on 10/20/2019.
 -->

<aura:component description="DemoLargeModal" implements="force:lightningQuickActionWithoutHeader,force:hasRecordId">
    <!--ltng:require styles="{!$Resource.largeModal}" /-->
    <section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1"
             class="slds-modal slds-fade-in-open slds-modal_large">
        <div class="slds-modal__container">
            <header class="slds-modal__header">
                <lightning:buttonIcon iconName="utility:close"
                                      onclick="{! c.closeModel }"
                                      alternativeText="close"
                                      variant="bare"
                                      class="slds-modal__close">
                </lightning:buttonIcon>
                <h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate"> Header </h2>
            </header>
            <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
                Content
            </div>
        </div>
    </section>
</aura:component>

bound to button :



The height is configuration but width is not, and upon clicking the button, it would show narrow modal.




We can fix this by adding stylesheet to our lightning component. However it needs to be added using stylesheet.

Create CSS static resource:




1
2
3
4
.slds-modal__container{
    max-width: 70rem !important;
    width:80% !important;
}


Add the CSS in the lightning component:



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!--
 - Created by Chintan Shah on 10/20/2019.
 -->

<aura:component description="DemoLargeModal" implements="force:lightningQuickActionWithoutHeader,force:hasRecordId">
    <ltng:require styles="{!$Resource.largeModal}" />
    <section role="dialog" tabindex="-1" aria-labelledby="modal-heading-01" aria-modal="true" aria-describedby="modal-content-id-1"
             class="slds-modal slds-fade-in-open slds-modal_large">
        <div class="slds-modal__container">
            <header class="slds-modal__header">
                <lightning:buttonIcon iconName="utility:close"
                                      onclick="{! c.closeModel }"
                                      alternativeText="close"
                                      variant="bare"
                                      class="slds-modal__close">
                </lightning:buttonIcon>
                <h2 id="modal-heading-01" class="slds-text-heading_medium slds-hyphenate"> Header </h2>
            </header>
            <div class="slds-modal__content slds-p-around_medium" id="modal-content-id-1">
                Content
            </div>
        </div>
    </section>
</aura:component>


Now modal shows up wide:



Source code can be found at : https://github.com/springsoa/springsoa-salesforce

Sunday, October 20, 2019

Email Log

I know it has been around for a long time, but recently found this useful feature, where I can request the logs of all emails that has been sent. It can be super useful for troubleshooting.


It would be nice to have some heroku app which can download this periodically and create cases for emails that are not delivered.