SAML統合でSalesforceからAWSのサービスにアクセスする (3)

こんにちは、@stomita です。

SAMLを使ったSalesforceとAWS S3の連携についての記事書いてますが、どうやらこれで最終回を迎えそうです。

前回は設定ばかりでかなりめんどくさかったと思いますが、重要なところ(SalesforceからSAML連携してS3のAPIの呼び出しまで)はだいたい達成できました。

今回は、アプリケーションとして実際に役に立つように、作成したアプリに少しづつ手を加えていきます。内容はかなりJavaScriptがメインになります。

はじめてこの記事にアクセスされた方は、これまでの記事をぜひご覧ください。

それではどうぞ。




アプリケーション構築手順

1. ファイルアップロード機能

前回、AWS SDKSalesforceから発行されたSAMLアサーションを利用して、AWS S3に格納されているファイルのリストを取得できたが、まだファイルの追加アップロードやファイルの中身の参照ができない。

ファイルのアップロードについては、一般的に、HTMLのファイル選択要素からファイルを選択させるか、ファイルをドラッグ&ドロップさせるかの2つ方法がある。

前者の場合、ブラウザはHTML5のFile APIをサポートする必要があり、後者の場合はそれに加えてHTML5 Drag and Drop APIをサポートしている必要がある。IE10+とか最近のモダンなブラウザなら両方ともOK。詳しいブラウザのサポート状況はCan I Useを参照。

ここではファイル選択要素を使って、選択されたファイルを自動的にアップロードするようにする。

以下のようにHTML内にファイル選択要素を追加。

...
    <body>
        <input type="hidden" id="samlResponse" value="{!$CurrentPage.parameters.SAMLResponse}" />
        <div>
        Select File to Upload: <input type="file" id="fileSelect" />
        </div>
        <ul id="fileList"></ul>
    </body>
...

ファイル選択のイベントハンドラを定義し、その中でAWS SDKのputObject APIを使って、選択されたファイルをアップロードする。

ファイルのリスト表示部分はアップロード後にも再度使われるので、初期化処理とは別の関数に分割して整理している。

var bucketName = 'salesforce-bucket';
var bucket;
 
$(function() {
  initAWSCredentials();
  initEventHandlers();
  updateFileList();
});
 
function initAWSCredentials() {
  var assertion = $('#samlResponse').val();
  if (!assertion) {
    throw Error("No assertion is given to this page. Please access from app menu.");
  }
  AWS.config.credentials = new AWS.SAMLCredentials({
    RoleArn: 'arn:aws:iam::119301928242:role/SalesforceUserRole',
    PrincipalArn: 'arn:aws:iam::119301928242:saml-provider/SalesforceIdP',
    SAMLAssertion: assertion
  });
  bucket = new AWS.S3({ params: { Bucket: bucketName } });
}
 
function initEventHandlers() {
  $('#fileSelect').on('change', function() {
    uploadFile(this.files);
  });
}
 
function updateFileList() {
  bucket.listObjects({}, function(err, resp) {
    if (err) { throw err; }
    $('#fileList').empty();
    $.each(resp.Contents, function(i, content) {
      $('<li>').text(content.Key).appendTo($('#fileList'));
    });
  });
}
 
function uploadFile(files) {
  if (files && files.length > 0) {
    var file = files[0];
    var params = { Key: file.name, ContentType: file.type, Body: file };
    bucket.putObject(params, function(err, resp) {
       updateFileList();
    });
  }
}

S3 File Listアプリにアクセスし、ファイル選択をクリックすると、ファイル選択ダイアログが現れ、選択したファイルが自動的にS3にアップロードされることを確認。

f:id:shinichitomita:20131225172223j:plain

2. ファイルの参照リンクの作成

次にファイルの内容を参照するためのリンクを付加する。

ファイルの中身を参照する際には、JavaScript SDK経由ではなくブラウザから直接アクセスする必要があるため、一時的な認証情報を含めた署名つきURLをJavaScirpt SDKから発行しておく必要がある。この認証情報は比較的短い有効期限に設定されるため、リンクを表示する際に署名URLを生成するのではなく、クリック時に動的にURLを生成して開くようにする。

