diff --git a/gns3server/api/routes/controller/permissions.py b/gns3server/api/routes/controller/permissions.py index 9a40d554..8667903c 100644 --- a/gns3server/api/routes/controller/permissions.py +++ b/gns3server/api/routes/controller/permissions.py @@ -65,9 +65,10 @@ async def create_permission( Create a new permission. """ - if await rbac_repo.check_permission_exists(permission_create): - raise ControllerBadRequestError(f"Permission '{permission_create.methods} {permission_create.path} " - f"{permission_create.action}' already exists") + # TODO: should we prevent having multiple permissions with same methods/path? + #if await rbac_repo.check_permission_exists(permission_create): + # raise ControllerBadRequestError(f"Permission '{permission_create.methods} {permission_create.path} " + # f"{permission_create.action}' already exists") for route in request.app.routes: if isinstance(route, APIRoute): @@ -142,3 +143,15 @@ async def delete_permission( raise ControllerNotFoundError(f"Permission '{permission_id}' could not be deleted") return Response(status_code=status.HTTP_204_NO_CONTENT) + + +@router.post("/prune", status_code=status.HTTP_204_NO_CONTENT) +async def prune_permissions( + rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)) +) -> Response: + """ + Prune orphaned permissions. + """ + + await rbac_repo.prune_permissions() + return Response(status_code=status.HTTP_204_NO_CONTENT) diff --git a/gns3server/db/repositories/rbac.py b/gns3server/db/repositories/rbac.py index 6e1096c9..02fd652c 100644 --- a/gns3server/db/repositories/rbac.py +++ b/gns3server/db/repositories/rbac.py @@ -17,7 +17,7 @@ from uuid import UUID from typing import Optional, List, Union -from sqlalchemy import select, update, delete +from sqlalchemy import select, update, delete, null from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload @@ -194,7 +194,8 @@ class RbacRepository(BaseRepository): Get all permissions. """ - query = select(models.Permission) + query = select(models.Permission).\ + order_by(models.Permission.path.desc()) result = await self._db_session.execute(query) return result.scalars().all() @@ -257,6 +258,22 @@ class RbacRepository(BaseRepository): await self._db_session.commit() return result.rowcount > 0 + async def prune_permissions(self) -> int: + """ + Prune orphaned permissions. + """ + + query = select(models.Permission).\ + filter((~models.Permission.roles.any()) & (models.Permission.user_id == null())) + result = await self._db_session.execute(query) + permissions = result.scalars().all() + permissions_deleted = 0 + for permission in permissions: + if await self.delete_permission(permission.permission_id): + permissions_deleted += 1 + log.info(f"{permissions_deleted} orphaned permissions have been deleted") + return permissions_deleted + def _match_permission( self, permissions: List[models.Permission], @@ -282,9 +299,9 @@ class RbacRepository(BaseRepository): """ query = select(models.Permission).\ - join(models.User.permissions). \ + join(models.User.permissions).\ filter(models.User.user_id == user_id).\ - order_by(models.Permission.path) + order_by(models.Permission.path.desc()) result = await self._db_session.execute(query) return result.scalars().all() @@ -379,11 +396,11 @@ class RbacRepository(BaseRepository): """ query = select(models.Permission).\ - join(models.Permission.roles). \ - join(models.Role.groups). \ - join(models.UserGroup.users). \ + join(models.Permission.roles).\ + join(models.Role.groups).\ + join(models.UserGroup.users).\ filter(models.User.user_id == user_id).\ - order_by(models.Permission.path) + order_by(models.Permission.path.desc()) result = await self._db_session.execute(query) permissions = result.scalars().all() diff --git a/tests/api/routes/controller/test_permissions.py b/tests/api/routes/controller/test_permissions.py index 2ce3dbb6..60744b9f 100644 --- a/tests/api/routes/controller/test_permissions.py +++ b/tests/api/routes/controller/test_permissions.py @@ -32,7 +32,7 @@ class TestPermissionRoutes: new_permission = { "methods": ["GET"], - "path": "/templates", + "path": "/templates/f6113095-a703-4967-b039-ab95ac3eb4f5", "action": "ALLOW" } response = await client.post(app.url_path_for("create_permission"), json=new_permission) @@ -75,7 +75,7 @@ class TestPermissionRoutes: async def test_update_permission(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: rbac_repo = RbacRepository(db_session) - permission_in_db = await rbac_repo.get_permission_by_path("/templates") + permission_in_db = await rbac_repo.get_permission_by_path("/templates/*") update_permission = { "methods": ["GET"], @@ -101,3 +101,12 @@ class TestPermissionRoutes: permission_in_db = await rbac_repo.get_permission_by_path("/appliances") response = await client.delete(app.url_path_for("delete_permission", permission_id=permission_in_db.permission_id)) assert response.status_code == status.HTTP_204_NO_CONTENT + + async def test_prune_permissions(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None: + + response = await client.post(app.url_path_for("prune_permissions")) + assert response.status_code == status.HTTP_204_NO_CONTENT + + rbac_repo = RbacRepository(db_session) + permissions_in_db = await rbac_repo.get_permissions() + assert len(permissions_in_db) == 5 # 5 default permissions