2020.12.02

Apexを用いたファイルダウンロード方法の制御について

はじめに

昨今の情勢では働き方も大きく変わってきまして、リモートワークを始めとした新しいワーキングスタイルがみなさまの会社でも続々と導入が進んでいるかと思います。
そのような中、いかにセキュリティレベルを高く維持し、システム運用を続けていくかという部分は、ますます重要度が高まっています。
このような観点から、今回はSalesforce上へアップロードしたファイルについて、どのようなダウンロード制御がバックエンドから制御できるかについて焦点を当て、お話をさせていただきます。

よろしくお願いいたします!

Salesforce Filesについて簡単に

Salesforce Filesについては、よくご存じの方もいらっしゃるかと思いますが、Salesforce上でのファイル管理機能(アップロード,ダウンロード、フォルダ管理...etc)となります。今回は、Files自体の細かな機能説明については割愛させていただきます。
また、Filesと似ているSalesforceの標準機能として、添付ファイル、ドキュメントなどもあります。各機能の差異や機能概要については、下記公式ページにてわかりやすくまとまっておりますので、もしご興味ある場合は参考となれば幸いです。今回はFilesでのダウンロード制御へ焦点をあてご紹介いたします。
ファイル、Salesforce CRM Content、Salesforce ナレッジ、ドキュメント、添付ファイルの違い
ではここから、ファイルダウンロードをどのように制御していくかについて説明いたします。

とりあえずファイルをそのまま上げて、そのまま落としてみました

Salesforceへログイン後、ファイルタブへ遷移し、"ファイルをアップロード"から任意のファイルをアップしてみます。

アップが完了したら画面上にアップロード済のファイル名が表示されるかと思いますので、そのファイル名を押下し、画面上部ダウンロードリンクからファイルをダウンロードします。

ダウンロードリンク

ダウンロード成功

ここまでは特に何も制御を入れておりませんので「普通のファイル機能そのまま」といった形となります。
では次のセクションで、ファイル情報、ユーザ情報を問わずダウンロードをApexで全面禁止としてみましょう。

ファイルダウンロードを全面禁止にしてみました

お待たせしました、ここからがいよいよ本題となります。
以下のコードをApexクラスを先ほどのファイルダウンロードした同組織へ作成してみます。
※ApexAPIバージョンは39以降のみ使用可能となっておりますのでお気を付けください。
public class ContentDownloadHandlerFactoryImpl implements Sfc.ContentDownloadHandlerFactory {

    public Sfc.ContentDownloadHandler getContentDownloadHandler(List<ID> ids, Sfc.ContentDownloadContext context) {
        Sfc.ContentDownloadHandler contentDownloadHandler = new Sfc.ContentDownloadHandler();
        //ダウンロード禁止
        contentDownloadHandler.isDownloadAllowed = false;
        //禁止のエラーメッセージ
        contentDownloadHandler.downloadErrorMessage = 'ファイルダウンロード全般禁止します!';
        return contentDownloadHandler;
    }

}
ContentDownloadHandlerFactoryImpl
非常に短いソースですが、ダウンロードをとりあえず止めたいということでしたら最低限の記述としてはこちらのみとなります。では、こちらのクラスを作成した状態で先ほどのファイルを改めて落としてみましょう。

先ほどのファイルダウンロードリンク

エラー画面へリダイレクト

先ほどと同じダウンロード操作を実施したところ、エラーとなってしまいました。
ソースコード上で定義しているエラーメッセージも画面に表示されておりますので、エラーがしっかりと効いていることが確認できます。また、今回はLightning Experience上のファイル画面からエラーを発生させましたが、リダイレクト先を特にコード上で指定していないためにエラー画面はクラシックの標準エラーページとなっているようです。
こちらについても後述のまとめにて触れさせていただきます。

ダウンロード制御機能の詳細について

上記の説明では、とりあえずダウンロードを止めるにはSfc.ContentDownloadHandlerFactoryインターフェースを実装したクラスを用意すればいい!というところまでを説明いたしました。しかし、実際の運用ではダウンロードを全面禁止するというケースは稀かと思います。
本セクションでは、クラス上でどのような粒度のダウンロード制御ができるかに着眼し説明させていただきます。
① ContentDownloadContext による制御
最初にご紹介するのが、こちらの列挙型変数です。
こちらの変数をgetContentDownloadHandler()メソッドの第二引数として受け取れますので、実行ユーザがどのコンテキストからダウンロード操作を実行したのか、というのが以下のパターンにて判別可能です。
 

引用:ContentDownloadContext 列挙

写真提供:Salesforce Developers
Sampleとして、モバイルデバイスからのダウンロードのみを禁止したい場合は、先ほどのコードを修正し以下のようにif文で分岐をさせるイメージとなるかと思います。
public class ContentDownloadHandlerFactoryImpl implements Sfc.ContentDownloadHandlerFactory {

    public Sfc.ContentDownloadHandler getContentDownloadHandler(List<ID> ids, Sfc.ContentDownloadContext context) {
        Sfc.ContentDownloadHandler contentDownloadHandler = new Sfc.ContentDownloadHandler();
        //モバイルデバイス判定
        if(context == Sfc.ContentDownloadContext.MOBILE) {
            //モバイルデバイスの場合はダウンロード禁止
            contentDownloadHandler.isDownloadAllowed = false;
            contentDownloadHandler.downloadErrorMessage = 'モバイルデバイスではダウンロード禁止です!';
            return contentDownloadHandler;
        }
        //モバイル以外はダウンロード許可
        contentDownloadHandler.isDownloadAllowed = true;
        return contentDownloadHandler;
    }

}
ContentDownloadHandlerFactoryImpl - MOBILE
もちろんif文ですので、複数の条件をANDやORで組み合わせることも可能です。
また、この部分について複数のコンテキストが組み合わさった操作をした場合に値はどうなるのか?という疑問を持たれた方もいらっしゃるかと思います。
そちらについては、現時点での動作確認結果を共有いたしますのでご参考となれば幸いです。

