ApexにおけるSalesforce項目レベルセキュリティ(FLS)開発者ガイド

背景と適用シナリオ

Salesforce 開発者の皆さん、こんにちは!本日は Salesforce のセキュリティモデルの根幹をなす、Field-Level Security (FLS)、日本語では項目レベルセキュリティについて、開発者の視点から深く掘り下げていきたいと思います。私は Salesforce 開発者として、日々セキュアなアプリケーションを構築する中で FLS の重要性を痛感しています。

FLS とは、その名の通り、特定の項目(フィールド)に対して、どのユーザーが参照・編集できるかを制御する機能です。これは Salesforce の宣言的な(Declarative)設定であり、通常はシステム管理者が Profile (プロファイル)Permission Set (権限セット) を通じて設定します。

具体的なシナリオを考えてみましょう。例えば、商談 (Opportunity) オブジェクトに「予想利益率」というカスタム項目があるとします。この項目は、営業マネージャーには参照・編集してほしいですが、一般の営業担当者には見せたくない、あるいは参照のみに制限したい、といった要件は非常に一般的です。このような場合に FLS を使用することで、同じ商談レコードを見ていても、ユーザーの役割に応じて表示される情報や操作可能な項目を細かく制御できます。これにより、データの機密性を保ち、適切な情報アクセスを実現します。

しかし、我々開発者が注意しなければならないのは、この FLS の挙動が Apex や API のコンテキストでは、UI とは異なる振る舞いをすることです。この点を理解せずにコードを記述すると、意図せずセキュリティホールを生み出してしまう可能性があります。本記事では、Apex 開発において FLS を正しく尊重し、堅牢なアプリケーションを構築するための方法論とベストプラクティスを解説します。


原理説明

FLS の原理を理解する上で最も重要な概念は、System Context (システムコンテキスト) です。デフォルトでは、Apex クラスは `without sharing` キーワードの有無に関わらず、システムコンテキストで実行されます。これは、コードが実行される際に、オブジェクトレベルの権限、項目レベルのセキュリティ (FLS)、共有ルールといった、実行ユーザーの権限設定の多くが無視されることを意味します。

なぜこのような仕様になっているのでしょうか?それは、システム全体のロジックを円滑に実行するためです。例えば、ユーザーが直接アクセスできない項目であっても、トリガー内の処理でその値を更新する必要があるケースなどが考えられます。もし Apex が常にユーザー権限に従うと、多くの自動化処理が権限エラーで失敗してしまうでしょう。

しかし、この強力な「システム権限」は、開発者にとって諸刃の剣です。特に、Visualforce ページや Lightning コンポーネントから呼び出される Apex メソッドが、ユーザーに表示すべきでないデータを返してしまったり、編集権限のない項目を更新させてしまったりするリスクを孕んでいます。したがって、開発者は意図的に FLS をチェックし、適用するコードを記述する責任があります。

Apex で FLS を適用するには、主に以下の3つのアプローチがあります。

1. Describe 情報を使用した手動チェック

Apex の `Schema` 名前空間にあるメソッド群を利用して、オブジェクトや項目のメタデータを動的に取得し、その中に含まれるアクセシビリティ情報を確認する方法です。`Schema.DescribeSObjectResult` や `Schema.DescribeFieldResult` といったクラスの `isAccessible()`、`isCreateable()`、`isUpdateable()` などのメソッドを使います。これは最も古くからある伝統的な方法で、非常に細かい制御が可能です。

2. SOQL の WITH SECURITY_ENFORCED

Spring '20 で導入された比較的新しい機能です。SOQL クエリに `WITH SECURITY_ENFORCED` 句を追加するだけで、クエリ実行時に自動的に FLS とオブジェクト権限がチェックされます。ユーザーがアクセス権を持たない項目を `SELECT` 句に含めると、`System.QueryException` がスローされます。これにより、データ読み込み時のセキュリティチェックを非常にシンプルに記述できます。

3. Security.stripInaccessible メソッド

