Skip to content

Commit

Permalink
Merge branch 'main' into tborisova/2791-firewall-migration-crashes-if…
Browse files Browse the repository at this point in the history
…-no-firewall-deployed
  • Loading branch information
tanya-borisova committed Oct 31, 2022
2 parents a643a5a + 6034dc3 commit 5e98138
Show file tree
Hide file tree
Showing 36 changed files with 669 additions and 233 deletions.
41 changes: 22 additions & 19 deletions .github/workflows/cli-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,48 @@ name: cli-package
on:
push:
branches: ["main"]
workflow_dispatch:
inputs:
environment:
description: The environment to run this workflow in
type: environment
default: CICD
required: true

jobs:
build:
runs-on: ubuntu-latest
# environment is used to access the secrets for Azure credentials
# to sign in to a container registry to re-use a dev container image as a build cache
environment: ${{ inputs.environment || 'CICD'}}

steps:
- name: Checkout (GitHub)
uses: actions/checkout@v2

- name: Login to Container Registry
uses: docker/login-action@v1
if: github.ref == 'refs/heads/main'
with:
registry: ${{ secrets.ACTIONS_ACR_NAME }}.azurecr.io/
username: ${{ secrets.ACTIONS_ACR_NAME }}
password: ${{ secrets.ACTIONS_ACR_PASSWORD }}

- name: Build and run dev container task
uses: devcontainers/ci@v0.2
uses: ./.github/actions/devcontainer_run_command
with:
imageName: ${{ secrets.ACTIONS_ACR_NAME }}.azurecr.io/tredev:latest
runCmd: |
COMMAND: |
# Validate installation and showing help output
cd cli
make pip-install install-cli
source <(_TRE_COMPLETE=bash_source tre)
tre --help
push: never
DEVCONTAINER_TAG: latest
CI_CACHE_ACR_NAME: ${{ secrets.ACR_NAME}}
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}

- name: Build and run dev container task
uses: devcontainers/ci@v0.2
- name: Create the CLI package
uses: ./.github/actions/devcontainer_run_command
with:
imageName: ${{ secrets.ACTIONS_ACR_NAME }}.azurecr.io/tredev:latest
runCmd: |
COMMAND: |
# Create the python wheel
cd cli
make build-package
push: never

sudo make build-package
DEVCONTAINER_TAG: latest
CI_CACHE_ACR_NAME: ${{ secrets.ACR_NAME}}
AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }}

