Salesforce Object Level and Field Level Security Architecture

The Salesforce Security review require that both:

  1. Object Level (OLS)
  2. Field Level (FLS)

Security is applied for the following areas:

  1. Query of data – Selectors
  2. Triggers – Domains
  3. DML – Unit of Work

To check both (OLS) and FLS we can use fflib_SecurityUtils it has all the to methods to check if a user can access an object and also individual fields for that object. An Apex exception is thrown if the user does not have access to the given object and/or fields.

Example is when a community user tries to create an Account the following error occurs if OLS is enabled

fflib_SecurityUtils.CrudException: You do not have permission to insert Account
Class.fflib_SecurityUtils.checkObjectIsInsertable: line 309, column 1
Class.fflib_SObjectUnitOfWork.insertDmlByType: line 583, column 1
Class.fflib_SObjectUnitOfWork.doCommitWork: line 531, column 1
Class.fflib_SObjectUnitOfWork.commitWork: line 509, column 1
Class.App_Service_Test.testSecurityUOW: line 81, column 1

  1. Query of data – Selectors

The key purpose of this class is to make building dynamic SOQL queries safer and more robust than traditional string concatenation or String.format approaches. It also has an option to automatically check read security for the objects and fields given to it. Here are some of the methods we use to dynamically build all our queries.

Enable FLS check for queries before they run, by setting setEnforceFLS(true)

OR set the OLS and FLS when constructing a new Selector so it will be implement for all queries

OpportunitiesSelector oppsSelector = 
new OpportunitiesSelector(includeFieldSetFields, enforceObjectSecurity, enforceFLS);

2. Triggers – Domains

Domain classes has minimal functionality in it other than the routing of trigger events to the applicable virtual methods and object security enforcement.

Domain classes by OLS by default as part of it’s Configuration class:

3. DML – Unit of Work

This is not currently implemented to check the following:

1. Insert

  if (enforceOLS){
       fflib_SecurityUtils.checkObjectIsInsertable(sObjectType);
  }

2. Update

 if (enforceOLS){
       fflib_SecurityUtils.checkObjectIsUpdateable(sObjectType);
  }


3. Delete 

if (enforceOLS){
     fflib_SecurityUtils.checkObjectIsDeletable(m_sObjectTypes[objectIdx--]);
  }

Disadvantages of OLS and FLS implemented across your app

OLS – this is a check that is done to see if the user profile has access to do CRUD on the specific object. If this check is not done and user tries to do any of the CRUD operations it will just result in a Salesforce error. Doing this check on an object level is not too big of an overhead. If these checks are not done, Permission errors will be thrown as the profile does not have the correct permission.

FLS – this is not recommended as we have to iterate through every field of the object and check if the user has the privileges to read/update/insert/delete to that field. This slows down operations down drastically and only needs to be used in a few uses cases.

Possible solutions run security checks on app

Have a way to enable both OLS and FLS in your code then run through all your test and see if any break. If the break GREAT fix your Object Settings/FLS so that they work. When running in Production have a way to disable this as it may/will slow down your operations/services.

Apex Interview Question > FizzBuzz

Consider the following problem:

Write a short program that prints each number from 1 to 100 in one line

For each multiple of 3, print “Fizz” instead of the number.

For each multiple of 5, print “Buzz” instead of the number.

For numbers which are multiples of both 3 and 5, print “FizzBuzz” instead of the number.
Write a solution (or reduce an existing one) so it has as few characters as possible.

Solution 1: For loop with multiple if statements

String str = '';
for(Integer i = 1; i<=100; i++){
    if(Math.mod(i,3)==0)
        str+='Fizz';
    if(Math.mod(i,5)==0)
        str+='Buzz';
    else if(Math.mod(i, 3) != 0)
		str+=i;
    str +=',';
}
System.debug(str);

Output

1,2,Fizz,4,Buzz,Fizz,7,8,Fizz,Buzz,11,Fizz,13,14,FizzBuzz,16,17,Fizz,19,
Buzz,Fizz,22,23,Fizz,Buzz,26,Fizz,28,29,FizzBuzz,31,32,Fizz,34,Buzz,
Fizz,37,38,Fizz,Buzz,41,Fizz,43,44,FizzBuzz,46,47,Fizz,49,Buzz,
Fizz,52,53,Fizz,Buzz,56,Fizz,58,59,FizzBuzz,61,62,Fizz,64,Buzz,
Fizz,67,68,Fizz,Buzz,71,Fizz,73,74,FizzBuzz,76,77,Fizz,79,Buzz,
Fizz,82,83,Fizz,Buzz,86,Fizz,88,89,FizzBuzz,91,92,Fizz,94,Buzz,
Fizz,97,98,Fizz,Buzz

