【Google Apps Script】その23　パスワード管理ツールを作る

この記事は[Google Apps Scriptを実例交えて基礎からざっくり学ぶ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/gas_beginner)　23日目の記事です。

本アドベントカレンダーは@rt_pの個人プロジェクトですが、筆者は[Ateam Brides Inc. Advent Calendar 2017](https://qiita.com/advent-calendar/2017/a-t-brides)にも参加しています。そちらでも出張版記事を書いているので、覗いていただけると嬉しいです。

# はじめに

社内でのパスワード管理、どうしていますか？
管理方法の一例を挙げてみます。

**1. 全て同じパスワードを使用（論外）**
メリット：管理がしやすい、覚えやすい、楽
デメリット：1回漏洩すると全サービス漏洩。パスワード変更の際に全サービス一気に変更する必要が出てきてしんどい

**2. Excelで一元管理（論外）**
メリット：管理がしやすい
デメリット：悪意あるスタッフがいるとファイルコピペで数秒のうちに漏洩。メール添付等の漏洩事故も起きやすい。ファイルをコピーされると管理の収集がつかなくなる

**3. Excelで一元管理＋Excelにパスワード（論外）**
メリット：管理がしやすい、一応守られている感
デメリット：パスワードを管理する為のパスワードが必要になる。Excelパスワードが複雑だとメモる人がでてくるし、簡単だとパスワードの意味が無い。手間が増えるだけで気休めにしかならない

**4. IDとパスワードを別の社内システムで管理**
メリット：一発コピペみたいな漏洩リスクがない
デメリット：探すのが面倒

**5. クラウドのパスワード管理システムを使う**
メリット：サービスによっては直接パスワードをスタッフに教えなくてもいいので、スタッフから漏洩しづらい。管理が楽
デメリット：そのサービスで漏洩があった場合、全てが漏洩する可能性がある。有料のことが多い

**6. LDAP等で管理する**
メリット：退職リスクに最も強い。IDパスが個人管理になるので漏洩しづらいし、漏洩しても特定が容易
デメリット：LDAP自体が導入されていない場合、導入コストが高い。対応していないWebサービスも多い

**7. 管理者がパスワードを全て管理する**
メリット：人による漏洩リスクが少ない
デメリット：業務スピードに支障を来す。その人が急にいなくなると困る

セキュリティの専門家ではないので他にもあるかもしれませんが、パッと思いついたところではこんなところです。

観点としては

- サクッとコピペできる管理方法だと事故が怖い
- ボトルネックが1箇所だと、そこが崩れた時に全てが崩壊する
- あまりに厳しくすると業務スピードに悪影響

というところでしょうか。

# じゃあどれにしましょう

4～6のどれかかなぁと思います。
5と6に関しては「じゃあ導入してください」で記事が終わってしまうので、今回はGASで「4. IDとパスワードを別の社内システムで管理」を実現します。
**あくまでも、本システムはGASを使い無料でパスワード管理を実現できるサンプルの為、ご利用は自己責任でお願いします。**
**本システムによるパスワード漏洩等が発生した場合でも、当方は一切責任を負いかねます。**

# 本システムの完成イメージ

GASを使い、スプレッドシートに記載されたサービス名とログインIDの紐付け情報を1セットで返すhtmlページを用意する。
ログイン情報照会時はスプレッドシートにログを記録。
パスワードは直接スプレッドシートで管理せず、社内ファイルサーバの置き場パスを表示する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/199678/2f0867d9-c34f-bee3-a4d9-1a455fd323df.png)

# スプレッドシートの準備（アクセス権限）

まずは権限の設定。
右上の共有を選択し、「リンクを知っている社内の全員が編集可」に設定します。
（編集権限を付けないとアクセスログ追記ができないですが、スプレッドシートURLを渡さなければこのスプレッドシートへのアクセスは無いはずです）

