Google Apps Scriptで一部だけをユーザ権限で動かしたい時は?

Google Apps Scriptでウェブアプリケーション等を動かそうとした場合に必ずぶつかる問題が「実行権限とアクセス権限、そしてアクセストークン」の処理です。とりわけ、Google Picker APIを動かす場合や、カレンダーを操作するなんて場合になかなか厄介な問題点として立ちはだかります。通常のウェブアプリを作ってる分には困らないのですが、この問題点を解消する為の手段を検証してみたいと思います。

過去にもトリッキーな方法でこれを実現しようとしたパターンがありました。が、結構複雑になりがちなので、今回の手法で解決できるか?挑戦してみます。このテクニックはかなり広範囲且つ、これまでのGASでは難しかった問題点を突破できるテクニックになります。

Google Apps Scriptでアクセスしてるユーザを元に処理をする方法【GAS】

今回利用するスプレッドシート等

今回の手法は、GCP側に用意したサービスアカウントを用いて間接的にアクセスしてきてるユーザのアクセストークンを取得し、そのトークンを持ってしてGASではなくGoogleのAPIを叩いて操作するという手法になります。

GCP側での準備やライブラリの追加が必要になります。今回のサンプルでは、GAS作成者の権限で実行する形でウェブアプリをデプロイしていただき、別のユーザでアクセスさせてGoogle Pickerの画面がユーザ毎の画面になるかどうか?がポイントになります。Google Pickerについては以下のエントリーを参考にしてみて下さい。

Pickerでファイルやフォルダを選択する画面を装備する

問題点の内容

Google Apps Scriptでウェブアプリケーションを動かしたい場合に遭遇する問題が「誰の権限で動かし、どのファイルにアクセスするか?」といった問題点があります。これを簡単にここにまとめてみたいと思います。

デプロイした時の実行権限

ウェブアプリケーションをデプロイする場合、2パターンの実行権限付与を求められます。

  1. GAS作成者の権限で動作させる
  2. ユーザ自身の権限で動作させる

通常は1.の権限で動かすのが良いのですが、ケースによってはユーザの権限で動かしたい場合があります。前者の場合、特定のAdmin権限を要求するようなメソッドをユーザがGAS作成者の権限を通して利用が出来るというメリットがあり、またユーザに触らせたくないファイルへも部分的にGASで許容した範囲で処理が出来ます(ファイルを完全非公開でも処理の過程でアクセスが可能になる)

両方のいいとこ取りが実現出来るのがこのコードです。

権限と課題

例えば、前述で1.の作成者の権限で動かした場合に問題となるのが

  • Google Pickerなどを使って見えるファイルが、GAS作成者の権限で見える範囲となってしまうので、見せたくないものが見えたり、本来ユーザのファイルを選ばせたくても、共有されていなければ見えない
  • APIの実行についても、GAS作成者の権限で動かしてるので、カレンダーの予定などを追加するものを作ると、GAS作成者のカレンダーに追加されてしまう(本来はアクセスしてるユーザのカレンダーに追加したいのに)
  • かといって、ユーザ権限で動かすとなると、見せたくない管理ファイルなどが自由にアクセス可能になってしまう(アクセス可能にしておかないとGASが実行できない)
  • また、同じくユーザ権限の場合、Admin SDKなどの管理者権限を要求するメソッドは動かすことが出来なくなってしまう

ジレンマというか痛し痒しというか、この問題点にぶつかり、如何ともしがたい状態になってしまいます。結局妥協して運用で誤魔化すといったような事が必要になり、旨味が激減してしまうのです。

解決策

通常のGASのメソッドを動かす場合は、デプロイした時の権限で動作する為、例えばScriptApp.getOAuthToken()でアクセストークンを取得した場合、作成者の権限で動かしていると、誰がアクセスしても、作成者のAccess Tokenが取得されます。同様に各種GASのメソッドを実行する場合も同じです。

