Metadata-Version: 2.4
Name: authhub-rbac-sdk
Version: 0.1.0
Summary: 子平台接入 AuthHub 的官方 Python SDK：登录、运行时鉴权一站直达。
Author: AuthHub Team
License: Proprietary
Keywords: authhub,rbac,philvault,auth,sdk
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: Other/Proprietary License
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.27
Provides-Extra: django
Requires-Dist: Django<6,>=4.2; extra == "django"
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.110; extra == "fastapi"
Provides-Extra: all
Requires-Dist: Django<6,>=4.2; extra == "all"
Requires-Dist: fastapi>=0.110; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: Django<6,>=4.2; extra == "dev"
Requires-Dist: fastapi>=0.110; extra == "dev"
Dynamic: license-file

# authhub_sdk

子平台（cortexa / athanor / …）接入 AuthHub 的官方 SDK。

**核心约束**：子平台**只调 AuthHub**，绝不直接连 PhilVault；登录、token 刷新、
每次请求的鉴权决策都走 AuthHub HTTP API（`/api/v2/auth/*`、`/api/v2/sdk/*`）。
路由 → 资源/动作 的映射由 AuthHub 服务端匹配，SDK 端无感。

## 与历史 `authz/` 的关系

旧的 `authz/` 是 Vortek/湍流版权的 Django 中间件，直接调用 `philvault_sdk`。
本 SDK 是其替代品：

| 维度 | 旧 `authz/` (Vortek) | 新 `authhub_sdk/` |
|---|---|---|
| 通信对象 | 直连 PhilVault | 只调 AuthHub |
| 路由映射匹配 | 子平台拉 `/authz/mapping` 本地缓存 | AuthHub 服务端匹配 |
| 鉴权决策 | 本地 + PhilVault check | 一次 `POST /sdk/authorize` 拿结论 |
| 跨框架 | 仅 Django | Django + FastAPI |
| Token 缓存 | 30s 本地缓存 | 默认无缓存（运行时鉴权链路不缓存） |

新接入的子平台**只用本包**，不要再引入 `authz/`。

## 安装

仓库内开发：

```bash
# 基础安装（只用 AuthhubClient / AsyncAuthhubClient）
pip install /path/to/authhub/authhub_sdk

# Django 接入（自动带上 Django >=4.2）
pip install "authhub-sdk[django] @ file:///path/to/authhub/authhub_sdk"

# FastAPI 接入
pip install "authhub-sdk[fastapi] @ file:///path/to/authhub/authhub_sdk"

# Django + FastAPI 都装
pip install "authhub-sdk[all] @ file:///path/to/authhub/authhub_sdk"

# 开发依赖（含 pytest）
pip install -e "/path/to/authhub/authhub_sdk[dev]"
```

依赖：

- `httpx >= 0.27`（基础）
- Python 3.10+
- Django >=4.2（extra `[django]`）
- FastAPI >=0.110（extra `[fastapi]`）

> 包采用 `src/` 布局：源码在 `authhub_sdk/src/authhub_sdk/`，安装后通过
> `import authhub_sdk` 引用，与 Django/FastAPI 上游包不会发生命名冲突。

## 三件套环境变量

```bash
export AUTHHUB_BASE_URL=http://authhub.internal:6001  # 不带 /api/v2
export AUTHHUB_APP_CODE=cortexa
# 可选
export AUTHHUB_TIMEOUT=5
export AUTHHUB_VERIFY_SSL=true
export AUTHHUB_BYPASS_PATHS=/health,/static/,/api/v2/sdk/healthz
```

`AUTHHUB_BYPASS_PATHS` 是逗号分隔的**前缀**列表，命中后不调 AuthHub。

## 基础用法：HTTP 客户端

### 同步

```python
from authhub_sdk import AuthhubClient

with AuthhubClient.from_env() as client:
    # 登录（子平台前端或后端都可发起）
    result = client.login("alice", "User@123456")
    access_token = result["access_token"]

    # 当前用户信息
    me = client.me(access_token)
    print(me.user_id, me.username, me.domain_roles)

    # 每次业务请求前调一次（中间件已自动做了，这里仅演示）
    outcome = client.authorize(
        access_token,
        method="GET",
        path="/api/v1/projects/123",
    )
    if not outcome.allow:
        raise PermissionError(outcome.reason)
```

### 异步

```python
from authhub_sdk import AsyncAuthhubClient

async with AsyncAuthhubClient.from_env() as client:
    outcome = await client.authorize(token, method="POST", path="/api/v1/orders")
```

显式传配置（不走 env）：

```python
from authhub_sdk import AuthhubClient
from authhub_sdk.config import AuthhubConfig

cfg = AuthhubConfig(base_url="http://authhub.internal:6001", app_code="cortexa")
client = AuthhubClient(config=cfg)
```

## Django 接入

### 1. 安装中间件

`settings.py`：

```python
INSTALLED_APPS = [
    # ...
]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.middleware.common.CommonMiddleware",
    "corsheaders.middleware.CorsMiddleware",  # 若用 corsheaders
    "authhub_sdk.django.AuthhubMiddleware",   # ← 放在业务中间件之前
    # ...
]

# 与 from_env 等价；也可以省略改用环境变量
AUTHHUB_SDK = {
    "base_url": "http://authhub.internal:6001",
    "app_code": "cortexa",
    "bypass_paths": ["/health/", "/static/", "/admin/login/"],
}
```

### 2. 在 view 里拿用户

中间件鉴权通过后会注入：

