Skip to content

Commit

Permalink
Create and delete Airlock Review VMs (#2740)
Browse files Browse the repository at this point in the history
  • Loading branch information
tanya-borisova authored Oct 17, 2022
1 parent d08fec3 commit 0d64b67
Show file tree
Hide file tree
Showing 16 changed files with 452 additions and 91 deletions.
2 changes: 1 addition & 1 deletion api_app/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.53"
__version__ = "0.4.54"
164 changes: 147 additions & 17 deletions api_app/api/routes/airlock.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import logging

from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, Depends, HTTPException, status, Response

from jsonschema.exceptions import ValidationError

from db.repositories.user_resources import UserResourceRepository
from db.repositories.workspace_services import WorkspaceServiceRepository
from db.repositories.operations import OperationRepository
from db.repositories.resource_templates import ResourceTemplateRepository
from db.repositories.airlock_requests import AirlockRequestRepository
from db.errors import EntityDoesNotExist, UserNotAuthorizedToUseTemplate

from api.dependencies.database import get_repository
from api.dependencies.workspaces import get_workspace_by_id_from_path, get_deployed_workspace_by_id_from_path
from api.dependencies.airlock import get_airlock_request_by_id_from_path
from models.domain.airlock_request import AirlockRequestStatus, AirlockRequestType, AirlockReviewDecision

from models.domain.airlock_request import AirlockRequest, AirlockRequestStatus, AirlockRequestType, AirlockReviewDecision, AirlockReviewUserResource
from models.schemas.operation import OperationInResponse
from models.schemas.user_resource import UserResourceInCreate
from models.schemas.airlock_request_url import AirlockRequestTokenInResponse

from db.repositories.airlock_requests import AirlockRequestRepository
from models.schemas.airlock_request import AirlockRequestInCreate, AirlockRequestInResponse, AirlockRequestWithAllowedUserActionsInList, AirlockReviewInCreate
from resources import strings
from services.authentication import get_current_workspace_owner_or_researcher_user_or_airlock_manager, get_current_workspace_owner_or_researcher_user, get_current_airlock_manager_user

from .airlock_resource_helpers import save_and_publish_event_airlock_request, update_and_publish_event_airlock_request, enrich_requests_with_allowed_actions, get_airlock_requests_by_user_and_workspace
from .airlock_resource_helpers import save_and_publish_event_airlock_request, update_and_publish_event_airlock_request, enrich_requests_with_allowed_actions, \
get_airlock_requests_by_user_and_workspace, delete_review_user_resources
from .resource_helpers import save_and_deploy_resource, construct_location_header

from services.airlock import validate_user_allowed_to_access_storage_account, \
get_account_by_request, get_airlock_request_container_sas_token, validate_request_status
Expand Down Expand Up @@ -67,18 +75,123 @@ async def retrieve_airlock_request_by_id(airlock_request=Depends(get_airlock_req

@airlock_workspace_router.post("/workspaces/{workspace_id}/requests/{airlock_request_id}/submit", status_code=status.HTTP_200_OK, response_model=AirlockRequestInResponse, name=strings.API_SUBMIT_AIRLOCK_REQUEST, dependencies=[Depends(get_current_workspace_owner_or_researcher_user), Depends(get_workspace_by_id_from_path)])
async def create_submit_request(airlock_request=Depends(get_airlock_request_by_id_from_path), user=Depends(get_current_workspace_owner_or_researcher_user), airlock_request_repo=Depends(get_repository(AirlockRequestRepository)), workspace=Depends(get_workspace_by_id_from_path)) -> AirlockRequestInResponse:
updated_resource = await update_and_publish_event_airlock_request(airlock_request, airlock_request_repo, user, AirlockRequestStatus.Submitted, workspace)
updated_resource = await update_and_publish_event_airlock_request(airlock_request, airlock_request_repo, user, workspace, new_status=AirlockRequestStatus.Submitted)
return AirlockRequestInResponse(airlockRequest=updated_resource)


@airlock_workspace_router.post("/workspaces/{workspace_id}/requests/{airlock_request_id}/cancel", status_code=status.HTTP_200_OK, response_model=AirlockRequestInResponse, name=strings.API_CANCEL_AIRLOCK_REQUEST, dependencies=[Depends(get_current_workspace_owner_or_researcher_user), Depends(get_workspace_by_id_from_path)])
async def create_cancel_request(airlock_request=Depends(get_airlock_request_by_id_from_path), user=Depends(get_current_workspace_owner_or_researcher_user), airlock_request_repo=Depends(get_repository(AirlockRequestRepository)), workspace=Depends(get_workspace_by_id_from_path)) -> AirlockRequestInResponse:
updated_resource = await update_and_publish_event_airlock_request(airlock_request, airlock_request_repo, user, AirlockRequestStatus.Cancelled, workspace)
updated_resource = await update_and_publish_event_airlock_request(airlock_request, airlock_request_repo, user, workspace, new_status=AirlockRequestStatus.Cancelled)
return AirlockRequestInResponse(airlockRequest=updated_resource)


@airlock_workspace_router.post("/workspaces/{workspace_id}/requests/{airlock_request_id}/review-user-resource", status_code=status.HTTP_202_ACCEPTED, response_model=OperationInResponse, name=strings.API_CREATE_AIRLOCK_REVIEW_USER_RESOURCE, dependencies=[Depends(get_current_airlock_manager_user), Depends(get_workspace_by_id_from_path)])
async def create_review_user_resource(
response: Response,
airlock_request=Depends(get_airlock_request_by_id_from_path),
user=Depends(get_current_airlock_manager_user),
workspace=Depends(get_deployed_workspace_by_id_from_path),
user_resource_repo=Depends(get_repository(UserResourceRepository)),
workspace_service_repo=Depends(get_repository(WorkspaceServiceRepository)),
operation_repo=Depends(get_repository(OperationRepository)),
airlock_request_repo=Depends(get_repository(AirlockRequestRepository)),
resource_template_repo=Depends(get_repository(ResourceTemplateRepository))) -> OperationInResponse:

if airlock_request.status != AirlockRequestStatus.InReview:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST,
detail="Airlock request must be in 'in_review' status to create a Review User Resource")

try:
# Getting the review configuration from the airlock request's workspace properties
if airlock_request.requestType == AirlockRequestType.Import:
config = workspace.properties["airlock_review_config"]["import"]
workspace_id = config["workspace_id"]
else:
assert airlock_request.requestType == AirlockRequestType.Export
config = workspace.properties["airlock_review_config"]["export"]
workspace_id = workspace.id
workspace_service_id = config["workspace_service_id"]
user_resource_template_name = config["user_resource_template_name"]

logging.info(f"Going to create a user resource in {workspace_id} {workspace_service_id} {user_resource_template_name}")
except (KeyError, TypeError) as e:
logging.error(f"Failed to parse configuration: {e}")
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Failed to retrieve Airlock Review configuration for workspace {workspace.id}.\
Please ask your TRE administrator to check the configuration. Details: {str(e)}")

# Find workspace service to create user resource in
try:
workspace_service = workspace_service_repo.get_workspace_service_by_id(workspace_id=workspace_id, service_id=workspace_service_id)
except EntityDoesNotExist as e:
logging.error(f"Failed to get workspace service {workspace_service_id} for workspace {workspace_id}: {str(e)}")
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Failed to retrieve Airlock Review configuration for workspace {workspace.id}.\
Please ask your TRE administrator to check the configuration. Details: {str(e)}")

# Getting the SAS URL (this function raises HTTPException in case of error)
airlock_request_sas_url = get_airlock_container_link(airlock_request, user, workspace)

# Now have all components for user resource, create an object for it
user_resource_create = UserResourceInCreate(
templateName=user_resource_template_name,
properties={
"display_name": "Airlock Review VM",
"description": f"Airlock Review VM for request {airlock_request.requestTitle} (ID {airlock_request.id})",
"airlock_request_sas_url": airlock_request_sas_url
}
)

# Start VM creation
try:
user_resource, resource_template = user_resource_repo.create_user_resource_item(
user_resource_create, workspace_id, workspace_service_id, workspace_service.templateName, user.id, user.roles)
except (ValidationError, ValueError) as e:
logging.error(f"Failed create user resource model instance due to validation error: {e}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Invalid configuration for creating user resource. Please contact your TRE administrator. \
Details: {str(e)}")
except UserNotAuthorizedToUseTemplate as e:
logging.error(f"User not authorized to use template: {e}")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))