よって、これを解決するには以下の要件を満たす必要があります。

  • デプロイし実行する権限は作成者の権限で動かしたい
  • 但し、一部の作業はユーザの権限で動かしたい(Google Pickerの表示内容や実際のカレンダーの操作等)

これらの要件を満たすには

  1. 何らかの手法を用いて、作成者の権限で動かしてるスクリプト中で、アクセスしてきてるユーザのAccess Tokenを取得する
  2. そのTokenを用いて、GASのメソッドではなくDrive APIやCalendar APIを用いて処理を実装する
  3. 出来れば、実行時にOAuth2.0認証などの画面が出ないままスムーズに実行できると尚良い(トークン管理が必要になってしまうため)

今回の課題は、上記の1.の何らかの手法を用いて、作成者権限で動かしてる中でユーザのAccess Tokenを取得するという、なんともトリッキーに見える手法です。

注意点

この手法はGCPのサービスアカウントを利用してアクセストークンを取得する方法ですが、同一ドメイン内のユーザであればIAMにユーザ登録等の作業をせずともデプロイしたURLにアクセスすれば利用することが可能です。しかし、社外のメンバーや特に普通のGoogleアカウントの場合は利用することができません(というより、もともとGASのアプリは大概的に利用出来るように作られていない)。

社外用として利用する場合は、GASではなく通常のHTMLにて記述したものをWebサーバで公開し、GASのApps Script APIを使って叩くなどの別の仕組みが必要になります。

また、一部をユーザ権限で動かすといっても例えばDriveやCalendarの操作で使う標準のGASのメソッドは常時デプロイしたユーザの権限で動いてしまうので使えません。Access Tokenを利用してDrive APIやCalendar APIなどをUrlfetchAppでREST APIを叩く必要があります。

事前準備

サービスアカウントを用意する

直接的にユーザのアクセストークンを自分の権限で実行中のスクリプトから取得するのは出来ません。そこで利用するのが「サービスアカウント」。以下のエントリーのサービスアカウントを作成するにて作成し、

  • APIアクセス用のPrivate Keyの値
  • サービスアカウントのメールアドレス
  • クライアントID
  • アクセスしてきてるユーザのメールアドレス

の4点を用意する必要があります。エントリーを参考にサービスアカウントを作成し、鍵を追加、JSON形式でダウンロードすると上記の内のユーザのメールアドレス以外が全て記述されているので、その内容を他のユーザがアクセスできない形でロードするようにします(GASで非公開のJSONファイルにアクセスするのでも良いし、ユーザが直接アクセスできないスクリプトプロパティに格納するのも良し)

今回は便宜的にコード内に記述しますが、安全に値が取れるように配慮が必要ですし漏洩しないように最大限の注意が必要です。

※GCP側のIAMに全ユーザを登録する必要はありません。あくまでもGAS作成者のアカウントがあれば問題ないです。

図:サービスアカウントのJSONデータの取得が必要

Google Cloud Consoleを弄ってみる

Picker APIのキーを取得する

今回のサンプルでは、Google Picker APIでファイルを選択する画面を利用しています。その為、このAPIで利用する為のAPI Keyを取得しておく必要があります。取得したらコード内に記述してロード時に利用します。前述で紹介のエントリーの「APIを有効にする」の項目で取得手順が掲載されています。

API keyは漏洩したり第三者から見えるような場所には配置しないようにしましょう。また、利用するに当たってはGoogle Picker APIに限定しておくことも忘れずに。

図:Google Picker APIを有効化して認証キーを作成

管理コンソールでの作業

Google Workspaceの管理コンソールでも作業が必要になります。

  1. 管理コンソールの「セキュリティ」⇒「アクセスとデータ管理」⇒「APIの制御」⇒「ドメイン全体の委任」を開きます
  2. APIクライアントの新規追加をクリック
  3. クライアントIDに、前述のJSONファイル内にあるclient_idを追加する
  4. Scopeには、「https://www.googleapis.com/auth/drive」を追加する

