【Apex】承認プロセスの承認・却下・取消

本記事は、承認プロセスの操作をApexで行う際に実行可能なアクションの理解に錯誤があったため、個人的に記録としておいておきます。

# 承認プロセスのアクションと実行権限
承認プロセスには「 __承認__ ・ __却下__ ・ __取消__ 」という3種類のアクションがあります。
標準の承認プロセスの機能の範囲では、それぞれのアクションに対して下記のようなユーザが実行権限を持ちます。

- __承認__ ：承認ステップの __承認者__ として設定されたユーザ
- __却下__ ：承認ステップの __承認者__ として設定されたユーザ
- __取消__ ：承認プロセスの __申請者__ として設定されたユーザ（取消を許可している場合に限る）

また、上記のユーザに加えて、システム管理者権限の「すべてのデータの編集」または対象オブジェクトの「すべて変更」の権限を持つユーザが例外的にアクションを実行することができます。

# Apexによる承認プロセスのアクションの実行
Apexで承認プロセスのアクションを実行する場合は、ApprovalクラスとProcessWorkitemRequestクラスを使用します。

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_approval.htm

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_ProcessWorkitemRequest.htm

本記事では、Lightning Flowから呼び出し可能なメソッドを下記のように構成して検証を実施しました。

```apex:ApprovalControl.cls
public virtual class ApprovalControl {
    public virtual class Request {
        @InvocableVariable
        public Id workitemId;
        @InvocableVariable
        public String action;
        @InvocableVariable
        public String comments;
    }
    public virtual class Result {
        @InvocableVariable
        public Boolean isSuccess;
    }
    public static List<Result> process( List<Request> requests ){
        return createResults( Approval.process( createRequests( requests ) ) );
    }
    private static List<Approval.ProcessWorkitemRequest> createRequests( List<Request> requests ){
        List<Approval.ProcessWorkitemRequest> processRequests = new List<Approval.ProcessWorkitemRequest>();
        for( Request request : requests ){
            Approval.ProcessWorkitemRequest processRequest = new Approval.ProcessWorkitemRequest();
            processRequest.setWorkitemId(request.workitemId);
            processRequest.setAction(request.action);
            processRequest.setComments(request.comments);
            processRequests.add( processRequest );
        }
        return processRequests;
    }
    private static List<Result> createResults( List<Approval.ProcessResult> processResults ){
        List<Result> results = new List<Result>();
        for( Approval.ProcessResult processResult : processResults ){
            Result result = new Result();
            result.isSuccess = processResult.isSuccess();
        }
        return results;
    }
}
```

```apex:ApprovalWithSharingControl.cls
public with sharing class ApprovalWithSharingControl extends ApprovalControl {
    @InvocableMethod(Label='Approval With Sharing Control')
    public static List<Result> control( List<Request> requests ){
        return process( requests );
    }
}
```

```apex:ApprovalWithoutSharingControl.cls
public without sharing class ApprovalWithoutSharingControl extends ApprovalControl {
    @InvocableMethod(Label='Approval Without Sharing Control')
    public static List<Result> control( List<Request> requests ){
        return process( requests );
    }
}
```

:::note info
承認プロセスの操作を行う際にProcessInstanceWorkitemオブジェクトのレコードIDが必要になります。
ProcessInstanceWorkitemオブジェクトのレコードの取得に関しては、承認プロセスを実行している対象レコードを取得できるかに依存しており、「対象レコードを取得できない場合はProcessInstanceWorkitemオブジェクトのレコードを取得できない」という形で承認プロセスのアクションの実行を制限されることになります。
ただし、本記事は承認プロセスのアクションの実行を検証することを目的とするため、ProcessInstanceWorkitemオブジェクトのレコードIDは下記のApexを利用して取得し、ProcessInstanceWorkitemオブジェクトに対する取得可能性を加味しません。

```apex:ProcessInstanceWorkitemControl.cls
public virtual class ProcessInstanceWorkitemControl {
    public virtual class Request {
        @InvocableVariable
        public Id targetId;
        @InvocableVariable
        public String action;
        @InvocableVariable
        public String status;
    }
    public virtual class Result {
        @InvocableVariable
        public Id workitemId;
    }
    public static List<Result> run( List<Request> requests ){
        List<Result> getResults = getControl( requests );
        List<Result> results = new List<Result>();
        for( Integer i = 0 ; i < requests.size() ; i++ ){
            switch on requests.get( i ).action {
                when 'Get' {
                    results.add( getResults.get( i ) );
                }
                when else {
                    results.add( new Result() );
                }
            }
        }
        return results;
    }
    private static List<Result> getControl( List<Request> requests ){
        Set<Id> targetIds = new Set<Id>();
        Set<String> status = new Set<String>();
        for( Request request : requests ){
            if( request.action != 'Get' ){
                continue;
            }
            targetIds.add( request.targetId );
            status.add( request.status );
        }
        List<ProcessInstanceWorkitem> processWorkitems = [SELECT Id , ProcessInstance.TargetObjectId , ProcessInstance.Status FROM ProcessInstanceWorkitem WHERE ProcessInstance.TargetObjectId IN :targetIds AND ProcessInstance.Status IN :status ORDER BY CreatedDate ASC];
        List<Result> results = new List<Result>();
        for( Request request : requests ){
            Result result = new Result();
            if( request.action != 'Get' ){
                results.add( result );
                continue;
            }
            for( ProcessInstanceWorkitem processWorkitem : processWorkitems ){
                if( processWorkitem.ProcessInstance.TargetObjectId == request.targetId && processWorkitem.ProcessInstance.Status == request.status ){
                    result.workitemId = processWorkitem.Id;
                }
            }
            results.add( result );
        }
        return results;
    }
}
```

