# アプリポータル allowlist管理画面 設計書 作成日: 2026-07-26 ## 背景・目的 `apps/app-portal`のダッシュボードへのアクセス許可は、現状`PORTAL_ALLOWED_EMAILS`環境変数(カンマ区切り文字列)をDokploy Environment画面で設定する運用になっている。この方式には以下の課題がある。 - 変更のたびにDokploy管理画面での環境変数編集とコンテナの再作成が必要(2026-07-25のセッションで、環境変数変更がコンテナに反映されずデプロイが実質スキップされる問題に長時間ハマった経緯がある) - 許可対象を増やす・減らすたびにインフラ操作が発生し、運用者(自分)以外が気軽に行える作業ではない これを解消するため、Master Key(固定の共有シークレット)でログインできる簡易管理画面を追加し、許可メールアドレスの一覧・追加・削除をWebブラウザから即時反映できるようにする。 ## スコープ - 対象: `apps/app-portal`への追加機能として実装する(新規アプリの切り出しは行わない) - 対象外: Master Keyの多要素化、ログイン試行のレート制限、複数管理者の権限分離(いずれもYAGNIとして見送る。1〜数人運用が前提) ## アーキテクチャ概要 ``` [ブラウザ] ──(通常アクセス)──→ Traefik(oauth-auth, oauth-errors) ──→ [app-portal] "/" (既存ダッシュボード) └─(/admin配下)────→ Traefik(認証ミドルウェアなし、別ルーター) ─┘ "/admin" (allowlist管理、新規) ``` `/admin`配下のパスだけ、Traefikに認証ミドルウェアを付けない専用ルーター(`PathPrefix`ルール、`websecure`ルーターより高いpriority)を追加する。これにより、Keycloak/LINE WORKS SSO認証を経由せず、Master Keyのみでアクセスできる別ルートとして機能する。 同一Expressアプリ(`app-portal`)内にadmin用ルートを実装する。既存の`createAllowlistMiddleware`は`/`等の通常ルートにのみ適用し、`/admin`配下には適用しない。 allowlistデータは`data/allowlist.json`としてボリュームマウントし、コンテナ再作成をまたいで永続化する。初回起動時にファイルが存在しなければ、既存の`PORTAL_ALLOWED_EMAILS`環境変数の値から生成する(後方互換)。 Master Keyは新規環境変数`PORTAL_MASTER_KEY`として、`AUTH_SECRETS.md`同様の機密情報扱いでDokploy Environment画面にのみ設定する。 ## Traefikルーティング分離 `apps/app-portal/docker-compose.yml`のTraefikラベルに、`/admin`パス専用のルーターを追加する。 - 既存: `app-portal-websecure`ルーター(`Host(\`portal.apps.next-hd.net\`)`、`oauth-errors@file,oauth-auth@file`ミドルウェア付き) - 追加: `app-portal-admin-websecure`ルーター(`Host(\`portal.apps.next-hd.net\`) && PathPrefix(\`/admin\`)`、認証ミドルウェアなし、`priority`を既存ルーターより高く設定) Traefikは複数ルーターが同じHostにマッチする場合、ルール文字列の長さが長い方が優先されるが、明示的に`priority`ラベルを設定して確実にadminルーターを優先させる。 ## Master Keyログイン・セッション管理 - `GET /admin/login`: Master Key入力フォームを表示 - `POST /admin/login`: 入力値を`PORTAL_MASTER_KEY`と比較(`crypto.timingSafeEqual`でタイミング攻撃対策)。一致すればセッションCookieを発行し`/admin`へリダイレクト。不一致ならエラーメッセージ付きでフォームを再表示 - セッションはステートレスなHMAC署名Cookie方式。`PORTAL_MASTER_KEY`をHMAC鍵として使い回す(追加のシークレット管理をしない)。Cookie値は`発行時刻(unixtime).署名`の形式で、検証時に署名の正当性と有効期限(24時間)をチェックする - `/admin`配下(`/admin/login`を除く)は、このセッションCookieを検証するミドルウェアを通す。無効・期限切れなら`/admin/login`へリダイレクト - `POST /admin/logout`: Cookie削除のみの簡易実装 - ログイン試行のレート制限は今回のスコープ外(1〜数人運用のため) ## allowlist管理UI・データ操作 **データ形式:** `data/allowlist.json` ```json {"emails": ["a@example.com", "b@example.com"]} ``` **エンドポイント:** | エンドポイント | 内容 | |---|---| | `GET /admin` | 現在のallowlist一覧(各行にメールアドレス+削除ボタン)と、下部に追加用フォーム(メールアドレス入力欄+追加ボタン)を表示 | | `POST /admin/allowlist/add` | メールアドレスを簡易バリデーション(形式チェック)し、重複していなければファイルに追記して`/admin`へリダイレクト | | `POST /admin/allowlist/remove` | 指定メールアドレスをファイルから除去して`/admin`へリダイレクト | **既存`allowlist.js`との統合:** 現行の`createAllowlistMiddleware(allowedEmailsCsv)`(起動時に固定文字列を受け取り`Set`を構築する実装)を、ファイルパスを受け取りリクエストごとにファイルを読み込んでチェックする方式に変更する。 - 環境変数`PORTAL_ALLOWED_EMAILS`は「`data/allowlist.json`が存在しない場合の初期値」としてのみ使う(後方互換。既存のDokploy Environment設定を壊さない) - 同時書き込みの排他制御は行わない(1〜数人運用のためYAGNI) - ファイルは小規模(数〜数十件想定)なため、リクエストごとの読み込みでパフォーマンス上の問題はないと判断する ## 認証・権限 - `/admin`配下へのアクセスはMaster Keyのみで完結し、Keycloak/LINE WORKS SSOやapp-portal本体の`PORTAL_ALLOWED_EMAILS`allowlistとは独立した認可経路とする - Master Keyは`AUTH_SECRETS.md`と同様の機密情報として扱い、チャット等に出力しない ## エラーハンドリング - Master Key不一致: ログインフォームに「Master Keyが正しくありません」を表示して再入力させる - セッションCookie無効・期限切れ: `/admin/login`へリダイレクト - 追加時の不正なメールアドレス形式: エラーメッセージを表示して`/admin`に戻す - `allowlist.json`読み込み失敗(壊れたファイル等): 500エラーを返しログ出力(自動修復はしない) - ボリュームマウント先ディレクトリの書き込み権限: Dockerfileの`USER node`実行と整合するよう、マウント先ディレクトリの所有者・権限を確認する ## テスト方針 **ユニットテスト:** - `allowlist.js`: ファイルベース版のミドルウェア(モックファイルシステムを使用、既存の`test/allowlist.test.js`のパターンを踏襲) - Master Key検証ロジック(`timingSafeEqual`を使った比較関数) - セッションCookieのHMAC署名生成・検証・有効期限判定ロジック - allowlistデータの追加・削除・重複チェックロジック **手動E2E確認(実機、Dokployへの実デプロイ後):** 1. `/admin`未ログインアクセス→ログインフォームへ遷移すること 2. 誤ったMaster Keyでログイン試行→エラーメッセージが表示されること 3. 正しいMaster Keyでログイン→allowlist一覧が表示されること 4. 新規メールアドレスを追加→一覧に反映され、コンテナ再作成後も永続化されていること 5. 追加したメールアドレスで実際にKeycloak/LINE WORKS SSOログイン→ダッシュボードにアクセスできることを確認(allowlistとして実際に機能していること) 6. メールアドレスを削除→アクセス不可に戻ることを確認 7. `/`(通常ダッシュボード)側は引き続きoauth2-proxy認証を要求すること(Traefikルーティング分離が正しく行われていること) ## 対象ファイル(実装時) - `apps/app-portal/src/allowlist.js`(ファイルベース方式への変更) - `apps/app-portal/src/adminAuth.js`(新規、Master Key検証・セッションCookie発行検証) - `apps/app-portal/src/allowlistStore.js`(新規、`data/allowlist.json`の読み書き・追加削除ロジック) - `apps/app-portal/src/index.js`(`/admin`系ルーティング追加) - `apps/app-portal/.env.example`(`PORTAL_MASTER_KEY`追記) - `apps/app-portal/docker-compose.yml`(admin用Traefikルーター追加、ボリュームマウント追加) - `apps/app-portal/docker-compose.local.yml`(ローカル検証用ボリューム設定追加) ## 未解決・後続タスク - Traefikの`priority`ラベルによるルーター優先順位の実地検証(実装時に確認) - ボリュームマウントのパーミッション実地検証(実装時に確認)