これで、Google Workspace内で利用が可能になります。これを行っておかないと、「Error: Access not granted or expired. at Service_.getAccessToken」というエラーが出て、サービスアカウントの初回認証が失敗します。

図:管理コンソールでAPI利用の許可をする

ライブラリを追加する

GASからサービスアカウントにアクセスして処理をする為の認証用ライブラリとして、以下の手順でOAuth2 library for Google Apps Scriptライブラリを追加しましょう。

  1. スクリプトエディタを開きます。
  2. サイドバーよりより「ライブラリ」の+ボタンをクリック
  3. ライブラリを追加欄に「1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF」を追加します。
  4. 今回はバージョンは43を選択してみます。
  5. 保存ボタンを押して完了

これで、OAuth2.0認証にまつわる様々な関数を手軽に利用できるようになります。

図:ライブラリを追加した様子

ウェブアプリケーションのデプロイ

ここまで準備が整いましたら、最期にウェブアプリケーションとしてデプロイを行います。ただし、1つ注意点もあります。以下の手順でまずはウェブアプリケーションとしてデプロイを行いましょう。デプロイする前に適当に関数を手動実行して認証し、使えるようにしておきましょう。

スプレッドシートはあくまでも管理者が使う為だけのもので、ユーザが利用するのはGASで生成したウェブアプリの側を利用します。ウェブアプリケーションをデプロイをします。

  1. スクリプトエディタを開く
  2. 右上のデプロイをクリック
  3. 新しいデプロイをクリック
  4. 種類の選択ではウェブアプリを選択し、「次のユーザとして実行は自分」にしておきます(管理者権限で動作しますが、スプレッドシートを公開する必要はありません)
  5. アクセスできるユーザは、社内公開する必要があるので「ドメイン内組織内全員」としておきます
  6. 末尾がexecで終わるURLが発行される。これがウェブアプリケーションのページとなります。
  7. 次回以降コードを編集して再デプロイ時はデプロイを管理から同じURLにて、新しいバージョンを指定して発行することが出来ます。

GASのウェブアプリですので、複数アカウントでログインした状態の場合、403エラーで表示されないこともあるので、基本は1プロファイル1アカウントでログインしての運用が必要です。

図:組織内の全員が使えるようにしておきます。

ソースコード

GAS側コード

メインのコード

  • 冒頭はサービスアカウント生成時に取得したJSONファイル内のPrivate keyやClient ID、サービスアカウントのメアドなどを入れる
  • private_keyは「BEGIN PRIVATE KEY」といった文字列まで含めて全部入れておいて良いです。
  • またPicker API用のAPI Keyのコードも入れておく
  • GetUser関数で現在アクセスしてきてるユーザのメアドを取得可能です。
  • GetUserToken関数がメインの処理を行う関数で、複数名同時にアクセスしてくる可能性があるため、排他制御を入れている。
  • ユーザがアクセスするとまず、reset関数でスクリプトプロパティの認証情報をクリアしてから、再度サービスアカウントを持ってAccess Tokenを取り直している(ここがとても重要で、resetしないと前のユーザの認証情報が出てきてしまう。できればトークン取得直後にresetを入れた方が良い)
  • getOAuthService関数がサービスアカウントを叩いて、ユーザ単位のアクセストークンを取得して返す関数です。
  • setSubjectにアクセスしてきてるユーザのメアドを入れることで、その人のAccess Tokenが取得できるようになります。
  • GASの各種メソッド自体はあくまでもデプロイした人間の権限で動いてしまうので、SpreadsheetやCalendarを操作したい場合は、UrlfetchAppを使ってGoogle APIを叩きに行くようにしましょう。

この処理によって、ウェブアプリケーションはGAS作成者の権限でデプロイしつつ、特定のAPIを叩く時にだけユーザ単位のAccess Tokenを取得して個別のGoogle Picker APIのファイル選択画面を出すことが可能です。もちろん、同じトークンを使ってユーザレベルでGoogle Drive APIを叩いたりといったことも可能です。

排他制御でGoogle Apps Scriptを安全に実行【GAS】

