feat: implement RBAC and field masking for system_user module

Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
This commit is contained in:
dxc
2026-02-27 14:28:48 +08:00
parent 3f83e8742b
commit 6fa5233ea6
2 changed files with 103 additions and 15 deletions

View File

@ -2,10 +2,56 @@
from flask import Blueprint, request, jsonify, current_app from flask import Blueprint, request, jsonify, current_app
from flask_jwt_extended import jwt_required, get_jwt from flask_jwt_extended import jwt_required, get_jwt
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
from app.utils.decorators import permission_required
auth_bp = Blueprint('auth', __name__) auth_bp = Blueprint('auth', __name__)
# ==============================================================================
# 辅助函数:获取当前用户的完整权限列表(基于角色查询)
# ==============================================================================
def get_current_user_permissions():
"""
返回当前用户拥有的所有权限码列表(包括菜单和元素)
此函数根据角色查询数据库得到权限。
"""
claims = get_jwt()
user_role = claims.get('role')
if not user_role:
return []
# 超级管理员返回所有字段权限
if user_role == 'super_admin':
return ['system_user:*']
perm_dict = AuthService.get_user_permissions(user_role)
# 合并菜单和元素权限
perms = perm_dict.get('menus', []) + perm_dict.get('elements', [])
return perms
def filter_item_by_permissions(item_dict, user_permissions):
"""
根据用户权限过滤 item 字典,无权限的字段值置为 None
"""
# 字段名到权限码的映射(与前端 permissionMap 保持一致)
field_to_perm = {
'id': 'system_user:id',
'username': 'system_user:username',
'account_id': 'system_user:account_id',
'email': 'system_user:email',
'department': 'system_user:department',
'role': 'system_user:role',
'status': 'system_user:status',
'created_at': 'system_user:created_at',
}
# 如果用户是超级管理员且有 'system_user:*',则不过滤
if 'system_user:*' in user_permissions:
return item_dict
for field, perm_code in field_to_perm.items():
if field in item_dict and perm_code not in user_permissions:
item_dict[field] = None
return item_dict
@auth_bp.route('/login', methods=['POST']) @auth_bp.route('/login', methods=['POST'])
def login(): def login():
try: try:
@ -34,6 +80,7 @@ def login():
@auth_bp.route('/user/create', methods=['POST']) @auth_bp.route('/user/create', methods=['POST'])
@jwt_required() @jwt_required()
@permission_required('system_user:operation')
def create_user(): def create_user():
try: try:
data = request.get_json() data = request.get_json()
@ -51,6 +98,7 @@ def create_user():
# [新增] 更新用户 # [新增] 更新用户
@auth_bp.route('/user/<int:user_id>', methods=['PUT']) @auth_bp.route('/user/<int:user_id>', methods=['PUT'])
@jwt_required() @jwt_required()
@permission_required('system_user:operation')
def update_user(user_id): def update_user(user_id):
try: try:
data = request.get_json() data = request.get_json()
@ -67,10 +115,14 @@ def update_user(user_id):
@auth_bp.route('/users', methods=['GET']) @auth_bp.route('/users', methods=['GET'])
@jwt_required() @jwt_required()
@permission_required('system_user')
def get_users(): def get_users():
try: try:
users = AuthService.get_all_users() users = AuthService.get_all_users()
return jsonify({'msg': '获取成功', 'data': users}), 200 # 字段级脱敏
user_permissions = get_current_user_permissions()
filtered_users = [filter_item_by_permissions(user, user_permissions) for user in users]
return jsonify({'msg': '获取成功', 'data': filtered_users}), 200
except Exception as e: except Exception as e:
current_app.logger.error(f"Get Users Failed: {str(e)}") current_app.logger.error(f"Get Users Failed: {str(e)}")
return jsonify({'msg': '获取用户列表失败'}), 500 return jsonify({'msg': '获取用户列表失败'}), 500
@ -78,8 +130,9 @@ def get_users():
@auth_bp.route('/user/<int:user_id>', methods=['DELETE']) @auth_bp.route('/user/<int:user_id>', methods=['DELETE'])
@jwt_required() @jwt_required()
@permission_required('system_user:operation')
def delete_user(user_id): def delete_user(user_id):
try: try {
claims = get_jwt() claims = get_jwt()
operator_role = claims.get('role') operator_role = claims.get('role')

View File

@ -16,23 +16,33 @@
border border
style="width: 100%" style="width: 100%"
> >
<el-table-column prop="username" label="用户标识" min-width="180" /> <el-table-column v-if="hasColumnPermission('username')" prop="username" label="用户标识" min-width="180" />
<el-table-column prop="department" label="所属部门" width="150"> <el-table-column v-if="hasColumnPermission('department')" prop="department" label="所属部门" width="150">
<template #default="scope"> <template #default="scope">
<el-tag>{{ scope.row.department }}</el-tag> <el-tag>{{ scope.row.department }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="role" label="系统角色" width="180"> <el-table-column v-if="hasColumnPermission('role')" prop="role" label="系统角色" width="180">
<template #default="scope"> <template #default="scope">
{{ formatRole(scope.row.role) }} {{ formatRole(scope.row.role) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="email" label="邮箱" min-width="200" /> <el-table-column v-if="hasColumnPermission('email')" prop="email" label="邮箱" min-width="200" />
<el-table-column label="操作" width="180" fixed="right"> <el-table-column v-if="hasColumnPermission('status')" prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 'active' ? 'success' : 'danger'">
{{ scope.row.status === 'active' ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="hasColumnPermission('created_at')" prop="created_at" label="创建时间" width="160" />
<el-table-column v-if="userStore.hasPermission('system_user:operation')" label="操作" width="180" fixed="right">
<template #default="scope"> <template #default="scope">
<el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button> <el-button link type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-popconfirm title="确定要删除该用户吗?" @confirm="handleDelete(scope.row)"> <el-popconfirm title="确定要删除该用户吗?" @confirm="handleDelete(scope.row)">
@ -61,7 +71,7 @@
<el-input <el-input
v-model="form.cn_name" v-model="form.cn_name"
placeholder="请输入中文姓名 (如: 张三)" placeholder="请输入中文姓名 (如: 张三)"
:disabled="isEdit" :disabled="isEdit || !userStore.hasPermission('system_user:operation')"
@input="handleNameInput" @input="handleNameInput"
/> />
</el-form-item> </el-form-item>
@ -70,7 +80,7 @@
<el-input <el-input
v-model="form.username" v-model="form.username"
placeholder="自动生成,可修改 (如: zhangsan)" placeholder="自动生成,可修改 (如: zhangsan)"
:disabled="isEdit" :disabled="isEdit || !userStore.hasPermission('system_user:operation')"
> >
<template #append> <template #append>
<span v-if="!isEdit" style="font-size: 12px; color: #999;">重复自动+1</span> <span v-if="!isEdit" style="font-size: 12px; color: #999;">重复自动+1</span>
@ -84,6 +94,7 @@
type="password" type="password"
show-password show-password
:placeholder="isEdit ? '不修改请留空' : '设置初始密码'" :placeholder="isEdit ? '不修改请留空' : '设置初始密码'"
:disabled="!userStore.hasPermission('system_user:operation')"
/> />
</el-form-item> </el-form-item>
@ -95,13 +106,14 @@
filterable filterable
allow-create allow-create
default-first-option default-first-option
:disabled="!userStore.hasPermission('system_user:operation')"
> >
<el-option v-for="item in departmentOptions" :key="item" :label="item" :value="item" /> <el-option v-for="item in departmentOptions" :key="item" :label="item" :value="item" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="系统角色" prop="role"> <el-form-item label="系统角色" prop="role">
<el-select v-model="form.role" placeholder="授予权限" style="width: 100%"> <el-select v-model="form.role" placeholder="授予权限" style="width: 100%" :disabled="!userStore.hasPermission('system_user:operation')">
<el-option <el-option
v-for="option in roleOptions" v-for="option in roleOptions"
:key="option.value" :key="option.value"
@ -112,14 +124,14 @@
</el-form-item> </el-form-item>
<el-form-item label="邮箱" prop="email"> <el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" /> <el-input v-model="form.email" placeholder="请输入邮箱" :disabled="!userStore.hasPermission('system_user:operation')" />
</el-form-item> </el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :loading="submitLoading"> <el-button v-if="userStore.hasPermission('system_user:operation')" type="primary" @click="onSubmit" :loading="submitLoading">
{{ isEdit ? '确认修改' : '确认创建' }} {{ isEdit ? '确认修改' : '确认创建' }}
</el-button> </el-button>
</div> </div>
@ -135,6 +147,27 @@ import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { pinyin } from 'pinyin-pro' // ★ 务必安装: npm install pinyin-pro import { pinyin } from 'pinyin-pro' // ★ 务必安装: npm install pinyin-pro
const userStore = useUserStore()
// 列与权限Code的映射关系数据库中的code
const permissionMap: Record<string, string> = {
username: 'system_user:username',
department: 'system_user:department',
role: 'system_user:role',
email: 'system_user:email',
status: 'system_user:status',
created_at: 'system_user:created_at',
}
// 检查列权限
const hasColumnPermission = (prop: string) => {
if (userStore.role === 'SUPER_ADMIN' || userStore.username === 'IRIS') {
return true
}
const code = permissionMap[prop]
return code ? userStore.hasPermission(code) : false
}
const userStore = useUserStore() const userStore = useUserStore()
const tableLoading = ref(false) const tableLoading = ref(false)
const submitLoading = ref(false) const submitLoading = ref(false)
@ -241,6 +274,7 @@ const getList = async () => {
tableData.value = res.data || [] tableData.value = res.data || []
extractDepartments(tableData.value) extractDepartments(tableData.value)
} catch (error) { } catch (error) {
// 错误已由全局拦截器统一处理
console.error('Fetch users failed:', error) console.error('Fetch users failed:', error)
} finally { } finally {
tableLoading.value = false tableLoading.value = false
@ -317,7 +351,7 @@ const onSubmit = async () => {
dialogVisible.value = false dialogVisible.value = false
getList() getList()
} catch (error) { } catch (error) {
// request 拦截器会处理错误 // 错误已由全局拦截器统一处理
} finally { } finally {
submitLoading.value = false submitLoading.value = false
} }
@ -342,6 +376,7 @@ const handleDelete = async (row: any) => {
ElMessage.success('删除成功') ElMessage.success('删除成功')
getList() getList()
} catch (error) { } catch (error) {
// 错误已由全局拦截器统一处理
} }
} }