From a3f8d7b8cf93954512803fd20a80c41c46ea3e20 Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Tue, 16 Sep 2025 16:28:15 +0800 Subject: [PATCH 01/10] feat(role&permission): migrate the roles and permissions from freeleaps to authentication --- .../backend/models/permission/constants.py | 17 +++-- .../backend/models/permission/models.py | 1 + .../webapi/providers/permission_initialize.py | 66 ++++++++++++++++--- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/apps/authentication/backend/models/permission/constants.py b/apps/authentication/backend/models/permission/constants.py index 0499d50..e5765f9 100644 --- a/apps/authentication/backend/models/permission/constants.py +++ b/apps/authentication/backend/models/permission/constants.py @@ -12,7 +12,10 @@ class DefaultRole: # Default roles, which all tenants will have, cannot be modified. class DefaultRoleEnum(Enum): - ADMIN = DefaultRole("Administrator", "admin", "Have all permissions", 0) + ADMIN = DefaultRole("Admin", "admin", "Have all permissions", 1) + OPERATOR = DefaultRole("Operator", "operator", "System operator with deployment and management permissions", 10) + DEVELOPER = DefaultRole("Developer", "developer", "Developer with git and issue management access", 100) + QA = DefaultRole("QA", "qa", "Quality assurance with bug and testing permissions", 1000) @dataclass(frozen=True) # frozen=True @@ -24,9 +27,15 @@ class DefaultPermission: # Default permissions, which all tenants will have, cannot be modified. class DefaultPermissionEnum(Enum): - CHANGE_ROLES = DefaultPermission("change:roles", "Change roles", "Add/Update/Delete roles") - CHANGE_PERMISSIONS = DefaultPermission("change:permissions", "Change permissions", "Add/Update/Remove permissions") - ASSIGN_ROLES = DefaultPermission("assign:roles", "Assign roles", "Assign roles to user") + INVITE_COLLABORATOR = DefaultPermission("invite:collaborator", "Add/Remove participants", "Add/Remove participants") + PUBLISH_PRODUCTION = DefaultPermission("publish:production", "Deploy to production", "Deploy to production") + EDIT_PRODUCT = DefaultPermission("edit:product", "View product management UX", "View product management UX") + ACCESS_GIT_REPOSITORIES = DefaultPermission("access:git_repositories", "Access to git repositories", "Access to git repositories") + ACCESS_ISSUE_MANAGEMENT = DefaultPermission("access:issue_management", "Access to issue management", "Access to issue management") + PUBLISH_ALPHA = DefaultPermission("publish:alpha", "Access to alpha deployment", "Access to alpha deployment") + OPEN_BUGS = DefaultPermission("open:bugs", "Open/Close/Re-open bugs", "Open/Close/Re-open bugs") + QA_FAILED_PASSED = DefaultPermission("qa:failed_passed", "Update QA status - QA failed/passed", "Update QA status - QA failed/passed") + QA_TEST_REPORTS = DefaultPermission("qa:test_reports", "Update QA status - Test reports", "Update QA status - Test reports (Test coverage)") class AdministrativeRole(IntEnum): diff --git a/apps/authentication/backend/models/permission/models.py b/apps/authentication/backend/models/permission/models.py index 1197613..c7c3619 100644 --- a/apps/authentication/backend/models/permission/models.py +++ b/apps/authentication/backend/models/permission/models.py @@ -26,6 +26,7 @@ class RoleDoc(Document): role_description: Optional[str] = None permission_ids: list[str] role_level: int + revision_id: Optional[str] = None # Revision ID for version control created_at: datetime = datetime.now() # Creation timestamp, auto-generated updated_at: datetime = datetime.now() # Last update timestamp, auto-updated is_default: bool = False diff --git a/apps/authentication/webapi/providers/permission_initialize.py b/apps/authentication/webapi/providers/permission_initialize.py index 7054360..b70b897 100644 --- a/apps/authentication/webapi/providers/permission_initialize.py +++ b/apps/authentication/webapi/providers/permission_initialize.py @@ -10,12 +10,9 @@ def register(app): @app.on_event("startup") async def init_admin_permission(): - # Initialize permissions if not exist - default_permission_ids = [] - for default_permission in \ - [DefaultPermissionEnum.CHANGE_PERMISSIONS, - DefaultPermissionEnum.CHANGE_ROLES, - DefaultPermissionEnum.ASSIGN_ROLES]: + # Initialize all permissions if not exist + permission_id_map = {} + for default_permission in DefaultPermissionEnum: if not await PermissionDoc.find_one( {str(PermissionDoc.permission_key): default_permission.value.permission_key}): doc = await PermissionDoc( @@ -24,17 +21,66 @@ def register(app): description=default_permission.value.permission_description, is_default=True, ).insert() - default_permission_ids.append(str(doc.id)) - logging.info(f"default permissions initialized {default_permission_ids}") + permission_id_map[default_permission.value.permission_key] = str(doc.id) + else: + # Get existing permission ID + existing_doc = await PermissionDoc.find_one( + {str(PermissionDoc.permission_key): default_permission.value.permission_key}) + permission_id_map[default_permission.value.permission_key] = str(existing_doc.id) + + logging.info(f"default permissions initialized {list(permission_id_map.keys())}") + + # Define role permission mappings based on the provided data + role_permission_mappings = { + DefaultRoleEnum.ADMIN: [ + DefaultPermissionEnum.PUBLISH_ALPHA, + DefaultPermissionEnum.PUBLISH_PRODUCTION, + DefaultPermissionEnum.INVITE_COLLABORATOR, + DefaultPermissionEnum.EDIT_PRODUCT, + DefaultPermissionEnum.ACCESS_GIT_REPOSITORIES, + DefaultPermissionEnum.ACCESS_ISSUE_MANAGEMENT, + DefaultPermissionEnum.OPEN_BUGS, + DefaultPermissionEnum.QA_FAILED_PASSED, + DefaultPermissionEnum.QA_TEST_REPORTS + ], + DefaultRoleEnum.OPERATOR: [ + DefaultPermissionEnum.PUBLISH_ALPHA, + DefaultPermissionEnum.PUBLISH_PRODUCTION, + DefaultPermissionEnum.EDIT_PRODUCT, + DefaultPermissionEnum.ACCESS_GIT_REPOSITORIES, + DefaultPermissionEnum.ACCESS_ISSUE_MANAGEMENT, + DefaultPermissionEnum.OPEN_BUGS, + DefaultPermissionEnum.QA_FAILED_PASSED, + DefaultPermissionEnum.QA_TEST_REPORTS, + ], + DefaultRoleEnum.DEVELOPER: [ + DefaultPermissionEnum.ACCESS_GIT_REPOSITORIES, + DefaultPermissionEnum.ACCESS_ISSUE_MANAGEMENT, + DefaultPermissionEnum.PUBLISH_ALPHA, + ], + DefaultRoleEnum.QA: [ + DefaultPermissionEnum.OPEN_BUGS, + DefaultPermissionEnum.QA_FAILED_PASSED, + DefaultPermissionEnum.QA_TEST_REPORTS, + ], + } + # Initialize roles if not exist default_role_ids = [] - for default_role in [DefaultRoleEnum.ADMIN]: + for default_role in DefaultRoleEnum: if not await RoleDoc.find_one({str(RoleDoc.role_key): default_role.value.role_key}): + # Get permission IDs for this role + role_permission_ids = [] + if default_role in role_permission_mappings: + for permission in role_permission_mappings[default_role]: + if permission.value.permission_key in permission_id_map: + role_permission_ids.append(permission_id_map[permission.value.permission_key]) + doc = await RoleDoc( role_key=default_role.value.role_key, role_name=default_role.value.role_name, role_description=default_role.value.role_description, - permission_ids=default_permission_ids, + permission_ids=role_permission_ids, role_level=default_role.value.role_level, is_default=True, ).insert() From 03bd84ba9a297466edb145cd0b24d50f8118e8f1 Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Tue, 16 Sep 2025 16:30:43 +0800 Subject: [PATCH 02/10] feat(permission): use new defined permission --- .../webapi/routes/permission/create_permission.py | 2 +- .../webapi/routes/permission/delete_permission.py | 2 +- .../webapi/routes/permission/update_permission.py | 2 +- apps/authentication/webapi/routes/role/assign_permissions.py | 2 +- apps/authentication/webapi/routes/role/create_role.py | 2 +- apps/authentication/webapi/routes/role/delete_role.py | 2 +- apps/authentication/webapi/routes/role/update_role.py | 2 +- apps/authentication/webapi/routes/user/assign_roles.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/authentication/webapi/routes/permission/create_permission.py b/apps/authentication/webapi/routes/permission/create_permission.py index cffb410..d9b4211 100644 --- a/apps/authentication/webapi/routes/permission/create_permission.py +++ b/apps/authentication/webapi/routes/permission/create_permission.py @@ -37,7 +37,7 @@ class PermissionResponse(BaseModel): ) async def create_permission( req: CreatePermissionRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key])) + _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> PermissionResponse: doc = await permission_service.create_permission(req.permission_key, req.permission_name, req.description) diff --git a/apps/authentication/webapi/routes/permission/delete_permission.py b/apps/authentication/webapi/routes/permission/delete_permission.py index be8f344..0b15f77 100644 --- a/apps/authentication/webapi/routes/permission/delete_permission.py +++ b/apps/authentication/webapi/routes/permission/delete_permission.py @@ -27,7 +27,7 @@ class DeletePermissionResponse(BaseModel): ) async def delete_permission( req: DeletePermissionRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key])) + _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> DeletePermissionResponse: await permission_service.delete_permission(req.permission_id) return DeletePermissionResponse(success=True) diff --git a/apps/authentication/webapi/routes/permission/update_permission.py b/apps/authentication/webapi/routes/permission/update_permission.py index 176b231..cf1d624 100644 --- a/apps/authentication/webapi/routes/permission/update_permission.py +++ b/apps/authentication/webapi/routes/permission/update_permission.py @@ -39,7 +39,7 @@ class PermissionResponse(BaseModel): ) async def update_permission( req: UpdatePermissionRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key])) + _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> PermissionResponse: doc = await permission_service.update_permission(req.permission_id, req.permission_key, req.permission_name, req.description) diff --git a/apps/authentication/webapi/routes/role/assign_permissions.py b/apps/authentication/webapi/routes/role/assign_permissions.py index 3095a4a..46a038c 100644 --- a/apps/authentication/webapi/routes/role/assign_permissions.py +++ b/apps/authentication/webapi/routes/role/assign_permissions.py @@ -35,7 +35,7 @@ class RoleResponse(BaseModel): ) async def assign_permissions_to_role( req: AssignPermissionsRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key])) + _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> RoleResponse: doc = await role_service.assign_permissions_to_role(req.role_id, req.permission_ids) return RoleResponse(**doc.dict()) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/create_role.py b/apps/authentication/webapi/routes/role/create_role.py index 2936f31..9d17028 100644 --- a/apps/authentication/webapi/routes/role/create_role.py +++ b/apps/authentication/webapi/routes/role/create_role.py @@ -40,7 +40,7 @@ class RoleResponse(BaseModel): ) async def create_role( req: CreateRoleRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key])) + _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> RoleResponse: doc = await role_service.create_role(req.role_key, req.role_name, req.role_description, req.role_level) return RoleResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/role/delete_role.py b/apps/authentication/webapi/routes/role/delete_role.py index 95f6d5a..8cb1610 100644 --- a/apps/authentication/webapi/routes/role/delete_role.py +++ b/apps/authentication/webapi/routes/role/delete_role.py @@ -27,7 +27,7 @@ class DeleteRoleResponse(BaseModel): ) async def delete_role( req: DeleteRoleRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key])) + _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> DeleteRoleResponse: await role_service.delete_role(req.role_id) return DeleteRoleResponse(success=True) diff --git a/apps/authentication/webapi/routes/role/update_role.py b/apps/authentication/webapi/routes/role/update_role.py index 71717a2..e14038f 100644 --- a/apps/authentication/webapi/routes/role/update_role.py +++ b/apps/authentication/webapi/routes/role/update_role.py @@ -41,7 +41,7 @@ class RoleResponse(BaseModel): ) async def update_role( req: UpdateRoleRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key])) + _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> RoleResponse: doc = await role_service.update_role(req.role_id, req.role_key, req.role_name, req.role_description, req.role_level) return RoleResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/user/assign_roles.py b/apps/authentication/webapi/routes/user/assign_roles.py index e14c89b..213fadf 100644 --- a/apps/authentication/webapi/routes/user/assign_roles.py +++ b/apps/authentication/webapi/routes/user/assign_roles.py @@ -31,7 +31,7 @@ class UserRoleResponse(BaseModel): ) async def assign_roles_to_user( req: AssignRolesRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.ASSIGN_ROLES.value.permission_key])), + _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])), ) -> UserRoleResponse: doc = await user_management_service.assign_roles_to_user(req.user_id, req.role_ids) return UserRoleResponse(**doc.dict()) From 49770da205676854dee8d5d6c3c52872c7e6a7a0 Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Tue, 23 Sep 2025 09:46:54 +0800 Subject: [PATCH 03/10] feat(custom): enable custom_id function to migrate freeleaps data safely --- .../infra/permission/permission_handler.py | 44 +++++++++++++---- .../backend/infra/permission/role_handler.py | 47 ++++++++++++++++--- .../services/permission/permission_service.py | 8 ++-- .../services/permission/role_service.py | 8 ++-- .../routes/permission/create_permission.py | 5 +- .../routes/permission/update_permission.py | 5 +- .../webapi/routes/role/create_role.py | 5 +- .../webapi/routes/role/update_role.py | 5 +- 8 files changed, 96 insertions(+), 31 deletions(-) diff --git a/apps/authentication/backend/infra/permission/permission_handler.py b/apps/authentication/backend/infra/permission/permission_handler.py index 586fb6a..4076185 100644 --- a/apps/authentication/backend/infra/permission/permission_handler.py +++ b/apps/authentication/backend/infra/permission/permission_handler.py @@ -12,7 +12,7 @@ class PermissionHandler: pass async def create_permission(self, permission_key: str, permission_name: str, - description: Optional[str] = None) -> Optional[PermissionDoc]: + description: Optional[str] = None, custom_permission_id: Optional[str] = None) -> Optional[PermissionDoc]: """Create a new permission document""" if not permission_key or not permission_name: raise RequestValidationError("permission_key and permission_name are required.") @@ -21,6 +21,14 @@ class PermissionHandler: {str(PermissionDoc.permission_key): permission_key}) or await PermissionDoc.find_one( {str(PermissionDoc.permission_name): permission_name}): raise RequestValidationError("permission has already been created.") + if custom_permission_id: + try: + custom_id = PydanticObjectId(custom_permission_id) + if await PermissionDoc.get(custom_id): + raise RequestValidationError("Permission with the provided custom_permission_id already exists.") + except Exception: + raise RequestValidationError("Invalid custom_permission_id format. Must be a valid ObjectId.") + doc = PermissionDoc( permission_key=permission_key, permission_name=permission_name, @@ -28,11 +36,15 @@ class PermissionHandler: created_at=datetime.now(), updated_at=datetime.now() ) + + if custom_permission_id: + doc.id = PydanticObjectId(custom_permission_id) + await doc.insert() return doc async def update_permission(self, permission_id: PydanticObjectId, permission_key: Optional[str] = None, - permission_name: Optional[str] = None, description: Optional[str] = None) -> Optional[ + permission_name: Optional[str] = None, description: Optional[str] = None, custom_permission_id: Optional[str] = None) -> Optional[ PermissionDoc]: """Update an existing permission document by id, ensuring permission_key is unique""" if not permission_id or not permission_key or not permission_name: @@ -40,8 +52,8 @@ class PermissionHandler: doc = await PermissionDoc.get(permission_id) if not doc: raise RequestValidationError("Permission not found.") - if doc.is_default: - raise RequestValidationError("Default permission cannot be updated.") + #if doc.is_default: + # raise RequestValidationError("Default permission cannot be updated.") # Check for uniqueness (exclude self) conflict = await PermissionDoc.find_one({ "$and": [ @@ -58,8 +70,24 @@ class PermissionHandler: doc.permission_name = permission_name doc.description = description doc.updated_at = datetime.now() - - await doc.save() + + if custom_permission_id: + # Store the old ID for cleanup + old_id = doc.id + doc.id = PydanticObjectId(custom_permission_id) + await doc.save() + + # Delete the old document with the original ID + try: + old_doc = await PermissionDoc.get(old_id) + if old_doc: + await old_doc.delete() + except Exception as e: + # Log the error but don't fail the operation + print(f"Warning: Failed to delete old permission document {old_id}: {e}") + else: + await doc.save() + return doc async def query_permissions( @@ -92,6 +120,6 @@ class PermissionHandler: if not doc: raise RequestValidationError("Permission not found.") # Check if the permission is default - if doc.is_default: - raise RequestValidationError("Default permission cannot be deleted.") + #if doc.is_default: + # raise RequestValidationError("Default permission cannot be deleted.") await doc.delete() diff --git a/apps/authentication/backend/infra/permission/role_handler.py b/apps/authentication/backend/infra/permission/role_handler.py index 18b6136..bb7391e 100644 --- a/apps/authentication/backend/infra/permission/role_handler.py +++ b/apps/authentication/backend/infra/permission/role_handler.py @@ -11,13 +11,23 @@ class RoleHandler: def __init__(self): pass - async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> Optional[RoleDoc]: + async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int, custom_role_id: Optional[str] = None) -> Optional[RoleDoc]: """Create a new role, ensuring role_key and role_name are unique and not empty""" if not role_key or not role_name: raise RequestValidationError("role_key and role_name are required.") if await RoleDoc.find_one({str(RoleDoc.role_key): role_key}) or await RoleDoc.find_one( {str(RoleDoc.role_name): role_name}): raise RequestValidationError("role_key or role_name has already been created.") + + # Check if custom_role_id is provided and if it already exists + if custom_role_id: + try: + custom_id = PydanticObjectId(custom_role_id) + if await RoleDoc.get(custom_id): + raise RequestValidationError("Role with the provided custom_role_id already exists.") + except Exception: + raise RequestValidationError("Invalid custom_role_id format. Must be a valid ObjectId.") + doc = RoleDoc( role_key=role_key, role_name=role_name, @@ -27,11 +37,16 @@ class RoleHandler: created_at=datetime.now(), updated_at=datetime.now() ) + + # Set custom ID if provided + if custom_role_id: + doc.id = PydanticObjectId(custom_role_id) + await doc.insert() return doc async def update_role(self, role_id: PydanticObjectId, role_key: str, role_name: str, - role_description: Optional[str], role_level: int) -> Optional[ + role_description: Optional[str], role_level: int, custom_role_id: Optional[str] = None) -> Optional[ RoleDoc]: """Update an existing role, ensuring role_key and role_name are unique and not empty""" if not role_id or not role_key or not role_name: @@ -39,8 +54,8 @@ class RoleHandler: doc = await RoleDoc.get(role_id) if not doc: raise RequestValidationError("role not found.") - if doc.is_default: - raise RequestValidationError("Default role cannot be updated.") + #if doc.is_default: + # raise RequestValidationError("Default role cannot be updated.") # Check for uniqueness (exclude self) conflict = await RoleDoc.find_one({ "$and": [ @@ -58,7 +73,25 @@ class RoleHandler: doc.role_description = role_description doc.role_level = role_level doc.updated_at = datetime.now() - await doc.save() + + # Set custom role ID if provided + if custom_role_id: + # Store the old ID for cleanup + old_id = doc.id + doc.id = PydanticObjectId(custom_role_id) + await doc.save() + + # Delete the old document with the original ID + try: + old_doc = await RoleDoc.get(old_id) + if old_doc: + await old_doc.delete() + except Exception as e: + # Log the error but don't fail the operation + print(f"Warning: Failed to delete old role document {old_id}: {e}") + else: + await doc.save() + return doc async def query_roles(self, role_key: Optional[str], role_name: Optional[str], skip: int = 0, limit: int = 10) -> \ @@ -108,6 +141,6 @@ class RoleHandler: if not doc: raise RequestValidationError("Role not found.") # Check if the role is default - if doc.is_default: - raise RequestValidationError("Default role cannot be deleted.") + #if doc.is_default: + # raise RequestValidationError("Default role cannot be deleted.") await doc.delete() diff --git a/apps/authentication/backend/services/permission/permission_service.py b/apps/authentication/backend/services/permission/permission_service.py index 1c13488..1feb8be 100644 --- a/apps/authentication/backend/services/permission/permission_service.py +++ b/apps/authentication/backend/services/permission/permission_service.py @@ -10,13 +10,13 @@ class PermissionService: def __init__(self): self.permission_handler = PermissionHandler() - async def create_permission(self, permission_key: str, permission_name: str, description: Optional[str] = None) -> PermissionDoc: + async def create_permission(self, permission_key: str, permission_name: str, description: Optional[str] = None, custom_permission_id: Optional[str] = None) -> PermissionDoc: """Create a new permission document""" - return await self.permission_handler.create_permission(permission_key, permission_name, description) + return await self.permission_handler.create_permission(permission_key, permission_name, description, custom_permission_id) - async def update_permission(self, permission_id: str, permission_key: Optional[str] = None, permission_name: Optional[str] = None, description: Optional[str] = None) -> PermissionDoc: + async def update_permission(self, permission_id: str, permission_key: Optional[str] = None, permission_name: Optional[str] = None, description: Optional[str] = None, custom_permission_id: Optional[str] = None) -> PermissionDoc: """Update an existing permission document by id""" - return await self.permission_handler.update_permission(PydanticObjectId(permission_id), permission_key, permission_name, description) + return await self.permission_handler.update_permission(PydanticObjectId(permission_id), permission_key, permission_name, description, custom_permission_id) async def query_permissions(self, permission_key: Optional[str] = None, permission_name: Optional[str] = None, page: int = 1, page_size: int = 10) -> Dict[str, Any]: """Query permissions with pagination and fuzzy search""" diff --git a/apps/authentication/backend/services/permission/role_service.py b/apps/authentication/backend/services/permission/role_service.py index 081ba77..929639f 100644 --- a/apps/authentication/backend/services/permission/role_service.py +++ b/apps/authentication/backend/services/permission/role_service.py @@ -10,16 +10,16 @@ class RoleService: def __init__(self): self.role_handler = RoleHandler() - async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> RoleDoc: + async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int, custom_role_id: Optional[str] = None) -> RoleDoc: """Create a new role, ensuring role_key and role_name are unique and not empty""" - doc = await self.role_handler.create_role(role_key, role_name, role_description, role_level) + doc = await self.role_handler.create_role(role_key, role_name, role_description, role_level, custom_role_id) return doc - async def update_role(self, role_id: str, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> RoleDoc: + async def update_role(self, role_id: str, role_key: str, role_name: str, role_description: Optional[str], role_level: int, custom_role_id: Optional[str] = None) -> RoleDoc: """Update an existing role, ensuring role_key and role_name are unique and not empty""" - doc = await self.role_handler.update_role(PydanticObjectId(role_id), role_key, role_name, role_description, role_level) + doc = await self.role_handler.update_role(PydanticObjectId(role_id), role_key, role_name, role_description, role_level, custom_role_id) return doc async def query_roles(self, role_key: Optional[str], role_name: Optional[str], page: int = 1, page_size: int = 10) -> Dict[str, Any]: diff --git a/apps/authentication/webapi/routes/permission/create_permission.py b/apps/authentication/webapi/routes/permission/create_permission.py index d9b4211..27ecd8c 100644 --- a/apps/authentication/webapi/routes/permission/create_permission.py +++ b/apps/authentication/webapi/routes/permission/create_permission.py @@ -17,6 +17,7 @@ class CreatePermissionRequest(BaseModel): permission_key: str permission_name: str description: Optional[str] = None + custom_permission_id: Optional[str] = None class PermissionResponse(BaseModel): @@ -37,8 +38,8 @@ class PermissionResponse(BaseModel): ) async def create_permission( req: CreatePermissionRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> PermissionResponse: - doc = await permission_service.create_permission(req.permission_key, req.permission_name, req.description) + doc = await permission_service.create_permission(req.permission_key, req.permission_name, req.description, req.custom_permission_id) return PermissionResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/permission/update_permission.py b/apps/authentication/webapi/routes/permission/update_permission.py index cf1d624..7a87c27 100644 --- a/apps/authentication/webapi/routes/permission/update_permission.py +++ b/apps/authentication/webapi/routes/permission/update_permission.py @@ -19,6 +19,7 @@ class UpdatePermissionRequest(BaseModel): permission_key: str permission_name: str description: Optional[str] = None + custom_permission_id: Optional[str] = None class PermissionResponse(BaseModel): @@ -39,8 +40,8 @@ class PermissionResponse(BaseModel): ) async def update_permission( req: UpdatePermissionRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> PermissionResponse: doc = await permission_service.update_permission(req.permission_id, req.permission_key, req.permission_name, - req.description) + req.description, req.custom_permission_id) return PermissionResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/role/create_role.py b/apps/authentication/webapi/routes/role/create_role.py index 9d17028..1fcfc11 100644 --- a/apps/authentication/webapi/routes/role/create_role.py +++ b/apps/authentication/webapi/routes/role/create_role.py @@ -18,6 +18,7 @@ class CreateRoleRequest(BaseModel): role_name: str role_description: Optional[str] = None role_level: int + custom_role_id: Optional[str] = None class RoleResponse(BaseModel): @@ -40,7 +41,7 @@ class RoleResponse(BaseModel): ) async def create_role( req: CreateRoleRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> RoleResponse: - doc = await role_service.create_role(req.role_key, req.role_name, req.role_description, req.role_level) + doc = await role_service.create_role(req.role_key, req.role_name, req.role_description, req.role_level, req.custom_role_id) return RoleResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/role/update_role.py b/apps/authentication/webapi/routes/role/update_role.py index e14038f..c8788fe 100644 --- a/apps/authentication/webapi/routes/role/update_role.py +++ b/apps/authentication/webapi/routes/role/update_role.py @@ -19,6 +19,7 @@ class UpdateRoleRequest(BaseModel): role_name: str role_description: Optional[str] = None role_level: int + custom_role_id: Optional[str] = None class RoleResponse(BaseModel): @@ -41,7 +42,7 @@ class RoleResponse(BaseModel): ) async def update_role( req: UpdateRoleRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> RoleResponse: - doc = await role_service.update_role(req.role_id, req.role_key, req.role_name, req.role_description, req.role_level) + doc = await role_service.update_role(req.role_id, req.role_key, req.role_name, req.role_description, req.role_level, req.custom_role_id) return RoleResponse(**doc.dict()) From f01dd755aaa01f66fabf99a9daeb8c7200f890e5 Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Tue, 23 Sep 2025 09:56:20 +0800 Subject: [PATCH 04/10] chore(role&permission): remove definitions of roles and permissions from authentication --- .../backend/models/permission/constants.py | 39 +------- .../webapi/bootstrap/application.py | 2 - .../webapi/providers/permission_initialize.py | 88 ------------------- .../routes/permission/create_permission.py | 1 - .../routes/permission/delete_permission.py | 3 +- .../routes/permission/update_permission.py | 1 - .../webapi/routes/role/assign_permissions.py | 3 +- .../webapi/routes/role/create_role.py | 1 - .../webapi/routes/role/delete_role.py | 3 +- .../webapi/routes/role/update_role.py | 1 - .../webapi/routes/user/assign_roles.py | 3 +- 11 files changed, 5 insertions(+), 140 deletions(-) delete mode 100644 apps/authentication/webapi/providers/permission_initialize.py diff --git a/apps/authentication/backend/models/permission/constants.py b/apps/authentication/backend/models/permission/constants.py index e5765f9..89c6104 100644 --- a/apps/authentication/backend/models/permission/constants.py +++ b/apps/authentication/backend/models/permission/constants.py @@ -1,41 +1,4 @@ -from dataclasses import dataclass -from enum import IntEnum, Enum - - -@dataclass(frozen=True) # frozen=True -class DefaultRole: - role_name: str - role_key: str - role_description: str - role_level: int - - -# Default roles, which all tenants will have, cannot be modified. -class DefaultRoleEnum(Enum): - ADMIN = DefaultRole("Admin", "admin", "Have all permissions", 1) - OPERATOR = DefaultRole("Operator", "operator", "System operator with deployment and management permissions", 10) - DEVELOPER = DefaultRole("Developer", "developer", "Developer with git and issue management access", 100) - QA = DefaultRole("QA", "qa", "Quality assurance with bug and testing permissions", 1000) - - -@dataclass(frozen=True) # frozen=True -class DefaultPermission: - permission_key: str - permission_name: str - permission_description: str - - -# Default permissions, which all tenants will have, cannot be modified. -class DefaultPermissionEnum(Enum): - INVITE_COLLABORATOR = DefaultPermission("invite:collaborator", "Add/Remove participants", "Add/Remove participants") - PUBLISH_PRODUCTION = DefaultPermission("publish:production", "Deploy to production", "Deploy to production") - EDIT_PRODUCT = DefaultPermission("edit:product", "View product management UX", "View product management UX") - ACCESS_GIT_REPOSITORIES = DefaultPermission("access:git_repositories", "Access to git repositories", "Access to git repositories") - ACCESS_ISSUE_MANAGEMENT = DefaultPermission("access:issue_management", "Access to issue management", "Access to issue management") - PUBLISH_ALPHA = DefaultPermission("publish:alpha", "Access to alpha deployment", "Access to alpha deployment") - OPEN_BUGS = DefaultPermission("open:bugs", "Open/Close/Re-open bugs", "Open/Close/Re-open bugs") - QA_FAILED_PASSED = DefaultPermission("qa:failed_passed", "Update QA status - QA failed/passed", "Update QA status - QA failed/passed") - QA_TEST_REPORTS = DefaultPermission("qa:test_reports", "Update QA status - Test reports", "Update QA status - Test reports (Test coverage)") +from enum import IntEnum class AdministrativeRole(IntEnum): diff --git a/apps/authentication/webapi/bootstrap/application.py b/apps/authentication/webapi/bootstrap/application.py index d857e7f..3f6dfb3 100644 --- a/apps/authentication/webapi/bootstrap/application.py +++ b/apps/authentication/webapi/bootstrap/application.py @@ -11,7 +11,6 @@ from webapi.providers import metrics # from webapi.providers import scheduler from webapi.providers import exception_handler -from webapi.providers import permission_initialize from .freeleaps_app import FreeleapsApp from common.config.app_settings import app_settings @@ -24,7 +23,6 @@ def create_app() -> FastAPI: register(app, exception_handler) register(app, database) register(app, router) - register(app, permission_initialize) # register(app, scheduler) register(app, common) diff --git a/apps/authentication/webapi/providers/permission_initialize.py b/apps/authentication/webapi/providers/permission_initialize.py deleted file mode 100644 index b70b897..0000000 --- a/apps/authentication/webapi/providers/permission_initialize.py +++ /dev/null @@ -1,88 +0,0 @@ -import logging - -from backend.models.permission import PermissionDoc, RoleDoc -from backend.models.permission.constants import DefaultPermissionEnum, DefaultRoleEnum - - -def register(app): - # Configure logging for pymongo - logging.getLogger("init_admin_permission").setLevel(logging.INFO) # Suppress DEBUG logs - - @app.on_event("startup") - async def init_admin_permission(): - # Initialize all permissions if not exist - permission_id_map = {} - for default_permission in DefaultPermissionEnum: - if not await PermissionDoc.find_one( - {str(PermissionDoc.permission_key): default_permission.value.permission_key}): - doc = await PermissionDoc( - permission_key=default_permission.value.permission_key, - permission_name=default_permission.value.permission_name, - description=default_permission.value.permission_description, - is_default=True, - ).insert() - permission_id_map[default_permission.value.permission_key] = str(doc.id) - else: - # Get existing permission ID - existing_doc = await PermissionDoc.find_one( - {str(PermissionDoc.permission_key): default_permission.value.permission_key}) - permission_id_map[default_permission.value.permission_key] = str(existing_doc.id) - - logging.info(f"default permissions initialized {list(permission_id_map.keys())}") - - # Define role permission mappings based on the provided data - role_permission_mappings = { - DefaultRoleEnum.ADMIN: [ - DefaultPermissionEnum.PUBLISH_ALPHA, - DefaultPermissionEnum.PUBLISH_PRODUCTION, - DefaultPermissionEnum.INVITE_COLLABORATOR, - DefaultPermissionEnum.EDIT_PRODUCT, - DefaultPermissionEnum.ACCESS_GIT_REPOSITORIES, - DefaultPermissionEnum.ACCESS_ISSUE_MANAGEMENT, - DefaultPermissionEnum.OPEN_BUGS, - DefaultPermissionEnum.QA_FAILED_PASSED, - DefaultPermissionEnum.QA_TEST_REPORTS - ], - DefaultRoleEnum.OPERATOR: [ - DefaultPermissionEnum.PUBLISH_ALPHA, - DefaultPermissionEnum.PUBLISH_PRODUCTION, - DefaultPermissionEnum.EDIT_PRODUCT, - DefaultPermissionEnum.ACCESS_GIT_REPOSITORIES, - DefaultPermissionEnum.ACCESS_ISSUE_MANAGEMENT, - DefaultPermissionEnum.OPEN_BUGS, - DefaultPermissionEnum.QA_FAILED_PASSED, - DefaultPermissionEnum.QA_TEST_REPORTS, - ], - DefaultRoleEnum.DEVELOPER: [ - DefaultPermissionEnum.ACCESS_GIT_REPOSITORIES, - DefaultPermissionEnum.ACCESS_ISSUE_MANAGEMENT, - DefaultPermissionEnum.PUBLISH_ALPHA, - ], - DefaultRoleEnum.QA: [ - DefaultPermissionEnum.OPEN_BUGS, - DefaultPermissionEnum.QA_FAILED_PASSED, - DefaultPermissionEnum.QA_TEST_REPORTS, - ], - } - - # Initialize roles if not exist - default_role_ids = [] - for default_role in DefaultRoleEnum: - if not await RoleDoc.find_one({str(RoleDoc.role_key): default_role.value.role_key}): - # Get permission IDs for this role - role_permission_ids = [] - if default_role in role_permission_mappings: - for permission in role_permission_mappings[default_role]: - if permission.value.permission_key in permission_id_map: - role_permission_ids.append(permission_id_map[permission.value.permission_key]) - - doc = await RoleDoc( - role_key=default_role.value.role_key, - role_name=default_role.value.role_name, - role_description=default_role.value.role_description, - permission_ids=role_permission_ids, - role_level=default_role.value.role_level, - is_default=True, - ).insert() - default_role_ids.append(str(doc.id)) - logging.info(f"default roles initialized {default_role_ids}") diff --git a/apps/authentication/webapi/routes/permission/create_permission.py b/apps/authentication/webapi/routes/permission/create_permission.py index 27ecd8c..e186ef7 100644 --- a/apps/authentication/webapi/routes/permission/create_permission.py +++ b/apps/authentication/webapi/routes/permission/create_permission.py @@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel from typing import Optional -from backend.models.permission.constants import DefaultPermissionEnum from backend.services.permission.permission_service import PermissionService from common.token.token_manager import TokenManager diff --git a/apps/authentication/webapi/routes/permission/delete_permission.py b/apps/authentication/webapi/routes/permission/delete_permission.py index 0b15f77..34038fb 100644 --- a/apps/authentication/webapi/routes/permission/delete_permission.py +++ b/apps/authentication/webapi/routes/permission/delete_permission.py @@ -1,7 +1,6 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel -from backend.models.permission.constants import DefaultPermissionEnum from backend.services.permission.permission_service import PermissionService from common.token.token_manager import TokenManager @@ -27,7 +26,7 @@ class DeletePermissionResponse(BaseModel): ) async def delete_permission( req: DeletePermissionRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> DeletePermissionResponse: await permission_service.delete_permission(req.permission_id) return DeletePermissionResponse(success=True) diff --git a/apps/authentication/webapi/routes/permission/update_permission.py b/apps/authentication/webapi/routes/permission/update_permission.py index 7a87c27..073516a 100644 --- a/apps/authentication/webapi/routes/permission/update_permission.py +++ b/apps/authentication/webapi/routes/permission/update_permission.py @@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel from typing import Optional -from backend.models.permission.constants import DefaultPermissionEnum from backend.services.permission.permission_service import PermissionService from common.token.token_manager import TokenManager diff --git a/apps/authentication/webapi/routes/role/assign_permissions.py b/apps/authentication/webapi/routes/role/assign_permissions.py index 46a038c..51fca2a 100644 --- a/apps/authentication/webapi/routes/role/assign_permissions.py +++ b/apps/authentication/webapi/routes/role/assign_permissions.py @@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel from typing import List -from backend.models.permission.constants import DefaultPermissionEnum from backend.services.permission.role_service import RoleService from common.token.token_manager import TokenManager @@ -35,7 +34,7 @@ class RoleResponse(BaseModel): ) async def assign_permissions_to_role( req: AssignPermissionsRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> RoleResponse: doc = await role_service.assign_permissions_to_role(req.role_id, req.permission_ids) return RoleResponse(**doc.dict()) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/create_role.py b/apps/authentication/webapi/routes/role/create_role.py index 1fcfc11..d68de94 100644 --- a/apps/authentication/webapi/routes/role/create_role.py +++ b/apps/authentication/webapi/routes/role/create_role.py @@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel from typing import Optional, List -from backend.models.permission.constants import DefaultPermissionEnum from backend.services.permission.role_service import RoleService from common.token.token_manager import TokenManager diff --git a/apps/authentication/webapi/routes/role/delete_role.py b/apps/authentication/webapi/routes/role/delete_role.py index 8cb1610..8a8832a 100644 --- a/apps/authentication/webapi/routes/role/delete_role.py +++ b/apps/authentication/webapi/routes/role/delete_role.py @@ -1,7 +1,6 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel -from backend.models.permission.constants import DefaultPermissionEnum from backend.services.permission.role_service import RoleService from common.token.token_manager import TokenManager @@ -27,7 +26,7 @@ class DeleteRoleResponse(BaseModel): ) async def delete_role( req: DeleteRoleRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) ) -> DeleteRoleResponse: await role_service.delete_role(req.role_id) return DeleteRoleResponse(success=True) diff --git a/apps/authentication/webapi/routes/role/update_role.py b/apps/authentication/webapi/routes/role/update_role.py index c8788fe..137eac3 100644 --- a/apps/authentication/webapi/routes/role/update_role.py +++ b/apps/authentication/webapi/routes/role/update_role.py @@ -4,7 +4,6 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel from typing import Optional, List -from backend.models.permission.constants import DefaultPermissionEnum from backend.services.permission.role_service import RoleService from common.token.token_manager import TokenManager diff --git a/apps/authentication/webapi/routes/user/assign_roles.py b/apps/authentication/webapi/routes/user/assign_roles.py index 213fadf..79b2ab3 100644 --- a/apps/authentication/webapi/routes/user/assign_roles.py +++ b/apps/authentication/webapi/routes/user/assign_roles.py @@ -3,7 +3,6 @@ from fastapi.params import Depends from pydantic import BaseModel from typing import List, Optional -from backend.models.permission.constants import DefaultPermissionEnum from backend.services.user.user_management_service import UserManagementService from common.token.token_manager import TokenManager @@ -31,7 +30,7 @@ class UserRoleResponse(BaseModel): ) async def assign_roles_to_user( req: AssignRolesRequest, - _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])), + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])), ) -> UserRoleResponse: doc = await user_management_service.assign_roles_to_user(req.user_id, req.role_ids) return UserRoleResponse(**doc.dict()) From bda5660bb6d494fc1c29232f72cd34696a37c12f Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Wed, 24 Sep 2025 17:29:27 +0800 Subject: [PATCH 05/10] feat(api): add query role by id API --- .../backend/infra/permission/role_handler.py | 9 ++++ .../services/permission/role_service.py | 4 ++ .../webapi/routes/role/__init__.py | 2 + .../webapi/routes/role/query_role_by_id.py | 51 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 apps/authentication/webapi/routes/role/query_role_by_id.py diff --git a/apps/authentication/backend/infra/permission/role_handler.py b/apps/authentication/backend/infra/permission/role_handler.py index bb7391e..d096d7e 100644 --- a/apps/authentication/backend/infra/permission/role_handler.py +++ b/apps/authentication/backend/infra/permission/role_handler.py @@ -107,6 +107,15 @@ class RoleHandler: docs = await cursor.skip(skip).limit(limit).to_list() return docs, total + async def query_roles_by_id(self, role_id: PydanticObjectId) -> Optional[RoleDoc]: + """Query a role by its ID""" + if not role_id: + raise RequestValidationError("role_id is required.") + doc = await RoleDoc.get(role_id) + if not doc: + raise RequestValidationError("Role not found.") + return doc + async def assign_permissions_to_role(self, role_id: PydanticObjectId, permission_ids: List[str]) -> Optional[RoleDoc]: """Assign permissions to a role by updating the permission_ids field""" if not role_id or not permission_ids: diff --git a/apps/authentication/backend/services/permission/role_service.py b/apps/authentication/backend/services/permission/role_service.py index 929639f..143b381 100644 --- a/apps/authentication/backend/services/permission/role_service.py +++ b/apps/authentication/backend/services/permission/role_service.py @@ -39,6 +39,10 @@ class RoleService: """Assign permissions to a role by updating the permission_ids field""" return await self.role_handler.assign_permissions_to_role(PydanticObjectId(role_id), permission_ids) + async def query_roles_by_id(self, role_id: str) -> RoleDoc: + """Query a single role by ID""" + return await self.role_handler.query_roles_by_id(PydanticObjectId(role_id)) + async def delete_role(self, role_id: str) -> None: """Delete a role document after checking if it is referenced by any user""" return await self.role_handler.delete_role(PydanticObjectId(role_id)) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/__init__.py b/apps/authentication/webapi/routes/role/__init__.py index 61d7118..f82d8b7 100644 --- a/apps/authentication/webapi/routes/role/__init__.py +++ b/apps/authentication/webapi/routes/role/__init__.py @@ -2,6 +2,7 @@ from fastapi import APIRouter from .create_role import router as create_role_router from .update_role import router as update_role_router from .query_role import router as query_role_router +from .query_role_by_id import router as query_role_by_id_router from .assign_permissions import router as assign_permissions_router from .delete_role import router as delete_role_router @@ -10,5 +11,6 @@ router = APIRouter() router.include_router(create_role_router, prefix="/role", tags=["role"]) router.include_router(update_role_router, prefix="/role", tags=["role"]) router.include_router(query_role_router, prefix="/role", tags=["role"]) +router.include_router(query_role_by_id_router, prefix="/role", tags=["role"]) router.include_router(assign_permissions_router, prefix="/role", tags=["role"]) router.include_router(delete_role_router, prefix="/role", tags=["role"]) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/query_role_by_id.py b/apps/authentication/webapi/routes/role/query_role_by_id.py new file mode 100644 index 0000000..87fdafc --- /dev/null +++ b/apps/authentication/webapi/routes/role/query_role_by_id.py @@ -0,0 +1,51 @@ +from datetime import datetime + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from typing import Optional, List + +from backend.services.permission.role_service import RoleService +from common.token.token_manager import TokenManager + +router = APIRouter() +token_manager = TokenManager() +role_service = RoleService() + +class QueryRoleByIdRequest(BaseModel): + role_id: str + +class QueryRoleByIdResponse(BaseModel): + id: str + role_key: str + role_name: str + role_description: Optional[str] = None + permission_ids: List[str] + role_level: int + created_at: datetime + updated_at: datetime + + +@router.post( + "/query_by_id", + response_model=QueryRoleByIdResponse, + operation_id="query-role-by-id", + summary="Query Role by Id", + description="Query role by id." +) +async def query_roles_by_id( + req: QueryRoleByIdRequest, +) -> QueryRoleByIdResponse: + try: + result = await role_service.query_roles_by_id(req.role_id) + return QueryRoleByIdResponse( + id=str(result.id), + role_key=result.role_key, + role_name=result.role_name, + role_description=result.role_description, + permission_ids=result.permission_ids, + role_level=result.role_level, + created_at=result.created_at, + updated_at=result.updated_at + ) + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) \ No newline at end of file From bad6224bd9ea3218f1a97e30506868a89d68ac4d Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Wed, 24 Sep 2025 19:46:19 +0800 Subject: [PATCH 06/10] fix(bug): not delete the correct and exsited data --- .../backend/infra/permission/permission_handler.py | 2 +- apps/authentication/backend/infra/permission/role_handler.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/authentication/backend/infra/permission/permission_handler.py b/apps/authentication/backend/infra/permission/permission_handler.py index 4076185..9983396 100644 --- a/apps/authentication/backend/infra/permission/permission_handler.py +++ b/apps/authentication/backend/infra/permission/permission_handler.py @@ -80,7 +80,7 @@ class PermissionHandler: # Delete the old document with the original ID try: old_doc = await PermissionDoc.get(old_id) - if old_doc: + if (str(old_id) != custom_permission_id) and old_doc: await old_doc.delete() except Exception as e: # Log the error but don't fail the operation diff --git a/apps/authentication/backend/infra/permission/role_handler.py b/apps/authentication/backend/infra/permission/role_handler.py index d096d7e..c1dd796 100644 --- a/apps/authentication/backend/infra/permission/role_handler.py +++ b/apps/authentication/backend/infra/permission/role_handler.py @@ -84,7 +84,7 @@ class RoleHandler: # Delete the old document with the original ID try: old_doc = await RoleDoc.get(old_id) - if old_doc: + if (str(old_id) != custom_role_id) and old_doc: await old_doc.delete() except Exception as e: # Log the error but don't fail the operation @@ -122,7 +122,7 @@ class RoleHandler: raise RequestValidationError("role_id and permission_ids are required.") doc = await RoleDoc.get(role_id) if not doc: - raise RequestValidationError("Role not found.") + raise RequestValidationError(f"Role not found. {role_id}") # Validate that all permission_ids exist in the permission collection for permission_id in permission_ids: From 58eea0e2b2accc05351e0fa24e77199991be3bb8 Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Fri, 26 Sep 2025 17:15:18 +0800 Subject: [PATCH 07/10] feat(recover): recover the initial version because we will use the new interfaces --- .../webapi/routes/permission/create_permission.py | 5 ++--- .../webapi/routes/permission/delete_permission.py | 2 +- .../webapi/routes/permission/update_permission.py | 5 ++--- apps/authentication/webapi/routes/role/assign_permissions.py | 2 +- apps/authentication/webapi/routes/role/create_role.py | 5 ++--- apps/authentication/webapi/routes/role/delete_role.py | 2 +- apps/authentication/webapi/routes/role/update_role.py | 5 ++--- 7 files changed, 11 insertions(+), 15 deletions(-) diff --git a/apps/authentication/webapi/routes/permission/create_permission.py b/apps/authentication/webapi/routes/permission/create_permission.py index e186ef7..2482fef 100644 --- a/apps/authentication/webapi/routes/permission/create_permission.py +++ b/apps/authentication/webapi/routes/permission/create_permission.py @@ -16,7 +16,6 @@ class CreatePermissionRequest(BaseModel): permission_key: str permission_name: str description: Optional[str] = None - custom_permission_id: Optional[str] = None class PermissionResponse(BaseModel): @@ -37,8 +36,8 @@ class PermissionResponse(BaseModel): ) async def create_permission( req: CreatePermissionRequest, - #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key])) ) -> PermissionResponse: - doc = await permission_service.create_permission(req.permission_key, req.permission_name, req.description, req.custom_permission_id) + doc = await permission_service.create_permission(req.permission_key, req.permission_name, req.description) return PermissionResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/permission/delete_permission.py b/apps/authentication/webapi/routes/permission/delete_permission.py index 34038fb..18cc508 100644 --- a/apps/authentication/webapi/routes/permission/delete_permission.py +++ b/apps/authentication/webapi/routes/permission/delete_permission.py @@ -26,7 +26,7 @@ class DeletePermissionResponse(BaseModel): ) async def delete_permission( req: DeletePermissionRequest, - #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key])) ) -> DeletePermissionResponse: await permission_service.delete_permission(req.permission_id) return DeletePermissionResponse(success=True) diff --git a/apps/authentication/webapi/routes/permission/update_permission.py b/apps/authentication/webapi/routes/permission/update_permission.py index 073516a..1d3cce3 100644 --- a/apps/authentication/webapi/routes/permission/update_permission.py +++ b/apps/authentication/webapi/routes/permission/update_permission.py @@ -18,7 +18,6 @@ class UpdatePermissionRequest(BaseModel): permission_key: str permission_name: str description: Optional[str] = None - custom_permission_id: Optional[str] = None class PermissionResponse(BaseModel): @@ -39,8 +38,8 @@ class PermissionResponse(BaseModel): ) async def update_permission( req: UpdatePermissionRequest, - #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key])) ) -> PermissionResponse: doc = await permission_service.update_permission(req.permission_id, req.permission_key, req.permission_name, - req.description, req.custom_permission_id) + req.description) return PermissionResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/role/assign_permissions.py b/apps/authentication/webapi/routes/role/assign_permissions.py index 51fca2a..0b007e2 100644 --- a/apps/authentication/webapi/routes/role/assign_permissions.py +++ b/apps/authentication/webapi/routes/role/assign_permissions.py @@ -34,7 +34,7 @@ class RoleResponse(BaseModel): ) async def assign_permissions_to_role( req: AssignPermissionsRequest, - #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key])) ) -> RoleResponse: doc = await role_service.assign_permissions_to_role(req.role_id, req.permission_ids) return RoleResponse(**doc.dict()) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/create_role.py b/apps/authentication/webapi/routes/role/create_role.py index d68de94..1d1366e 100644 --- a/apps/authentication/webapi/routes/role/create_role.py +++ b/apps/authentication/webapi/routes/role/create_role.py @@ -17,7 +17,6 @@ class CreateRoleRequest(BaseModel): role_name: str role_description: Optional[str] = None role_level: int - custom_role_id: Optional[str] = None class RoleResponse(BaseModel): @@ -40,7 +39,7 @@ class RoleResponse(BaseModel): ) async def create_role( req: CreateRoleRequest, - #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + # _: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key])) ) -> RoleResponse: - doc = await role_service.create_role(req.role_key, req.role_name, req.role_description, req.role_level, req.custom_role_id) + doc = await role_service.create_role(req.role_key, req.role_name, req.role_description, req.role_level) return RoleResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/role/delete_role.py b/apps/authentication/webapi/routes/role/delete_role.py index 8a8832a..7ee71df 100644 --- a/apps/authentication/webapi/routes/role/delete_role.py +++ b/apps/authentication/webapi/routes/role/delete_role.py @@ -26,7 +26,7 @@ class DeleteRoleResponse(BaseModel): ) async def delete_role( req: DeleteRoleRequest, - #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key])) ) -> DeleteRoleResponse: await role_service.delete_role(req.role_id) return DeleteRoleResponse(success=True) diff --git a/apps/authentication/webapi/routes/role/update_role.py b/apps/authentication/webapi/routes/role/update_role.py index 137eac3..24bc668 100644 --- a/apps/authentication/webapi/routes/role/update_role.py +++ b/apps/authentication/webapi/routes/role/update_role.py @@ -18,7 +18,6 @@ class UpdateRoleRequest(BaseModel): role_name: str role_description: Optional[str] = None role_level: int - custom_role_id: Optional[str] = None class RoleResponse(BaseModel): @@ -41,7 +40,7 @@ class RoleResponse(BaseModel): ) async def update_role( req: UpdateRoleRequest, - #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.INVITE_COLLABORATOR.value.permission_key])) + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_ROLES.value.permission_key])) ) -> RoleResponse: - doc = await role_service.update_role(req.role_id, req.role_key, req.role_name, req.role_description, req.role_level, req.custom_role_id) + doc = await role_service.update_role(req.role_id, req.role_key, req.role_name, req.role_description, req.role_level) return RoleResponse(**doc.dict()) From 89d0f2ee8f18a75d10eb685c1fe4f059d9c29aa8 Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Fri, 26 Sep 2025 17:17:14 +0800 Subject: [PATCH 08/10] feat(new): new interfaces for create or update role&permission, and query method not using pagination --- .../permission/create_or_update_permission.py | 45 +++++++++++++++++ .../query_permission_no_pagination.py | 45 +++++++++++++++++ .../routes/role/create_or_update_role.py | 49 +++++++++++++++++++ .../routes/role/query_role_no_pagination.py | 47 ++++++++++++++++++ 4 files changed, 186 insertions(+) create mode 100644 apps/authentication/webapi/routes/permission/create_or_update_permission.py create mode 100644 apps/authentication/webapi/routes/permission/query_permission_no_pagination.py create mode 100644 apps/authentication/webapi/routes/role/create_or_update_role.py create mode 100644 apps/authentication/webapi/routes/role/query_role_no_pagination.py diff --git a/apps/authentication/webapi/routes/permission/create_or_update_permission.py b/apps/authentication/webapi/routes/permission/create_or_update_permission.py new file mode 100644 index 0000000..74bd900 --- /dev/null +++ b/apps/authentication/webapi/routes/permission/create_or_update_permission.py @@ -0,0 +1,45 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from typing import Optional + +from backend.services.permission.permission_service import PermissionService +from common.token.token_manager import TokenManager + + +router = APIRouter() +token_manager = TokenManager() +permission_service = PermissionService() + + +class CreateOrUpdatePermissionRequest(BaseModel): + permission_key: str + permission_name: str + custom_permission_id: Optional[str] = None + description: Optional[str] = None + + +class PermissionResponse(BaseModel): + id: str + permission_key: str + permission_name: str + description: Optional[str] = None + created_at: datetime + updated_at: datetime + + +@router.post( + "/create-or-update", + response_model=PermissionResponse, + operation_id="create-or-update-permission", + summary="Create or Update Permission", + description="Create or update a permission by id." +) +async def create_or_update_permission( + req: CreateOrUpdatePermissionRequest, + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key])) +) -> PermissionResponse: + doc = await permission_service.create_or_update_permission(req.permission_key, req.permission_name, req.custom_permission_id, + req.description) + return PermissionResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/permission/query_permission_no_pagination.py b/apps/authentication/webapi/routes/permission/query_permission_no_pagination.py new file mode 100644 index 0000000..fc2bfba --- /dev/null +++ b/apps/authentication/webapi/routes/permission/query_permission_no_pagination.py @@ -0,0 +1,45 @@ +from datetime import datetime + +from fastapi import APIRouter +from pydantic import BaseModel +from typing import Optional, List +from backend.services.permission.permission_service import PermissionService +from common.token.token_manager import TokenManager + +router = APIRouter() +token_manager = TokenManager() +permission_service = PermissionService() + +class QueryPermissionNoPaginationRequest(BaseModel): + permission_id: Optional[str] = None + permission_key: Optional[str] = None + permission_name: Optional[str] = None + +class PermissionResponse(BaseModel): + id: str + permission_key: str + permission_name: str + description: Optional[str] = None + created_at: datetime + updated_at: datetime + +class QueryPermissionNoPaginationResponse(BaseModel): + items: List[PermissionResponse] + total: int + +@router.post( + "/query_permission_no_pagination", + response_model=QueryPermissionNoPaginationResponse, + operation_id="query-permission-no-pagination", + summary="Query Permission No Pagination", + description="Query permissions fuzzy search." +) +async def query_permission_no_pagination( + req: QueryPermissionNoPaginationRequest, +) -> QueryPermissionNoPaginationResponse: + result = await permission_service.query_permissions_no_pagination(req.permission_id, req.permission_key, req.permission_name) + items = [PermissionResponse(**item) for item in result["items"]] + return QueryPermissionNoPaginationResponse( + items=items, + total=result["total"] + ) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/create_or_update_role.py b/apps/authentication/webapi/routes/role/create_or_update_role.py new file mode 100644 index 0000000..d96343e --- /dev/null +++ b/apps/authentication/webapi/routes/role/create_or_update_role.py @@ -0,0 +1,49 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends +from pydantic import BaseModel +from typing import Optional, List + +from backend.services.permission.role_service import RoleService +from common.token.token_manager import TokenManager + + +router = APIRouter() +token_manager = TokenManager() +role_service = RoleService() + + +class CreateOrUpdateRoleRequest(BaseModel): + role_key: str + role_name: str + role_level: int + custom_role_id: Optional[str] = None + role_description: Optional[str] = None + + + +class RoleResponse(BaseModel): + id: str + role_key: str + role_name: str + role_description: Optional[str] = None + permission_ids: List[str] + role_level: int + created_at: datetime + updated_at: datetime + + +@router.post( + "/create-or-update", + response_model=RoleResponse, + operation_id="create-or-update-role", + summary="Create or Update Role", + description="Create or update a role by id." +) +async def create_or_update_permission( + req: CreateOrUpdateRoleRequest, + #_: bool = Depends(token_manager.has_all_permissions([DefaultPermissionEnum.CHANGE_PERMISSIONS.value.permission_key])) +) -> RoleResponse: + doc = await role_service.create_or_update_role(req.role_key, req.role_name, req.role_level, req.custom_role_id, req.role_description) + + return RoleResponse(**doc.dict()) diff --git a/apps/authentication/webapi/routes/role/query_role_no_pagination.py b/apps/authentication/webapi/routes/role/query_role_no_pagination.py new file mode 100644 index 0000000..3721d3b --- /dev/null +++ b/apps/authentication/webapi/routes/role/query_role_no_pagination.py @@ -0,0 +1,47 @@ +from datetime import datetime + +from fastapi import APIRouter +from pydantic import BaseModel +from typing import Optional, List +from backend.services.permission.role_service import RoleService +from common.token.token_manager import TokenManager + +router = APIRouter() +token_manager = TokenManager() +role_service = RoleService() + +class QueryRoleNoPaginationRequest(BaseModel): + role_id: Optional[str] = None + role_key: Optional[str] = None + role_name: Optional[str] = None + +class RoleResponse(BaseModel): + id: str + role_key: str + role_name: str + role_description: Optional[str] = None + permission_ids: List[str] + role_level: int + created_at: datetime + updated_at: datetime + +class QueryRoleNoPaginationResponse(BaseModel): + items: List[RoleResponse] + total: int + +@router.post( + "/query_role_no_pagination", + response_model=QueryRoleNoPaginationResponse, + operation_id="query-role-no-pagination", + summary="Query Role No Pagination", + description="Query roles fuzzy search without pagination." +) +async def query_role_no_pagination( + req: QueryRoleNoPaginationRequest, +) -> QueryRoleNoPaginationResponse: + result = await role_service.query_roles_no_pagination(req.role_id, req.role_key, req.role_name) + items = [RoleResponse(**item) for item in result["items"]] + return QueryRoleNoPaginationResponse( + items=items, + total=result["total"] + ) From 3a222d14dc5a9508a89d681feaad4490d6ec281e Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Fri, 26 Sep 2025 17:18:44 +0800 Subject: [PATCH 09/10] feat(new): add new methonds --- .../infra/permission/permission_handler.py | 126 +++++++++++----- .../backend/infra/permission/role_handler.py | 138 +++++++++++------- .../services/permission/permission_service.py | 19 ++- .../services/permission/role_service.py | 24 ++- .../webapi/routes/permission/__init__.py | 7 +- .../webapi/routes/role/__init__.py | 6 +- 6 files changed, 220 insertions(+), 100 deletions(-) diff --git a/apps/authentication/backend/infra/permission/permission_handler.py b/apps/authentication/backend/infra/permission/permission_handler.py index 9983396..a0b482b 100644 --- a/apps/authentication/backend/infra/permission/permission_handler.py +++ b/apps/authentication/backend/infra/permission/permission_handler.py @@ -12,7 +12,7 @@ class PermissionHandler: pass async def create_permission(self, permission_key: str, permission_name: str, - description: Optional[str] = None, custom_permission_id: Optional[str] = None) -> Optional[PermissionDoc]: + description: Optional[str] = None) -> Optional[PermissionDoc]: """Create a new permission document""" if not permission_key or not permission_name: raise RequestValidationError("permission_key and permission_name are required.") @@ -21,14 +21,6 @@ class PermissionHandler: {str(PermissionDoc.permission_key): permission_key}) or await PermissionDoc.find_one( {str(PermissionDoc.permission_name): permission_name}): raise RequestValidationError("permission has already been created.") - if custom_permission_id: - try: - custom_id = PydanticObjectId(custom_permission_id) - if await PermissionDoc.get(custom_id): - raise RequestValidationError("Permission with the provided custom_permission_id already exists.") - except Exception: - raise RequestValidationError("Invalid custom_permission_id format. Must be a valid ObjectId.") - doc = PermissionDoc( permission_key=permission_key, permission_name=permission_name, @@ -36,15 +28,11 @@ class PermissionHandler: created_at=datetime.now(), updated_at=datetime.now() ) - - if custom_permission_id: - doc.id = PydanticObjectId(custom_permission_id) - await doc.insert() return doc async def update_permission(self, permission_id: PydanticObjectId, permission_key: Optional[str] = None, - permission_name: Optional[str] = None, description: Optional[str] = None, custom_permission_id: Optional[str] = None) -> Optional[ + permission_name: Optional[str] = None, description: Optional[str] = None) -> Optional[ PermissionDoc]: """Update an existing permission document by id, ensuring permission_key is unique""" if not permission_id or not permission_key or not permission_name: @@ -52,8 +40,8 @@ class PermissionHandler: doc = await PermissionDoc.get(permission_id) if not doc: raise RequestValidationError("Permission not found.") - #if doc.is_default: - # raise RequestValidationError("Default permission cannot be updated.") + if doc.is_default: + raise RequestValidationError("Default permission cannot be updated.") # Check for uniqueness (exclude self) conflict = await PermissionDoc.find_one({ "$and": [ @@ -70,26 +58,70 @@ class PermissionHandler: doc.permission_name = permission_name doc.description = description doc.updated_at = datetime.now() - - if custom_permission_id: - # Store the old ID for cleanup - old_id = doc.id - doc.id = PydanticObjectId(custom_permission_id) - await doc.save() - - # Delete the old document with the original ID - try: - old_doc = await PermissionDoc.get(old_id) - if (str(old_id) != custom_permission_id) and old_doc: - await old_doc.delete() - except Exception as e: - # Log the error but don't fail the operation - print(f"Warning: Failed to delete old permission document {old_id}: {e}") - else: - await doc.save() - + + await doc.save() return doc + async def create_or_update_permission(self, permission_key: str, permission_name: str, custom_permission_id: Optional[str], description: Optional[str] = None) -> Optional[PermissionDoc]: + """Create or update a permission document""" + # Input validation + if not permission_key or not permission_name: + raise RequestValidationError("permission_key and permission_name are required.") + + def create_new_doc(): + return PermissionDoc( + permission_key=permission_key, + permission_name=permission_name, + description=description, + created_at=datetime.now(), + updated_at=datetime.now() + ) + + def update_doc_fields(doc): + doc.permission_key = permission_key + doc.permission_name = permission_name + doc.description = description + doc.updated_at = datetime.now() + + try: + # Check if permission with this key already exists + existing_doc = await PermissionDoc.find_one( + {str(PermissionDoc.permission_key): permission_key} + ) + except Exception as e: + existing_doc = None + + if existing_doc: + # If permission with this key already exists + if custom_permission_id and str(custom_permission_id) != str(existing_doc.id): + # Different ID provided - replace the document + id_conflict = await PermissionDoc.get(custom_permission_id) + if id_conflict: + raise RequestValidationError("Permission with the provided ID already exists.") + + new_doc = create_new_doc() + new_doc.id = PydanticObjectId(custom_permission_id) + await new_doc.insert() + await existing_doc.delete() + return new_doc + else: + # Same ID or no ID provided - update existing document + update_doc_fields(existing_doc) + await existing_doc.save() + return existing_doc + else: + # If no existing document with this key, create new document + new_doc = create_new_doc() + + if custom_permission_id: + id_conflict = await PermissionDoc.get(custom_permission_id) + if id_conflict: + raise RequestValidationError("Permission with the provided ID already exists.") + new_doc.id = PydanticObjectId(custom_permission_id) + + await new_doc.insert() + return new_doc + async def query_permissions( self, permission_key: Optional[str] = None, @@ -108,6 +140,28 @@ class PermissionHandler: docs = await cursor.skip(skip).limit(limit).to_list() return docs, total + async def query_permissions_no_pagination( + self, + permission_id: Optional[str] = None, + permission_key: Optional[str] = None, + permission_name: Optional[str] = None + ) -> Tuple[List[PermissionDoc], int]: + """Query permissions fuzzy search""" + query = {} + if permission_id: + try: + query[str(PermissionDoc.id)] = PydanticObjectId(permission_id) + except Exception: + raise RequestValidationError("Invalid permission_id format. Must be a valid ObjectId.") + if permission_key: + query[str(PermissionDoc.permission_key)] = {"$regex": permission_key, "$options": "i"} + if permission_name: + query[str(PermissionDoc.permission_name)] = {"$regex": permission_name, "$options": "i"} + cursor = PermissionDoc.find(query) + total = await cursor.count() + docs = await cursor.to_list() + return docs, total + async def delete_permission(self, permission_id: PydanticObjectId) -> None: """Delete a permission document after checking if it is referenced by any role and is not default""" if not permission_id: @@ -120,6 +174,6 @@ class PermissionHandler: if not doc: raise RequestValidationError("Permission not found.") # Check if the permission is default - #if doc.is_default: - # raise RequestValidationError("Default permission cannot be deleted.") + if doc.is_default: + raise RequestValidationError("Default permission cannot be deleted.") await doc.delete() diff --git a/apps/authentication/backend/infra/permission/role_handler.py b/apps/authentication/backend/infra/permission/role_handler.py index c1dd796..4b77d75 100644 --- a/apps/authentication/backend/infra/permission/role_handler.py +++ b/apps/authentication/backend/infra/permission/role_handler.py @@ -11,23 +11,13 @@ class RoleHandler: def __init__(self): pass - async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int, custom_role_id: Optional[str] = None) -> Optional[RoleDoc]: + async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> Optional[RoleDoc]: """Create a new role, ensuring role_key and role_name are unique and not empty""" if not role_key or not role_name: raise RequestValidationError("role_key and role_name are required.") if await RoleDoc.find_one({str(RoleDoc.role_key): role_key}) or await RoleDoc.find_one( {str(RoleDoc.role_name): role_name}): raise RequestValidationError("role_key or role_name has already been created.") - - # Check if custom_role_id is provided and if it already exists - if custom_role_id: - try: - custom_id = PydanticObjectId(custom_role_id) - if await RoleDoc.get(custom_id): - raise RequestValidationError("Role with the provided custom_role_id already exists.") - except Exception: - raise RequestValidationError("Invalid custom_role_id format. Must be a valid ObjectId.") - doc = RoleDoc( role_key=role_key, role_name=role_name, @@ -37,16 +27,11 @@ class RoleHandler: created_at=datetime.now(), updated_at=datetime.now() ) - - # Set custom ID if provided - if custom_role_id: - doc.id = PydanticObjectId(custom_role_id) - await doc.insert() return doc async def update_role(self, role_id: PydanticObjectId, role_key: str, role_name: str, - role_description: Optional[str], role_level: int, custom_role_id: Optional[str] = None) -> Optional[ + role_description: Optional[str], role_level: int) -> Optional[ RoleDoc]: """Update an existing role, ensuring role_key and role_name are unique and not empty""" if not role_id or not role_key or not role_name: @@ -54,8 +39,8 @@ class RoleHandler: doc = await RoleDoc.get(role_id) if not doc: raise RequestValidationError("role not found.") - #if doc.is_default: - # raise RequestValidationError("Default role cannot be updated.") + if doc.is_default: + raise RequestValidationError("Default role cannot be updated.") # Check for uniqueness (exclude self) conflict = await RoleDoc.find_one({ "$and": [ @@ -73,27 +58,69 @@ class RoleHandler: doc.role_description = role_description doc.role_level = role_level doc.updated_at = datetime.now() - - # Set custom role ID if provided - if custom_role_id: - # Store the old ID for cleanup - old_id = doc.id - doc.id = PydanticObjectId(custom_role_id) - await doc.save() - - # Delete the old document with the original ID - try: - old_doc = await RoleDoc.get(old_id) - if (str(old_id) != custom_role_id) and old_doc: - await old_doc.delete() - except Exception as e: - # Log the error but don't fail the operation - print(f"Warning: Failed to delete old role document {old_id}: {e}") - else: - await doc.save() - + await doc.save() return doc + async def create_or_update_role(self, role_key: str, role_name: str, role_level: int, custom_role_id: Optional[str], role_description: Optional[str] = None) -> Optional[RoleDoc]: + """Create or update a role document""" + # Input validation + if not role_key or not role_name: + raise RequestValidationError("role_key and role_name are required.") + + def create_new_doc(): + return RoleDoc( + role_key=role_key, + role_name=role_name, + role_description=role_description, + role_level=role_level, + permission_ids=[], + created_at=datetime.now(), + updated_at=datetime.now() + ) + def update_doc_fields(doc): + doc.role_key = role_key + doc.role_name = role_name + doc.role_description = role_description + doc.role_level = role_level + doc.updated_at = datetime.now() + + # Check if role with this key already exists + existing_doc = await RoleDoc.find_one( + {str(RoleDoc.role_key): role_key} + ) + + if existing_doc: + # If role with this key already exists + if custom_role_id and str(custom_role_id) != str(existing_doc.id): + # Different ID provided - replace the document + id_conflict = await RoleDoc.get(custom_role_id) + if id_conflict: + raise RequestValidationError("Role with the provided ID already exists.") + + new_doc = create_new_doc() + new_doc.id = PydanticObjectId(custom_role_id) + await new_doc.insert() + await existing_doc.delete() + return new_doc + + else: + # Same ID or no ID provided - update existing document + update_doc_fields(existing_doc) + await existing_doc.save() + return existing_doc + else: + # If no existing document with this key, create new document + new_doc = create_new_doc() + + if custom_role_id: + id_conflict = await RoleDoc.get(custom_role_id) + if id_conflict: + raise RequestValidationError("Role with the provided ID already exists.") + new_doc.id = PydanticObjectId(custom_role_id) + + await new_doc.insert() + return new_doc + async def query_roles(self, role_key: Optional[str], role_name: Optional[str], skip: int = 0, limit: int = 10) -> \ Tuple[List[RoleDoc], int]: """Query roles with pagination and fuzzy search by role_key and role_name""" @@ -107,14 +134,27 @@ class RoleHandler: docs = await cursor.skip(skip).limit(limit).to_list() return docs, total - async def query_roles_by_id(self, role_id: PydanticObjectId) -> Optional[RoleDoc]: - """Query a role by its ID""" - if not role_id: - raise RequestValidationError("role_id is required.") - doc = await RoleDoc.get(role_id) - if not doc: - raise RequestValidationError("Role not found.") - return doc + async def query_roles_no_pagination( + self, + role_id: Optional[str] = None, + role_key: Optional[str] = None, + role_name: Optional[str] = None + ) -> Tuple[List[RoleDoc], int]: + """Query roles fuzzy search without pagination""" + query = {} + if role_id: + try: + query[str(RoleDoc.id)] = PydanticObjectId(role_id) + except Exception: + raise RequestValidationError("Invalid role_id format. Must be a valid ObjectId.") + if role_key: + query[str(RoleDoc.role_key)] = {"$regex": role_key, "$options": "i"} + if role_name: + query[str(RoleDoc.role_name)] = {"$regex": role_name, "$options": "i"} + cursor = RoleDoc.find(query) + total = await cursor.count() + docs = await cursor.to_list() + return docs, total async def assign_permissions_to_role(self, role_id: PydanticObjectId, permission_ids: List[str]) -> Optional[RoleDoc]: """Assign permissions to a role by updating the permission_ids field""" @@ -122,7 +162,7 @@ class RoleHandler: raise RequestValidationError("role_id and permission_ids are required.") doc = await RoleDoc.get(role_id) if not doc: - raise RequestValidationError(f"Role not found. {role_id}") + raise RequestValidationError("Role not found.") # Validate that all permission_ids exist in the permission collection for permission_id in permission_ids: @@ -150,6 +190,6 @@ class RoleHandler: if not doc: raise RequestValidationError("Role not found.") # Check if the role is default - #if doc.is_default: - # raise RequestValidationError("Default role cannot be deleted.") + if doc.is_default: + raise RequestValidationError("Default role cannot be deleted.") await doc.delete() diff --git a/apps/authentication/backend/services/permission/permission_service.py b/apps/authentication/backend/services/permission/permission_service.py index 1feb8be..304b58a 100644 --- a/apps/authentication/backend/services/permission/permission_service.py +++ b/apps/authentication/backend/services/permission/permission_service.py @@ -10,14 +10,18 @@ class PermissionService: def __init__(self): self.permission_handler = PermissionHandler() - async def create_permission(self, permission_key: str, permission_name: str, description: Optional[str] = None, custom_permission_id: Optional[str] = None) -> PermissionDoc: + async def create_permission(self, permission_key: str, permission_name: str, description: Optional[str] = None) -> PermissionDoc: """Create a new permission document""" - return await self.permission_handler.create_permission(permission_key, permission_name, description, custom_permission_id) + return await self.permission_handler.create_permission(permission_key, permission_name, description) - async def update_permission(self, permission_id: str, permission_key: Optional[str] = None, permission_name: Optional[str] = None, description: Optional[str] = None, custom_permission_id: Optional[str] = None) -> PermissionDoc: + async def update_permission(self, permission_id: str, permission_key: Optional[str] = None, permission_name: Optional[str] = None, description: Optional[str] = None) -> PermissionDoc: """Update an existing permission document by id""" - return await self.permission_handler.update_permission(PydanticObjectId(permission_id), permission_key, permission_name, description, custom_permission_id) + return await self.permission_handler.update_permission(PydanticObjectId(permission_id), permission_key, permission_name, description) + async def create_or_update_permission(self, permission_key: str, permission_name: str, custom_permission_id: Optional[str], description: Optional[str] = None) -> PermissionDoc: + """Create or update a permission document""" + return await self.permission_handler.create_or_update_permission(permission_key, permission_name, custom_permission_id, description) + async def query_permissions(self, permission_key: Optional[str] = None, permission_name: Optional[str] = None, page: int = 1, page_size: int = 10) -> Dict[str, Any]: """Query permissions with pagination and fuzzy search""" if page < 1 or page_size < 1: @@ -30,6 +34,13 @@ class PermissionService: "page": page, "page_size": page_size } + async def query_permissions_no_pagination(self, permission_id: Optional[str] = None, permission_key: Optional[str] = None, permission_name: Optional[str] = None) -> Dict[str, Any]: + """Query permissions fuzzy search""" + docs, total = await self.permission_handler.query_permissions_no_pagination(permission_id, permission_key, permission_name) + return { + "items": [doc.dict() for doc in docs], + "total": total + } async def delete_permission(self, permission_id: str) -> None: """Delete a permission document after checking if it is referenced by any role""" diff --git a/apps/authentication/backend/services/permission/role_service.py b/apps/authentication/backend/services/permission/role_service.py index 143b381..97f84e2 100644 --- a/apps/authentication/backend/services/permission/role_service.py +++ b/apps/authentication/backend/services/permission/role_service.py @@ -10,18 +10,22 @@ class RoleService: def __init__(self): self.role_handler = RoleHandler() - async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int, custom_role_id: Optional[str] = None) -> RoleDoc: + async def create_role(self, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> RoleDoc: """Create a new role, ensuring role_key and role_name are unique and not empty""" - doc = await self.role_handler.create_role(role_key, role_name, role_description, role_level, custom_role_id) + doc = await self.role_handler.create_role(role_key, role_name, role_description, role_level) return doc - async def update_role(self, role_id: str, role_key: str, role_name: str, role_description: Optional[str], role_level: int, custom_role_id: Optional[str] = None) -> RoleDoc: + async def update_role(self, role_id: str, role_key: str, role_name: str, role_description: Optional[str], role_level: int) -> RoleDoc: """Update an existing role, ensuring role_key and role_name are unique and not empty""" - doc = await self.role_handler.update_role(PydanticObjectId(role_id), role_key, role_name, role_description, role_level, custom_role_id) + doc = await self.role_handler.update_role(PydanticObjectId(role_id), role_key, role_name, role_description, role_level) return doc + async def create_or_update_role(self, role_key: str, role_name: str, role_level: int, custom_role_id: Optional[str], role_description: Optional[str] = None) -> RoleDoc: + """Create or update a role document""" + return await self.role_handler.create_or_update_role(role_key, role_name, role_level, custom_role_id, role_description) + async def query_roles(self, role_key: Optional[str], role_name: Optional[str], page: int = 1, page_size: int = 10) -> Dict[str, Any]: """Query roles with pagination and fuzzy search by role_key and role_name""" if page < 1 or page_size < 1: @@ -35,14 +39,18 @@ class RoleService: "page_size": page_size } + async def query_roles_no_pagination(self, role_id: Optional[str] = None, role_key: Optional[str] = None, role_name: Optional[str] = None) -> Dict[str, Any]: + """Query roles fuzzy search without pagination""" + docs, total = await self.role_handler.query_roles_no_pagination(role_id, role_key, role_name) + return { + "items": [doc.dict() for doc in docs], + "total": total + } + async def assign_permissions_to_role(self, role_id: str, permission_ids: List[str]) -> RoleDoc: """Assign permissions to a role by updating the permission_ids field""" return await self.role_handler.assign_permissions_to_role(PydanticObjectId(role_id), permission_ids) - async def query_roles_by_id(self, role_id: str) -> RoleDoc: - """Query a single role by ID""" - return await self.role_handler.query_roles_by_id(PydanticObjectId(role_id)) - async def delete_role(self, role_id: str) -> None: """Delete a role document after checking if it is referenced by any user""" return await self.role_handler.delete_role(PydanticObjectId(role_id)) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/permission/__init__.py b/apps/authentication/webapi/routes/permission/__init__.py index cf1aafb..49fb7b2 100644 --- a/apps/authentication/webapi/routes/permission/__init__.py +++ b/apps/authentication/webapi/routes/permission/__init__.py @@ -1,13 +1,18 @@ from fastapi import APIRouter +from .create_or_update_permission import router as cup_router from .create_permission import router as cp_router from .query_permission import router as qp_router from .update_permission import router as up_router from .delete_permission import router as delp_router +from .query_permission_no_pagination import router as qpno_router + router = APIRouter() +router.include_router(cup_router, prefix="/permission", tags=["permission"]) router.include_router(cp_router, prefix="/permission", tags=["permission"]) router.include_router(qp_router, prefix="/permission", tags=["permission"]) router.include_router(up_router, prefix="/permission", tags=["permission"]) -router.include_router(delp_router, prefix="/permission", tags=["permission"]) \ No newline at end of file +router.include_router(delp_router, prefix="/permission", tags=["permission"]) +router.include_router(qpno_router, prefix="/permission", tags=["permission"]) \ No newline at end of file diff --git a/apps/authentication/webapi/routes/role/__init__.py b/apps/authentication/webapi/routes/role/__init__.py index f82d8b7..c71537c 100644 --- a/apps/authentication/webapi/routes/role/__init__.py +++ b/apps/authentication/webapi/routes/role/__init__.py @@ -1,16 +1,18 @@ from fastapi import APIRouter +from .create_or_update_role import router as create_or_update_role_router from .create_role import router as create_role_router from .update_role import router as update_role_router from .query_role import router as query_role_router -from .query_role_by_id import router as query_role_by_id_router +from .query_role_no_pagination import router as query_role_no_pagination_router from .assign_permissions import router as assign_permissions_router from .delete_role import router as delete_role_router router = APIRouter() +router.include_router(create_or_update_role_router, prefix="/role", tags=["role"]) router.include_router(create_role_router, prefix="/role", tags=["role"]) router.include_router(update_role_router, prefix="/role", tags=["role"]) router.include_router(query_role_router, prefix="/role", tags=["role"]) -router.include_router(query_role_by_id_router, prefix="/role", tags=["role"]) +router.include_router(query_role_no_pagination_router, prefix="/role", tags=["role"]) router.include_router(assign_permissions_router, prefix="/role", tags=["role"]) router.include_router(delete_role_router, prefix="/role", tags=["role"]) \ No newline at end of file From b244fd5bc89d432ae7956888b09473d9de8e815f Mon Sep 17 00:00:00 2001 From: YuehuCao Date: Fri, 26 Sep 2025 17:19:30 +0800 Subject: [PATCH 10/10] chore: delete used interface --- .../webapi/routes/role/query_role_by_id.py | 51 ------------------- 1 file changed, 51 deletions(-) delete mode 100644 apps/authentication/webapi/routes/role/query_role_by_id.py diff --git a/apps/authentication/webapi/routes/role/query_role_by_id.py b/apps/authentication/webapi/routes/role/query_role_by_id.py deleted file mode 100644 index 87fdafc..0000000 --- a/apps/authentication/webapi/routes/role/query_role_by_id.py +++ /dev/null @@ -1,51 +0,0 @@ -from datetime import datetime - -from fastapi import APIRouter, HTTPException -from pydantic import BaseModel -from typing import Optional, List - -from backend.services.permission.role_service import RoleService -from common.token.token_manager import TokenManager - -router = APIRouter() -token_manager = TokenManager() -role_service = RoleService() - -class QueryRoleByIdRequest(BaseModel): - role_id: str - -class QueryRoleByIdResponse(BaseModel): - id: str - role_key: str - role_name: str - role_description: Optional[str] = None - permission_ids: List[str] - role_level: int - created_at: datetime - updated_at: datetime - - -@router.post( - "/query_by_id", - response_model=QueryRoleByIdResponse, - operation_id="query-role-by-id", - summary="Query Role by Id", - description="Query role by id." -) -async def query_roles_by_id( - req: QueryRoleByIdRequest, -) -> QueryRoleByIdResponse: - try: - result = await role_service.query_roles_by_id(req.role_id) - return QueryRoleByIdResponse( - id=str(result.id), - role_key=result.role_key, - role_name=result.role_name, - role_description=result.role_description, - permission_ids=result.permission_ids, - role_level=result.role_level, - created_at=result.created_at, - updated_at=result.updated_at - ) - except Exception as e: - raise HTTPException(status_code=400, detail=str(e)) \ No newline at end of file