Skip to content

Commit dadeb6e

Browse files
author
Alexander Green
committed
feat: 多租户权限与安全体系重构
- 新增 RBAC 权限矩阵: 自定义角色、权限定义、成员权限管理 - 新增 TOTP 双因素认证: 启用、确认、验证完整流程 - 新增配额管理系统: 账户配额模型与执行逻辑 - 新增用户与账户模型: 独立用户认证与多租户账户关联 - 新增安全中间件: Admin 鉴权中间件、依赖注入鉴权 - 新增 gRPC 增强: 流超时控制、设备注销、会话管理优化 - 新增 OOBE 初始化引导模块 - 新增加密服务: 独立 crypto 模块 - 新增 OpenAPI 导出脚本 - 重构 Admin API: 拆分路由为账户/角色/权限/配额/TOTP 子模块 - 重构租户体系: 移除旧 tenant 模型,采用 account 体系 - 重构认证令牌与资源令牌服务 - 增强测试覆盖: 安全攻击、多租户隔离、权限、配额、TOTP 等 - 修复数据库安全: 参数化 schema 操作,防止 SQL 注入 - 更新依赖与项目配置
1 parent d727da9 commit dadeb6e

178 files changed

Lines changed: 5978 additions & 2059 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,3 +190,6 @@ settings.json
190190
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
191191
#.idea/
192192
cims_server.key
193+
194+
# CIMS 本地配置
195+
.cims/

.idea/dataSources.xml

Lines changed: 4 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/db-forest-config.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""HTTP API 路由聚合包。"""

app/api/admin/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""管理端 API 路由子包。"""

app/api/admin/account_routes.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""账户管理路由。
2+
3+
提供账户列表查询和详情查看端点,
4+
需要管理员或以上权限。
5+
"""
6+
7+
from fastapi import APIRouter, Depends, HTTPException
8+
from sqlalchemy import select
9+
from sqlalchemy.ext.asyncio import AsyncSession
10+
11+
from app.models.session import get_db
12+
from app.models.account import Account
13+
from app.core.auth.dependencies import require_role
14+
from app.api.schemas.account import AccountOut
15+
16+
router = APIRouter()
17+
_admin = require_role(90)
18+
19+
20+
@router.get("", response_model=list[AccountOut])
21+
async def list_accounts(
22+
db: AsyncSession = Depends(get_db),
23+
_user=Depends(_admin),
24+
):
25+
"""查询所有账户列表(需管理员)。"""
26+
result = await db.execute(select(Account))
27+
return [_to_out(a) for a in result.scalars().all()]
28+
29+
30+
@router.get("/{account_id}", response_model=AccountOut)
31+
async def get_account(
32+
account_id: str,
33+
db: AsyncSession = Depends(get_db),
34+
_user=Depends(_admin),
35+
):
36+
"""查询单个账户详情(需管理员)。"""
37+
result = await db.execute(select(Account).where(Account.id == account_id))
38+
acct = result.scalar_one_or_none()
39+
if not acct:
40+
raise HTTPException(status_code=404, detail="账户不存在")
41+
return _to_out(acct)
42+
43+
44+
def _to_out(acct) -> AccountOut:
45+
"""将 Account 模型转换为响应模型。"""
46+
return AccountOut(
47+
id=acct.id,
48+
name=acct.name,
49+
slug=acct.slug,
50+
api_key=acct.api_key,
51+
is_active=acct.is_active,
52+
created_at=str(acct.created_at),
53+
)

app/api/admin/auth_routes.py

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,73 @@
11
"""管理端身份认证处理器。
22
3-
管理使用 ADMIN_SECRET 的登录以及令牌注销逻辑
3+
提供用户注册和登录端点,支持 TOTP 2FA 流程
44
"""
55

6-
from fastapi import APIRouter, Body, HTTPException, Request
7-
from app.core.config import ADMIN_SECRET
8-
from app.api.schemas.auth import TokenResponse, AdminLoginRequest
9-
from app.services import auth_token
6+
from fastapi import APIRouter, Body, Depends, HTTPException
7+
from sqlalchemy.ext.asyncio import AsyncSession
8+
9+
from app.models.session import get_db
10+
from app.api.schemas.user import UserRegisterRequest, UserLoginRequest
11+
from app.api.schemas.auth import TokenResponse, MessageResponse
12+
from app.services.user.register import register_user
13+
from app.services.user.login import login_user
1014

1115
router = APIRouter()
1216

1317

14-
@router.post("/login", response_model=TokenResponse)
15-
async def admin_login(
16-
payload: AdminLoginRequest = Body(...),
18+
@router.post("/register", response_model=TokenResponse)
19+
async def user_register(
20+
payload: UserRegisterRequest = Body(...),
21+
db: AsyncSession = Depends(get_db),
1722
):
18-
"""通过全局管理密钥兑换 Admin Bearer 令牌。"""
19-
if payload.secret != ADMIN_SECRET:
20-
raise HTTPException(status_code=403, detail="Invalid secret")
23+
"""注册新用户并自动创建关联账户。"""
24+
try:
25+
user = await register_user(
26+
payload.username,
27+
payload.email,
28+
payload.password,
29+
payload.display_name,
30+
db,
31+
)
32+
except ValueError as exc:
33+
raise HTTPException(status_code=400, detail=str(exc))
34+
from app.services.crypto.token_factory import create_session_token
2135

22-
token = await auth_token.generate_token("admin")
36+
token = await create_session_token(user.id)
2337
return TokenResponse(token=token)
2438

2539

26-
@router.post("/logout")
27-
async def admin_logout(request: Request):
28-
"""使当前的 Admin 会话失效并注销。"""
29-
auth = request.headers.get("authorization", "")
30-
bearer = auth[7:] if auth.startswith("Bearer ") else ""
31-
if bearer:
32-
await auth_token.revoke_token(bearer)
33-
return {"status": "success", "message": "Logged out"}
40+
@router.post("/login")
41+
async def user_login(
42+
payload: UserLoginRequest = Body(...),
43+
db: AsyncSession = Depends(get_db),
44+
):
45+
"""用户登录,支持 2FA 流程。"""
46+
try:
47+
token, user, needs_2fa = await login_user(
48+
payload.email,
49+
payload.password,
50+
db,
51+
)
52+
except ValueError as exc:
53+
raise HTTPException(status_code=401, detail=str(exc))
54+
if needs_2fa:
55+
from app.api.admin.totp_verify import create_2fa_temp_token
56+
57+
temp = await create_2fa_temp_token(user.id)
58+
return {"requires_2fa": True, "temp_token": temp}
59+
return {"token": token}
60+
61+
62+
@router.post("/logout", response_model=MessageResponse)
63+
async def user_logout(request=None):
64+
"""注销当前用户会话。"""
65+
if request:
66+
auth = request.headers.get("authorization", "")
67+
if auth.startswith("Bearer "):
68+
from app.core.redis.accessor import get_redis
69+
from app.core.config import REDIS_DB_AUTH
70+
71+
rd = get_redis(REDIS_DB_AUTH)
72+
await rd.delete(f"session:{auth[7:]}")
73+
return MessageResponse(message="已登出")

app/api/admin/helpers.py

Lines changed: 0 additions & 31 deletions
This file was deleted.

app/api/admin/permission_routes.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""权限管理路由。
2+
3+
提供权限定义查询、授予和撤销端点。
4+
"""
5+
6+
from fastapi import APIRouter, Depends, HTTPException
7+
from sqlalchemy import select
8+
from sqlalchemy.ext.asyncio import AsyncSession
9+
10+
from app.models.session import get_db
11+
from app.models.permission_def import PermissionDef
12+
from app.core.auth.dependencies import require_role
13+
from app.api.schemas.permission import (
14+
PermissionGrantRequest,
15+
PermissionRevokeRequest,
16+
PermissionDefOut,
17+
)
18+
from app.services.permission.grants import (
19+
grant_permission,
20+
revoke_permission,
21+
)
22+
23+
router = APIRouter()
24+
_admin = require_role(90)
25+
26+
27+
@router.get("/defs", response_model=list[PermissionDefOut])
28+
async def list_permission_defs(
29+
db: AsyncSession = Depends(get_db),
30+
_user=Depends(_admin),
31+
):
32+
"""查询所有权限定义(需管理员)。"""
33+
result = await db.execute(select(PermissionDef))
34+
return [
35+
PermissionDefOut(code=p.code, label=p.label, category=p.category)
36+
for p in result.scalars().all()
37+
]
38+
39+
40+
@router.post("/grant")
41+
async def grant(
42+
body: PermissionGrantRequest,
43+
db: AsyncSession = Depends(get_db),
44+
_user=Depends(_admin),
45+
):
46+
"""授予或显式拒绝成员权限(需管理员)。"""
47+
perm = await grant_permission(
48+
body.member_id, body.permission_code, db, granted=body.granted
49+
)
50+
return {"status": "success", "id": perm.id}
51+
52+
53+
@router.post("/revoke")
54+
async def revoke(
55+
body: PermissionRevokeRequest,
56+
db: AsyncSession = Depends(get_db),
57+
_user=Depends(_admin),
58+
):
59+
"""撤销成员权限覆盖(需管理员)。"""
60+
ok = await revoke_permission(body.member_id, body.permission_code, db)
61+
if not ok:
62+
raise HTTPException(status_code=404, detail="权限记录不存在")
63+
return {"status": "success"}