Apex Test Active Case Assignment Rules Assertion Fails

If you are testing active assignment rules you need to add it as an DMLOptions during testing else your assignment rule(s) will not get called and your assertion(s) may fail.

If you have one active Case Assignment Rule you can use dmlOpts.assignmentRuleHeader.useDefaultRule = true else you need to query for AssignmentRules. To query AssignmentRules you will need to have without sharing set as your class level access, else you will not get access to AssignmentRules.

If you have one method to insertCases you can make sure your assignmentRules run every time you execute a test class.

  public Id insertCase(Case bpCase){
      fflib_ISObjectUnitOfWork uow = Rest_App.UnitOfWork.newInstance();

      if (Test.isRunningTest()){
        Database.DMLOptions dmlOpts = new Database.DMLOptions();
         dmlOpts.assignmentRuleHeader.useDefaultRule = true;
         bpCase.SetOptions(dmlOpts);
      }

      uow.registerNew(bpCase);
      uow.commitWork();
      return bpCase.Id;
}

Here is the actual test class:

	@isTest(SeeAllData=true) static void testClientRequest(){
		RestRequest req = new RestRequest();
		RestResponse res = new RestResponse();
		Map<String, String> mapOfIds = App_Global_Test.setupCommunityUserReturnIds();

		test.startTest();
			System.runAs(new User(Id=mapOfIds.get('UserId'))){
				req.requestBody = Blob.valueOf('{"contactId":"'+mapOfIds.get('ContactId')+'", "type":"Client Request", "description":"Please help me with my account", "subject":"Client Request", "reason":"Client Request", "origin":"App"}');
				req.requestURI = '/v1/support/case';
				req.httpMethod = 'POST';
				RestContext.request = req;
				RestContext.response = res;

				App_Rest_Dispatcher.doPost();
				System.assert(res.responseBody!=null);
				System.assertEquals(res.statusCode, 200);
		 }
		test.stopTest();

		List<Case> supportCase = [Select Id, Subject, Description, Type, Owner.Name from Case where ContactId=:mapOfIds.get('ContactId')];
		System.assertEquals(supportCase.size(), 1);
		System.assertEquals(supportCase.get(0).Subject, 'Client Request');
		System.assertEquals(supportCase.get(0).Type, 'Client Request');
		System.assertEquals(supportCase.get(0).Owner.Name, 'Client Services Queue');
	}

Apex DocumentLink Trigger to Share Documents to Community Contacts

Trigger to check if the custom object has the correct file shared to community user

public without sharing class Domain_Documents extends fflib_SObjectDomain {

	public Domain_Documents(List<Documents__c> records) {
		super(records);
	}

	public class Constructor implements fflib_SObjectDomain.IConstructable{
		public fflib_SObjectDomain construct(List<SObject> sObjectList){
			return new Domain_Documents(sObjectList);
		}
	}

	public override void onAfterUpdate(Map<Id,SObject> existingRecords){
		List<ContentDocumentLink> contentDocumentLinksList = new List<ContentDocumentLink>();
		for (Documents__c documents : (List<Documents__c>) Records){
			if (documents.File_Id__c!=null){
				Documents__c existingDocument = (Documents__c)existingRecords.get(documents.Id);
				if (existingDocument.File_Id__c==null){
					List<ContentDocumentLink> contentDocumentLinkExists = [select ContentDocumentId, Id, IsDeleted, LinkedEntityId, ShareType, SystemModstamp, Visibility from ContentDocumentLink where ContentDocumentId=:documents.File_Id__c];
					Id userToShareWith = [Select Client__r.User__c from Documents__c where Id=:documents.Id].Client__r.User__c;
					Boolean flagToAddToDocs = true;
					for (ContentDocumentLink contentDocumentLinkExist : contentDocumentLinkExists){
						if (contentDocumentLinkExist.LinkedEntityId==userToShareWith){
							flagToAddToDocs=false;
						}
					}
					if (flagToAddToDocs){
						ContentDocumentLink cdl = new ContentDocumentLink(LinkedEntityId = userToShareWith , ContentDocumentId=documents.File_Id__c, shareType = 'C');
						contentDocumentLinksList.add(cdl);
					}
				 }
             }
		}

		if (!contentDocumentLinksList.isEmpty()){
			insert contentDocumentLinksList;
		}
	}
}

