GoogleDriveで社外共有しているユーザーとファイルをGASで取得

### 前置き

前置き長いですから、時間が無い方はサンプルコードから見てみてください。

会社でAppsを使っていると管理側として気になるのが外部とのファイル共有ですよね。情報漏洩怖いですしね。
ちょっと前にもGoogleグループ経由での情報漏洩が問題になりましたよね。

そんなこんなで、情報漏洩が怖いからGoogleDriveでドメイン外との共有はオフにしているって組織も多いのではないでしょうか？

でも、それって本当に問題解決してます？AppsのGoogleDriveが使えないからって個人用アカウントでやりとりしてたりDropboxを使われてたりしませんでしょうか？
責任が利用者個人に移っただけで情報漏洩自体のリスクは減ってませんよね。それよりも利用者個人の責任になったぶんセキュリティの考えが緩くなったり管理者の目が届かない場所での拡散が進むかもしれませんよね。

それに、今どきはメールでの送信も出来るのにファイル共有を許可しなければ大丈夫だなんていうのもイケてませんよね。「あいつはいつもいつも頭が固いからな。」とか「情シスは古いよね。」とか言われますし。

ということで、GoogleDriveでのドメイン外との共有はONにしたうえで、共有してる人と共有先を管理できるようにしてみようというのが今回の内容です。

### サンプルコード

早速ですが、サンプルとしてのコードは以下の感じです。

``` JS:ファイル共有チェック.gas
function main() {
  var prop = PropertiesService.getScriptProperties();

  var myDomain = prop.getProperty('myDomain'); // チェックするドメインを指定
  if (myDomain == undefined) {
    Logger.log('Alert: スクリプトのプロパティにmyDomainをセットしてください');
    return undefined;
  }

  var pem64 = prop.getProperty('pem64');
  if (pem64 == undefined) {
    Logger.log('Alert: スクリプトのプロパティにpem64をセットしてください');
    return undefined;
  }

  var serviceAccount = prop.getProperty('serviceAccount');
  if (serviceAccount == undefined) {
    Logger.log('Alert: スクリプトのプロパティにserviceAccountをセットしてください');
    return undefined;
  }

  var api = "https://www.googleapis.com";
  var scope = [
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/drive.file",
    "https://www.googleapis.com/auth/drive.readonly",
    "https://www.googleapis.com/auth/drive.metadata.readonly",
    "https://www.googleapis.com/auth/drive.appdata",
    "https://www.googleapis.com/auth/drive.apps.readonly"
  ];

  var users = AdminDirectory.Users.list({domain: myDomain, maxResults: 500}).users;
  Logger.log("domain member: " + users.length + " users" )

  var shares = {};
  for (var i in users) {

    var target = users[i].primaryEmail;
    var userName = users[i].name.fullName

    var invoker = new OAuth2Invoker(serviceAccount, pem64, scope.join(" "), target);

    var items_options = {
      "q": "%27" + target + "%27+in+owners",
      "maxResults": 1000
    };
    var items_res = invoker.get(api + '/drive/v2/files', items_options);
    var items = JSON.parse(items_res).items;
    Logger.log(userName + ": " + items.length + " files");

    for (var i in items) {

      var fileName = items[i].title;

      var permissions_res = invoker.get(api + "/drive/v2/files/" + items[i].id + "/permissions");
      var permissions = JSON.parse(permissions_res).items;

      for (var j in permissions) {

        var email = permissions[j].emailAddress;

        if (email != undefined && email.split("@")[1] != myDomain) {
          if (!shares[userName]) { shares[userName] = {} }
          if (!shares[userName][fileName]) { shares[userName][fileName] = [] }
          shares[userName][fileName].push(email);
        }
      }
    }
  }

  Logger.log(shares);

}

```

コード的にはこんな感じで楽に取れます。今回はログに残してるだけですが、GASなんでSpreadsheetsに一覧を作成してレポートを作ったり直接ユーザーに警告メールを送ったり、有無を言わさずに共有を止めることも比較的簡単に出来るはずです。そうGASならね。

ですが、本格的に動かす際にはいくつか注意点が...

まず、GASなので実行時間が5分間と制限されてます。なので、Appsのユーザーが多くてGoogleDriveを積極的に使ってるという環境だとチェックする対象を何回かに分けて実行する必要があります。こちらで試した感じだと一回の起動で600ファイルくらいが限度でしょうか。
環境に応じて、ユーザーのグループを分けたりファイルリスト取得のクエリーを調整する必要があります。

それと、APIが使えるまでの設定も結構面倒です。今回は管理者権限でユーザーのファイルリストを取得しますので、サービスアカウントを作成してそれを使うという必要がありますし、サービスアカウントに対応したOAuthライブラリも必要になります。

### OAuthライブラリの準備

GAS用のOAuth2ライブラリである[gas-oauth2-gae][1]の[OAuth2Invoker.gs][2]と[rsa_and_polyfill.gs][3]をコピーしてGASのスクリプトとして保存します。
保存したライブラリはサービスアカウントに対応していないので、以下のパッチを当てます。
内容的にはサービスアカウントが取得するユーザーを指定できるようにしてるのと、認証API先の変更、スコープが長すぎてCacheサービスのキーに出来ない(なのでCacheしない←ここは他の実装考えたほうがいいかも)あたりを調整しています。

