From fa2c9968e61c357210bda44eecd2ec8ae5eef6de Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Wed, 17 Jun 2026 07:16:02 +0800 Subject: [PATCH] feat(permissions): add permission system and checker - 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 --- packages/domain/permissions.py | 301 +++++++++++++++++++++++++++++++++ tests/unit/test_permissions.py | 207 +++++++++++++++++++++++ 2 files changed, 508 insertions(+) create mode 100644 packages/domain/permissions.py create mode 100644 tests/unit/test_permissions.py diff --git a/packages/domain/permissions.py b/packages/domain/permissions.py new file mode 100644 index 000000000..096a2136c --- /dev/null +++ b/packages/domain/permissions.py @@ -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 diff --git a/tests/unit/test_permissions.py b/tests/unit/test_permissions.py new file mode 100644 index 000000000..a841fbb3c --- /dev/null +++ b/tests/unit/test_permissions.py @@ -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