テストパターン① モバイル端末 & モバイルブラウザ & ファイルタブよりファイルダウンロード
⇒ContentDownloadContextの値:CHATTER

テストパターン② モバイル端末 & モバイルブラウザ & CHATTERよりファイルダウンロード
⇒ContentDownloadContextの値:CHATTER

テストパターン③ モバイル端末 & Salesforceモバイル(アプリ) & ファイルタブよりファイルダウンロード
⇒ContentDownloadContextの値:REST_API

テストパターン④ モバイル端末 & Salesforceモバイル(アプリ) & CHATTERよりファイルダウンロード
⇒ContentDownloadContextの値:REST_API

実運用の際はContentDownloadContextの値が期待値通りのものとなっているのか十分なテスト実施のうえ、ご使用いただくのが良いかと思います。
② 実行ユーザ情報での制御
実装クラス上でのコンテキストユーザはダウンロード実行者となるため、通常のApexコードと同様にUserInfoクラスのメソッドを用いコンテキストユーザ情報の取得が可能です。
getUserId()を用い特定ユーザのみピンポイントで、もしくはgetProfileId()を用いプロファイル単位で制御を掛ける等多様な条件判定を構築することが可能となっています。

参考:Apex 開発者ガイド - UserInfo クラス
https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_methods_system_userinfo.htm
③ ContentVersion IDを用いたファイル単位での制御
実装メソッドgetContentDownloadHandler()の第一引数(上記Sample内:List<ID> ids)では、ダウンロード対象のファイルデータであるContentVersionオブジェクトIDが渡ってきます。
こちらのIDを基にContentVersionの各項目へアクセスできるため、特定のファイル拡張子であればダウンロードを許容しないなど、ファイル単位での細かな制御も可能となります。

参考:SOAP API 開発者ガイド - ContentVersion
https://developer.salesforce.com/docs/atlas.ja-jp.api.meta/api/sforce_api_objects_contentversion.htm

ダウンロードエラーを起こした際のリダイレクト先について

先ほどのSampleでは特にエラー時のリダイレクト先を特に指定しておりませんでしたが、ダウンロードエラ―に抵触した際に指定のURLへリダイレクトさせることが可能です。

それがContentDownloadHandler.redirectUrlです。

禁止にする際は、ContentDownloadHandler.isDownloadAllowedへfalseをセットしますが、returnする前にContentDownloadHandler.redirectUrlへ相対パスでのリダイレクト先をセットしてあげると、エラーの際に指定したページへリダイレクトされる形となります。
下記引用の公式リファレンスSampleでは、遷移先をVisualforceページでありパラメータとしてダウンロード対象のファイルIDを渡し、遷移先ページ側で問題のファイルをチェックするために外部のIRM製品へコールアウトを実施している流れとなっておりますので、実際の運用でContentDownloadHandler.redirectUrlを指定する際は近しい形での用途が多いかと思われます。
// Allow customization of the content Download experience
public class ContentDownloadHandlerFactoryImpl implements Sfc.ContentDownloadHandlerFactory {

public Sfc.ContentDownloadHandler getContentDownloadHandler(List<ID> ids, Sfc.ContentDownloadContext context) {
    Sfc.ContentDownloadHandler contentDownloadHandler = new Sfc.ContentDownloadHandler();

    if(UserInfo.getUserId() == '005xx') {
        contentDownloadHandler.isDownloadAllowed = true;
        return contentDownloadHandler;
    }
    
    contentDownloadHandler.isDownloadAllowed = false;
    contentDownloadHandler.downloadErrorMessage = 'This file needs to be IRM controlled. You're not allowed to download it';
    contentDownloadHandler.redirectUrl ='/apex/IRMControl?Id='+ids.get(0);
    return contentDownloadHandler;
}
}
ContentDownloadHandlerFactoryImpl
public class IRMController {
    
private String downloadEndpoint;
    
public IRMController() {
    downloadEndpoint = '';
}
    
public void applyIrmControl() {
    String versionId = ApexPages.currentPage().getParameters().get('id');
    Http h = new Http();

    //Instantiate a new HTTP request, specify the method (GET) as well as the endpoint
    HttpRequest req = new HttpRequest();
    req.setEndpoint('http://irmsystem?versionId=' + versionId);
    req.setMethod('GET');

    // Send the request, and retrieve a response
    HttpResponse r = h.send(req);
    JSONParser parser = JSON.createParser(r.getBody());
      while (parser.nextToken() != null) {
        if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
            (parser.getText() == 'downloadEndpoint')) {
                parser.nextToken();
                downloadEndpoint = parser.getText();
                break;
        }
    }
}
    
public String getDownloadEndpoint() {
    return downloadEndpoint;
}
    
}
IRMController
引用:Custom File Download Examples

おわりに

いかがでしたでしょうか?実際の運用では上記①~③の組み合わせのみに限らず、よりさまざまなデータを基にダウンロード制御を試みるケースもあるかと思います。
条件の複雑化は不具合発生リスクとのトレードオフにも成り得ますので、どこまで細かく制御を入れていくかの判断は難しいところですが、自由度は高い機能となっておりますので何かしらの一助となれば幸いです。
39 件
     
  • banner
  • banner

関連する記事