Skip to content

Commit

Permalink
Additional fields for AirlockNotification event (#2798)
Browse files Browse the repository at this point in the history
  • Loading branch information
tanya-borisova authored Nov 1, 2022
1 parent 403804a commit d0aaad7
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 60 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.5.8"
__version__ = "0.5.9"
4 changes: 2 additions & 2 deletions api_app/api/routes/airlock_resource_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async def save_and_publish_event_airlock_request(airlock_request: AirlockRequest
try:
logging.debug(f"Sending status changed event for airlock request item: {airlock_request.id}")
await send_status_changed_event(airlock_request=airlock_request, previous_status=None)
await send_airlock_notification_event(airlock_request, role_assignment_details)
await send_airlock_notification_event(airlock_request, workspace, role_assignment_details)
except Exception as e:
airlock_request_repo.delete_item(airlock_request.id)
logging.error(f"Failed sending status_changed message: {e}")
Expand Down Expand Up @@ -86,7 +86,7 @@ async def update_and_publish_event_airlock_request(
await send_status_changed_event(airlock_request=updated_airlock_request, previous_status=airlock_request.status)
access_service = get_access_service()
role_assignment_details = access_service.get_workspace_role_assignment_details(workspace)
await send_airlock_notification_event(updated_airlock_request, role_assignment_details)
await send_airlock_notification_event(updated_airlock_request, workspace, role_assignment_details)
return updated_airlock_request
except Exception as e:
logging.error(f"Failed sending status_changed message: {e}")
Expand Down
42 changes: 36 additions & 6 deletions api_app/event_grid/event_sender.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import logging
import re
import json

from typing import Dict, Optional
from azure.eventgrid import EventGridEvent
from models.domain.events import StatusChangedData, AirlockNotificationData
from models.domain.events import AirlockNotificationRequestData, AirlockNotificationWorkspaceData, StatusChangedData, AirlockNotificationData
from event_grid.helpers import publish_event
from core import config
from models.domain.airlock_request import AirlockRequest, AirlockRequestStatus
from models.domain.workspace import Workspace


async def send_status_changed_event(airlock_request: AirlockRequest, previous_status: Optional[AirlockRequestStatus]):
Expand All @@ -25,17 +28,44 @@ async def send_status_changed_event(airlock_request: AirlockRequest, previous_st
await publish_event(status_changed_event, config.EVENT_GRID_STATUS_CHANGED_TOPIC_ENDPOINT)


async def send_airlock_notification_event(airlock_request: AirlockRequest, emails: Dict):
async def send_airlock_notification_event(airlock_request: AirlockRequest, workspace: Workspace, role_assignment_details: Dict[str, str]):
def to_snake_case(string: str):
return re.sub(r'(?<!^)(?=[A-Z])', '_', string).lower()

request_id = airlock_request.id
status = airlock_request.status.value
workspace_id = airlock_request.workspaceId
snake_case_emails = {re.sub(r'(?<!^)(?=[A-Z])', '_', role_name).lower(): role_id for role_name, role_id in emails.items()}
recipient_emails_by_role = {to_snake_case(role_name): role_id for role_name, role_id in role_assignment_details.items()}

data = AirlockNotificationData(
event_type="status_changed",
recipient_emails_by_role=recipient_emails_by_role,
request=AirlockNotificationRequestData(
id=request_id,
created_when=airlock_request.createdWhen,
created_by=airlock_request.createdBy,
updated_when=airlock_request.updatedWhen,
updated_by=airlock_request.updatedBy,
request_type=airlock_request.type,
files=airlock_request.files,
status=airlock_request.status.value,
business_justification=airlock_request.businessJustification),
workspace=AirlockNotificationWorkspaceData(
id=workspace.id,
display_name=workspace.properties["display_name"],
description=workspace.properties["description"]),
)

# For EventGridEvent, data should be a Dict[str, object]
# Becuase data has nested objects, they all need to be recursively converted to dict
# To do that, we use a json() method implemented for all objects in AzureTREModel, and convert it back from json
data_dict = json.loads(data.json())

airlock_notification = EventGridEvent(
event_type="airlockNotification",
data=AirlockNotificationData(request_id=request_id, event_type="status_changed", event_value=status, emails=snake_case_emails, workspace_id=workspace_id).__dict__,
data=data_dict,
subject=f"{request_id}/airlockNotification",
data_version="3.0"
data_version="4.0"
)

logging.info(f"Sending airlock notification event with request ID {request_id}, status: {status}")
await publish_event(airlock_notification, config.EVENT_GRID_AIRLOCK_NOTIFICATION_TOPIC_ENDPOINT)
34 changes: 29 additions & 5 deletions api_app/models/domain/events.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
from typing import Dict, Optional
from typing import Dict, List, Optional

from models.domain.azuretremodel import AzureTREModel
from models.domain.airlock_request import AirlockFile, AirlockRequestStatus, AirlockRequestType


class AirlockNotificationUserData(AzureTREModel):
name: str
email: str


class AirlockNotificationRequestData(AzureTREModel):
id: str
created_when: float
created_by: AirlockNotificationUserData
updated_when: float
updated_by: AirlockNotificationUserData
request_type: AirlockRequestType
files: List[AirlockFile]
status: AirlockRequestStatus
business_justification: str


class AirlockNotificationWorkspaceData(AzureTREModel):
id: str
display_name: str
description: str


class AirlockNotificationData(AzureTREModel):
request_id: str
event_type: str
event_value: str
emails: Dict
workspace_id: str
recipient_emails_by_role: Dict[str, List[str]]
request: AirlockNotificationRequestData
workspace: AirlockNotificationWorkspaceData


class StatusChangedData(AzureTREModel):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import time
from fastapi import HTTPException, status
import pytest
from mock import AsyncMock, patch, MagicMock

from models.domain.events import AirlockNotificationData, StatusChangedData
from models.domain.events import AirlockNotificationData, AirlockNotificationUserData, StatusChangedData, \
AirlockNotificationRequestData, AirlockNotificationWorkspaceData, AirlockRequestStatus, AirlockFile, AirlockRequestType
from api.routes.airlock_resource_helpers import save_and_publish_event_airlock_request, \
update_and_publish_event_airlock_request, get_airlock_requests_by_user_and_workspace, get_allowed_actions
from db.repositories.airlock_requests import AirlockRequestRepository
from models.domain.workspace import Workspace
from tests_ma.test_api.conftest import create_test_user, create_workspace_airlock_manager_user
from models.domain.airlock_request import AirlockRequest, AirlockRequestStatus, AirlockRequestType, AirlockReview, AirlockReviewDecision, AirlockActions
from models.domain.airlock_request import AirlockRequest, AirlockReview, AirlockReviewDecision, AirlockActions
from azure.eventgrid import EventGridEvent
from api.routes.airlock import create_airlock_review, create_cancel_request, create_submit_request

Expand All @@ -17,10 +19,20 @@
WORKSPACE_ID = "abc000d3-82da-4bfc-b6e9-9a7853ef753e"
AIRLOCK_REQUEST_ID = "5dbc15ae-40e1-49a5-834b-595f59d626b7"
AIRLOCK_REVIEW_ID = "96d909c5-e913-4c05-ae53-668a702ba2e5"
CURRENT_TIME = time.time()


def sample_workspace():
return Workspace(id=WORKSPACE_ID, templateName='template name', templateVersion='1.0', etag='', properties={"client_id": "12345"}, resourcePath="test")
return Workspace(
id=WORKSPACE_ID,
templateName='template name',
templateVersion='1.0',
etag='',
properties={
"client_id": "12345",
"display_name": "my research workspace",
"description": "for science!"},
resourcePath="test")


@pytest.fixture
Expand All @@ -34,9 +46,22 @@ def sample_airlock_request(status=AirlockRequestStatus.Draft):
id=AIRLOCK_REQUEST_ID,
workspaceId=WORKSPACE_ID,
type=AirlockRequestType.Import,
files=[],
files=[AirlockFile(
name="data.txt",
size=5
)],
businessJustification="some test reason",
status=status
status=status,
createdWhen=CURRENT_TIME,
createdBy=AirlockNotificationUserData(
name="John Doe",
email="john@example.com"
),
updatedWhen=CURRENT_TIME,
updatedBy=AirlockNotificationUserData(
name="Test User",
email="test@user.com"
)
)
return airlock_request

Expand All @@ -54,9 +79,36 @@ def sample_status_changed_event(new_status="draft", previous_status=None):
def sample_airlock_notification_event(status="draft"):
status_changed_event = EventGridEvent(
event_type="airlockNotification",
data=AirlockNotificationData(request_id=AIRLOCK_REQUEST_ID, event_type="status_changed", event_value=status, emails={"workspace_researcher": ["researcher@outlook.com"], "workspace_owner": ["owner@outlook.com"], "airlock_manager": ["manager@outlook.com"]}, workspace_id=WORKSPACE_ID).__dict__,
data=AirlockNotificationData(
event_type="status_changed",
recipient_emails_by_role={"workspace_researcher": ["researcher@outlook.com"], "workspace_owner": ["owner@outlook.com"], "airlock_manager": ["manager@outlook.com"]},
request=AirlockNotificationRequestData(
id=AIRLOCK_REQUEST_ID,
created_when=CURRENT_TIME,
created_by=AirlockNotificationUserData(
name="John Doe",
email="john@example.com"
),
updated_when=CURRENT_TIME,
updated_by=AirlockNotificationUserData(
name="Test User",
email="test@user.com"
),
request_type=AirlockRequestType.Import,
files=[AirlockFile(
name="data.txt",
size=5
)],
status=status,
business_justification="some test reason"
),
workspace=AirlockNotificationWorkspaceData(
id=WORKSPACE_ID,
display_name="my research workspace",
description="for science!"
)),
subject=f"{AIRLOCK_REQUEST_ID}/airlockNotification",
data_version="2.0"
data_version="4.0"
)
return status_changed_event

Expand All @@ -78,8 +130,8 @@ def get_required_roles(endpoint):

@patch("event_grid.helpers.EventGridPublisherClient", return_value=AsyncMock())
@patch("services.aad_authentication.AzureADAuthorization.get_workspace_role_assignment_details", return_value={"WorkspaceResearcher": ["researcher@outlook.com"], "WorkspaceOwner": ["owner@outlook.com"], "AirlockManager": ["manager@outlook.com"]})
async def test_save_and_publish_event_airlock_request_saves_item(_, event_grid_publisher_client_mock,
airlock_request_repo_mock):
@patch('api.routes.airlock_resource_helpers.get_timestamp', return_value=CURRENT_TIME)
async def test_save_and_publish_event_airlock_request_saves_item(_, __, event_grid_publisher_client_mock, airlock_request_repo_mock):
airlock_request_mock = sample_airlock_request()
airlock_request_repo_mock.save_item = MagicMock(return_value=None)
status_changed_event_mock = sample_status_changed_event()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import json
from fastapi import HTTPException, status
import pytest
import time

from mock import AsyncMock, patch
from models.domain.events import AirlockNotificationUserData, AirlockFile
from models.domain.airlock_request import AirlockRequest, AirlockRequestStatus, AirlockRequestType
from models.domain.workspace import Workspace
from service_bus.airlock_request_status_update import receive_step_result_message_and_update_status
Expand All @@ -12,10 +14,21 @@
WORKSPACE_ID = "abc000d3-82da-4bfc-b6e9-9a7853ef753e"
AIRLOCK_REQUEST_ID = "5dbc15ae-40e1-49a5-834b-595f59d626b7"
EVENT_ID = "0000c8e7-5c42-4fcb-a7fd-294cfc27aa76"
CURRENT_TIME = time.time()


def sample_workspace():
return Workspace(id=WORKSPACE_ID, templateName='template name', templateVersion='1.0', etag='', properties={"client_id": "12345"}, resourcePath="test")
return Workspace(
id=WORKSPACE_ID,
templateName='template name',
templateVersion='1.0',
etag='',
properties={
"display_name": "research workspace",
"description": "research workspace",
"client_id": "12345"
},
resourcePath="test")


pytestmark = pytest.mark.asyncio
Expand Down Expand Up @@ -62,10 +75,22 @@ def sample_airlock_request(status=AirlockRequestStatus.Submitted):
id=AIRLOCK_REQUEST_ID,
workspaceId=WORKSPACE_ID,
type=AirlockRequestType.Import,
files=[],
files=[AirlockFile(
name="data.txt",
size=5
)],
businessJustification="some test reason",
status=status,
reviews=[]
createdWhen=CURRENT_TIME,
createdBy=AirlockNotificationUserData(
name="John Doe",
email="john@example.com"
),
updatedWhen=CURRENT_TIME,
updatedBy=AirlockNotificationUserData(
name="Test User",
email="test@user.com"
)
)
return airlock_request

Expand Down Expand Up @@ -99,7 +124,14 @@ async def test_receiving_good_message(_, app, sb_client, logging_mock, workspace
await receive_step_result_message_and_update_status(app)

airlock_request_repo().get_airlock_request_by_id.assert_called_once_with(test_sb_step_result_message["data"]["request_id"])
airlock_request_repo().update_airlock_request.assert_called_once_with(original_request=expected_airlock_request, updated_by=expected_airlock_request.updatedBy, new_status=test_sb_step_result_message["data"]["new_status"], request_files=None, status_message=None, airlock_review=None, review_user_resource=None)
airlock_request_repo().update_airlock_request.assert_called_once_with(
original_request=expected_airlock_request,
updated_by=expected_airlock_request.updatedBy,
new_status=test_sb_step_result_message["data"]["new_status"],
request_files=None,
status_message=None,
airlock_review=None,
review_user_resource=None)
assert eg_client().send.call_count == 2
logging_mock.assert_not_called()
sb_client().get_queue_receiver().complete_message.assert_called_once_with(service_bus_received_message_mock)
Expand Down
Loading

0 comments on commit d0aaad7

Please sign in to comment.