Winter '20 で導入されたメソッドです。このメソッドは、DML 操作(Insert, Update)の前に sObject のリストからアクセス不能な項目を自動的に除去したり、クエリ結果からアクセス不能な項目を削除したりするために使用します。例外をスローするのではなく、安全な状態にデータを「クリーンアップ」するアプローチです。これにより、エラーハンドリングを簡素化しつつ、安全なデータ操作を実現できます。

これらのアプローチを適切に使い分けることが、セキュアな Apex 開発の鍵となります。


示例コード

ここでは、前述したアプローチの具体的なコード例を見ていきましょう。すべてのコードは Salesforce の公式ドキュメントで解説されている概念に基づいています。

1. Describe 情報を使用した FLS チェック

以下の例では、取引先責任者 (Contact) の `FirstName` と `Email` 項目に対するアクセス権をチェックしています。これは、ユーザーにデータを表示する前に、そのユーザーが項目を見る権限を持っているかを確認する典型的なパターンです。

// アクセス権をチェックしたいsObjectと項目のリストを準備
List<String> fields = new List<String>{'FirstName', 'Email'};
Map<String, Schema.SObjectField> fieldMap = Schema.SObjectType.Contact.fields.getMap();

// 各項目をループしてアクセス権をチェック
for (String fieldName : fields) {
    // isAccessible() メソッドで参照権限を確認
    if (!fieldMap.get(fieldName).getDescribe().isAccessible()) {
        // 権限がない場合はエラーメッセージなどを処理
        System.debug(Logginglevel.ERROR, 'ユーザーには項目 ' + fieldName + ' への参照アクセス権がありません。');
        // ここでカスタム例外をスローするなどの対応が考えられる
        // throw new MySecurityException('項目へのアクセスが拒否されました。');
    } else {
        System.debug('項目 ' + fieldName + ' への参照アクセス権があります。');
    }
}

// 同様に、更新権限もチェック可能
if (!Schema.SObjectType.Contact.fields.Email.getDescribe().isUpdateable()) {
    System.debug(Logginglevel.ERROR, 'ユーザーには Email 項目への更新アクセス権がありません。');
}

この方法は柔軟性が高い反面、コードが冗長になりがちです。多くの項目をチェックする必要がある場合は、共通のユーティリティメソッドを作成することが推奨されます。

2. WITH SECURITY_ENFORCED を使用した SOQL クエリ

データを読み込む時点で FLS を強制したい場合に最もシンプルで強力な方法です。ユーザーが `AnnualRevenue` 項目にアクセスできない場合、このクエリは例外を発生させます。

// WITH SECURITY_ENFORCED を使用して、FLS とオブジェクト権限を強制
try {
    List<Account> accounts = [
        SELECT Id, Name, AnnualRevenue 
        FROM Account 
        WITH SECURITY_ENFORCED
    ];
    System.debug('取引先レコードの取得に成功しました。');
    // 取得したデータを安全に処理
    for(Account acc : accounts) {
        System.debug('取引先名: ' + acc.Name + ', 年間売上: ' + acc.AnnualRevenue);
    }
} catch (System.QueryException e) {
    // アクセス権がない場合、QueryException がスローされる
    System.debug('セキュリティエラー: ' + e.getMessage());
    // UI にフレンドリーなエラーメッセージを表示するなどの対応
}

3. Security.stripInaccessible メソッドの使用

DML 操作の前に、ユーザーが編集権限を持たない項目を sObject から安全に取り除く例です。これにより、意図しない項目の更新を防ぎます。

// 更新対象のレコードを準備
List<Account> accountsToUpdate = [SELECT Id, Name, TickerSymbol, AnnualRevenue FROM Account LIMIT 2];

for (Account acc : accountsToUpdate) {
    // ユーザーに編集権限があるかどうかにかかわらず、値を設定しようとする
    acc.TickerSymbol = 'CRM'; 
    acc.AnnualRevenue = 5000000;
}

// Security.stripInaccessible を呼び出し、アクセスできない項目を削除
// 第2引数にはチェックしたいアクセスレベルを指定 (ここでは UPDATE)
SObjectAccessDecision decision = Security.stripInaccessible(
    AccessType.UPDATABLE,
    accountsToUpdate
);

