概要

項目内容
案件IDMAS-205
カテゴリセキュリティ
PhaseP1
優先度★★★
所要時間2〜3時間
対象ファイル100_config/101_sys_config.js(onOpen 特権メニュー制御)
前提案件MAS-179(監査証跡の強化・98_audit_log の存在)
関連案件MAS-206(外部共有制限)、MAS-213(監査ログ保護)

目的

財務データを扱う全ユーザーに 2 段階認証(MFA)を義務化し、システム設定・データ整備操作を特権アカウントのみに制限する。これにより、アカウント乗っ取りによる財務データの不正閲覧・操作リスクを低減し、GAS 構築フェーズにおける最優先セキュリティ基盤を確立する。

現在のコード

ファイル: 100_config/101_sys_config.js(行 299〜324)

MAS-214 (PR #230、2026-04-20 マージ済) により、onOpen()Constants.MENU_DEFINITION をループして動的にメニューを生成する構造にリファクタリング済み。

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  // N-38: Constants.MENU_DEFINITION をループしてメニューを動的生成
  // 全操作は右側サイドバーに集約 (狭い画面での top-level メニュー切り詰めを回避)
  try {
    Constants.MENU_DEFINITION.forEach(function(catDef) {
      var menu = ui.createMenu(catDef.category);
      catDef.items.forEach(function(item) {
        if (item.separator) {
          menu.addSeparator();
        } else if (item.items) {
          // サブメニュー (ネスト構造がある場合)
          var sub = ui.createMenu(item.label);
          item.items.forEach(function(subItem) {
            if (subItem.separator) sub.addSeparator();
            else sub.addItem(subItem.label, subItem.funcName);
          });
          menu.addSubMenu(sub);
        } else {
          menu.addItem(item.label, item.funcName);
        }
      });
      menu.addToUi();
    });
  } catch (e) {}
}

現状の問題点: onOpen() には権限チェックが存在せず、MENU_DEFINITION に登録された全カテゴリが全ユーザーに表示される。スプレッドシートにアクセスできる全ユーザーが 💾 バックアップ メニューを含むすべての操作を実行可能な状態である。サイドバー経由で呼び出されるすべての特権操作関数(setupAllSchemas 等)にも同様の制限はない。

特権操作関数の一覧(現状、誰でも実行可能)

関数名ファイルリスク
setupAllSchemas(isFull)101_sys_config.js (行583)DDL 全実行。スキーマ破壊の可能性
setupAllSchemasIncremental()101_sys_config.js (行440)同上(Incremental モード)
sortSheetsByName()101_sys_config.js (行9)全タブ順序変更
cleanupDuplicateRows()101_sys_config.js (行25)31/32/33 タブ行マーク
cleanupOrphanTrn()101_sys_config.js (行226)42 タブ行削除(不可逆)
installAutoOpenSidebarTrigger()101_sys_config.js (行345)プロジェクトトリガー追加
runManualBackup()800_ops/809_backup_tool.jsバックアップ操作
installBackupTriggers()800_ops/809_backup_tool.jsトリガー追加

修正方針

本案件は 2 つの独立したパートで構成される。


Part 1: MFA 義務化(Google Workspace 管理コンソール設定)

GAS コードの変更は不要。 Google Workspace 管理者が管理コンソールで設定する運用手順。

設定手順

  1. Google 管理コンソール(admin.google.com)にスーパー管理者でサインイン
  2. セキュリティ認証2 段階認証プロセス を開く
  3. 2 段階認証プロセスの適用 を「ON(ユーザーにとって必須)」に設定
  4. 対象 OU(組織部門)を選択:
    • 財務データにアクセスする全ユーザーを含む OU を対象にする
    • OU の範囲: Workspace 管理者と判断(「人間が検討すべき事項」参照)
  5. 適用開始日 を設定し、猶予期間を設ける(既存ユーザーへの移行猶予)
  6. 保存 をクリックして適用

推奨 MFA 方法

方法推奨度備考
Google Authenticator(TOTP)デバイス依存なし
Google プロンプト(スマートフォン)簡便だがスマートフォン必須
SMS / 音声通話SIMスワップ攻撃に弱い。緊急時のみ
セキュリティキー(YubiKey 等)フィッシング耐性最高(将来推奨)

設定値一覧 (2 段階認証プロセス)

管理コンソール admin.google.com → セキュリティ → 認証 → 2 段階認証プロセス で以下の値を設定する。実際の運用日 (D0) は後日決定 (最低 D-14 以上先)。

設定項目推奨値根拠
2 段階認証プロセスの有効化ON (ユーザーが 2 段階認証プロセスを有効にできるようにする)MFA 使用の前提
適用指定日以降に強制 (運用日: 後日決定・最低 2 週間先)既存ユーザーのロックアウト回避、段階移行
新しいユーザーの登録期間7〜14 日新規入社者が 2FA を設定する猶予
信頼できるデバイスの登録許可しない (OFF)会計データ扱うためログインごとに 2FA を要求
方法テキスト メッセージまたは音声通話で受け取った確認コード以外SMS 認証は SIM スワップ攻撃リスクがあるため除外。Authenticator アプリ / セキュリティキー運用
停止猶予期間1〜3 日セキュリティキー紛失時の救済措置
セキュリティ コード (ローカル)許可認証アプリ利用時の標準動作
セキュリティ コード (リモート)許可しない (OFF)リモート共有による乗っ取りリスク回避