ファイルのリスト表示を以下のように変更し、ファイル名にダウンロードリンクを付加する。

function updateFileList() {
  bucket.listObjects({}, function(err, resp) {
    if (err) { throw err; }
    $('#fileList').empty();
    $.each(resp.Contents, function(i, content) {
      var viewLink = $('<a href="#" class="view-link">').text(content.Key);
      $('<li>').data('key', content.Key)
               .append(viewLink)
               .appendTo($('#fileList'));
    });
  });
}

参照リンクがクリックされた時のイベントハンドラを追加し、getSignedUrl API から取得した署名付きURLを開くようにする。

function initEventHandlers() {
  // ...
  //
  $('#fileList').on('click', 'a.view-link', function() {
    var key = $(this).parents('li').data('key');
    bucket.getSignedUrl('getObject', { Key: key }, function (err, url) {
      window.open(url, '_blank');
    });
  });
}

S3 File Listアプリを開いて、ファイルリストの各ファイルのリンクをクリックしたらファイルの内容が参照できることを確認する。

f:id:shinichitomita:20131225172224j:plain

2. ファイルの削除

アップロードしたファイルを削除することももちろん必要なので、削除機能についても実装する。

ファイルのリスト表示を以下のように変更し、ファイル名の横に削除リンクを付加する。

function updateFileList() {
  bucket.listObjects({}, function(err, resp) {
    if (err) { throw err; }
    $('#fileList').empty();
    $.each(resp.Contents, function(i, content) {
      var viewLink = $('<a href="#" class="view-link">').text(content.Key);
      var deleteLink = $('<a href="#" class="delete-link">').text('Delete');
      $('<li>').data('key', content.Key)
               .append(viewLink)
               .append('&nbsp;[')
               .append(deleteLink)
               .append(']')
               .appendTo($('#fileList'));
    });
  });
}

削除リンクがクリックされた時のイベントハンドラを追加し、確認ダイアログを表示した後、deleteObject API を呼び出してKeyに設定されているファイルを削除する。

function initEventHandlers() {
  // ...
  //
  $('#fileList').on('click', 'a.delete-link', function() {
    var key = $(this).parents('li').data('key');
    if (confirm('Are you sure you want to delete the file "' + key + '" ?')) {
      bucket.deleteObject({ Key: key }, function (err) {
        updateFileList();
      });
    }
  });
}

S3 File Listアプリを開いて、ファイルリストの各ファイルの削除リンクをクリックしたらファイルを削除できることを確認する。

f:id:shinichitomita:20131225172225j:plain

4. 詳細ページ埋め込み用Visualforceページの作成

ここまでで、ファイルの参照・追加・削除などの一通りのファイル操作ができるようになったので、添付ファイルの代替としてS3を利用できるように、Salesforceのレコード表示の詳細ページにS3 File Listアプリを埋め込んでみる。

今までlistObject APIには何もパラメータを渡していなかったため、Bucketにある全ファイルがリストされていたが、Prefixを指定することで、あるディレクトリ内に含まれるファイルのみをリストする、といったことができる。

ここでは、Prefixとして各レコードのIDを指定し、ファイルを各レコードIDのディレクトリに振り分けることで、レコードに関連したファイルのみをリストして、添付ファイルとして活用できるようにする。

まず、詳細レコードに埋め込むためのVisualforce Pageを作成する。詳細ページに埋め込むVisualforce Pageは各オブジェクトタイプごとに必要なので、例えば商談の詳細ページに埋め込みたい場合は、それ専用のPageを作る必要がある。

以下は商談の詳細ページに埋め込む場合のVisualforce Pageの例

<apex:page standardController="Opportunity">
<apex:iframe frameborder="false" width="100%" height="200" scrolling="true"
             src="/idp/login?app=0spi0000000Kyou" />
<script>
window.recordId = '{!id}';
</script>
</apex:page>

apex:page タグの standardController 属性に埋め込むオブジェクトのオブジェクトタイプを記載(この例ではOpportunity)を設定するのを忘れないこと。これを忘れると後のステップでレイアウトに埋め込む際の候補に該当ページがリストされない。

