本記事は、承認プロセスの操作をApexで行う際に実行可能なアクションの理解に錯誤があったため、個人的に記録としておいておきます。
承認プロセスのアクションと実行権限
承認プロセスには「 承認 ・ 却下 ・ 取消 」という3種類のアクションがあります。
標準の承認プロセスの機能の範囲では、それぞれのアクションに対して下記のようなユーザが実行権限を持ちます。
- 承認 :承認ステップの 承認者 として設定されたユーザ
- 却下 :承認ステップの 承認者 として設定されたユーザ
- 取消 :承認プロセスの 申請者 として設定されたユーザ(取消を許可している場合に限る)
また、上記のユーザに加えて、システム管理者権限の「すべてのデータの編集」または対象オブジェクトの「すべて変更」の権限を持つユーザが例外的にアクションを実行することができます。
Apexによる承認プロセスのアクションの実行
Apexで承認プロセスのアクションを実行する場合は、ApprovalクラスとProcessWorkitemRequestクラスを使用します。
本記事では、Lightning Flowから呼び出し可能なメソッドを下記のように構成して検証を実施しました。
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;
}
}
public with sharing class ApprovalWithSharingControl extends ApprovalControl {
@InvocableMethod(Label='Approval With Sharing Control')
public static List<Result> control( List<Request> requests ){
return process( requests );
}
}
public without sharing class ApprovalWithoutSharingControl extends ApprovalControl {
@InvocableMethod(Label='Approval Without Sharing Control')
public static List<Result> control( List<Request> requests ){
return process( requests );
}
}
承認プロセスの操作を行う際にProcessInstanceWorkitemオブジェクトのレコードIDが必要になります。
ProcessInstanceWorkitemオブジェクトのレコードの取得に関しては、承認プロセスを実行している対象レコードを取得できるかに依存しており、「対象レコードを取得できない場合はProcessInstanceWorkitemオブジェクトのレコードを取得できない」という形で承認プロセスのアクションの実行を制限されることになります。
ただし、本記事は承認プロセスのアクションの実行を検証することを目的とするため、ProcessInstanceWorkitemオブジェクトのレコードIDは下記のApexを利用して取得し、ProcessInstanceWorkitemオブジェクトに対する取得可能性を加味しません。
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;
}
}
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で操作をしたとしても厳密に制御されているため、承認・却下と同じように考えられない点は落とし穴になりやすいかと思います。