MAS-205: GASフェーズ: MFA義務化と特権アカウント分離
概要
| 項目 | 内容 |
|---|---|
| 案件ID | MAS-205 |
| カテゴリ | セキュリティ |
| Phase | P1 |
| 優先度 | ★★★ |
| 所要時間 | 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 管理者が管理コンソールで設定する運用手順。
設定手順
- Google 管理コンソール(
admin.google.com)にスーパー管理者でサインイン - セキュリティ → 認証 → 2 段階認証プロセス を開く
- 2 段階認証プロセスの適用 を「ON(ユーザーにとって必須)」に設定
- 対象 OU(組織部門)を選択:
- 財務データにアクセスする全ユーザーを含む OU を対象にする
- OU の範囲: Workspace 管理者と判断(「人間が検討すべき事項」参照)
- 適用開始日 を設定し、猶予期間を設ける(既存ユーザーへの移行猶予)
- 保存 をクリックして適用
推奨 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-1 | 2FA 未設定ユーザーをレポート画面で洗い出し、個別フォロー | 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.js の onOpen() に権限チェックを追加し、特権メニューを特権ユーザーのみに表示する。また、重要な特権関数の冒頭に同一の権限チェックを追加する(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.js の MENU_DEFINITION に privileged: 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.js の onOpen() ループ内にスキップ判定を追加
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フラグを参照し、非特権ユーザーには CSSdisplay: 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.js | 1 行追加 | 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 コード変更なし |
注意事項
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 化済み)。
Constants._paramsCacheのキャッシュ_paramsCacheはスクリプト実行ごとにnullで初期化される(002_constants.js行 146)。スクリプトは GAS のサンドボックス上で実行のたびにリセットされるため、03_sys_paramsを更新した後にスプレッドシートを開き直せば最新値が反映される。キャッシュ問題は発生しない。Defense in Depth の必要性
UI メニューから非表示にするだけでは、GAS エディタから関数を直接実行する内部脅威(不正操作)を防げない。特にcleanupOrphanTrn()(行削除・不可逆)とsetupAllSchemas()(DDL 全実行)は冒頭権限チェックが必須。計算ロジックへの影響なし
本案件は権限制御のみを対象とし、会計計算ロジック(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_params の PRIVILEGED_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 Sonnet | onOpen() の挿入位置特定と既存パターン適用が必要。複数関数への Defense in Depth 追加も中程度の判断を要する |
変更履歴
| 日付 | 変更内容 |
|---|---|
| 2026-04-20 | 初版作成 |
| 2026-04-20 | MAS-214 (MENU_DEFINITION ループ) との整合改訂。Step B (onOpen) を privileged フラグ + ループ内スキップ判定に再構成。000_infra/002_constants.js を修正対象に追加。現在のコード記載・影響範囲・エッジケース・動作確認手順・実装プロンプトの制約/対象ファイル記述も MAS-214 後の状態に合わせて更新 |
| 2026-04-21 | Part 1 MFA 義務化の Workspace 管理コンソール設定値を 8 項目で確定。運用日 (D0) は後日記入。段階導入フロー (D-14 〜 D+1) と関連 URL (MFA 設定ページ / 2FA レポート / 停止猶予コード生成) を追記 |
| 2026-04-21 | Part 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・必ず遵守すること)】
- 拡張思考の使い分け: Phase 1(設計)では拡張思考をフル活用し、ファイル名形式・エッジケース一覧・Step 分割粒度・固有名詞(関数名/シート名/列名/行番号)を完全に確定させる。Phase 2(清書)の各 Step 内では拡張思考を最小限に抑え、Phase 1 で確定済みの内容の書き下しに徹する。出力途中で再考しない。
- テキスト報告の禁止: 「〜を作成します」等の text のみで tool_use なしに turn を終了しない。説明は 1 文以内。直ちに tool を呼ぶ。
- 4-5 分割の Write/Edit 実行: 仕様書作成は Step 2-1〜2-4 に分けて実行する(詳細は各 Step 参照)。1 回の Write/Edit は約 300 行以内を目安にする。
- 各 Step で何を書くかを具体指示: 設計判断を Phase 2 実行時に持ち込まない。Phase 1 で確定した内容だけを清書する。
======================================================================
あなたはGAS会計システム(bizlp-gas-accounting)のシニア開発者兼仕様書ライターです。
案件 MAS-205「GASフェーズ: MFA義務化と特権アカウント分離」の開発仕様書を作成してください。
作成後は docs/_config.json の nav 配列にも必ず追記してください。
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.js | onOpen() の全メニュー構造(メニュー名・サブメニュー名の正確な文字列)、setupAllSchemas 等の特権操作関数名、ファイル全体の行番号 |
000_infra/002_constants.js | Constants.getParam(key, defaultVal) の実装(03_sys_params シート名がハードコードされていることを確認)、_paramsCache の初期化タイミング |
000_infra/004_utils.js | Utils.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 に進むこと:
onOpen()が存在するファイルパスと行番号- 特権メニューとして制御対象にすべきメニュー名の正確な文字列(Read した実在する名前のみ)
- フォールバック設計の選択:「スクリプトオーナーを常に特権とする」か「空欄時は警告ダイアログで中断する」か(どちらか一方を選択し仕様書に明記する)
Session.getActiveUser().getEmail()が simple trigger(onOpenのシンプルトリガー)のコンテキストで空文字を返す可能性の確認 → エッジケーステーブルに必ず記載する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 した実際のシグネチャに従うこと
影響範囲(変更ファイル・変更量・既存動作への影響)
注意事項(以下を含む):
- Simple trigger (
onOpen) のコンテキストではSession.getActiveUser().getEmail()が空文字を返す場合がある。Installable trigger との違いを明記し、必要に応じてデプロイ方法を記述する Constants._paramsCacheはスクリプト実行ごとに初期化されるため、03_sys_params更新後は再ロードで反映される(キャッシュ問題なし)- 特権メニューを UI から非表示にするだけでは、関数を直接 GAS エディタから呼び出す攻撃を防げない。重要な特権関数の冒頭には同一の権限チェックを追加する(Defense in Depth)
- 本案件は計算ロジックを含まないため、
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 Sonnet | onOpen() の挿入位置特定と既存パターン適用が必要 |
変更履歴テーブル(日付: 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