- name: Upload Wheel as artifact
uses: actions/upload-artifact@v3
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
FEATURES:
* Display workspace and shared services total costs for admin role in UI [#2738](https://github.com/microsoft/AzureTRE/pull/2772)
* Automatically validate all resources have tre_id tag via TFLint [#2774](https://github.com/microsoft/AzureTRE/pull/2774)
* Add metadata endpoint and simplify `tre` CLI login (also adds API version to UI) (#2794)

ENHANCEMENTS:
* Renamed several airlock fields to make them more descriptive and added a createdBy field. Included migration for backwards compatibility ([#2779](https://github.com/microsoft/AzureTRE/pull/2779))
* Show error message when Review VMs are not configured in the current workspace
* CLI: Add missing endpoints and minor bug fixes (#2784)

BUG FIXES:
* Show the correct createdBy value for airlock requests in UI and in API queries ([#2779](https://github.com/microsoft/AzureTRE/pull/2779))
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,10 @@ bundle-register:
$(call target_title, "Registering ${DIR} bundle") \
&& . ${MAKEFILE_DIR}/devops/scripts/check_dependencies.sh porter,env,auth \
&& az acr login --name $${ACR_NAME} \
&& . ${MAKEFILE_DIR}/devops/scripts/get_access_token.sh \
&& ${MAKEFILE_DIR}/devops/scripts/ensure_cli_signed_in.sh TRE_URL="$${TRE_URL:-https://$${TRE_ID}.$${LOCATION}.cloudapp.azure.com}" \
&& cd ${DIR} \
&& ${MAKEFILE_DIR}/devops/scripts/register_bundle_with_api.sh --acr-name "$${ACR_NAME}" --bundle-type "$${BUNDLE_TYPE}" \
--current --insecure --tre_url "$${TRE_URL:-https://$${TRE_ID}.$${LOCATION}.cloudapp.azure.com}" --verify \
--current --verify \
--workspace-service-name "$${WORKSPACE_SERVICE_NAME}"

workspace_bundle:
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.5.6"
__version__ = "0.5.8"
2 changes: 1 addition & 1 deletion api_app/api/routes/airlock.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ async def get_all_airlock_requests_by_workspace(

@airlock_workspace_router.get("/workspaces/{workspace_id}/requests/{airlock_request_id}", status_code=status_code.HTTP_200_OK,
response_model=AirlockRequestInResponse, name=strings.API_GET_AIRLOCK_REQUEST,
dependencies=[Depends(get_current_workspace_owner_or_researcher_user), Depends(get_workspace_by_id_from_path)])
dependencies=[Depends(get_current_workspace_owner_or_researcher_user_or_airlock_manager), Depends(get_workspace_by_id_from_path)])
async def retrieve_airlock_request_by_id(airlock_request=Depends(get_airlock_request_by_id_from_path)) -> AirlockRequestInResponse:
return AirlockRequestInResponse(airlockRequest=airlock_request)

Expand Down
6 changes: 4 additions & 2 deletions api_app/api/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from api.dependencies.database import get_repository
from db.repositories.workspaces import WorkspaceRepository
from api.routes import health, ping, workspaces, workspace_templates, workspace_service_templates, user_resource_templates, \
shared_services, shared_service_templates, migrations, costs, airlock, operations
shared_services, shared_service_templates, migrations, costs, airlock, operations, metadata
from core import config

core_tags_metadata = [
Expand All @@ -30,11 +30,13 @@
router = APIRouter()
router.include_router(health.router, tags=["health"])
router.include_router(ping.router, tags=["health"])
router.include_router(metadata.router, tags=["metadata"])

# Core API
core_router = APIRouter(prefix=config.API_PREFIX)
core_router.include_router(health.router, tags=["health"])
core_router.include_router(ping.router, tags=["health"])
core_router.include_router(metadata.router, tags=["metadata"])
core_router.include_router(workspace_templates.workspace_templates_admin_router, tags=["workspace templates"])
core_router.include_router(workspace_service_templates.workspace_service_templates_core_router, tags=["workspace service templates"])
core_router.include_router(user_resource_templates.user_resource_templates_core_router, tags=["user resource templates"])
Expand Down Expand Up @@ -77,7 +79,7 @@ async def get_swagger(request: Request):
init_oauth={
"usePkceWithAuthorizationCodeGrant": True,
"clientId": config.SWAGGER_UI_CLIENT_ID,
"scopes": ["openid", "offline_access", f"api://{config.API_CLIENT_ID}/user_impersonation"]
"scopes": ["openid", "offline_access", config.API_ROOT_SCOPE]
}
)

Expand Down
17 changes: 17 additions & 0 deletions api_app/api/routes/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from fastapi import APIRouter
from resources import strings
from _version import __version__
from core import config
from models.schemas.metadata import Metadata

router = APIRouter()


@router.get("/.metadata", name=strings.API_GET_PING)
def metadata() -> Metadata:
return Metadata(
api_version=__version__,
api_client_id=config.API_CLIENT_ID,
api_root_scope=config.API_ROOT_SCOPE,
aad_tenant_id=config.AAD_TENANT_ID
)
10 changes: 5 additions & 5 deletions api_app/api/routes/workspace_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,27 @@
from models.schemas.resource_template import ResourceTemplateInResponse, ResourceTemplateInformationInList
from models.schemas.workspace_template import WorkspaceTemplateInCreate, WorkspaceTemplateInResponse
from resources import strings
from services.authentication import get_current_admin_user
from services.authentication import get_current_tre_user_or_tre_admin, get_current_admin_user
from .resource_helpers import get_current_template_by_name


workspace_templates_admin_router = APIRouter(dependencies=[Depends(get_current_admin_user)])
workspace_templates_admin_router = APIRouter(dependencies=[Depends(get_current_tre_user_or_tre_admin)])


@workspace_templates_admin_router.get("/workspace-templates", response_model=ResourceTemplateInformationInList, name=strings.API_GET_WORKSPACE_TEMPLATES)
async def get_workspace_templates(authorized_only: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository)), user=Depends(get_current_admin_user)) -> ResourceTemplateInformationInList:
async def get_workspace_templates(authorized_only: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository)), user=Depends(get_current_tre_user_or_tre_admin)) -> ResourceTemplateInformationInList:
templates_infos = template_repo.get_templates_information(ResourceType.Workspace, user.roles if authorized_only else None)
return ResourceTemplateInformationInList(templates=templates_infos)


@workspace_templates_admin_router.get("/workspace-templates/{workspace_template_name}", response_model=WorkspaceTemplateInResponse, name=strings.API_GET_WORKSPACE_TEMPLATE_BY_NAME, response_model_exclude_none=True)
async def get_current_workspace_template_by_name(workspace_template_name: str, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> WorkspaceTemplateInResponse:
async def get_current_workspace_template_by_name(workspace_template_name: str, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository)), user=Depends(get_current_tre_user_or_tre_admin)) -> WorkspaceTemplateInResponse:
template = get_current_template_by_name(workspace_template_name, template_repo, ResourceType.Workspace, is_update=is_update)
return parse_obj_as(WorkspaceTemplateInResponse, template)


@workspace_templates_admin_router.post("/workspace-templates", status_code=status.HTTP_201_CREATED, response_model=WorkspaceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_WORKSPACE_TEMPLATES)
async def register_workspace_template(template_input: WorkspaceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInResponse:
async def register_workspace_template(template_input: WorkspaceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository)), user=Depends(get_current_admin_user)) -> ResourceTemplateInResponse:
try:
return template_repo.create_and_validate_template(template_input, ResourceType.Workspace)
except EntityVersionExist:
Expand Down
7 changes: 4 additions & 3 deletions api_app/api/routes/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
get_access_service, get_current_workspace_owner_user, get_current_workspace_owner_or_researcher_user, get_current_tre_user_or_tre_admin, \
get_current_workspace_owner_or_tre_admin, \
get_current_workspace_owner_or_researcher_user_or_airlock_manager, \
get_current_workspace_owner_or_airlock_manager, \
get_current_workspace_owner_or_researcher_user_or_airlock_manager_or_tre_admin
from services.authentication import extract_auth_information
from services.azure_resource_status import get_azure_resource_status
Expand Down Expand Up @@ -241,7 +242,7 @@ async def create_workspace_service(response: Response, workspace_service_input:


@workspace_services_workspace_router.patch("/workspaces/{workspace_id}/workspace-services/{service_id}", status_code=status.HTTP_202_ACCEPTED, response_model=OperationInResponse, name=strings.API_UPDATE_WORKSPACE_SERVICE, dependencies=[Depends(get_current_workspace_owner_or_researcher_user), Depends(get_workspace_by_id_from_path)])
async def patch_workspace_service(workspace_service_patch: ResourcePatch, response: Response, user=Depends(get_current_workspace_owner_or_researcher_user), workspace_service_repo=Depends(get_repository(WorkspaceServiceRepository)), workspace_service=Depends(get_workspace_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...)) -> OperationInResponse:
async def patch_workspace_service(workspace_service_patch: ResourcePatch, response: Response, user=Depends(get_current_workspace_owner_user), workspace_service_repo=Depends(get_repository(WorkspaceServiceRepository)), workspace_service=Depends(get_workspace_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...)) -> OperationInResponse:
try:
patched_workspace_service, resource_template = workspace_service_repo.patch_workspace_service(workspace_service, workspace_service_patch, etag, resource_template_repo, user)
operation = await send_resource_request_message(
Expand Down Expand Up @@ -302,12 +303,12 @@ async def invoke_action_on_workspace_service(response: Response, action: str, us


# workspace service operations
@workspace_services_workspace_router.get("/workspaces/{workspace_id}/workspace-services/{service_id}/operations", response_model=OperationInList, name=strings.API_GET_RESOURCE_OPERATIONS, dependencies=[Depends(get_current_workspace_owner_or_researcher_user_or_airlock_manager), Depends(get_workspace_by_id_from_path)])
@workspace_services_workspace_router.get("/workspaces/{workspace_id}/workspace-services/{service_id}/operations", response_model=OperationInList, name=strings.API_GET_RESOURCE_OPERATIONS, dependencies=[Depends(get_current_workspace_owner_or_airlock_manager), Depends(get_workspace_by_id_from_path)])
async def retrieve_workspace_service_operations_by_workspace_service_id(workspace_service=Depends(get_workspace_service_by_id_from_path), operations_repo=Depends(get_repository(OperationRepository))) -> OperationInList:
return OperationInList(operations=operations_repo.get_operations_by_resource_id(resource_id=workspace_service.id))


@workspace_services_workspace_router.get("/workspaces/{workspace_id}/workspace-services/{service_id}/operations/{operation_id}", response_model=OperationInResponse, name=strings.API_GET_RESOURCE_OPERATION_BY_ID, dependencies=[Depends(get_current_workspace_owner_or_researcher_user_or_airlock_manager), Depends(get_workspace_by_id_from_path)])
@workspace_services_workspace_router.get("/workspaces/{workspace_id}/workspace-services/{service_id}/operations/{operation_id}", response_model=OperationInResponse, name=strings.API_GET_RESOURCE_OPERATION_BY_ID, dependencies=[Depends(get_current_workspace_owner_or_airlock_manager), Depends(get_workspace_by_id_from_path)])
async def retrieve_workspace_service_operation_by_workspace_service_id_and_operation_id(workspace_service=Depends(get_workspace_service_by_id_from_path), operation=Depends(get_operation_by_id_from_path)) -> OperationInList:
return OperationInResponse(operation=operation)

Expand Down
2 changes: 2 additions & 0 deletions api_app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,5 @@
API_AUDIENCE: str = config("API_AUDIENCE", default=API_CLIENT_ID)

AIRLOCK_SAS_TOKEN_EXPIRY_PERIOD_IN_HOURS: int = config("AIRLOCK_SAS_TOKEN_EXPIRY_PERIOD_IN_HOURS", default=1)

API_ROOT_SCOPE: str = f"api://{API_CLIENT_ID}/user_impersonation"
8 changes: 8 additions & 0 deletions api_app/models/schemas/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from pydantic import BaseModel


class Metadata(BaseModel):
api_version: str
api_client_id: str
api_root_scope: str
aad_tenant_id: str
1 change: 1 addition & 0 deletions api_app/resources/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# API Descriptions
API_GET_HEALTH_STATUS = "Get health status"
API_GET_PING = "Simple endpoint to test calling the API"
API_GET_METADATA = "Get public API metadata (e.g. to support the UI and CLI)"
API_MIGRATE_DATABASE = "Migrate documents in the database"

API_GET_MY_OPERATIONS = "Get Operations that the current user has initiated"
Expand Down
3 changes: 3 additions & 0 deletions api_app/services/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ def get_access_service(provider: str = AuthProvider.AAD) -> AccessService:
get_current_workspace_owner_or_researcher_user = AzureADAuthorization(require_one_of_roles=['WorkspaceOwner', 'WorkspaceResearcher'])


get_current_workspace_owner_or_airlock_manager = AzureADAuthorization(require_one_of_roles=['WorkspaceOwner', 'AirlockManager'])


get_current_workspace_owner_or_researcher_user_or_airlock_manager = AzureADAuthorization(require_one_of_roles=['WorkspaceOwner', 'WorkspaceResearcher', 'AirlockManager'])


Expand Down
4 changes: 0 additions & 4 deletions api_app/tests_ma/test_api/test_routes/test_api_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,6 @@ def log_in_with_non_admin(self, app, non_admin_user):
with patch('services.aad_authentication.AzureADAuthorization._get_user_from_token', return_value=non_admin_user()):
yield

async def test_get_workspace_templates_requires_admin_rights(self, app, client):
response = await client.get(app.url_path_for(strings.API_GET_WORKSPACE_TEMPLATES))
assert response.status_code == status.HTTP_403_FORBIDDEN

async def test_post_workspace_templates_requires_admin_rights(self, app, client):
response = await client.post(app.url_path_for(strings.API_CREATE_WORKSPACE_TEMPLATES), json='{}')
assert response.status_code == status.HTTP_403_FORBIDDEN
Expand Down
Loading

0 comments on commit 5e98138

Please sign in to comment.