Apex でユーザモードでデータベースを操作する

Spring '23 (APIバージョン 57.0) から、ユーザモードでのデータベース操作が GA になりました。Apex はデフォルトではシステムモードで起動するため、明示的な権限チェックを実装しないとオブジェクト権限や項目レベルセキュリティを超えた処理を行えてしまう可能性がありました。ユーザモードでのデータベース操作を用いると、シンプルな記法で、オブジェクトや項目の権限、他の標準のデータセキュリティ機能に準拠することができます。この記事では、ユーザモードでのデータベース操作の使い方や注意事項、既存機能との違いを取り上げます。

## はじめに
次のテストメソッドを考えます。runAs するユーザには取引先オブジェクトの作成権限がないものとします。このときテストメソッドの実行結果はどうなるでしょうか。

```ExampleDmlTest.cls
@isTest
public static void testAccountInsert() {
  // 中略 (実行ユーザを取得または作成)
  System.runAs(u) { // u には取引先オブジェクトの作成権限がないと仮定
    insert new Account(Name = 'Test Company');
  }
  List<Account> result = [SELECT Id FROM Account WHERE Name = 'Test Company'];
  Assert.areEqual(1, result.size(), 'Expected to find one account');
}
```

上記のテストメソッドは特にエラーが発生することなく正常終了します。これは標準機能と異なり Apex クラスがデフォルトでシステムモード(オブジェクト権限・項目レベルセキュリティを適用しない)で実行されるためです。

クエリの例も挙げてみます。以下のようなメソッドがあるとし、実行ユーザにはレコードの参照権限はあるがカスタム項目 `SLA__c` の参照権限がないとします。この場合の戻り値はどうなるでしょうか。

```ExampleAccountSelector.cls
public with sharing ExampleAccountSelector {
  public static List<Account> getAccounts() {
    return [SELECT Id, Name, SLA__c FROM Account];
  }
}
```

カスタム項目が削られたりエラーになることはなく、すべての項目が返されます。クラス定義のキーワード(`with sharing`, `without sharing`, `inherited sharing`)はレコードレベルセキュリティを適用しますが、最初の DML の例と同様に Apex ではデフォルトでオブジェクト権限や項目レベルセキュリティは適用されないため、クラス定義のキーワードにかかわらず、標準機能では参照できない項目が参照できてしまう可能性があります。

### ユーザモードで動く機能
以下のような機能は実行時に実行ユーザのオブジェクト権限・項目レベルセキュリティ・レコードセキュリティが適用されます。

* 標準画面
* 標準のAPI
* 標準のVFコントローラ
* 匿名実行 Apex

### システムモードで動く機能
以下は、デフォルトでは実行時に実行ユーザのオブジェクト権限・項目レベルセキュリティが適用されません。
* Apex クラス
* Apex トリガ
* Apex Web サービス

## オブジェクト権限・項目レベルセキュリティを適用するこれまでの方法
Apex と標準機能にはデータセキュリティの適用について差異があることがわかりましたが、これまで Apex でオブジェクト権限・項目レベルセキュリティを適用するには以下のような方法がありました。

### 静的メソッドを使う
`Schema.DescribeSObjectResult` や `Schema.DescribeSObjectResult` の `isAccessible`, `isCreateable`, `isUpdateable`, `isDeletable` といったメソッドを呼び出すことで権限をチェックすることができます。数が少なければ良いですが、なかなか冗長になってしまいます。

```apex
if (
Schema.sObjectType.Account.isUpdateable() &&
Schema.sObjectType.Account.fields.SLA__c.isUpdateable() &&
Schema.sObjectType.Account.fields.OwnerId.isUpdateable() // && 他の項目権限判定...
) {
  update accounts;
}
```

