From 82b391d777eab6cabee36c45d9fe552a192ca55e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:48:38 +0000 Subject: [PATCH 1/5] Initial plan From f20cad1848b6c3d7d769baa3069995807ce9109b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 12:59:42 +0000 Subject: [PATCH 2/5] Filter deleted resources in get_by_id methods - Updated get_workspace_by_id to exclude deleted workspaces - Updated get_workspace_service_by_id to exclude deleted services - Updated get_user_resource_by_id to exclude deleted user resources - Updated get_shared_service_by_id to exclude deleted shared services - Added/updated unit tests to verify deleted resources are filtered - Updated CHANGELOG.md with bug fix entry Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com> --- CHANGELOG.md | 1 + api_app/db/repositories/shared_services.py | 2 ++ api_app/db/repositories/user_resources.py | 3 ++- api_app/db/repositories/workspace_services.py | 3 ++- api_app/db/repositories/workspaces.py | 3 ++- .../test_shared_service_repository.py | 8 ++++++++ .../test_user_resource_repository.py | 11 +++++++++-- .../test_workpaces_repository.py | 17 +++++++++++++++-- .../test_workpaces_service_repository.py | 13 +++++++++++-- 9 files changed, 52 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b9e4d2c95..56cfca22bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ENHANCEMENTS: * Make workspace shared storage quota updateable ([#4314](https://github.com/microsoft/AzureTRE/issues/4314)) 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)) 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..ab141ce61d 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 @@ -6,6 +6,7 @@ from db.errors import DuplicateEntity, EntityDoesNotExist from db.repositories.shared_services import SharedServiceRepository from db.repositories.operations import OperationRepository +from models.domain.operation import Status from models.domain.shared_service import SharedService from models.domain.resource import ResourceType from models.schemas.shared_service import SharedServiceInCreate @@ -60,6 +61,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 a72b2d85e1..1b4a7d393e 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) From 52a086550ff5f70d85f2e4e5c09429a1aeae8893 Mon Sep 17 00:00:00 2001 From: Marcus Robinson Date: Thu, 18 Dec 2025 13:03:08 +0000 Subject: [PATCH 3/5] Potential fix for pull request finding 'Unused import' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> --- .../test_db/test_repositories/test_shared_service_repository.py | 1 - 1 file changed, 1 deletion(-) 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 ab141ce61d..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 @@ -6,7 +6,6 @@ from db.errors import DuplicateEntity, EntityDoesNotExist from db.repositories.shared_services import SharedServiceRepository from db.repositories.operations import OperationRepository -from models.domain.operation import Status from models.domain.shared_service import SharedService from models.domain.resource import ResourceType from models.schemas.shared_service import SharedServiceInCreate From 2e4d7e83c7cae1c17e17cc8a00b533b113e1efb9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 18 Dec 2025 13:07:24 +0000 Subject: [PATCH 4/5] Update API version to 0.25.7 Incremented API version from 0.25.6 to 0.25.7 for bug fix release. Added version update entry to CHANGELOG.md COMPONENTS section. Co-authored-by: marrobi <17089773+marrobi@users.noreply.github.com> --- CHANGELOG.md | 1 + api_app/_version.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 56cfca22bb..6376c79686 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ BUG FIXES: * Fix missing metastoreDomains for Databricks, which caused metastore outages for some domains ([#4779](https://github.com/microsoft/AzureTRE/issues/4779)) 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 84f5d85159..5461be1190 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.25.6" +__version__ = "0.25.7" From 7faea01aa1cba729d04f4abc42228b240972156a Mon Sep 17 00:00:00 2001 From: James Chapman Date: Fri, 9 Jan 2026 10:25:28 +0000 Subject: [PATCH 5/5] bump api version --- api_app/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"