Test class to check that sharing is done correctly

@isTest(SeeAllData=true) static void testUpdate() {
		User communityUser = Global_Test.setupCommunityUserAndLogin();

		Documents__c bpDocument = (Documents__c)SmartFactory.createSObject('Documents__c');
		bpDocument.OwnerId = communityUser.Id;
		bpDocument.File_Name__c='Test';
		bpDocument.Document_Status__c = 'New';
		bpDocument.Client__c = communityUser.ContactId;
		insert bpDocument;

		String yourFiles = 'Some file content here';

		ContentVersion conVer = new ContentVersion();
		conVer.ContentLocation = 'S'; // S specify this document is in SF, use E for external files
		conVer.PathOnClient = 'test.txt'; // The files name, extension is very important here which will help the file in preview.
		conVer.Title = 'Test file '; // Display name of the files
		conVer.VersionData = EncodingUtil.base64Decode(yourFiles); // converting your binary string to Blog
		conVer.NetworkId=[Select Id from Network where Status='Live'][0].Id; //current Live community, should just be 1 but could have many
		insert conVer;

		Id conDoc = [SELECT ContentDocumentId FROM ContentVersion WHERE Id =:conVer.Id].ContentDocumentId; //query for contentDocumentId
	 	ContentDocumentLink cDe = new ContentDocumentLink();
		cDe.ContentDocumentId = conDoc;
		cDe.LinkedEntityId = bpDocument.Id; // you can use objectId,GroupId etc
		cDe.ShareType = 'C';
		cDe.Visibility = 'AllUsers';
		insert cDe;

		bpDocument.File_Id__c = conDoc;
		update bpDocument;

		Test.startTest();
			System.runAs(communityUser){
				bpDocument.File_Name__c='Update Name of File';
				update bpDocument;
			}
		Test.stopTest();

		Documents__c queryBpDocuments = [Select Id, File_Name__c, Document_Type__c from Documents__c where Id=:bpDocument.Id][0];
		System.assertEquals(queryBpDocuments.File_Name__c, 'Update Name of File');
	}

Apex generate a hash value for encrypting and decrypting emails

String cryptoSalt='249D5EC76175B12A';
Blob cryptoKey=Blob.valueof('J@NcRfUjXn2r5u8x/A?D(G-KaPdSgVkY');
Blob cryptoIv =Blob.valueof('4t6w9z$C&F)J@NcR');

Blob data = Blob.valueOf('youremail@gmail.com'+cryptoSalt);
Blob encryptedData = Crypto.encrypt('AES256', cryptoKey, cryptoIv, data);
String encodeDataHex = EncodingUtil.convertToHex(encryptedData);
System.debug('>>> encodeDataHex >>> ' + encodeDataHex);
//92939c472350e797aca42206a1b1d90f3b33bd1dbbac23af5fba40866515ef43e11e35edd354d341253bf89eb4094f2c

String encodeData = EncodingUtil.base64Encode(encryptedData);
System.debug('>>> encodeData >>> ' + encodeData);
//kpOcRyNQ55espCIGobHZDzszvR27rCOvX7pAhmUV70PhHjXt01TTQSU7+J60CU8s

// Decrypt the data - the first 16 bytes contain the initialization vector
Blob decodeData = EncodingUtil.base64Decode(encodeData);
Blob decryptedData = Crypto.decrypt('AES256', cryptoKey, cryptoIv, decodeData);

// Decode the decrypted data for subsequent use
String decryptedDataString = decryptedData.toString();
System.debug('>>> ' + decryptedDataString);
//youremail@gmail.com249D5EC76175B12A

Apex save class when scheduled > ‘This schedulable class has jobs pending or in progress’

How do I develop in a class that is referenced in a scheduled job?You will see the following error when you try to save this class:

This schedulable class has jobs pending or in progress

There is the workaround, what I did is create a Job scheduler so basically 1 scheduled class from where I reference all my scheduled classes. We are not initializing the class by creating a object of the class by name:

global class App_Job_Scheduler implements Schedulable {

	global void execute(SchedulableContext sc) {
		System.Type closedAccountsScheduler = Type.forName('Job_Closed_Accounts');
		Schedulable closedAccountObject = (Schedulable)closedAccountsScheduler.newInstance();
		closedAccountObject.execute(sc);
       }
}

Scheduler class, now you can schedule this class and will not get the >This schedulable class has jobs pending or in progress error when you try to save any code to App_Service or Selector_Financial_Account

