Files
beaver_project/authz-service/src/README.md
steven_li b3767dd4ab feat(outlook): 添加 Outlook MCP 集成支持并优化分页功能
- 新增 NANO_OUTLOOK_MCP_URL 和 NANO_OUTLOOK_MCP_SERVER_ID 环境变量配置
- 实现 Outlook 邮件和日历的分页查询功能,添加安全参数验证
- 为 app-instance 创建脚本添加 Outlook MCP 服务器 ID 参数
- 更新前端 Outlook 页面实现邮件列表和日历事件的分页浏览
- 添加 Git 忽略文件配置和 Docker 挂载路径修复

BREAKING CHANGE: Outlook 集成现在需要配置 MCP URL 和服务器 ID 环境变量
2026-03-16 17:01:58 +08:00

640 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Authz-service
## 概览
这是一个基于 FastAPI 的认证授权服务,当前实现主要提供 4 类能力:
1. 后端实例注册与管理
2. 用户注册时自动绑定/创建后端
3. 基于 OAuth 2.0 `client_credentials` 的 access token 签发
4. token 内省、权限配置、Outlook 配置管理
文档内容按当前代码实现整理,接口定义来源于 [app/main.py](/home/ivan/xuan/steven_project/Authz-service/app/main.py)。
## 鉴权模型
### 1. 当前真正支持的登录方式
当前服务没有“用户名 + 密码登录”接口。
真正用于获取 access token 的方式只有一种:
- OAuth 2.0 `client_credentials`
- 调用接口:`POST /oauth/token`
- 需要提供:`client_id``client_secret``aud`
- 可选提供:`scope`
也就是说:
- `username` / `password` 只在 `POST /oauth/register` 注册阶段出现
- 其中 `password` 当前仅做“必填校验”,不会被保存,也不会参与后续登录或鉴权
### 2. 内部接口鉴权
以下接口要求请求头带内部 Bearer Token
- `GET /internal/backends/{backend_id}/settings/outlook`
- `POST /oauth/introspect`
请求头格式:
```http
Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>
```
服务会将 Bearer Token 与环境变量 `AUTHZ_INTERNAL_TOKEN` 做精确匹配。
### 3. Token 签名与校验
- 签名算法:`RS256`
- JWKS 地址:`GET /.well-known/jwks.json`
- OAuth 元数据地址:`GET /.well-known/oauth-authorization-server`
- access token 默认有效期:`3600` 秒,可通过环境变量 `AUTHZ_ACCESS_TOKEN_TTL_SECONDS` 调整
## 基础信息
- 默认 `issuer``http://127.0.0.1:19090`
- 默认 `token_endpoint``/oauth/token`
- 默认 `introspection_endpoint``/oauth/introspect`
- 默认只声明支持 `client_secret_post`
如果服务已启动FastAPI 默认也会提供:
- `GET /docs`
- `GET /openapi.json`
## 典型流程
### 流程 A注册用户并自动创建后端
1. 调用 `POST /oauth/register`
2. 服务创建或更新 backend
3. 服务返回 `backend_id``client_id`
4. 如果 backend 是首次创建,还会返回一次性的 `client_secret`
5. 客户端使用 `client_id + client_secret + aud` 调用 `POST /oauth/token` 换取 access token
### 流程 B单独注册 backend
1. 调用 `POST /backends/register`
2. 记录返回的 `client_id``client_secret`
3. 配置 `POST /backends/{backend_id}/permissions`
4.`POST /oauth/token` 获取 token
### 流程 C由 Auth Portal 发起的一站式注册
1. Auth Portal 调用 `POST /portal/register`
2. AuthZ 先调用 deploy-control 创建或解析实例
3. AuthZ 再调用实例自己的 `POST /api/auth/register`
4. 实例在注册过程中回调 AuthZ 的 `/oauth/register` / `/backends/register`
5. AuthZ 将最终 token 和 backend 连接信息回传给 Auth Portal
## 注册时需要提供什么信息
### 用户注册接口:`POST /oauth/register`
请求体字段如下:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `username` | 是 | `string` | 用户名,去掉首尾空格后不能为空 |
| `password` | 是 | `string` | 当前仅用于必填校验,不会被持久化,也不会用于后续登录 |
| `email` | 否 | `string` | 用户邮箱 |
| `name` | 否 | `string` | backend 名称的候选值之一 |
| `backend_name` | 否 | `string` | backend 名称 |
| `backend_id` | 否 | `string` | 指定 backend 唯一标识;不传则自动生成 |
| `base_url` | 否 | `string` | backend 服务地址 |
| `public_base_url` | 否 | `string` | `base_url` 的候选字段之一 |
| `frontend_base_url` | 否 | `string` | backend 前端地址 |
| `backend` | 否 | `object` | 嵌套 backend 配置,优先级高于同名顶层字段 |
其中真正影响 backend 创建/更新的有效字段有:
- backend 名称:按以下优先级取值
`backend.name` -> `backend_name` -> `name` -> `username`
- backend id按以下优先级取值
`backend.backend_id` -> `backend_id` -> 自动生成
- backend 服务地址:按以下优先级取值
`backend.base_url` -> `public_base_url` -> `base_url`
- backend 前端地址:按以下优先级取值
`backend.frontend_base_url` -> `frontend_base_url`
`base_url` 最终必须能解析出有效值,否则会返回 `400 base_url is required`
#### 推荐最小请求体
```json
{
"username": "alice",
"password": "any-non-empty-value",
"email": "alice@example.com",
"backend_name": "Alice Workspace",
"base_url": "https://api.example.com",
"frontend_base_url": "https://app.example.com"
}
```
#### 使用嵌套 backend 的请求体
```json
{
"username": "alice",
"password": "any-non-empty-value",
"email": "alice@example.com",
"backend": {
"name": "Alice Workspace",
"backend_id": "alice-workspace",
"base_url": "https://api.example.com",
"frontend_base_url": "https://app.example.com"
}
}
```
#### 成功响应
```json
{
"user": {
"username": "alice",
"email": "alice@example.com",
"default_backend_id": "alice-workspace",
"created_at": "2026-03-13T08:00:00+00:00",
"updated_at": "2026-03-13T08:00:00+00:00"
},
"backend": {
"backend_id": "alice-workspace",
"client_id": "alice-workspace",
"client_secret": "generated-secret-only-when-created",
"name": "Alice Workspace",
"base_url": "https://api.example.com",
"frontend_base_url": "https://app.example.com",
"status": "active",
"created_at": "2026-03-13T08:00:00+00:00"
}
}
```
#### 特别说明
- 如果 `backend_id` 不存在,服务会新建 backend并返回新的 `client_secret`
- 如果 `backend_id` 已存在,服务会更新该 backend 信息,但 `client_secret` 会返回 `null`
- 用户数据当前只保存:`username``email``default_backend_id`
- 当前实现没有用户密码校验、密码存储、密码登录、刷新 token
### 后端注册接口:`POST /backends/register`
请求体字段如下:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `name` | 是 | `string` | backend 名称 |
| `base_url` | 是 | `string` | backend 服务地址 |
| `backend_id` | 否 | `string` | 指定 backend id不传则按 `name` 自动生成 |
| `frontend_base_url` | 否 | `string` | backend 前端地址 |
示例:
```json
{
"name": "Local Backend",
"base_url": "https://api.example.com",
"backend_id": "local-backend",
"frontend_base_url": "https://app.example.com"
}
```
成功响应:
```json
{
"backend_id": "local-backend",
"client_id": "local-backend",
"client_secret": "generated-secret",
"created_at": "2026-03-13T08:00:00+00:00",
"frontend_base_url": "https://app.example.com"
}
```
## 登录/鉴权时需要什么信息
### 换取 access token`POST /oauth/token`
这是当前服务唯一的“登录/鉴权入口”。
支持两种提交方式:
- `application/x-www-form-urlencoded`
- `application/json`
请求字段如下:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `grant_type` | 否 | `string` | 仅支持 `client_credentials`,默认值也是这个 |
| `client_id` | 是 | `string` | 注册 backend 后得到,默认等于 `backend_id` |
| `client_secret` | 是 | `string` | 注册或轮转密钥时得到 |
| `aud` | 是 | `string` | 目标 audience决定可申请哪些 scope |
| `scope` / `scopes` | 否 | `string` / `string[]` | 需要的权限范围;不传则自动下发该 audience 下允许的全部 scope |
说明:
- 表单模式下使用字段 `scope`,多个 scope 用空格分隔
- JSON 模式下使用字段 `scopes`,类型为字符串数组
- JSON 模式下也可以把 `aud` 换成 `resource` 吗:不可以。`resource` 只在表单解析分支里作为 `aud` 的兜底字段
#### 表单请求示例
```bash
curl -X POST http://127.0.0.1:19090/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=local-backend" \
-d "client_secret=<client-secret>" \
-d "aud=mcp:outlook" \
-d "scope=list_tools tool:mail_list_messages"
```
#### JSON 请求示例
```json
{
"grant_type": "client_credentials",
"client_id": "local-backend",
"client_secret": "<client-secret>",
"aud": "mcp:outlook",
"scopes": [
"list_tools",
"tool:mail_list_messages"
]
}
```
#### 成功响应
```json
{
"access_token": "<jwt>",
"token_type": "bearer",
"expires_in": 3600
}
```
### `aud` 和 `scope` 的校验规则
当前代码只支持两类 audience
#### 1. MCP audience
格式:
```text
mcp:<server_id>
```
例如:
```text
mcp:outlook
```
只有当权限配置满足以下条件时才能签发 token
```json
{
"mcp": {
"outlook": {
"enabled": true,
"tools": [
"mail_list_messages",
"mail_send_email"
]
}
}
}
```
这时允许的 scope 会自动变成:
- `list_tools`
- `tool:mail_list_messages`
- `tool:mail_send_email`
#### 2. A2A audience
格式:
```text
a2a:<agent_id>
```
例如:
```text
a2a:planner
```
只有当权限配置满足以下条件时才能签发 token
```json
{
"a2a": {
"enabled": true,
"agents": [
"planner"
]
}
}
```
这时允许的 scope 只有:
- `run_task`
#### 3. 请求 scope 的规则
- 如果不传 `scope/scopes`,服务会返回该 audience 下允许的全部 scope
- 如果传了 `scope/scopes`,必须是允许 scope 的子集,否则返回 `403 Requested scopes exceed backend permissions`
- 如果 audience 未启用,返回 `403 Audience is not enabled for this backend`
- 当前默认开启 `AUTHZ_MCP_PERMISSIVE_DEFAULT=1`
-`mcp:*` audience会优先放行本次请求里声明的 scopes并自动补 `list_tools`
- 如果你后面要改回严格模式,把这个环境变量设成 `0`
### Token 内省:`POST /oauth/introspect`
此接口是内部接口,必须带内部 Bearer Token。
请求头:
```http
Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>
```
请求体:
```json
{
"token": "<access-token>"
}
```
成功响应:
```json
{
"active": true,
"client_id": "local-backend",
"backend_id": "local-backend",
"aud": "mcp:outlook",
"scp": [
"list_tools",
"tool:mail_list_messages"
],
"exp": 1773392400
}
```
token 无效时返回:
```json
{
"active": false
}
```
## 接口清单
### 公开接口
| 方法 | 路径 | 鉴权 | 说明 |
| --- | --- | --- | --- |
| `GET` | `/healthz` | 无 | 健康检查 |
| `GET` | `/.well-known/oauth-authorization-server` | 无 | OAuth 元数据 |
| `GET` | `/.well-known/jwks.json` | 无 | JWKS 公钥 |
| `POST` | `/backends/register` | 无 | 注册 backend |
| `GET` | `/backends` | 无 | 查询全部 backend |
| `GET` | `/backends/{backend_id}` | 无 | 查询单个 backend |
| `PUT` | `/backends/{backend_id}` | 无 | 更新 backend |
| `POST` | `/backends/{backend_id}/disable` | 无 | 禁用 backend |
| `POST` | `/backends/{backend_id}/enable` | 无 | 启用 backend |
| `POST` | `/backends/{backend_id}/rotate-secret` | 无 | 轮转 client secret |
| `GET` | `/backends/{backend_id}/permissions` | 无 | 查询权限配置 |
| `POST` | `/backends/{backend_id}/permissions` | 无 | 保存权限配置 |
| `GET` | `/backends/{backend_id}/settings/outlook` | 无 | 查询 Outlook 配置,密码会被隐藏 |
| `POST` | `/backends/{backend_id}/settings/outlook` | 无 | 保存 Outlook 配置 |
| `DELETE` | `/backends/{backend_id}/settings/outlook` | 无 | 删除 Outlook 配置 |
| `POST` | `/oauth/register` | 无 | 用户注册并绑定 backend |
| `POST` | `/oauth/token` | 无 | 使用 client credentials 获取 token |
### 内部接口
| 方法 | 路径 | 鉴权 | 说明 |
| --- | --- | --- | --- |
| `GET` | `/internal/backends/{backend_id}/settings/outlook` | `Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>` | 查询完整 Outlook 配置,包含密码 |
| `POST` | `/oauth/introspect` | `Authorization: Bearer <AUTHZ_INTERNAL_TOKEN>` | 内省 token |
## 详细接口说明
### 1. `GET /healthz`
响应:
```json
{
"status": "ok"
}
```
### 2. `GET /.well-known/oauth-authorization-server`
响应示例:
```json
{
"issuer": "http://127.0.0.1:19090",
"token_endpoint": "http://127.0.0.1:19090/oauth/token",
"introspection_endpoint": "http://127.0.0.1:19090/oauth/introspect",
"jwks_uri": "http://127.0.0.1:19090/.well-known/jwks.json",
"grant_types_supported": [
"client_credentials"
],
"token_endpoint_auth_methods_supported": [
"client_secret_post"
]
}
```
### 3. `GET /.well-known/jwks.json`
返回 RSA 公钥集合,用于校验 `RS256` access token。
### 4. `GET /backends`
返回全部 backend 列表。
### 5. `GET /backends/{backend_id}`
- backend 不存在时返回 `404 Backend not found`
### 6. `PUT /backends/{backend_id}`
请求体字段:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `name` | 否 | `string` | backend 名称 |
| `base_url` | 否 | `string` | backend 服务地址 |
| `frontend_base_url` | 否 | `string` | backend 前端地址 |
说明:
- 只有非空字段才会更新
- 当前实现不支持把已有字段显式清空为 `null`
### 7. `POST /backends/{backend_id}/disable`
将 backend 状态改为 `disabled`
### 8. `POST /backends/{backend_id}/enable`
将 backend 状态改为 `active`
### 9. `POST /backends/{backend_id}/rotate-secret`
返回新的 `client_secret`,并覆盖旧的凭证。
成功响应:
```json
{
"backend_id": "local-backend",
"client_id": "local-backend",
"client_secret": "new-generated-secret",
"rotated_at": "2026-03-13T08:00:00+00:00"
}
```
### 10. `GET /backends/{backend_id}/permissions`
返回指定 backend 的权限配置;若从未配置,返回空对象 `{}`
### 11. `POST /backends/{backend_id}/permissions`
请求体是任意 JSON 对象,当前实际生效的结构建议如下:
```json
{
"mcp": {
"outlook": {
"enabled": true,
"tools": [
"mail_list_messages",
"mail_send_email"
]
}
},
"a2a": {
"enabled": true,
"agents": [
"planner"
]
}
}
```
服务会原样保存该 JSON。
### 12. `GET /backends/{backend_id}/settings/outlook`
返回 Outlook 配置,但不会暴露明文密码。
未配置时响应:
```json
{
"configured": false
}
```
已配置时响应示例:
```json
{
"configured": true,
"email": "user@example.com",
"username": "user",
"domain": "example.com",
"service_endpoint": null,
"server": "mail.example.com",
"autodiscover": false,
"default_timezone": "Asia/Shanghai",
"updated_at": "2026-03-13T08:00:00+00:00",
"password_masked": true
}
```
### 13. `POST /backends/{backend_id}/settings/outlook`
请求体字段:
| 字段 | 是否必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `configured` | 否 | `boolean` | 默认 `true`,一般传 `true` |
| `email` | 是 | `string` | 邮箱地址 |
| `username` | 是 | `string` | Outlook 用户名 |
| `domain` | 否 | `string` | 域名 |
| `service_endpoint` | 否 | `string` | 服务地址 |
| `server` | 否 | `string` | 邮件服务器 |
| `autodiscover` | 否 | `boolean` | 是否自动发现 |
| `default_timezone` | 否 | `string` | 默认时区,默认 `Asia/Shanghai` |
| `password` | 是,但更新时可空 | `string` | 初次配置必须传;后续更新可传空字符串表示沿用旧密码 |
初次配置时如果没有密码,会返回:
```json
{
"detail": "Password is required for initial Outlook setup"
}
```
### 14. `DELETE /backends/{backend_id}/settings/outlook`
成功响应:
```json
{
"ok": true
}
```
### 15. `GET /internal/backends/{backend_id}/settings/outlook`
内部接口,返回完整 Outlook 配置,包含明文 `password`
### 16. `POST /oauth/register`
见上文“注册时需要提供什么信息”。
### 17. `POST /oauth/token`
见上文“登录/鉴权时需要什么信息”。
### 18. `POST /oauth/introspect`
见上文“Token 内省”。
## 常见错误码
| HTTP 状态码 | 场景 |
| --- | --- |
| `400` | 缺少 `username``password``base_url`,或 `grant_type` 不支持 |
| `401` | `client_id/client_secret` 无效,或内部 Bearer Token 无效 |
| `403` | backend 被禁用、audience 未启用、请求 scope 超出权限 |
| `404` | backend 不存在,或内部 Outlook 配置不存在 |
| `409` | 注册 backend 时 `backend_id` 已存在 |
## 当前实现限制
1. `/oauth/register``password` 目前不会被保存,也不会用于认证。
2. 没有用户名密码登录接口,只有 `client_credentials`
3. 没有 refresh token。
4. 公开的 backend 管理接口当前未做访问控制,调用方需自行在网关或网络层保护。