段階導入の運用フロー

Dayアクション担当
D-14上記設定のうち「適用: 指定日以降に強制 (D0)」で予約、周知メール送信Workspace 管理者
D-14 〜 D-1各ユーザーが自身で 2FA を設定 (Authenticator / セキュリティキー)全ユーザー
D-12FA 未設定ユーザーをレポート画面で洗い出し、個別フォローWorkspace 管理者
D0強制発動。未設定ユーザーはログイン不可(システム自動)
D+1 以降ロックアウトされたユーザーは管理者が緊急解除コード発行で救済Workspace 管理者

関連する管理コンソール URL / 確認手段

  • MFA 設定ページ: admin.google.com → セキュリティ → 認証 → 2 段階認証プロセス
  • ユーザー別 2FA 状態レポート: admin.google.com → レポート → セキュリティ → 2 段階認証
  • 停止猶予コード生成: admin.google.com → セキュリティ → 認証 → 停止猶予コード

Part 2: 特権アカウント分離(GAS コード改修)

100_config/101_sys_config.jsonOpen() に権限チェックを追加し、特権メニューを特権ユーザーのみに表示する。また、重要な特権関数の冒頭に同一の権限チェックを追加する(Defense in Depth)。

Step A: 03_sys_params に新規キーを追加

キー名用途
PRIVILEGED_USERS文字列(カンマ区切りメールアドレス)[email protected],[email protected]特権ユーザーのリスト

Constants.getParam('PRIVILEGED_USERS', '') で取得できる(000_infra/002_constants.js 行 147〜167)。

Step B: ユーザー判定ヘルパー関数の追加

/**
 * N-29: 現在のユーザーが特権ユーザーかを判定する
 * @returns {boolean} 特権ユーザーなら true
 */
function isPrivilegedUser_() {
  var email = '';
  try { email = Session.getActiveUser().getEmail() || ''; } catch (e) { email = ''; }

  // Simple trigger コンテキストでは getEmail() が空文字を返す場合がある
  // 空文字の場合は false(一般ユーザー扱い)とし、特権メニューは非表示にする
  if (!email) return false;

  var raw = Constants.getParam('PRIVILEGED_USERS', '');
  if (!raw) {
    // PRIVILEGED_USERS 未設定: 警告ダイアログは onOpen コンテキストで不可。
    // フォールバック: false を返し特権メニューを非表示にする
    console.warn('[N-29] PRIVILEGED_USERS が未設定です。特権メニューは非表示になります。');
    return false;
  }

  var list = raw.split(',').map(function(s) { return s.trim().toLowerCase(); });
  return list.indexOf(email.toLowerCase()) >= 0;
}

Step C: MENU_DEFINITION への privileged フラグ付与 + onOpen() ループ内スキップ判定

MAS-214 で onOpen() はハードコード構造から MENU_DEFINITION ループ駆動に移行済みのため、ハードコード onOpen に逆戻りする修正は禁止。代わりに次の 2 点を実施する。

C-1. 000_infra/002_constants.jsMENU_DEFINITIONprivileged: true を付与

特権ユーザーのみに表示するカテゴリに privileged: true フラグを追加する。本案件の対象カテゴリは '💾 バックアップ'

// 000_infra/002_constants.js 内の Constants.MENU_DEFINITION 定義
{
  category: '💾 バックアップ',
  privileged: true,  // ← N-29: 特権ユーザーのみに表示
  items: [
    { label: '📦 手動バックアップ実行', funcName: 'runManualBackup', description: '...' },
    // ...既存項目は変更しない
  ]
},

将来サイドバー項目 (MAS-217) が MENU_DEFINITION 化された際も、同様に privileged: true を付与するだけで制御できる拡張性を確保する。

C-2. 100_config/101_sys_config.jsonOpen() ループ内にスキップ判定を追加

MAS-214 で実装済みの onOpen() (行 299〜324) の MENU_DEFINITION.forEach ループ冒頭に、catDef.privileged && !isPrivilegedUser_() の場合に return でスキップする 1 行を追加する。既存のループ構造・サブメニュー処理・try/catch は一切変更しない。

function onOpen() {
  var ui = SpreadsheetApp.getUi();
  try {
    Constants.MENU_DEFINITION.forEach(function(catDef) {
      // N-29: privileged カテゴリは特権ユーザー以外にはスキップ
      if (catDef.privileged && !isPrivilegedUser_()) return;

      var menu = ui.createMenu(catDef.category);
      catDef.items.forEach(function(item) {
        // ...(N-38 実装のまま。変更不要)
      });
      menu.addToUi();
    });
  } catch (e) {}
}

補足: 🚀 BizLP のサイドバーから呼び出される特権操作 (setupAllSchemas 等) は、サイドバー表示自体はすべてのユーザーに開放しつつ、各関数の冒頭で権限チェックを行う (Step D = Defense in Depth)。

サイドバー側の将来拡張: MAS-217 (サイドバー項目の MENU_DEFINITION 統合) 完了後、サイドバー HTML レンダリング時にも privileged フラグを参照し、非特権ユーザーには CSS display: none 等で非表示にする方式を採用する。本案件ではサイドバー HTML は変更対象外。

Step D: 特権関数の冒頭に権限チェックを追加(Defense in Depth)