global class Job_Closed_Accounts implements System.Schedulable, Database.Batchable<SObject>, Database.Stateful, Database.AllowsCallouts {

	global Database.QueryLocator start(Database.BatchableContext BC) {
		return App_Selector_Financial_Account.newInstance().selectAccountAndRelatedObjectsToDelete();
	}

    global void execute(Database.BatchableContext BC, List<Financial_Account__c> financialAccountIdsToBeDeleted) {
		List<Financial_Account__c> financialAccountsToDelete = App_Selector_Financial_Account.newInstance().selectAccountAndRelatedObjectsToDelete(financialAccountIdsToBeDeleted);
		App_Service.instance.deleteRelatedAccountsRecords(financialAccountsToDelete);
	}

	global void finish(Database.BatchableContext BC) {}
}

Apex callout PATCH to Heroku Controller workaround

Setup a method to send Aync call to Heroku, add ‘?_HttpMethod=PATCH’ to the path to notify Heroku it’s a patch request

@future(callout=true)
	public static void callApiEndpointAsync(String apiEndpoint, String method, String aPayload){
		HttpRequest req = new HttpRequest();
		List<String> theArgs = new List<String>();
		HttpResponse res = new HttpResponse();
    try {
      if (apiEndpoint != null) {
        req.setTimeout(120000);
				if ('PATCH'.equals(method)){
					apiEndpoint += '?_HttpMethod=PATCH';
					req.setMethod('POST');
				} else {
					req.setMethod(method);
				}

        setAuthHeaderAsync(req);
        req.setEndpoint(herokuUrl + apiEndpoint);
				if (aPayload!=null)
					req.setBody(String.valueOf(aPayload));

				if (Test.isRunningTest() && (mock!=null)) {
					 mock.respond(req);
			 	} else {
					Integer retry = 0;

					while (retry < 2){
						Http http = new Http();
		        res =  http.send(req);
						if (res.getStatusCode() >= 200 && res.getStatusCode() < 300) {
							retry +=2;
						} else if (res.getStatusCode()==503 || res.getStatusCode()==400){
							retry +=1;
							res = http.send(req);
						} else {
							retry +=1;
							theArgs.add('Api');
							theArgs.add(req.getEndpoint());
							theArgs.add(res.getBody());
						}
					}
					throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED', null, theArgs));
				}

      } else {
        System.debug('Service apiEndpoint and payload must not be null');
        throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED'));
      }
    } catch (Exception ex) {
			theArgs.add('Api');
			theArgs.add(req.getEndpoint());
			theArgs.add(res.getBody());
			throw new Rest_Exception(ResponseCodes_Mgr.getCode('HEROKU_REQUEST_CALLOUT_FAILED', ex, theArgs));
			
    }
}

Spring controller checks the RequestParam _HttpMethod to see if it’s a POST or PATCH request

@RequestMapping(value = "/customer", method = RequestMethod.POST, produces = "application/json")
@ResponseBody
@ApiOperation(value = "Creates new payment customer")
@ApiResponses(value = {@ApiResponse(code = 200, message = "Creates new payment customer")})
public String createCustomer(@RequestBody PaymentCustomerWrapper paymentCustomerWrapper, @RequestParam(value="_HttpMethod", defaultValue="POST")  String httpMethod)
        throws IOException, URISyntaxException {
     if (httpMethod!=null && "PATCH".equals(httpMethod)){
         itsLogger.debug("update payment customer {}", paymentCustomerWrapper.toString());
         return paymentService.updatePaymentCustomer(paymentCustomerWrapper).toJson();
    } else {
         itsLogger.debug("create payment customer {}", paymentCustomerWrapper.toString());
         return paymentService.createChargeBeeCustomer(paymentCustomerWrapper).toJson();
    }
}

Apex Return Diff fields between records

Sometimes you want to determine what has changed between an existing record in the database and an update to the record. The diffRecord method will return all the fields that changed for a specific record.

Compare the difference between to records and return changed fields

public SObject diffRecord(SObject updatedRecord, SObject currentRecord, String sObjectName){
  SObjectType objToken = Schema.getGlobalDescribe().get(sObjectName);
  DescribeSObjectResult objDef = objToken.getDescribe();
  Map<String, SObjectField> fields = objDef.fields.getMap();
  Type classType = Type.forName(sObjectName);
  SObject diffRecord = (SObject)JSON.deserialize('{}', classType);
  for (String field : fields.keySet()){
    if (!fieldsToExclude.contains(fields.get(field).getDescribe().getName()) && (fields.get(field).getDescribe().isUpdateable() || fields.get(field).getDescribe().isCreateable())){
        if (updatedRecord.get(field)!=currentRecord.get(field))
        	diffRecord.put(field, updatedRecord.get(field));
    }
  }
  return diffRecord;
}

