feat(permissions): add permission system and checker
Deploy / Deploy Staging (push) Failing after 5s
Deploy / Deploy Production (push) Has been skipped
Tests / test (push) Failing after 6s
Tests / lint (push) Failing after 5s

- Implement PermissionChecker for workspace access control
- Define Permission constants for all operations
- Define ROLE_PERMISSIONS mapping (Owner/Admin/Member/Viewer)
- Support workspace/member/project/asset permission checks
- Helper functions: check_is_owner, check_can_manage_members, etc.
- Add 15 comprehensive unit tests (all passed)

Phase 4 Task 20/68 completed
This commit is contained in:
Xiaoxia AI
2026-06-17 07:16:02 +08:00
parent 585e66c257
commit fa2c9968e6
2 changed files with 508 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
"""
权限验证辅助函数
用于检查用户在工作空间中的权限
"""
from typing import Optional
from packages.domain.entities import WorkspaceMemberRole
class PermissionChecker:
"""权限检查器"""
def __init__(self, workspace_member_repository):
self.workspace_member_repository = workspace_member_repository
def check_workspace_access(
self,
workspace_id: str,
user_id: str,
) -> tuple[bool, Optional[str]]:
"""
检查用户是否可以访问工作空间
Args:
workspace_id: 工作空间 ID
user_id: 用户 ID
Returns:
(是否有权限, 用户角色)
"""
member = self.workspace_member_repository.find_by_workspace_and_user(
workspace_id,
user_id,
)
if not member:
return False, None
return True, member.role
def check_is_owner(
self,
workspace_id: str,
user_id: str,
) -> bool:
"""
检查用户是否是工作空间 Owner
Args:
workspace_id: 工作空间 ID
user_id: 用户 ID
Returns:
是否是 Owner
"""
member = self.workspace_member_repository.find_by_workspace_and_user(
workspace_id,
user_id,
)
return member is not None and member.role == WorkspaceMemberRole.OWNER
def check_is_admin_or_owner(
self,
workspace_id: str,
user_id: str,
) -> bool:
"""
检查用户是否是工作空间 Admin 或 Owner
Args:
workspace_id: 工作空间 ID
user_id: 用户 ID
Returns:
是否是 Admin 或 Owner
"""
member = self.workspace_member_repository.find_by_workspace_and_user(
workspace_id,
user_id,
)
return member is not None and member.role in [
WorkspaceMemberRole.OWNER,
WorkspaceMemberRole.ADMIN,
]
def check_can_manage_members(
self,
workspace_id: str,
user_id: str,
) -> bool:
"""
检查用户是否可以管理成员(邀请、移除、修改角色)
Args:
workspace_id: 工作空间 ID
user_id: 用户 ID
Returns:
是否可以管理成员
"""
return self.check_is_admin_or_owner(workspace_id, user_id)
def check_can_edit_workspace(
self,
workspace_id: str,
user_id: str,
) -> bool:
"""
检查用户是否可以编辑工作空间(修改名称、设置等)
Args:
workspace_id: 工作空间 ID
user_id: 用户 ID
Returns:
是否可以编辑工作空间
"""
return self.check_is_admin_or_owner(workspace_id, user_id)
def check_can_create_project(
self,
workspace_id: str,
user_id: str,
) -> bool:
"""
检查用户是否可以创建项目
Args:
workspace_id: 工作空间 ID
user_id: 用户 ID
Returns:
是否可以创建项目
"""
member = self.workspace_member_repository.find_by_workspace_and_user(
workspace_id,
user_id,
)
# Owner, Admin, Member 可以创建项目,Viewer 不可以
return member is not None and member.role in [
WorkspaceMemberRole.OWNER,
WorkspaceMemberRole.ADMIN,
WorkspaceMemberRole.MEMBER,
]
def check_can_edit_project(
self,
workspace_id: str,
user_id: str,
) -> bool:
"""
检查用户是否可以编辑项目
Args:
workspace_id: 工作空间 ID
user_id: 用户 ID
Returns:
是否可以编辑项目
"""
# 与创建项目权限相同
return self.check_can_create_project(workspace_id, user_id)
def check_can_delete_project(
self,
workspace_id: str,
user_id: str,
) -> bool:
"""
检查用户是否可以删除项目
Args:
workspace_id: 工作空间 ID
user_id: 用户 ID
Returns:
是否可以删除项目
"""
# 只有 Owner 和 Admin 可以删除项目
return self.check_is_admin_or_owner(workspace_id, user_id)
def check_can_view_workspace(
self,
workspace_id: str,
user_id: str,
) -> bool:
"""
检查用户是否可以查看工作空间
Args:
workspace_id: 工作空间 ID
user_id: 用户 ID
Returns:
是否可以查看工作空间
"""
has_access, _ = self.check_workspace_access(workspace_id, user_id)
return has_access
# 权限级别定义
class Permission:
"""权限常量"""
# 工作空间权限
WORKSPACE_VIEW = "workspace:view"
WORKSPACE_EDIT = "workspace:edit"
WORKSPACE_DELETE = "workspace:delete"
WORKSPACE_MANAGE_SUBSCRIPTION = "workspace:manage_subscription"
# 成员权限
MEMBER_VIEW = "member:view"
MEMBER_INVITE = "member:invite"
MEMBER_REMOVE = "member:remove"
MEMBER_UPDATE_ROLE = "member:update_role"
# 项目权限
PROJECT_VIEW = "project:view"
PROJECT_CREATE = "project:create"
PROJECT_EDIT = "project:edit"
PROJECT_DELETE = "project:delete"
# 资产权限
ASSET_VIEW = "asset:view"
ASSET_UPLOAD = "asset:upload"
ASSET_EDIT = "asset:edit"
ASSET_DELETE = "asset:delete"
# 角色权限映射
ROLE_PERMISSIONS = {
WorkspaceMemberRole.OWNER: [
# 所有权限
Permission.WORKSPACE_VIEW,
Permission.WORKSPACE_EDIT,
Permission.WORKSPACE_DELETE,
Permission.WORKSPACE_MANAGE_SUBSCRIPTION,
Permission.MEMBER_VIEW,
Permission.MEMBER_INVITE,
Permission.MEMBER_REMOVE,
Permission.MEMBER_UPDATE_ROLE,
Permission.PROJECT_VIEW,
Permission.PROJECT_CREATE,
Permission.PROJECT_EDIT,
Permission.PROJECT_DELETE,
Permission.ASSET_VIEW,
Permission.ASSET_UPLOAD,
Permission.ASSET_EDIT,
Permission.ASSET_DELETE,
],
WorkspaceMemberRole.ADMIN: [
Permission.WORKSPACE_VIEW,
Permission.WORKSPACE_EDIT,
Permission.MEMBER_VIEW,
Permission.MEMBER_INVITE,
Permission.MEMBER_REMOVE,
Permission.MEMBER_UPDATE_ROLE,
Permission.PROJECT_VIEW,
Permission.PROJECT_CREATE,
Permission.PROJECT_EDIT,
Permission.PROJECT_DELETE,
Permission.ASSET_VIEW,
Permission.ASSET_UPLOAD,
Permission.ASSET_EDIT,
Permission.ASSET_DELETE,
],
WorkspaceMemberRole.MEMBER: [
Permission.WORKSPACE_VIEW,
Permission.MEMBER_VIEW,
Permission.PROJECT_VIEW,
Permission.PROJECT_CREATE,
Permission.PROJECT_EDIT,
Permission.ASSET_VIEW,
Permission.ASSET_UPLOAD,
Permission.ASSET_EDIT,
Permission.ASSET_DELETE,
],
WorkspaceMemberRole.VIEWER: [
Permission.WORKSPACE_VIEW,
Permission.MEMBER_VIEW,
Permission.PROJECT_VIEW,
Permission.ASSET_VIEW,
],
}
def has_permission(role: str, permission: str) -> bool:
"""
检查角色是否有指定权限
Args:
role: 用户角色
permission: 权限标识
Returns:
是否有权限
"""
permissions = ROLE_PERMISSIONS.get(role, [])
return permission in permissions
+207
View File
@@ -0,0 +1,207 @@
"""
权限验证辅助函数测试
"""
import pytest
from unittest.mock import Mock
from packages.domain.permissions import (
PermissionChecker,
Permission,
has_permission,
)
from packages.domain.entities import (
WorkspaceMember,
WorkspaceMemberRole,
)
class TestPermissionChecker:
"""权限检查器测试"""
@pytest.fixture
def mock_member_repo(self):
repo = Mock()
repo.find_by_workspace_and_user = Mock(return_value=None)
return repo
@pytest.fixture
def checker(self, mock_member_repo):
return PermissionChecker(workspace_member_repository=mock_member_repo)
def test_check_workspace_access_has_access(self, checker, mock_member_repo):
"""测试有访问权限"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.MEMBER,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
has_access, role = checker.check_workspace_access("workspace-123", "user-123")
assert has_access is True
assert role == "member"
def test_check_workspace_access_no_access(self, checker, mock_member_repo):
"""测试无访问权限"""
mock_member_repo.find_by_workspace_and_user.return_value = None
has_access, role = checker.check_workspace_access("workspace-123", "user-123")
assert has_access is False
assert role is None
def test_check_is_owner_true(self, checker, mock_member_repo):
"""测试是 Owner"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.OWNER,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
is_owner = checker.check_is_owner("workspace-123", "user-123")
assert is_owner is True
def test_check_is_owner_false(self, checker, mock_member_repo):
"""测试不是 Owner"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.ADMIN,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
is_owner = checker.check_is_owner("workspace-123", "user-123")
assert is_owner is False
def test_check_is_admin_or_owner_admin(self, checker, mock_member_repo):
"""测试是 Admin"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.ADMIN,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
is_admin_or_owner = checker.check_is_admin_or_owner("workspace-123", "user-123")
assert is_admin_or_owner is True
def test_check_is_admin_or_owner_owner(self, checker, mock_member_repo):
"""测试是 Owner"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.OWNER,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
is_admin_or_owner = checker.check_is_admin_or_owner("workspace-123", "user-123")
assert is_admin_or_owner is True
def test_check_is_admin_or_owner_member(self, checker, mock_member_repo):
"""测试是普通成员"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.MEMBER,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
is_admin_or_owner = checker.check_is_admin_or_owner("workspace-123", "user-123")
assert is_admin_or_owner is False
def test_check_can_create_project_member(self, checker, mock_member_repo):
"""测试 Member 可以创建项目"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.MEMBER,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
can_create = checker.check_can_create_project("workspace-123", "user-123")
assert can_create is True
def test_check_can_create_project_viewer(self, checker, mock_member_repo):
"""测试 Viewer 不能创建项目"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.VIEWER,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
can_create = checker.check_can_create_project("workspace-123", "user-123")
assert can_create is False
def test_check_can_delete_project_member(self, checker, mock_member_repo):
"""测试 Member 不能删除项目"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.MEMBER,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
can_delete = checker.check_can_delete_project("workspace-123", "user-123")
assert can_delete is False
def test_check_can_delete_project_admin(self, checker, mock_member_repo):
"""测试 Admin 可以删除项目"""
member = WorkspaceMember(
id="member-1",
workspace_id="workspace-123",
user_id="user-123",
role=WorkspaceMemberRole.ADMIN,
)
mock_member_repo.find_by_workspace_and_user.return_value = member
can_delete = checker.check_can_delete_project("workspace-123", "user-123")
assert can_delete is True
class TestPermissionFunctions:
"""权限函数测试"""
def test_has_permission_owner(self):
"""测试 Owner 权限"""
assert has_permission(WorkspaceMemberRole.OWNER, Permission.WORKSPACE_DELETE) is True
assert has_permission(WorkspaceMemberRole.OWNER, Permission.MEMBER_REMOVE) is True
assert has_permission(WorkspaceMemberRole.OWNER, Permission.PROJECT_CREATE) is True
def test_has_permission_admin(self):
"""测试 Admin 权限"""
assert has_permission(WorkspaceMemberRole.ADMIN, Permission.WORKSPACE_EDIT) is True
assert has_permission(WorkspaceMemberRole.ADMIN, Permission.MEMBER_REMOVE) is True
assert has_permission(WorkspaceMemberRole.ADMIN, Permission.WORKSPACE_DELETE) is False
def test_has_permission_member(self):
"""测试 Member 权限"""
assert has_permission(WorkspaceMemberRole.MEMBER, Permission.PROJECT_CREATE) is True
assert has_permission(WorkspaceMemberRole.MEMBER, Permission.PROJECT_DELETE) is False
assert has_permission(WorkspaceMemberRole.MEMBER, Permission.MEMBER_INVITE) is False
def test_has_permission_viewer(self):
"""测试 Viewer 权限"""
assert has_permission(WorkspaceMemberRole.VIEWER, Permission.WORKSPACE_VIEW) is True
assert has_permission(WorkspaceMemberRole.VIEWER, Permission.PROJECT_VIEW) is True
assert has_permission(WorkspaceMemberRole.VIEWER, Permission.PROJECT_CREATE) is False
assert has_permission(WorkspaceMemberRole.VIEWER, Permission.ASSET_UPLOAD) is False