```apex:PIWWithoutSharingControl.cls
public without sharing class PIWWithoutSharingControl extends ProcessInstanceWorkitemControl {
    @InvocableMethod(Label='Process Instance Workitem Without Sharing Control')
    public static List<Result> control( List<Request> requests ){
        return run( requests );
    }
}
```

:::

# 検証するアクターと設定
検証は下記のアクターを想定して実施します。

- 承認者に設定されているユーザ
- 申請者に設定されているユーザ
- 承認者の上位ロールに設定されているユーザ
- 申請者の上位ロールに設定されているユーザ
- 対象レコードの所有者に設定されているユーザ
- 対象レコードの所有者の上位ロールに設定されているユーザ
- 「すべてのデータの編集」を持つユーザ
- 「すべて変更」を持つユーザ
- 上記以外でレコードが共有されているユーザ
- 上記以外でレコードが共有されていないユーザ

設定に関しては下記のケースを考慮します。

- 承認プロセスが申請者による取消を許可するか
- レコードロックがかかっているか
- アクションがレコード共有を加味するか

# 検証結果
検証結果として、下記のような対応になることがわかりました。

### パターン - 1

- 承認プロセスが申請者による取消を許可する
- レコードロックがかかっている
- アクションがレコード共有を加味する

|アクター|承認|却下|取消|
|:-:|:-:|:-:|:-:|
|承認者に設定されているユーザ|可|可|不可|
|申請者に設定されているユーザ|不可|不可|可|
|承認者の上位ロールに設定されているユーザ|不可|不可|不可|
|申請者の上位ロールに設定されているユーザ|不可|不可|不可|
|申請者の上位ロールに設定されているユーザ|不可|不可|不可|
|対象レコードの所有者に設定されているユーザ|不可|不可|不可|
|対象レコードの所有者の上位ロールに設定されているユーザ|不可|不可|不可|
|「すべてのデータの編集」を持つユーザ|可|可|可|
|「すべて変更」を持つユーザ|可|可|可|
|上記以外でレコードが共有されているユーザ|不可|不可|不可|
|上記以外でレコードが共有されていないユーザ|不可|不可|不可|

### パターン - 2

- 承認プロセスが申請者による取消を許可する
- レコードロックがかかっている
- アクションがレコード共有を加味しない

|アクター|承認|却下|取消|
|:-:|:-:|:-:|:-:|
|承認者に設定されているユーザ|可|可|不可|
|申請者に設定されているユーザ|__可__|__可__|可|
|承認者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|申請者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|申請者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|対象レコードの所有者に設定されているユーザ|__可__|__可__|不可|
|対象レコードの所有者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|「すべてのデータの編集」を持つユーザ|可|可|可|
|「すべて変更」を持つユーザ|可|可|可|
|上記以外でレコードが共有されているユーザ|__可__|__可__|不可|
|上記以外でレコードが共有されていないユーザ|__可__|__可__|不可|

### パターン - 3

- 承認プロセスが申請者による取消を許可する
- レコードロックがかかっていない
- アクションがレコード共有を加味する

|アクター|承認|却下|取消|
|:-:|:-:|:-:|:-:|
|承認者に設定されているユーザ|可|可|不可|
|申請者に設定されているユーザ|不可|不可|可|
|承認者の上位ロールに設定されているユーザ|不可|不可|不可|
|申請者の上位ロールに設定されているユーザ|不可|不可|不可|
|申請者の上位ロールに設定されているユーザ|不可|不可|不可|
|対象レコードの所有者に設定されているユーザ|不可|不可|不可|
|対象レコードの所有者の上位ロールに設定されているユーザ|不可|不可|不可|
|「すべてのデータの編集」を持つユーザ|可|可|可|
|「すべて変更」を持つユーザ|可|可|可|
|上記以外でレコードが共有されているユーザ|不可|不可|不可|
|上記以外でレコードが共有されていないユーザ|不可|不可|不可|

### パターン - 4

- 承認プロセスが申請者による取消を許可する
- レコードロックがかかっていない
- アクションがレコード共有を加味しない