### stripInaccessible メソッドを使う
`stripInaccessbile` メソッドは項目レベルセキュリティを満たす項目だけを含むレコード変数を返します。少し使い方に癖がありますが静的メソッドで判定するよりはシンプルになります。詳しい書き方やサンプルは @atskimura さんの記事[「逆引きSpring'20の新しいFLSチェックの使い方」](https://qiita.com/atskimura/items/d762b08ed07f9982df54)が分かりやすくてお勧めです。

```apex
// 項目レベルセキュリティを満たさない項目をそぎ落としたレコード変数を取得できる
update Security.stripInaccessible(AccessType.UPDATABLE, accounts).getRecords();
```

## ユーザモードでのデータベース操作

ユーザモードのデータベース操作を用いるとより簡単な方法で Apex にユーザ権限を適用することができます。

### DML
DML キーワードとレコード変数の間に `as user` を加えるだけです。

```apex
insert as user new Account(Name = 'Test Company', Rank__c = 'A');
```

上記の例だと、取引先の作成権限がない場合や、項目に権限がない場合、`DMLException` がスローされます。項目レベルセキュリティに違反した項目を調べるには Exception の `getFieldNames()` メソッドが利用できます。

Database クラスの場合は第 2 引数に実行モードを渡すことによってユーザモードで DML を実行できます。

```apex
Database.insert(account, AccessLevel.USER_MODE);
```

### クエリ
クエリの場合は `WITH USER_MODE` を加えるだけで、オブジェクト権限・項目レベルセキュリティ・レコード権限を適用できます。

```apex
List<Account> accounts = [
  SELECT Id, Name, SLA__c
  FROM Account WHERE Id IN :accountIds
  WITH USER_MODE
];
```

オブジェクトへの参照権限がない場合やクエリする項目の項目レベルセキュリティがない場合、`QueryException` がスローされます。また、`WITH USER_MODE` 句はレコードレベルのセキュリティも適用することに注意が必要です。**without sharing クラス内で使用した場合も WITH USER_MODE が優先される**ため、実行ユーザが参照できるレコードしか取得できません。

動的 SOQL の場合(Database.query メソッド)は、DML と同様に第 2 引数に`AccessLevel.USER_MODE` を渡すことによってユーザモードでクエリを実行できます。

### 既存機能と ユーザモードの Apex の違いを考える
既存の `stripInaccessible` や `WITH SECURITY_ENFORCED` は、制限ルールのような新しいデータセキュリティ機能をサポートしてませんが、`as user` や `WITH USER_MODE` のようなユーザモードの Apex はそれらをサポートしています。また、`WITH SECURITY_ENFORCED` は Task の WhatId のような多態的な項目をサポートしていないなど細かな欠点もありました。

既存機能はオブジェクト権限や項目レベルセキュリティのシンプルなチェックを主眼においていたため、新しいデータセキュリティ機能やエッジケースへの対応が難しくなっていました。一方、**ユーザモードの Apex は、データセキュリティ機能全般を標準機能に合わせるための新しい仕組み**であると捉えることができます。また、ユーザモードの Apex は、以下のように、権限セットで一時的にエスカレーションできるような便利な機能追加も予定されています。

```apex
AccessLevel appEscalated = AccessLevel.USER_MODE.withPermissionSetID('0PSXXX...');
Database.insert(new Account(Name = 'Test Company'), appEscalated);
```

## コードフォーマッタや静的解析の適用
### Prettier
Prettier でユーザモードの Apex を扱うには、v1.13.0 以降の [prettier-plugin-apex](https://github.com/dangmai/prettier-plugin-apex) をインストールしてください。

### Apex PMD
Apex PMD でユーザモードの Apex を扱うには、v6.53 以降の PMD が必要です。コミュニティで開発されている [VS Code の Apex PMD 拡張機能](https://marketplace.visualstudio.com/items?itemName=chuckjonas.apex-pmd)が最近ようやくアップデートされたため、これを使用しても良いですが、公式から [Salesforce Code Analyzer (sfdx-scanner) の拡張機能](https://marketplace.visualstudio.com/items?itemName=salesforce.sfdx-code-analyzer-vscode)の提供が始まりました。記事公開時点でベータ版ですが、メンテナンス頻度を考えるとこちらに移行しても良さそうです。

コマンドパレットまたはファイルを右クリックして `SFDX: Scan
 current file...` を実行すると、問題タブにスキャン結果が表示されます。

![Scanner command palette.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184624/83b00cd6-6b0a-7e8e-ab81-8f65640c9f0d.png)

![スクリーンショット 2023-09-23 11.55.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184624/fe43f3da-6227-41a8-a385-e007c7a37227.png)

オブジェクト権限チェック([ApexCRUDViolation](https://pmd.github.io/pmd/pmd_rules_apex_security.html#apexcrudviolation))違反の警告が表示されていることがわかります。従来の方法で権限チェックを行なっても良いですが、以下のように、ユーザモードの Apex でも警告が解消できていることが確認できます。

![スクリーンショット 2023-09-23 12.00.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184624/1a9e4872-d437-fee7-4b81-f190a3c5a0e3.png)


## 参考リンク
ユーザモードの Apex はシンプルで将来性の高い機能です。積極的に活用していきましょう:muscle:
* [Apex Developer Guide - Enforce User Mode for Database Operations](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_classes_enforce_usermode.htm)