``` DIFF:OAuth2Invoker.gas.diff
--- OAuth2Invoker.gs.org 2014-12-12 15:45:41 +0900
+++ OAuth2Invoker.gs     2014-12-12 15:46:04 +0900
@@ -1,7 +1,7 @@
 /**
 * Used for invoked Google App Engine services from Google Apps Script.
 */
-function OAuth2Invoker(email, pemBase64, scope){
+function OAuth2Invoker(email, pemBase64, scope, target){
   this.post = function(url, payload){
     var params = {
       method:'post',
@@ -28,13 +28,16 @@
       }
     }

+    var s = [];
     if(payload){
-      payload.payload = payload;
+      for (var i in payload) {
+        s.push(i + "=" + payload[i]);
+      }
     }

     Logger.log(params);

-    var response = UrlFetchApp.fetch(url, params);
+    var response = UrlFetchApp.fetch(url + "?" + s.join("&"), params);
     return response;
   }

@@ -64,9 +67,10 @@
     var exp = iat + 3600; //expire in 1 hour

     var jwtClaimSet = {
+      "sub": target,
       "iss":email,
       "scope":scope,
-      "aud":"https://accounts.google.com/o/oauth2/token", //this is always the value for google tokens
+      "aud":"https://www.googleapis.com/oauth2/v3/token", //this is always the value for google tokens
       exp: exp,
       iat: iat
     };
@@ -77,7 +81,7 @@
     var signedBase64 = sign(headerBase64 + '.' + jwtClaimBase64);
     var assertion = headerBase64 + '.' + jwtClaimBase64 + '.' + signedBase64;

-    var resp = UrlFetchApp.fetch("https://accounts.google.com/o/oauth2/token",{
+    var resp = UrlFetchApp.fetch("https://www.googleapis.com/oauth2/v3/token",{
       'method':'post',
       'payload' : {
         'grant_type':"urn:ietf:params:oauth:grant-type:jwt-bearer",
@@ -103,7 +107,7 @@
     } else {
       accessToken = requestAccessToken();
       var fiftyFiveMinutes = 3300;
-      CacheService.getPrivateCache().put(email + scope, accessToken, fiftyFiveMinutes);
+      //CacheService.getPrivateCache().put(email + scope, accessToken, fiftyFiveMinutes);
     }

     return accessToken;
```

### サービスアカウントの登録と共有鍵の取得等

サービスアカウントを使うには以下のステップが必要です。

  * スクリプトエディタのメニューにある「リソース > Googleの拡張サービス...」を開く
  * 「Admin Directory API」と「Drive API」を「ON」にする
  * リンクになってる[Google デベロッパー コンソール][4]をクリックしてConsoleを開く
  * ここでも「Admin Directory API」と「Drive API」を「ON」にする
  * 左のメニューから「認証情報」を選び、「新しいIDを作成」をクリック
  * アプリケショーンの種類から「サービスアカウント」を選び、「クライアントIDを作成」をクリック
  * 自動的に秘密キーがダウンロードされるので、秘密キーのパスワードを控えておく
    * パスワードは毎回同じなのですが、こんなもんでしょうか？「notasecret」なのでいいのかもしれませんが。
  * 作成したサービスアカウントの「メールアドレス」をコピーして、AppsのAdminコンソールを開く
  * 「セキュリティ > もっと見る > 詳細設定」から [API クライアント アクセスを管理する][5]を開く
  * 先ほどコピーしたサービスアカウントのメールアドレスをクライアント名に入力し、APIの範囲にはカンマ区切りの以下を入力して「承認」ボタンをクリック
    `https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/drive.appdata,https://www.googleapis.com/auth/drive.apps.readonly,https://www.googleapis.com/auth/drive.file,https://www.googleapis.com/auth/drive.metadata.readonly,https://www.googleapis.com/auth/drive.readonly `

  * OpenSSLコマンドの入ってるマシンで以下のコマンドを実行
    * 
    `openssl pkcs12 -in YOURPRIVATEKEY.p12 -nodes | openssl rsa | base64 > myfile.pem.b64`

  * 上記のコマンドで出来た「myfile.pem.b64」を開き、改行を削除して一行にまとめる。
  * GASのスクリプトエディタのメニューにある「ファイル > プロジェクトのプロパティ」を開く
  * 「スクリプトのプロパティ」の「Add row」をクリックし以下のキーと値を登録する
    * pem64: 先ほど一行にまとめた鍵の文字列を入力する
    * serviceAccount: サービスアカウントのメールアドレス
    * myDomain: チェックしたいドメイン名

と、ここまでやってやっと頭のコードが動き出します。いやー、結構面倒ですよね。書いてる本人がよくわかります。

### 最後に

実際の運用となると、

  * 許可する共有元と共有先のリストをSpreadsheetに作成
  * そのリストにない共有元と共有先があればそのファイル名とともに外部共有されてるリストとして定期的に自動作成
  * 新しい共有元と共有先が発見されると同時に管理者へ自動的にメール送信

という流れになるでしょうか。
このくらいまで行えば使う方も管理する方もそれほどストレスなく安全にドメイン外との共有をそこそこ安全に行えると思います。

しかし、多分GoogleDriveUnlimitedを使えばこんなに苦労しなくても管理できるんでしょうね。でも、ユーザー数がそこそこいると500円/月は高いですよね。おまけにドメイン全体で契約しなければいけない感じですし。
これができると企業のお固い人達に対して導入時のいいアピールになると思うんですよね。

ということで、Googleさんにはアンリミテッドじゃない一般Appsにもこのような機能をお願いしたいところです。アンリミテッドな容量は要りませんので。

それでは、それでは。良いお年を！

[1]: https://github.com/mcdanielgilbert/gas-oauth2-gae
[2]: https://raw.githubusercontent.com/mcdanielgilbert/gas-oauth2-gae/master/OAuth2Invoker.gs
[3]: https://raw.githubusercontent.com/mcdanielgilbert/gas-oauth2-gae/master/rsa_and_polyfill.gs
[4]: https://console.developers.google.com/project/236641309529/apiui/api
[5]: https://admin.google.com/AdminHome?chromeless=1#OGX:ManageOauthClients