The reason for the error is that by design you are only able to recall if you are the submitter or a system administrator; but the following workaround will allow recalling without compromising security:
- 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
You can read more about this on my blog here: http://danielsokolowski.blogspot.ca/2017/11/force-recall-salesforce-approval-process.html
Code Excerpt:
global virtual with sharing class ID_Item extends cms.ContentTemplateController implements Messaging.InboundEmailHandler {
...
@RemoteAction
global static Map<String, Object> mApprovalRecallAction(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<String, Object>{'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<String, Object>{'bSuccess' => true, 'e' => JSON.serialize('ERROR: no email service setup, `e' + String.valueOf(e) + '`')};
}
Map<String, Object> oParameterMap = new Map<String, Object>{'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<Messaging.SingleEmailMessage> {oMessage});
System.debug('=== ID_Item.mApprovalRecallAction(...): `oResults' + String.valueOf(oResults) + '`');
System.debug('<<< ID_Item.mApprovalRecallAction(...)');
if (oResults[0].success) {
return new Map<String, Object>{'bSuccess' => true, 'oResults' => JSON.serialize(String.valueOf(oResults))};
} else {
return new Map<String, Object>{'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<String,Object> oParameterMap = new Map<String, Object>{'sAction' => 'UNKNOWN'};
try {
oParameterMap = (Map<String, Object>)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<ProcessInstanceWorkItem> 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;
}
...
}