app/api/admin/quota_routes.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""限额管理路由。
2+
3+
提供账户限额查询和设置端点,
4+
需要超级管理员权限。
5+
"""
6+
7+
from fastapi import APIRouter, Depends
8+
from sqlalchemy.ext.asyncio import AsyncSession
9+
10+
from app.models.session import get_db
11+
from app.core.auth.dependencies import require_role
12+
from app.api.schemas.quota import QuotaSetRequest, QuotaOut
13+
from app.services.quota.manager import get_quotas, set_quota
14+
15+
router = APIRouter()
16+
_superadmin = require_role(100)
17+
18+
19+
@router.get("/{account_id}", response_model=list[QuotaOut])
20+
async def list_quotas(
21+
account_id: str,
22+
db: AsyncSession = Depends(get_db),
23+
_user=Depends(_superadmin),
24+
):
25+
"""查询账户的所有限额(需超级管理员)。"""
26+
quotas = await get_quotas(account_id, db)
27+
return [_to_out(q) for q in quotas]
28+
29+
30+
@router.put("/{account_id}", response_model=QuotaOut)
31+
async def update_quota(
32+
account_id: str,
33+
body: QuotaSetRequest,
34+
db: AsyncSession = Depends(get_db),
35+
_user=Depends(_superadmin),
36+
):
37+
"""设置或更新账户限额(需超级管理员)。"""
38+
quota = await set_quota(account_id, body.quota_key, body.max_value, db)
39+
return _to_out(quota)
40+
41+
42+
def _to_out(q) -> QuotaOut:
43+
"""将 AccountQuota 模型转换为响应模型。"""
44+
return QuotaOut(
45+
id=q.id,
46+
account_id=q.account_id,
47+
quota_key=q.quota_key,
48+
max_value=q.max_value,
49+
current_value=q.current_value,
50+
)

0 commit comments

Comments
 (0)