ウェブアプリ用コード

こちらは、ウェブアプリケーションの出力と、ウェブアプリ側へTokenやAPIキーなどを返却するだけのコードです。

HTML側コード

こちらは単純にGAS側へAPIキーやユーザのアクセストークンをリクエストして、Pickerの画面を出力するだけです。但し、GAS作成者の権限でデプロイしていても、各ユーザのアクセス可能な範囲でのPicker表示が実現できています。

図:自分のファイルしか見えないよ

応用事例

カレンダー情報を操作

前述の事例はGoogle Picker APIを叩く際のTokenとして今回の手法を使ってユーザのピッカー画面を出していました。もう一つ身近な事例として、カレンダーに本日の勤務場所を登録するみたいな場合に於いて、CalendarAppではなくUrlfetchAppでCalendar API v3を叩き、ユーザのカレンダーの勤務場所に設定するといったものを作ってみました。

そもそも勤務場所の読み書き用API自体が比較的新しくリリースされたばかりのものなのですが、ばっちり動作しました。これまではこの処理を通常のCalendarAppで行ってしまうと、デプロイしたユーザのカレンダーに追加されてしまっていたので見事に両立できています。

※setScopeやドメイン全体の委任でのスコープはカレンダーなので、「https://www.googleapis.com/auth/calendar」を指定する必要があります。

  • UrlfetchAppでのBearer Tokenでユーザのトークンを呼び出す処理に変更しています。
  • CalendarIdはGetUserでユーザのメアドを取得し、それをもってエンドポイントURLとしています。
  • イベントデータとして勤務場所をカレンダーに記録するプロパティをセットしています。
  • 日付形式はyyyy-MM-ddThh:mm:ss+09:00の形式で指定が必要なので専用の関数を用意しています。

Google Driveを操作

Drive API v3を利用してユーザの権限でアクセスできるGoogle Driveの範疇に絞って共有ドライブのリストを返したり、そのメンバー情報を取得してみようと思います。通常はデプロイした人の権限なので管理者だと全部返してしまうのですが、この手法を使う事でユーザ単位で異なるリストを返すことが可能になります。

※setScopeやドメイン全体の委任でのスコープはドライブなので、「https://www.googleapis.com/auth/drive」を指定する必要があります。

共有ドライブの一覧を取得

  • getUserTokenにてユーザ単位のアクセストークンを取得します。
  • サービスではなくREST APIのDrive APIを利用する必要があるので、UrlfetchAppにてGETにてリクエストをします。
  • エンドポイントURLにクエリパラメータとしてpageSize, pageToken, useDomainAdminAccessの3つを指定してリクエストを行います。
  • その後の処理についてはサービスのDrive APIとほぼ変わらないですが、レスポンスデータがv3ではちょっと異なる点がある(レスポンスを取得する部分が、sharedDrives.drivesで取得する必要がある。Drive API v2だとitemsとなってる)
  • またレスポンスデータはJSON.parseする必要があるので注意。
共有ドライブのメンバーを取得

  • リクエストするスタイルは前述のドライブリスト一覧とほぼ同じ
  • エンドポイントはhttps://www.googleapis.com/drive/v3/files/{fileId}/permissionsのスタイルにGETリクエストする必要がある
  • エンドポイントURLにクエリパラメータとしてpageSize, pageToken, useDomainAdminAccess, supportsAllDrives, fieldsの5つを指定してリクエストを行います。
  • 公式ドキュメントに記載が無いのですが、クエリパラメータでfileds=*を指定しないと、emailAddressなどの他の要素がDrive API v3では出てきません。必ずつけるようにしましょう。
  • その後の処理についてはサービスのDrive APIとほぼ変わらないですが、レスポンスデータがv3ではちょっと異なる点がある(レスポンスを取得する部分が、pList.permissionsで取得する必要がある。Drive API v2だとitemsとなってる)
  • またレスポンスデータはJSON.parseする必要があるので注意。

関連リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)