0

I have a business Requirement, where i have to provide an option for some users to recall the approval process.

To my understanding I have created a button on Object. On click of this button I'm calling an apex function to recall the approval Process.

This works fine for Profile:System Administrator but fails for other profile with a message: INSUFFICIENT_ACCESS_ON_CROSS_REFERENCE_ENTITY

Button Code:

{!REQUIRESCRIPT("/soap/ajax/29.0/connection.js")}
{!REQUIRESCRIPT("/soap/ajax/29.0/apex.js")}

function recallApproval(){
    alert('hello');
    alert('{!Custom_Object__c.Id}');
    var x = '{!Custom_Object__c.Id}';
    sforce.apex.execute("HD_RecallApproval", "recallApproval", {paramId: x});

}

setTimeout(recallApproval(), 2000);
location.reload();

Apex Code:

global without sharing class HD_RecallApproval {

    webservice static void recallApproval(Id paramId){

        Approval.ProcessWorkitemRequest pwr = new     Approval.ProcessWorkitemRequest();
        Id objectId = paramId;
        List<ProcessInstance> procins = new List<ProcessInstance>([select Id from ProcessInstance where Status = 'Pending' and TargetObjectId = :objectId]);
        List<ProcessInstanceWorkitem>  workitem = new List<ProcessInstanceWorkitem>([select Id from ProcessInstanceWorkitem where ProcessInstanceId = :procins[0].id]);
        if ((workitem != null) && (workitem.size() > 0)){
                pwr.SetComments('removed by staff user');
                pwr.setWorkItemId(workitem[0].id);
                pwr.setAction('Removed'); 
                Approval.ProcessResult pr = Approval.process(pwr);
                system.debug(pr);
        }
    }
}

Edit:

One thing I observed now that if I replace 'Removed' with 'Reject'. The pending approval process is getting rejected for all profiles.

  • does those non-profile user have object access ? – Ratan Paul May 23 '16 at 13:04
  • Yes they have. They can edit the record. – pranav prashant May 23 '16 at 13:05
0

Do the other profiles have access to the HD_RecallApproval Class? Check in the 'Enabled Apex Class Access' section of the Profile settings for one of the profiles that cannot run the code.

  • I've enabled the class for all the profiles under my Org. Tried with different users too – pranav prashant May 23 '16 at 15:44
  • Did you try the button while logged in as the designated approver for the approval process? I'm wondering if the only user who could successfully run the code is the one who has approval authority (or the designated approver for that user). – DLee728 May 24 '16 at 13:10
0

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;
}

...

}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.