chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -0,0 +1,384 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 管理后台角色管理 API
|
||||
# =============================================================================
|
||||
# 说明:管理后台的角色管理接口
|
||||
# 包含:
|
||||
# 1. 角色管理(CRUD)
|
||||
# 2. 用户角色分配/撤销
|
||||
# 3. 角色映射规则管理
|
||||
# 所有接口需要 admin 角色权限
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_current_user, UserInfo, require_role
|
||||
from app.database import get_db
|
||||
from app.models.role import Role
|
||||
from app.models.role_mapping_rule import RoleMappingRule
|
||||
from app.models.user_role import UserRole
|
||||
from app.schemas.role import (
|
||||
RoleAssignRequest,
|
||||
RoleMappingRuleRequest,
|
||||
RoleMappingRuleResponse,
|
||||
RoleRevokeRequest,
|
||||
RoleResponse,
|
||||
UserRoleResponse,
|
||||
)
|
||||
from app.utils.response import AppException, success_response
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _mask_sensitive_data(value: str, visible_chars: int = 3) -> str:
|
||||
"""脱敏处理敏感数据。
|
||||
|
||||
Args:
|
||||
value: 原始值
|
||||
visible_chars: 开头保留的字符数
|
||||
|
||||
Returns:
|
||||
str: 脱敏后的值,如 "abc***def"
|
||||
"""
|
||||
if not value:
|
||||
return ""
|
||||
if len(value) <= visible_chars:
|
||||
return "*" * len(value)
|
||||
return f"{value[:visible_chars]}{'*' * (len(value) - visible_chars)}"
|
||||
|
||||
|
||||
# 创建路由器
|
||||
router = APIRouter(prefix="/admin/roles")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# 管理后台权限校验依赖
|
||||
# --------------------------------------------------------------------------
|
||||
async def require_admin(
|
||||
current_user: UserInfo = Depends(get_current_user),
|
||||
) -> UserInfo:
|
||||
"""管理后台权限校验:仅 admin 角色可访问。
|
||||
|
||||
Args:
|
||||
current_user: 当前用户(通过认证依赖注入)
|
||||
|
||||
Returns:
|
||||
UserInfo: 具有管理权限的用户信息
|
||||
|
||||
Raises:
|
||||
AppException: 非管理员角色(错误码 1004)
|
||||
"""
|
||||
if "admin" not in current_user.roles:
|
||||
raise AppException(1004, "无管理权限")
|
||||
return current_user
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 1. 角色管理
|
||||
# ==========================================================================
|
||||
|
||||
# ---------- GET /api/admin/roles ----------
|
||||
@router.get("")
|
||||
async def get_roles(
|
||||
admin: UserInfo = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取所有角色列表。
|
||||
|
||||
返回角色列表,包含每个角色的用户数量统计。
|
||||
|
||||
Args:
|
||||
admin: 管理员(权限校验)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含角色列表
|
||||
"""
|
||||
# 查询所有角色
|
||||
stmt = select(Role).order_by(Role.is_default.desc(), Role.name)
|
||||
result = await db.execute(stmt)
|
||||
roles = result.scalars().all()
|
||||
|
||||
# 构建响应,包含用户数量
|
||||
role_list = []
|
||||
for role in roles:
|
||||
# 统计拥有该角色的用户数
|
||||
count_stmt = select(func.count()).select_from(UserRole).where(UserRole.role_id == role.id)
|
||||
count_result = await db.execute(count_stmt)
|
||||
user_count = count_result.scalar() or 0
|
||||
|
||||
role_list.append(
|
||||
RoleResponse(
|
||||
id=role.id,
|
||||
name=role.name,
|
||||
display_name=role.display_name,
|
||||
description=role.description,
|
||||
permissions=role.permissions or [],
|
||||
is_default=role.is_default,
|
||||
user_count=user_count,
|
||||
created_at=role.created_at,
|
||||
updated_at=role.updated_at,
|
||||
)
|
||||
)
|
||||
|
||||
return success_response(data=[r.model_dump() for r in role_list])
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 2. 用户角色分配/撤销
|
||||
# ==========================================================================
|
||||
|
||||
# ---------- POST /api/admin/roles/assign ----------
|
||||
@router.post("/assign")
|
||||
async def assign_role(
|
||||
body: RoleAssignRequest,
|
||||
admin: UserInfo = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""手动分配角色。
|
||||
|
||||
为指定用户分配角色,记录分配者和分配原因。
|
||||
安全限制:禁止管理员给自己分配角色。
|
||||
|
||||
Args:
|
||||
body: 分配角色请求
|
||||
admin: 管理员(权限校验)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式
|
||||
"""
|
||||
# 安全限制:禁止管理员给自己分配角色
|
||||
if body.employee_id == admin.employee_id:
|
||||
raise AppException(4014, "不能给自己分配角色")
|
||||
|
||||
# 查询目标角色
|
||||
role_stmt = select(Role).where(Role.name == body.role_name)
|
||||
role_result = await db.execute(role_stmt)
|
||||
role = role_result.scalars().first()
|
||||
|
||||
if not role:
|
||||
raise AppException(4004, f"角色 {body.role_name} 不存在")
|
||||
|
||||
# 检查是否已拥有该角色
|
||||
existing_stmt = select(UserRole).where(
|
||||
UserRole.employee_id == body.employee_id,
|
||||
UserRole.role_id == role.id,
|
||||
)
|
||||
existing_result = await db.execute(existing_stmt)
|
||||
existing = existing_result.scalars().first()
|
||||
|
||||
if existing:
|
||||
raise AppException(4009, f"用户已拥有 {body.role_name} 角色")
|
||||
|
||||
# 创建用户角色关联
|
||||
user_role = UserRole(
|
||||
employee_id=body.employee_id,
|
||||
role_id=role.id,
|
||||
source="manual",
|
||||
assigned_by=admin.employee_id,
|
||||
)
|
||||
db.add(user_role)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"管理员 {_mask_sensitive_data(admin.employee_id)} 为用户 {_mask_sensitive_data(body.employee_id)} 分配角色 {body.role_name},原因:{body.reason}")
|
||||
|
||||
return success_response(message=f"角色 {body.role_name} 分配成功")
|
||||
|
||||
|
||||
# ---------- POST /api/admin/roles/revoke ----------
|
||||
@router.post("/revoke")
|
||||
async def revoke_role(
|
||||
body: RoleRevokeRequest,
|
||||
admin: UserInfo = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""撤销角色。
|
||||
|
||||
撤销指定用户的角色。
|
||||
安全限制:禁止管理员撤销自己的角色。
|
||||
|
||||
Args:
|
||||
body: 撤销角色请求
|
||||
admin: 管理员(权限校验)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式
|
||||
"""
|
||||
# 安全限制:禁止管理员撤销自己的角色
|
||||
if body.employee_id == admin.employee_id:
|
||||
raise AppException(4015, "不能撤销自己的角色")
|
||||
|
||||
# 查询目标角色
|
||||
role_stmt = select(Role).where(Role.name == body.role_name)
|
||||
role_result = await db.execute(role_stmt)
|
||||
role = role_result.scalars().first()
|
||||
|
||||
if not role:
|
||||
raise AppException(4004, f"角色 {body.role_name} 不存在")
|
||||
|
||||
# 不允许撤销默认角色
|
||||
if role.is_default:
|
||||
raise AppException(4010, "不能撤销默认角色")
|
||||
|
||||
# 查询用户角色关联
|
||||
user_role_stmt = select(UserRole).where(
|
||||
UserRole.employee_id == body.employee_id,
|
||||
UserRole.role_id == role.id,
|
||||
)
|
||||
user_role_result = await db.execute(user_role_stmt)
|
||||
user_role = user_role_result.scalars().first()
|
||||
|
||||
if not user_role:
|
||||
raise AppException(4011, f"用户没有 {body.role_name} 角色")
|
||||
|
||||
# 删除用户角色关联
|
||||
await db.delete(user_role)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"管理员 {_mask_sensitive_data(admin.employee_id)} 撤销用户 {_mask_sensitive_data(body.employee_id)} 的角色 {body.role_name},原因:{body.reason}")
|
||||
|
||||
return success_response(message=f"角色 {body.role_name} 撤销成功")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# 3. 角色映射规则管理
|
||||
# ==========================================================================
|
||||
|
||||
# ---------- GET /api/admin/roles/mapping-rules ----------
|
||||
@router.get("/mapping-rules")
|
||||
async def get_mapping_rules(
|
||||
admin: UserInfo = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取所有角色映射规则。
|
||||
|
||||
Args:
|
||||
admin: 管理员(权限校验)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含映射规则列表
|
||||
"""
|
||||
# 查询所有映射规则
|
||||
stmt = (
|
||||
select(RoleMappingRule, Role)
|
||||
.join(Role, RoleMappingRule.role_id == Role.id)
|
||||
.order_by(RoleMappingRule.priority.desc(), RoleMappingRule.source_type)
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
rules = result.all()
|
||||
|
||||
# 构建响应
|
||||
rule_list = []
|
||||
for rule, role in rules:
|
||||
rule_list.append(
|
||||
RoleMappingRuleResponse(
|
||||
id=rule.id,
|
||||
role_id=rule.role_id,
|
||||
role_name=role.name,
|
||||
source_type=rule.source_type,
|
||||
source_value=rule.source_value,
|
||||
priority=rule.priority,
|
||||
is_active=rule.is_active,
|
||||
created_at=rule.created_at,
|
||||
)
|
||||
)
|
||||
|
||||
return success_response(data=[r.model_dump() for r in rule_list])
|
||||
|
||||
|
||||
# ---------- POST /api/admin/roles/mapping-rules ----------
|
||||
@router.post("/mapping-rules")
|
||||
async def create_mapping_rule(
|
||||
body: RoleMappingRuleRequest,
|
||||
admin: UserInfo = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建角色映射规则。
|
||||
|
||||
Args:
|
||||
body: 创建映射规则请求
|
||||
admin: 管理员(权限校验)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含新创建的规则 ID
|
||||
"""
|
||||
# 查询目标角色
|
||||
role_stmt = select(Role).where(Role.name == body.role_name)
|
||||
role_result = await db.execute(role_stmt)
|
||||
role = role_result.scalars().first()
|
||||
|
||||
if not role:
|
||||
raise AppException(4004, f"角色 {body.role_name} 不存在")
|
||||
|
||||
# 检查是否已存在相同的规则
|
||||
existing_stmt = select(RoleMappingRule).where(
|
||||
RoleMappingRule.role_id == role.id,
|
||||
RoleMappingRule.source_type == body.source_type,
|
||||
RoleMappingRule.source_value == body.source_value,
|
||||
)
|
||||
existing_result = await db.execute(existing_stmt)
|
||||
existing = existing_result.scalars().first()
|
||||
|
||||
if existing:
|
||||
raise AppException(4012, "已存在相同的映射规则")
|
||||
|
||||
# 创建映射规则
|
||||
rule = RoleMappingRule(
|
||||
role_id=role.id,
|
||||
source_type=body.source_type,
|
||||
source_value=body.source_value,
|
||||
priority=body.priority,
|
||||
is_active=body.is_active,
|
||||
)
|
||||
db.add(rule)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"管理员 {_mask_sensitive_data(admin.employee_id)} 创建映射规则:{body.source_type}={body.source_value} → {body.role_name}")
|
||||
|
||||
return success_response(
|
||||
message="映射规则创建成功",
|
||||
data={"id": rule.id},
|
||||
)
|
||||
|
||||
|
||||
# ---------- DELETE /api/admin/roles/mapping-rules/{rule_id} ----------
|
||||
@router.delete("/mapping-rules/{rule_id}")
|
||||
async def delete_mapping_rule(
|
||||
rule_id: str,
|
||||
admin: UserInfo = Depends(require_admin),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除角色映射规则。
|
||||
|
||||
Args:
|
||||
rule_id: 规则 ID
|
||||
admin: 管理员(权限校验)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式
|
||||
"""
|
||||
# 查询规则
|
||||
rule_stmt = select(RoleMappingRule).where(RoleMappingRule.id == rule_id)
|
||||
rule_result = await db.execute(rule_stmt)
|
||||
rule = rule_result.scalars().first()
|
||||
|
||||
if not rule:
|
||||
raise AppException(4013, "映射规则不存在")
|
||||
|
||||
# 删除规则
|
||||
await db.delete(rule)
|
||||
await db.commit()
|
||||
|
||||
logger.info(f"管理员 {_mask_sensitive_data(admin.employee_id)} 删除映射规则 {rule_id}")
|
||||
|
||||
return success_response(message="映射规则删除成功")
|
||||
Reference in New Issue
Block a user