// 削除された項目を確認
for (String fieldName : decision.getRemovedFields().get('Account')) {
    System.debug('更新から削除された項目: Account.' + fieldName);
}

// 安全になった sObject リストで DML 操作を実行
// これで、ユーザーが権限を持たない項目を更新しようとしてもエラーにならず、
// かつ、その項目は更新対象から除外される
try {
    update decision.getRecords();
    System.debug('レコードの更新が正常に完了しました。');
} catch (DmlException e) {
    System.debug('DML エラー: ' + e.getMessage());
}

このメソッドは、特に動的に項目が更新されるような汎用的なコンポーネントを開発する際に非常に有用です。


注意事項

FLS を扱う際には、以下の点に特に注意してください。

権限とコンテキスト

前述の通り、Apex はデフォルトでシステムコンテキストで実行されます。`with sharing` キーワードはレコードの共有設定(どのレコードが見えるか)には影響しますが、FLS(どの項目が見えるか)には影響しません。FLS のチェックは、共有設定とは独立して、明示的に行う必要があります。

API 制限とパフォーマンス

`Schema.getGlobalDescribe()` や `Schema.describeSObjects()` などの Describe 呼び出しは、ガバナ制限の対象となります。ループ内で繰り返し Describe 呼び出しを行うと、簡単に制限に達してしまう可能性があります。Describe 結果はキャッシュされるため、同じオブジェクトや項目に対する2回目以降の呼び出しは高速ですが、初回呼び出しのコストは考慮すべきです。可能な限り Describe 情報を一度取得し、Map などに保持して使い回す設計がパフォーマンス向上に繋がります。

エラーハンドリング

`WITH SECURITY_ENFORCED` を使用する場合、アクセス違反は `QueryException` を引き起こします。これは致命的なエラーであり、`try-catch` ブロックで適切に捕捉しないとトランザクション全体がロールバックされます。ユーザーに分かりやすいエラーメッセージを返すなど、丁寧なエラーハンドリングが求められます。一方、`stripInaccessible` は例外をスローしないため、どの項目が削除されたかを把握したい場合は、戻り値である `SObjectAccessDecision` オブジェクトを評価する必要があります。

管理パッケージにおける考慮事項

AppExchange で配布する管理パッケージを開発する場合、FLS の扱いはさらに重要になります。インストール先の組織で、管理者が設定した FLS を尊重する設計にしなければ、セキュリティレビューに合格できません。パッケージ内のコードでは、FLS チェックを徹底することが必須要件です。


まとめとベストプラクティス

Salesforce 開発者として、FLS を正しく理解し、Apex コードに組み込むことは、信頼性が高くセキュアなアプリケーションを構築するための基本です。最後に、ベストプラクティスをまとめます。

  1. 常にセキュリティを意識する: Apex はシステムコンテキストで動作することを常に念頭に置き、「デフォルトで安全」なコーディングを心がけましょう。
  2. 適切なツールを選択する:
    • データの読み取り時には、可能な限り `WITH SECURITY_ENFORCED` を使用し、クエリレベルでセキュリティを確保します。
    • データの書き込み(DML)前には、`Security.stripInaccessible` を使用して、不正な更新を未然に防ぎます。
    • 非常に動的で複雑な権限チェックが必要な場合に限り、伝統的な Describe メソッドを使用します。
  3. セキュリティチェックを共通化する: FLS チェックロジックを繰り返し記述するのではなく、再利用可能なユーティリティクラス(例: `SecurityUtils`)にまとめることで、コードの保守性と一貫性を高めます。
  4. テストを徹底する: `System.runAs()` メソッドを使用して、異なる権限を持つユーザーとしてテストを実行し、FLS が正しく機能していることを確認します。参照権限のないユーザー、編集権限のないユーザーなど、複数のシナリオをカバーするテストクラスを作成しましょう。

項目レベルセキュリティは、単なる管理者の設定項目ではありません。我々開発者がその動作を深く理解し、コードレベルで尊重することによって初めて、Salesforce プラットフォームの堅牢なセキュリティモデルが完成します。セキュアなコーディングを実践し、顧客のデータを守る信頼性の高いアプリケーションを構築していきましょう。

コメント