operation = await save_and_deploy_resource(
resource=user_resource,
resource_repo=user_resource_repo,
operations_repo=operation_repo,
resource_template_repo=resource_template_repo,
user=user,
resource_template=resource_template)

# Update the Airlock Request with the information on the VM
updated_resource = await update_and_publish_event_airlock_request(
airlock_request,
airlock_request_repo,
user,
workspace,
review_user_resource=AirlockReviewUserResource(
workspaceId=workspace_id,
workspaceServiceId=workspace_service_id,
userResourceId=user_resource.id
))
logging.info(f"Airlock Request {updated_resource.id} updated to include {updated_resource.reviewUserResources}")

response.headers["Location"] = construct_location_header(operation)
return OperationInResponse(operation=operation)


@airlock_workspace_router.post("/workspaces/{workspace_id}/requests/{airlock_request_id}/review", status_code=status.HTTP_200_OK, response_model=AirlockRequestInResponse, name=strings.API_REVIEW_AIRLOCK_REQUEST, dependencies=[Depends(get_current_airlock_manager_user), Depends(get_workspace_by_id_from_path)])
async def create_airlock_review(airlock_review_input: AirlockReviewInCreate, airlock_request=Depends(get_airlock_request_by_id_from_path), user=Depends(get_current_airlock_manager_user), airlock_request_repo=Depends(get_repository(AirlockRequestRepository)), workspace=Depends(get_deployed_workspace_by_id_from_path)) -> AirlockRequestInResponse:
async def create_airlock_review(
airlock_review_input: AirlockReviewInCreate,
airlock_request=Depends(get_airlock_request_by_id_from_path),
user=Depends(get_current_airlock_manager_user),
workspace=Depends(get_deployed_workspace_by_id_from_path),
airlock_request_repo=Depends(get_repository(AirlockRequestRepository)),
user_resource_repo=Depends(get_repository(UserResourceRepository)),
workspace_service_repo=Depends(get_repository(WorkspaceServiceRepository)),
operation_repo=Depends(get_repository(OperationRepository)),
resource_template_repo=Depends(get_repository(ResourceTemplateRepository))) -> AirlockRequestInResponse:

