优化权限控制

This commit is contained in:
yml2213
2026-06-22 17:07:29 +08:00
parent 480dfa6085
commit 8771d91a30
18 changed files with 222 additions and 16 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+7
View File
@@ -44,6 +44,13 @@ def _migrate():
conn.execute(text("ALTER TABLE accounts ADD COLUMN tag VARCHAR(64) DEFAULT ''"))
conn.commit()
# 检查 users.custom_permissions 列是否存在
result = conn.execute(text("PRAGMA table_info(users)"))
columns = [row[1] for row in result]
if 'custom_permissions' not in columns:
conn.execute(text("ALTER TABLE users ADD COLUMN custom_permissions JSON DEFAULT NULL"))
conn.commit()
def _seed():
"""写入默认超管账号和角色。"""
+2 -2
View File
@@ -8,7 +8,7 @@ from jose import JWTError
from .database import get_db
from .security import decode_access_token
from .models import User
from .permissions import get_role_permissions
from .permissions import get_user_permissions
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login")
@@ -43,7 +43,7 @@ def require_permission(permission: str):
def checker(current_user: User = Depends(get_current_user)) -> User:
if not current_user.is_active:
raise HTTPException(status_code=403, detail="账号已禁用")
perms = get_role_permissions(current_user.role)
perms = get_user_permissions(current_user)
if permission not in perms:
raise HTTPException(status_code=403, detail=f"无权限: {permission}")
return current_user
+1
View File
@@ -18,6 +18,7 @@ class User(Base):
role = Column(String(32), nullable=False, default="support") # super_admin / operation / support
is_active = Column(Boolean, default=True)
remark = Column(String(256), default="")
custom_permissions = Column(JSON, nullable=True, comment="自定义权限列表,null表示使用角色默认权限")
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
+10 -1
View File
@@ -48,7 +48,6 @@ ROLE_PERMISSIONS = {
],
"support": [
"account:view_assigned",
"login:view_assigned",
],
}
@@ -65,6 +64,16 @@ def get_role_permissions(role: str) -> list[str]:
return ROLE_PERMISSIONS.get(role, [])
def get_user_permissions(user) -> list[str]:
"""获取用户的实际权限列表。
优先使用用户自定义权限(custom_permissions),
若为 None 则回退到角色默认权限。
"""
if user.custom_permissions is not None:
return user.custom_permissions
return get_role_permissions(user.role)
def has_permission(role: str, permission: str) -> bool:
"""检查角色是否拥有某权限。"""
return permission in get_role_permissions(role)
Binary file not shown.
+3 -3
View File
@@ -7,7 +7,7 @@ from sqlalchemy.orm import Session
from ..database import get_db
from ..models import User, AuditLog
from ..security import verify_password, create_access_token
from ..permissions import get_role_permissions, ROLE_LABELS
from ..permissions import get_user_permissions, ROLE_LABELS
from ..schemas import LoginRequest, TokenResponse
from ..deps import get_current_user
@@ -23,7 +23,7 @@ def login(req: LoginRequest, db: Session = Depends(get_db)):
raise HTTPException(status_code=403, detail="账号已禁用,请联系管理员")
token = create_access_token({"sub": str(user.id), "role": user.role})
perms = get_role_permissions(user.role)
perms = get_user_permissions(user)
# 审计
db.add(AuditLog(user_id=user.id, username=user.username, action="login", target="auth"))
@@ -46,7 +46,7 @@ def me(current_user: User = Depends(get_current_user)):
"role_label": ROLE_LABELS.get(current_user.role, current_user.role),
"is_active": current_user.is_active,
"remark": current_user.remark,
"permissions": get_role_permissions(current_user.role),
"permissions": get_user_permissions(current_user),
}
+19 -4
View File
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Session
from ..database import get_db
from ..models import User, AuditLog
from ..security import hash_password
from ..permissions import ROLE_LABELS, get_role_permissions
from ..permissions import ROLE_LABELS, PERMISSIONS, get_user_permissions
from ..schemas import UserCreate, UserUpdate, UserInfo
from ..deps import require_permission, get_current_user
@@ -28,7 +28,8 @@ def list_users(
is_active=u.is_active,
remark=u.remark or "",
created_at=u.created_at,
permissions=get_role_permissions(u.role),
permissions=get_user_permissions(u),
custom_permissions=u.custom_permissions,
))
return result
@@ -60,7 +61,8 @@ def create_user(
return UserInfo(
id=user.id, username=user.username, role=user.role,
is_active=user.is_active, remark=user.remark or "",
permissions=get_role_permissions(user.role),
permissions=get_user_permissions(user),
custom_permissions=user.custom_permissions,
)
@@ -83,6 +85,8 @@ def update_user(
user.is_active = req.is_active
if req.remark is not None:
user.remark = req.remark
if req.custom_permissions is not None:
user.custom_permissions = req.custom_permissions
db.commit()
db.refresh(user)
@@ -94,7 +98,8 @@ def update_user(
return UserInfo(
id=user.id, username=user.username, role=user.role,
is_active=user.is_active, remark=user.remark or "",
permissions=get_role_permissions(user.role),
permissions=get_user_permissions(user),
custom_permissions=user.custom_permissions,
)
@@ -115,3 +120,13 @@ def delete_user(
db.delete(user)
db.commit()
return {"message": "已删除", "success": True}
@router.get("/permissions/list")
def list_permissions(_: User = Depends(require_permission("user:assign_permissions"))):
"""返回所有可用权限点及角色默认权限映射。"""
from ..permissions import ROLE_PERMISSIONS
return {
"permissions": PERMISSIONS,
"role_permissions": ROLE_PERMISSIONS,
}
+2
View File
@@ -27,6 +27,7 @@ class UserInfo(BaseModel):
remark: str = ""
created_at: Optional[datetime] = None
permissions: list[str] = []
custom_permissions: Optional[list[str]] = None
class Config:
from_attributes = True
@@ -45,6 +46,7 @@ class UserUpdate(BaseModel):
role: Optional[str] = None
is_active: Optional[bool] = None
remark: Optional[str] = None
custom_permissions: Optional[list[str]] = None
# ---- 账号 ----
+3 -1
View File
@@ -15,6 +15,7 @@ export interface UserInfo {
is_active: boolean;
remark: string;
permissions: string[];
custom_permissions?: string[] | null;
}
export const authApi = {
@@ -30,9 +31,10 @@ export const userApi = {
list: () => api.get<any, UserInfo[]>('/users'),
create: (data: { username: string; password: string; role: string; remark?: string }) =>
api.post<any, UserInfo>('/users', data),
update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string }) =>
update: (id: number, data: { password?: string; role?: string; is_active?: boolean; remark?: string; custom_permissions?: string[] | null }) =>
api.put<any, UserInfo>(`/users/${id}`, data),
delete: (id: number) => api.delete<any, any>(`/users/${id}`),
listPermissions: () => api.get<any, any>('/users/permissions/list'),
};
export const accountApi = {
+1 -1
View File
@@ -46,7 +46,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
}
// 登录任务
if (hasPerm(user, 'login:batch') || hasPerm(user, 'login:view_assigned')) {
if (hasPerm(user, 'login:batch') || hasPerm(user, 'login:view_all')) {
menuItems.push({ key: '/login-tasks', label: '登录任务', icon: <ApiOutlined /> });
}
+173 -4
View File
@@ -1,9 +1,11 @@
import { useEffect, useState } from 'react';
import {
Table, Button, Modal, Form, Input, Select, Tag, Popconfirm, message, Space,
Checkbox, Divider, Alert, Tooltip,
} from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { PlusOutlined, EditOutlined, DeleteOutlined, SafetyOutlined } from '@ant-design/icons';
import { userApi, type UserInfo } from '../api/modules';
import { getUser, hasPerm } from '../store/auth';
const ROLE_OPTIONS = [
{ value: 'super_admin', label: '超级管理员' },
@@ -17,6 +19,21 @@ const ROLE_COLORS: Record<string, string> = {
support: 'green',
};
// 权限分组定义
const PERMISSION_GROUPS = [
{ label: '用户管理', prefix: 'user:' },
{ label: '账号管理', prefix: 'account:' },
{ label: '登录任务', prefix: 'login:' },
{ label: 'Cookie', prefix: 'cookie:' },
{ label: '代理 & 白名单', prefix: ['proxy:', 'whitelist:'] },
{ label: '系统', prefix: ['system:', 'audit:'] },
];
function isPrefixMatch(key: string, prefix: string | string[]): boolean {
if (Array.isArray(prefix)) return prefix.some(p => key.startsWith(p));
return key.startsWith(prefix);
}
export default function UsersPage() {
const [users, setUsers] = useState<UserInfo[]>([]);
const [loading, setLoading] = useState(false);
@@ -24,6 +41,17 @@ export default function UsersPage() {
const [editing, setEditing] = useState<UserInfo | null>(null);
const [form] = Form.useForm();
// 权限管理
const [permModalOpen, setPermModalOpen] = useState(false);
const [permUser, setPermUser] = useState<UserInfo | null>(null);
const [permList, setPermList] = useState<Record<string, string>>({});
const [rolePermMap, setRolePermMap] = useState<Record<string, string[]>>({});
const [selectedPerms, setSelectedPerms] = useState<string[]>([]);
const [useCustom, setUseCustom] = useState(false);
const [permLoading, setPermLoading] = useState(false);
const canAssignPerm = hasPerm(getUser(), 'user:assign_permissions');
const loadUsers = async () => {
setLoading(true);
try {
@@ -86,6 +114,52 @@ export default function UsersPage() {
}
};
// ---- 权限管理 ----
const handlePermClick = async (user: UserInfo) => {
setPermUser(user);
setPermModalOpen(true);
setPermLoading(true);
try {
const data = await userApi.listPermissions();
setPermList(data.permissions || {});
setRolePermMap(data.role_permissions || {});
// 初始化选中权限:如果用户有 custom_permissions 则使用,否则使用角色默认
if (user.custom_permissions !== null && user.custom_permissions !== undefined) {
setUseCustom(true);
setSelectedPerms(user.custom_permissions);
} else {
setUseCustom(false);
setSelectedPerms(data.role_permissions?.[user.role] || []);
}
} catch (e: any) {
message.error(e.message);
} finally {
setPermLoading(false);
}
};
const handlePermSave = async () => {
if (!permUser) return;
setPermLoading(true);
try {
await userApi.update(permUser.id, {
custom_permissions: useCustom ? selectedPerms : null,
});
message.success('权限已更新');
setPermModalOpen(false);
loadUsers();
} catch (e: any) {
message.error(e.message);
} finally {
setPermLoading(false);
}
};
const handleRoleChangeForPerm = (role: string) => {
const rolePerms = rolePermMap[role] || [];
setSelectedPerms(rolePerms);
};
const columns = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '用户名', dataIndex: 'username' },
@@ -97,18 +171,33 @@ export default function UsersPage() {
return <Tag color={ROLE_COLORS[role] || 'default'}>{label}</Tag>;
},
},
{
title: '权限模式',
dataIndex: 'custom_permissions',
width: 100,
render: (_: any, record: UserInfo) =>
record.custom_permissions !== null && record.custom_permissions !== undefined
? <Tag color="orange"></Tag>
: <Tag></Tag>,
},
{
title: '状态',
dataIndex: 'is_active',
width: 80,
render: (active: boolean) => active ? <Tag color="green"></Tag> : <Tag></Tag>,
},
{ title: '备注', dataIndex: 'remark' },
{ title: '备注', dataIndex: 'remark', ellipsis: true },
{
title: '操作',
width: 160,
width: 200,
render: (_: any, record: UserInfo) => (
<Space>
<Button size="small" icon={<EditOutlined />} onClick={() => handleEdit(record)}></Button>
{canAssignPerm && (
<Tooltip title="权限管理">
<Button size="small" icon={<SafetyOutlined />} onClick={() => handlePermClick(record)}></Button>
</Tooltip>
)}
{record.role !== 'super_admin' && (
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Button danger size="small" icon={<DeleteOutlined />}></Button>
@@ -133,6 +222,8 @@ export default function UsersPage() {
size="small"
pagination={{ pageSize: 20 }}
/>
{/* 编辑/新建用户 Modal */}
<Modal
title={editing ? '编辑用户' : '新建用户'}
open={modalOpen}
@@ -159,6 +250,84 @@ export default function UsersPage() {
</Form.Item>
</Form>
</Modal>
{/* 权限管理 Modal */}
<Modal
title={permUser ? `权限管理 - ${permUser.username}` : '权限管理'}
open={permModalOpen}
onCancel={() => setPermModalOpen(false)}
onOk={handlePermSave}
okText="保存"
width={640}
confirmLoading={permLoading}
>
{permUser && (
<>
<Alert
message={
useCustom
? '当前使用自定义权限,修改角色不会自动更新权限'
: '当前使用角色默认权限,修改角色后权限会自动跟随'
}
type={useCustom ? 'warning' : 'info'}
showIcon
style={{ marginBottom: 16 }}
/>
<div style={{ marginBottom: 16 }}>
<span></span>
<Select
value={useCustom ? 'custom' : 'role'}
style={{ width: 140 }}
onChange={(v) => {
if (v === 'role') {
setUseCustom(false);
setSelectedPerms(rolePermMap[permUser.role] || []);
} else {
setUseCustom(true);
}
}}
options={[
{ value: 'role', label: '角色默认' },
{ value: 'custom', label: '自定义' },
]}
/>
{useCustom && (
<Button
type="link"
size="small"
onClick={() => handleRoleChangeForPerm(permUser.role)}
>
</Button>
)}
</div>
<Checkbox.Group
value={selectedPerms}
onChange={(vals) => setSelectedPerms(vals as string[])}
disabled={!useCustom}
>
{PERMISSION_GROUPS.map((group) => {
const groupKeys = Object.keys(permList).filter(k => isPrefixMatch(k, group.prefix));
if (groupKeys.length === 0) return null;
return (
<div key={group.label} style={{ marginBottom: 12 }}>
<Divider orientation="left" style={{ margin: '8px 0 12px' }}>
{group.label}
</Divider>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px 24px' }}>
{groupKeys.map(key => (
<Checkbox key={key} value={key}>
{permList[key]}
</Checkbox>
))}
</div>
</div>
);
})}
</Checkbox.Group>
</>
)}
</Modal>
</div>
);
}
}