セキュリティに配慮しながらGASを書くTips


# 背景

職場で業務効率化のためによく書くGASですが、個人情報やパスワード情報に気を遣うことが多々あります。
どのGoogleアカウントと紐づいているのか、誰がソースコードを見られるのか、誰が実行したのか等々実にややこしい……。

今回はGASを書く中で自分なりに確立してきた手法をいくつか紹介していきます。

どれも既に有名かもしれませんが、集約することで組み合わせたり応用したりしてもらえると嬉しいです。

# この記事を読んで得られる知見

+ 重要なデータ（APIキーやID、パスワード）をスクリプトプロパティに格納して秘匿する
+ GASと使用するファイル（ドキュメントやスプレッドシート）を別々に作成し、閲覧権限を分けて管理する
+ ファイルの共有設定をGASから行う
+ GASを実行できるユーザーを制限する

# 環境・使用ツール

+ GASスクリプトエディタ（古い方の話も出てきます）
+ GitHub（[GAS GitHubアシスタント](https://chrome.google.com/webstore/detail/google-apps-script-github/lfjcgcmkmjjlieihflfhjopckgpelofo?hl=ja)を使うことで簡単にリポジトリを管理できます）

## 重要なデータをスクリプトプロパティに格納して秘匿する
例えば、下記のようにIncomingWebhookを使って、SlackBotにメッセージを送る場合……

```javascript
// Slack にメッセージを送るプログラム
function sendToSlack(body) {
　　　　// 送信するJSONデータ
  let data = {
    text: body,
  };
  let payload = JSON.stringify(data);
  // Slackに送信する情報
  let options = {
    'method': 'POST',
    'contentType': 'application/json',
    'payload': payload
  };
  let webHookURL = "https://hooks.slack.com/services/123456789/abcdefghijklmn";
  
  // Slackに投稿
  let response = UrlFetchApp.fetch(webHookURL, options);
}
```

fetchするURLをそのまま書いてしまいがちです。
このままだと、共有設定を誤ったりGitHubなどでコードを公開した時に、第三者がSlackにアクセスできるようになってしまいます。

ここで、スクリプトプロパティというスクリプトに紐づいた変数格納機能を使ってあげるとこの問題は解決します。


スクリプトプロパティを使うには、2つの方法があります。
例として、プロパティを`incomingURL`、値を`https://hooks.slack.com/services/123456789/abcdefghijklmn`として説明します。

（2023年11月7日更新：新スクリプトエディタでも設定できるようになりました）

~~+ **旧スクリプトエディタからGUIで設定する**~~

~~1. 普通に開けるスクリプトエディタは新しいものになるので、画面右上から`以前のエディタを使用`で旧スクリプトエディタを開く~~
~~1. **ファイル→プロジェクトのプロパティ→スクリプトのプロパティ→行を追加**でプロパティと値を入力する~~
~~1. `PropertiesService.getScriptProperties().getProperty("incomingURL")`で呼び出す~~

+ **新スクリプトエディタでの追加方法**
>スクリプト プロパティを追加する
Apps Script プロジェクトを開きます。
左側にある [プロジェクトの設定] プロジェクト設定のアイコン をクリックします。
最初のプロパティを追加するには、[スクリプト プロパティ] で [スクリプト プロパティを追加] をクリックします。
2 番目以降のプロパティを追加するには、[スクリプトのプロパティ] で [スクリプトのプロパティを編集] > [スクリプト プロパティを追加] をクリックします。
[プロパティ] にキー名を入力します。
[値] にキーの値を入力します。
（省略可）プロパティを追加するには、[スクリプト プロパティを追加] をクリックします。
[スクリプトのプロパティを保存] をクリックします。

https://developers.google.com/apps-script/guides/properties?hl=ja#manage_script_properties_manually

+ **スクリプトで設定する**

1. `PropertiesService.getScriptProperties().setProperty("incomingURL","https://hooks.slack.com/services/123456789/abcdefghijklmn")`で設定する（1回でOK）
1. `PropertiesService.getScriptProperties().getProperty("incomingURL")`で呼び出す


いずれかの方法でスクリプトプロパティを設定すると、前述の問題のコードをこのように書き換えることができます。

```javascript
let webHookURL = PropertiesService.getScriptProperties().getProperty("incomingURL");
```


:::note warn
上記のコードを一回実行することで設定できます。
そのまま置いておくと秘匿できないので、実行してプロパティ設定が終わったら消し忘れないようにご注意ください。
:::

## GASと使用するファイルを別々に作成し、閲覧権限を分けて管理する
GASは、ファイルと紐づいたContainer-bound Scriptsと、独立して作成されるStandalone Scriptsの２種類があります。
Container-bound Scriptsだと権限も共有してしまうため、「ファイルは見られてもいいがスクリプトは見られたくない」という場合に不自由です。
そこを解決できるのがStandalone Scriptsです。

実装は簡単です。
ドキュメントやスプレッドシートからGASを作成せずに、GoogleDriveで**新規→その他→Google Apps Script**から作成します。

そうすることで、ファイルとGASそれぞれの閲覧権限を別々に設定可能になります。
GASからファイルを呼び出す際は、下記のように書けば大丈夫です。（例はスプレッドシートの場合）

```javascript
// DriveIDから呼び出し
const spreadsheetId = "XXXXXXXXXXXXXXXXXXXXXXXX";
const sheet = SpreadsheetApp.openById(spreadsheetId).getActiveSheet();

// URLから呼び出し
const spreadsheetURL　= "https://XXXXXXXXXXXXXXXX";
const sheetName = "XXXXXXX";
const sheet = SpreadsheetApp.openByUrl(spreadsheetURL).getSheetByName(sheetName);
```

この方法とスクリプトプロパティを組み合わせるだけでも、十分安全に管理できると思います。

## ファイルの共有設定をGASから行う
ファイルの共有先を管理する時も気をつける必要があります。
不要な相手に権限を与えてしまうと、閲覧のみならず編集されてしまうことも起こり得ます。

手動でも設定できますが、GASで設定することでヒューマンエラーを防げます。

Protectionクラスを使います。

ユーザーは基本的にメールアドレスで管理します。

`addViewer`で閲覧権限を追加、`removeViewer`で閲覧権限を剥奪、
`addEditor`で編集権限を追加、`removeEditor`で編集権限を剥奪です。

「ドメイン」というグループ単位でも権限を付与できますが、そちらは`setDomainEdit(true/false)`で管理します。

試しに、スプレッドシート（`sheet`）の現在の編集権限保持者を全て削除して、自分のみに権限を付与してみましょう。
ドメインかメールアドレスどちらかで権限がある場合は編集可能になるため、ドメインの権限も削除します。


```javascript
let me = Session.getEffectiveUser() // 編集中（自分）のメールアドレスを取得
let protection = sheet.protect(); // シートのProtectionインスタンスを生成

let nowEditors = protection.getEditors(); // 現在の編集権限保持者を取得
protection.removeEditors(nowEditors); // 現在の編集権限保持者から権限を削除

protection.addEditor(me); // 自分のみ権限を付与
    
//ドメインに編集権限が付いてる場合は削除
if (protection.canDomainEdit()) {
   protection.setDomainEdit(false);
}

```

:::note warn
DriveAppクラスからこちらのメソッドを実行すると、共用設定の通知が全員に行き渡ってしまいます
本記事のようにSpreadsheetApp、DocumentAppから呼び出す分には通知が行きません（現状）
:::

## GASを実行できるユーザーを制限する
ファイルからボタンで実行するタイプのGASの中には、実行可能なユーザーを指定したい場合もあります。
そんな時は、実行可能なメールアドレスをリストにして、GASの実行した際に取得したメールアドレスと照合することで解決できます。

```javascript
// 実行可能なメールアドレスをリストにする
const canDoList = ["aaa@bb.com", "ccc@dd.com", "eee@ff.com"];

const activeUser = Session.getEffectiveUser(); // スクリプトを実行するユーザーのメールアドレス
// もし実行可能なユーザーだったら
if (canDoList.includes(activeUser)) {
  // 処理を書く
}
```

:::note warn
GASとしてそのまま実行してもメールアドレスの取得はできません。
何かしらのファイルと紐づけたGASを実行する場合に限ります。
:::

# 最後に
GASはGoogleサービスを扱う上でとても便利で使いやすいです。
一方で、セキュリティや実行速度、スクリプトの処理時間などその分落とし穴も多くなってきます。
メリットデメリットを把握した上で使えるようにしていきたいです。

# 参考資料

+ 公式ドキュメント：https://developers.google.com/apps-script
+ スクリプトプロパティについては：https://developers.google.com/apps-script/guides/properties?hl=ja#manage_script_properties_manually