Box API Java SDK でサービスアカウント → ユーザーにフォルダー共有する
まとめ記事ができました
このスクラップについて
Box 連携するアプリとユーザーの間でフォルダーを共有したい。
ユーザー認証(OAuth 2.0)を使えばユーザーの代理アクセスができるがユーザーによる設定が必要となる。
具体的にはアプリを承認(Box では authorization / 認可を「承認」と呼んでいる)してもらう必要がある。
サーバー認証を使えば管理者がユーザーの代わりにアプリを認可できる。
as-user ヘッダーを使うことでユーザーの代理アクセスもできる。
しかし、アプリの権限がかなり強大になるので安全性に懸念がある。
もう少しライトな代替手段としてフォルダー共有(Box では「コラボレーション」と呼ばれる)がある。
フォルダー共有はユーザーとアプリ(厳密にはサービスアカウント)の間でも可能であり、過去にユーザー → アプリへのフォルダー共有には成功した経験がある。
しかし、アプリ → ユーザーへのフォルダー共有はまだやったことがない。
このスクラップでは Kotlin から Box API Java SDK を使ってアプリ → ユーザーへのフォルダー共有ができるかどうかを試してみようと思う。
アプリ → ユーザーにフォルダー共有する場合、ユーザーにとっては招待メールに含まれるリンクにアクセスするだけなので設定が楽になりそう。
関連スクラップ
過去に作成した Box 関連のスクラップは下記の通り。
いつの間にかたくさん書いていたことに自分でも驚いた。
関係ないけど
自分がたくさん並んでいてちょっとシュール。
カスタムアプリ作成から始める
前に作成したカスタムアプリを流用することもできるが設定手順の記憶があやふやになっていきているので復習を兼ねてカスタムアプリ作成から始めようと思う。
カスタムアプリ作成
開発者コンソールのマイアプリページで「アプリの新規作成」ボタンを押す。
「カスタムアプリ」を選ぶ。
アプリ名(例:SA2User)と目的(例:自動化)を入力してから「次へ」ボタンを押す。
認証方法として「サーバー認証(クライアント資格情報許可)」を選んでから「アプリの作成」ボタンを押す。
無事にアプリが作成された。
OAuth 2.0 資格情報の取得
カスタムアプリ構成タブのOAuth 2.0 資格情報セクションから取得する。
「クライアントシークレットを取得」ボタンを押すと 2 段階認証ページが表示される。
認証コードを入力してから「送信」ボタンを押すと元のページに戻るので再度「クライアントシークレットを取得」ボタンを押す。
プロジェクト作成
IntelliJ IDEA を起動して新規プロジェクトを作成する。
jdkVersion の修正
build.gradle.kts を開いて kotlin > jvmToolchain の jdkVersion を 11 → 8 に変更する。
kotlin {
jvmToolchain(8)
}
Box API Java SDK の追加
build.gradle.kts を開いて dependencies セクションに依存関係を追加する。
dependencies {
testImplementation(kotlin("test"))
implementation("com.box:box-java-sdk:4.1.0")
}
依存関係を追加したら Gradle ツールウィンドウの左上のリロード 🔃 ボタンを押す。
環境変数の追加
Run / Debug 構成を編集して下記 3 点の環境変数を追加する。
- BOX_CLIENT_ID
- BOX_CLIENT_SECRET
- BOX_ENTERPRISE_ID
Enterprise ID はカスタムアプリ一般設定タブで確認できる。
コーディング
フォルダーを作成してみる。
import com.box.sdk.BoxCCGAPIConnection
import com.box.sdk.BoxFolder
fun main(args: Array<String>) {
val clientID = System.getenv("BOX_CLIENT_ID")
val clientSecret = System.getenv("BOX_CLIENT_SECRET")
val enterpriseID = System.getenv("BOX_ENTERPRISE_ID")
val api = BoxCCGAPIConnection.applicationServiceAccountConnection(
clientID,
clientSecret,
enterpriseID,
)
val rootFolder = BoxFolder.getRootFolder(api)
rootFolder.createFolder("Folder to be shared")
}
この時点で実行してみる
まだアプリの承認すらしていないので下記の例外が発生した。
Exception in thread "main" com.box.sdk.BoxAPIResponseException: The API returned an error code [400] invalid_grant - Grant credentials are invalid
承認していない場合は 400 Bad Request エラーになるようだ。
カスタムアプリ承認
管理コンソールのカスタムアプリマネージャページで「アプリの追加」ボタンを押す。
クライアント ID を入力してから「次へ」ボタンを押す。
情報を確認してから「承認」ボタンを押す。
カスタムアプリが承認された。
この状態で実行する。
例外が変化した、権限設定を行なっていないのでこれは期待通り。
Exception in thread "main" com.box.sdk.BoxAPIResponseException: The API returned an error code [403 | .06e4ae6ed24930073215d89d53503a9cf]
403 であることから権限まわりのエラーであることが推測される。
アプリケーションスコープ追加
開発者コンソールのカスタムアプリ構成タブでアプリケーションスコープセクションに含まれる「Boxに格納されているすべてのファイルとフォルダへの書き込み」にチェックを入れてから「変更を保存」ボタンを押す。
カスタムアプリ再承認
スコープを追加した後は管理コンソールから再承認が必要になる。
カスタムアプリマネージャでメニューボタンを押して「アプリを再承認」を押す。
この際、アプリケーションスコープが反映されていないことがあるので注意。
一度「キャンセル」ボタンを押してアプリの詳細を表示する。
カスタムアプリマネージャに戻って同じ操作を行い、反映されたことを確認してから「再承認」ボタンを押す。
無事に再承認された。
不安な場合はアプリの詳細を表示して確認する。
この状態で実行してみる
特にエラーが発生せずに実行を完了できたのでフォルダーが作成されたようだ。
不安な場合は再実行するとフォルダー名の重複で例外が発生することで確認できる。
Exception in thread "main" com.box.sdk.BoxAPIResponseException: The API returned an error code [409 | smugmshdgc272ag3.0f6c1a187f12b4ddd36b8a1b270b18a58] item_name_in_use - Item with the same name already exists
今日はここまで
ここまでは準備だった。
次回以降は実際にフォルダー共有を試してみよう。
今日はここから
まずはコラボレーション関係の API を調べよう。
コラボレーション追加
フォルダー ID とユーザー ID が必要になる。
BoxCollaborator user = new BoxUser(api, "user-id")
BoxFolder folder = new BoxFolder(api, "folder-id");
folder.collaborate(user, BoxCollaboration.Role.EDITOR);
ユーザー ID の代わりにメールを指定することもできる。
BoxFolder folder = new BoxFile(api, "id");
folder.collaborate("gcurtis@box.com", BoxCollaboration.Role.EDITOR);
フォルダー ID 取得
import com.box.sdk.BoxCCGAPIConnection
import com.box.sdk.BoxFolder
fun main(args: Array<String>) {
val clientID = System.getenv("BOX_CLIENT_ID")
val clientSecret = System.getenv("BOX_CLIENT_SECRET")
val enterpriseID = System.getenv("BOX_ENTERPRISE_ID")
val api = BoxCCGAPIConnection.applicationServiceAccountConnection(
clientID,
clientSecret,
enterpriseID,
)
val rootFolder = BoxFolder.getRootFolder(api)
for (info in rootFolder) {
println("%s %s".format(info.id, info.name))
}
}
123456789012 Folder to be shared
フォルダー ID は環境変数 BOX_FOLDER_ID として登録しておく。
環境変数の追加
ついでに BOX_USER_EMAIL という名前でコラボレーション先のユーザーのメールアドレスを登録しておく。
コーディング
import com.box.sdk.BoxCCGAPIConnection
import com.box.sdk.BoxCollaboration
import com.box.sdk.BoxFolder
fun main(args: Array<String>) {
val clientID = System.getenv("BOX_CLIENT_ID")
val clientSecret = System.getenv("BOX_CLIENT_SECRET")
val enterpriseID = System.getenv("BOX_ENTERPRISE_ID")
val folderID = System.getenv("BOX_FOLDER_ID")
val userEmail = System.getenv("BOX_USER_EMAIL")
val api = BoxCCGAPIConnection.applicationServiceAccountConnection(
clientID,
clientSecret,
enterpriseID,
)
val folderToBeShared = BoxFolder(api, folderID)
folderToBeShared.collaborate(userEmail, BoxCollaboration.Role.EDITOR)
}
この時点で実行する
下記の例外が発生した。
Exception in thread "main" com.box.sdk.BoxAPIResponseException: The API returned an error code [400] invalid_client - The client credentials are invalid
もう少し強い権限設定が必要なのかな?
権限の変更
開発者コンソールからアプリアクセスレベルを変更する。
管理コンソールからカスタムアプリを再承認する。
この状態で実行してみる
結果は変わらなかった。
Exception in thread "main" com.box.sdk.BoxAPIResponseException: The API returned an error code [400] invalid_client - The client credentials are invalid
ユーザー管理権限を増やしてみる
しっかり確認してから再承認する。
この状態で実行する
結果は変わらなかった。
Exception in thread "main" com.box.sdk.BoxAPIResponseException: The API returned an error code [400] invalid_client - The client credentials are invalid
原因はしょうもないことだった
環境変数を入力するときに誤操作があったのか BOX_CLIENT_ID が BOX_CLIENT_IDs という名前になっていた。
権限を元に戻して試してみる
下記の状態の最も弱い権限でも正常に終了した。
メールを確認してみる
下記のようなメールが届いていた。
通知も来ている
ルートフォルダーにも追加されている
共有フォルダーにファイルを追加してみる
import com.box.sdk.BoxCCGAPIConnection
import com.box.sdk.BoxFolder
fun main(args: Array<String>) {
val clientID = System.getenv("BOX_CLIENT_ID")
val clientSecret = System.getenv("BOX_CLIENT_SECRET")
val enterpriseID = System.getenv("BOX_ENTERPRISE_ID")
val folderID = System.getenv("BOX_FOLDER_ID")
val api = BoxCCGAPIConnection.applicationServiceAccountConnection(
clientID,
clientSecret,
enterpriseID,
)
val sharedFolder = BoxFolder(api, folderID)
val fileContent = "It works!"
val inputStream = fileContent.byteInputStream(Charsets.UTF_8)
val fileName = "File to be shared"
sharedFolder.uploadFile(inputStream, fileName);
}
動作確認
プログラムを実行してから Box Web UI で Folder to be shared の中に File to be shared があることを確認する。
内容はダウンロードすると確認できる
ファイル名を File to be shared.txt とかにすれば良かった。
先ほどは言及しなかったが
コラボレーションを作成した時にユーザーの方では特に何もすることなくフォルダー共有が行われていた。
ユーザーの方でメール記載のリンクをクリックして承認とか必要なのかと思っていたがそれすらないようだ。
無事に検証できた
サービスアカウント → ユーザーへのフォルダー共有を確認することができた。
必要となる権限も少ないのでユーザー代理アクセスよりもおすすめかも知れない。
まとめ
- フォルダー共有するにはフォルダー ID とユーザーのメールアドレスが必要になる。
- 未検証だがユーザーのメールアドレスはユーザー ID で代替できるようだ。
- 必要なアプリアクセスレベルは「アプリアクセスのみ」で大丈夫。
- 必要なアプリケーションスコープは「書き込み」で大丈夫。
- フォルダー共有を行うと即座に承認される、ユーザーの方で承認操作は必要ない。
- ユーザーへの通知はメールと Web UI の両方から来る。
おわりに
先ほども言及したがユーザーとファイルをやり取りするために必要となる権限がユーザー代理アクセスよりもはるかに弱いので安全性が高い方法だと感じた。
Box 連携アプリを開発する場合はこちらの方法を使う方がベターだと感じた。
必要となるのもユーザーのメールアドレスだけなのでユーザー ID を調べなくて良いのはとてもありがたい。