![share.png](https://qiita-image-store.s3.amazonaws.com/0/199678/3230137e-56a1-f83e-4d24-657be4c2d825.png)

**大前提として、このスプレッドシート自体のURLはパスワード管理ツールの利用者にもトップシークレットです。スプレッドシートの権限は共有しますが、URLは管理者だけに共有しましょう。**

「オーナーの設定」の以下チェックはONにしておきましょう。
社外アカウントを勝手に追加されたり、印刷やDLリスクを軽減できます。

- 編集者によるアクセス権の変更や新しいユーザーの追加を禁止します
- コメント権を持つユーザーと閲覧権を持つユーザーのダウンロード、印刷、コピーを無効にします

**このファイル作成者のあなたが退職等をする場合は、スプレッドシートのオーナーを譲渡しましょう。**

# スプレッドシートの準備（シート作成）

下記フォーマットでA1セルにカーソルを合わせてペーストします。

> シート「パスワード一覧」

| カテゴリ | サービス名 | URL | 使用部署 | ID | パスワード | 備考欄 |
|----------|------------|-----|----------|----|------------|--------|

> シート「アクセスログ」

|アクセス日時|ユーザー|サービス|
|----------|------------|-----|

パスワード一覧シートにはダミーデータをいくつか入れましょう。

![sheet.png](https://qiita-image-store.s3.amazonaws.com/0/199678/2b7e2d17-2b58-7939-8ccf-0899758f126c.png)

「カテゴリ」列はフリーワードにすると表記ブレ等でカテゴリの意味を成さなくなりかねないので、プルダウンの入力規則等で縛った方が良いでしょう。

スプレッドシート側の準備はこれで完了です。

# GASの準備

スクリプトエディタを開き、以下コードに置き換えて実行します。
スクリプトエディタの開き方や*承認が必要です*メッセージが出た際の対処法が分からない場合は
[アドベントカレンダー1日目のHello, world!記事](https://qiita.com/rf_p/items/0535ec495df8f9d2b894)をご参照ください。

```javascript:password_management_tool.gs
var sheet = SpreadsheetApp.getActive().getSheetByName('パスワード一覧');

function doGet() {
  var template = HtmlService.createTemplateFromFile("passwordForm");
  var values = sheet.getDataRange().getValues();
  var services = {};
  for (var i = 1; i < values.length; i++) { // 1行目(key:0)は説明行なので1からループ開始
    var value = values[i];
    if (services[value[0]] == undefined) {
      services[value[0]] = [];
    }
    services[value[0]][i] = value[1];
  }
  template.services = services;
  return template.evaluate().setSandboxMode(HtmlService.SandboxMode.IFRAME).setTitle('パスワード管理ツール');
}

// パスワードを返す
function getLoginData(key) {
  var row = parseInt(key) + 1; // 行番号は1始まり、配列は0始まりなので1足す
  var loginData = sheet.getRange(row, 2, 1, 6).getValues();
  insertAccessLog(loginData[0][0]); // どのサービスの問合せをしたか
  return loginData[0];
}

// アクセスログを残す
function insertAccessLog(service) {
  var accessSheet = SpreadsheetApp.getActive().getSheetByName('アクセスログ');
  var now   = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss');
  var email = Session.getActiveUser().getEmail();
  accessSheet.appendRow([now, email, service]);
}
```

> createTemplateFromFile(filename) 

https://developers.google.com/apps-script/reference/html/html-service#createtemplatefromfilefilename

これを使うことでGASプロジェクト内のhtmlファイルを読み込み、ウェブアプリケーションとして公開した際にhtmlファイルをテンプレートとして、Webページを表示します。

HTMLServiceの使い方に関しては、以下記事を大いに参考にさせていただきました。
参考：http://dackdive.hateblo.jp/entry/2015/02/01/010540

# htmlファイルの作成

スクリプトエディタの*ファイル→新規作成→HTML ファイル*から、プロジェクトにhtmlファイルを追加して**passwordForm**と名付けます。

余談ですが、GASプロジェクト内でHTMLファイルを作成するという発想が無かった作者は、Googleドライブの同じフォルダ内にファイルを置いて「見れないなー」と長時間詰まっていました:sweat_smile:

![html.png](https://qiita-image-store.s3.amazonaws.com/0/199678/92c60ffa-c5d3-5c8b-12db-200c9ac5d632.png)

passwordForm.htmlに以下内容を貼り付けて保存します。

```html:passwordForm.html
<!DOCTYPE html>
<html lang="ja">
  <head>
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
  <script>
    var service = null;
    $(function() {
      $('#service').change(function() {
        service = $(this).val();
      }).change();
    });
    function dispLoginData(loginData) { // スプレッドシートから取得したログイン情報を元にテーブルを構成
      var td = '';
      for (var i in loginData) {
        if (i == 1) {
          td += '<td><a href="' + loginData[i] + '" target="_blank">' + loginData[i] + '</a></td>';
        } else {
          td += '<td>' + loginData[i] + '</td>';
        }
      }
      $('#loginTr').html(td);
    }
  </script>
  </head>
  <body>
    <div id="container">
      <h1>パスワード管理ツール</h1>
      <div class="form-inline text-center">
        <div class="form-group">
          <select class="form-control" id="service">
            <? for (var service in services) { ?>
              <optgroup label="<?= service ?>">
                <? for (var i = 0; i < services[service].length; i++) { ?>
                <? if (services[service][i] == null) continue ?>
                  <option value="<?= i ?>"><?= services[service][i] ?></option>
                <? } ?>
              </optgroup>
            <? } ?>
          </select>
        </div>
        <div class="form-group">
          <input type="button" class="btn btn-success" value="照会"
            onclick="google.script.run.withSuccessHandler(dispLoginData).getLoginData(service)">
        </div>
      </div>
      <br>
      <table class="table table-bordered table-striped">
        <tr>
          <th>サービス名</th>
          <th>URL</th>
          <th>使用部署</th>
          <th>ID</th>
          <th>パスワード</th>
          <th>備考欄</th>
        </tr>
        <tr id="loginTr">
        </tr>
      </table>
    </div>
  </body>
</html>
```

> onclick="google.script.run.withSuccessHandler(dispLoginData).getLoginData(service)"

https://developers.google.com/apps-script/guides/html/reference/run

`google.script.run`を使うことで、htmlテンプレートからGASの関数を呼び出すことができます。
withSuccessHandler([テンプレートファイルのjs関数]).[GASの関数(GASに渡す引数)]

withSuccessHandlerは、GAS関数を呼び出して値が正常に戻ってきた際に発火します。

ちなみに`<form>～</form>`ではなくdivにform-inlineクラスを振っている理由は、formで囲むとbuttonがsubmitとして機能してしまいjsが正常に動かない為です。

# ウェブアプリケーションとして公開する

**ここの公開範囲も設定を失敗すると全世界に公開しかねないので要注意です！**

スクリプトエディタの*公開→ウェブアプリケーションとして導入*を選択。

実行ユーザーはアクセスしたユーザー、アクセスできるユーザーは社内ドメインの全員。

![webapp.png](https://qiita-image-store.s3.amazonaws.com/0/199678/ac7e36a9-6731-f5a8-8c47-3d968ad9effd.png)

公開するとURLが発行されるので、アクセスします。
以下のような表記が出たらREVIEW PERMISSIONから許可します。

![accept.png](https://qiita-image-store.s3.amazonaws.com/0/199678/1a6447b1-f4b5-ea3c-8fec-6838162d41dc.png)

アクセスしたら、セレクトボックスから適当なサービス名を選んで「照会」を押します。
スプレッドシートの「アクセスログ」シートにメールアドレスと日時の記録が残り、htmlビューにパスワードの置き場が表示されたらOKです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/199678/2f0867d9-c34f-bee3-a4d9-1a455fd323df.png)

別のアカウントでも正常に表示ができるか、アクセスログに別のメールアドレスとして保存されるか確認し、問題なければ完成です。IDとパスの組み合わせを本物に置き換えましょう。

**ちなみに、スプレッドシート上にIDとパスワードそのものを置かないのは、このスプレッドシート自体が漏洩した場合に全て漏れてしまうのを防ぐ為です。**
少々面倒でも、パスワードそのものではなく社内のファイル置き場を表示すると良いでしょう。

## イベント発火順の解説

ざっくりお伝えすると

1. まずWebアプリケーションとしてアクセスするので、GASのdoGet(e)が発火
2. htmlテンプレートが表示される
3. `<select>`の値がserviceという変数に入っているので、ボタンを押すとそれが引数としてGASのgetLoginData()に渡される
4. GAS側でログイン情報を返すと同時にinsertAccessLog()が発火し、アクセスログが残る
5. GASから正常に情報が返ってくるとwithSuccessHandlerからdispLoginData()が発火し、htmlファイルのtr#loginTrにログイン情報DOMが生成されレンダリングされる

という流れです。

# さらに改良点もたくさん

スプレッドシート自体を公開せずにわざわざ問い合わせ式のフォームを用意しているのは、1発コピペできないようにしているのと、不審なアクセスがあったら後からアクセスログを元に追えるようにする為です。

アクセスログの情報を元に、1日に複数サービスのパスワードを問い合わせるような不審な動きがあった際に通知したり、短時間に複数サービスのパスワードを問い合わせできない仕組みを作ってもいいかもしれません。

また、パスワード定期変更も忘れがちなので、スプレッドシートに「パスワード最終更新日」列を追加して、半年変更していない場合にメールやChatworkで通知するシステムを組み込んでも面白そうですね。

# 注意点

GASのコードを修正した場合は、必ず新しいバージョンを作成しましょう。
既存バージョンの更新だと、本番ページに反映されません。

スプレッドシートは200万セルという制限があるので、あまりにも長いことこのシステムを使い続けているとアクセスログが記録できなくなる可能性があります。
ある程度で古いログから消す処理を入れると親切ですね。

# おわりに

漏洩リスクを考慮し画面上でIDパスが揃わない仕組みにしていますが、もしGASだけでIDパス両方を表示させたい場合は、**パスワードだけ集めたスプレッドシートを別ファイルで用意し、getLoginData()の中でSpreadsheetApp.openById()を使って返す**等もアリかと思います。
2つのスプレッドシートファイルであれば、よっぽど両方漏洩することはないので。

今回の仕組みを使えば簡易的なWebページを無料で簡単に作れるので、いろいろ試してみてください。

# 明日

[【Google Apps Script】その24　GASの自家製Cronで複数スクリプトの定期実行を一元管理する（1/2）](https://qiita.com/rf_p/items/a1be4699479026fc50d8)
となります。
24日目、25日目は同じテーマの前半後半という構成になります。

前の記事
[【Google Apps Script】その22　Google Analyticsのデータを引っ張ってくる](https://qiita.com/rf_p/items/a40468f6414703d63df3)
次の記事
[【Google Apps Script】その24　GASの自家製Cronで複数スクリプトの定期実行を一元管理する（1/2）](https://qiita.com/rf_p/items/a1be4699479026fc50d8)