fix: AI設定APIのテナント分離不備とAPIキー平文露出 #41

Closed
opened 2026-09-18 10:42:02 +00:00 by joe · 1 comment
Owner

概要

AI設定 API(/api/ai-settings)にテナント分離の不備と API キーの平文露出があります。P5 の設定画面で UI から PUT が叩けるようになり、露出面が広がりました。

問題

  1. テナント分離が機能していない
    • src/o2/services/ai_settings_service.py:14-19 の get_settings() が select(AISettings).first() で、テナントを絞らずに最初の1件を返す
    • update_settings() も同じ設定を更新するため、GET/PUT /api/ai-settings が他テナントの設定を読み書きし得る
    • src/o2/api/ai_settings.py:52 付近の「テナントスコープ検証」は pass のみの実質 no-op
  2. APIキーが平文で返る
    • AISettingsResponse.api_key(src/o2/api/ai_settings.py:28)が setting.api_key をそのまま返す
    • GET は read 権限のみのため、VIEWER でも平文キーを取得可能

影響

  • 他テナントの AI 設定(接続先・キー)を閲覧/上書きできる
  • 閲覧者ロールに API キーが漏洩する

対応方針

  1. AISettingsService.get_settings(tenant_id) をテナントスコープに。AGENTS.md の「テナント固有設定がない場合は既定設定を返す」フォールバックを実装(DEFAULT_TENANT_ID の設定にフォールバック。無ければ既定を1件作成)
  2. update_settings(tenant_id, **kwargs) はテナント固有設定を upsert
  3. GET /api/ai-settings のレスポンスから平文 api_key を排除し、has_api_key: bool を返す。PUT は api_key が指定されたときのみ更新(空文字/未指定なら既存維持)
  4. Web UI(/ai-settings)を has_api_key 表示+「変更する場合のみ入力」に合わせる

受け入れ条件

  • テナントAのユーザーがテナントBの AI 設定を取得/更新できない
  • GET に平文 API キーが含まれない(has_api_key のみ)
  • PUT でキー未指定なら既存キーが保持される
  • tests/test_ai_settings.py / test_ai_settings_api.py / test_ai_tenant.py が緑(テナント分離テストを追加)