try:
airlock_review = airlock_request_repo.create_airlock_review_item(airlock_review_input, user)
except (ValidationError, ValueError) as e:
Expand All @@ -90,19 +203,36 @@ async def create_airlock_review(airlock_review_input: AirlockReviewInCreate, air
elif airlock_review.reviewDecision.value == AirlockReviewDecision.Rejected:
review_status = AirlockRequestStatus.RejectionInProgress

updated_airlock_request = await update_and_publish_event_airlock_request(airlock_request=airlock_request, airlock_request_repo=airlock_request_repo, user=user, new_status=review_status, workspace=workspace, airlock_review=airlock_review)
updated_airlock_request = await update_and_publish_event_airlock_request(airlock_request=airlock_request, airlock_request_repo=airlock_request_repo, user=user, workspace=workspace, new_status=review_status, airlock_review=airlock_review)

# If there was a VM created for the request, clean it up as it will no longer be needed
# In this request, we aren't returning the operations for clean up of VMs,
# however the operations still will be saved in the DB and displayed on the UI as normal.
_ = await delete_review_user_resources(
airlock_request=airlock_request,
user_resource_repo=user_resource_repo,
workspace_service_repo=workspace_service_repo,
resource_template_repo=resource_template_repo,
operations_repo=operation_repo,
user=user
)

return AirlockRequestInResponse(airlockRequest=updated_airlock_request)


def get_airlock_container_link(airlock_request: AirlockRequest, user, workspace):
validate_user_allowed_to_access_storage_account(user, airlock_request)
validate_request_status(airlock_request)
account_name: str = get_account_by_request(airlock_request, workspace)
return get_airlock_request_container_sas_token(account_name, airlock_request)


@airlock_workspace_router.get("/workspaces/{workspace_id}/requests/{airlock_request_id}/link",
status_code=status.HTTP_200_OK, response_model=AirlockRequestTokenInResponse,
name=strings.API_AIRLOCK_REQUEST_LINK,
dependencies=[Depends(get_current_workspace_owner_or_researcher_user_or_airlock_manager)])
async def get_airlock_container_link(workspace=Depends(get_deployed_workspace_by_id_from_path),
airlock_request=Depends(get_airlock_request_by_id_from_path),
user=Depends(get_current_workspace_owner_or_researcher_user_or_airlock_manager)) -> AirlockRequestTokenInResponse:
validate_user_allowed_to_access_storage_account(user, airlock_request)
validate_request_status(airlock_request)
account_name: str = get_account_by_request(airlock_request, workspace)
container_url = get_airlock_request_container_sas_token(account_name, airlock_request)
async def get_airlock_container_link_method(workspace=Depends(get_deployed_workspace_by_id_from_path),
airlock_request=Depends(get_airlock_request_by_id_from_path),
user=Depends(get_current_workspace_owner_or_researcher_user_or_airlock_manager)) -> AirlockRequestTokenInResponse:
container_url = get_airlock_container_link(airlock_request, user, workspace)
return AirlockRequestTokenInResponse(containerUrl=container_url)
Loading

0 comments on commit 0d64b67

Please sign in to comment.