# v0.2 用户和权限管理规格

本文定义 HWLAB `v0.2` 用户、session、API key、Code Agent owner 和工具能力的最小权限口径。当前 HWPOD 快速闭环阶段只设计核心业务功能；不为旧设备路由、旧授权表或旧 REST/job 保留兼容路径。

登录入口、Keycloak OIDC、Web session、CLI API key 和 `AuthPrincipal` 归一见 [spec-v02-auth.md](spec-v02-auth.md)。OpenFGA、Admin Access WebUI 和同路径 CLI 细节见 [spec-v02-openfga-authorization.md](spec-v02-openfga-authorization.md)。HWPOD 概念、`hwpod-spec`、`hwpod-cli`、`hwpod-ctl`、`hwpod-compiler-cli`、`hwpod-node-ops` 和 `hwpod-node` 见 [spec-hwpod-harness.md](spec-hwpod-harness.md)。

## 在系统中的职责划分

用户和权限管理不拆独立微服务，权威实现收敛在 `hwlab-cloud-api`：它消费认证模块产出的 `AuthPrincipal`，负责角色、OpenFGA check/write、用户 API key、tool capability 和 Code Agent session owner 校验。`hwlab-cloud-web` 只提供浏览器 UI 和同源代理；AgentRun v0.1 只消费 cloud-api 按用户权限注入的 actor/session/tool 上下文，不成为 HWLAB 用户权限 authority。

Postgres 是用户、session、业务对象和迁移 ledger 的持久化边界；OpenFGA 是细粒度授权关系与授权判定边界。Kubernetes namespace、ServiceAccount、Service 直连和 host route 都不能替代用户权限模型；普通用户不获得 kubeconfig、内部 Service 直连能力、OpenFGA token 或长期 Secret。

## 规格目标

- 只保留两类基础角色：`admin` 和 `user`。
- Code Agent session 直接归属于创建它的用户；普通用户只能查看、继续和取消自己的 session。
- 工具能力独立授权，例如 `hwpod`、`unidesk_ssh`、`trans_cmd` 和 GitHub 写工具；拥有 Code Agent session 不等于拥有这些工具。
- 当前 HWPOD 快速阶段不引入用户级 hwpod 授权矩阵；`hwpod-spec` 位于 Code Agent workspace，执行链路按 owner session 和工具能力约束。
- 普通用户不获得 Kubernetes 用户、kubeconfig、namespace 管理权或直接访问内部 Service 的权限；所有用户权限判断在 cloud-api 应用层完成。
- MVP 不新增产品级 `audit_events` 用户审计表，也不把用户权限依赖到 audit。
- v0.2 权限数据必须与 `hwlab-dev`/`hwlab-prod` 运行数据隔离。

## 表结构

### `users`

用户身份和角色 source of truth。

```sql
CREATE TABLE IF NOT EXISTS users (
  id TEXT PRIMARY KEY,
  username TEXT NOT NULL UNIQUE,
  display_name TEXT NOT NULL DEFAULT '',
  role TEXT NOT NULL CHECK (role IN ('admin', 'user')),
  status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
  password_hash TEXT,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL
);
```

### `user_sessions`

浏览器 server session/cookie 的持久状态。cookie 中只保存不可逆 session token；数据库保存 token hash。

```sql
CREATE TABLE IF NOT EXISTS user_sessions (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES users(id),
  session_token_hash TEXT NOT NULL UNIQUE,
  created_at TEXT NOT NULL,
  last_seen_at TEXT NOT NULL,
  expires_at TEXT NOT NULL,
  revoked_at TEXT
);
```

### `api_keys`

CLI 和 runner 的用户 API key 必须映射到 `users.id`。API key 只能收窄用户权限，不能授予超过用户和 OpenFGA tuple 的能力。

### `agent_sessions`

复用现有 `agent_sessions` 作为 Code Agent session 归属记录，不新增同义表。

```sql
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS owner_user_id TEXT REFERENCES users(id);
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS conversation_id TEXT;
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS thread_id TEXT;
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS last_trace_id TEXT;
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS session_json TEXT NOT NULL DEFAULT '{}';
ALTER TABLE agent_sessions ADD COLUMN IF NOT EXISTS updated_at TEXT;
CREATE INDEX IF NOT EXISTS idx_agent_sessions_owner ON agent_sessions(owner_user_id);
CREATE INDEX IF NOT EXISTS idx_agent_sessions_conversation ON agent_sessions(conversation_id);
```

新建 Code Agent session 必须写入 `owner_user_id`。历史 ownerless session 在迁移时可以一次性归属 bootstrap `admin` 或标记为 `expired`；迁移完成后不再允许 ownerless 活跃 session。

## 权限矩阵

| 操作 | `admin` | `user` |
| --- | --- | --- |
| 管理用户和权限 | 可以；应镜像为 `system:hwlab#admin` 或 `access_manager` | 只有被授予 `access_manager` 才可以 |
| 创建自己的 Code Agent session | 可以 | 可以 |
| 查看、继续、取消自己的 Code Agent session | 可以 | 拥有该 session 的 `viewer/operator` 时可以 |
| 查看、取消别人的 Code Agent session | 可以 | 需要该 session 的显式 relation |
| 调用 `hwpod-cli` / `hwpod-ctl` | 可以，但仍受 session owner 和工具边界约束 | 需要 `tool:hwpod#can_use` |
| 调用 UniDesk SSH / trans cmd / GitHub 写工具 | 可以，但仍受工具边界约束 | 需要对应 `tool:*#can_use` |