UI から非表示にするだけでは、GAS エディタから関数を直接実行する内部脅威を防げない。重要な特権関数の冒頭に同一チェックを追加する。

function setupAllSchemas(isFull) {
  // N-29: Defense in Depth — 権限チェック
  if (!isPrivilegedUser_()) {
    SpreadsheetApp.getUi().alert('🚫 この操作は特権ユーザーのみ実行できます。\n管理者に連絡してください。');
    Utils.auditLog('RUN', '', '', '', 'setupAllSchemas', '', '', '非特権ユーザーによる実行試行');
    return;
  }
  Utils.auditLog('RUN', '', '', '', 'setupAllSchemas', '', '', '特権操作: DDL全実行');
  // ... 既存処理
}

同様のパターンを以下の関数に適用:

  • cleanupOrphanTrn() — 行削除(不可逆)のため必須
  • runManualBackup() — バックアップ操作
  • installBackupTriggers() / uninstallBackupTriggers() — トリガー操作

影響範囲

対象ファイル変更量既存動作への影響
MENU_DEFINITION💾 バックアップ カテゴリに privileged: true 付与000_infra/002_constants.js1 行追加MAS-214 で追加されたカテゴリ定義オブジェクトに新規フィールドを追加するのみ。既存カテゴリ・項目は変更なし
onOpen() ループ内スキップ判定追加100_config/101_sys_config.js (行 304 の forEach 冒頭)1〜2 行追加非特権ユーザーの 💾 バックアップ メニューが非表示になる。MAS-214 で実装済みのループ構造は維持
isPrivilegedUser_() 追加100_config/101_sys_config.js約 15 行追加新規追加のみ。既存関数に影響なし
setupAllSchemas() 先頭100_config/101_sys_config.js (行 583〜)約 5 行追加非特権ユーザーが直接呼ぶと中断される。通常はサイドバー経由
cleanupOrphanTrn() 先頭100_config/101_sys_config.js (行 226〜)約 5 行追加同上
バックアップ関数群800_ops/809_backup_tool.js各 5 行追加同上
Part 1 MFA 設定Google Workspace 管理コンソール設定変更のみGAS コード変更なし

注意事項

  1. Simple trigger の getEmail() 空文字問題
    onOpen() は Simple trigger(GAS の自動バインド)として動作する。Simple trigger のコンテキストでは、スクリプトを認可していないユーザーや、共有スプレッドシートで別ドメインのユーザーが開いた場合に Session.getActiveUser().getEmail() が空文字を返す。

    • onOpenAutoShowSidebar_() は Installable trigger で呼ばれる(行 337 参照: simple trigger からは ui.showSidebar() を呼べない)。
    • 本案件では Simple trigger の onOpen()isPrivilegedUser_() を追加する。空文字 → false → 特権メニュー非表示、という安全側フォールバック設計を採用する。
    • Installable trigger への完全移行は MAS-214 等の将来案件で検討(現状のサイドバー起動は Installable trigger 化済み)。
  2. Constants._paramsCache のキャッシュ
    _paramsCache はスクリプト実行ごとに null で初期化される(002_constants.js 行 146)。スクリプトは GAS のサンドボックス上で実行のたびにリセットされるため、03_sys_params を更新した後にスプレッドシートを開き直せば最新値が反映される。キャッシュ問題は発生しない。

  3. Defense in Depth の必要性
    UI メニューから非表示にするだけでは、GAS エディタから関数を直接実行する内部脅威(不正操作)を防げない。特に cleanupOrphanTrn()(行削除・不可逆)と setupAllSchemas()(DDL 全実行)は冒頭権限チェックが必須。

  4. 計算ロジックへの影響なし
    本案件は権限制御のみを対象とし、会計計算ロジック(SubledgerEngine・RPA 各関数・DataMart)は一切変更しない。filterWithRecalcTotal / filterValues の選択基準は対象外。

エッジケース

