Non-submitter and non-administrator approval recall
Salesforce lacks the functionality to recall an in-flight approval process for anyone other than the original submitter or a system administrator. For our latest client this posed a problem as their specific business needs required the ability to recall an approval one step back in a multi person process, and giving the approvers administrative permissions posed an unacceptable security risk.
Thus until Salesforce implements this idea (please upvote) to extend recalling to record owners, below approach workaround will accomplish approval recall without compromising security - albeit in an interesting way.
- setup an email service that runs in elevated privileges and recalls force recall an approval
- provide normal users the ability to execute an Apex method that internally emails that service
Reference setup
Below are the relevant bits extracted from a working solution, leverage and adapt to your own unique solution.
Apex Class
global virtual with sharing class ID_Item extends cms.ContentTemplateController implements Messaging.InboundEmailHandler { ... @RemoteAction global static MapmApprovalRecallAction(String sContentID, String sName) { System.debug('>>> ID_Item.mApprovalRecallAction(...)'); ... // submitter is the person recalling, so we have permission to do so (otherwise we need to be an admin: https://success.salesforce.com/ideaView?id=08730000000iY1aAAE) if (oApproval.CreatedById == UserInfo.getUserId()) { Approval.ProcessWorkitemRequest oPWR = new Approval.ProcessWorkitemRequest(); oPWR.setComments('Recalled by `' + oApproval.CreatedBy.Name + '`'); oPWR.setAction('Removed'); if (!Test.isRunningTest()) { // can't seem to create ProcessInstanceWorkItem records in tests oPWR.setWorkItemId(oWorkItemList[0].id); Approval.ProcessResult oPR = Approval.process(oPWR); } // Next the approval process needs to have a recalled action that sets `cms__Approval__c.Status = 'Recalled'` which is levraged by OrchestraCMS - but just incase these were deleted we are changing the status in apex as well oApproval.cms__Status__c = 'Recalled'; oApproval.Content_Status__c = 'Recalled'; update oApproval; System.debug('=== ID_Item.mApprovalRecallAction(...): `JSON.serialize(oPWR)=' + JSON.serialize(oPWR) + '`'); return new Map {'bSuccess' => true, 'sPWR' => JSON.serialize(oPWR)}; } // we assume `this.bApprovalRecalVisible` did our security checks else { // we leverage the functionality that an email service allows us to run under a adming user Messaging.SingleEmailMessage oMessage = new Messaging.SingleEmailMessage(); try { // find our email service to use, as convention we prefixed it with class name EmailServicesAddress oEmailServiceAddress = [ SELECT e.SystemModstamp, e.RunAsUserId, e.LocalPart, e.LastModifiedDate, e.LastModifiedById, e.IsActive, e.Id, e.FunctionId, e.EmailDomainName, e.CreatedDate, e.CreatedById, e.AuthorizedSenders FROM EmailServicesAddress e WHERE e.IsActive = true AND e.LocalPart LIKE 'ID_Item%' // `LIKE` is case-insensetive, so we keep capitialization same as class name as it's easier for search and replace ].get(0); oMessage.toAddresses = new String[] { oEmailServiceAddress.LocalPart + '@' + oEmailServiceAddress.EmailDomainName }; } catch (Exception e) { System.debug('=== ID_Item.mApprovalRecallAction(...): ERROR: no email service setup, `e' + String.valueOf(e) + '`'); return new Map {'bSuccess' => true, 'e' => JSON.serialize('ERROR: no email service setup, `e' + String.valueOf(e) + '`')}; } Map oParameterMap = new Map {'sAction' => 'mApprovalRecallActionWithAdminPermissions', 'sApprovalID' => oApproval.Id}; oMessage.subject = '=== ID_Item.mApprovalRecallAction(...): `JSON.serialize(oParameterMap)=' + JSON.serialize(oParameterMap) + '`'; // using subject is not safe because SF can modify it sometimes (ex. prefixing `Sandbox` oMessage.plainTextBody = JSON.serialize(oParameterMap); Messaging.SendEmailResult[] oResults = Messaging.sendEmail(new List {oMessage}); System.debug('=== ID_Item.mApprovalRecallAction(...): `oResults' + String.valueOf(oResults) + '`'); System.debug('<<< ID_Item.mApprovalRecallAction(...)'); if (oResults[0].success) { return new Map {'bSuccess' => true, 'oResults' => JSON.serialize(String.valueOf(oResults))}; } else { return new Map {'bSuccess' => false, 'oResults' => JSON.serialize(String.valueOf(oResults))}; } } } global Messaging.InboundEmailResult handleInboundEmail(Messaging.inboundEmail oInboundEmail, Messaging.InboundEnvelope oInboundEnvelope){ System.debug('>>> ID_Item.handleInboundEmail(...)'); Messaging.InboundEmailResult oInboundEmailResult = new Messaging.InboundEmailResult(); Map oParameterMap = new Map {'sAction' => 'UNKNOWN'}; try { oParameterMap = (Map )JSON.deserializeUntyped(oInboundEmail.plainTextBody); // expected string input format is "{'sAction': 'fooBar', 'sApprovalID': 'foo', ...}" } catch (QueryException e) { System.debug('=== ID_Item.handleInboundEmail(...): ERROR: `e=' + e + '`'); System.debug('<<< ID_Item.handleInboundEmail(...)'); return oInboundEmailResult; } try { if (oParameterMap.get('sAction') == 'mApprovalRecallActionWithAdminPermissions') { String sApprovalID = (String)oParameterMap.get('sApprovalID'); cms__Approval__c oApproval = [Select ID, c.cms__Content__c FROM cms__Approval__c c WHERE c.Id = :sApprovalID].get(0); List oWorkItemList = [Select ProcessInstance.TargetObjectId, ActorId From ProcessInstanceWorkitem where ProcessInstance.TargetObjectId = :oApproval.ID]; //System.debug(oWorkItemList[0]); Approval.ProcessWorkitemRequest oPWR = new Approval.ProcessWorkitemRequest(); oPWR.setComments('Recalled by `' + oInboundEnvelope.fromAddress + '`'); oPWR.setAction('Reject'); if (Test.isRunningTest() == false) { oPWR.setWorkItemId(oWorkItemList[0].id); Approval.ProcessResult oPR = Approval.process(oPWR); } } } catch (QueryException e) { System.debug('=== ID_Item.handleInboundEmail(...): ERROR: `e=' + e + '`'); } oInboundEmailResult.success = true; System.debug('<<< ID_Item.handleInboundEmail(...)'); return oInboundEmailResult; } ... }
Email Service Setup
Ensure that the recall code is run under a System Administrator user.
If this has helped you please comment and follow me on twitter @danielsokolows.
Comments
Post a Comment