15fb73791f
## 需求背景 将比价、短信与登录、广告、引导与账号、风控免告警等限制统一配置,并支持按手机号或设备设置有有效期的临时白名单。 ## 主要改动 - 新增统一限制策略注册表、全局 JSON 配置与白名单覆盖表 - 新增白名单管理、设备检索、批量追加与主体统一编辑接口 - 接入比价、短信登录、广告奖励、引导视频、账号换绑及风险告警调用链 - 保留旧配置接口兼容,并同步统一策略全局值 - 增加单主体唯一有效期、恢复全局、审计日志和风险事件自动处理 - 增加数据库迁移及完整回归测试 ## 验证 - 白名单、权限、配置及风控测试 50 项通过 - 短信、登录、比价、广告关联测试 98 项通过 - Ruff 与 Python 编译检查通过 - Alembic 保持单一 head - 已同步最新 main --------- Co-authored-by: unknown <798648091@qq.com> Reviewed-on: #207 Co-authored-by: linkeyu <linkeyu@wonderable.ai> Co-committed-by: linkeyu <linkeyu@wonderable.ai>
839 lines
26 KiB
Python
839 lines
26 KiB
Python
"""Unified limit rules and per-phone/device whitelist overrides."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
|
|
|
from app.admin.audit import write_audit
|
|
from app.admin.deps import AdminDb, CurrentAdmin, get_client_ip, require_page
|
|
from app.admin.repositories import limit_whitelist as repo
|
|
from app.admin.schemas.limit_whitelist import (
|
|
DeviceCandidateOut,
|
|
GlobalLimitUpdate,
|
|
LimitOverrideBulkWrite,
|
|
LimitOverrideList,
|
|
LimitOverrideOut,
|
|
LimitOverridePatch,
|
|
LimitOverrideWrite,
|
|
LimitRuleOut,
|
|
LimitSubjectEnabledPatch,
|
|
LimitSubjectList,
|
|
LimitSubjectOut,
|
|
)
|
|
from app.core import limit_policy
|
|
from app.models.limit_policy import LimitPolicyOverride
|
|
from app.repositories import risk as risk_repo
|
|
|
|
router = APIRouter(
|
|
prefix="/admin/api/limit-whitelist",
|
|
tags=["admin-limit-whitelist"],
|
|
dependencies=[Depends(require_page("limit-whitelist"))],
|
|
)
|
|
|
|
_WHITELIST_MODES = {
|
|
limit_policy.MODE_UNLIMITED,
|
|
limit_policy.MODE_SUPPRESS_ALERT,
|
|
}
|
|
|
|
|
|
def _subject_whitelist_rows(
|
|
db,
|
|
*,
|
|
subject_type: str,
|
|
subject_value: str,
|
|
) -> list[LimitPolicyOverride]:
|
|
return [
|
|
row
|
|
for row in repo.rows_for_subject(
|
|
db,
|
|
subject_type=subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
if row.mode in _WHITELIST_MODES
|
|
]
|
|
|
|
|
|
def _reconcile_risk_rule(db, rule_code: str) -> None:
|
|
now = risk_repo.utcnow()
|
|
if rule_code == "risk.sms.hourly":
|
|
risk_repo.reconcile_behavior_rule(
|
|
db,
|
|
rule_code=risk_repo.RULE_SMS_HOURLY,
|
|
at=now,
|
|
commit=False,
|
|
)
|
|
elif rule_code == "risk.oneclick.daily":
|
|
risk_repo.reconcile_behavior_rule(
|
|
db,
|
|
rule_code=risk_repo.RULE_ONECLICK_DAILY,
|
|
at=now,
|
|
commit=False,
|
|
)
|
|
elif rule_code == "risk.compare.daily":
|
|
risk_repo.reconcile_compare_rule(db, at=now, commit=False)
|
|
|
|
|
|
def _row_or_404(db, override_id: int) -> LimitPolicyOverride:
|
|
row = db.get(LimitPolicyOverride, override_id, populate_existing=True)
|
|
if row is None:
|
|
raise HTTPException(status_code=404, detail="白名单配置不存在")
|
|
return row
|
|
|
|
|
|
def _out(db, row: LimitPolicyOverride) -> LimitOverrideOut:
|
|
return LimitOverrideOut(**repo.to_dict(db, row))
|
|
|
|
|
|
def _audit_payload(value: LimitOverrideOut) -> dict:
|
|
return value.model_dump(mode="json")
|
|
|
|
|
|
def _subject_out(
|
|
db,
|
|
*,
|
|
subject_type: str,
|
|
subject_value: str,
|
|
rows: list[LimitPolicyOverride],
|
|
created_at,
|
|
) -> LimitSubjectOut:
|
|
items = [_out(db, row) for row in rows]
|
|
group_counts: dict[str, int] = {}
|
|
for item in items:
|
|
group_counts[item.rule_group] = group_counts.get(item.rule_group, 0) + 1
|
|
return LimitSubjectOut(
|
|
subject_type=subject_type,
|
|
subject_value=subject_value,
|
|
group_counts=group_counts,
|
|
total_rules=len(items),
|
|
items=items,
|
|
created_at=created_at,
|
|
updated_at=max(
|
|
(item.updated_at for item in items),
|
|
default=created_at,
|
|
),
|
|
)
|
|
|
|
|
|
@router.get("/rules", response_model=list[LimitRuleOut], summary="读取所有限制规则")
|
|
def list_rules(db: AdminDb) -> list[LimitRuleOut]:
|
|
return [LimitRuleOut(**item) for item in limit_policy.rule_catalog(db)]
|
|
|
|
|
|
@router.patch(
|
|
"/rules/{rule_code}",
|
|
response_model=LimitRuleOut,
|
|
summary="修改规则的全局限制值",
|
|
)
|
|
def update_rule(
|
|
rule_code: str,
|
|
body: GlobalLimitUpdate,
|
|
request: Request,
|
|
admin: CurrentAdmin,
|
|
db: AdminDb,
|
|
) -> LimitRuleOut:
|
|
try:
|
|
before, after = repo.update_global_limit(db, rule_code, body.value, admin_id=admin.id)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail="未知限制规则") from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
_reconcile_risk_rule(db, rule_code)
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="limit.rule.update",
|
|
target_type="limit_rule",
|
|
target_id=rule_code,
|
|
detail={"before": before, "after": after},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
item = next(item for item in limit_policy.rule_catalog(db) if item["code"] == rule_code)
|
|
return LimitRuleOut(**item)
|
|
|
|
|
|
@router.get(
|
|
"/device-candidates",
|
|
response_model=list[DeviceCandidateOut],
|
|
summary="按限制项搜索可加入白名单的设备",
|
|
)
|
|
def list_device_candidates(
|
|
db: AdminDb,
|
|
rule_code: str = Query(..., min_length=1, max_length=64),
|
|
keyword: str | None = Query(None, max_length=128),
|
|
limit: int = Query(30, ge=1, le=50),
|
|
) -> list[DeviceCandidateOut]:
|
|
try:
|
|
rows = repo.list_device_candidates(
|
|
db,
|
|
rule_code=rule_code,
|
|
keyword=keyword,
|
|
limit=limit,
|
|
)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail="未知限制规则") from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return [DeviceCandidateOut(**row) for row in rows]
|
|
|
|
|
|
@router.get("", response_model=LimitOverrideList, summary="读取白名单配置")
|
|
def list_overrides(
|
|
db: AdminDb,
|
|
subject_type: str | None = Query(None, pattern="^(phone|device)$"),
|
|
keyword: str | None = Query(None, max_length=128),
|
|
rule_code: str | None = Query(None, max_length=64),
|
|
offset: int = Query(0, ge=0),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
) -> LimitOverrideList:
|
|
rows, total = repo.list_rows(
|
|
db,
|
|
subject_type=subject_type,
|
|
keyword=keyword,
|
|
rule_code=rule_code,
|
|
offset=offset,
|
|
limit=limit,
|
|
)
|
|
return LimitOverrideList(items=[_out(db, row) for row in rows], total=total)
|
|
|
|
|
|
@router.get(
|
|
"/subjects",
|
|
response_model=LimitSubjectList,
|
|
summary="按手机号或设备聚合读取白名单配置",
|
|
)
|
|
def list_override_subjects(
|
|
db: AdminDb,
|
|
subject_type: str | None = Query(None, pattern="^(phone|device)$"),
|
|
keyword: str | None = Query(None, max_length=128),
|
|
rule_code: str | None = Query(None, max_length=64),
|
|
offset: int = Query(0, ge=0),
|
|
limit: int = Query(10, ge=1, le=100),
|
|
) -> LimitSubjectList:
|
|
subjects, total = repo.list_subject_rows(
|
|
db,
|
|
subject_type=subject_type,
|
|
keyword=keyword,
|
|
rule_code=rule_code,
|
|
offset=offset,
|
|
limit=limit,
|
|
)
|
|
return LimitSubjectList(
|
|
items=[
|
|
_subject_out(
|
|
db,
|
|
subject_type=item_subject_type,
|
|
subject_value=subject_value,
|
|
rows=rows,
|
|
created_at=created_at,
|
|
)
|
|
for item_subject_type, subject_value, rows, created_at in subjects
|
|
],
|
|
total=total,
|
|
)
|
|
|
|
|
|
@router.patch(
|
|
"/subjects/enabled",
|
|
response_model=LimitSubjectOut,
|
|
summary="整体启用或停用一个手机号或设备的白名单",
|
|
)
|
|
def set_override_subject_enabled(
|
|
body: LimitSubjectEnabledPatch,
|
|
request: Request,
|
|
admin: CurrentAdmin,
|
|
db: AdminDb,
|
|
) -> LimitSubjectOut:
|
|
try:
|
|
subject_value = limit_policy.validate_whitelist_subject(
|
|
body.subject_type, body.subject_value
|
|
)
|
|
repo.lock_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
rows = _subject_whitelist_rows(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
if not rows:
|
|
raise HTTPException(status_code=404, detail="白名单主体不存在")
|
|
before = [_audit_payload(_out(db, row)) for row in rows]
|
|
for row in rows:
|
|
repo.update(
|
|
db,
|
|
row,
|
|
enabled=body.enabled,
|
|
starts_at=None,
|
|
expires_at=None,
|
|
reason=None,
|
|
fields_set={"enabled"},
|
|
)
|
|
for rule_code in {row.rule_code for row in rows}:
|
|
_reconcile_risk_rule(db, rule_code)
|
|
after = [_audit_payload(_out(db, row)) for row in rows]
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="limit.override.subject_enabled",
|
|
target_type="limit_override_subject",
|
|
target_id=f"{body.subject_type}:{subject_value}",
|
|
detail={"before": before, "after": after},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
except HTTPException:
|
|
raise
|
|
except (KeyError, ValueError) as exc:
|
|
db.rollback()
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
rows = [
|
|
row
|
|
for row in repo.rows_for_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
if row.mode
|
|
in {
|
|
limit_policy.MODE_UNLIMITED,
|
|
limit_policy.MODE_SUPPRESS_ALERT,
|
|
}
|
|
]
|
|
return _subject_out(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
rows=rows,
|
|
created_at=min(row.created_at for row in rows),
|
|
)
|
|
|
|
|
|
@router.put(
|
|
"/subjects",
|
|
response_model=LimitSubjectOut,
|
|
summary="整体更新一个手机号或设备的白名单限制项",
|
|
)
|
|
def replace_override_subject(
|
|
body: LimitOverrideBulkWrite,
|
|
request: Request,
|
|
admin: CurrentAdmin,
|
|
db: AdminDb,
|
|
) -> LimitSubjectOut:
|
|
current_rule_code = ""
|
|
try:
|
|
subject_value = limit_policy.validate_whitelist_subject(
|
|
body.subject_type, body.subject_value
|
|
)
|
|
repo.lock_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
existing_rows = repo.rows_for_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
subject_created_at = min(
|
|
(row.created_at for row in existing_rows),
|
|
default=None,
|
|
)
|
|
before = [_audit_payload(_out(db, row)) for row in existing_rows]
|
|
existing_by_rule = {row.rule_code: row for row in existing_rows}
|
|
selected_codes = set(body.rule_codes)
|
|
touched_rule_codes = set(selected_codes)
|
|
|
|
for current_rule_code in body.rule_codes:
|
|
rule = limit_policy.get_rule(current_rule_code)
|
|
mode = (
|
|
limit_policy.MODE_SUPPRESS_ALERT if rule.alert_only else limit_policy.MODE_UNLIMITED
|
|
)
|
|
row = existing_by_rule.get(current_rule_code)
|
|
if row is None:
|
|
row = repo.create(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
rule_code=current_rule_code,
|
|
mode=mode,
|
|
limit_value=None,
|
|
enabled=body.enabled,
|
|
starts_at=body.starts_at,
|
|
expires_at=body.expires_at,
|
|
reason=body.reason,
|
|
admin_id=admin.id,
|
|
)
|
|
# The table is ordered by the subject's original creation
|
|
# time. If an edit replaces every rule, carry that timestamp
|
|
# to the new rows so the subject does not jump to the top.
|
|
if subject_created_at is not None:
|
|
row.created_at = subject_created_at
|
|
continue
|
|
|
|
# Historical custom-value rows are converted to the only supported
|
|
# product modes when the administrator selects that rule again.
|
|
row.mode = mode
|
|
row.limit_value = None
|
|
row.reset_at = None
|
|
repo.update(
|
|
db,
|
|
row,
|
|
enabled=body.enabled,
|
|
starts_at=body.starts_at,
|
|
expires_at=body.expires_at,
|
|
reason=body.reason,
|
|
fields_set={"enabled", "starts_at", "expires_at", "reason"},
|
|
)
|
|
|
|
for row in existing_rows:
|
|
if row.rule_code not in selected_codes and row.mode in {
|
|
limit_policy.MODE_UNLIMITED,
|
|
limit_policy.MODE_SUPPRESS_ALERT,
|
|
}:
|
|
touched_rule_codes.add(row.rule_code)
|
|
db.delete(row)
|
|
db.flush()
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail="未知限制规则") from exc
|
|
except repo.DuplicateOverrideError as exc:
|
|
rule = limit_policy.get_rule(current_rule_code)
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail=f"“{rule.label}”已有白名单配置,请刷新后重试",
|
|
) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
for rule_code in touched_rule_codes:
|
|
_reconcile_risk_rule(db, rule_code)
|
|
rows = [
|
|
row
|
|
for row in repo.rows_for_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
if row.mode
|
|
in {
|
|
limit_policy.MODE_UNLIMITED,
|
|
limit_policy.MODE_SUPPRESS_ALERT,
|
|
}
|
|
]
|
|
after = [_audit_payload(_out(db, row)) for row in rows]
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="limit.override.subject_replace",
|
|
target_type="limit_override_subject",
|
|
target_id=f"{body.subject_type}:{subject_value}",
|
|
detail={"before": before, "after": after},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
rows = [
|
|
row
|
|
for row in repo.rows_for_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
if row.mode
|
|
in {
|
|
limit_policy.MODE_UNLIMITED,
|
|
limit_policy.MODE_SUPPRESS_ALERT,
|
|
}
|
|
]
|
|
return _subject_out(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
rows=rows,
|
|
created_at=min(row.created_at for row in rows),
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"",
|
|
response_model=LimitOverrideOut,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="新增白名单配置",
|
|
)
|
|
def create_override(
|
|
body: LimitOverrideWrite,
|
|
request: Request,
|
|
admin: CurrentAdmin,
|
|
db: AdminDb,
|
|
) -> LimitOverrideOut:
|
|
before: list[dict] = []
|
|
touched_rows: list[LimitPolicyOverride] = []
|
|
try:
|
|
subject_value = limit_policy.validate_whitelist_subject(
|
|
body.subject_type, body.subject_value
|
|
)
|
|
repo.lock_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
existing_rows = _subject_whitelist_rows(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
before = [_audit_payload(_out(db, item)) for item in existing_rows]
|
|
row = repo.create(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
rule_code=body.rule_code,
|
|
mode=body.mode,
|
|
enabled=body.enabled,
|
|
starts_at=body.starts_at,
|
|
expires_at=body.expires_at,
|
|
reason=body.reason,
|
|
limit_value=None,
|
|
admin_id=admin.id,
|
|
)
|
|
touched_rows = [*existing_rows, row]
|
|
if row.mode in _WHITELIST_MODES:
|
|
# Keep the legacy single-rule endpoint compatible without letting
|
|
# it create a second validity period for the same logical subject.
|
|
for existing in existing_rows:
|
|
repo.update(
|
|
db,
|
|
existing,
|
|
enabled=body.enabled,
|
|
starts_at=body.starts_at,
|
|
expires_at=body.expires_at,
|
|
reason=None,
|
|
fields_set={"enabled", "starts_at", "expires_at"},
|
|
)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail="未知限制规则") from exc
|
|
except repo.DuplicateOverrideError as exc:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail="该手机号或设备已配置此规则,请编辑现有配置",
|
|
) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
for rule_code in {item.rule_code for item in touched_rows}:
|
|
_reconcile_risk_rule(db, rule_code)
|
|
after_rows = _subject_whitelist_rows(
|
|
db,
|
|
subject_type=row.subject_type,
|
|
subject_value=row.subject_value,
|
|
)
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="limit.override.create",
|
|
target_type="limit_override_subject",
|
|
target_id=f"{row.subject_type}:{row.subject_value}",
|
|
detail={
|
|
"before": before,
|
|
"after": [_audit_payload(_out(db, item)) for item in after_rows],
|
|
},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
return _out(db, row)
|
|
|
|
|
|
@router.post(
|
|
"/bulk",
|
|
response_model=list[LimitOverrideOut],
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="批量新增临时不限或免告警白名单",
|
|
)
|
|
def create_overrides_bulk(
|
|
body: LimitOverrideBulkWrite,
|
|
request: Request,
|
|
admin: CurrentAdmin,
|
|
db: AdminDb,
|
|
) -> list[LimitOverrideOut]:
|
|
rows: list[LimitPolicyOverride] = []
|
|
before: list[dict] = []
|
|
current_rule_code = ""
|
|
try:
|
|
subject_value = limit_policy.validate_whitelist_subject(
|
|
body.subject_type, body.subject_value
|
|
)
|
|
repo.lock_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
existing_rows = repo.rows_for_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
whitelist_modes = {
|
|
limit_policy.MODE_UNLIMITED,
|
|
limit_policy.MODE_SUPPRESS_ALERT,
|
|
}
|
|
existing_whitelist_rows = [row for row in existing_rows if row.mode in whitelist_modes]
|
|
before = [_audit_payload(_out(db, row)) for row in existing_whitelist_rows]
|
|
existing_by_rule = {row.rule_code: row for row in existing_rows}
|
|
selected_codes = set(body.rule_codes)
|
|
for current_rule_code in body.rule_codes:
|
|
rule = limit_policy.get_rule(current_rule_code)
|
|
mode = (
|
|
limit_policy.MODE_SUPPRESS_ALERT if rule.alert_only else limit_policy.MODE_UNLIMITED
|
|
)
|
|
row = existing_by_rule.get(current_rule_code)
|
|
if row is None:
|
|
row = repo.create(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
rule_code=current_rule_code,
|
|
mode=mode,
|
|
limit_value=None,
|
|
enabled=body.enabled,
|
|
starts_at=body.starts_at,
|
|
expires_at=body.expires_at,
|
|
reason=body.reason,
|
|
admin_id=admin.id,
|
|
)
|
|
else:
|
|
row.mode = mode
|
|
row.limit_value = None
|
|
row.reset_at = None
|
|
repo.update(
|
|
db,
|
|
row,
|
|
enabled=body.enabled,
|
|
starts_at=body.starts_at,
|
|
expires_at=body.expires_at,
|
|
reason=body.reason,
|
|
fields_set={
|
|
"enabled",
|
|
"starts_at",
|
|
"expires_at",
|
|
"reason",
|
|
},
|
|
)
|
|
|
|
# 同一手机号或设备在产品上是一条白名单。追加限制项时保留原规则,
|
|
# 但统一使用最后一次配置的启用状态与有效期,避免一个主体出现多套时间。
|
|
rows = [
|
|
row
|
|
for row in repo.rows_for_subject(
|
|
db,
|
|
subject_type=body.subject_type,
|
|
subject_value=subject_value,
|
|
)
|
|
if row.mode in whitelist_modes
|
|
]
|
|
for row in rows:
|
|
if row.rule_code in selected_codes:
|
|
continue
|
|
repo.update(
|
|
db,
|
|
row,
|
|
enabled=body.enabled,
|
|
starts_at=body.starts_at,
|
|
expires_at=body.expires_at,
|
|
reason=None,
|
|
fields_set={"enabled", "starts_at", "expires_at"},
|
|
)
|
|
except KeyError as exc:
|
|
raise HTTPException(status_code=404, detail="未知限制规则") from exc
|
|
except repo.DuplicateOverrideError as exc:
|
|
rule = limit_policy.get_rule(current_rule_code)
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail=f"“{rule.label}”白名单配置发生并发更新,请刷新后重试",
|
|
) from exc
|
|
except ValueError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
|
|
for row in rows:
|
|
_reconcile_risk_rule(db, row.rule_code)
|
|
results = [_out(db, row) for row in rows]
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="limit.override.bulk_create",
|
|
target_type="limit_override",
|
|
target_id=",".join(str(row.id) for row in rows),
|
|
detail={
|
|
"before": before,
|
|
"after": [_audit_payload(item) for item in results],
|
|
},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
return [_out(db, row) for row in rows]
|
|
|
|
|
|
@router.patch(
|
|
"/{override_id}",
|
|
response_model=LimitOverrideOut,
|
|
summary="编辑白名单配置",
|
|
)
|
|
def update_override(
|
|
override_id: int,
|
|
body: LimitOverridePatch,
|
|
request: Request,
|
|
admin: CurrentAdmin,
|
|
db: AdminDb,
|
|
) -> LimitOverrideOut:
|
|
row = _row_or_404(db, override_id)
|
|
repo.lock_subject(
|
|
db,
|
|
subject_type=row.subject_type,
|
|
subject_value=row.subject_value,
|
|
)
|
|
row = _row_or_404(db, override_id)
|
|
subject_rows = (
|
|
_subject_whitelist_rows(
|
|
db,
|
|
subject_type=row.subject_type,
|
|
subject_value=row.subject_value,
|
|
)
|
|
if row.mode in _WHITELIST_MODES
|
|
else [row]
|
|
)
|
|
before = [_audit_payload(_out(db, item)) for item in subject_rows]
|
|
fields_set = set(body.model_fields_set)
|
|
try:
|
|
period_fields = {"enabled", "starts_at", "expires_at"}
|
|
if row.mode in _WHITELIST_MODES and fields_set & period_fields:
|
|
new_enabled = body.enabled if "enabled" in fields_set else row.enabled
|
|
new_starts = body.starts_at if "starts_at" in fields_set else row.starts_at
|
|
new_expires = body.expires_at if "expires_at" in fields_set else row.expires_at
|
|
for item in subject_rows:
|
|
item_fields = set(period_fields)
|
|
if item.id == row.id and "reason" in fields_set:
|
|
item_fields.add("reason")
|
|
repo.update(
|
|
db,
|
|
item,
|
|
enabled=new_enabled,
|
|
starts_at=new_starts,
|
|
expires_at=new_expires,
|
|
reason=body.reason if item.id == row.id else None,
|
|
fields_set=item_fields,
|
|
)
|
|
else:
|
|
repo.update(
|
|
db,
|
|
row,
|
|
**body.model_dump(),
|
|
fields_set=fields_set,
|
|
)
|
|
except (KeyError, ValueError) as exc:
|
|
db.rollback()
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
for rule_code in {item.rule_code for item in subject_rows}:
|
|
_reconcile_risk_rule(db, rule_code)
|
|
after_rows = [_audit_payload(_out(db, item)) for item in subject_rows]
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="limit.override.update",
|
|
target_type="limit_override_subject",
|
|
target_id=f"{row.subject_type}:{row.subject_value}",
|
|
detail={"before": before, "after": after_rows},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
return _out(db, row)
|
|
|
|
|
|
@router.post(
|
|
"/{override_id}/reset",
|
|
response_model=LimitOverrideOut,
|
|
summary="停用例外配置并恢复全局策略",
|
|
)
|
|
def restore_global_policy(
|
|
override_id: int,
|
|
request: Request,
|
|
admin: CurrentAdmin,
|
|
db: AdminDb,
|
|
) -> LimitOverrideOut:
|
|
row = _row_or_404(db, override_id)
|
|
repo.lock_subject(
|
|
db,
|
|
subject_type=row.subject_type,
|
|
subject_value=row.subject_value,
|
|
)
|
|
row = _row_or_404(db, override_id)
|
|
subject_rows = (
|
|
_subject_whitelist_rows(
|
|
db,
|
|
subject_type=row.subject_type,
|
|
subject_value=row.subject_value,
|
|
)
|
|
if row.mode in _WHITELIST_MODES
|
|
else [row]
|
|
)
|
|
before = [_audit_payload(_out(db, item)) for item in subject_rows]
|
|
for item in subject_rows:
|
|
repo.restore_global(item)
|
|
for rule_code in {item.rule_code for item in subject_rows}:
|
|
_reconcile_risk_rule(db, rule_code)
|
|
after = [_audit_payload(_out(db, item)) for item in subject_rows]
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="limit.override.restore_global",
|
|
target_type="limit_override_subject",
|
|
target_id=f"{row.subject_type}:{row.subject_value}",
|
|
detail={"before": before, "after": after},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|
|
return _out(db, row)
|
|
|
|
|
|
@router.delete(
|
|
"/{override_id}",
|
|
status_code=status.HTTP_204_NO_CONTENT,
|
|
summary="删除白名单配置",
|
|
)
|
|
def delete_override(
|
|
override_id: int,
|
|
request: Request,
|
|
admin: CurrentAdmin,
|
|
db: AdminDb,
|
|
) -> None:
|
|
row = _row_or_404(db, override_id)
|
|
repo.lock_subject(
|
|
db,
|
|
subject_type=row.subject_type,
|
|
subject_value=row.subject_value,
|
|
)
|
|
row = _row_or_404(db, override_id)
|
|
before = _audit_payload(_out(db, row))
|
|
target_id = str(row.id)
|
|
rule_code = row.rule_code
|
|
db.delete(row)
|
|
db.flush()
|
|
_reconcile_risk_rule(db, rule_code)
|
|
write_audit(
|
|
db,
|
|
admin,
|
|
action="limit.override.delete",
|
|
target_type="limit_override",
|
|
target_id=target_id,
|
|
detail={"before": before},
|
|
ip=get_client_ip(request),
|
|
commit=False,
|
|
)
|
|
db.commit()
|