82 lines
3.7 KiB
Python
82 lines
3.7 KiB
Python
"""多租户公共依赖 — 账套模式:token优先,query/header降级为Bot服务白名单"""
|
||
from fastapi import Request, Header, Query, Depends, HTTPException
|
||
from typing import Optional
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.database import get_db
|
||
from app.auth_middleware import get_token_entity_id
|
||
from app.models import Entity
|
||
|
||
|
||
def get_entity_id(
|
||
request: Request,
|
||
x_entity_id: Optional[str] = Header(None, alias="X-Entity-Id"),
|
||
entity_id: Optional[int] = Query(None, ge=1),
|
||
db: Session = Depends(get_db),
|
||
) -> int:
|
||
"""解析当前企业ID(账套模式):token优先 → query/header(Bot白名单)→ 默认1
|
||
|
||
解析链(倒置后):
|
||
1. Authorization Bearer token → 优先返回 token.entity_id(唯一可信来源)
|
||
2. query参数 / X-Entity-Id header → 仅Bot服务通道使用(校验entity状态active)
|
||
3. body中的 entity_id(POST场景)→ Bot服务通道兼容
|
||
4. 兜底默认 1(酣客)
|
||
|
||
安全说明:登录用户必须通过token绑定账套,query/header传入的entity_id
|
||
在token存在时被忽略,防止越权传参(旧漏洞:query优先且无授权校验)。
|
||
"""
|
||
# 1. token优先(账套模式唯一来源)
|
||
auth = request.headers.get("Authorization", "")
|
||
if auth.startswith("Bearer "):
|
||
token_entity = get_token_entity_id(auth[7:])
|
||
if token_entity is not None:
|
||
# 越权防护:显式传入的 query/header entity_id 与 token 绑定不一致 → 403
|
||
explicit = None
|
||
if entity_id is not None:
|
||
explicit = entity_id
|
||
elif x_entity_id and x_entity_id.isdigit():
|
||
explicit = int(x_entity_id)
|
||
if explicit is not None and explicit != token_entity:
|
||
raise HTTPException(403, f"无权访问企业 entity_id={explicit}(当前账套: {token_entity})")
|
||
return token_entity
|
||
# token存在但是旧格式/无entity → 账套模式下强制走白名单或默认(由require_auth拦截)
|
||
# 这里不抛401:公开接口可能带旧token,交给require_auth统一处理
|
||
|
||
# 2. query/header → Bot服务通道白名单(校验entity状态active)
|
||
candidate = None
|
||
if entity_id is not None:
|
||
candidate = entity_id
|
||
elif x_entity_id and x_entity_id.isdigit():
|
||
candidate = int(x_entity_id)
|
||
|
||
# 3. body 中的 entity_id(POST场景,Bot通道兼容)
|
||
if candidate is None and request.method in ("POST", "PUT", "PATCH"):
|
||
try:
|
||
body = request.state.body_json or {}
|
||
if body.get("entity_id"):
|
||
candidate = int(body["entity_id"])
|
||
except Exception:
|
||
pass
|
||
|
||
if candidate is not None:
|
||
ent = db.query(Entity).filter(Entity.id == candidate).first()
|
||
if ent and ent.status == "active":
|
||
return candidate
|
||
# Bot通道校验不通过:不返回默认,直接拒绝(防越权注入无效entity)
|
||
raise HTTPException(403, f"企业 entity_id={candidate} 不存在或未激活")
|
||
|
||
return 1 # 默认酣客(无token、无参数时兜底,兼容存量公开接口)
|
||
|
||
|
||
def resolve_entity_for_request(request: Request, fallback: int = 1) -> int:
|
||
"""账套模式:请求级entity解析(供从body读取entity_id的接口使用)
|
||
登录用户(带Bearer token)→ token绑定的entity(唯一来源)
|
||
无token(Bot服务通道)→ 回退到调用方传入的fallback(body中的entity_id等)
|
||
"""
|
||
auth = request.headers.get("Authorization", "")
|
||
if auth.startswith("Bearer "):
|
||
token_entity = get_token_entity_id(auth[7:])
|
||
if token_entity is not None:
|
||
return token_entity
|
||
return fallback
|