この記録は、とある組織で Google Workspace の共有ドライブ権限を監査・削除する社内ツール「Drive Out」を開発した際のログと、その過程で直面した課題や学びをまとめたものです。
この記事を読んだほうが良い人
- Google Workspace 環境で共有ドライブの権限管理に課題を感じている情報システム担当者
- Google Apps Script (GAS) を使って Admin SDK や Drive API を操作するツールを開発する予定のあるエンジニア
- 退職者や異動者、外部委託先のアクセス権限を効率的に棚卸ししたいと考えている管理者
- GAS の Web アプリ開発における API の挙動やデプロイ周りの注意点を知りたい方
背景
多くの組織で Google Workspace の利用が進む中、共有ドライブは柔軟なファイル共有を可能にする一方で、その権限管理は複雑化しがちです。特に退職者や異動者、契約終了した外部委託先のアクセス権限が放置されると、情報漏洩やセキュリティリスクに直結します。手動での権限確認と削除は多大な工数を要し、ヒューマンエラーのリスクも伴います。このような背景から、共有ドライブの権限を可視化し、不要な権限を安全かつ効率的に削除できるツールの必要性が高まりました。
開発したツールの概要
「Drive Out」は、Google Workspace 管理者向けの Web アプリケーションとして開発されました。特定のメールアドレスを持つユーザーがアクセス権を持つ共有ドライブを検索し、その権限を一覧表示、そして不要な権限をワンクリックまたは一括で削除できる機能を提供します。
機能要件
初版で提供する機能は以下の通りです。
- メールアドレス検索: メールアドレスを入力すると、そのユーザーがアクセス権を持つ共有ドライブを全件走査して一覧表示します。
- 直接権限の検出: ユーザーが直接(
userタイプで)付与されている共有ドライブ権限を検出します。 - グループ経由権限の検出: ユーザーが所属するグループ経由で付与されている共有ドライブ権限を検出します。
- 外部ユーザー対応: ドメイン外のユーザー(外部委託先など)でも直接権限の検索・削除が可能です。
- 単一権限削除: 検索結果から個別の権限を確認ダイアログ付きで削除できます。
- 一括権限削除: チェックボックスで複数選択し、最大20件まで一括削除できます。
- 削除前の安全検証: 削除直前に
permissionを2回取得し、内容が変わっていないことを確認する「楽観ロック」を実装しています。 - 監査ログ: 削除操作を実行者・対象・結果とともに Google スプレッドシートの
AuditLogシートに記録します。 - 削除後の自動再検索: 削除完了後、同じメールアドレスで自動的に再検索し、結果を最新化します。
- 検索結果サマリ: ユーザー存在有無、所属グループ数、権限ヒット数(直接/グループ経由の内訳)をサマリ表示します。
非機能要件
運用面の前提と制約は以下の通りです。
- 認証・認可: Google Workspace 管理者アカウントで実行され、Admin SDK(User/Group 読取)と Drive API(権限読取・削除)のスコープが必要です。
- 実行環境: Google Apps Script (GAS) の Web アプリとしてデプロイされ、実行ユーザーはスクリプトオーナー(管理者)です。
- UI: HTML Service による SPA (Single Page Application) 風の Web UI を採用し、レスポンシブ対応で最大幅 1400px を想定しています。
- スケーラビリティ: 共有ドライブの全件走査を行うため、ドライブ数が多い環境では実行時間が長くなる傾向があり、GAS の6分実行制限に注意が必要です。
- エラーハンドリング: API エラーをユーザー向けメッセージと開発者ログに分離し、想定内のエラーはユーザーに分かりやすく表示します。
スコープ外とした機能(初版)
初版では意図的に対象から外した機能は以下です。運用しながら必要性を判断し、次のイテレーションで拡張するかを決める想定です。
- グループのネスト(入れ子)を再帰的に辿る機能
domain/anyoneタイプの権限の表示・削除- 複数メールアドレスの同時検索
- マイドライブ内ファイルの権限監査
- 権限の追加・変更(読み取りと削除のみに限定)
解決した業務課題とユーザーストーリー
Drive Out は、以下のような具体的な業務課題を解決するために設計されました。
- 退職者の権限棚卸し: 情報システム部の管理者が退職予定者の全共有ドライブ権限を一覧で確認し、退職日までに不要な権限を漏れなく削除することで情報漏洩リスクをゼロにします。
- 外部委託先の契約終了対応: プロジェクトマネージャーが契約終了した外部委託先メンバーの権限を確認・削除し、契約終了後に社内ドライブへアクセスされるリスクを排除します。
- 部署異動に伴う権限見直し: 情報システム部の管理者が異動者の旧部署共有ドライブ権限を把握し、最小権限の原則を維持します。
- 定期セキュリティ監査: セキュリティ担当者が特定ユーザーの共有ドライブ権限全体像をスナップショットとして取得し、四半期ごとのアクセス権レビューを効率的に実施します。
- 誤削除時のリカバリ判断: 管理者が削除前に確認ダイアログで対象の詳細を確認し、誤削除リスクを最小化します。削除直前の整合性チェックにより、権限内容の不一致があればエラーを通知します。
開発中にハマったポイントと学び
開発の過程では、Google Apps Script や Google API の特性に起因するいくつかの課題に直面しました。
1. GoogleJsonResponseException のステータスコード取得
症状: 存在しないユーザーを検索した際に、特定のメッセージではなく「予期しないエラーが発生しました」と表示されていました。
原因: Google Apps Script の GoogleJsonResponseException が返すエラーオブジェクトの構造を誤解していました。通常、HTTP ステータスコードは e.details.code に含まれるべきですが、実装では e.details.error.code を参照していました。e.details.error が undefined であったため、404 エラーを適切に検知できず、全てのエラーが汎用的な「予期しないエラー」として処理されてしまっていました。
教訓: GAS の各 Advanced Service (Admin SDK や Drive API など) が返す例外オブジェクトの具体的な構造はドキュメントに詳細が記載されていないことがあります。このような場合、console.error を使ってエラー発生時のオブジェクト全体をダンプし、実際のプロパティパスを確認することが重要です。e.details.code と e.details.error.code の両方をチェックするなど、防御的なコーディングを心がけるべきです。
2. 外部ユーザーが「存在しない」扱いになる
症状: ドメイン外の外部委託先のメールアドレスで検索すると「ユーザーが存在しません」と表示されるにもかかわらず、実際にはそのユーザーが共有ドライブにアクセス権を持っているケースがありました。
原因: AdminDirectory.Users.get() メソッドは、原則としてドメイン内のユーザー情報しか返しません。そのため、外部ユーザーのメールアドレスでこのメソッドを呼び出すと 404 エラーが返されます。初版の設計ではこの 404 エラーを「ユーザーが存在しない」と判断し、それ以上権限の検索を行わない仕様になっていたため、外部ユーザーの権限を一切検索できませんでした。
対処: ユーザーの存在確認ロジックを修正しました。具体的には、ユーザーが存在しない場合にエラーを返す ensureUserExists_ という関数を、internal (ドメイン内ユーザー)、group (グループアドレス)、external (ドメイン外ユーザー) のいずれかを判別する checkUserType_ に変更しました。external と判別された場合は、Admin SDK によるグループ検索をスキップし、Drive API による直接権限の走査のみを実行するようにしました。
教訓: 「ユーザーの存在確認」と「共有ドライブの権限検索」は、異なるコンテキストで考えるべき独立した処理です。特にドメイン外ユーザーとの共有が日常的に行われる組織では、AdminDirectory にユーザーが存在しなくとも、Drive API を通じて権限が付与されている可能性を考慮し、外部ユーザーの権限監査を重要なユースケースとして設計に含める必要があります。
3. AdminDirectory.Users.Groups.list は存在しない
症状: ドメイン内ユーザーの所属グループを検索する際に、Cannot read properties of undefined (reading 'list') というエラーが発生しました。
原因: Google Apps Script の Admin SDK サービスには AdminDirectory.Users.Groups というサブリソースは存在しません。これは REST API のパス /admin/directory/v1/users/{userKey}/groups から類推してコードを書いてしまったことによるミスマッチでした。GAS のサービスラッパーでは、ユーザーが所属するグループをリストするには AdminDirectory.Groups.list({ userKey: email }) が正しい呼び出し方です。
教訓: GAS の Advanced Service が提供するメソッド構造は、必ずしも基となる Google REST API のパス構造と1対1で対応するわけではありません。GAS 開発環境の自動補完機能や公式リファレンスで、実際に存在するメソッドやその引数を確認することが重要です。REST API ドキュメントだけを見てコードを記述すると、こうしたラッパー層でのミスマッチが発生し、実行時エラーにつながることがあります。
4. 共有ドライブ全件走査のパフォーマンス
症状: 共有ドライブが100件を超える環境で、ユーザーの権限検索に30秒以上かかることが頻発し、GAS の6分実行制限に近づくケースも確認されました。
原因: ユーザーの共有ドライブ権限を特定するためには、まず DriveApp.getSharedDrives() で組織内の全共有ドライブを取得し、次に各共有ドライブに対して Drive.Permissions.list() を呼び出して権限一覧を取得するという、いわゆる N+1 問題(リスト上の N 個の要素それぞれに対して追加で1回ずつ問い合わせが発生し、合計で N+1 回の API コールになる非効率なデータ取得パターン)のような API 呼び出しパターンになっていました。ドライブ数が増えれば増えるほど、「ドライブ数 × 各ドライブの権限ページネーション回数」という形で API コールが増大し、実行時間が比例して長くなっていました。
教訓: Drive API には、特定のユーザーが持つ共有ドライブ権限だけを横断的に検索するような直接的なエンドポイントは、現状提供されていません。そのため、全件走査は現時点での唯一の確実な方法となります。しかし、共有ドライブ数が多い組織では実行時間の長期化は避けられないため、UI 上で検索の進捗状況や予想実行時間を明示し、ユーザーの期待値を適切に管理することが重要です。将来的には、共有ドライブのメタデータをキャッシュする仕組みや、差分更新のロジックを導入することでパフォーマンス改善の余地があるかもしれません。
5. 削除の楽観ロックで意図しないブロック
症状: 権限検索後に別の管理者が同じ権限のロール(例: 閲覧者から編集者へ)を変更した場合、Drive Out でその権限を削除しようとすると「権限内容が変わりました」というエラーが発生し、削除がブロックされてしまいました。
原因: 安全性を高めるため、削除処理の直前に permission オブジェクトを2回取得し、検索時点から内容が変わっていないかを検証する「楽観ロック」の仕組みを実装していました。しかし、この検証ではロールの変更も「不一致」と判定されるため、正当な削除操作であってもブロックされてしまうという過剰な防御になっていました。
教訓: 安全策のつもりがユーザー体験を損なったり、正当な操作を妨げたりするケースがあります。このような場合、エラーメッセージで「画面を更新して再試行してください」といった具体的な案内を提示し、ユーザーが再検索・再削除のフローで容易にリカバリできるように設計することが重要です。完全な整合性保証よりも、ユーザーがスムーズに作業を継続できることを優先する判断も必要です。
6. clasp push が無言でスキップされる
症状: clasp push コマンドを実行しても、Skipping push. というメッセージが表示されるだけで、ローカルのコード変更が Google Apps Script プロジェクトに反映されないことがありました。
原因: clasp は、リモートのプロジェクトとローカルのファイルシステムの状態を比較し、差分がないと判断した場合(またはファイルシステムのタイムスタンプなど、特定のメタデータの問題で)プッシュをスキップすることがあります。特に Google Drive 同期フォルダ内で clasp を使って作業している場合、ファイルシステムのメタデータが通常のローカルファイルとは異なる挙動を示すことが原因となることがあります。
対処: この問題に遭遇した場合、clasp push --force コマンドを使って強制的にプッシュすることで解決できます。
教訓: Google Drive 同期フォルダ内で clasp を利用して Apps Script プロジェクトを管理する際は、--force オプションを常に付与するか、またはローカルの通常のディレクトリで作業してからプッシュする運用を徹底することが、予期せぬスキップを防ぎ、開発の安定性を高める上で有効です。
技術スタック
今回の実装で採用した主な技術スタックは以下の通りです。
| 項目 | 技術 |
|---|---|
| 実行環境 | Google Apps Script (V8 ランタイム) |
| フロントエンド | HTML Service(テンプレート HTML + Vanilla JS + CSS) |
| バックエンド API | Admin SDK Directory API v1 / Google Drive API v3 |
| 認証 | OAuth 2.0(GAS 組み込み、管理者スコープ) |
| 監査ログ | Google Spreadsheet(AuditLog シート) |
| デプロイ | clasp CLI |
導入ステップ
同じような社内ツールをゼロから組み立てる場合、全体の流れをつかむためのガイドとして以下の順序を参考にしてください。個別のハマりどころは前章で説明した通りです。
- Google Apps Script プロジェクトを新規作成し、Web アプリとしてデプロイします。
appsscript.jsonマニフェストファイルで、Admin SDK Directory API と Google Drive API のスコープを適切に設定します。- プロジェクトの Advanced Google Services から
Admin SDK APIとDrive APIを有効化します。 - HTML Service を利用して、メールアドレス入力フォームと権限一覧表示領域を持つ UI を実装します。
- サーバーサイド(
Code.gsなど)で、入力されたメールアドレスを元に以下の処理を実装します。AdminDirectory.Users.get()を試行し、ユーザーの存在を確認します。外部ユーザーの場合は 404 エラーを検知し、グループ検索をスキップするロジックを組み込みます。- ユーザーがドメイン内ユーザーであれば、
AdminDirectory.Groups.list({ userKey: email })を使って所属グループを取得します。 DriveApp.getSharedDrives()で全共有ドライブを取得し、各ドライブに対してDrive.Permissions.list()を実行して、対象ユーザー(またはその所属グループ)が持つ権限を抽出します。
- 抽出した権限情報を UI に表示します。
- 権限削除ボタンが押された際、
Drive.Permissions.get()で現在の権限情報を2回取得し、変更がないか確認する楽観ロックを実装します。 - 整合性が確認できたら
Drive.Permissions.remove()で権限を削除し、同時に削除ログをスプレッドシートに記録します。 claspCLI を使用してローカルでの開発とデプロイを行います。clasp push --forceの利用も検討してください。
コーポレートITのご相談はお気軽に
この記事で書いたような業務改善・自動化の設計から実装まで、DRASENASではコーポレートITの現場に寄り添った支援を行っています。 「まず相談だけ」でも大歓迎です。DRASENAS 公式サイトからお気軽にどうぞ。
御社の IT 部門、ここにあります。
「ITのことはあまりわからない」── そのような状態からで、まったく問題ございません。まずはお気軽にご相談ください。