証拠

  • src/o2/services/ai_settings_service.py:14-19
  • src/o2/api/ai_settings.py:28,52
  • P5 レビュー(#29 コメント)で検出
## 概要 AI設定 API(`/api/ai-settings`)にテナント分離の不備と API キーの平文露出があります。P5 の設定画面で UI から PUT が叩けるようになり、露出面が広がりました。 ## 問題 1. **テナント分離が機能していない** - `src/o2/services/ai_settings_service.py:14-19` の `get_settings()` が `select(AISettings).first()` で、テナントを絞らずに**最初の1件**を返す - `update_settings()` も同じ設定を更新するため、`GET/PUT /api/ai-settings` が**他テナントの設定を読み書き**し得る - `src/o2/api/ai_settings.py:52` 付近の「テナントスコープ検証」は `pass` のみの実質 no-op 2. **APIキーが平文で返る** - `AISettingsResponse.api_key`(`src/o2/api/ai_settings.py:28`)が `setting.api_key` をそのまま返す - `GET` は read 権限のみのため、**VIEWER でも平文キーを取得可能** ## 影響 - 他テナントの AI 設定(接続先・キー)を閲覧/上書きできる - 閲覧者ロールに API キーが漏洩する ## 対応方針 1. `AISettingsService.get_settings(tenant_id)` を**テナントスコープ**に。AGENTS.md の「テナント固有設定がない場合は既定設定を返す」フォールバックを実装(`DEFAULT_TENANT_ID` の設定にフォールバック。無ければ既定を1件作成) 2. `update_settings(tenant_id, **kwargs)` はテナント固有設定を upsert 3. `GET /api/ai-settings` のレスポンスから平文 `api_key` を排除し、`has_api_key: bool` を返す。`PUT` は `api_key` が指定されたときのみ更新(空文字/未指定なら既存維持) 4. Web UI(`/ai-settings`)を `has_api_key` 表示+「変更する場合のみ入力」に合わせる ## 受け入れ条件 - [ ] テナントAのユーザーがテナントBの AI 設定を取得/更新できない - [ ] `GET` に平文 API キーが含まれない(`has_api_key` のみ) - [ ] `PUT` でキー未指定なら既存キーが保持される - [ ] `tests/test_ai_settings.py` / `test_ai_settings_api.py` / `test_ai_tenant.py` が緑(テナント分離テストを追加) ## 証拠 - `src/o2/services/ai_settings_service.py:14-19` - `src/o2/api/ai_settings.py:28,52` - P5 レビュー(#29 コメント)で検出
Author
Owner

対応完了

AI設定 API のテナント分離と API キー露出を修正しました。main a012933 にマージ・push 済み。

修正

  • テナント分離: AISettingsService.get_settings(tenant_id) / update_settings(tenant_id, ...)
    • resolve_write_tenant 起点で対象を統一(admin None は DEFAULT 行)
    • 読み: 自テナント行 → DEFAULT 行フォールバック
    • 書き: 自テナント行を upsert(DEFAULT を書き換えない)
    • 自テナント行を新規作成する際は フォールバック元(DEFAULT)の設定を継承(api_key 等の消失を防止)
  • 平文キー非返却: AISettingsResponse から api_key を削除し has_api_key: bool に。GET/PUT とも平文を返さない
  • PUT のキー保持: api_key 未指定(None)なら既存を保持。指定時のみ更新
  • UI: 新UI /ai-settings とレガシー /settings の両方で、空欄保存時はキーを送らない/「設定済み・未設定」を表示

テスト

  • 新規 test_ai_settings_tenant_isolation.py、test_ai_settings.py / test_ai_settings_api.py / test_settings_page.py 更新
  • テナントA/B分離、admin の GET/PUT 一致、フォールバックのキー継承、平文非公開を検証
  • 対象 42 passed / コア 5481 passed, 0 failed

成果物

  • コミット b92411a(本体)、c08e666(レガシー設定ページ)、5d0d279(重要バグ修正)→ main a012933

残課題(別途)

  • AISettings.api_key はDBに平文保存のまま(暗号化は別タスク)
  • openapi.json が旧スキーマのまま(再生成が必要)
  • AISettings.tenant_id に unique 制約が無い(並行時に重複行の可能性)
  • PUT は実質全置換(未指定フィールドが既定に戻る)。api_key="" 明示はクリア、None/未指定は保持

本 Issue は完了につきクローズします。

## 対応完了 AI設定 API のテナント分離と API キー露出を修正しました。main `a012933` にマージ・push 済み。 ### 修正 - **テナント分離**: `AISettingsService.get_settings(tenant_id)` / `update_settings(tenant_id, ...)` - `resolve_write_tenant` 起点で対象を統一(admin `None` は DEFAULT 行) - 読み: 自テナント行 → DEFAULT 行フォールバック - 書き: 自テナント行を upsert(DEFAULT を書き換えない) - 自テナント行を新規作成する際は **フォールバック元(DEFAULT)の設定を継承**(api_key 等の消失を防止) - **平文キー非返却**: `AISettingsResponse` から `api_key` を削除し `has_api_key: bool` に。GET/PUT とも平文を返さない - **PUT のキー保持**: `api_key` 未指定(`None`)なら既存を保持。指定時のみ更新 - **UI**: 新UI `/ai-settings` とレガシー `/settings` の両方で、空欄保存時はキーを送らない/「設定済み・未設定」を表示 ### テスト - 新規 `test_ai_settings_tenant_isolation.py`、`test_ai_settings.py` / `test_ai_settings_api.py` / `test_settings_page.py` 更新 - テナントA/B分離、admin の GET/PUT 一致、フォールバックのキー継承、平文非公開を検証 - 対象 42 passed / コア **5481 passed, 0 failed** ### 成果物 - コミット `b92411a`(本体)、`c08e666`(レガシー設定ページ)、`5d0d279`(重要バグ修正)→ main `a012933` ### 残課題(別途) - `AISettings.api_key` は**DBに平文保存**のまま(暗号化は別タスク) - `openapi.json` が旧スキーマのまま(再生成が必要) - `AISettings.tenant_id` に unique 制約が無い(並行時に重複行の可能性) - PUT は実質全置換(未指定フィールドが既定に戻る)。`api_key=""` 明示はクリア、`None`/未指定は保持 本 Issue は完了につきクローズします。
joe closed this issue 2026-09-18 11:43:30 +00:00
Sign in to join this conversation.
No labels
h1-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
joe/o2#41
No description provided.