| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- """DMS Bearer 认证与角色授权装饰器。"""
- from __future__ import annotations
- from functools import wraps
- from typing import Any, Callable, TypeVar, cast
- from flask import g, request
- from sqlalchemy import select
- from dms.common.enums import EnabledStatus, RoleCode, SecurityLevel
- from dms.common.errors import (
- AuthenticationError,
- AuthVersionMismatchError,
- ForbiddenError,
- ReauthRequiredError,
- TokenExpiredError,
- UserDisabledError,
- )
- from dms.extensions import db
- from dms.models import User
- from dms.security.auth_context import AuthContext, get_auth_context
- from dms.security.jwt_tokens import (
- REAUTH_TOKEN_TYPE,
- decode_access_token,
- decode_reauth_token,
- )
- F = TypeVar("F", bound=Callable[..., Any])
- def extract_bearer_token() -> str:
- authorization = request.headers.get("Authorization", "")
- scheme, separator, token = authorization.partition(" ")
- if not separator or scheme.lower() != "bearer" or not token.strip():
- raise AuthenticationError()
- return token.strip()
- def authenticate_request(token: str) -> AuthContext:
- claims = decode_access_token(token)
- try:
- user_id = int(claims["sub"])
- token_auth_version = int(claims["authVersion"])
- token_username = str(claims["username"])
- token_role = str(claims["roleCode"])
- except (KeyError, TypeError, ValueError) as exc:
- raise AuthenticationError() from exc
- user = db.session.scalar(select(User).where(User.id == user_id))
- if user is None or user.is_deleted:
- raise AuthenticationError()
- if user.status != EnabledStatus.ENABLED:
- raise UserDisabledError()
- if user.auth_version != token_auth_version:
- raise AuthVersionMismatchError()
- if user.username != token_username or user.role_code != token_role:
- raise AuthenticationError()
- context = AuthContext(
- user_id=user.id,
- username=user.username,
- real_name=user.real_name,
- organization_id=user.organization_id,
- organization_name=user.organization_name,
- role_code=RoleCode(user.role_code),
- security_level=SecurityLevel(user.security_level),
- auth_version=user.auth_version,
- )
- g.dms_auth_context = context
- return context
- def bearer_auth_required(view: F) -> F:
- @wraps(view)
- def wrapped(*args: Any, **kwargs: Any):
- authenticate_request(extract_bearer_token())
- return view(*args, **kwargs)
- return cast(F, wrapped)
- def admin_required(view: F) -> F:
- @wraps(view)
- def wrapped(*args: Any, **kwargs: Any):
- if get_auth_context().role_code != RoleCode.ADMIN:
- raise ForbiddenError()
- return view(*args, **kwargs)
- return cast(F, wrapped)
- def _require_manage_flag(can_manage) -> None:
- """加载当前用户并按管理位判定(admin 账号恒通过)。"""
- context = get_auth_context()
- user = db.session.get(User, context.user_id)
- if user is None or not can_manage(db.session, user):
- raise ForbiddenError()
- def system_management_required(view: F) -> F:
- """系统管理页签接口鉴权(契约1.23):manage_system 管理位。"""
- @wraps(view)
- def wrapped(*args: Any, **kwargs: Any):
- from dms.services.admin_permission import can_manage_system
- _require_manage_flag(can_manage_system)
- return view(*args, **kwargs)
- return cast(F, wrapped)
- def audit_log_required(view: F) -> F:
- """日志审计页签接口鉴权(契约1.23):manage_audit_log 管理位。"""
- @wraps(view)
- def wrapped(*args: Any, **kwargs: Any):
- from dms.services.admin_permission import can_view_audit_log
- _require_manage_flag(can_view_audit_log)
- return view(*args, **kwargs)
- return cast(F, wrapped)
- def role_edit_required(view: F) -> F:
- """角色管理写操作鉴权(契约1.25):manage_role_edit 管理位。"""
- @wraps(view)
- def wrapped(*args: Any, **kwargs: Any):
- from dms.services.admin_permission import can_edit_roles
- _require_manage_flag(can_edit_roles)
- return view(*args, **kwargs)
- return cast(F, wrapped)
- REAUTH_HEADER = "X-DMS-Reauth-Token"
- def require_reauth() -> None:
- """敏感操作二次认证:校验 X-DMS-Reauth-Token。
- 缺失、无效、过期、非本人或 authVersion 不符一律拒绝(403 REAUTH_REQUIRED)。
- """
- context = get_auth_context()
- token = request.headers.get(REAUTH_HEADER, "").strip()
- if not token:
- raise ReauthRequiredError()
- try:
- claims = decode_reauth_token(token)
- if (
- str(claims.get("type")) != REAUTH_TOKEN_TYPE
- or int(claims["sub"]) != context.user_id
- or int(claims["authVersion"]) != context.auth_version
- ):
- raise ReauthRequiredError()
- except (
- AuthenticationError,
- TokenExpiredError,
- KeyError,
- TypeError,
- ValueError,
- ) as exc:
- raise ReauthRequiredError() from exc
- def reauth_required(view: F) -> F:
- @wraps(view)
- def wrapped(*args: Any, **kwargs: Any):
- require_reauth()
- return view(*args, **kwargs)
- return cast(F, wrapped)
|