1e1e5957e8
- Ips.vue: el-select 添加 width: 280px,修复网段下拉框过窄 - Ips.vue: query 参数 networkId → network_id(与 FastAPI snake_case 一致) - Scan.vue: el-select 用 network.id 代替整个 network 对象作为 value,修复切换网段不生效 - enhanced_scan_service.py: 替换失效的 scapy ARP 扫描为 arp-scan 命令;扫描结果自动创建缺失 IP 记录
421 lines
14 KiB
Python
421 lines
14 KiB
Python
from typing import Optional, List
|
|
from sqlalchemy.orm import Session
|
|
from datetime import datetime, timedelta
|
|
from fastapi import HTTPException, status
|
|
|
|
from app.models.auth import User, UserRole, UserStatus, Permission, RolePermission, RefreshToken
|
|
from app.core.security import SecurityUtils
|
|
|
|
|
|
# 默认权限项 + 角色权限映射
|
|
DEFAULT_PERMISSIONS = [
|
|
# 网段
|
|
("network:view", "查看网段", "network"),
|
|
("network:create", "创建网段", "network"),
|
|
("network:edit", "编辑网段", "network"),
|
|
("network:delete", "删除网段", "network"),
|
|
# IP
|
|
("ip:view", "查看 IP", "ip"),
|
|
("ip:edit", "编辑 IP", "ip"),
|
|
# 扫描
|
|
("scan:run", "执行扫描", "scan"),
|
|
("scan:view", "查看扫描结果", "scan"),
|
|
# SNMP
|
|
("snmp:view", "查看 SNMP", "snmp"),
|
|
("snmp:edit", "配置 SNMP", "snmp"),
|
|
# 告警
|
|
("alert:view", "查看告警", "alert"),
|
|
("alert:ack", "确认告警", "alert"),
|
|
("alert:resolve", "解决告警", "alert"),
|
|
# 报告
|
|
("report:export", "导出报告", "report"),
|
|
# 用户管理
|
|
("user:view", "查看用户", "system"),
|
|
("user:create", "创建用户", "system"),
|
|
("user:edit", "编辑用户", "system"),
|
|
("user:delete", "删除用户", "system"),
|
|
# 审计
|
|
("audit:view", "查看审计日志", "system"),
|
|
]
|
|
|
|
|
|
# 角色 -> 权限代码集合
|
|
DEFAULT_ROLE_PERMISSIONS = {
|
|
UserRole.SUPER_ADMIN: {p[0] for p in DEFAULT_PERMISSIONS}, # 全部
|
|
UserRole.ADMIN: {
|
|
"network:view", "network:create", "network:edit", "network:delete",
|
|
"ip:view", "ip:edit",
|
|
"scan:run", "scan:view",
|
|
"snmp:view", "snmp:edit",
|
|
"alert:view", "alert:ack", "alert:resolve",
|
|
"report:export",
|
|
"user:view", "audit:view",
|
|
},
|
|
UserRole.OPERATOR: {
|
|
"network:view",
|
|
"ip:view", "ip:edit",
|
|
"scan:run", "scan:view",
|
|
"snmp:view",
|
|
"alert:view", "alert:ack",
|
|
"report:export",
|
|
},
|
|
UserRole.VIEWER: {
|
|
"network:view",
|
|
"ip:view",
|
|
"scan:view",
|
|
"snmp:view",
|
|
"alert:view",
|
|
},
|
|
}
|
|
|
|
|
|
class UserService:
|
|
"""用户服务"""
|
|
|
|
@staticmethod
|
|
def init_default_permissions(db: Session) -> int:
|
|
"""初始化默认权限项和角色权限映射(已存在则跳过)"""
|
|
created = 0
|
|
perm_map = {}
|
|
for code, name, category in DEFAULT_PERMISSIONS:
|
|
existing = db.query(Permission).filter(Permission.code == code).first()
|
|
if existing:
|
|
perm_map[code] = existing
|
|
continue
|
|
p = Permission(code=code, name=name, category=category)
|
|
db.add(p)
|
|
db.flush()
|
|
perm_map[code] = p
|
|
created += 1
|
|
|
|
# 角色 -> 权限关联
|
|
for role, codes in DEFAULT_ROLE_PERMISSIONS.items():
|
|
for code in codes:
|
|
if code not in perm_map:
|
|
continue
|
|
pid = perm_map[code].id
|
|
link_existing = db.query(RolePermission).filter(
|
|
RolePermission.role == role,
|
|
RolePermission.permission_id == pid
|
|
).first()
|
|
if link_existing:
|
|
continue
|
|
db.add(RolePermission(role=role, permission_id=pid))
|
|
|
|
db.commit()
|
|
return created
|
|
|
|
@staticmethod
|
|
def get_user_permissions(db: Session, user: User) -> set:
|
|
"""获取用户拥有的全部权限代码集合"""
|
|
if user.role == UserRole.SUPER_ADMIN:
|
|
return {p.code for p in db.query(Permission).all()}
|
|
# 其他角色查 role_permissions 关联表
|
|
rows = db.query(Permission.code).join(
|
|
RolePermission, RolePermission.permission_id == Permission.id
|
|
).filter(RolePermission.role == user.role).all()
|
|
return {r[0] for r in rows}
|
|
|
|
@staticmethod
|
|
def init_default_admin(db: Session):
|
|
"""初始化默认管理员账户"""
|
|
admin_exists = db.query(User).filter(User.username == "admin").first()
|
|
if not admin_exists:
|
|
admin = User(
|
|
username="admin",
|
|
email="admin@ipam.local",
|
|
real_name="系统管理员",
|
|
password_hash=SecurityUtils.hash_password("admin123"),
|
|
role=UserRole.SUPER_ADMIN,
|
|
status=UserStatus.ACTIVE
|
|
)
|
|
db.add(admin)
|
|
db.commit()
|
|
db.refresh(admin)
|
|
return admin
|
|
return None
|
|
|
|
@staticmethod
|
|
def authenticate(db: Session, username: str, password: str) -> Optional[User]:
|
|
"""用户认证"""
|
|
user = db.query(User).filter(User.username == username).first()
|
|
|
|
if not user:
|
|
return None
|
|
|
|
if not SecurityUtils.verify_password(password, user.password_hash):
|
|
# 记录登录失败
|
|
user.login_failed_count += 1
|
|
# 登录失败5次锁定账号
|
|
if user.login_failed_count >= 5:
|
|
user.status = UserStatus.LOCKED
|
|
db.commit()
|
|
return None
|
|
|
|
# 登录成功,重置失败计数
|
|
if user.login_failed_count > 0:
|
|
user.login_failed_count = 0
|
|
db.commit()
|
|
|
|
# 检查用户状态
|
|
if user.status != UserStatus.ACTIVE:
|
|
return None
|
|
|
|
return user
|
|
|
|
@staticmethod
|
|
def create_user(db: Session,
|
|
username: str,
|
|
password: str,
|
|
email: Optional[str] = None,
|
|
phone: Optional[str] = None,
|
|
real_name: Optional[str] = None,
|
|
role: UserRole = UserRole.VIEWER,
|
|
created_by: Optional[int] = None) -> User:
|
|
"""创建用户"""
|
|
# 检查用户名是否存在
|
|
existing = db.query(User).filter(User.username == username).first()
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="用户名已存在"
|
|
)
|
|
|
|
# 检查邮箱是否存在
|
|
if email:
|
|
existing_email = db.query(User).filter(User.email == email).first()
|
|
if existing_email:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="邮箱已被使用"
|
|
)
|
|
|
|
user = User(
|
|
username=username,
|
|
email=email,
|
|
phone=phone,
|
|
real_name=real_name,
|
|
password_hash=SecurityUtils.hash_password(password),
|
|
role=role,
|
|
status=UserStatus.ACTIVE,
|
|
created_by=created_by
|
|
)
|
|
|
|
db.add(user)
|
|
db.commit()
|
|
db.refresh(user)
|
|
return user
|
|
|
|
@staticmethod
|
|
def update_login_info(db: Session, user_id: int, ip_address: str = None):
|
|
"""更新登录信息"""
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if user:
|
|
user.last_login_at = datetime.utcnow()
|
|
user.last_login_ip = ip_address
|
|
db.commit()
|
|
|
|
@staticmethod
|
|
def get_user_by_id(db: Session, user_id: int) -> Optional[User]:
|
|
"""根据ID获取用户"""
|
|
return db.query(User).filter(User.id == user_id).first()
|
|
|
|
@staticmethod
|
|
def get_user_by_username(db: Session, username: str) -> Optional[User]:
|
|
"""根据用户名获取用户"""
|
|
return db.query(User).filter(User.username == username).first()
|
|
|
|
@staticmethod
|
|
def get_users(db: Session, skip: int = 0, limit: int = 50, status: UserStatus = None, role: UserRole = None) -> tuple[int, List[User]]:
|
|
"""获取用户列表"""
|
|
query = db.query(User)
|
|
|
|
if status:
|
|
query = query.filter(User.status == status)
|
|
if role:
|
|
query = query.filter(User.role == role)
|
|
|
|
total = query.count()
|
|
items = query.order_by(User.id.desc()).offset(skip).limit(limit).all()
|
|
|
|
return total, items
|
|
|
|
@staticmethod
|
|
def change_password(db: Session, user_id: int, old_password: str, new_password: str) -> bool:
|
|
"""修改密码"""
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if not user:
|
|
return False
|
|
|
|
if not SecurityUtils.verify_password(old_password, user.password_hash):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="原密码不正确"
|
|
)
|
|
|
|
user.password_hash = SecurityUtils.hash_password(new_password)
|
|
db.commit()
|
|
return True
|
|
|
|
@staticmethod
|
|
def reset_password(db: Session, user_id: int, new_password: str) -> bool:
|
|
"""管理员重置密码"""
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if not user:
|
|
return False
|
|
|
|
user.password_hash = SecurityUtils.hash_password(new_password)
|
|
db.commit()
|
|
return True
|
|
|
|
@staticmethod
|
|
def update_user_status(db: Session, user_id: int, status: UserStatus) -> Optional[User]:
|
|
"""更新用户状态"""
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if not user:
|
|
return None
|
|
|
|
user.status = status
|
|
db.commit()
|
|
db.refresh(user)
|
|
return user
|
|
|
|
@staticmethod
|
|
def delete_user(db: Session, user_id: int) -> bool:
|
|
"""删除用户(软删除,实际标记为禁用)"""
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if not user:
|
|
return False
|
|
|
|
user.status = UserStatus.INACTIVE
|
|
db.commit()
|
|
return True
|
|
|
|
@staticmethod
|
|
def unlock_user(db: Session, user_id: int) -> Optional[User]:
|
|
"""解锁被锁定的用户"""
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if not user:
|
|
return None
|
|
|
|
if user.status == UserStatus.LOCKED:
|
|
user.status = UserStatus.ACTIVE
|
|
user.login_failed_count = 0
|
|
db.commit()
|
|
db.refresh(user)
|
|
|
|
return user
|
|
|
|
|
|
class TokenService:
|
|
"""令牌服务"""
|
|
|
|
@staticmethod
|
|
def create_tokens(db: Session, user: User, ip_address: str = None, user_agent: str = None) -> dict:
|
|
"""创建访问令牌和刷新令牌"""
|
|
access_token_expires = timedelta(minutes=120) # 访问令牌2小时
|
|
access_token = SecurityUtils.create_access_token(
|
|
data={
|
|
"sub": user.username,
|
|
"user_id": user.id,
|
|
"role": user.role.value
|
|
},
|
|
expires_delta=access_token_expires
|
|
)
|
|
|
|
refresh_token_expires = timedelta(days=7) # 刷新令牌7天
|
|
refresh_token_value = SecurityUtils.create_refresh_token(
|
|
data={
|
|
"sub": user.username,
|
|
"user_id": user.id
|
|
},
|
|
expires_delta=refresh_token_expires
|
|
)
|
|
|
|
# 保存刷新令牌
|
|
refresh_token = RefreshToken(
|
|
user_id=user.id,
|
|
token=refresh_token_value,
|
|
expires_at=datetime.utcnow() + refresh_token_expires,
|
|
ip_address=ip_address,
|
|
user_agent=user_agent
|
|
)
|
|
db.add(refresh_token)
|
|
db.commit()
|
|
|
|
return {
|
|
"access_token": access_token,
|
|
"refresh_token": refresh_token_value,
|
|
"token_type": "bearer",
|
|
"expires_in": int(access_token_expires.total_seconds())
|
|
}
|
|
|
|
@staticmethod
|
|
def refresh_access_token(db: Session, refresh_token: str) -> Optional[dict]:
|
|
"""使用刷新令牌获取新的访问令牌"""
|
|
try:
|
|
payload = SecurityUtils.verify_token(refresh_token)
|
|
username = payload.get("sub")
|
|
user_id = payload.get("user_id")
|
|
|
|
# 检查刷新令牌是否有效且未被撤销
|
|
token_record = db.query(RefreshToken).filter(
|
|
RefreshToken.token == refresh_token,
|
|
RefreshToken.user_id == user_id,
|
|
RefreshToken.revoked == False,
|
|
RefreshToken.expires_at > datetime.utcnow()
|
|
).first()
|
|
|
|
if not token_record:
|
|
return None
|
|
|
|
user = db.query(User).filter(User.id == user_id).first()
|
|
if not user or user.status != UserStatus.ACTIVE:
|
|
return None
|
|
|
|
# 创建新的访问令牌
|
|
access_token_expires = timedelta(minutes=120)
|
|
access_token = SecurityUtils.create_access_token(
|
|
data={
|
|
"sub": user.username,
|
|
"user_id": user.id,
|
|
"role": user.role.value
|
|
},
|
|
expires_delta=access_token_expires
|
|
)
|
|
|
|
return {
|
|
"access_token": access_token,
|
|
"token_type": "bearer",
|
|
"expires_in": int(access_token_expires.total_seconds())
|
|
}
|
|
|
|
except Exception:
|
|
return None
|
|
|
|
@staticmethod
|
|
def revoke_token(db: Session, refresh_token: str) -> bool:
|
|
"""撤销刷新令牌"""
|
|
token_record = db.query(RefreshToken).filter(RefreshToken.token == refresh_token).first()
|
|
if token_record:
|
|
token_record.revoked = True
|
|
token_record.revoked_at = datetime.utcnow()
|
|
db.commit()
|
|
return True
|
|
return False
|
|
|
|
@staticmethod
|
|
def revoke_all_user_tokens(db: Session, user_id: int) -> int:
|
|
"""撤销用户所有刷新令牌"""
|
|
tokens = db.query(RefreshToken).filter(
|
|
RefreshToken.user_id == user_id,
|
|
RefreshToken.revoked == False
|
|
).all()
|
|
|
|
for token in tokens:
|
|
token.revoked = True
|
|
token.revoked_at = datetime.utcnow()
|
|
|
|
db.commit()
|
|
return len(tokens)
|