Test that only changed fields are returned

@isTest static void testDiffRecords(){
  String currentRecordJSON = '{"Id":"a0L63000000qc43EAA","Account_Type__c":"Managed","Amount__c":50.00,"Category__c":"Deposit","Description__c":"Deposit","End_Date__c":"2020-03-31T15:06:57.000+0000","Frequency__c":"Monthly"}';
  String updatedRecordJSON = '{"Amount__c":50.0,"Id":"a0L63000000qc43EAA","Frequency__c":"Annually"}';
  Type classType = Type.forName('Account');
  SObject currentRecord = (SObject)JSON.deserialize(currentRecordJSON, classType);
  SObject updatedRecord = (SObject)JSON.deserialize(updatedRecordJSON, classType);
  Test.startTest();
    SObject diffRecord = App_Service.instance.diffRecord(updatedRecord, currentRecord, 'Account');
    System.assertEquals(diffRecord.get('Amount__c'), null);
    System.assertEquals(diffRecord.get('Frequency__c'), 'Annually');
    System.assertEquals(diffRecord.get('Account_Type__c'), null);
    System.assertEquals(diffRecord.get('Category__c'), null);
  Test.stopTest();
}

Apex Compare two Records and Merge updated fields

When you have two record, one being the existing queried record and the other the updated record. To merge the original with the updated field without committing the changes you can iterate the current record’s fields and check which of the fields changed. Update the changed fields to the new updated fields value. Now we have an merged record. Here is how I went about doing it.

Original SObject record

{"Account_Type__c":"Managed","Amount__c":50.00,"Category__c":"Deposit",
"Description__c":"Deposit","End_Date__c":"2020-03-31T15:06:57.000+0000",
"Frequency__c":"Monthly"}

Update SObject record

{"Amount__c":500.0,"Id":"a0L63000000qc43EAA","Frequency__c":"Annually"}

Merge two records with changes

private static Set<String> fieldsToExclude = new Set<String>{'OwnerId','OtherGeocodeAccuracy','MailingGeocodeAccuracy','BillingGeocodeAccuracy','ShippingGeocodeAccuracy'};
public SObject mergeRecords(SObject updatedRecord, SObject currentRecord, String sObjectName){
  SObjectType objToken = Schema.getGlobalDescribe().get(sObjectName);
  DescribeSObjectResult objDef = objToken.getDescribe();
  Map<String, SObjectField> fields = objDef.fields.getMap();
  Type classType = Type.forName(sObjectName);
  SObject mergedRecord = (SObject)JSON.deserialize('{}', classType);
  for (String field : fields.keySet()){
    if (!fieldsToExclude.contains(fields.get(field).getDescribe().getName()) && (fields.get(field).getDescribe().isUpdateable() || fields.get(field).getDescribe().isCreateable())){
        if (updatedRecord.get(field)!=null){
        	mergedRecord.put(field, updatedRecord.get(field));
    	} else if (currentRecord.get(field)!=null){
        	mergedRecord.put(field, currentRecord.get(field));
    	}
    }
  }
  return mergedRecord;
}

Test that fields were merged correctly

@isTest static void testMergeRecords(){
  String currentRecordJSON = '{"Id":"a0L63000000qc43EAA","Account_Type__c":"Managed","Amount__c":50.00,"Category__c":"Deposit","Description__c":"Deposit","End_Date__c":"2020-03-31T15:06:57.000+0000","Frequency__c":"Monthly"}';
  String updatedRecordJSON = '{"Amount__c":500.0,"Id":"a0L63000000qc43EAA","Frequency__c":"Annually"}';
  Type classType = Type.forName('Account');
  SObject currentRecord = (SObject)JSON.deserialize(currentRecordJSON, classType);
  SObject updatedRecord = (SObject)JSON.deserialize(updatedRecordJSON, classType);
  Test.startTest();
    SObject mergedRecord = mergeRecords(updatedRecord, currentRecord, 'Account');
    System.assertEquals(mergedRecord.get('Amount__c'), 500.0);
    System.assertEquals(mergedRecord.get('Frequency__c'), 'Annually');
    System.assertEquals(mergedRecord.get('Account_Type__c'), 'Managed');
    System.assertEquals(mergedRecord.get('Category__c'), 'Deposit');
  Test.stopTest();
}