このページのiframe内に、これまでに作成したS3 File Listアプリを表示することになるが、S3へのアクセスにはSAMLアサーションが必要なため、src属性にはIdP-initiated URLを指定し、SAMLアサーションが発行されるようにする。

また、現在のレコードIDをJavaScriptの変数に格納している。これは後でframe内のアプリから参照できるようにするため。

5. 詳細ページレイアウトへの埋め込み

続いて、商談のレイアウトを編集し、Visualforce Pageリストから作成したVisualforce Pageを選択し、適当なセクションにドラッグ&ドロップして埋め込む。

f:id:shinichitomita:20131225164936j:plain

レイアウトを保存すると、商談の詳細ページでは以下のように表示される。ただし、この時点ではまだBucket内の全てのオブジェクトが表示されていることに注意。

f:id:shinichitomita:20131225165030j:plain

Visualforceページ内のJavaScriptコードの、ファイルリスト表示部分を以下のように変更する。

function updateFileList() {
  bucket.listObjects({ Prefix: getPrefix() }, function(err, resp) {
    if (err) { throw err; }
    $('#fileList').empty();
    if (resp.Contents.length === 0) {
      $('<li>').text('No files available').appendTo($('#fileList'));
    } else {
      $.each(resp.Contents, function(i, content) {
        var key = content.Key;
        var fileName = key.split('/').pop();
        var viewLink = $('<a href="#" class="view-link">').text(fileName);
        var deleteLink = $('<a href="#" class="delete-link">').text('Delete');
        $('<li>').data('key', key).data('fileName', fileName)
                 .append(viewLink)
                 .append('&nbsp;[')
                 .append(deleteLink)
                 .append(']')
                 .appendTo($('#fileList'));
      });
    }
  });
}

上記で利用しているgetPrefix関数は、親Windowがある場合に親Window内のrecordId変数の値をprefixとして返す。

function getPrefix() {
  try {
    return window.parent.recordId ? window.parent.recordId + '/' : '';
  } catch(e) {
    return ''; // when access is not allowed to parent window object (maybe in different origin)
  }
}


さらに、アップロードの際にもファイル名にPrefixを付加してアップロードするように変更する。

function uploadFile(files) {
  if (files && files.length > 0) {
    var file = files[0];
    var params = { Key: getPrefix() + file.name, ContentType: file.type, Body: file };
    bucket.putObject(params, function(err, resp) {
       updateFileList();
    });
  }
}

もう一度商談の詳細ページに戻りリロードする。以下のように表示されればOK。

f:id:shinichitomita:20131225165556j:plain

ファイルをアップロードするとレコードID別にフォルダに格納される

商談その1:
f:id:shinichitomita:20131225165641j:plain

商談その2:
f:id:shinichitomita:20131225165659j:plain

AWS Consoleから見た結果。ちゃんとIDに分けてフォルダごとに格納されているのが分かる。

商談その1:
f:id:shinichitomita:20131225165727j:plain

商談その2:
f:id:shinichitomita:20131225165744j:plain

6. 細かい調整

HTML5 Drag and Drop APIを使ってドラッグ&ドロップでアップロードできるようにしたり、複数ファイルを同時にアップロードできるようにしたり、API呼び出し時にローディングメッセージを表示できるようにしたり、色々スタイルを修正したりした結果がこちら。

複数ファイルのドラッグドロップでの追加は、こんな感じになる。

f:id:shinichitomita:20131225170054p:plain

最後に

以上、SAMLを使ったSalesforceとAWS S3サービスとの連携方法について、3回に分けてお伝えしました。

SAMLは主に、企業向けビジネスアプリケーションサービス間におけるシングルサインオンの用途で利用されることが多いですが、AWSの場合は単なるログインだけでなく、SAMLアサーションAPIの認証連携にも使えるようになっているため、APIを利用して複数サービスの良い所を組み合わせて利用できるという、大変興味深いことができます。

AWSにはS3だけでなく、DynamoDBやSNS、SQSなどのサービスもあります。もちろんSalesforceにも豊富なAPIがありますので、組み合わを色々考えると夢が広がりますね!

ではでは。