## 请求链路

cloud-api 每个用户态请求都按同一顺序处理：

```text
authenticate -> actor -> authorize(actor, action, resource) -> execute
```

### 登录、API key 和 session 恢复

```text
browser
-> Keycloak OIDC login/callback
-> cloud-api auth controller
-> users issuer+sub/status/role mapping
-> user_sessions insert token hash
-> Set-Cookie httpOnly sameSite
-> browser

cli
-> Authorization: Bearer hwl_live_...
-> cloud-api api_keys lookup
-> users status/role check
-> AuthPrincipal(authMethod='api-key')
```

### 用户创建或继续 Code Agent session

```text
browser
-> cloud-api POST /v1/agent/chat
-> authenticate actor
-> find or create agent_sessions(owner_user_id=actor.id, conversation_id)
-> run Code Agent turn
-> persist last_trace_id/thread_id/session_json
-> return 202 trace/result polling pointer
```

`GET /v1/agent/chat/result/{traceId}`、`GET /v1/agent/chat/trace/{traceId}` 和 `POST /v1/agent/chat/cancel` 必须通过 `agent_sessions.owner_user_id` 校验 owner；`admin` 可跨用户查看和取消。

### Code Agent 使用 HWPOD

```text
code agent turn
-> hwpod-cli in workspace
-> hwpod-compiler-cli reads .hwlab/hwpod-spec.yaml
-> hwpod-node-ops plan
-> cloud-api /v1/hwpod-node-ops
-> hwpod-node
```

Cloud API 给 AgentRun runner 注入 `hwpod`、UniDesk SSH、`trans_cmd` 或 GitHub 写工具前，必须先按 [spec-v02-openfga-authorization.md](spec-v02-openfga-authorization.md) 检查对应 `tool:*#can_use`。

## API 接口说明

| 接口 | 说明 |
| --- | --- |
| `GET /auth/oidc/login`、`GET /auth/oidc/callback` | Web 登录入口；按 [spec-v02-auth.md](spec-v02-auth.md) 接入 Keycloak 并写入 24 小时 `user_sessions` token hash。 |
| `GET /auth/session` | 从 cookie 恢复 actor、role 和 session 状态；API key actor 摘要见 `/v1/users/me`。 |
| `GET /v1/auth/session`、`GET /v1/users/me`、`GET /v1/access/status`、`GET /v1/setup/status` | REST 状态入口；不得读取或返回 password hash、session token 原文或 Secret 值。 |
| `GET/POST /v1/api-keys...` | 用户 API key 管理入口；CLI `HWLAB_API_KEY` 规则见 [spec-v02-auth.md](spec-v02-auth.md)。 |
| `POST /v1/setup/first-admin` | 仅当 `users` 表为空时创建第一个 `admin` 并建立 session；一旦已有用户必须返回 `409 setup_already_completed`。 |
| `POST /auth/logout` | 设置 `revoked_at`，撤销当前 browser session。 |
| `POST /v1/admin/users` | admin 创建用户，响应不得返回 `password_hash` 或 token。 |
| `GET/PATCH/PUT/DELETE /v1/admin/access...` | admin Access API，读写 OpenFGA 授权、tool capability、role/status 和 effective matrix。 |
| `POST /v1/hwpod-node-ops` | HWPOD node-ops 转发入口；业务翻译必须已由 workspace-local compiler 完成。 |
| `POST /v1/agent/chat` 及 result/trace/cancel | 必须校验 `agent_sessions.owner_user_id`；admin 可跨用户查看和取消。 |

## 测试规格

## T1

阅读 docs/reference/spec-user-access.md，然后用 CLI 手动测试以下内容：创建普通用户，确认 `role=user`、`status=active`，响应不包含 password hash、session token 或 Secret 值。

## T2

阅读 docs/reference/spec-user-access.md，然后用普通用户创建 Code Agent session，确认 `agent_sessions.owner_user_id` 写入当前用户；另一个普通用户不能读取或取消该 session。

## T3

阅读 docs/reference/spec-user-access.md，然后撤销普通用户的 `tool:hwpod` 能力，创建新的 Code Agent session 并调用 HWPOD，确认 cloud-api 或 runner 装配返回结构化 authorization blocker。

## T4

阅读 docs/reference/spec-user-access.md，然后检查 source schema、migration 和 live Postgres，确认不存在旧设备授权表或旧 profile authority 表作为授权来源。

## 规格的实现情况

| 规格项 | 状态 | 说明 |
| --- | --- | --- |
| admin/user 两角色模型 | 已实现/持续约束 | cloud-api 已实现 `/auth/*`、bootstrap admin、admin/user 创建和 admin-only route。 |
| `users`、`user_sessions`、`api_keys`、OpenFGA relation | 已实现/持续约束 | 当前授权只表达用户、session 和 tool capability；不保留旧设备授权 source。 |
| Code Agent owner 绑定 | 已实现/持续约束 | session owner、trace/result/cancel 必须按 owner 校验。 |
| HWPOD 工具能力授权 | 目标状态 | `tool:hwpod` 控制 runner 是否注入 HWPOD 能力；具体 HWPOD 执行链路见 `spec-hwpod-harness.md`。 |
