「自分」で実行する Web アプリで、Google が守ってくれる範囲
Web アプリを「自分」として実行する設定で、広く公開したとします。 開いた人に設置者の Drive を覗かれることはありません。ただ、Google が止めてくれるのは入口までで、入口の先はコードの書き方しだいです。
「自分」で実行する Web アプリは、誰が開いても、デプロイした人の権限で動きます。設置者が承認した権限を、開いた人がアプリ越しに借りている形です。
そう聞くと不安になりますが、開いた人が設置者のアカウントを直接触れるわけではありません。触れるのは、アプリが用意した関数を通したときだけです。気をつけるのは、その関数がどこまでできてしまうかです。
Google が受け持っているところ
サーバー側のコード(.gs)は、画面には送られません。中に書いたスプレッドシートやフォルダの ID も、戻り値や HTML に入れない限り、開いた人からは見えません。
画面の HTML は、サンドボックスの iframe の中で動きます。開いた人のブラウザで動くのは、doGet が返した HTML とスクリプトだけです。
ID が知られても、それだけでファイルが開けるわけではありません。Drive のファイルは、共有の設定で許された人しか開けないからです。危ないのは、「リンクを知っている全員」に共有しているファイルです。こちらは ID がわかれば開けてしまいます。
公開している関数は、誰でも好きな引数で呼べる
google.script.run から呼べるのは、トップレベルで宣言した関数のうち、名前が _ で終わらないものすべてです。公式のガイドには、画面で一度も呼んでいない関数でも、ソースを調べた人には名前がわかる、と書かれています。
呼ぶ側は、ブラウザの開発者ツールから、好きな引数を付けて呼べます。画面のボタンを管理者にだけ出していても、関数そのものは誰からでも呼べる状態です。
そして呼ばれた関数は、設置者の権限で動きます。画面から来た値を信じて動く関数は、設置者の権限を誰にでも貸し出す窓口になります。
受け取った ID を、そのまま使わない
添付ファイルを消す関数を、こう書いたとします。
function deleteAttachment(fileId) {
DriveApp.getFileById(fileId).setTrashed(true);
}
画面からは、添付ファイルの ID しか渡さないつもりです。ところが呼ぶ人は、設置者の Drive にあるどのファイルの ID でも渡せます。ファイルをゴミ箱に入れる DriveApp の操作は、drive(Drive 全体を読み書きする)スコープで承認されています。設置者の権限の範囲内なので、Google はこれを止めません。
画面には、アプリの中だけで通じる番号を渡します。Drive の ID は、サーバー側の表にだけ控えておきます。
function deleteAttachment(pageId, attachmentId) {
const row = findAttachment_(pageId, attachmentId); // 自分の表から、そのページの添付を探す
if (!row) throw new Error('添付が見つかりません');
DriveApp.getFileById(row.driveId).setTrashed(true);
removeAttachmentRow_(row);
}
findAttachment_() と removeAttachmentRow_() は、添付の目録を読み書きする自分の関数に置き換えてください。表にない組み合わせは、Drive に触る前に止まります。名前が _ で終わっているので、この2つは画面から直接は呼べません。
ページを見てよい人かどうかも、同じ関数の中で確かめます。目録に載っていても、見る権利のないページの添付は消させません。
エディタ用とトリガー用の関数も、窓口になる
エディタから1回だけ実行してもらう設定用の関数や、トリガーから動かす関数も、名前が _ で終わらなければ画面から呼べます。画面で使っていないので、うっかり見落としやすいところです。
毎朝お知らせのメールを送る関数を、誰かが画面から何度も呼んだとします。そのたびにメールが送られ、設置者の1日の送信枠が減っていきます。
時間主導型トリガーから動いたときは、イベントの triggerUid に、そのトリガーの ID が入っています。登録したトリガーの ID と一致したときだけ動くようにしておけば、画面から呼ばれても何もしません。
function sendDailyDigest(e) {
const trigger = ScriptApp.getProjectTriggers()
.find((t) => t.getHandlerFunction() === 'sendDailyDigest');
if (!trigger || !e || String(e.triggerUid) !== trigger.getUniqueId()) return; // トリガー以外から呼ばれた
sendDigest(); // 本来の処理
}
'sendDailyDigest' はこの関数自身の名前、sendDigest() は本来の処理に置き換えて使います。トリガーの ID は画面には渡していないので、呼ぶ側は当てられません。
ただ、triggerUid は公式には「イベントを起こしたトリガーの ID」としか説明されていません。秘密の鍵として扱える保証はないので、メールを送るような処理には「1日1回まで」のような歯止めも足しておくと安心です。送った日付を PropertiesService に控えておき、同じ日なら何もせずに終わらせます。
エディタ用の関数は、設置者が実行したときだけ動くようにします。「自分」で実行する Web アプリでは、設置者本人以外が開いても Session.getActiveUser().getEmail() は基本的に空です。エディタで設置者が実行したときはアドレスが取れるので、それを設置者のアドレスと比べます。見分け方は誰が開いているかを見分ける記事にまとめています。
戻り値に入れたものは、すべて見える
戻り値は、画面に表示していない項目も含めて、そのまま開いた人のブラウザに届きます。開発者ツールを開けば読めます。
表の行をそのまま返すと、表示に使っていない列まで渡ります。Drive の ID、メールアドレス、表の行番号のようなものです。画面に返すときは、返す項目を決めた形に詰め直してください。
function attachmentView_(row) {
return { id: row.id, name: row.name, size: row.size }; // driveId は入れない
}
戻り値を作る関数を1つ決めておくと、あとから列が増えても、勝手に画面へ出ていくことはありません。
呼ばれるたびに、設置者の枠が減る
呼ばれた関数が使う枠は、すべて設置者のものです。連打されると、設置者が自分で使う分まで減ります。
| 枠 | 個人のアカウント | Workspace |
|---|---|---|
| メールを送れる宛先の数 | 1日100件 | 1日1500件 |
| トリガーで動く合計 | 1日90分 | 1日6時間 |
| 1回の実行 | 6分 | 6分 |
メールを送る関数や重い処理には、回数の上限を付けておくと安心です。CacheService に呼んだ回数を数えておき、一定の時間内に決めた回数を超えたら断る、という形で作れます。
公開する前に、窓口を数える
コードの中から、トップレベルにあって名前が _ で終わらない関数を全部書き出します。それがそのまま、誰でも呼べる窓口の一覧です。
1つずつ、誰が呼んでよいか、受け取った値を何と突き合わせているか、何を返しているかを確かめます。画面で使っていない関数が出てきたら、名前の末尾に _ を付けるか、中で呼んだ人を確かめてください。
書かなくても動く状態のコードを、下のツールで配っています。
社内wiki手順書や議事録を1か所にまとめます。契約も月額もなく、データは自分の Google ドライブの中だけに置かれます。