BoxとServiceNow連携：Box UI Elementsを埋め込む

# 概要
前回の記事（[BoxとServiceNow連携：コラボレーション作成](https://qiita.com/mshcruz/items/c9f47b8a8e015d01ebbf)）でServiceNowの[サービスポータル](https://docs.servicenow.com/csh?topicname=c_ServicePortal.html&version=latest)からBox上のフォルダ作成を依頼することを紹介しました。

依頼フォームでフォルダ名、親フォルダIDとコラボレーターのメールアドレスを入力する必要がありますが、普通はフォルダIDをわかる人が少ないと思います。URLから取得することができますが、それより使いやすくするため、本記事で[Box UI Elements](https://ja.developer.box.com/guides/embed/ui-elements/)を[レコードプロデューサー](https://docs.servicenow.com/csh?topicname=c_RecordProducer.html&version=latest)に埋め込み、簡単に親フォルダを指定する方法を紹介します。

# 準備

以前紹介したBoxフォルダ作成依頼のレコードプロデューサーが用意されていることを仮定します。本記事で作成する[カスタムウィジェット](https://docs.servicenow.com/csh?topicname=widget-dev-guide.html&version=latest)を利用し、既存のレコードプロデューサーを拡張します。レコードプロデューサーの作成について、[BoxとServiceNow連携：コラボレーション作成](https://qiita.com/mshcruz/items/c9f47b8a8e015d01ebbf)を参考してください。

追加準備として、Box UI Elementsを利用するために、Boxカスタムアプリの「構成」タブの「[CORSドメイン](https://ja.developer.box.com/guides/security/cors/)」セクションでServiceNowのインスタンスURLを設定する必要があります。例えば、`https://dev12345.service-now.com`.

![Screenshot 2024-11-15 at 17.23.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/a11be9ea-d2fa-589c-8d4a-82decb3dd162.png)


# 実装

Box UI Elementsは[JavaScriptとCSSので提供されています](https://ja.developer.box.com/guides/embed/ui-elements/installation/)が、レコードプロデューサーで直接にJavaScriptを埋め込むことができなさそうです。ワークアラウンドとして、カスタムウィジェットにBox UI Elementsを埋め込んで、カスタムウィジェットをレコードプロデューサーの変数として設定します。

親フォルダを選択するには、Box UI Elementsの[Content Picker](https://ja.developer.box.com/guides/embed/ui-elements/picker/)を使いますので、ServiceNowのRESTメソッド（サーバー側）でトークンを取得し、Content Picker（クライアント側）にトークンを渡します。ただ、そのままトークンを渡すと、トークンを発行したアカウントが見える全てのファイル・フォルダはContent Pickerで表示されてしまいます。また、クライアント側から他の目的にそのトークンが使え、セキュリティ面でリスクが発生します。

対策として、Content Pickerにトークンを渡す前に、[トークンダウンスコープ](https://ja.developer.box.com/guides/authentication/tokens/downscope/)でトークンの権限を絞ります。それで、表示されるコンテンツ（ファイル・フォルダ）が制限され、そのトークンできることは限られます。

上記を実現するには以下のステップを実装します：
1. トークンダウンスコープのHTTPメソッドを追加
2. カスタムウィジェットを作成
3. レコードプロデューサーを拡張

最後に、動作確認を行います。

:::note warn
この記事の最初バージョンに「トークン取得のHTTPメソッドを追加」というステップもありましたが、[ServiceNowは自動的に取得するトークンを使った方がシンプル](https://developer.servicenow.com/dev.do#!/reference/api/washingtondc/server/sn_auth-namespace/c_GlideOAuthClient#r_GlideOAuthClientRequestgetToken)だとわかりました。そのため、明示的にトークンを取得することが不要で、略します。
:::

## 「トークンダウンスコープ」のHTTPメソッドを追加

取得したトークンの権限を絞るため、また`https://api.box.com/oauth2/token`のエンドポイントを使いますが、リクエストの本文で指定するパラメータが異なります（[ガイド](https://ja.developer.box.com/guides/authentication/tokens/downscope/#%E3%83%80%E3%82%A6%E3%83%B3%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97%E3%81%AE%E5%AE%9F%E4%BE%8B)）：
- subject_token：ダウンスコープされるトークンを渡しますので、変数にします
- subject_token_type：トークンダウンスコープの場合は`urn:ietf:params:oauth:token-type:access_token`で固定されています
- scope：[目的に応じて変わります](https://ja.developer.box.com/guides/api-calls/permissions-and-errors/scopes/#%E3%83%80%E3%82%A6%E3%83%B3%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97%E7%94%A8%E3%81%AE%E3%82%B9%E3%82%B3%E3%83%BC%E3%83%97)が、レコードプロデューサーのフォームでの親フォルダ選択に使いますので、「base_picker」にします
- grant_type：トークンダウンスコープの場合は`urn:ietf:params:oauth:grant-type:token-exchange`で固定されています

:::note 
「resource」という任意パラメータを指定すると、ダウンスコープされるトークンアクセスできるコンテンツを特定のファイル・フォルダに制限できます。
:::

トークンをダウンスコープするには新規なHTTPメソッドを作成します：
- 名前：<任意の名前>（例：Boxトークンをダウンスコープ）
- HTTP メソッド：POST
- REST エンドポイント： https://api.box.com/oauth2/token

HTTP要求」タブの「HTTPヘッダー」として、`Content-Type`ヘッダーを追加し、値を`application/json`にします。

最後に、「HTTP要求」タブの「コンテンツ」の内容は以下のJSONです。JSONでは`access_token`を変数として指定します

```JSON
{
  "subject_token": "${access_token}",
  "subject_token_type": "urn:ietf:params:oauth:token-type:access_token",
  "scope": "base_picker",
  "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange"
}
```

![トークンダウンスコープHTTPメソッド.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/4d263f0e-d53c-0725-67b7-7fc8ebe357b3.png)

レコードを保存すると、画面の下に「REST メッセージ関数パラメーター」タブが表示されます。「関連リンク」の「変数の自動生成」をクリックすると、JSONで指定した変数がパラメータとして設定されます。

## カスタムウィジェットを作成

レコードプロデューサーの変数として使うカスタムウィジェットは取得・ダウンスコープしたトークンを使って、ユーザーが選択できるフォルダを表示します。ユーザーはフォルダを選んだら、レコードプロデューサーの[「u_parent_folder_id」変数](https://qiita.com/mshcruz/items/c9f47b8a8e015d01ebbf#%E3%83%AC%E3%82%B3%E3%83%BC%E3%83%89%E3%83%97%E3%83%AD%E3%83%87%E3%83%A5%E3%83%BC%E3%82%B5%E3%83%BC%E3%82%92%E4%BD%9C%E6%88%90:~:text=%E5%90%8D%EF%BC%88%E4%BE%8B%EF%BC%9Au_folder_name%EF%BC%89-,%E3%80%8C%E8%A6%AA%E3%83%95%E3%82%A9%E3%83%AB%E3%83%80ID%E3%80%8D%E5%A4%89%E6%95%B0,-%E3%82%BF%E3%82%A4%E3%83%97%EF%BC%9A1%E8%A1%8C)の値が自動的に変更されることを実装します。

まずは、新規なポータルのウィジェットを作成し、以下のように設定します：
- 名前：<任意の名前>（例：Boxフォルダピッカー）
- ID：<任意のID>（例：box_folder_picker_widget）
- プレビューあり：チェック

「本文 HTML テンプレート」を以下のHTMLにします。

```HTML
<div id="box-picker" style="width:800px" class="container"></div>
```

![カスタムウィジェット1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/4486e58c-ceef-896c-535d-3b1868e8e647.png)

「サーバースクリプト」を以下のJavaScriptコードにします。このコードは作成したHTTPメソッドを使い、アクセストークンを取得・ダウンスコープします。
RESTメソッドで明示的にアクセストークンを取得するより、[ServiceNowのRESTメッセージで取得されるトークンを使います](https://developer.servicenow.com/dev.do#!/reference/api/washingtondc/server/sn_auth-namespace/c_GlideOAuthClient#r_GlideOAuthClientRequestgetToken)。

```JavaScript
(function() {
	var oauthProfileName = 'Boxサーバー認証（CCG） default_profile'; // RESTメッセージで設定されているOAuthプロファイル名
	var boxRestMessageName = 'Box'; // Box用のRESTメッセージ名
	var boxRestMessageDefaultRequestName = 'Default GET'; // Box用のRESTメッセージのデフォルトメソッド名
	var boxRestMessageTokenDownscopeRequestName = 'Boxトークンをダウンスコープ';  // Box用のRESTメッセージのトークンダウンスコープメソッド名
	var oauthProfileId, requestorId, oauthEntityProfileGR, requestorProfileGR = '';
	var restMessage, response, responseBody, responseData = '';
	var oAuthClient, oauthToken, accessToken, downscopedAccessToken = '';	
	
	// アクセストーク取得で利用されるOAuth エンティティプロファイルとOAuth 要求者プロファイルを取得する
	// 参照：https://developer.servicenow.com/dev.do#!/reference/api/washingtondc/server/sn_auth-namespace/c_GlideOAuthClient#r_GlideOAuthClientRequestgetToken
    oauthEntityProfileGR = new GlideRecord('oauth_entity_profile');
    oauthEntityProfileGR.addQuery('name', oauthProfileName); 
    oauthEntityProfileGR.query();	
    if (oauthEntityProfileGR.next()) {
        oauthProfileId = oauthEntityProfileGR.getValue('sys_id'); 
		requestorProfileGR = new GlideRecord('oauth_requestor_profile');
		requestorProfileGR.addQuery('oauth_entity_profile', oauthProfileId);
		requestorProfileGR.query();
		if (requestorProfileGR.next()) {
			requestorId = requestorProfileGR.getValue('requestor_id');
		}
    }

	// OAuth エンティティプロファイルとOAuth 要求者プロファイルを利用し、アクセストークを取得してみる
	oAuthClient = new  sn_auth.GlideOAuthClient();
	oauthToken = oAuthClient.getToken(requestorId, oauthProfileId);
	accessToken = oauthToken.getAccessToken();

	// アクセストークンが存在しない（有効期限が切れた）場合は、デフォルトのリクエストを実施し、アクセストークンが取得される
	if (!accessToken) {
		restMessage = new sn_ws.RESTMessageV2(boxRestMessageName, boxRestMessageDefaultRequestName);
		response = restMessage.execute();
		oAuthClient = new  sn_auth.GlideOAuthClient();
		oauthToken = oAuthClient.getToken(requestorId, oauthProfileId);
		accessToken = oauthToken.getAccessToken();		
	} 
	
	// アクセストークンをダウンスコープ
	restMessage = new sn_ws.RESTMessageV2(boxRestMessageName, boxRestMessageTokenDownscopeRequestName);
	restMessage.setStringParameterNoEscape('access_token', accessToken);
	response = restMessage.execute();
	responseBody = response.getBody();
	responseData = JSON.parse(responseBody);
	downscopedAccessToken = responseData.access_token;

	// ダウンスコープされたトークンをクライアントに渡す
	data.downscopedAccessToken = downscopedAccessToken;
})();
```

「クライアントコントローラー」を以下のJavaScriptコードにします。

```JavaScript
function($scope) {
	var controller = this;
	var accessToken = controller.data.downscopedAccessToken;
    var rootFolderId = '0'; // 最初に表示されるフォルダのID
    var folderPicker = new Box.FolderPicker();

    // 親フォルダが選択されたら、レコードプロデューサーのフォーム変数にIDを保存
    folderPicker.addListener('choose', function(items) {		
     	$scope.page.g_form.setValue('u_parent_folder_id', items[0].id);
    });

    folderPicker.show(rootFolderId, accessToken, {
        container: '#box-picker', // 「本文 HTML テンプレート」で設定したdivのID
		maxSelectable: 1, // フォルダ1つしか選択できない
		canUpload: false, // Boxへのアップロードができない
		canSetShareAccess: false, // アクセス権権の調整ができない
		canCreateNewFolder: false, // フォルダ作成ができない
		logoUrl: "box" // デフォルトのBoxロゴを利用する
    });
};
```

:::note
Content Pickerの見た目や機能などをオプションパラメータで調整できますので、[ガイド](https://ja.developer.box.com/guides/embed/ui-elements/picker/#%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3)を参考にしてください。
:::

![カスタムウィジェット2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/2b1f2517-c04b-0895-e4e9-9890490c27ec.png)

:::note warn
この記事では、なるべくわかりやすくするため、コードを極力省いています。
セキュリティ面での行うべき考慮を省略し、エラーハンドリングなども実装されていません。
具体的な実装時には非機能面の考慮と実装方法を開発者が行う必要があります。
:::

ウィジェットレコードを保存すると、画面の下に「依存関係」タブが表示されます。ここで[Content Pickerのインストール](https://ja.developer.box.com/guides/embed/ui-elements/installation/#%E6%89%8B%E5%8B%95%E3%81%AB%E3%82%88%E3%82%8B%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB:~:text=no.react.js-,Content%20Picker,-21.0.0)で利用するJavaScriptとCSSを設定しますので、新規な依存関係を作ります：
- 名前：<任意の名前>（例：Boxコンテンツピッカー）
- ページロード時にインクルード：チェック

![Boxコンテンツピッカー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/a734daa3-d010-2390-59a6-aab73d84d86f.png)

依存関係レコードを保存すると、画面の下に「JS インクルード」と「CSS インクルード」タブが表示されます。まずは、新規なJSインクルードを作成します：
- 表示名：<任意の名前>（例：BoxコンテンツピッカーJS）
- ソース：URL
- JS ファイル URL：https://cdn01.boxcdn.net/platform/elements/21.0.0/ja-JP/picker.js

![JSインクルード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/e9d66ad7-21e7-7561-be95-2b8ce7f2455f.png)

JSインクルードレコードを保存して、依存関係一覧に戻ります。次に、新規なCSSインクルードを作成します：
- 表示名：<任意の名前>（例：BoxコンテンツピッカーCSS）
- ソース：URL
- JS ファイル URL：https://cdn01.boxcdn.net/platform/elements/21.0.0/ja-JP/picker.css

![CSSインクルード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/7bedb8be-c326-859e-4307-eaa9ef814c60.png)

:::note
Box UI Elementsが対応する言語の一覧は[ガイド](https://ja.developer.box.com/guides/embed/ui-elements/installation/#%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%83%AD%E3%82%B1%E3%83%BC%E3%83%AB)で記載されています。
:::

CSSインクルードレコードを保存して、ウィジェットレコードに戻り、保存します。

## レコードプロデューサーを拡張

次に、[「Boxファイル作成依頼」レコードプロデューサー](https://qiita.com/mshcruz/items/c9f47b8a8e015d01ebbf)にカスタムウィジェットを埋め込みます。

まずは、レコードプロデューサーの新規な変数を作成します：
- タイプ：ラベル付きカスタム
- 「質問」タブの「質問」：<任意>（例：親フォルダを選択してください）
- 「質問」タブの「名前」：<任意>（例：parent_folder_picker）

![レコードプロデューサーの新規な変数1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/a21dd8cb-8abe-697d-1e95-3bcd158b63b0.png)

- 「タイプ仕様」タブの「ウィジェット」：作成したカスタムウィジェットを選択します（例：Boxフォルダピッカー）

![レコードプロデューサーの新規な変数2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/8bf8875d-6107-c370-411a-2d0c57af89c9.png)

本記事の実装が以上で、次に結果を確認します。

## 動作確認

これでContent Pickerがレコードプロデューサーのフォームに表示されます。そして、フォルダを選んだら、カスタムウィジェットが親フォルダIDのテキストボックスの値を変更します。

![親フォルダID変更.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/895687/5ea2f678-3230-6120-c049-e37783931109.gif)

フォームの他の項目に変更がありませんので、入力して送信すると、Box上のフォルダが作成され、コラボレーターが追加されます。

# まとめ

この記事では、ServiceNowにBox UI Elementsを埋め込む方法を紹介しました。

フォルダ選択だけではなく、Box UI Elementsは柔軟性が高く、ServiceNowの様々なプロセスで活用できます。