authhub-rbac-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2025 Vortek Inc. and Tuanliu (Hainan Special Economic Zone) Technology Co., Ltd.
2
+
3
+ All rights reserved.
4
+
5
+ 本软件版权归 Vortek Inc.(除中国大陆地区)与 湍流(海南经济特区)科技有限责任公司(中国大陆地区)所有。
6
+
7
+ 请根据许可协议使用本软件。
8
+
9
+ PROPRIETARY AND CONFIDENTIAL
10
+
11
+ This software is proprietary and confidential. Unauthorized copying, transfer, or
12
+ use of this software, via any medium, is strictly prohibited without the express
13
+ written permission of the copyright holders.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,322 @@
1
+ Metadata-Version: 2.4
2
+ Name: authhub-rbac-sdk
3
+ Version: 0.1.0
4
+ Summary: 子平台接入 AuthHub 的官方 Python SDK:登录、运行时鉴权一站直达。
5
+ Author: AuthHub Team
6
+ License: Proprietary
7
+ Keywords: authhub,rbac,philvault,auth,sdk
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: License :: Other/Proprietary License
13
+ Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: httpx>=0.27
18
+ Provides-Extra: django
19
+ Requires-Dist: Django<6,>=4.2; extra == "django"
20
+ Provides-Extra: fastapi
21
+ Requires-Dist: fastapi>=0.110; extra == "fastapi"
22
+ Provides-Extra: all
23
+ Requires-Dist: Django<6,>=4.2; extra == "all"
24
+ Requires-Dist: fastapi>=0.110; extra == "all"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=8; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
28
+ Requires-Dist: Django<6,>=4.2; extra == "dev"
29
+ Requires-Dist: fastapi>=0.110; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # authhub_sdk
33
+
34
+ 子平台(cortexa / athanor / …)接入 AuthHub 的官方 SDK。
35
+
36
+ **核心约束**:子平台**只调 AuthHub**,绝不直接连 PhilVault;登录、token 刷新、
37
+ 每次请求的鉴权决策都走 AuthHub HTTP API(`/api/v2/auth/*`、`/api/v2/sdk/*`)。
38
+ 路由 → 资源/动作 的映射由 AuthHub 服务端匹配,SDK 端无感。
39
+
40
+ ## 与历史 `authz/` 的关系
41
+
42
+ 旧的 `authz/` 是 Vortek/湍流版权的 Django 中间件,直接调用 `philvault_sdk`。
43
+ 本 SDK 是其替代品:
44
+
45
+ | 维度 | 旧 `authz/` (Vortek) | 新 `authhub_sdk/` |
46
+ |---|---|---|
47
+ | 通信对象 | 直连 PhilVault | 只调 AuthHub |
48
+ | 路由映射匹配 | 子平台拉 `/authz/mapping` 本地缓存 | AuthHub 服务端匹配 |
49
+ | 鉴权决策 | 本地 + PhilVault check | 一次 `POST /sdk/authorize` 拿结论 |
50
+ | 跨框架 | 仅 Django | Django + FastAPI |
51
+ | Token 缓存 | 30s 本地缓存 | 默认无缓存(运行时鉴权链路不缓存) |
52
+
53
+ 新接入的子平台**只用本包**,不要再引入 `authz/`。
54
+
55
+ ## 安装
56
+
57
+ 仓库内开发:
58
+
59
+ ```bash
60
+ # 基础安装(只用 AuthhubClient / AsyncAuthhubClient)
61
+ pip install /path/to/authhub/authhub_sdk
62
+
63
+ # Django 接入(自动带上 Django >=4.2)
64
+ pip install "authhub-sdk[django] @ file:///path/to/authhub/authhub_sdk"
65
+
66
+ # FastAPI 接入
67
+ pip install "authhub-sdk[fastapi] @ file:///path/to/authhub/authhub_sdk"
68
+
69
+ # Django + FastAPI 都装
70
+ pip install "authhub-sdk[all] @ file:///path/to/authhub/authhub_sdk"
71
+
72
+ # 开发依赖(含 pytest)
73
+ pip install -e "/path/to/authhub/authhub_sdk[dev]"
74
+ ```
75
+
76
+ 依赖:
77
+
78
+ - `httpx >= 0.27`(基础)
79
+ - Python 3.10+
80
+ - Django >=4.2(extra `[django]`)
81
+ - FastAPI >=0.110(extra `[fastapi]`)
82
+
83
+ > 包采用 `src/` 布局:源码在 `authhub_sdk/src/authhub_sdk/`,安装后通过
84
+ > `import authhub_sdk` 引用,与 Django/FastAPI 上游包不会发生命名冲突。
85
+
86
+ ## 三件套环境变量
87
+
88
+ ```bash
89
+ export AUTHHUB_BASE_URL=http://authhub.internal:6001 # 不带 /api/v2
90
+ export AUTHHUB_APP_CODE=cortexa
91
+ # 可选
92
+ export AUTHHUB_TIMEOUT=5
93
+ export AUTHHUB_VERIFY_SSL=true
94
+ export AUTHHUB_BYPASS_PATHS=/health,/static/,/api/v2/sdk/healthz
95
+ ```
96
+
97
+ `AUTHHUB_BYPASS_PATHS` 是逗号分隔的**前缀**列表,命中后不调 AuthHub。
98
+
99
+ ## 基础用法:HTTP 客户端
100
+
101
+ ### 同步
102
+
103
+ ```python
104
+ from authhub_sdk import AuthhubClient
105
+
106
+ with AuthhubClient.from_env() as client:
107
+ # 登录(子平台前端或后端都可发起)
108
+ result = client.login("alice", "User@123456")
109
+ access_token = result["access_token"]
110
+
111
+ # 当前用户信息
112
+ me = client.me(access_token)
113
+ print(me.user_id, me.username, me.domain_roles)
114
+
115
+ # 每次业务请求前调一次(中间件已自动做了,这里仅演示)
116
+ outcome = client.authorize(
117
+ access_token,
118
+ method="GET",
119
+ path="/api/v1/projects/123",
120
+ )
121
+ if not outcome.allow:
122
+ raise PermissionError(outcome.reason)
123
+ ```
124
+
125
+ ### 异步
126
+
127
+ ```python
128
+ from authhub_sdk import AsyncAuthhubClient
129
+
130
+ async with AsyncAuthhubClient.from_env() as client:
131
+ outcome = await client.authorize(token, method="POST", path="/api/v1/orders")
132
+ ```
133
+
134
+ 显式传配置(不走 env):
135
+
136
+ ```python
137
+ from authhub_sdk import AuthhubClient
138
+ from authhub_sdk.config import AuthhubConfig
139
+
140
+ cfg = AuthhubConfig(base_url="http://authhub.internal:6001", app_code="cortexa")
141
+ client = AuthhubClient(config=cfg)
142
+ ```
143
+
144
+ ## Django 接入
145
+
146
+ ### 1. 安装中间件
147
+
148
+ `settings.py`:
149
+
150
+ ```python
151
+ INSTALLED_APPS = [
152
+ # ...
153
+ ]
154
+
155
+ MIDDLEWARE = [
156
+ "django.middleware.security.SecurityMiddleware",
157
+ "django.middleware.common.CommonMiddleware",
158
+ "corsheaders.middleware.CorsMiddleware", # 若用 corsheaders
159
+ "authhub_sdk.django.AuthhubMiddleware", # ← 放在业务中间件之前
160
+ # ...
161
+ ]
162
+
163
+ # 与 from_env 等价;也可以省略改用环境变量
164
+ AUTHHUB_SDK = {
165
+ "base_url": "http://authhub.internal:6001",
166
+ "app_code": "cortexa",
167
+ "bypass_paths": ["/health/", "/static/", "/admin/login/"],
168
+ }
169
+ ```
170
+
171
+ ### 2. 在 view 里拿用户
172
+
173
+ 中间件鉴权通过后会注入:
174
+
175
+ | 属性 | 类型 | 含义 |
176
+ |---|---|---|
177
+ | `request.authhub_outcome` | `AuthorizeOutcome` | 完整鉴权结果 |
178
+ | `request.authhub_user` | `dict` | PhilVault `/auth/me` 返回的用户字典 |
179
+ | `request.authhub_user_id` | `str` | 用户 ObjectId |
180
+ | `request.authhub_access_token` | `str` | 透出本次请求的 token |
181
+
182
+ ```python
183
+ from django.http import JsonResponse
184
+
185
+ def get_my_projects(request):
186
+ user_id = request.authhub_user_id
187
+ return JsonResponse({"user_id": user_id})
188
+ ```
189
+
190
+ ### 3. 错误响应
191
+
192
+ | 场景 | HTTP | body |
193
+ |---|---|---|
194
+ | 缺/无效 Bearer token | 401 | `{"detail": "Unauthorized: ...", "request_id": "..."}` |
195
+ | AuthHub 决策为 deny | 403 | `{"detail": <reason>, "decision": "deny", "mapping": {...}}` |
196
+ | AuthHub 不可达 / 5xx | 502 | `{"detail": "Auth backend unavailable"}` |
197
+ | CORS preflight (OPTIONS) | — | 不鉴权,直接放行 |
198
+
199
+ ## FastAPI 接入
200
+
201
+ ```python
202
+ from fastapi import FastAPI, Depends
203
+ from authhub_sdk.fastapi import AuthhubContext, authhub_required
204
+
205
+ app = FastAPI()
206
+ guard = authhub_required() # 从 AUTHHUB_* env 读配置
207
+
208
+ @app.get("/api/v1/projects/{project_id}")
209
+ async def get_project(
210
+ project_id: str,
211
+ ctx: AuthhubContext = Depends(guard),
212
+ ):
213
+ return {
214
+ "project_id": project_id,
215
+ "viewer": ctx.user_id,
216
+ "decision": ctx.outcome.decision,
217
+ }
218
+ ```
219
+
220
+ 按 view 粒度切换鉴权:把不需要鉴权的 view 不依赖 `guard` 即可。或者全局挂依赖:
221
+
222
+ ```python
223
+ app = FastAPI(dependencies=[Depends(guard)])
224
+ ```
225
+
226
+ 错误响应(标准 FastAPI HTTPException):
227
+
228
+ - 401 `{"detail": "Unauthorized: missing bearer token"}`
229
+ - 403 `{"detail": "<reason>"}`
230
+ - 502 `{"detail": "Auth backend unavailable"}`
231
+
232
+ ## 登录链路(前端直调 AuthHub)
233
+
234
+ ```
235
+ ┌──────────┐ ┌──────────┐ ┌────────────┐
236
+ │ 子平台前端 │ ──login()──▶ │ AuthHub │ ──login()────▶ │ PhilVault │
237
+ │ │ ◀─token────── │ │ ◀─token─────── │ │
238
+ └──────────┘ └──────────┘ └────────────┘
239
+
240
+ │ Authorization: Bearer <token>
241
+
242
+ ┌──────────┐ ┌──────────┐ ┌────────────┐
243
+ │ 子平台后端 │ ◀──HTTP────── │ 中间件 │ ─authorize()─▶│ AuthHub │
244
+ │ (view) │ │ (SDK) │ │ /sdk/... │
245
+ └──────────┘ └──────────┘ └────────────┘
246
+ ```
247
+
248
+ - 用户在子平台前端直接 `POST /api/v2/auth/login` 到 **AuthHub**(不经过子平台后端)
249
+ - 拿到 `access_token` 后所有业务请求都带 `Authorization: Bearer <token>`
250
+ - 子平台后端的中间件收到请求 → 调 `POST /api/v2/sdk/authorize` → 按 `allow` 字段放/拒
251
+ - token 过期:前端用 `refresh_token` 调 `/api/v2/auth/refresh` 拿新 access_token
252
+
253
+ ## 异常体系
254
+
255
+ ```python
256
+ from authhub_sdk import AuthhubError, AuthhubHTTPError, AuthhubUnauthorized
257
+
258
+ try:
259
+ client.authorize(token, method="GET", path="/api/v1/items")
260
+ except AuthhubUnauthorized:
261
+ # 401:token 无效或过期。让用户重新登录
262
+ ...
263
+ except AuthhubHTTPError as exc:
264
+ # 其它 4xx/5xx。exc.status_code / exc.payload / exc.request_id
265
+ ...
266
+ except AuthhubError:
267
+ # 网络层(超时、DNS、连接拒绝)
268
+ ...
269
+ ```
270
+
271
+ 继承关系:
272
+
273
+ ```
274
+ Exception
275
+ └── AuthhubError # 网络层 / SDK 本身错误
276
+ └── AuthhubHTTPError # 后端返回 4xx/5xx
277
+ └── AuthhubUnauthorized # 后端返回 401(特殊处理用)
278
+ ```
279
+
280
+ ## 行为约定
281
+
282
+ - **不缓存**任何 token → user_id 信息。AuthHub 是单点 SOT,权限变更秒级生效
283
+ - **OPTIONS** 请求直接放行(CORS preflight 兼容)
284
+ - **bypass_paths** 走前缀匹配;不走鉴权但仍注入空 `ctx`(FastAPI)
285
+ - **超时默认 5 秒**;线上建议结合熔断和重试策略由调用方控制
286
+ - 调用 `/sdk/authorize` 时若 AuthHub 不可达 → SDK 抛 `AuthhubError` → 中间件返回 502;
287
+ AuthHub 决策为 deny → 中间件返回 403。两种情况要分清楚
288
+
289
+ ## 调试
290
+
291
+ 需要看每次鉴权的细节?AuthHub `/sdk/authorize` 返回包含:
292
+
293
+ ```json
294
+ {
295
+ "allow": false,
296
+ "decision": "deny",
297
+ "reason": "PhilVault 拒绝:用户 ... 在 cortexa 域对 rbac_cortexa::asset(read) 无授权",
298
+ "user": {"id": "...", "username": "..."},
299
+ "mapping": {"app_code": "cortexa", "method": "GET", "path": "/api/v1/asset/<id>", ...},
300
+ "philvault_resource_id": "rbac_cortexa::asset",
301
+ "action_code": "read"
302
+ }
303
+ ```
304
+
305
+ 在 Django view 里:`print(request.authhub_outcome.raw)` 看完整内容。
306
+
307
+ 打开 AuthHub 「授权排查」页面(`/diagnose`),输入 user + method + path 可以
308
+ **离线**还原决策路径,不消耗 PhilVault 调用配额。
309
+
310
+ ## 测试
311
+
312
+ ```bash
313
+ cd authhub_sdk
314
+ pytest
315
+ ```
316
+
317
+ 22 个单测覆盖 client / Django 中间件 / FastAPI 依赖三个层,使用
318
+ `httpx.MockTransport` 模拟 AuthHub,零外部依赖。
319
+
320
+ ## 版本
321
+
322
+ 0.1.0 —— 初版。后续兼容性以 [SemVer](https://semver.org/) 为准。
@@ -0,0 +1,291 @@
1
+ # authhub_sdk
2
+
3
+ 子平台(cortexa / athanor / …)接入 AuthHub 的官方 SDK。
4
+
5
+ **核心约束**:子平台**只调 AuthHub**,绝不直接连 PhilVault;登录、token 刷新、
6
+ 每次请求的鉴权决策都走 AuthHub HTTP API(`/api/v2/auth/*`、`/api/v2/sdk/*`)。
7
+ 路由 → 资源/动作 的映射由 AuthHub 服务端匹配,SDK 端无感。
8
+
9
+ ## 与历史 `authz/` 的关系
10
+
11
+ 旧的 `authz/` 是 Vortek/湍流版权的 Django 中间件,直接调用 `philvault_sdk`。
12
+ 本 SDK 是其替代品:
13
+
14
+ | 维度 | 旧 `authz/` (Vortek) | 新 `authhub_sdk/` |
15
+ |---|---|---|
16
+ | 通信对象 | 直连 PhilVault | 只调 AuthHub |
17
+ | 路由映射匹配 | 子平台拉 `/authz/mapping` 本地缓存 | AuthHub 服务端匹配 |
18
+ | 鉴权决策 | 本地 + PhilVault check | 一次 `POST /sdk/authorize` 拿结论 |
19
+ | 跨框架 | 仅 Django | Django + FastAPI |
20
+ | Token 缓存 | 30s 本地缓存 | 默认无缓存(运行时鉴权链路不缓存) |
21
+
22
+ 新接入的子平台**只用本包**,不要再引入 `authz/`。
23
+
24
+ ## 安装
25
+
26
+ 仓库内开发:
27
+
28
+ ```bash
29
+ # 基础安装(只用 AuthhubClient / AsyncAuthhubClient)
30
+ pip install /path/to/authhub/authhub_sdk
31
+
32
+ # Django 接入(自动带上 Django >=4.2)
33
+ pip install "authhub-sdk[django] @ file:///path/to/authhub/authhub_sdk"
34
+
35
+ # FastAPI 接入
36
+ pip install "authhub-sdk[fastapi] @ file:///path/to/authhub/authhub_sdk"
37
+
38
+ # Django + FastAPI 都装
39
+ pip install "authhub-sdk[all] @ file:///path/to/authhub/authhub_sdk"
40
+
41
+ # 开发依赖(含 pytest)
42
+ pip install -e "/path/to/authhub/authhub_sdk[dev]"
43
+ ```
44
+
45
+ 依赖:
46
+
47
+ - `httpx >= 0.27`(基础)
48
+ - Python 3.10+
49
+ - Django >=4.2(extra `[django]`)
50
+ - FastAPI >=0.110(extra `[fastapi]`)
51
+
52
+ > 包采用 `src/` 布局:源码在 `authhub_sdk/src/authhub_sdk/`,安装后通过
53
+ > `import authhub_sdk` 引用,与 Django/FastAPI 上游包不会发生命名冲突。
54
+
55
+ ## 三件套环境变量
56
+
57
+ ```bash
58
+ export AUTHHUB_BASE_URL=http://authhub.internal:6001 # 不带 /api/v2
59
+ export AUTHHUB_APP_CODE=cortexa
60
+ # 可选
61
+ export AUTHHUB_TIMEOUT=5
62
+ export AUTHHUB_VERIFY_SSL=true
63
+ export AUTHHUB_BYPASS_PATHS=/health,/static/,/api/v2/sdk/healthz
64
+ ```
65
+
66
+ `AUTHHUB_BYPASS_PATHS` 是逗号分隔的**前缀**列表,命中后不调 AuthHub。
67
+
68
+ ## 基础用法:HTTP 客户端
69
+
70
+ ### 同步
71
+
72
+ ```python
73
+ from authhub_sdk import AuthhubClient
74
+
75
+ with AuthhubClient.from_env() as client:
76
+ # 登录(子平台前端或后端都可发起)
77
+ result = client.login("alice", "User@123456")
78
+ access_token = result["access_token"]
79
+
80
+ # 当前用户信息
81
+ me = client.me(access_token)
82
+ print(me.user_id, me.username, me.domain_roles)
83
+
84
+ # 每次业务请求前调一次(中间件已自动做了,这里仅演示)
85
+ outcome = client.authorize(
86
+ access_token,
87
+ method="GET",
88
+ path="/api/v1/projects/123",
89
+ )
90
+ if not outcome.allow:
91
+ raise PermissionError(outcome.reason)
92
+ ```
93
+
94
+ ### 异步
95
+
96
+ ```python
97
+ from authhub_sdk import AsyncAuthhubClient
98
+
99
+ async with AsyncAuthhubClient.from_env() as client:
100
+ outcome = await client.authorize(token, method="POST", path="/api/v1/orders")
101
+ ```
102
+
103
+ 显式传配置(不走 env):
104
+
105
+ ```python
106
+ from authhub_sdk import AuthhubClient
107
+ from authhub_sdk.config import AuthhubConfig
108
+
109
+ cfg = AuthhubConfig(base_url="http://authhub.internal:6001", app_code="cortexa")
110
+ client = AuthhubClient(config=cfg)
111
+ ```
112
+
113
+ ## Django 接入
114
+
115
+ ### 1. 安装中间件
116
+
117
+ `settings.py`:
118
+
119
+ ```python
120
+ INSTALLED_APPS = [
121
+ # ...
122
+ ]
123
+
124
+ MIDDLEWARE = [
125
+ "django.middleware.security.SecurityMiddleware",
126
+ "django.middleware.common.CommonMiddleware",
127
+ "corsheaders.middleware.CorsMiddleware", # 若用 corsheaders
128
+ "authhub_sdk.django.AuthhubMiddleware", # ← 放在业务中间件之前
129
+ # ...
130
+ ]
131
+
132
+ # 与 from_env 等价;也可以省略改用环境变量
133
+ AUTHHUB_SDK = {
134
+ "base_url": "http://authhub.internal:6001",
135
+ "app_code": "cortexa",
136
+ "bypass_paths": ["/health/", "/static/", "/admin/login/"],
137
+ }
138
+ ```
139
+
140
+ ### 2. 在 view 里拿用户
141
+
142
+ 中间件鉴权通过后会注入:
143
+
144
+ | 属性 | 类型 | 含义 |
145
+ |---|---|---|
146
+ | `request.authhub_outcome` | `AuthorizeOutcome` | 完整鉴权结果 |
147
+ | `request.authhub_user` | `dict` | PhilVault `/auth/me` 返回的用户字典 |
148
+ | `request.authhub_user_id` | `str` | 用户 ObjectId |
149
+ | `request.authhub_access_token` | `str` | 透出本次请求的 token |
150
+
151
+ ```python
152
+ from django.http import JsonResponse
153
+
154
+ def get_my_projects(request):
155
+ user_id = request.authhub_user_id
156
+ return JsonResponse({"user_id": user_id})
157
+ ```
158
+
159
+ ### 3. 错误响应
160
+
161
+ | 场景 | HTTP | body |
162
+ |---|---|---|
163
+ | 缺/无效 Bearer token | 401 | `{"detail": "Unauthorized: ...", "request_id": "..."}` |
164
+ | AuthHub 决策为 deny | 403 | `{"detail": <reason>, "decision": "deny", "mapping": {...}}` |
165
+ | AuthHub 不可达 / 5xx | 502 | `{"detail": "Auth backend unavailable"}` |
166
+ | CORS preflight (OPTIONS) | — | 不鉴权,直接放行 |
167
+
168
+ ## FastAPI 接入
169
+
170
+ ```python
171
+ from fastapi import FastAPI, Depends
172
+ from authhub_sdk.fastapi import AuthhubContext, authhub_required
173
+
174
+ app = FastAPI()
175
+ guard = authhub_required() # 从 AUTHHUB_* env 读配置
176
+
177
+ @app.get("/api/v1/projects/{project_id}")
178
+ async def get_project(
179
+ project_id: str,
180
+ ctx: AuthhubContext = Depends(guard),
181
+ ):
182
+ return {
183
+ "project_id": project_id,
184
+ "viewer": ctx.user_id,
185
+ "decision": ctx.outcome.decision,
186
+ }
187
+ ```
188
+
189
+ 按 view 粒度切换鉴权:把不需要鉴权的 view 不依赖 `guard` 即可。或者全局挂依赖:
190
+
191
+ ```python
192
+ app = FastAPI(dependencies=[Depends(guard)])
193
+ ```
194
+
195
+ 错误响应(标准 FastAPI HTTPException):
196
+
197
+ - 401 `{"detail": "Unauthorized: missing bearer token"}`
198
+ - 403 `{"detail": "<reason>"}`
199
+ - 502 `{"detail": "Auth backend unavailable"}`
200
+
201
+ ## 登录链路(前端直调 AuthHub)
202
+
203
+ ```
204
+ ┌──────────┐ ┌──────────┐ ┌────────────┐
205
+ │ 子平台前端 │ ──login()──▶ │ AuthHub │ ──login()────▶ │ PhilVault │
206
+ │ │ ◀─token────── │ │ ◀─token─────── │ │
207
+ └──────────┘ └──────────┘ └────────────┘
208
+
209
+ │ Authorization: Bearer <token>
210
+
211
+ ┌──────────┐ ┌──────────┐ ┌────────────┐
212
+ │ 子平台后端 │ ◀──HTTP────── │ 中间件 │ ─authorize()─▶│ AuthHub │
213
+ │ (view) │ │ (SDK) │ │ /sdk/... │
214
+ └──────────┘ └──────────┘ └────────────┘
215
+ ```
216
+
217
+ - 用户在子平台前端直接 `POST /api/v2/auth/login` 到 **AuthHub**(不经过子平台后端)
218
+ - 拿到 `access_token` 后所有业务请求都带 `Authorization: Bearer <token>`
219
+ - 子平台后端的中间件收到请求 → 调 `POST /api/v2/sdk/authorize` → 按 `allow` 字段放/拒
220
+ - token 过期:前端用 `refresh_token` 调 `/api/v2/auth/refresh` 拿新 access_token
221
+
222
+ ## 异常体系
223
+
224
+ ```python
225
+ from authhub_sdk import AuthhubError, AuthhubHTTPError, AuthhubUnauthorized
226
+
227
+ try:
228
+ client.authorize(token, method="GET", path="/api/v1/items")
229
+ except AuthhubUnauthorized:
230
+ # 401:token 无效或过期。让用户重新登录
231
+ ...
232
+ except AuthhubHTTPError as exc:
233
+ # 其它 4xx/5xx。exc.status_code / exc.payload / exc.request_id
234
+ ...
235
+ except AuthhubError:
236
+ # 网络层(超时、DNS、连接拒绝)
237
+ ...
238
+ ```
239
+
240
+ 继承关系:
241
+
242
+ ```
243
+ Exception
244
+ └── AuthhubError # 网络层 / SDK 本身错误
245
+ └── AuthhubHTTPError # 后端返回 4xx/5xx
246
+ └── AuthhubUnauthorized # 后端返回 401(特殊处理用)
247
+ ```
248
+
249
+ ## 行为约定
250
+
251
+ - **不缓存**任何 token → user_id 信息。AuthHub 是单点 SOT,权限变更秒级生效
252
+ - **OPTIONS** 请求直接放行(CORS preflight 兼容)
253
+ - **bypass_paths** 走前缀匹配;不走鉴权但仍注入空 `ctx`(FastAPI)
254
+ - **超时默认 5 秒**;线上建议结合熔断和重试策略由调用方控制
255
+ - 调用 `/sdk/authorize` 时若 AuthHub 不可达 → SDK 抛 `AuthhubError` → 中间件返回 502;
256
+ AuthHub 决策为 deny → 中间件返回 403。两种情况要分清楚
257
+
258
+ ## 调试
259
+
260
+ 需要看每次鉴权的细节?AuthHub `/sdk/authorize` 返回包含:
261
+
262
+ ```json
263
+ {
264
+ "allow": false,
265
+ "decision": "deny",
266
+ "reason": "PhilVault 拒绝:用户 ... 在 cortexa 域对 rbac_cortexa::asset(read) 无授权",
267
+ "user": {"id": "...", "username": "..."},
268
+ "mapping": {"app_code": "cortexa", "method": "GET", "path": "/api/v1/asset/<id>", ...},
269
+ "philvault_resource_id": "rbac_cortexa::asset",
270
+ "action_code": "read"
271
+ }
272
+ ```
273
+
274
+ 在 Django view 里:`print(request.authhub_outcome.raw)` 看完整内容。
275
+
276
+ 打开 AuthHub 「授权排查」页面(`/diagnose`),输入 user + method + path 可以
277
+ **离线**还原决策路径,不消耗 PhilVault 调用配额。
278
+
279
+ ## 测试
280
+
281
+ ```bash
282
+ cd authhub_sdk
283
+ pytest
284
+ ```
285
+
286
+ 22 个单测覆盖 client / Django 中间件 / FastAPI 依赖三个层,使用
287
+ `httpx.MockTransport` 模拟 AuthHub,零外部依赖。
288
+
289
+ ## 版本
290
+
291
+ 0.1.0 —— 初版。后续兼容性以 [SemVer](https://semver.org/) 为准。
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "authhub-rbac-sdk"
7
+ version = "0.1.0"
8
+ description = "子平台接入 AuthHub 的官方 Python SDK:登录、运行时鉴权一站直达。"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Proprietary" }
12
+ authors = [{ name = "AuthHub Team" }]
13
+ keywords = ["authhub", "rbac", "philvault", "auth", "sdk"]
14
+ classifiers = [
15
+ "Intended Audience :: Developers",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "License :: Other/Proprietary License",
20
+ "Topic :: System :: Systems Administration :: Authentication/Directory",
21
+ ]
22
+ dependencies = [
23
+ "httpx>=0.27",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ django = ["Django>=4.2,<6"]
28
+ fastapi = ["fastapi>=0.110"]
29
+ all = [
30
+ "Django>=4.2,<6",
31
+ "fastapi>=0.110",
32
+ ]
33
+ dev = [
34
+ "pytest>=8",
35
+ "pytest-asyncio>=0.23",
36
+ "Django>=4.2,<6",
37
+ "fastapi>=0.110",
38
+ ]
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
42
+
43
+ [tool.pytest.ini_options]
44
+ asyncio_mode = "auto"
45
+ testpaths = ["tests"]
46
+ pythonpath = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+