条件挙動対策・備考
PRIVILEGED_USERS キーが 03_sys_params に未設定または空Constants.getParam('PRIVILEGED_USERS', '')'' を返す。isPrivilegedUser_()false を返す特権メニューが全ユーザーに非表示になる。console.warn でログ出力。初回導入時は必ず 03_sys_params に追加してから運用開始すること
Simple trigger コンテキストで Session.getActiveUser().getEmail() が空文字を返すisPrivilegedUser_()false を返す → 特権メニューが非表示安全側フォールバック(特権メニュー非表示)。通常は Installable trigger 経由のサイドバーから操作するため運用上の支障は少ない
特権ユーザーが自分のメールアドレスを PRIVILEGED_USERS から削除自分も一般ユーザー扱いになり特権メニューが非表示になる(自己ロックアウト)PRIVILEGED_USERS には常に最低 2 名以上登録することを運用ルールとする。GAS エディタから直接実行すれば復旧可能(ただしエディタアクセス権が必要)
一般ユーザーが特権関数(setupAllSchemas 等)を GAS エディタから直接実行isPrivilegedUser_()false → アラートダイアログを表示して処理中断。Utils.auditLog に試行記録が残るDefense in Depth(Step D)で防止。GAS エディタへのアクセス権自体を制限する(スクリプトのデプロイ設定で管理者のみに限定)ことが最終的な対策
03_sys_params シートが存在しないConstants.getParam() の try-catch(行 150〜163)で握りつぶされ ''(defaultVal)を返すisPrivilegedUser_()false → フォールバック発動。setupAllSchemas を実行して 03_sys_params を作成後に PRIVILEGED_USERS を追加する
カンマ区切りメールアドレスの大文字小文字不一致(例: [email protected] vs [email protected].toLowerCase() で正規化しているため一致するisPrivilegedUser_() 内で両辺を toLowerCase() 変換済み。スペース含む入力も .trim() で除去済み
Utils.auditLog()98_audit_log シートが未作成Utils.auditLog 内部で console.error を出力して握りつぶす(004_utils.js 行 277〜279)setupAllSchemas 実行後に 98_audit_log が作成される。MAS-205 実装前に setupAllSchemas を一度実行しておくことを推奨
catDef.privileged === undefined (既存カテゴリ・将来追加カテゴリで privileged 未指定)全ユーザーに表示される(既定で公開)MENU_DEFINITION の既存カテゴリ (🚀 BizLP 等) を変更せずそのまま公開扱いにできる。公開/特権の切替はフラグ付与/削除のみで完結
サイドバー HTML (templates/operations_sidebar.html) が MENU_DEFINITION から動的生成化された将来ケース (MAS-217 以降)HTML レンダリング時に privileged フラグを参照し、非特権ユーザーには CSS display: none / テンプレート側の <? if (!privileged || isPriv) { ?> 等で制御する想定本案件では HTML 側の変更は対象外。GAS トップメニューのみ privileged フラグで制御し、サイドバー側の権限制御は MAS-217 完了後の後続案件で実装

実データ検証

実装前に以下を MCP または手動で確認すること。

確認項目確認方法期待値
03_sys_params シートの行構造シートを直接確認A列: キー名、B列: 値(1行目はヘッダー)
PRIVILEGED_USERS キーの存在A列を目視確認存在しない場合は手動追加が必要
98_audit_log シートの存在タブ一覧を確認存在しない場合は setupAllSchemas を実行して作成
onOpen() が現在 Simple trigger か Installable trigger かGAS エディタ → トリガー一覧onOpen は Simple trigger(ファイル名なし)。onOpenAutoShowSidebar_ のみ Installable trigger として登録されているはず
Session.getActiveUser().getEmail() の実際の動作dev 環境で onOpen() を手動実行してログ確認認可済みユーザーなら自分のメールアドレスが返るはず

関連ドキュメント

ドキュメント関連箇所
MAS-179 監査証跡の強化98_audit_log の DDL 定義・Utils.auditLog() の仕様
MAS-201 スプレッドシート定期バックアップバックアップ関数(runManualBackup 等)の権限チェック対象
MAS-213 監査ログ保護MAS-205 の特権アカウントが 98_audit_log の編集権限を持つ設計と連動
MAS-206 外部共有制限同時期に実施すべきセキュリティ施策
CLAUDE.md環境判定(Env.isDev() 等)・GAS 環境分離の規約

人間が検討すべき事項

以下は Workspace 管理者(PO 兼務可)が意思決定する項目。

項目内容判断が必要な理由
特権ユーザーの初期登録03_sys_paramsPRIVILEGED_USERS に登録するメールアドレスを決定する誰が setupAllSchemas 等の管理操作を実行できるかを明示する必要がある。最低 2 名推奨
Simple trigger の認証制限への対処方針onOpen() の Simple trigger で getEmail() が空文字を返す場合の対応を決定する。現設計では「空文字 → 特権メニュー非表示」で安全側に倒している。Installable trigger へ完全移行するか、現状を許容するかInstallable trigger に移行すると onOpen の自動実行にユーザー認可が必要になる(初回に手動実行が必要)。現状の Simple trigger との共存が望ましい
MFA ポリシー適用後の猶予期間既存ユーザーへの移行猶予期間(例: 2 週間)を設定する猶予期間なしで即時強制すると業務が一時停止するリスクがある
MFA 対象 OU の範囲Google Workspace 管理コンソールで MFA を強制する組織部門(OU)の範囲を決定する外部パートナー等に共有している場合、その対象者が自社ドメインに属さない可能性がある
特権アカウントの命名規則と運用フロー特権アカウントと日常業務アカウントを別メールアドレスで分けるか(例: [email protected][email protected])、同一アカウントで PRIVILEGED_USERS リストで制御するかを決定するアカウント分離の粒度により、運用コストと安全性のトレードオフがある。GAS フェーズでは同一アカウントで PRIVILEGED_USERS リスト管理で十分な可能性が高い
GAS エディタへのアクセス制限スクリプトの編集権限を持つユーザーを GAS プロジェクト設定で管理者のみに限定するか決定するGAS エディタからは Defense in Depth の権限チェックを迂回して直接関数を実行できる(ただし PRIVILEGED_USERS チェックは通過できないが、コードを直接変更できる)

実装プロンプト(Claude Code 用)

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-205「GASフェーズ: MFA義務化と特権アカウント分離」の Part 2(特権アカウント分離)を実装してください。

## 実行前タスク
- `100_config/101_sys_config.js` を Read して onOpen()(行 299〜317)の全メニュー構造と特権操作関数名を確認
- `000_infra/002_constants.js` を Read して Constants.getParam() の引数・返却値の型を確認(行 147〜167)
- `000_infra/004_utils.js` を Read して Utils.auditLog() の引数順序を確認(行 273)
- `03_sys_params` シートに PRIVILEGED_USERS キーが存在しない場合は手動で追加してから実装を開始する
  - A列: PRIVILEGED_USERS  B列: あなたのメールアドレス(カンマ区切りで複数可)

## 修正対象ファイル
- `000_infra/002_constants.js` — Step B-1 で MENU_DEFINITION の `💾 バックアップ` カテゴリに `privileged: true` フラグを追加
- `100_config/101_sys_config.js` — Step A(isPrivilegedUser_)/ Step B-2(onOpen ループ内スキップ判定)/ Step C(特権関数への Defense in Depth)
- `800_ops/809_backup_tool.js` — Step C(バックアップ関数群の Defense in Depth)

## 実装内容

### Step A: isPrivilegedUser_() ヘルパー関数の追加

`onOpen()` 関数(行 299)の直前に以下を追加:

    /**
     * MAS-205: 現在のユーザーが特権ユーザーかを判定する
     * @returns {boolean} 特権ユーザーなら true
     */
    function isPrivilegedUser_() {
      var email = '';
      try { email = Session.getActiveUser().getEmail() || ''; } catch (e) { email = ''; }
      // Simple trigger コンテキストでは getEmail() が空文字を返す場合がある
      // 空文字の場合は false(一般ユーザー扱い)とし、特権メニューは非表示にする
      if (!email) return false;
      var raw = Constants.getParam('PRIVILEGED_USERS', '');
      if (!raw) {
        console.warn('[MAS-205] PRIVILEGED_USERS が未設定です。特権メニューは非表示になります。');
        return false;
      }
      var list = raw.split(',').map(function(s) { return s.trim().toLowerCase(); });
      return list.indexOf(email.toLowerCase()) >= 0;
    }

### Step B: MENU_DEFINITION に privileged フラグ + onOpen ループ内スキップ判定

MAS-214 (PR #230) で onOpen() は MENU_DEFINITION ループに移行済み。ハードコード構造への逆戻りは禁止。次の 2 点を実施する。

**B-1. `000_infra/002_constants.js` の MENU_DEFINITION に privileged フラグ付与:**

Constants.MENU_DEFINITION の対象カテゴリに `privileged: true` を 1 行追加する。本案件では `'💾 バックアップ'` カテゴリを対象。他の既存カテゴリは変更しない。

    {
      category: '💾 バックアップ',
      privileged: true,  // ← MAS-205: 特権ユーザーのみに表示
      items: [
        // 既存項目は一切変更しない
      ]
    },

**B-2. `100_config/101_sys_config.js` の onOpen() ループ内にスキップ判定追加:**

行 304 の `Constants.MENU_DEFINITION.forEach(function(catDef) {` の直後に、次の 1 行を追加する。既存のループ構造・サブメニュー処理・try/catch は一切変更しない。

    Constants.MENU_DEFINITION.forEach(function(catDef) {
      // MAS-205: privileged カテゴリは特権ユーザー以外にはスキップ
      if (catDef.privileged && !isPrivilegedUser_()) return;

      var menu = ui.createMenu(catDef.category);
      // ... (既存処理は変更なし)
    });

### Step C: 特権関数への Defense in Depth 追加

以下の各関数の冒頭(既存の処理の前)に権限チェックを追加する:

**setupAllSchemas(isFull)(行 583〜)の先頭:**

    if (!isPrivilegedUser_()) {
      try { SpreadsheetApp.getUi().alert('🚫 この操作は特権ユーザーのみ実行できます。\n管理者に連絡してください。'); } catch(e) {}
      Utils.auditLog('RUN', '', '', '', 'setupAllSchemas', '', '', '非特権ユーザーによる実行試行');
      return;
    }
    Utils.auditLog('RUN', '', '', '', 'setupAllSchemas', '', '', '特権操作: DDL全実行');

**cleanupOrphanTrn()(行 226〜)の先頭:**

    if (!isPrivilegedUser_()) {
      SpreadsheetApp.getUi().alert('🚫 この操作は特権ユーザーのみ実行できます。');
      Utils.auditLog('RUN', '', '', '', 'cleanupOrphanTrn', '', '', '非特権ユーザーによる実行試行');
      return;
    }
    Utils.auditLog('RUN', '', '', '', 'cleanupOrphanTrn', '', '', '特権操作: 孤立TRN削除');

**809_backup_tool.js の runManualBackup() / installBackupTriggers() / uninstallBackupTriggers() 各先頭:**
(Read で行番号を確認してから追加すること)

    if (!isPrivilegedUser_()) {
      SpreadsheetApp.getUi().alert('🚫 この操作は特権ユーザーのみ実行できます。');
      Utils.auditLog('RUN', '', '', '', 'runManualBackup', '', '', '非特権ユーザーによる実行試行');
      return;
    }

## 制約
- MAS-214 (PR #230) で実装済みの MENU_DEFINITION ループ型 onOpen 構造に逆戻りする修正は禁止。ハードコード化は絶対に行わない
- `000_infra/002_constants.js` の MENU_DEFINITION 既存項目 (label / funcName / description / items 配列) は一切変更しない。`privileged: true` の 1 フィールド追加のみ許可
- メニュー名文字列・関数名は MENU_DEFINITION 定義を信頼する(逐語引用は MENU_DEFINITION から)
- Utils.auditLog() の引数順序は必ずシグネチャ通りに渡す:
  auditLog(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note)

## エッジケース(実装時に考慮すること)
- PRIVILEGED_USERS 未設定時: false を返す(特権メニュー非表示)。console.warn でログ出力
- getEmail() が空文字の場合: false を返す(安全側フォールバック)
- 03_sys_params シートが存在しない: Constants.getParam() が '' を返す → false
- メールアドレスの大文字小文字: toLowerCase() で正規化済み
- カンマ区切りの前後スペース: trim() で除去済み

## 動作確認
1. `npm run push:dev` でデプロイ
2. `03_sys_params` シートに `PRIVILEGED_USERS` キーを追加し、自分のメールアドレスを設定
3. スプレッドシートを開き直し、`💾 バックアップ` メニューが表示されることを確認。**他のカテゴリ(`🚀 BizLP` 等、`privileged` フラグ未指定のカテゴリ)は引き続き表示されることも合わせて確認**
4. 別の一般アカウントで開き直し、`💾 バックアップ` メニューが非表示になることを確認。**他のカテゴリは表示され続ける**
5. `setupAllSchemas` を GAS エディタから直接実行し、アラートダイアログが表示されることを確認(一般アカウントで)
6. 特権アカウントで `setupAllSchemas` を実行し、`98_audit_log` に `RUN / setupAllSchemas / 特権操作: DDL全実行` が記録されることを確認
7. `PRIVILEGED_USERS` を空にしてスプレッドシートを開き直し、`💾 バックアップ` メニューが非表示になること(フォールバック発動)を確認。このとき `isPrivilegedUser_()` が `false` を返すため、`privileged: true` が付与されたカテゴリ(`💾 バックアップ`)**のみ**がスキップされ、他の公開カテゴリは従来通り表示される設計であることを確認

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read) | あり | メニュー構造・引数順序の正確な把握 |
| 実装(Edit) | なし | Read で確定した内容の書き下しのみ |

推奨実行モデル

工程推奨モデル理由
実装(Part 2 GASコード改修)Claude SonnetonOpen() の挿入位置特定と既存パターン適用が必要。複数関数への Defense in Depth 追加も中程度の判断を要する

変更履歴

日付変更内容
2026-04-20初版作成
2026-04-20MAS-214 (MENU_DEFINITION ループ) との整合改訂。Step B (onOpen) を privileged フラグ + ループ内スキップ判定に再構成。000_infra/002_constants.js を修正対象に追加。現在のコード記載・影響範囲・エッジケース・動作確認手順・実装プロンプトの制約/対象ファイル記述も MAS-214 後の状態に合わせて更新
2026-04-21Part 1 MFA 義務化の Workspace 管理コンソール設定値を 8 項目で確定。運用日 (D0) は後日記入。段階導入フロー (D-14 〜 D+1) と関連 URL (MFA 設定ページ / 2FA レポート / 停止猶予コード生成) を追記
2026-04-21Part 1 (MFA 義務化) 運用開始。D0 = 2026-04-21。Workspace 管理コンソール 8 項目設定完了、代表者 ([email protected]) のパスキー登録完了 (MacBook Touch ID)。シークレットウィンドウでのログイン試行でパスキー要求画面が発動することを確認済。Admin レポート「2 段階認証プロセスによる保護: 保護されています」「登録: 登録済み」に反映。「適用」列は新規登録猶予期間 7-14 日扱いで「未適用」表示だが、後日自動で「適用済み」に更新見込み。Part 1 / Part 2 ともに実施完了により MAS-205 案件自体をクローズ

仕様書作成プロンプト(再現性・監査性のため必ず記録)

展開して表示

【タイムアウト回避・実行原則(v1.7・必ず遵守すること)】

  1. 拡張思考の使い分け: Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
  2. テキスト報告の禁止: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
  3. 4-5 分割の Write/Edit 実行: 仕様書作成は Step 2-1〜2-4 に分けて実行する(詳細は各 Step 参照)。1 回の Write/Edit は約 300 行以内を目安にする。
  4. 各 Step で何を書くかを具体指示: 設計判断を Phase 2 実行時に持ち込まない。Phase 1 で確定した内容だけを清書する。

====================================================================== あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。 案件 MAS-205「GASフェーズ: MFA義務化と特権アカウント分離」の開発仕様書を作成してください。 作成後は docs/_config.jsonnav 配列にも必ず追記してください。


Phase 1: 実行前調査(テキスト報告禁止。即座にツール実行)

以下を Read ツール で順番に調査し、Phase 2 で必要な固有名詞・行番号・構造をすべて確定させること。Grep で存在を確認しただけで構造を類推してはならない(失敗パターン #18-#20)。

1-A: 案件定義の読み込み

  • docs/_internal/TODO_future.md → MAS-205 の案件名・概要・人間が検討すべき事項を取得

1-B: プロジェクト規約の読み込み

  • CLAUDE.md → コーディング規約・ファイル番号体系を把握

1-C: 既存仕様書テンプレートの読み込み(フォーマット確認)

  • docs/dev/dev_mas-178_error_handling.md → 基盤・インフラ改善案件のフォーマットを把握

1-D: 実装対象コードの Read(必須。以下を全て Read すること)

ファイル確認すべき事項
100_config/101_sys_config.jsonOpen() の全メニュー構造(メニュー名・サブメニュー名の正確な文字列)、setupAllSchemas 等の特権操作関数名、ファイル全体の行番号
000_infra/002_constants.jsConstants.getParam(key, defaultVal) の実装(03_sys_params シート名がハードコードされていることを確認)、_paramsCache の初期化タイミング
000_infra/004_utils.jsUtils.auditLog(operation, targetSheet, targetId, targetCol, funcName, beforeValue, afterValue, note) の引数順序と 98_audit_log への書き込み処理
docs/_config.json§E.1「基盤・DevOps」セクションの末尾エントリを確認(追記位置の特定)

Read 原則の徹底: onOpen() のメニュー名(例: 🔧 開発・設定)・サブメニュー名は、101_sys_config.js を Read して実在する文字列のみ引用すること。記憶・推測で書かない。

1-E: Phase 1 で確定すべき設計判断事項

Read 完了後、以下を確定させてから Phase 2 に進むこと:

  1. onOpen() が存在するファイルパスと行番号
  2. 特権メニューとして制御対象にすべきメニュー名の正確な文字列(Read した実在する名前のみ)
  3. フォールバック設計の選択:「スクリプトオーナーを常に特権とする」か「空欄時は警告ダイアログで中断する」か(どちらか一方を選択し仕様書に明記する)
  4. Session.getActiveUser().getEmail() が simple trigger(onOpen のシンプルトリガー)のコンテキストで空文字を返す可能性の確認 → エッジケーステーブルに必ず記載する
  5. Utils.auditLog() に渡す funcName 引数の命名規則(既存の auditLog 呼び出し箇所を Grep して確認)

Phase 2: 仕様書の分割作成

出力先: docs/dev/dev_mas-205_mfa_privilege_separation.md

【絶対に1回のツール呼び出しで全内容を出力しない。以下の Step に分割して実行すること。】


Step 2-1: 骨格の作成(File Write、約 20 行)

以下の見出しのみを含む骨格ファイルを Write する(本文は空で可):

# N-29: GASフェーズ: MFA義務化と特権アカウント分離
## 概要
## 目的
## 現在のコード
## 修正方針
## 影響範囲
## 注意事項
## エッジケース
## 実データ検証
## 関連ドキュメント
## 人間が検討すべき事項
## 実装プロンプト(Claude Code 用)
## 推奨実行モデル
## 変更履歴
## 仕様書作成プロンプト(再現性・監査性のため必ず記録)

Step 2-2: 前半セクションの追記(File Edit または Bash heredoc、約 300 行)

以下の内容を書く(Phase 1 で確定した固有名詞・行番号を使用すること):

概要テーブル(案件ID, カテゴリ, Phase, 優先度, 所要時間, 対象ファイル, 前提案件)

目的(1〜3文。「管理操作を特権アカウントのみに制限し、MFA を組み合わせることで不正操作リスクを低減する」旨を簡潔に)

現在のコードonOpen() の該当箇所スニペット。ファイルパス+行番号を明記。特権チェックが存在しない現状を示す)

修正方針(以下の 2 部構成で記述):

  • Part 1: MFA義務化(Google Workspace 管理コンソール設定)

    • GAS コードの変更は不要
    • Google Workspace 管理コンソールの具体的な設定パス(セキュリティ → 2段階認証プロセス)
    • 対象OU・ポリシー強制の手順概要
  • Part 2: 特権アカウント分離(GAS コード改修)

    • 03_sys_params シートに新規キー PRIVILEGED_USERS を追加(値: カンマ区切りのメールアドレス)
    • 取得方法: Constants.getParam('PRIVILEGED_USERS', '') → カンマで split → trim で配列化
    • 現在のユーザー取得: Session.getActiveUser().getEmail()
    • onOpen() 内で上記を組み合わせ、特権メニュー(Phase 1 で確定した実在するメニュー名)の addToUi() を条件分岐で制御
    • フォールバック設計: Phase 1 で選択した方式を実装コード例付きで明記
    • 監査ログ: 特権操作関数(Phase 1 で特定した関数名)の冒頭に Utils.auditLog('RUN', '', '', '', '関数名', '', '', '特権操作の説明') を追加
      • Utils.auditLog() の引数順序は必ず Phase 1 で Read した実際のシグネチャに従うこと

影響範囲(変更ファイル・変更量・既存動作への影響)

注意事項(以下を含む):

  1. Simple trigger (onOpen) のコンテキストでは Session.getActiveUser().getEmail() が空文字を返す場合がある。Installable trigger との違いを明記し、必要に応じてデプロイ方法を記述する
  2. Constants._paramsCache はスクリプト実行ごとに初期化されるため、03_sys_params 更新後は再ロードで反映される(キャッシュ問題なし)
  3. 特権メニューを UI から非表示にするだけでは、関数を直接 GAS エディタから呼び出す攻撃を防げない。重要な特権関数の冒頭には同一の権限チェックを追加する(Defense in Depth)
  4. 本案件は計算ロジックを含まないため、filterWithRecalcTotal / filterValues の選択基準は対象外

Step 2-3a: エッジケース〜人間が検討すべき事項の追記(File Edit または Bash、約 200 行)

エッジケーステーブル(以下を必ず含める):

条件挙動対策・備考
PRIVILEGED_USERS キーが 03_sys_params に未設定または空Phase 1 で選択したフォールバック動作フォールバック設計の選択理由を記載
Simple trigger コンテキストで getEmail() が空文字を返す特権チェックが常に false になり全ユーザーが一般権限扱いInstallable trigger への移行、または空文字時の挙動を明記
特権ユーザーが自分のメールアドレスを PRIVILEGED_USERS から削除自分も一般ユーザー扱いになり管理者メニューが消える最低1名の特権ユーザー存在チェックや、スプレッドシートオーナーのフォールバック
一般ユーザーが特権関数を GAS エディタから直接実行UI からは見えないが実行される特権関数冒頭の権限チェック(Defense in Depth)で防止
03_sys_params シートが存在しないConstants.getParam() の try-catch で握りつぶされ空文字返却フォールバック発動

実データ検証(実装前に確認すべき項目):

  • 03_sys_params シートの現在の行構造(A列: キー, B列: 値 であることを MCP で確認)
  • 98_audit_log シートの存在確認(setupAllSchemas 未実行環境では未作成の可能性あり)
  • onOpen() が現在 Simple trigger か Installable trigger か確認

関連ドキュメント

人間が検討すべき事項(TODO_future.md の記載 + 以下を追加):

  • フォールバック設計の最終決定(「スクリプトオーナー常時特権」 vs「空欄時は警告で中断」)
  • Simple trigger の認証制限への対処方針(Installable trigger 化するか許容するか)
  • 特権ユーザーの初期登録メールアドレス(03_sys_params への初期投入内容)
  • MFA ポリシー適用後の既存ユーザーへの移行猶予期間

Step 2-3b: 実装プロンプト〜変更履歴の追記(File Edit または Bash、約 250 行)

【注意】実装プロンプトはマークダウンのコードブロック(バッククォート)で囲まず、行頭スペース4つのインデントで出力すること。

実装プロンプトには以下を含める(行頭4スペースインデント):

あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者です。
案件 MAS-205「GASフェーズ: MFA義務化と特権アカウント分離」の Part 2(特権アカウント分離)を実装してください。

## 実行前タスク
- `100_config/101_sys_config.js` を Read して onOpen() の全メニュー構造と特権操作関数名を確認
- `000_infra/002_constants.js` を Read して Constants.getParam() の引数・返却値の型を確認
- `000_infra/004_utils.js` を Read して Utils.auditLog() の引数順序を確認
- `03_sys_params` シートに PRIVILEGED_USERS キーが存在しない場合は手動で追加してから実装を開始する

## 修正対象ファイル
`100_config/101_sys_config.js` のみ

## 実装内容
(Phase 1 で確定したメニュー名・行番号・フォールバック設計を元に、具体的な diff 形式または挿入位置指示を記述)

## 制約
- `000_infra/` 配下のファイルは変更しない
- onOpen() のメニュー名文字列は Read で確認した実在する文字列のみ使用する
- Utils.auditLog() の引数順序は必ずシグネチャ通りに渡す

## エッジケース
(Step 2-3a のエッジケーステーブルの内容を転記)

## 動作確認
1. `npm run push:dev` でデプロイ
2. `03_sys_params` シートに `PRIVILEGED_USERS` キーを追加し、自分のメールアドレスを設定
3. スプレッドシートを開き直し、特権メニューが表示されることを確認
4. 別の一般アカウントで開き直し、特権メニューが非表示になることを確認
5. 特権操作を実行し、`98_audit_log` に記録が追記されることを確認
6. `PRIVILEGED_USERS` を空にしてフォールバック動作を確認

### 拡張思考の使用状況
| フェーズ | 拡張思考 | 備考 |
|---------|---------|------|
| 実行前タスク(Read) | あり | メニュー構造・引数順序の正確な把握 |
| 実装(Edit) | なし | Read で確定した内容の書き下しのみ |

推奨実行モデルテーブル:

工程推奨モデル理由
実装(Part 2 GASコード改修)Claude SonnetonOpen() の挿入位置特定と既存パターン適用が必要

変更履歴テーブル(日付: 2026-04-20)


Step 2-4: 仕様書作成プロンプトの記録(File Edit または Bash)

末尾の ## 仕様書作成プロンプト セクションに、以下の形式で <instruction> 全文を記録する:

<details><summary>展開して表示</summary>

(この instruction タグの全文をそのまま貼り付け)

</details>

Phase 3: 保存と記録

3-B: docs/_config.json にナビゲーション登録(必須)

docs/_config.json の §E.1「基盤・DevOps」セクションに追記:

{ "file": "dev/dev_mas-205_mfa_privilege_separation.md", "title": "E.1.X N-29 MFA義務化と特権アカウント分離" }

3-C: changelog 追記

docs/_internal/changelog.md の先頭行(ヘッダー直後)に追記:

| 2026-04-20 | [dev_mas-205_mfa_privilege_separation.md](dev_mas-205_mfa_privilege_separation.md) | 初版作成。MFA義務化手順とGAS特権アカウント分離の設計を記述 |

3-D: コミット&プッシュ

git add docs/dev/dev_mas-205_mfa_privilege_separation.md docs/_config.json docs/_internal/changelog.md
git commit -m "docs: N-29 MFA義務化と特権アカウント分離の開発仕様書を作成

Part1(Workspace MFA設定)とPart2(onOpen特権メニュー制御)の2部構成。
PRIVILEGED_USERSキーをsys_paramsで管理する方式を採用。

https://claude.ai/code/session_XXXXX"
git push -u origin docs/dev-N-29