サービス管理者・監査ログ 仕様書
ステータス: Draft / 作成日: 2026-05-27 依存: なし(
users+tenantsテーブルのみ使用)
1. 概要
本 PR は 2 つの機能を追加する。
- サービス管理者フラグ —
users.is_adminによる、テナントを横断した全権管理者の識別 - 監査ログ — 権限付与・重要操作をすべて
audit_logsテーブルに記録し、管理者が検索・閲覧できる
管理者(is_admin = true)が実行できる操作:
閲覧は読み取り専用: 管理者はテナント・プロジェクト・タスクの内容を閲覧のみできる。ユーザーのデータを直接編集する権限は持たない。
監査ログはサービスの透明性・セキュリティ調査・コンプライアンス対応のために設計する。
2. データモデル
2.1 users テーブルへの追加
ALTER TABLE users ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE users ADD COLUMN is_suspended BOOLEAN NOT NULL DEFAULT false;
// entities/users.rs に追加
pub is_admin: bool,
pub is_suspended: bool,
1 ユーザーにつき is_admin フラグ 1 本のみ。ロール階層は設けない(管理者か否かの 2 値)。
2.2 audit_logs テーブル
pub struct Model {
pub id: Uuid,
pub actor_id: Option<Uuid>, // NULL = システム自動実行
pub actor_type: String, // "user" | "system"
pub action: String, // ドット記法(例: "user.admin.grant")
pub resource_type: String, // "user" | "tenant" | "project" | ...
pub resource_id: String, // 対象リソースの ID(UUID 文字列など)
pub tenant_id: Option<Uuid>, // テナントスコープ操作のみ設定
pub metadata: Option<serde_json::Value>, // before/after・理由など
pub ip_address: Option<String>, // IPv4 / IPv6 文字列
pub user_agent: Option<String>,
pub created_at: DateTimeUtc,
}
監査ログは不変(append-only): UPDATE・DELETE 不可。物理削除は保持ポリシー(デフォルト 2 年)による自動アーカイブのみ。
3. マイグレーション
ALTER TABLE users ADD COLUMN is_admin BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE users ADD COLUMN is_suspended BOOLEAN NOT NULL DEFAULT false;
CREATE TABLE audit_logs (
id UUID PRIMARY KEY,
actor_id UUID REFERENCES users(id) ON DELETE SET NULL,
actor_type VARCHAR NOT NULL,
action VARCHAR NOT NULL,
resource_type VARCHAR NOT NULL,
resource_id VARCHAR NOT NULL,
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL,
metadata JSONB,
ip_address VARCHAR(45),
user_agent VARCHAR,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_logs_actor ON audit_logs(actor_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
CREATE INDEX idx_audit_logs_resource ON audit_logs(resource_type, resource_id);
CREATE INDEX idx_audit_logs_tenant ON audit_logs(tenant_id);
CREATE INDEX idx_audit_logs_created ON audit_logs(created_at DESC);
-- audit_logs は append-only。アプリロールへの UPDATE/DELETE を剥奪する
REVOKE UPDATE, DELETE ON audit_logs FROM app_role;
4. 初期管理者のブートストラップ
アプリ起動時に BOOTSTRAP_ADMIN_EMAIL 環境変数が設定されている場合、該当ユーザーの is_admin を true に設定する。
起動時:
BOOTSTRAP_ADMIN_EMAIL が設定されている
→ users WHERE is_admin = true の件数を確認
→ 管理者が 1 人以上存在する場合: ブートストラップをスキップ(ログ出力のみ)
→ 管理者が 0 人の場合のみ:
→ users WHERE email = $email → is_admin = true に UPDATE
→ audit_logs に action="user.admin.grant", actor_type="system" を INSERT
(metadata には user_id のみ記録。メールアドレスは含めない)
安全ガード: 管理者が既に存在する場合はブートストラップを実行しない。これにより、運用中に環境変数が誤設定されても意図しない権限付与が発生しない。初期構築後は
BOOTSTRAP_ADMIN_EMAILを環境変数から削除することを推奨する。
5. AdminUser エクストラクタ
/// is_admin = true のユーザー専用エクストラクタ。セッション認証のみ受理(PAT 不可)。
pub struct AdminUser {
pub user_id: Uuid,
}
impl FromRequestParts<AppState> for AdminUser {
type Rejection = AppError;
async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, Self::Rejection> {
let auth = AuthUser::from_request_parts(parts, state)
.await
.map_err(|_| AppError::Unauthorized)?;
// 管理者操作はセッション認証のみ。PAT は権限範囲外として 403。
auth.require_session().map_err(|_| AppError::Forbidden)?;
let user = users::Entity::find_by_id(auth.user_id)
.one(&state.db)
.await?
.ok_or(AppError::Unauthorized)?;
if !user.is_admin {
return Err(AppError::Forbidden);
}
Ok(AdminUser { user_id: auth.user_id })
}
}
管理者専用エンドポイントはすべて AdminUser エクストラクタを使用する。非管理者・PAT 認証は 403 Forbidden。
AuthUser エクストラクタには is_suspended チェックを追加する:
// extractors.rs: user_id 取得後に追加
let user = users::Entity::find_by_id(user_id).one(&state.db).await?
.ok_or(AuthError::Unauthorized)?;
if user.is_suspended {
return Err(AuthError::Suspended); // → 403 Forbidden
}
6. 監査ログ記録対象
6.1 アクション一覧
Phase A(本 PR で必須実装)
管理者が行う操作のみを記録する。
Phase B(後続 PR で追加)
一般ユーザーの重要操作まで監査対象を拡大する。各イベントは対応機能の PR と同時に実装する。
6.2 metadata フィールドの構造例
// user.admin.grant
{ "reason": "初期管理者設定" }
// user.password_reset
{ "target_user_id": "uuid", "send_to": "support-relay@example.com" }
// user.2fa.reset
{ "reason": "ユーザーがデバイスを紛失" }
// project.member.role.change
{ "before": "Member", "after": "Admin" }
// auth.login.failure
{ "reason": "invalid_password" }
// tenant.delete
{ "tenant_name": "Example Corp" }
6.3 記録タイミング
操作が成功した後に INSERT する。失敗した操作(バリデーションエラー等)は基本的に記録しない。
例外: auth.login.failure は失敗時にのみ記録する。
7. API
7.1 管理者専用 API(ユーザー管理)
すべて AdminUser エクストラクタを使用。セッション認証必須(PAT 不可)。
パスワードリセットリンク生成:
POST /v1/admin/users/{id}/password-reset リクエスト:
{
"send_to": "support-relay@example.com"
}
send_toは省略可。省略時はユーザーの登録メールアドレスに送信する- 指定した場合はその宛先に送信する(ユーザーがメールアドレスを削除・変更してしまった場合の救済用)
- 生成されたリセットトークンは パスワードリセット仕様書 と同じ TTL(30 分)で有効
- 監査ログに
user.password_reset+metadata.reset_emailを記録する
2FA 強制リセット:
POST /v1/admin/users/{id}/reset-2fa:
totp_credentialsレコードを削除しusers.totp_enabled = falseに設定- リカバリーコードも全削除
- 対象ユーザーの全セッションを無効化(次回ログイン時に再設定を促す)
- 監査ログに
user.2fa.resetを記録する
7.2 管理者専用 API(テナント・閲覧)
7.3 監査ログ閲覧 API
GET /v1/admin/audit-logs
クエリパラメータ:
レスポンス:
{
"logs": [
{
"id": "uuid",
"actor_id": "uuid",
"actor_type": "user",
"action": "user.admin.grant",
"resource_type": "user",
"resource_id": "uuid",
"tenant_id": null,
"metadata": { "reason": "初期管理者設定" },
"ip_address": "203.0.113.1",
"created_at": "2026-05-27T10:00:00Z"
}
],
"next_cursor": "base64cursor"
}
7.4 システム設定 API
設定項目(初期スコープ):
{
"user_registration_enabled": true,
"drive_default_quota_mb": 10240,
"drive_system_max_quota_mb": 102400
}
8. セキュリティ
9. フロントエンド(Phase B)
設計原則: 管理者 UI の完全分離
管理者の権限で通常のテナント・プロジェクト・タスク画面を見ても、一般ユーザーと同じ表示になる。管理者専用の操作・データは /admin 配下のページからのみアクセスできる。
- 通常画面(
/tenants/{id}/...)に管理者バッジや追加操作ボタンは一切表示しない - ナビゲーションバーに「管理者」リンクを 1 つ追加するのみ(
is_adminのときのみ表示) - テナント・プロジェクト・タスクの管理者用閲覧は
/admin/tenants/{id}/...の専用ルートで行う
管理者ダッシュボード
/admin
┌──────────────────────────────────────────────────┐
│ 管理者ダッシュボード │
├──────────────────────────────────────────────────┤
│ ユーザー数: 1,234 テナント数: 87 停止中: 3 │
├──────────────────────────────────────────────────┤
│ [ユーザー管理] [テナント管理] [監査ログ] [設定] │
└──────────────────────────────────────────────────┘
ユーザー管理画面
/admin/users
┌────────────────────────────────────────────────────────────────┐
│ ユーザー管理 [+ ユーザー作成] │
├──────────────┬──────────────────┬──────────┬───────────────────┤
│ 名前 │ メール │ 状態 │ 操作 │
├──────────────┼──────────────────┼──────────┼───────────────────┤
│ Alice │ alice@example.com│ 通常 │ [詳細▼] │
│ Bob │ bob@example.com │ 停止中 │ [詳細▼] │
└──────────────┴──────────────────┴──────────┴───────────────────┘
ユーザー詳細ドロワー([詳細▼] クリック時):
┌──────────────────────────────────────┐
│ bob@example.com │
│ 状態: 停止中 │
│ │
│ [停止解除] [パスワードリセット送信] │
│ [2FA リセット] [パスキー削除] │
│ [OAuth 解除] [強制削除] │
│ │
│ 管理者フラグ: ○ 付与 ● 剥奪 │
└──────────────────────────────────────┘
監査ログ画面
/admin/audit-logs
┌────────────────────────────────────────────────────────────────────┐
│ 監査ログ [アクション▼] [期間▼] [🔍検索] │
├──────────────────┬──────────────────┬──────────────────┬───────────┤
│ 日時 │ 操作者 │ アクション │ 対象 │
├──────────────────┼──────────────────┼──────────────────┼───────────┤
│ 2026-05-27 10:02 │ alice@example.com│ user.admin.grant │ user:bob │
│ 2026-05-27 09:55 │ system │ user.admin.grant │ user:alice│
│ 2026-05-27 09:30 │ bob@example.com │ auth.2fa.enable │ user:bob │
└──────────────────┴──────────────────┴──────────────────┴───────────┘
ルートガード
// middleware/admin.ts
export default defineNuxtRouteMiddleware(() => {
const { user } = useAuth()
if (!user.value?.is_admin) {
return navigateTo('/')
}
})
/admin/** 配下のすべてのページに middleware: ['auth', 'admin'] を適用する。
is_admin は /v1/users/me レスポンスに含め、フロントエンドが保持する。