dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

[Salesforce]UserRecordAccessでレコードの参照・編集権限をチェックする

こんなことやりたい

Apex で SOQL を実行してレコードを取得するとき、実行ユーザが編集権限のあるレコードだけ返したい。
このとき、レコードに対し参照権限だけあると SOQL では取得できてしまうため適宜フィルターする必要がある。


先にまとめ

UserRecordAccess オブジェクトというのがあるので、それを使いましょう。
UserRecordAccess | SOAP API 開発者ガイド | Salesforce Developers

UserRecordAccess には Has*Access ( *EditDelete など) という項目があるので、チェックしたい権限に応じた項目を使います。

また API バージョン 30.0 以降であれば UserRecordAccess は各 SObject のリレーション項目として取得できます。

例:SELECT Id, Name, Email, UserRecordAccess.HasEditAccess FROM Contact


方法

ほぼほぼこちらのテラスカイさんの記事にある通りです。感謝。
が、少々アップデートがあるみたいなので順を追って記載します。

ブログ記事にあるように、あるユーザが特定のレコードに対して参照・編集・削除などの権限を有しているかを判定するためのオブジェクトとして UserRecordAccess というオブジェクトがあります。

UserRecordAccess | SOAP API 開発者ガイド | Salesforce Developers

上のオブジェクト定義を見るとわかりますが、 UserRecordAccess には以下の項目があります。

  • 各種の権限があるかどうかのフラグ項目
    • HasReadAccess : 参照
    • HasEditAccess : 編集
    • HasDeleteAccess : 削除
    • ほか
  • UserId
  • RecordId

UserIdRecordId を WHERE 句に指定することで、「あるユーザが特定のレコードに対して権限を有しているか」を判断できるわけです。

なので、冒頭に記載したように「編集権限のあるレコードだけ返す」を実現するためには、こういったメソッドを用意すればいいでしょう。

public List<Contact> getEditableContacts() {
    Map<Id, Contact> contactMap = new Map<Id, Contact>([
        SELECT Id, Name, Email FROM Contact
    ]);
                                                         
    List<UserRecordAccess> accesses = [
        SELECT
            RecordId, HasEditAccess
        FROM
            UserRecordAccess
        WHERE
            UserId = :UserInfo.getUserId()
        AND
            RecordId IN :contactMap.keySet()
    ];
                                                         
    List<Contact> result = new List<Contact>();
    for (UserRecordAccess access : accesses) {
        if (access.HasEditAccess) {
            result.add(contactMap.get(access.RecordId));
        }
    }
    return result;
}


制限事項

というわけで便利なオブジェクトですが、制限事項がいくつかあります。
↑のテラスカイさんのブログ記事でも述べられていますが、個人的に気になったものを挙げると

  • RecordId は単一または複数のレコードを WHERE 句に指定できるが、 UserId は単一のみ
    • UserId IN *** とすると Error: Can filter on only UserId = [single ID], either RecordId = [single ID] or RecordId IN [list of IDs], and Has*Access = true というエラーが出る
  • 一度に取得できるレコードは 200 件までで、SOQL の結果が 200 件を超えると QueryException が発生する

特に後者の制限が厳しいですね。。。


より良い方法

制限事項を回避できる何か良い方法はないのかと SOAP API 開発者ガイド を最後まで読むと、

API バージョン 30.0 以降では、UserRecordAccess はレコードの外部キーになります。このオブジェクトをルックアップまたは外部キーとして使用する場合は、UserId または RecordId 項目を検索条件に使用したり、指定したりすることはできません。前記のサンプルクエリは、次のように実行できます。
SELECT Id, Name, UserRecordAccess.HasReadAccess, UserRecordAccess.HasTransferAccess, UserRecordAccess.MaxAccessLevel FROM Account

お! UserRecordAccessAPI バージョン 30.0 以降だと任意の SObject のリレーション項目として一緒に取得できるようです。
なので、先ほどのサンプルは以下のように書き直すことができます。

/** Since API version 30.0 **/
public List<Contact> getEditableContacts() {
    List<Contact> contacts = [
        SELECT
            Id, Name, Email,
            UserRecordAccess.HasEditAccess
        FROM
            Contact
    ];
    List<Contact> result = new List<Contact>();
    for (Contact contact : contacts) {
        System.debug('Contact [' + contact.Name + '] is editable?: ' + contact.UserRecordAccess.HasEditAccess);
        if (contact.UserRecordAccess.HasEditAccess) {
            result.add(contact);
        }
    }
    return result;
}

これにより、制限事項のうち「一度に取得できるレコードは 200 件まで」は回避できました。よかった。

ただし、 UserRecordAccess.Has*Access は WHERE 句に直接は指定できず、以下のエラーになります。

Error: You cannot filter on UserRecordAccess when used in a relationship


注意事項

Task, Event など一部の標準オブジェクトについては、UserRecordAccess を項目として取得する方法は使えないようです。

UserRecordAccess オブジェクトへのクエリが一度に 200 件までという制限があることなど、内部的にパフォーマンスに影響ありそうなことをやってそうな香りがしますね。
実際に使うときはパフォーマンス面での影響を少し意識しなければ。


サンプルコード

https://github.com/zaki-yama/salesforce-samples/tree/master/user-record-access