|アクター|承認|却下|取消|
|:-:|:-:|:-:|:-:|
|承認者に設定されているユーザ|可|可|不可|
|申請者に設定されているユーザ|__可__|__可__|可|
|承認者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|申請者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|申請者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|対象レコードの所有者に設定されているユーザ|__可__|__可__|不可|
|対象レコードの所有者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|「すべてのデータの編集」を持つユーザ|可|可|可|
|「すべて変更」を持つユーザ|可|可|可|
|上記以外でレコードが共有されているユーザ|__可__|__可__|不可|
|上記以外でレコードが共有されていないユーザ|__可__|__可__|不可|

### パターン - 5

- 承認プロセスが申請者による取消を許可しない
- レコードロックがかかっている
- アクションがレコード共有を加味する

|アクター|承認|却下|取消|
|:-:|:-:|:-:|:-:|
|承認者に設定されているユーザ|可|可|不可|
|申請者に設定されているユーザ|不可|不可|__不可__|
|承認者の上位ロールに設定されているユーザ|不可|不可|不可|
|申請者の上位ロールに設定されているユーザ|不可|不可|不可|
|申請者の上位ロールに設定されているユーザ|不可|不可|不可|
|対象レコードの所有者に設定されているユーザ|不可|不可|不可|
|対象レコードの所有者の上位ロールに設定されているユーザ|不可|不可|不可|
|「すべてのデータの編集」を持つユーザ|可|可|可|
|「すべて変更」を持つユーザ|可|可|可|
|上記以外でレコードが共有されているユーザ|不可|不可|不可|
|上記以外でレコードが共有されていないユーザ|不可|不可|不可|

### パターン - 6

- 承認プロセスが申請者による取消を許可しない
- レコードロックがかかっている
- アクションがレコード共有を加味しない

|アクター|承認|却下|取消|
|:-:|:-:|:-:|:-:|
|承認者に設定されているユーザ|可|可|不可|
|申請者に設定されているユーザ|__可__|__可__|__不可__|
|承認者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|申請者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|申請者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|対象レコードの所有者に設定されているユーザ|__可__|__可__|不可|
|対象レコードの所有者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|「すべてのデータの編集」を持つユーザ|可|可|可|
|「すべて変更」を持つユーザ|可|可|可|
|上記以外でレコードが共有されているユーザ|__可__|__可__|不可|
|上記以外でレコードが共有されていないユーザ|__可__|__可__|不可|

### パターン - 7

- 承認プロセスが申請者による取消を許可しない
- レコードロックがかかっていない
- アクションがレコード共有を加味する

|アクター|承認|却下|取消|
|:-:|:-:|:-:|:-:|
|承認者に設定されているユーザ|可|可|不可|
|申請者に設定されているユーザ|不可|不可|__不可__|
|承認者の上位ロールに設定されているユーザ|不可|不可|不可|
|申請者の上位ロールに設定されているユーザ|不可|不可|不可|
|申請者の上位ロールに設定されているユーザ|不可|不可|不可|
|対象レコードの所有者に設定されているユーザ|不可|不可|不可|
|対象レコードの所有者の上位ロールに設定されているユーザ|不可|不可|不可|
|「すべてのデータの編集」を持つユーザ|可|可|可|
|「すべて変更」を持つユーザ|可|可|可|
|上記以外でレコードが共有されているユーザ|不可|不可|不可|
|上記以外でレコードが共有されていないユーザ|不可|不可|不可|

### パターン - 8

- 承認プロセスが申請者による取消を許可しない
- レコードロックがかかっていない
- アクションがレコード共有を加味しない

|アクター|承認|却下|取消|
|:-:|:-:|:-:|:-:|
|承認者に設定されているユーザ|可|可|不可|
|申請者に設定されているユーザ|__可__|__可__|__不可__|
|承認者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|申請者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|申請者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|対象レコードの所有者に設定されているユーザ|__可__|__可__|不可|
|対象レコードの所有者の上位ロールに設定されているユーザ|__可__|__可__|不可|
|「すべてのデータの編集」を持つユーザ|可|可|可|
|「すべて変更」を持つユーザ|可|可|可|
|上記以外でレコードが共有されているユーザ|__可__|__可__|不可|
|上記以外でレコードが共有されていないユーザ|__可__|__可__|不可|

# 検証結果のまとめ
検証結果から下記のことが言えそうです。

- 承認と却下は対象のProcessInstanceWorkitemレコードの権限に依存する
- 取消は対象のProcessInstanceレコードの権限およびProcessDefinisionの設定に依存する
- レコードロックの有無は承認・却下・取消の権限に影響を与えない

# まとめ
Apexで承認プロセスの操作を行う際には、第三者が承認・却下ができてしまう場合があるため注意が必要と感じます。
また、取消に関してはApexで操作をしたとしても厳密に制御されているため、承認・却下と同じように考えられない点は落とし穴になりやすいかと思います。