| 属性 | 类型 | 含义 |
|---|---|---|
| `request.authhub_outcome` | `AuthorizeOutcome` | 完整鉴权结果 |
| `request.authhub_user` | `dict` | PhilVault `/auth/me` 返回的用户字典 |
| `request.authhub_user_id` | `str` | 用户 ObjectId |
| `request.authhub_access_token` | `str` | 透出本次请求的 token |

```python
from django.http import JsonResponse

def get_my_projects(request):
    user_id = request.authhub_user_id
    return JsonResponse({"user_id": user_id})
```

### 3. 错误响应

| 场景 | HTTP | body |
|---|---|---|
| 缺/无效 Bearer token | 401 | `{"detail": "Unauthorized: ...", "request_id": "..."}` |
| AuthHub 决策为 deny | 403 | `{"detail": <reason>, "decision": "deny", "mapping": {...}}` |
| AuthHub 不可达 / 5xx | 502 | `{"detail": "Auth backend unavailable"}` |
| CORS preflight (OPTIONS) | — | 不鉴权，直接放行 |

## FastAPI 接入

```python
from fastapi import FastAPI, Depends
from authhub_sdk.fastapi import AuthhubContext, authhub_required

app = FastAPI()
guard = authhub_required()  # 从 AUTHHUB_* env 读配置

@app.get("/api/v1/projects/{project_id}")
async def get_project(
    project_id: str,
    ctx: AuthhubContext = Depends(guard),
):
    return {
        "project_id": project_id,
        "viewer": ctx.user_id,
        "decision": ctx.outcome.decision,
    }
```

按 view 粒度切换鉴权：把不需要鉴权的 view 不依赖 `guard` 即可。或者全局挂依赖：

```python
app = FastAPI(dependencies=[Depends(guard)])
```

错误响应（标准 FastAPI HTTPException）：

- 401 `{"detail": "Unauthorized: missing bearer token"}`
- 403 `{"detail": "<reason>"}`
- 502 `{"detail": "Auth backend unavailable"}`

## 登录链路（前端直调 AuthHub）

```
┌──────────┐                ┌──────────┐                ┌────────────┐
│ 子平台前端 │ ──login()──▶ │ AuthHub  │ ──login()────▶ │ PhilVault  │
│          │ ◀─token────── │          │ ◀─token─────── │            │
└──────────┘                └──────────┘                └────────────┘
     │
     │  Authorization: Bearer <token>
     ▼
┌──────────┐                ┌──────────┐                ┌────────────┐
│ 子平台后端 │ ◀──HTTP────── │ 中间件   │ ─authorize()─▶│  AuthHub   │
│ (view)   │                │ (SDK)    │                │ /sdk/...   │
└──────────┘                └──────────┘                └────────────┘
```

- 用户在子平台前端直接 `POST /api/v2/auth/login` 到 **AuthHub**（不经过子平台后端）
- 拿到 `access_token` 后所有业务请求都带 `Authorization: Bearer <token>`
- 子平台后端的中间件收到请求 → 调 `POST /api/v2/sdk/authorize` → 按 `allow` 字段放/拒
- token 过期：前端用 `refresh_token` 调 `/api/v2/auth/refresh` 拿新 access_token

## 异常体系

```python
from authhub_sdk import AuthhubError, AuthhubHTTPError, AuthhubUnauthorized

try:
    client.authorize(token, method="GET", path="/api/v1/items")
except AuthhubUnauthorized:
    # 401：token 无效或过期。让用户重新登录
    ...
except AuthhubHTTPError as exc:
    # 其它 4xx/5xx。exc.status_code / exc.payload / exc.request_id
    ...
except AuthhubError:
    # 网络层（超时、DNS、连接拒绝）
    ...
```

继承关系：

```
Exception
└── AuthhubError                # 网络层 / SDK 本身错误
    └── AuthhubHTTPError        # 后端返回 4xx/5xx
        └── AuthhubUnauthorized # 后端返回 401（特殊处理用）
```

## 行为约定

- **不缓存**任何 token → user_id 信息。AuthHub 是单点 SOT，权限变更秒级生效
- **OPTIONS** 请求直接放行（CORS preflight 兼容）
- **bypass_paths** 走前缀匹配；不走鉴权但仍注入空 `ctx`（FastAPI）
- **超时默认 5 秒**；线上建议结合熔断和重试策略由调用方控制
- 调用 `/sdk/authorize` 时若 AuthHub 不可达 → SDK 抛 `AuthhubError` → 中间件返回 502；
  AuthHub 决策为 deny → 中间件返回 403。两种情况要分清楚

## 调试

需要看每次鉴权的细节？AuthHub `/sdk/authorize` 返回包含：

```json
{
  "allow": false,
  "decision": "deny",
  "reason": "PhilVault 拒绝：用户 ... 在 cortexa 域对 rbac_cortexa::asset(read) 无授权",
  "user": {"id": "...", "username": "..."},
  "mapping": {"app_code": "cortexa", "method": "GET", "path": "/api/v1/asset/<id>", ...},
  "philvault_resource_id": "rbac_cortexa::asset",
  "action_code": "read"
}
```

在 Django view 里：`print(request.authhub_outcome.raw)` 看完整内容。

打开 AuthHub 「授权排查」页面（`/diagnose`），输入 user + method + path 可以
**离线**还原决策路径，不消耗 PhilVault 调用配额。

## 测试

```bash
cd authhub_sdk
pytest
```

22 个单测覆盖 client / Django 中间件 / FastAPI 依赖三个层，使用
`httpx.MockTransport` 模拟 AuthHub，零外部依赖。

## 版本

0.1.0 —— 初版。后续兼容性以 [SemVer](https://semver.org/) 为准。
