diff --git a/CHANGELOG.md b/CHANGELOG.md index d52d77954a..8db1fb4d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ENHANCEMENTS: * Update Porter, AzureCLI, Terraform and its providers across the solution ([#4799](https://github.com/microsoft/AzureTRE/issues/4799)) BUG FIXES: +* Fix deleted workspaces still accessible via URL - get_*_by_id methods now filter out deleted resources ([#4785](https://github.com/microsoft/AzureTRE/issues/4785)) * Fix circular dependancy in base workspace. ([#4756](https://github.com/microsoft/AzureTRE/pull/4756)) * Replaced deprecated `datetime.utcnow()` with `datetime.now(datetime.UTC)` in the API and airlock processor. ([#4743](https://github.com/microsoft/AzureTRE/issues/4743)) * Updated error messages when publishing a template version that is lower than the existing version. ([#4685](https://github.com/microsoft/AzureTRE/issues/4685)) @@ -25,6 +26,7 @@ BUG FIXES: * Fix cost display duplication when user resource is deleted - UI incorrectly reused cost data for remaining resources ([#4783](https://github.com/microsoft/AzureTRE/issues/4783)) COMPONENTS: +* API version updated to 0.25.7 ([#4785](https://github.com/microsoft/AzureTRE/issues/4785)) ## 0.26.0 (October 12, 2025) **BREAKING CHANGES** diff --git a/api_app/_version.py b/api_app/_version.py index 5461be1190..4f3cae6929 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.25.7" +__version__ = "0.25.8" diff --git a/api_app/db/repositories/shared_services.py b/api_app/db/repositories/shared_services.py index ceaa48d567..4d877fb53e 100644 --- a/api_app/db/repositories/shared_services.py +++ b/api_app/db/repositories/shared_services.py @@ -55,6 +55,8 @@ def active_shared_service_with_template_name_query(template_name: str): async def get_shared_service_by_id(self, shared_service_id: str): query, parameters = self.shared_service_query(str(shared_service_id)) + query += ' AND c.deploymentStatus != @deletedStatus' + parameters.append({'name': '@deletedStatus', 'value': Status.Deleted}) shared_services = await self.query(query=query, parameters=parameters) if not shared_services: raise EntityDoesNotExist diff --git a/api_app/db/repositories/user_resources.py b/api_app/db/repositories/user_resources.py index 3528d28d03..0ba896281a 100644 --- a/api_app/db/repositories/user_resources.py +++ b/api_app/db/repositories/user_resources.py @@ -77,8 +77,9 @@ async def get_user_resources_for_workspace_service(self, workspace_id: str, serv async def get_user_resource_by_id(self, workspace_id: str, service_id: str, resource_id: str) -> UserResource: query, parameters = self.user_resources_query(str(workspace_id), str(service_id)) - query += ' AND c.id = @resourceId' + query += ' AND c.id = @resourceId AND c.deploymentStatus != @deletedStatus' parameters.append({'name': '@resourceId', 'value': str(resource_id)}) + parameters.append({'name': '@deletedStatus', 'value': Status.Deleted}) user_resources = await self.query(query=query, parameters=parameters) if not user_resources: raise EntityDoesNotExist diff --git a/api_app/db/repositories/workspace_services.py b/api_app/db/repositories/workspace_services.py index ef21bfe6eb..0b2def4cdd 100644 --- a/api_app/db/repositories/workspace_services.py +++ b/api_app/db/repositories/workspace_services.py @@ -62,8 +62,9 @@ async def get_deployed_workspace_service_by_id(self, workspace_id: str, service_ async def get_workspace_service_by_id(self, workspace_id: str, service_id: str) -> WorkspaceService: query, parameters = self.workspace_services_query(str(workspace_id)) - query += ' AND c.id = @serviceId' + query += ' AND c.id = @serviceId AND c.deploymentStatus != @deletedStatus' parameters.append({'name': '@serviceId', 'value': str(service_id)}) + parameters.append({'name': '@deletedStatus', 'value': Status.Deleted}) workspace_services = await self.query(query=query, parameters=parameters) if not workspace_services: raise EntityDoesNotExist diff --git a/api_app/db/repositories/workspaces.py b/api_app/db/repositories/workspaces.py index 5be9cab979..013c1d54ae 100644 --- a/api_app/db/repositories/workspaces.py +++ b/api_app/db/repositories/workspaces.py @@ -71,8 +71,9 @@ async def get_deployed_workspace_by_id(self, workspace_id: str, operations_repo: async def get_workspace_by_id(self, workspace_id: str) -> Workspace: query, parameters = self.workspaces_query_string() - query += ' AND c.id = @workspaceId' + query += ' AND c.id = @workspaceId AND c.deploymentStatus != @deletedStatus' parameters.append({'name': '@workspaceId', 'value': str(workspace_id)}) + parameters.append({'name': '@deletedStatus', 'value': Status.Deleted}) workspaces = await self.query(query=query, parameters=parameters) if not workspaces: raise EntityDoesNotExist diff --git a/api_app/tests_ma/test_db/test_repositories/test_shared_service_repository.py b/api_app/tests_ma/test_db/test_repositories/test_shared_service_repository.py index 39393b6ac8..004048ed60 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_shared_service_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_shared_service_repository.py @@ -60,6 +60,13 @@ async def test_get_shared_service_by_id_raises_if_does_not_exist(shared_service_ await shared_service_repo.get_shared_service_by_id(SHARED_SERVICE_ID) +async def test_get_shared_service_by_id_raises_if_deleted(shared_service_repo): + shared_service_repo.query = AsyncMock(return_value=[]) + + with pytest.raises(EntityDoesNotExist): + await shared_service_repo.get_shared_service_by_id(SHARED_SERVICE_ID) + + async def test_get_active_shared_services_for_shared_queries_db(shared_service_repo): shared_service_repo.query = AsyncMock(return_value=[]) query, parameters = SharedServiceRepository.active_shared_services_query() diff --git a/api_app/tests_ma/test_db/test_repositories/test_user_resource_repository.py b/api_app/tests_ma/test_db/test_repositories/test_user_resource_repository.py index e271e6dac6..6ff58f33fe 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_user_resource_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_user_resource_repository.py @@ -95,12 +95,13 @@ async def test_get_user_resource_returns_resource_if_found(query_mock, user_reso @patch('db.repositories.user_resources.UserResourceRepository.query') async def test_get_user_resource_by_id_queries_db(query_mock, user_resource_repo, user_resource): query_mock.return_value = [user_resource.dict()] - expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.parentWorkspaceServiceId = @serviceId AND c.workspaceId = @workspaceId AND c.id = @resourceId' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.parentWorkspaceServiceId = @serviceId AND c.workspaceId = @workspaceId AND c.id = @resourceId AND c.deploymentStatus != @deletedStatus' expected_parameters = [ {'name': '@resourceType', 'value': ResourceType.UserResource}, {'name': '@serviceId', 'value': SERVICE_ID}, {'name': '@workspaceId', 'value': WORKSPACE_ID}, - {'name': '@resourceId', 'value': RESOURCE_ID} + {'name': '@resourceId', 'value': RESOURCE_ID}, + {'name': '@deletedStatus', 'value': Status.Deleted} ] await user_resource_repo.get_user_resource_by_id(WORKSPACE_ID, SERVICE_ID, RESOURCE_ID) @@ -112,3 +113,9 @@ async def test_get_user_resource_by_id_queries_db(query_mock, user_resource_repo async def test_get_user_resource_by_id_raises_entity_does_not_exist_if_not_found(_, user_resource_repo): with pytest.raises(EntityDoesNotExist): await user_resource_repo.get_user_resource_by_id(WORKSPACE_ID, SERVICE_ID, RESOURCE_ID) + + +@patch('db.repositories.user_resources.UserResourceRepository.query', return_value=[]) +async def test_get_user_resource_by_id_raises_entity_does_not_exist_if_resource_is_deleted(_, user_resource_repo): + with pytest.raises(EntityDoesNotExist): + await user_resource_repo.get_user_resource_by_id(WORKSPACE_ID, SERVICE_ID, RESOURCE_ID) diff --git a/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py b/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py index 9abbc96727..e71c98cd12 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py @@ -8,6 +8,7 @@ from db.errors import EntityDoesNotExist, InvalidInput, ResourceIsNotDeployed from db.repositories.operations import OperationRepository from db.repositories.workspaces import WorkspaceRepository +from models.domain.operation import Status from models.domain.resource import ResourceType from models.domain.workspace import Workspace from models.schemas.workspace import WorkspaceInCreate @@ -84,15 +85,27 @@ async def test_get_workspace_by_id_raises_entity_does_not_exist_if_item_does_not await workspace_repo.get_workspace_by_id(workspace_id) +@pytest.mark.asyncio +async def test_get_workspace_by_id_raises_entity_does_not_exist_if_workspace_is_deleted(workspace_repo, workspace): + workspace_id = workspace.id + workspace_repo.container.query_items = MagicMock() + workspace_repo.container.query_items.return_value = AsyncMock() + workspace_repo.container.query_items.return_value.__aiter__.return_value = [] + + with pytest.raises(EntityDoesNotExist): + await workspace_repo.get_workspace_by_id(workspace_id) + + @pytest.mark.asyncio async def test_get_workspace_by_id_queries_db(workspace_repo, workspace): workspace_query_item_result = AsyncMock() workspace_query_item_result.__aiter__.return_value = [workspace.dict()] workspace_repo.container.query_items = MagicMock(return_value=workspace_query_item_result) - expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.id = @workspaceId' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.id = @workspaceId AND c.deploymentStatus != @deletedStatus' expected_parameters = [ {'name': '@resourceType', 'value': ResourceType.Workspace}, - {'name': '@workspaceId', 'value': workspace.id} + {'name': '@workspaceId', 'value': workspace.id}, + {'name': '@deletedStatus', 'value': Status.Deleted} ] await workspace_repo.get_workspace_by_id(workspace.id) diff --git a/api_app/tests_ma/test_db/test_repositories/test_workpaces_service_repository.py b/api_app/tests_ma/test_db/test_repositories/test_workpaces_service_repository.py index b3f2c808fb..d5b2e2692c 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_workpaces_service_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_workpaces_service_repository.py @@ -6,6 +6,7 @@ from db.errors import EntityDoesNotExist, ResourceIsNotDeployed from db.repositories.workspace_services import WorkspaceServiceRepository from db.repositories.operations import OperationRepository +from models.domain.operation import Status from models.domain.resource import ResourceType from models.domain.workspace_service import WorkspaceService from models.schemas.workspace_service import WorkspaceServiceInCreate @@ -85,13 +86,21 @@ async def test_get_workspace_service_by_id_raises_entity_does_not_exist_if_no_av await workspace_service_repo.get_workspace_service_by_id(WORKSPACE_ID, SERVICE_ID) +async def test_get_workspace_service_by_id_raises_entity_does_not_exist_if_service_is_deleted(workspace_service_repo): + workspace_service_repo.query = AsyncMock(return_value=[]) + + with pytest.raises(EntityDoesNotExist): + await workspace_service_repo.get_workspace_service_by_id(WORKSPACE_ID, SERVICE_ID) + + async def test_get_workspace_service_by_id_queries_db(workspace_service_repo, workspace_service): workspace_service_repo.query = AsyncMock(return_value=[workspace_service]) - expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.workspaceId = @workspaceId AND c.id = @serviceId' + expected_query = 'SELECT * FROM c WHERE c.resourceType = @resourceType AND c.workspaceId = @workspaceId AND c.id = @serviceId AND c.deploymentStatus != @deletedStatus' expected_parameters = [ {'name': '@resourceType', 'value': ResourceType.WorkspaceService}, {'name': '@workspaceId', 'value': WORKSPACE_ID}, - {'name': '@serviceId', 'value': SERVICE_ID} + {'name': '@serviceId', 'value': SERVICE_ID}, + {'name': '@deletedStatus', 'value': Status.Deleted} ] await workspace_service_repo.get_workspace_service_by_id(WORKSPACE_ID, SERVICE_ID)