Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use delegated key when generating SAS token in API #2460

Merged
merged 14 commits into from
Aug 16, 2022
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ENHANCEMENTS:

* 'CreationTime' field was added to Airlock requests ([#2432](https://github.com/microsoft/AzureTRE/issues/2432))
* Bundles mirror Terraform plugins when built ([#2446](https://github.com/microsoft/AzureTRE/pull/2446))
* API uses user delagation key when generating SAS token for airlock requests. ([#2390](https://github.com/microsoft/AzureTRE/issues/2390))

BUG FIXES:

Expand Down
2 changes: 1 addition & 1 deletion airlock_processor/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.2"
__version__ = "0.4.3"
2 changes: 1 addition & 1 deletion airlock_processor/shared_code/blob_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def copy_data(source_account_name: str, destination_account_name: str, request_i
def get_credential() -> DefaultAzureCredential:
managed_identity = os.environ.get("MANAGED_IDENTITY_CLIENT_ID")
if managed_identity:
logging.info("using the Airlock processor's managed identity to get storage management client")
logging.info("using the Airlock processor's managed identity to get credentials.")
return DefaultAzureCredential(managed_identity_client_id=os.environ["MANAGED_IDENTITY_CLIENT_ID"],
exclude_shared_token_cache_credential=True) if managed_identity else DefaultAzureCredential()

Expand Down
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.11"
__version__ = "0.4.12"
7 changes: 4 additions & 3 deletions api_app/api/routes/airlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@
from .airlock_resource_helpers import save_airlock_review, save_and_publish_event_airlock_request, \
update_status_and_publish_event_airlock_request, RequestAccountDetails

from services.airlock import get_storage_management_client, validate_user_allowed_to_access_storage_account, \
from services.airlock import validate_user_allowed_to_access_storage_account, \
get_account_and_rg_by_request, get_airlock_request_container_sas_token, validate_request_status

airlock_workspace_router = APIRouter(dependencies=[Depends(get_current_workspace_owner_or_researcher_user_or_airlock_manager)])
storage_client = get_storage_management_client()


# airlock
Expand Down Expand Up @@ -98,6 +97,8 @@ async def get_airlock_container_link(workspace=Depends(get_deployed_workspace_by
user=Depends(get_current_workspace_owner_or_researcher_user)) -> AirlockRequestTokenInResponse:
validate_user_allowed_to_access_storage_account(user, airlock_request)
validate_request_status(airlock_request)
logging.info("got it")
logging.info("got it debug")
LizaShak marked this conversation as resolved.
Show resolved Hide resolved
request_account_details: RequestAccountDetails = get_account_and_rg_by_request(airlock_request, workspace)
container_url = get_airlock_request_container_sas_token(storage_client, request_account_details, airlock_request)
container_url = get_airlock_request_container_sas_token(request_account_details, airlock_request)
return AirlockRequestTokenInResponse(containerUrl=container_url)
37 changes: 19 additions & 18 deletions api_app/services/airlock.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import logging
from datetime import datetime, timedelta

from azure.mgmt.storage import StorageManagementClient
from azure.storage.blob import generate_container_sas, ContainerSasPermissions
from azure.storage.blob import generate_container_sas, ContainerSasPermissions, BlobServiceClient
from fastapi import HTTPException
from starlette import status

Expand All @@ -15,10 +15,12 @@
from resources import strings, constants


def get_storage_management_client():
token_credential = DefaultAzureCredential(managed_identity_client_id=config.MANAGED_IDENTITY_CLIENT_ID,
exclude_shared_token_cache_credential=True)
return StorageManagementClient(credential=token_credential, subscription_id=config.SUBSCRIPTION_ID)
def get_credential() -> DefaultAzureCredential:
managed_identity = config.MANAGED_IDENTITY_CLIENT_ID
if managed_identity:
logging.info("Using managed identity credentials.")
return DefaultAzureCredential(managed_identity_client_id=config.MANAGED_IDENTITY_CLIENT_ID,
exclude_shared_token_cache_credential=True) if managed_identity else DefaultAzureCredential()


def get_account_and_rg_by_request(airlock_request: AirlockRequest, workspace: Workspace) -> RequestAccountDetails:
LizaShak marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -88,31 +90,30 @@ def validate_request_status(airlock_request: AirlockRequest):
return


def get_storage_account_key(storage_client: StorageManagementClient, request_account_details: RequestAccountDetails):
return storage_client.storage_accounts.list_keys(request_account_details.account_rg,
request_account_details.account_name).keys[0].value


def get_required_permission(airlock_request: AirlockRequest) -> ContainerSasPermissions:
if airlock_request.status == AirlockRequestStatus.Draft:
return ContainerSasPermissions(read=True, write=True, list=True, delete=True)
else:
return ContainerSasPermissions(read=True, list=True)


def get_airlock_request_container_sas_token(storage_client: StorageManagementClient,
request_account_details: RequestAccountDetails,
airlock_request: AirlockRequest):
account_key = get_storage_account_key(storage_client, request_account_details)
required_permission = get_required_permission(airlock_request)
def get_airlock_request_container_sas_token(request_account_details: RequestAccountDetails, airlock_request: AirlockRequest):

source_blob_service_client = BlobServiceClient(account_url=get_account_url(request_account_details.account_name),
LizaShak marked this conversation as resolved.
Show resolved Hide resolved
credential=get_credential())
expiry = datetime.utcnow() + timedelta(hours=config.AIRLOCK_SAS_TOKEN_EXPIRY_PERIOD_IN_HOURS)
udk = source_blob_service_client.get_user_delegation_key(datetime.utcnow(), expiry)
required_permission = get_required_permission(airlock_request)

# TODO: use user delegated key https://github.com/microsoft/AzureTRE/issues/2185
token = generate_container_sas(container_name=airlock_request.id,
account_name=request_account_details.account_name,
account_key=account_key,
user_delegation_key=udk,
permission=required_permission,
expiry=expiry)

return "https://{}.blob.core.windows.net/{}?{}" \
.format(request_account_details.account_name, airlock_request.id, token)


def get_account_url(account_name: str) -> str:
return f"https://{account_name}.blob.core.windows.net/"
2 changes: 1 addition & 1 deletion templates/core/terraform/airlock/identity.tf
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@ resource "azurerm_role_assignment" "airlock_blob_data_contributor" {
resource "azurerm_role_assignment" "api_reader_data_access" {
LizaShak marked this conversation as resolved.
Show resolved Hide resolved
count = length(local.api_sa_reader_data_access)
scope = local.api_sa_reader_data_access[count.index]
role_definition_name = "Reader and Data Access"
role_definition_name = "Storage Blob Data Contributor"
principal_id = var.api_principal_id
}
54 changes: 49 additions & 5 deletions templates/core/terraform/airlock/storage_accounts.tf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,33 @@ resource "azurerm_storage_account" "sa_import_external" {
lifecycle { ignore_changes = [tags] }
}

data "azurerm_private_dns_zone" "blobcore" {
name = "privatelink.blob.core.windows.net"
resource_group_name = var.resource_group_name
}

resource "azurerm_private_endpoint" "stg_import_external_pe" {
name = "stg-ex-import-blob-${var.tre_id}"
location = var.location
resource_group_name = var.resource_group_name
subnet_id = var.airlock_storage_subnet_id
tags = var.tre_core_tags

lifecycle { ignore_changes = [tags] }

private_dns_zone_group {
name = "private-dns-zone-group-stg-export-app"
private_dns_zone_ids = [data.azurerm_private_dns_zone.blobcore.id]
}

private_service_connection {
name = "psc-stgeximport-${var.tre_id}"
private_connection_resource_id = azurerm_storage_account.sa_import_external.id
is_manual_connection = false
subresource_names = ["Blob"]
}
}

# 'Approved' export
resource "azurerm_storage_account" "sa_export_approved" {
name = local.export_approved_storage_name
Expand All @@ -42,6 +69,28 @@ resource "azurerm_storage_account" "sa_export_approved" {
lifecycle { ignore_changes = [tags] }
}

resource "azurerm_private_endpoint" "stg_export_approved_pe" {
name = "stg-app-export-blob-${var.tre_id}"
location = var.location
resource_group_name = var.resource_group_name
subnet_id = var.airlock_storage_subnet_id
tags = var.tre_core_tags

lifecycle { ignore_changes = [tags] }

private_dns_zone_group {
name = "private-dns-zone-group-stg-export-app"
private_dns_zone_ids = [data.azurerm_private_dns_zone.blobcore.id]
}

private_service_connection {
name = "psc-stgappexport-${var.tre_id}"
private_connection_resource_id = azurerm_storage_account.sa_export_approved.id
is_manual_connection = false
subresource_names = ["Blob"]
}
}

# 'In-Progress' storage account
resource "azurerm_storage_account" "sa_import_in_progress" {
name = local.import_in_progress_storage_name
Expand All @@ -67,11 +116,6 @@ resource "azurerm_storage_account" "sa_import_in_progress" {
lifecycle { ignore_changes = [tags] }
}

data "azurerm_private_dns_zone" "blobcore" {
name = "privatelink.blob.core.windows.net"
resource_group_name = var.resource_group_name
}

resource "azurerm_private_endpoint" "stg_import_inprogress_pe" {
name = "stg-ip-import-blob-${var.tre_id}"
location = var.location
Expand Down
2 changes: 1 addition & 1 deletion templates/core/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.4.14"
__version__ = "0.4.15"
2 changes: 1 addition & 1 deletion templates/workspaces/base/porter.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: tre-workspace-base
version: 0.3.23
version: 0.3.24
description: "A base Azure TRE workspace"
dockerfile: Dockerfile.tmpl
registry: azuretre
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,6 @@ resource "azurerm_role_assignment" "airlock_blob_data_contributor" {
resource "azurerm_role_assignment" "api_reader_data_access" {
LizaShak marked this conversation as resolved.
Show resolved Hide resolved
count = length(local.api_reader_data_access)
scope = local.api_reader_data_access[count.index]
role_definition_name = "Reader and Data Access"
role_definition_name = "Storage Blob Data Contributor"
principal_id = data.azurerm_user_assigned_identity.api_id.principal_id
}