sync: 同步 sql-platform-pro 最新代码 — 权限体系安全修复 v0.8.1

- P0: 实例访问控制全线接入权限组 (829测试)
- P1: permissions字段激活 + 操作级权限检查 (843测试)
- 前端: 删除数据权限页面, 权限组合并为唯一入口
- 新增: CodeMirror编辑器 + 查询模板 + 慢查询分析
- 新增: MongoDB管理页面 + Dashboard增强
- 修复: Pydantic V2迁移 + 前端TS错误修复
This commit is contained in:
logan committed 2026-05-23 14:41:06 +08:00
1 parent 6fcd9465cf
commit 4c9427e546
105 files changed
+23680 -471

No files matched your search

+360
View File
@@ -0,0 +1,360 @@
"""P1 操作级权限测试: InstanceGroup.permissions 字段生效
测试矩阵:
- admin 不受 permissions 限制
- 组没配置 permissions → 允许所有(向后兼容)
- 组配置了 ["query:select"] → 只有 select 允许,ticket:create 被拒绝
- 用户级覆盖权限优先于组默认
- 公开实例不受 permissions 限制
- 集成测试: query/ticket 端点权限检查
"""
import json
import pytest
from fastapi import HTTPException
from app.models.instance import Instance
from app.models.instance_group import InstanceGroup, user_groups
from app.core.security import create_access_token
# ── Fixtures ──────────────────────────────────────────────
@pytest.fixture
async def group_with_permissions(db):
"""创建带 permissions 配置的权限组"""
group = InstanceGroup(
name="只读组",
db_type="mysql",
permissions=json.dumps(["query:select", "instance:detail"]),
)
db.add(group)
await db.flush()
instance = Instance(
name="生产MySQL", db_type="mysql", host="10.0.0.1", port=3306,
username="root", password="pw", database="prod_db",
status="disconnected", group_id=group.id,
)
db.add(instance)
await db.commit()
await db.refresh(group)
await db.refresh(instance)
return group, instance
@pytest.fixture
async def group_no_permissions(db):
"""创建没有 permissions 配置的权限组(向后兼容)"""
group = InstanceGroup(name="默认组", db_type="mysql", permissions=None)
db.add(group)
await db.flush()
instance = Instance(
name="测试MySQL", db_type="mysql", host="127.0.0.1", port=3306,
username="root", password="pw", database="test_db",
status="disconnected", group_id=group.id,
)
db.add(instance)
await db.commit()
await db.refresh(group)
await db.refresh(instance)
return group, instance
# ── 单元测试: check_instance_permission ──────────────────
class TestCheckInstancePermission:
"""check_instance_permission 核心逻辑测试"""
@pytest.mark.asyncio
async def test_admin_bypasses_permission_check(
self, db, admin_user, group_with_permissions
):
"""admin 不受 permissions 限制"""
from app.core.permissions import check_instance_permission
group, instance = group_with_permissions
# ticket:create 不在组 permissions 中,但 admin 应该通过
await check_instance_permission(admin_user, instance, "ticket:create", db)
@pytest.mark.asyncio
async def test_public_instance_allows_all(self, db, developer_user):
"""公开实例不受 permissions 限制"""
from app.core.permissions import check_instance_permission
instance = Instance(
name="公开", db_type="mysql", host="127.0.0.1", port=3306,
username="root", password="pw", database="pub_db",
status="disconnected", group_id=None,
)
db.add(instance)
await db.commit()
await check_instance_permission(developer_user, instance, "ticket:create", db)
@pytest.mark.asyncio
async def test_allowed_action_passes(
self, db, developer_user, group_with_permissions
):
"""用户在组内,操作在 permissions 列表中 → 通过"""
from app.core.permissions import check_instance_permission
group, instance = group_with_permissions
await db.execute(
user_groups.insert().values(
user_id=developer_user.id, group_id=group.id
)
)
await db.commit()
# query:select 在组 permissions 中
await check_instance_permission(developer_user, instance, "query:select", db)
@pytest.mark.asyncio
async def test_denied_action_raises_403(
self, db, developer_user, group_with_permissions
):
"""用户在组内,操作不在 permissions 列表中 → 403"""
from app.core.permissions import check_instance_permission
group, instance = group_with_permissions
await db.execute(
user_groups.insert().values(
user_id=developer_user.id, group_id=group.id
)
)
await db.commit()
# ticket:create 不在组 permissions ["query:select", "instance:detail"] 中
with pytest.raises(HTTPException) as exc_info:
await check_instance_permission(
developer_user, instance, "ticket:create", db
)
assert exc_info.value.status_code == 403
@pytest.mark.asyncio
async def test_no_permissions_config_allows_all(
self, db, developer_user, group_no_permissions
):
"""组没有配置 permissions → 允许所有操作(向后兼容)"""
from app.core.permissions import check_instance_permission
group, instance = group_no_permissions
await db.execute(
user_groups.insert().values(
user_id=developer_user.id, group_id=group.id
)
)
await db.commit()
# ticket:create 应该被允许,因为组没有配置限制
await check_instance_permission(developer_user, instance, "ticket:create", db)
@pytest.mark.asyncio
async def test_user_override_permissions(
self, db, developer_user, group_with_permissions
):
"""用户级覆盖权限优先于组默认"""
from app.core.permissions import check_instance_permission
group, instance = group_with_permissions
# 组 permissions = ["query:select", "instance:detail"]
# 用户覆盖为 ["query:select"] — 只给查询权限
await db.execute(
user_groups.insert().values(
user_id=developer_user.id,
group_id=group.id,
permissions=json.dumps(["query:select"]),
)
)
await db.commit()
# query:select 通过(用户级覆盖包含)
await check_instance_permission(developer_user, instance, "query:select", db)
# instance:detail 被拒绝(组默认有,但用户覆盖只给了 query:select)
with pytest.raises(HTTPException) as exc_info:
await check_instance_permission(
developer_user, instance, "instance:detail", db
)
assert exc_info.value.status_code == 403
# ── 单元测试: _get_effective_permissions ──────────────────
class TestGetEffectivePermissions:
"""_get_effective_permissions 权限解析逻辑"""
@pytest.mark.asyncio
async def test_returns_all_when_no_config(
self, db, developer_user, group_no_permissions
):
"""无权限配置时返回全部权限"""
from app.core.permissions import _get_effective_permissions, PERMISSION_ACTIONS
group, instance = group_no_permissions
await db.execute(
user_groups.insert().values(
user_id=developer_user.id, group_id=group.id
)
)
await db.commit()
result = await _get_effective_permissions(developer_user, instance, db)
assert result == PERMISSION_ACTIONS
@pytest.mark.asyncio
async def test_returns_group_permissions(
self, db, developer_user, group_with_permissions
):
"""有组权限配置时返回组权限"""
from app.core.permissions import _get_effective_permissions
group, instance = group_with_permissions
await db.execute(
user_groups.insert().values(
user_id=developer_user.id, group_id=group.id
)
)
await db.commit()
result = await _get_effective_permissions(developer_user, instance, db)
assert result == {"query:select", "instance:detail"}
@pytest.mark.asyncio
async def test_user_override_takes_precedence(
self, db, developer_user, group_with_permissions
):
"""用户级覆盖优先于组默认"""
from app.core.permissions import _get_effective_permissions
group, instance = group_with_permissions
await db.execute(
user_groups.insert().values(
user_id=developer_user.id,
group_id=group.id,
permissions=json.dumps(["query:select"]),
)
)
await db.commit()
result = await _get_effective_permissions(developer_user, instance, db)
assert result == {"query:select"}
@pytest.mark.asyncio
async def test_admin_not_in_group(
self, db, admin_user, group_with_permissions
):
"""admin 不在任何组,但 check_instance_permission 不调用此函数(admin bypass)"""
# admin 在 check_instance_permission 中直接 return,不会调用 _get_effective_permissions
pass
# ── 集成测试: API 端点权限检查 ────────────────────────────
class TestPermissionEndpointIntegration:
"""权限检查在实际 API 端点中的集成"""
@pytest.mark.asyncio
async def test_query_with_permission(
self, client, db, developer_user, group_with_permissions
):
"""有 query:select 权限 → 查询不被 403"""
group, instance = group_with_permissions
await db.execute(
user_groups.insert().values(
user_id=developer_user.id, group_id=group.id
)
)
await db.commit()
token = create_access_token(
data={"sub": str(developer_user.id), "role": developer_user.role}
)
resp = await client.post(
"/api/v1/query",
json={"instance_id": instance.id, "database": "test", "sql": "SELECT 1"},
headers={"Authorization": f"Bearer {token}"},
)
# 不应是 403(可能 400/500 因为连不上真DB,但权限通过)
assert resp.status_code != 403
@pytest.mark.asyncio
async def test_ticket_create_without_permission(
self, client, db, developer_user, group_with_permissions
):
"""没有 ticket:create 权限 → 创建工单 403"""
group, instance = group_with_permissions
await db.execute(
user_groups.insert().values(
user_id=developer_user.id, group_id=group.id
)
)
await db.commit()
token = create_access_token(
data={"sub": str(developer_user.id), "role": developer_user.role}
)
resp = await client.post(
"/api/v1/tickets",
json={
"title": "测试工单标题",
"db_type": "mysql",
"instance_id": instance.id,
"target_database": "test",
"sql_content": "ALTER TABLE t ADD COLUMN c INT",
"reason": "测试创建工单权限",
},
headers={"Authorization": f"Bearer {token}"},
)
assert resp.status_code == 403
assert "ticket:create" in resp.json()["detail"]
@pytest.mark.asyncio
async def test_ticket_create_with_permission(
self, client, db, developer_user, group_no_permissions
):
"""组没配置 permissions → 创建工单不被 403(向后兼容)"""
group, instance = group_no_permissions
await db.execute(
user_groups.insert().values(
user_id=developer_user.id, group_id=group.id
)
)
await db.commit()
token = create_access_token(
data={"sub": str(developer_user.id), "role": developer_user.role}
)
resp = await client.post(
"/api/v1/tickets",
json={
"title": "测试工单标题",
"db_type": "mysql",
"instance_id": instance.id,
"target_database": "test",
"sql_content": "ALTER TABLE t ADD COLUMN c INT",
"reason": "测试创建工单权限",
},
headers={"Authorization": f"Bearer {token}"},
)
# 不应是 403(可能 400 因为数据校验,但权限通过)
assert resp.status_code != 403
@pytest.mark.asyncio
async def test_query_denied_for_group_without_select(
self, client, db, developer_user
):
"""组 permissions 只有 ticket:create → query 被拒绝"""
group = InstanceGroup(
name="工单组",
db_type="mysql",
permissions=json.dumps(["ticket:create"]),
)
db.add(group)
await db.flush()
instance = Instance(
name="工单MySQL", db_type="mysql", host="10.0.0.1", port=3306,
username="root", password="pw", database="prod_db",
status="disconnected", group_id=group.id,
)
db.add(instance)
await db.execute(
user_groups.insert().values(
user_id=developer_user.id, group_id=group.id
)
)
await db.commit()
token = create_access_token(
data={"sub": str(developer_user.id), "role": developer_user.role}
)
resp = await client.post(
"/api/v1/query",
json={"instance_id": instance.id, "database": "test", "sql": "SELECT 1"},
headers={"Authorization": f"Bearer {token}"},
)
assert resp.status_code == 403
assert "query:select" in resp.json()["detail"]