From 829be87c1572eb8c972134e1432f73850977302a Mon Sep 17 00:00:00 2001 From: David Moore Date: Mon, 17 Oct 2022 15:11:19 +0100 Subject: [PATCH 01/10] UI / css changes to create form --- ui/app/src/App.scss | 56 ++++++++++++++ .../create-update-resource/ResourceForm.tsx | 9 ++- ui/app/src/models/resourceTemplate.ts | 76 +++++++++++++------ 3 files changed, 117 insertions(+), 24 deletions(-) diff --git a/ui/app/src/App.scss b/ui/app/src/App.scss index eb04e76b65..6ac7e47fa1 100644 --- a/ui/app/src/App.scss +++ b/ui/app/src/App.scss @@ -236,6 +236,62 @@ input[readonly]{ background-color: #990000; } } + +/* create form overrides */ +/* panel header */ +.ms-Panel-commands{ + background:#fff; + border-bottom: 1px #ccc solid; + padding-bottom: 10px; +} + +/* template description at top of panel */ +.rjsf > .ms-Grid-col{ + margin-top: -25px; +} +.rjsf > .ms-Grid-col > span:first-child { + display:block; + background-color: #efefef; + padding:10px; + margin-bottom: 15px; + font-style: normal; +} + +/* border around sub-blocks */ +.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object, +.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-array{ + border: 1px #ccc dashed; + padding:10px; + background-color: #fcfcfc; +} + +/* sub titles and sub-sub titles */ +.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > label.ms-Label, +.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-array > label.ms-Label{ + font-size: 20px; +} +.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > .ms-Grid > .ms-Grid-row > .ms-Grid-col > label.ms-Label, +.ms-Panel-content .rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-array > .ms-Grid > .ms-Grid-row > .ms-Grid-col > label.ms-Label{ + font-size: 16px; +} + +/* remove secondary template description at the bottom of each template + sub blocks */ +.rjsf > .ms-Grid-col > span:last-child, +.rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > span:last-child, +.rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > .ms-Grid > .ms-Grid-row > .ms-Grid-col > span:last-child +{ + display:none; +} + +/* make descriptive text italic */ +.field span{ + font-style: italic; +} +.field span.ms-Checkbox-text { + font-style: normal; +} + +/* hide fields explicitly */ .tre-hidden{ display:none; } diff --git a/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx b/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx index fde31fd684..10beb481a2 100644 --- a/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx +++ b/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx @@ -8,6 +8,7 @@ import { Resource } from "../../../models/resource"; import { ResourceType } from "../../../models/resourceType"; import { APIError } from "../../../models/exceptions"; import { ExceptionLayout } from "../ExceptionLayout"; +import { ResourceTemplate, sanitiseTemplateForRJSF } from "../../../models/resourceTemplate"; interface ResourceFormProps { templateName: string, @@ -30,7 +31,9 @@ export const ResourceForm: React.FunctionComponent = (props: const getFullTemplate = async () => { try { // Get the full resource template containing the required parameters - const templateResponse = await apiCall(props.updateResource ? `${props.templatePath}?is_update=true` : props.templatePath, HttpMethod.Get); + const templateResponse = (await apiCall(props.updateResource ? `${props.templatePath}?is_update=true` : props.templatePath, HttpMethod.Get)) as ResourceTemplate; + + console.log("raw", templateResponse); // if it's an update, populate the form with the props that are available in the template if (props.updateResource) { @@ -41,7 +44,9 @@ export const ResourceForm: React.FunctionComponent = (props: setFormData(d); } - setTemplate(templateResponse); + const sanitisedTemplate = sanitiseTemplateForRJSF(templateResponse); + console.log("sanitised", sanitisedTemplate); + setTemplate(sanitisedTemplate); setLoading(LoadingState.Ok); } catch (err: any){ err.userMessage = "Error retrieving resource template"; diff --git a/ui/app/src/models/resourceTemplate.ts b/ui/app/src/models/resourceTemplate.ts index 6ccbfbedde..096f0424cf 100644 --- a/ui/app/src/models/resourceTemplate.ts +++ b/ui/app/src/models/resourceTemplate.ts @@ -1,32 +1,64 @@ import { ResourceType } from "./resourceType"; export interface ResourceTemplate { - id: string, - name: string, - version: string, - title: string, - resourceType: ResourceType, - current: boolean, - properties: any, - system_properties: any, - actions: Array, - customActions: Array, - uiSchema: any + id: string, + name: string, + type: string, + description: string, + version: string, + title: string, + resourceType: ResourceType, + current: boolean, + properties: any, + allOf?: Array, + system_properties: any, + actions: Array, + customActions: Array, + required: Array, + uiSchema: any, + pipeline: any } +export const sanitiseTemplateForRJSF = (template: ResourceTemplate) => { + if (template.properties) { + Object.keys(template.properties).forEach((key: string) => { + Object.keys(template.properties[key]).forEach((name: string) => { + if (template.properties[key][name] === null) { + delete template.properties[key][name] + } + }); + }); + } + + const sanitised = { + name: template.name, + type: template.type, + description: template.description, + title: template.title, + properties: template.properties, + allOf: template.allOf, + required: template.required, + uiSchema: template.uiSchema + } + + if (!sanitised.allOf) delete sanitised.allOf; + + return sanitised; +}; + export interface TemplateAction { - name: string, - description: string + name: string, + description: string } // make a sensible guess at an icon export const getActionIcon = (actionName: string) => { - switch(actionName.toLowerCase()){ - case 'start': - return 'Play'; - case 'stop': - return 'Stop'; - default: - return 'Asterisk' - } -} + switch(actionName.toLowerCase()){ + case 'start': + return 'Play'; + case 'stop': + return 'Stop'; + default: + return 'Asterisk' + } +}; From e86163fcfa5b5f3da8dfdd107baa244a2093858d Mon Sep 17 00:00:00 2001 From: David Moore Date: Mon, 17 Oct 2022 15:38:43 +0100 Subject: [PATCH 02/10] last-child-of-type --- ui/app/src/App.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/app/src/App.scss b/ui/app/src/App.scss index 6ac7e47fa1..7f721cfce9 100644 --- a/ui/app/src/App.scss +++ b/ui/app/src/App.scss @@ -249,7 +249,7 @@ input[readonly]{ .rjsf > .ms-Grid-col{ margin-top: -25px; } -.rjsf > .ms-Grid-col > span:first-child { +.rjsf > .ms-Grid-col > span:first-of-type { display:block; background-color: #efefef; padding:10px; @@ -276,9 +276,9 @@ input[readonly]{ } /* remove secondary template description at the bottom of each template + sub blocks */ -.rjsf > .ms-Grid-col > span:last-child, -.rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > span:last-child, -.rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > .ms-Grid > .ms-Grid-row > .ms-Grid-col > span:last-child +.rjsf > .ms-Grid-col > span:last-of-type, +.rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > span:last-of-type, +.rjsf > .ms-Grid-col > .ms-Grid > .ms-Grid-row > .field-object > .ms-Grid > .ms-Grid-row > .ms-Grid-col > span:last-of-type { display:none; } From aa74e9c0cec74992b6ffc86351f6947d0479f5e7 Mon Sep 17 00:00:00 2001 From: David Moore Date: Mon, 17 Oct 2022 20:48:51 +0100 Subject: [PATCH 03/10] removed props from static json files --- api_app/schemas/azuread.json | 13 +------------ api_app/schemas/workspace.json | 16 ---------------- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/api_app/schemas/azuread.json b/api_app/schemas/azuread.json index 29b8354a22..8c4fa52189 100644 --- a/api_app/schemas/azuread.json +++ b/api_app/schemas/azuread.json @@ -5,19 +5,8 @@ "title": "Azure AD Authorisation Schema", "default": {}, "required": [ - "client_id" ], "properties": { - "client_id": { - "type": "string", - "title": "Application (Client) ID", - "description": "The AAD Application Registration ID for the workspace. Use 'auto_create' if you wish TRE to create this." - }, - "client_secret": { - "type": "string", - "title": "Application (Client) Secret", - "description": "The AAD Application Registration secret for the workspace. Leave blank if using `auto_create` above. This value will be stored in the Workspace Key Vault.", - "sensitive": true - } + } } diff --git a/api_app/schemas/workspace.json b/api_app/schemas/workspace.json index 1f860b80a0..9e7711619f 100644 --- a/api_app/schemas/workspace.json +++ b/api_app/schemas/workspace.json @@ -27,22 +27,6 @@ "title": "Workspace Overview", "description": "Long form description of the workspace, in markdown syntax.", "updateable": true - }, - "address_space_size": { - "type": "string", - "title": "Address space size", - "description": "Network address size (small, medium, large or custom) to be used by the workspace.", - "enum": [ - "small", - "medium", - "large", - "custom" - ] - }, - "address_space": { - "type": "string", - "title": "Address space", - "description": "Network address space to be used by the workspace if 'Address space size' is custom." } } } From e1a0cf78b7a98abeda0c59a6024a2d0008897958 Mon Sep 17 00:00:00 2001 From: David Moore Date: Thu, 27 Oct 2022 16:20:39 +0100 Subject: [PATCH 04/10] conditional templates --- api_app/_version.py | 2 +- api_app/api/routes/airlock.py | 8 +- api_app/api/routes/api.py | 5 +- api_app/api/routes/resource_helpers.py | 11 + .../api/routes/shared_service_templates.py | 11 +- api_app/api/routes/user_resource_templates.py | 8 +- .../api/routes/workspace_service_templates.py | 8 +- api_app/api/routes/workspace_templates.py | 8 +- api_app/db/repositories/resource_templates.py | 3 + api_app/db/repositories/workspaces.py | 2 +- api_app/models/domain/resource_template.py | 3 +- api_app/resources/strings.py | 5 + api_app/service_bus/helpers.py | 4 +- api_app/services/aad_authentication.py | 16 +- api_app/services/schema_service.py | 4 + .../test_api/test_routes/test_airlock.py | 10 +- .../test_resource_repository.py | 25 +- .../test_workpaces_repository.py | 2 +- .../test_resource_request_sender.py | 2 +- .../test_services/test_aad_access_service.py | 4 +- e2e_tests/test_workspace_services.py | 2 +- templates/core/terraform/variables.tf | 2 +- .../airlock-import-review/porter.yaml | 3 +- .../template_schema.json | 200 +++++++--- templates/workspaces/base/porter.yaml | 3 +- .../workspaces/base/template_schema.json | 348 ++++++++++++------ templates/workspaces/unrestricted/porter.yaml | 3 +- .../unrestricted/template_schema.json | 298 ++++++++++++--- ui/app/src/App.scss | 1 + ui/app/src/components/shared/ResourceCard.tsx | 2 +- .../create-update-resource/ResourceForm.tsx | 79 +++- ui/app/src/index.tsx | 2 +- ui/app/src/models/operation.ts | 30 +- 33 files changed, 844 insertions(+), 270 deletions(-) diff --git a/api_app/_version.py b/api_app/_version.py index d3563f072c..92fced85ba 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.4.54" +__version__ = "0.4.55" diff --git a/api_app/api/routes/airlock.py b/api_app/api/routes/airlock.py index 68a5a9fe0b..2b373041c7 100644 --- a/api_app/api/routes/airlock.py +++ b/api_app/api/routes/airlock.py @@ -105,13 +105,15 @@ async def create_review_user_resource( # 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"] + workspace_id = config["import_vm_workspace_id"] + workspace_service_id = config["import_vm_workspace_service_id"] + user_resource_template_name = config["import_vm_user_resource_template_name"] 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"] + workspace_service_id = config["export_vm_orkspace_service_id"] + user_resource_template_name = config["export_vm_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: diff --git a/api_app/api/routes/api.py b/api_app/api/routes/api.py index e7570a195d..8f384c908b 100644 --- a/api_app/api/routes/api.py +++ b/api_app/api/routes/api.py @@ -34,8 +34,11 @@ core_router = APIRouter(prefix=config.API_PREFIX) core_router.include_router(health.router, tags=["health"]) 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"]) + +# NOTE: User Resource Templates need to be registered before workspace service templates to cater for the `/{service_template_name}/user-resources` +# Else when you call `/{service_template_name}/user-resources` it will call the workspace service endpoint, taking "user-resouces" as the version number. core_router.include_router(user_resource_templates.user_resource_templates_core_router, tags=["user resource templates"]) +core_router.include_router(workspace_service_templates.workspace_service_templates_core_router, tags=["workspace service templates"]) core_router.include_router(shared_service_templates.shared_service_templates_core_router, tags=["shared service templates"]) core_router.include_router(shared_services.shared_services_router, tags=["shared services"]) core_router.include_router(operations.operations_router, tags=["operations"]) diff --git a/api_app/api/routes/resource_helpers.py b/api_app/api/routes/resource_helpers.py index fd2680d4f9..4a9934c7b8 100644 --- a/api_app/api/routes/resource_helpers.py +++ b/api_app/api/routes/resource_helpers.py @@ -125,5 +125,16 @@ def get_current_template_by_name(template_name: str, template_repo: ResourceTemp raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=strings.STATE_STORE_ENDPOINT_NOT_RESPONDING) +def get_template_by_name_and_version(template_name: str, version: str, template_repo: ResourceTemplateRepository, resource_type: ResourceType, parent_service_template_name: str = "", is_update: bool = False) -> dict: + try: + template = template_repo.get_template_by_name_and_version(template_name, version, resource_type, parent_service_template_name) + return template_repo.enrich_template(template, is_update=is_update) + except EntityDoesNotExist: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.TEMPLATE_VERSION_DOES_NOT_EXIST) + except Exception as e: + logging.debug(e) + raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=strings.STATE_STORE_ENDPOINT_NOT_RESPONDING) + + def get_timestamp() -> float: return datetime.utcnow().timestamp() diff --git a/api_app/api/routes/shared_service_templates.py b/api_app/api/routes/shared_service_templates.py index 917f1ec88d..de79ddf1c7 100644 --- a/api_app/api/routes/shared_service_templates.py +++ b/api_app/api/routes/shared_service_templates.py @@ -9,7 +9,7 @@ from models.schemas.shared_service_template import SharedServiceTemplateInCreate, SharedServiceTemplateInResponse from resources import strings from services.authentication import get_current_admin_user, get_current_tre_user_or_tre_admin -from .resource_helpers import get_current_template_by_name +from api.routes.resource_helpers import get_current_template_by_name, get_template_by_name_and_version shared_service_templates_core_router = APIRouter(dependencies=[Depends(get_current_tre_user_or_tre_admin)]) @@ -30,6 +30,15 @@ async def get_current_shared_service_template_by_name(shared_service_template_na raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.SHARED_SERVICE_TEMPLATE_DOES_NOT_EXIST) +@shared_service_templates_core_router.get("/shared-service-templates/{shared_service_template_name}/{version}", response_model=SharedServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_GET_SHARED_SERVICE_TEMPLATE_BY_NAME_AND_VERSION, dependencies=[Depends(get_current_tre_user_or_tre_admin)]) +async def get_shared_service_template_by_name_and_version(shared_service_template_name: str, version, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> SharedServiceTemplateInResponse: + try: + template = get_template_by_name_and_version(shared_service_template_name, version, template_repo, ResourceType.SharedService, is_update=is_update) + return parse_obj_as(SharedServiceTemplateInResponse, template) + except EntityDoesNotExist: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.SHARED_SERVICE_TEMPLATE_DOES_NOT_EXIST) + + @shared_service_templates_core_router.post("/shared-service-templates", status_code=status.HTTP_201_CREATED, response_model=SharedServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_SHARED_SERVICE_TEMPLATES, dependencies=[Depends(get_current_admin_user)]) async def register_shared_service_template(template_input: SharedServiceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInResponse: try: diff --git a/api_app/api/routes/user_resource_templates.py b/api_app/api/routes/user_resource_templates.py index f2cc9859b2..2bdf1dbeb1 100644 --- a/api_app/api/routes/user_resource_templates.py +++ b/api_app/api/routes/user_resource_templates.py @@ -4,7 +4,7 @@ from api.dependencies.database import get_repository from api.dependencies.workspace_service_templates import get_workspace_service_template_by_name_from_path -from api.routes.workspace_templates import get_current_template_by_name +from api.routes.resource_helpers import get_current_template_by_name, get_template_by_name_and_version from db.errors import EntityVersionExist from db.repositories.resource_templates import ResourceTemplateRepository from models.domain.resource import ResourceType @@ -29,6 +29,12 @@ async def get_current_user_resource_template_by_name(service_template_name: str, return parse_obj_as(UserResourceTemplateInResponse, template) +@user_resource_templates_core_router.get("/workspace-service-templates/{service_template_name}/user-resource-templates/{user_resource_template_name}/{version}", response_model=UserResourceTemplateInResponse, response_model_exclude_none=True, name=strings.API_GET_USER_RESOURCE_TEMPLATE_BY_NAME_AND_VERSION, dependencies=[Depends(get_current_tre_user_or_tre_admin)]) +async def get_user_resource_template_by_name_and_version(service_template_name: str, user_resource_template_name: str, version, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> UserResourceTemplateInResponse: + template = get_template_by_name_and_version(user_resource_template_name, version, template_repo, ResourceType.UserResource, service_template_name, is_update=is_update) + return parse_obj_as(UserResourceTemplateInResponse, template) + + @user_resource_templates_core_router.post("/workspace-service-templates/{service_template_name}/user-resource-templates", status_code=status.HTTP_201_CREATED, response_model=UserResourceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_USER_RESOURCE_TEMPLATES, dependencies=[Depends(get_current_admin_user)]) async def register_user_resource_template(template_input: UserResourceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository)), workspace_service_template=Depends(get_workspace_service_template_by_name_from_path)) -> UserResourceTemplateInResponse: try: diff --git a/api_app/api/routes/workspace_service_templates.py b/api_app/api/routes/workspace_service_templates.py index 44601f8bc0..b1f5d2da39 100644 --- a/api_app/api/routes/workspace_service_templates.py +++ b/api_app/api/routes/workspace_service_templates.py @@ -2,7 +2,7 @@ from pydantic import parse_obj_as from api.dependencies.database import get_repository -from api.routes.workspace_templates import get_current_template_by_name +from api.routes.resource_helpers import get_current_template_by_name, get_template_by_name_and_version from db.errors import EntityVersionExist from db.repositories.resource_templates import ResourceTemplateRepository from models.domain.resource import ResourceType @@ -27,6 +27,12 @@ async def get_current_workspace_service_template_by_name(service_template_name: return parse_obj_as(WorkspaceServiceTemplateInResponse, template) +@workspace_service_templates_core_router.get("/workspace-service-templates/{service_template_name}/{version}", response_model=WorkspaceServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_GET_WORKSPACE_SERVICE_TEMPLATE_BY_NAME_AND_VERSION, dependencies=[Depends(get_current_tre_user_or_tre_admin)]) +async def get_workspace_service_template_by_name_and_version(service_template_name: str, version, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> WorkspaceServiceTemplateInResponse: + template = get_template_by_name_and_version(service_template_name, version, template_repo, ResourceType.WorkspaceService, is_update=is_update) + return parse_obj_as(WorkspaceServiceTemplateInResponse, template) + + @workspace_service_templates_core_router.post("/workspace-service-templates", status_code=status.HTTP_201_CREATED, response_model=WorkspaceServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES, dependencies=[Depends(get_current_admin_user)]) async def register_workspace_service_template(template_input: WorkspaceServiceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInResponse: try: diff --git a/api_app/api/routes/workspace_templates.py b/api_app/api/routes/workspace_templates.py index 8879628927..ef206e4e9a 100644 --- a/api_app/api/routes/workspace_templates.py +++ b/api_app/api/routes/workspace_templates.py @@ -10,7 +10,7 @@ from models.schemas.workspace_template import WorkspaceTemplateInCreate, WorkspaceTemplateInResponse from resources import strings from services.authentication import get_current_admin_user -from .resource_helpers import get_current_template_by_name +from api.routes.resource_helpers import get_current_template_by_name, get_template_by_name_and_version workspace_templates_admin_router = APIRouter(dependencies=[Depends(get_current_admin_user)]) @@ -28,6 +28,12 @@ async def get_current_workspace_template_by_name(workspace_template_name: str, i return parse_obj_as(WorkspaceTemplateInResponse, template) +@workspace_templates_admin_router.get("/workspace-templates/{workspace_template_name}/{version}", response_model=WorkspaceTemplateInResponse, name=strings.API_GET_WORKSPACE_TEMPLATE_BY_NAME_AND_VERSION, response_model_exclude_none=True) +async def get_workspace_template_by_name_and_version(workspace_template_name: str, version: str, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> WorkspaceTemplateInResponse: + template = get_template_by_name_and_version(workspace_template_name, version, 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: try: diff --git a/api_app/db/repositories/resource_templates.py b/api_app/db/repositories/resource_templates.py index d395dc4b94..404d16213a 100644 --- a/api_app/db/repositories/resource_templates.py +++ b/api_app/db/repositories/resource_templates.py @@ -116,6 +116,9 @@ def create_template(self, template_input: ResourceTemplateInCreate, resource_typ if "pipeline" in template_input.json_schema: template["pipeline"] = template_input.json_schema["pipeline"] + if "allOf" in template_input.json_schema: + template["allOf"] = template_input.json_schema["allOf"] + if resource_type == ResourceType.UserResource: template["parentWorkspaceService"] = parent_service_name template = parse_obj_as(UserResourceTemplate, template) diff --git a/api_app/db/repositories/workspaces.py b/api_app/db/repositories/workspaces.py index 0763ebbb09..7812b03a43 100644 --- a/api_app/db/repositories/workspaces.py +++ b/api_app/db/repositories/workspaces.py @@ -98,7 +98,7 @@ def get_workspace_owner(self, workspace_properties: dict, workspace_owner_object return workspace_owner_object_id if user_defined_workspace_owner_object_id is None else user_defined_workspace_owner_object_id def automatically_create_application_registration(self, workspace_properties: dict) -> bool: - return True if workspace_properties["client_id"] == "auto_create" else False + return True if ("auth_type" in workspace_properties and workspace_properties["auth_type"] == "Automatic") else False def get_address_space_based_on_size(self, workspace_properties: dict): # Default the address space to 'small' if not supplied. diff --git a/api_app/models/domain/resource_template.py b/api_app/models/domain/resource_template.py index f727956811..aa213dedef 100644 --- a/api_app/models/domain/resource_template.py +++ b/api_app/models/domain/resource_template.py @@ -68,10 +68,11 @@ class ResourceTemplate(AzureTREModel): required: List[str] = Field(title="List of properties which must be provided") authorizedRoles: Optional[List[str]] = Field(default=[], title="If not empty, the user is required to have one of these roles to install the template") properties: Dict[str, Property] = Field(title="Template properties") + allOf: Optional[List[dict]] = Field(default=None, title="All Of", description="Used for conditionally showing and validating fields") actions: List[CustomAction] = Field(default=[], title="Template actions") customActions: List[CustomAction] = Field(default=[], title="Template custom actions") pipeline: Optional[Pipeline] = Field(default=None, title="Template pipeline to define updates to other resources") uiSchema: Optional[dict] = Field(default={}, title="Dict containing a uiSchema object, if any") # setting this to false means if extra, unexpected fields are supplied, the request is invalidated - additionalProperties: bool = Field(default=False, title="Prevent unspecified properties being applied") + unevaluatedProperties: bool = Field(default=False, title="Prevent unspecified properties being applied") diff --git a/api_app/resources/strings.py b/api_app/resources/strings.py index ae9bfc30a4..1cdbb547d2 100644 --- a/api_app/resources/strings.py +++ b/api_app/resources/strings.py @@ -41,15 +41,18 @@ API_CREATE_WORKSPACE_TEMPLATES = "Register workspace template" API_GET_WORKSPACE_TEMPLATES = "Get workspace templates" API_GET_WORKSPACE_TEMPLATE_BY_NAME = "Get workspace template by name" +API_GET_WORKSPACE_TEMPLATE_BY_NAME_AND_VERSION = "Get workspace template by name and version" API_CREATE_WORKSPACE_SERVICE_TEMPLATES = "Register workspace service template" API_GET_WORKSPACE_SERVICE_TEMPLATES = "Get workspace service templates" API_GET_WORKSPACE_SERVICE_TEMPLATES_IN_WORKSPACE = "Get workspace service templates (on workspace level)" # only returns templates that the authenticated user is authorized to use API_GET_WORKSPACE_SERVICE_TEMPLATE_BY_NAME = "Get workspace service template by name" +API_GET_WORKSPACE_SERVICE_TEMPLATE_BY_NAME_AND_VERSION = "Get workspace service template by name and version" API_CREATE_SHARED_SERVICE_TEMPLATES = "Register shared service template" API_GET_SHARED_SERVICE_TEMPLATES = "Get shared service templates" API_GET_SHARED_SERVICE_TEMPLATE_BY_NAME = "Get shared service template by name" +API_GET_SHARED_SERVICE_TEMPLATE_BY_NAME_AND_VERSION = "Get shared service template by name and version" API_GET_ALL_SHARED_SERVICES = "Get all shared services" API_GET_SHARED_SERVICE_BY_ID = "Get shared service by ID" @@ -62,6 +65,7 @@ API_GET_USER_RESOURCE_TEMPLATES = "Get user resource templates applicable to the workspace service template" API_GET_USER_RESOURCE_TEMPLATES_IN_WORKSPACE = "Get user resource templates applicable to the workspace service template (on workspace level)" # only returns templates that the authenticated user is authorized to use API_GET_USER_RESOURCE_TEMPLATE_BY_NAME = "Get user resource template by name and workspace service" +API_GET_USER_RESOURCE_TEMPLATE_BY_NAME_AND_VERSION = "Get user resource template by name and version and workspace service" # cost report API_GET_COSTS = "Get overall costs" @@ -131,6 +135,7 @@ WORKSPACE_SERVICE_TEMPLATE_DOES_NOT_EXIST = "Could not retrieve the workspace service template specified" TEMPLATE_DOES_NOT_EXIST = "Could not retrieve the 'current' template with this name" +TEMPLATE_VERSION_DOES_NOT_EXIST = "Could not retrieve the template with this name and version" NO_UNIQUE_CURRENT_FOR_TEMPLATE = "The template has multiple 'current' versions" SHARED_SERVICE_DOES_NOT_EXIST = "Shared service does not exist" diff --git a/api_app/service_bus/helpers.py b/api_app/service_bus/helpers.py index 6ccfde2f3b..0dfce468b9 100644 --- a/api_app/service_bus/helpers.py +++ b/api_app/service_bus/helpers.py @@ -49,7 +49,7 @@ def update_resource_for_step(operation_step: OperationStep, resource_repo: Resou if primary_resource.resourceType == ResourceType.UserResource: primary_parent_workspace_service = resource_repo.get_resource_by_id(primary_resource.parentWorkspaceServiceId) primary_parent_service_name = primary_parent_workspace_service.templateName - primary_template = resource_template_repo.get_current_template(primary_resource.templateName, primary_resource.resourceType, primary_parent_service_name) + primary_template = resource_template_repo.get_template_by_name_and_version(primary_resource.templateName, primary_resource.templateVersion, primary_resource.resourceType, primary_parent_service_name) # get the template step template_step = None @@ -117,7 +117,7 @@ def try_upgrade(resource_repo: ResourceRepository, resource_template_repo: Resou if resource_to_update.resourceType == ResourceType.UserResource: parent_service_name = resource_to_update["parentWorkspaceServiceId"] - resource_template_to_send = resource_template_repo.get_current_template(resource_to_update.templateName, resource_to_update.resourceType, parent_service_name) + resource_template_to_send = resource_template_repo.get_template_by_name_and_version(resource_to_update.templateName, resource_to_update.templateVersion, resource_to_update.resourceType, parent_service_name) # create the patch patch = ResourcePatch( diff --git a/api_app/services/aad_authentication.py b/api_app/services/aad_authentication.py index 40640bcfff..abf118eb79 100644 --- a/api_app/services/aad_authentication.py +++ b/api_app/services/aad_authentication.py @@ -55,8 +55,11 @@ async def __call__(self, request: Request) -> User: # as we have a workspace_id not given, try decoding token logging.debug("Workspace ID was provided. Getting Workspace API app registration") try: + # get the app reg id - which might be blank if the workspace hasn't fully created yet. + # if it's blank, don't use workspace auth, use core auth - and a TRE Admin can still get it app_reg_id = self._fetch_ws_app_reg_id_from_ws_id(request) - decoded_token = self._decode_token(token, app_reg_id) + if app_reg_id != "": + decoded_token = self._decode_token(token, app_reg_id) except HTTPException as h: raise h except Exception as e: @@ -112,7 +115,10 @@ def _fetch_ws_app_reg_id_from_ws_id(request: Request) -> str: workspace_id = request.path_params['workspace_id'] ws_repo = WorkspaceRepository(get_db_client_from_request(request)) workspace = ws_repo.get_workspace_by_id(workspace_id) - ws_app_reg_id = workspace.properties['client_id'] + + ws_app_reg_id = "" + if "client_id" in workspace.properties: + ws_app_reg_id = workspace.properties['client_id'] return ws_app_reg_id except EntityDoesNotExist as e: @@ -294,7 +300,7 @@ def _get_batch_users_by_role_assignments_body(self, roles_graph_data): # This method is called when you create a workspace and you already have an AAD App Registration # to link it to. You pass in the client_id and go and get the extra information you need from AAD - # If the client_id is `auto_create`, then these values will be written by Terraform. + # If the auth_type is `Automatic`, then these values will be written by Terraform. def _get_app_auth_info(self, client_id: str) -> dict: graph_data = self._get_app_sp_graph_data(client_id) if 'value' not in graph_data or len(graph_data['value']) == 0: @@ -363,13 +369,13 @@ def _get_identity_type(self, id: str) -> str: return object_info["@odata.type"] def extract_workspace_auth_information(self, data: dict) -> dict: - if "client_id" not in data: + if ("auth_type" not in data) or (data["auth_type"] != "Automatic" and "client_id" not in data): raise AuthConfigValidationError(strings.ACCESS_PLEASE_SUPPLY_CLIENT_ID) auth_info = {} # The user may want us to create the AAD workspace app and therefore they # don't know the client_id yet. - if data["client_id"] != "auto_create": + if data["auth_type"] != "Automatic": auth_info = self._get_app_auth_info(data["client_id"]) # Check we've get all our required roles diff --git a/api_app/services/schema_service.py b/api_app/services/schema_service.py index 5f4eb057e7..76a54dfe2f 100644 --- a/api_app/services/schema_service.py +++ b/api_app/services/schema_service.py @@ -53,6 +53,10 @@ def enrich_template(original_template, extra_properties, is_update: bool = False if "updateable" not in prop.keys() or prop["updateable"] is not True: prop["readOnly"] = True + # if there is an 'allOf' property which is empty, the validator fails - so remove the key + if "allOf" in template and template["allOf"] is None: + template.pop("allOf") + if is_workspace_scope: id_field = "workspace_id" else: diff --git a/api_app/tests_ma/test_api/test_routes/test_airlock.py b/api_app/tests_ma/test_api/test_routes/test_airlock.py index d9abe52b73..2e20232e9a 100644 --- a/api_app/tests_ma/test_api/test_routes/test_airlock.py +++ b/api_app/tests_ma/test_api/test_routes/test_airlock.py @@ -105,13 +105,13 @@ def sample_airlock_review_config() -> dict: return { "airlock_review_config": { "import": { - "workspace_id": IMPORT_WORKSPACE_ID, - "workspace_service_id": WORKSPACE_SERVICE_ID, - "user_resource_template_name": "tre-service-guacamole-import-reviewvm" + "import_vm_workspace_id": IMPORT_WORKSPACE_ID, + "import_vm_workspace_service_id": WORKSPACE_SERVICE_ID, + "import_vm_user_resource_template_name": "tre-service-guacamole-import-reviewvm" }, "export": { - "workspace_service_id": WORKSPACE_SERVICE_ID, - "user_resource_template_name": "tre-service-guacamole-export-reviewvm" + "export_vm_workspace_service_id": WORKSPACE_SERVICE_ID, + "export_vm_user_resource_template_name": "tre-service-guacamole-export-reviewvm" } } } diff --git a/api_app/tests_ma/test_db/test_repositories/test_resource_repository.py b/api_app/tests_ma/test_db/test_repositories/test_resource_repository.py index db859f8cd3..9c3b1dbb5b 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_resource_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_resource_repository.py @@ -164,15 +164,22 @@ def test_validate_input_against_template_raises_value_error_if_the_user_resource @patch("db.repositories.resources.ResourceRepository._get_enriched_template") def test_validate_input_against_template_raises_value_error_if_payload_is_invalid(enriched_template_mock, resource_repo, workspace_input): - enriched_template_mock.return_value = ResourceTemplate(id="123", - name="template1", - description="description", - version="0.1.0", - resourceType=ResourceType.Workspace, - current=True, - required=["display_name"], - properties={}, - customActions=[]).dict() + template_dict = ResourceTemplate( + id="123", + name="template1", + description="description", + version="0.1.0", + resourceType=ResourceType.Workspace, + current=True, + required=["display_name"], + properties={}, + customActions=[]).dict() + + # the enrich template method does this + template_dict.pop("allOf") + + enriched_template_mock.return_value = template_dict + # missing display name workspace_input = WorkspaceInCreate(templateName="template1") 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 e4707298cc..ca4a8ce677 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 @@ -194,7 +194,7 @@ def test_create_workspace_item_raises_value_error_if_template_is_invalid(validat def test_automatically_create_application_registration_returns_true(workspace_repo): - dictToTest = {"client_id": "auto_create"} + dictToTest = {"auth_type": "Automatic"} assert workspace_repo.automatically_create_application_registration(dictToTest) is True diff --git a/api_app/tests_ma/test_service_bus/test_resource_request_sender.py b/api_app/tests_ma/test_service_bus/test_resource_request_sender.py index a6f5679209..f26f4117a1 100644 --- a/api_app/tests_ma/test_service_bus/test_resource_request_sender.py +++ b/api_app/tests_ma/test_service_bus/test_resource_request_sender.py @@ -105,7 +105,7 @@ async def test_multi_step_document_sends_first_step( temp_workspace_service, basic_shared_service, ] - resource_template_repo.get_current_template.side_effect = [ + resource_template_repo.get_template_by_name_and_version.side_effect = [ multi_step_resource_template, basic_shared_service_template, ] diff --git a/api_app/tests_ma/test_services/test_aad_access_service.py b/api_app/tests_ma/test_services/test_aad_access_service.py index d1355ff8c1..367f80c36c 100644 --- a/api_app/tests_ma/test_services/test_aad_access_service.py +++ b/api_app/tests_ma/test_services/test_aad_access_service.py @@ -10,7 +10,7 @@ def test_extract_workspace__raises_error_if_client_id_not_available(): access_service = AzureADAuthorization() with pytest.raises(AuthConfigValidationError): - access_service.extract_workspace_auth_information(data={}) + access_service.extract_workspace_auth_information(data={"auth_type": "Manual"}) @patch("services.aad_authentication.AzureADAuthorization._get_app_auth_info", @@ -62,7 +62,7 @@ def test_extract_workspace__returns_sp_id_and_roles(get_app_sp_graph_data_mock): } access_service = AzureADAuthorization() - actual_auth_info = access_service.extract_workspace_auth_information(data={"client_id": "1234"}) + actual_auth_info = access_service.extract_workspace_auth_information(data={"auth_type": "Manual", "client_id": "1234"}) assert actual_auth_info == expected_auth_info diff --git a/e2e_tests/test_workspace_services.py b/e2e_tests/test_workspace_services.py index feca64e510..caee085409 100644 --- a/e2e_tests/test_workspace_services.py +++ b/e2e_tests/test_workspace_services.py @@ -83,7 +83,7 @@ async def test_create_guacamole_service_into_aad_workspace(verify) -> None: "display_name": "E2E test guacamole service", "description": "workspace for E2E AAD", "address_space_size": "small", - "client_id": "auto_create" + "auth_type": "Automatic" } } if config.TEST_WORKSPACE_APP_PLAN != "": diff --git a/templates/core/terraform/variables.tf b/templates/core/terraform/variables.tf index fc732e980f..35f56afbc8 100644 --- a/templates/core/terraform/variables.tf +++ b/templates/core/terraform/variables.tf @@ -61,7 +61,7 @@ variable "terraform_state_container_name" { variable "resource_processor_number_processes_per_instance" { type = string - default = "2" + default = "5" description = "The number of CPU processes to run the RP on per VM instance" } diff --git a/templates/workspaces/airlock-import-review/porter.yaml b/templates/workspaces/airlock-import-review/porter.yaml index cab8360497..5815fb5c75 100644 --- a/templates/workspaces/airlock-import-review/porter.yaml +++ b/templates/workspaces/airlock-import-review/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-workspace-airlock-import-review -version: 0.4.0 +version: 0.5.0 description: "A workspace to do Airlock Data Import Reviews for Azure TRE" dockerfile: Dockerfile.tmpl registry: azuretre @@ -60,6 +60,7 @@ parameters: type: string description: "The object id of the user that will be granted WorkspaceOwner after it is created." - name: client_id + default: "" type: string description: "The client id of the workspace in the identity provider. This value is typically provided to you diff --git a/templates/workspaces/airlock-import-review/template_schema.json b/templates/workspaces/airlock-import-review/template_schema.json index 715539699f..c7a593d9d5 100644 --- a/templates/workspaces/airlock-import-review/template_schema.json +++ b/templates/workspaces/airlock-import-review/template_schema.json @@ -4,66 +4,172 @@ "type": "object", "title": "Airlock Import Review Workspace", "description": "This workspace template is intended to conduct Airlock Data Import reviews from.", - "required": [], + "required": [ + "auth_type", + "address_space_size" + ], + "authorizedRoles": [], "properties": { - "aad_redirect_uris": { - "$id": "#/properties/aad_redirect_uris", - "type": "array", - "title": "AAD Redirect URIs", - "description": "Redirect URIs for the AAD app in auto_create mode", - "updateable": true, - "default": [], - "items": { - "title": "items", - "type": "object", - "required": [ - "name", - "value" - ], - "properties": { - "name": { - "title": "name", - "type": "string", - "description": "Redirect URI Name", - "examples": [ - "My Redirect URI" - ], - "pattern": "^.*$" - }, - "value": { - "title": "value", - "type": "string", - "description": "Redirect URI Value", - "examples": [ - "https://a-domain-name.com/oauth/" - ] - } - } - } + "shared_storage_quota": { + "type": "integer", + "title": "Shared Storage Quota", + "description": "Quota (in GB) to set for the VM Shared Storage." }, "app_service_plan_sku": { - "$id": "#/properties/app_service_plan_sku", "type": "string", + "title": "App Service Plan SKU", + "description": "The SKU that will be used when deploying an Azure App Service Plan.", + "default": "P1v3", "enum": [ "P1v3", "P1v2", "S1" - ], - "default": "P1v3", - "title": "App Service Plan SKU", - "description": "The SKU that will be used when deploying an Azure App Service Plan." + ] + }, + "address_space_size": { + "type": "string", + "title": "Address space size", + "description": "Network address size (small, medium, large or custom) to be used by the workspace.", + "default": "small", + "enum": [ + "small", + "medium", + "large", + "custom" + ] }, - "enable_airlock": { - "$id": "#/properties/enable_airlock", - "type": "boolean", - "default": true, - "title": "Enable Airlock", - "description": "If enabled, allows imports and exports for the workspace." + "auth_type": { + "type": "string", + "title": "Workspace Authentication Type", + "description": "", + "default": "Automatic", + "enum": [ + "Automatic", + "Manual" + ], + "updateable": true } }, + "allOf": [ + { + "if": { + "properties": { + "address_space_size": { + "enum": [ + "custom" + ] + } + }, + "required": [ + "address_space_size" + ] + }, + "then": { + "properties": { + "address_space": { + "type": "string", + "title": "Address space", + "description": "Network address space to be used by the workspace if 'Address space size' is custom." + } + }, + "required": [ + "address_space" + ] + } + }, + { + "if": { + "properties": { + "auth_type": { + "const": "Manual" + } + }, + "required": [ + "auth_type" + ] + }, + "then": { + "properties": { + "client_id": { + "type": "string", + "title": "Application (Client) ID", + "description": "The AAD Application Registration ID for the workspace." + }, + "client_secret": { + "type": "string", + "title": "Application (Client) Secret", + "description": "The AAD Application Registration secret for the workspace. This value will be stored in the Workspace Key Vault.", + "sensitive": true + } + }, + "required": [ + "client_id" + ] + }, + "else": { + "properties": { + "create_aad_groups": { + "type": "boolean", + "title": "Create AAD Groups for each workspace role", + "description": "Create AAD Groups for the workspace roles. If this is set to true, the workspace will create new AAD Groups.", + "default": false + }, + "aad_redirect_uris": { + "type": "array", + "title": "AAD Redirect URIs", + "description": "Redirect URIs for the AAD app in Automatic Auth mode", + "items": { + "title": "items", + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "title": "name", + "type": "string", + "description": "Redirect URI Name", + "examples": [ + "My Redirect URI" + ], + "pattern": "^.*$" + }, + "value": { + "title": "value", + "type": "string", + "description": "Redirect URI Value", + "examples": [ + "https://a-domain-name.com/oauth/" + ] + } + } + } + } + } + } + } + ], + "actions": [], + "customActions": [], + "pipeline": null, "uiSchema": { "aad_redirect_uris": { "classNames": "tre-hidden" - } + }, + "ui:order": [ + "display_name", + "description", + "overview", + "shared_storage_quota", + "app_service_plan_sku", + "address_space_size", + "address_space", + "auth_type", + "create_aad_groups", + "client_id", + "client_secret", + "*" + ] } } diff --git a/templates/workspaces/base/porter.yaml b/templates/workspaces/base/porter.yaml index de93e0a486..08ff63bb26 100644 --- a/templates/workspaces/base/porter.yaml +++ b/templates/workspaces/base/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-workspace-base -version: 0.4.1 +version: 0.5.0 description: "A base Azure TRE workspace" dockerfile: Dockerfile.tmpl registry: azuretre @@ -68,6 +68,7 @@ parameters: description: "The object id of the user that will be granted WorkspaceOwner after it is created." - name: client_id type: string + default: "" description: "The client id of the workspace in the identity provider. This value is typically provided to you when you create the ws application" diff --git a/templates/workspaces/base/template_schema.json b/templates/workspaces/base/template_schema.json index cb0a23bc7c..bbb9521533 100644 --- a/templates/workspaces/base/template_schema.json +++ b/templates/workspaces/base/template_schema.json @@ -4,142 +4,282 @@ "type": "object", "title": "Base Workspace", "description": "This workspace template is the foundation for TRE workspaces and workspace services.", - "required": [], + "required": [ + "auth_type", + "address_space_size" + ], + "authorizedRoles": [], "properties": { "shared_storage_quota": { - "$id": "#/properties/shared_storage_quota", "type": "integer", "title": "Shared Storage Quota", "description": "Quota (in GB) to set for the VM Shared Storage." }, + "enable_airlock": { + "type": "boolean", + "title": "Enable Airlock", + "description": "Allow safe import and export to the workspace", + "default": true, + "updateable": true + }, "app_service_plan_sku": { - "$id": "#/properties/app_service_plan_sku", "type": "string", + "title": "App Service Plan SKU", + "description": "The SKU that will be used when deploying an Azure App Service Plan.", + "default": "P1v3", "enum": [ "P1v3", "P1v2", "S1" - ], - "default": "P1v3", - "title": "App Service Plan SKU", - "description": "The SKU that will be used when deploying an Azure App Service Plan." + ] }, - "enable_airlock": { - "$id": "#/properties/enable_airlock", - "type": "boolean", - "default": true, - "title": "Enable Airlock", - "description": "If enabled, allows imports and exports for the workspace." + "address_space_size": { + "type": "string", + "title": "Address space size", + "description": "Network address size (small, medium, large or custom) to be used by the workspace.", + "default": "small", + "enum": [ + "small", + "medium", + "large", + "custom" + ] }, - "airlock_review_config": { - "$id": "#properties/airlock_review_config", - "type": "object", - "title": "Airlock Review Config", - "description": "Configuration for Airlock Review feature. Needs to be set up after workspace creation", - "updateable": true, - "properties": { - "import": { - "$id": "#/properties/airlock_review_config/import", - "title": "Airlock Import Review configuration", - "type": "object", - "description": "Configure Airlock Review for Import Request", - "required": [ - "workspace_id", - "workspace_service_id", - "user_resource_template_name" - ], - "properties": { - "workspace_id": { - "title": "workspace_id", - "type": "string", - "description": "ID for Import Review workspace" - }, - "workspace_service_id": { - "title": "workspace_service_id", - "type": "string", - "description": "ID for Workspace Service ID where to deploy Review user resources" - }, - "user_resource_template_name": { - "title": "user_resource_template_name", - "type": "string", - "description": "Template Name for User Resource for reviewing Import Requests", - "examples": [ - "tre-service-guacamole-import-reviewvm" - ] - } + "auth_type": { + "type": "string", + "title": "Workspace Authentication Type", + "description": "", + "default": "Automatic", + "enum": [ + "Automatic", + "Manual" + ], + "updateable": true + } + }, + "allOf": [ + { + "if": { + "properties": { + "enable_airlock": { + "const": true } }, - "export": { - "title": "Airlock Export Review configuration", - "type": "object", - "description": "Configure Airlock Review for Export Request", - "required": [ - "workspace_service_id", - "user_resource_template_name" - ], - "properties": { - "workspace_service_id": { - "title": "workspace_service_id", - "type": "string", - "description": "ID for Workspace Service ID where to deploy Review user resources" - }, - "user_resource_template_name": { - "title": "user_resource_template_name", - "type": "string", - "description": "Template Name for User Resource for reviewing Export Requests", - "examples": [ - "tre-service-guacamole-export-reviewvm" - ] + "required": [ + "enable_airlock" + ] + }, + "then": { + "properties": { + "configure_review_vms": { + "type": "boolean", + "title": "Configure Review VMs", + "description": "Allow TRE to automatically create and delete review VMs for airlock approvals", + "default": false + } + } + } + }, + { + "if": { + "properties": { + "enable_airlock": { + "const": true + }, + "configure_review_vms": { + "const": true + } + }, + "required": [ + "enable_airlock", + "configure_review_vms" + ] + }, + "then": { + "properties": { + "airlock_review_config": { + "type": "object", + "title": "Airlock Review Config", + "default": null, + "description": "Configuration for Airlock Review feature. Needs to be set up after workspace creation", + "properties": { + "import": { + "title": "Import VM Settings", + "required": [ + "import_vm_workspace_id", + "import_vm_workspace_service_id", + "import_vm_user_resource_template_name" + ], + "properties": { + "import_vm_workspace_id": { + "title": "Import Workspace ID", + "type": "string", + "description": "ID for Import Review workspace" + }, + "import_vm_workspace_service_id": { + "title": "Import Workspace Service ID", + "type": "string", + "description": "ID for Workspace Service ID where to deploy Review user resources" + }, + "import_vm_user_resource_template_name": { + "title": "Import VM User Resource Template Name", + "type": "string", + "description": "Template Name for User Resource for reviewing Import Requests", + "examples": [ + "tre-service-guacamole-import-reviewvm" + ] + } + } + }, + "export": { + "title": "Export VM Settings", + "required": [ + "export_vm_workspace_service_id", + "export_vm_user_resource_template_name" + ], + "properties": { + "export_vm_workspace_service_id": { + "title": "Export Workspace Service ID", + "type": "string", + "description": "ID for Workspace Service ID where to deploy Review user resources" + }, + "export_vm_user_resource_template_name": { + "title": "Export VM User Resource Template Name", + "type": "string", + "description": "Template Name for User Resource for reviewing Export Requests", + "examples": [ + "tre-service-guacamole-export-reviewvm" + ] + } + } + } } } } } }, - "aad_redirect_uris": { - "$id": "#/properties/aad_redirect_uris", - "type": "array", - "title": "AAD Redirect URIs", - "description": "Redirect URIs for the AAD app in auto_create mode", - "updateable": true, - "default": [], - "items": { - "title": "items", - "type": "object", + { + "if": { + "properties": { + "address_space_size": { + "enum": [ + "custom" + ] + } + }, "required": [ - "name", - "value" - ], + "address_space_size" + ] + }, + "then": { "properties": { - "name": { - "title": "name", + "address_space": { "type": "string", - "description": "Redirect URI Name", - "examples": [ - "My Redirect URI" - ], - "pattern": "^.*$" + "title": "Address space", + "description": "Network address space to be used by the workspace if 'Address space size' is custom." + } + }, + "required": [ + "address_space" + ] + } + }, + { + "if": { + "properties": { + "auth_type": { + "const": "Manual" + } + }, + "required": [ + "auth_type" + ] + }, + "then": { + "properties": { + "client_id": { + "type": "string", + "title": "Application (Client) ID", + "description": "The AAD Application Registration ID for the workspace." }, - "value": { - "title": "value", + "client_secret": { "type": "string", - "description": "Redirect URI Value", - "examples": [ - "https://a-domain-name.com/oauth/" - ] + "title": "Application (Client) Secret", + "description": "The AAD Application Registration secret for the workspace. This value will be stored in the Workspace Key Vault.", + "sensitive": true + } + }, + "required": [ + "client_id" + ] + }, + "else": { + "properties": { + "create_aad_groups": { + "type": "boolean", + "title": "Create AAD Groups for each workspace role", + "description": "Create AAD Groups for the workspace roles. If this is set to true, the workspace will create new AAD Groups.", + "default": false + }, + "aad_redirect_uris": { + "type": "array", + "title": "AAD Redirect URIs", + "description": "Redirect URIs for the AAD app in Automatic Auth mode", + "items": { + "title": "items", + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "title": "name", + "type": "string", + "description": "Redirect URI Name", + "examples": [ + "My Redirect URI" + ], + "pattern": "^.*$" + }, + "value": { + "title": "value", + "type": "string", + "description": "Redirect URI Value", + "examples": [ + "https://a-domain-name.com/oauth/" + ] + } + } + } } } } - }, - "create_aad_groups": { - "type": "boolean", - "title": "Create AAD Groups for each workspace role", - "description": "Create AAD Groups for the workspace roles, requires `auto_create`. If this is set to true, the workspace will create new AAD Groups.", - "default": false } - }, + ], + "actions": [], + "customActions": [], + "pipeline": null, "uiSchema": { "aad_redirect_uris": { "classNames": "tre-hidden" - } + }, + "ui:order": [ + "display_name", + "description", + "overview", + "shared_storage_quota", + "app_service_plan_sku", + "address_space_size", + "address_space", + "auth_type", + "create_aad_groups", + "client_id", + "client_secret", + "enable_airlock", + "configure_review_vms", + "airlock_review_config", + "*" + ] } } diff --git a/templates/workspaces/unrestricted/porter.yaml b/templates/workspaces/unrestricted/porter.yaml index 539dbc5566..a631a74c31 100644 --- a/templates/workspaces/unrestricted/porter.yaml +++ b/templates/workspaces/unrestricted/porter.yaml @@ -1,6 +1,6 @@ --- name: tre-workspace-unrestricted -version: 0.2.0 +version: 0.5.0 description: "A base Azure TRE workspace" dockerfile: Dockerfile.tmpl registry: azuretre @@ -64,6 +64,7 @@ parameters: description: "The object id of the user that will be granted WorkspaceOwner after it is created." - name: client_id type: string + default: "" description: "The client id of the workspace in the identity provider. This value is typically provided to you when you create the ws application" diff --git a/templates/workspaces/unrestricted/template_schema.json b/templates/workspaces/unrestricted/template_schema.json index 1070883728..9df146c0d3 100644 --- a/templates/workspaces/unrestricted/template_schema.json +++ b/templates/workspaces/unrestricted/template_schema.json @@ -4,66 +4,282 @@ "type": "object", "title": "Unrestricted Workspace", "description": "Workspace with unrestricted access to the Internet", - "required": [], + "required": [ + "auth_type", + "address_space_size" + ], + "authorizedRoles": [], "properties": { "shared_storage_quota": { - "$id": "#/properties/shared_storage_quota", "type": "integer", "title": "Shared Storage Quota", "description": "Quota (in GB) to set for the VM Shared Storage." }, - "aad_redirect_uris": { - "$id": "#/properties/aad_redirect_uris", - "type": "array", - "title": "AAD Redirect URIs", - "description": "Redirect URIs for the AAD app in auto_create mode", - "updateable": true, - "default": [], - "items": { - "title": "items", - "type": "object", - "required": [ - "name", - "value" - ], - "properties": { - "name": { - "title": "name", - "type": "string", - "description": "Redirect URI Name", - "examples": [ - "My Redirect URI" - ], - "pattern": "^.*$" - }, - "value": { - "title": "value", - "type": "string", - "description": "Redirect URI Value", - "examples": [ - "https://a-domain-name.com/oauth/" - ] - } - } - } + "enable_airlock": { + "type": "boolean", + "title": "Enable Airlock", + "description": "Allow safe import and export to the workspace", + "default": true, + "updateable": true }, "app_service_plan_sku": { - "$id": "#/properties/app_service_plan_sku", "type": "string", + "title": "App Service Plan SKU", + "description": "The SKU that will be used when deploying an Azure App Service Plan.", + "default": "P1v3", "enum": [ "P1v3", "P1v2", "S1" + ] + }, + "address_space_size": { + "type": "string", + "title": "Address space size", + "description": "Network address size (small, medium, large or custom) to be used by the workspace.", + "default": "small", + "enum": [ + "small", + "medium", + "large", + "custom" + ] + }, + "auth_type": { + "type": "string", + "title": "Workspace Authentication Type", + "description": "", + "default": "Automatic", + "enum": [ + "Automatic", + "Manual" ], - "default": "P1v3", - "title": "App Service Plan SKU", - "description": "The SKU that will be used when deploying an Azure App Service Plan." + "updateable": true } }, + "allOf": [ + { + "if": { + "properties": { + "enable_airlock": { + "const": true + } + }, + "required": [ + "enable_airlock" + ] + }, + "then": { + "properties": { + "configure_review_vms": { + "type": "boolean", + "title": "Configure Review VMs", + "description": "Allow TRE to automatically create and delete review VMs for airlock approvals", + "default": false + } + } + } + }, + { + "if": { + "properties": { + "enable_airlock": { + "const": true + }, + "configure_review_vms": { + "const": true + } + }, + "required": [ + "enable_airlock", + "configure_review_vms" + ] + }, + "then": { + "properties": { + "airlock_review_config": { + "type": "object", + "title": "Airlock Review Config", + "default": null, + "description": "Configuration for Airlock Review feature. Needs to be set up after workspace creation", + "properties": { + "import": { + "title": "Import VM Settings", + "required": [ + "import_vm_workspace_id", + "import_vm_workspace_service_id", + "import_vm_user_resource_template_name" + ], + "properties": { + "import_vm_workspace_id": { + "title": "Import Workspace ID", + "type": "string", + "description": "ID for Import Review workspace" + }, + "import_vm_workspace_service_id": { + "title": "Import Workspace Service ID", + "type": "string", + "description": "ID for Workspace Service ID where to deploy Review user resources" + }, + "import_vm_user_resource_template_name": { + "title": "Import VM User Resource Template Name", + "type": "string", + "description": "Template Name for User Resource for reviewing Import Requests", + "examples": [ + "tre-service-guacamole-import-reviewvm" + ] + } + } + }, + "export": { + "title": "Export VM Settings", + "required": [ + "export_vm_workspace_service_id", + "export_vm_user_resource_template_name" + ], + "properties": { + "export_vm_workspace_service_id": { + "title": "Export Workspace Service ID", + "type": "string", + "description": "ID for Workspace Service ID where to deploy Review user resources" + }, + "export_vm_user_resource_template_name": { + "title": "Export VM User Resource Template Name", + "type": "string", + "description": "Template Name for User Resource for reviewing Export Requests", + "examples": [ + "tre-service-guacamole-export-reviewvm" + ] + } + } + } + } + } + } + } + }, + { + "if": { + "properties": { + "address_space_size": { + "enum": [ + "custom" + ] + } + }, + "required": [ + "address_space_size" + ] + }, + "then": { + "properties": { + "address_space": { + "type": "string", + "title": "Address space", + "description": "Network address space to be used by the workspace if 'Address space size' is custom." + } + }, + "required": [ + "address_space" + ] + } + }, + { + "if": { + "properties": { + "auth_type": { + "const": "Manual" + } + }, + "required": [ + "auth_type" + ] + }, + "then": { + "properties": { + "client_id": { + "type": "string", + "title": "Application (Client) ID", + "description": "The AAD Application Registration ID for the workspace." + }, + "client_secret": { + "type": "string", + "title": "Application (Client) Secret", + "description": "The AAD Application Registration secret for the workspace. This value will be stored in the Workspace Key Vault.", + "sensitive": true + } + }, + "required": [ + "client_id" + ] + }, + "else": { + "properties": { + "create_aad_groups": { + "type": "boolean", + "title": "Create AAD Groups for each workspace role", + "description": "Create AAD Groups for the workspace roles. If this is set to true, the workspace will create new AAD Groups.", + "default": false + }, + "aad_redirect_uris": { + "type": "array", + "title": "AAD Redirect URIs", + "description": "Redirect URIs for the AAD app in Automatic Auth mode", + "items": { + "title": "items", + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "title": "name", + "type": "string", + "description": "Redirect URI Name", + "examples": [ + "My Redirect URI" + ], + "pattern": "^.*$" + }, + "value": { + "title": "value", + "type": "string", + "description": "Redirect URI Value", + "examples": [ + "https://a-domain-name.com/oauth/" + ] + } + } + } + } + } + } + } + ], + "actions": [], + "customActions": [], "uiSchema": { "aad_redirect_uris": { "classNames": "tre-hidden" - } + }, + "ui:order": [ + "display_name", + "description", + "overview", + "shared_storage_quota", + "app_service_plan_sku", + "address_space_size", + "address_space", + "auth_type", + "create_aad_groups", + "client_id", + "client_secret", + "enable_airlock", + "configure_review_vms", + "airlock_review_config", + "*" + ] }, "pipeline": { "install": [ diff --git a/ui/app/src/App.scss b/ui/app/src/App.scss index 7f721cfce9..448624dc7d 100644 --- a/ui/app/src/App.scss +++ b/ui/app/src/App.scss @@ -54,6 +54,7 @@ code { .tre-notifications-dismiss { text-align: right; + padding-top: 10px; } ul.tre-notifications-list { diff --git a/ui/app/src/components/shared/ResourceCard.tsx b/ui/app/src/components/shared/ResourceCard.tsx index 67a4e851ba..b38df33654 100644 --- a/ui/app/src/components/shared/ResourceCard.tsx +++ b/ui/app/src/components/shared/ResourceCard.tsx @@ -74,7 +74,7 @@ export const ResourceCard: React.FunctionComponent = (props: { - props.resource.resourceType === ResourceType.Workspace && props.resource.properties.client_id === "auto_create" ? + props.resource.resourceType === ResourceType.Workspace && !props.resource.properties.scope_id ? // no scope id? no auth has been setup. {props.resource.properties.display_name} : { props.selectResource && props.selectResource(props.resource); return false }} style={headerLinkStyles}>{props.resource.properties.display_name} diff --git a/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx b/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx index 10beb481a2..baed955144 100644 --- a/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx +++ b/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx @@ -31,9 +31,7 @@ export const ResourceForm: React.FunctionComponent = (props: const getFullTemplate = async () => { try { // Get the full resource template containing the required parameters - const templateResponse = (await apiCall(props.updateResource ? `${props.templatePath}?is_update=true` : props.templatePath, HttpMethod.Get)) as ResourceTemplate; - - console.log("raw", templateResponse); + const templateResponse = (await apiCall(props.updateResource ? `${props.templatePath}/${props.updateResource.templateVersion}?is_update=true` : props.templatePath, HttpMethod.Get)) as ResourceTemplate; // if it's an update, populate the form with the props that are available in the template if (props.updateResource) { @@ -41,14 +39,13 @@ export const ResourceForm: React.FunctionComponent = (props: for (let prop in templateResponse.properties) { d[prop] = props.updateResource?.properties[prop]; } - setFormData(d); + setFormData(props.updateResource.properties); } const sanitisedTemplate = sanitiseTemplateForRJSF(templateResponse); - console.log("sanitised", sanitisedTemplate); setTemplate(sanitisedTemplate); setLoading(LoadingState.Ok); - } catch (err: any){ + } catch (err: any) { err.userMessage = "Error retrieving resource template"; setApiError(err); setLoading(LoadingState.Error); @@ -61,23 +58,69 @@ export const ResourceForm: React.FunctionComponent = (props: } }, [apiCall, props.templatePath, template, props.updateResource]); + const manuallyParseDataPayload = (data: any, template: ResourceTemplate): any => { + + // flatten all the nested properties from across the template into a basic array we can iterate easily + let allProps = {} as any; + + const recurseTemplate = (templateFragment: any) => { + Object.keys(templateFragment).forEach((key) => { + if (key === "properties") { + Object.keys(templateFragment[key]).forEach((prop) => { + allProps[prop] = templateFragment[key][prop]; + }); + } + if (typeof (templateFragment[key]) === "object" && key !== "if") { + recurseTemplate(templateFragment[key]); + } + }) + } + + recurseTemplate(template); + + // strip out the properties that are not on-screen at time of sending. + // if the properties aren't on the screen it means they're in conditional parts of the template and shouldn't be being sent + let onScreenPropLabelElements = document.getElementsByClassName('rjsf')[0].getElementsByClassName('ms-Label'); + let checkBoxes = document.getElementsByClassName('rjsf')[0].getElementsByClassName('ms-Checkbox-text'); + let labels: Array = [] + for (let i = 0; i < onScreenPropLabelElements.length; i++) { + labels.push((onScreenPropLabelElements[i] as any)['outerText']); + } + for (let i = 0; i < checkBoxes.length; i++) { + labels.push((checkBoxes[i] as any)['outerText']); + } + + // iterate the data payload + for (let prop in data) { + // if the prop isn't in the template, or it's readOnly, delete it + if (!allProps[prop] || allProps[prop].readOnly === true) { + delete data[prop]; + continue; + } + + // if it's not onscreen, delete it + let title = allProps[prop].title || prop; + if (!labels.includes(title)){ + delete data[prop] + } + } + + console.log("Sending payload", data) + return data; + } + const createUpdateResource = async (formData: any) => { + + let data = manuallyParseDataPayload(formData, template); + setSendingData(true); let response; - try - { + try { if (props.updateResource) { - // only send the properties we're allowed to send - let d: any = {} - for (let prop in template.properties) { - if (!template.properties[prop].readOnly) d[prop] = formData[prop]; - } - console.log("patching resource", d); let wsAuth = props.updateResource.resourceType === ResourceType.WorkspaceService || props.updateResource.resourceType === ResourceType.UserResource; - response = await apiCall(props.updateResource.resourcePath, HttpMethod.Patch, wsAuth ? props.workspaceApplicationIdURI : undefined, { properties: d }, ResultType.JSON, undefined, undefined, props.updateResource._etag); + response = await apiCall(props.updateResource.resourcePath, HttpMethod.Patch, wsAuth ? props.workspaceApplicationIdURI : undefined, { properties: data }, ResultType.JSON, undefined, undefined, props.updateResource._etag); } else { - const resource = { templateName: props.templateName, properties: formData }; - console.log(resource); + const resource = { templateName: props.templateName, properties: data }; response = await apiCall(props.resourcePath, HttpMethod.Post, props.workspaceApplicationIdURI, resource, ResultType.JSON); } @@ -98,7 +141,7 @@ export const ResourceForm: React.FunctionComponent = (props: } // if no specific order has been set, set a generic one with the primary fields at the top - if (!uiSchema["ui:order"] || uiSchema["ui:order"].length === 0){ + if (!uiSchema["ui:order"] || uiSchema["ui:order"].length === 0) { uiSchema["ui:order"] = [ "display_name", "description", diff --git a/ui/app/src/index.tsx b/ui/app/src/index.tsx index dd524e6520..f80733a32a 100644 --- a/ui/app/src/index.tsx +++ b/ui/app/src/index.tsx @@ -11,7 +11,7 @@ import { store } from './store/store'; // Inject some global styles mergeStyles({ - ':global(body,html,#root)': { + ':global(body,html)': { margin: 0, padding: 0, height: '100vh', diff --git a/ui/app/src/models/operation.ts b/ui/app/src/models/operation.ts index 9d95c0da14..b12e51db41 100644 --- a/ui/app/src/models/operation.ts +++ b/ui/app/src/models/operation.ts @@ -35,15 +35,23 @@ export const awaitingStates = [ "awaiting_action" ] -export const completedStates = [ +export const successStates = [ "deployed", - "deleted", "updated", - "failed", + "deleted", + "action_succeeded" +] + +export const failedStates = [ + "deployment_failed", "deleting_failed", "updating_failed", - "action_succeeded", - "action_failed" + "action_failed", +] + +export const completedStates = [ + ...failedStates, + ...successStates ] export const inProgressStates = [ @@ -55,19 +63,7 @@ export const inProgressStates = [ "pipeline_running" ] -export const failedStates = [ - "deployment_failed", - "deleting_failed", - "updating_failed", - "action_failed", -] -export const successStates = [ - "deployed", - "updated", - "deleted", - "action_succeeded" -] export const actionsDisabledStates = [ ...inProgressStates, From 4f317d22b373eb783a9e7ff5a204ca66dbeae9ad Mon Sep 17 00:00:00 2001 From: David Moore Date: Mon, 31 Oct 2022 20:45:00 +0000 Subject: [PATCH 05/10] unwound processors change --- templates/core/terraform/variables.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/core/terraform/variables.tf b/templates/core/terraform/variables.tf index 35f56afbc8..fc732e980f 100644 --- a/templates/core/terraform/variables.tf +++ b/templates/core/terraform/variables.tf @@ -61,7 +61,7 @@ variable "terraform_state_container_name" { variable "resource_processor_number_processes_per_instance" { type = string - default = "5" + default = "2" description = "The number of CPU processes to run the RP on per VM instance" } From 7c0601b900f4c500c8f71f2602e304525ac5a1d8 Mon Sep 17 00:00:00 2001 From: David Moore Date: Mon, 31 Oct 2022 21:13:14 +0000 Subject: [PATCH 06/10] unwound template version changes --- api_app/api/routes/api.py | 11 +++-------- api_app/api/routes/resource_helpers.py | 11 ----------- api_app/api/routes/shared_service_templates.py | 11 +---------- api_app/api/routes/user_resource_templates.py | 8 +------- .../api/routes/workspace_service_templates.py | 8 +------- api_app/api/routes/workspace_templates.py | 16 +++++----------- api_app/resources/strings.py | 7 +------ api_app/service_bus/helpers.py | 4 ++-- .../test_resource_request_sender.py | 2 +- 9 files changed, 15 insertions(+), 63 deletions(-) diff --git a/api_app/api/routes/api.py b/api_app/api/routes/api.py index 985395d596..4da2ab0183 100644 --- a/api_app/api/routes/api.py +++ b/api_app/api/routes/api.py @@ -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, metadata + shared_services, shared_service_templates, migrations, costs, airlock, operations from core import config core_tags_metadata = [ @@ -30,19 +30,14 @@ 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"]) - -# NOTE: User Resource Templates need to be registered before workspace service templates to cater for the `/{service_template_name}/user-resources` -# Else when you call `/{service_template_name}/user-resources` it will call the workspace service endpoint, taking "user-resouces" as the version number. -core_router.include_router(user_resource_templates.user_resource_templates_core_router, tags=["user resource 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"]) core_router.include_router(shared_service_templates.shared_service_templates_core_router, tags=["shared service templates"]) core_router.include_router(shared_services.shared_services_router, tags=["shared services"]) core_router.include_router(operations.operations_router, tags=["operations"]) @@ -82,7 +77,7 @@ async def get_swagger(request: Request): init_oauth={ "usePkceWithAuthorizationCodeGrant": True, "clientId": config.SWAGGER_UI_CLIENT_ID, - "scopes": ["openid", "offline_access", config.API_ROOT_SCOPE] + "scopes": ["openid", "offline_access", f"api://{config.API_CLIENT_ID}/user_impersonation"] } ) diff --git a/api_app/api/routes/resource_helpers.py b/api_app/api/routes/resource_helpers.py index 4a9934c7b8..fd2680d4f9 100644 --- a/api_app/api/routes/resource_helpers.py +++ b/api_app/api/routes/resource_helpers.py @@ -125,16 +125,5 @@ def get_current_template_by_name(template_name: str, template_repo: ResourceTemp raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=strings.STATE_STORE_ENDPOINT_NOT_RESPONDING) -def get_template_by_name_and_version(template_name: str, version: str, template_repo: ResourceTemplateRepository, resource_type: ResourceType, parent_service_template_name: str = "", is_update: bool = False) -> dict: - try: - template = template_repo.get_template_by_name_and_version(template_name, version, resource_type, parent_service_template_name) - return template_repo.enrich_template(template, is_update=is_update) - except EntityDoesNotExist: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.TEMPLATE_VERSION_DOES_NOT_EXIST) - except Exception as e: - logging.debug(e) - raise HTTPException(status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail=strings.STATE_STORE_ENDPOINT_NOT_RESPONDING) - - def get_timestamp() -> float: return datetime.utcnow().timestamp() diff --git a/api_app/api/routes/shared_service_templates.py b/api_app/api/routes/shared_service_templates.py index de79ddf1c7..917f1ec88d 100644 --- a/api_app/api/routes/shared_service_templates.py +++ b/api_app/api/routes/shared_service_templates.py @@ -9,7 +9,7 @@ from models.schemas.shared_service_template import SharedServiceTemplateInCreate, SharedServiceTemplateInResponse from resources import strings from services.authentication import get_current_admin_user, get_current_tre_user_or_tre_admin -from api.routes.resource_helpers import get_current_template_by_name, get_template_by_name_and_version +from .resource_helpers import get_current_template_by_name shared_service_templates_core_router = APIRouter(dependencies=[Depends(get_current_tre_user_or_tre_admin)]) @@ -30,15 +30,6 @@ async def get_current_shared_service_template_by_name(shared_service_template_na raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.SHARED_SERVICE_TEMPLATE_DOES_NOT_EXIST) -@shared_service_templates_core_router.get("/shared-service-templates/{shared_service_template_name}/{version}", response_model=SharedServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_GET_SHARED_SERVICE_TEMPLATE_BY_NAME_AND_VERSION, dependencies=[Depends(get_current_tre_user_or_tre_admin)]) -async def get_shared_service_template_by_name_and_version(shared_service_template_name: str, version, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> SharedServiceTemplateInResponse: - try: - template = get_template_by_name_and_version(shared_service_template_name, version, template_repo, ResourceType.SharedService, is_update=is_update) - return parse_obj_as(SharedServiceTemplateInResponse, template) - except EntityDoesNotExist: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.SHARED_SERVICE_TEMPLATE_DOES_NOT_EXIST) - - @shared_service_templates_core_router.post("/shared-service-templates", status_code=status.HTTP_201_CREATED, response_model=SharedServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_SHARED_SERVICE_TEMPLATES, dependencies=[Depends(get_current_admin_user)]) async def register_shared_service_template(template_input: SharedServiceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInResponse: try: diff --git a/api_app/api/routes/user_resource_templates.py b/api_app/api/routes/user_resource_templates.py index 2bdf1dbeb1..f2cc9859b2 100644 --- a/api_app/api/routes/user_resource_templates.py +++ b/api_app/api/routes/user_resource_templates.py @@ -4,7 +4,7 @@ from api.dependencies.database import get_repository from api.dependencies.workspace_service_templates import get_workspace_service_template_by_name_from_path -from api.routes.resource_helpers import get_current_template_by_name, get_template_by_name_and_version +from api.routes.workspace_templates import get_current_template_by_name from db.errors import EntityVersionExist from db.repositories.resource_templates import ResourceTemplateRepository from models.domain.resource import ResourceType @@ -29,12 +29,6 @@ async def get_current_user_resource_template_by_name(service_template_name: str, return parse_obj_as(UserResourceTemplateInResponse, template) -@user_resource_templates_core_router.get("/workspace-service-templates/{service_template_name}/user-resource-templates/{user_resource_template_name}/{version}", response_model=UserResourceTemplateInResponse, response_model_exclude_none=True, name=strings.API_GET_USER_RESOURCE_TEMPLATE_BY_NAME_AND_VERSION, dependencies=[Depends(get_current_tre_user_or_tre_admin)]) -async def get_user_resource_template_by_name_and_version(service_template_name: str, user_resource_template_name: str, version, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> UserResourceTemplateInResponse: - template = get_template_by_name_and_version(user_resource_template_name, version, template_repo, ResourceType.UserResource, service_template_name, is_update=is_update) - return parse_obj_as(UserResourceTemplateInResponse, template) - - @user_resource_templates_core_router.post("/workspace-service-templates/{service_template_name}/user-resource-templates", status_code=status.HTTP_201_CREATED, response_model=UserResourceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_USER_RESOURCE_TEMPLATES, dependencies=[Depends(get_current_admin_user)]) async def register_user_resource_template(template_input: UserResourceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository)), workspace_service_template=Depends(get_workspace_service_template_by_name_from_path)) -> UserResourceTemplateInResponse: try: diff --git a/api_app/api/routes/workspace_service_templates.py b/api_app/api/routes/workspace_service_templates.py index b1f5d2da39..44601f8bc0 100644 --- a/api_app/api/routes/workspace_service_templates.py +++ b/api_app/api/routes/workspace_service_templates.py @@ -2,7 +2,7 @@ from pydantic import parse_obj_as from api.dependencies.database import get_repository -from api.routes.resource_helpers import get_current_template_by_name, get_template_by_name_and_version +from api.routes.workspace_templates import get_current_template_by_name from db.errors import EntityVersionExist from db.repositories.resource_templates import ResourceTemplateRepository from models.domain.resource import ResourceType @@ -27,12 +27,6 @@ async def get_current_workspace_service_template_by_name(service_template_name: return parse_obj_as(WorkspaceServiceTemplateInResponse, template) -@workspace_service_templates_core_router.get("/workspace-service-templates/{service_template_name}/{version}", response_model=WorkspaceServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_GET_WORKSPACE_SERVICE_TEMPLATE_BY_NAME_AND_VERSION, dependencies=[Depends(get_current_tre_user_or_tre_admin)]) -async def get_workspace_service_template_by_name_and_version(service_template_name: str, version, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> WorkspaceServiceTemplateInResponse: - template = get_template_by_name_and_version(service_template_name, version, template_repo, ResourceType.WorkspaceService, is_update=is_update) - return parse_obj_as(WorkspaceServiceTemplateInResponse, template) - - @workspace_service_templates_core_router.post("/workspace-service-templates", status_code=status.HTTP_201_CREATED, response_model=WorkspaceServiceTemplateInResponse, response_model_exclude_none=True, name=strings.API_CREATE_WORKSPACE_SERVICE_TEMPLATES, dependencies=[Depends(get_current_admin_user)]) async def register_workspace_service_template(template_input: WorkspaceServiceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInResponse: try: diff --git a/api_app/api/routes/workspace_templates.py b/api_app/api/routes/workspace_templates.py index e29ca0861e..8879628927 100644 --- a/api_app/api/routes/workspace_templates.py +++ b/api_app/api/routes/workspace_templates.py @@ -9,33 +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_tre_user_or_tre_admin, get_current_admin_user +from services.authentication import get_current_admin_user from .resource_helpers import get_current_template_by_name -workspace_templates_admin_router = APIRouter(dependencies=[Depends(get_current_tre_user_or_tre_admin)]) +workspace_templates_admin_router = APIRouter(dependencies=[Depends(get_current_admin_user)]) @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_tre_user_or_tre_admin)) -> ResourceTemplateInformationInList: +async def get_workspace_templates(authorized_only: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository)), user=Depends(get_current_admin_user)) -> 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)), user=Depends(get_current_tre_user_or_tre_admin)) -> WorkspaceTemplateInResponse: +async def get_current_workspace_template_by_name(workspace_template_name: str, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> 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.get("/workspace-templates/{workspace_template_name}/{version}", response_model=WorkspaceTemplateInResponse, name=strings.API_GET_WORKSPACE_TEMPLATE_BY_NAME_AND_VERSION, response_model_exclude_none=True) -async def get_workspace_template_by_name_and_version(workspace_template_name: str, version: str, is_update: bool = False, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> WorkspaceTemplateInResponse: - template = get_template_by_name_and_version(workspace_template_name, version, 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)), user=Depends(get_current_admin_user)) -> ResourceTemplateInResponse: +async def register_workspace_template(template_input: WorkspaceTemplateInCreate, template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInResponse: try: return template_repo.create_and_validate_template(template_input, ResourceType.Workspace) except EntityVersionExist: diff --git a/api_app/resources/strings.py b/api_app/resources/strings.py index 536027888f..00a58d26b0 100644 --- a/api_app/resources/strings.py +++ b/api_app/resources/strings.py @@ -1,9 +1,9 @@ PONG = "pong" # API Descriptions +API_GET_METADATA = "Get public API metadata (e.g. to support the UI and CLI)" 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" @@ -43,18 +43,15 @@ API_CREATE_WORKSPACE_TEMPLATES = "Register workspace template" API_GET_WORKSPACE_TEMPLATES = "Get workspace templates" API_GET_WORKSPACE_TEMPLATE_BY_NAME = "Get workspace template by name" -API_GET_WORKSPACE_TEMPLATE_BY_NAME_AND_VERSION = "Get workspace template by name and version" API_CREATE_WORKSPACE_SERVICE_TEMPLATES = "Register workspace service template" API_GET_WORKSPACE_SERVICE_TEMPLATES = "Get workspace service templates" API_GET_WORKSPACE_SERVICE_TEMPLATES_IN_WORKSPACE = "Get workspace service templates (on workspace level)" # only returns templates that the authenticated user is authorized to use API_GET_WORKSPACE_SERVICE_TEMPLATE_BY_NAME = "Get workspace service template by name" -API_GET_WORKSPACE_SERVICE_TEMPLATE_BY_NAME_AND_VERSION = "Get workspace service template by name and version" API_CREATE_SHARED_SERVICE_TEMPLATES = "Register shared service template" API_GET_SHARED_SERVICE_TEMPLATES = "Get shared service templates" API_GET_SHARED_SERVICE_TEMPLATE_BY_NAME = "Get shared service template by name" -API_GET_SHARED_SERVICE_TEMPLATE_BY_NAME_AND_VERSION = "Get shared service template by name and version" API_GET_ALL_SHARED_SERVICES = "Get all shared services" API_GET_SHARED_SERVICE_BY_ID = "Get shared service by ID" @@ -67,7 +64,6 @@ API_GET_USER_RESOURCE_TEMPLATES = "Get user resource templates applicable to the workspace service template" API_GET_USER_RESOURCE_TEMPLATES_IN_WORKSPACE = "Get user resource templates applicable to the workspace service template (on workspace level)" # only returns templates that the authenticated user is authorized to use API_GET_USER_RESOURCE_TEMPLATE_BY_NAME = "Get user resource template by name and workspace service" -API_GET_USER_RESOURCE_TEMPLATE_BY_NAME_AND_VERSION = "Get user resource template by name and version and workspace service" # cost report API_GET_COSTS = "Get overall costs" @@ -137,7 +133,6 @@ WORKSPACE_SERVICE_TEMPLATE_DOES_NOT_EXIST = "Could not retrieve the workspace service template specified" TEMPLATE_DOES_NOT_EXIST = "Could not retrieve the 'current' template with this name" -TEMPLATE_VERSION_DOES_NOT_EXIST = "Could not retrieve the template with this name and version" NO_UNIQUE_CURRENT_FOR_TEMPLATE = "The template has multiple 'current' versions" SHARED_SERVICE_DOES_NOT_EXIST = "Shared service does not exist" diff --git a/api_app/service_bus/helpers.py b/api_app/service_bus/helpers.py index 0dfce468b9..6ccfde2f3b 100644 --- a/api_app/service_bus/helpers.py +++ b/api_app/service_bus/helpers.py @@ -49,7 +49,7 @@ def update_resource_for_step(operation_step: OperationStep, resource_repo: Resou if primary_resource.resourceType == ResourceType.UserResource: primary_parent_workspace_service = resource_repo.get_resource_by_id(primary_resource.parentWorkspaceServiceId) primary_parent_service_name = primary_parent_workspace_service.templateName - primary_template = resource_template_repo.get_template_by_name_and_version(primary_resource.templateName, primary_resource.templateVersion, primary_resource.resourceType, primary_parent_service_name) + primary_template = resource_template_repo.get_current_template(primary_resource.templateName, primary_resource.resourceType, primary_parent_service_name) # get the template step template_step = None @@ -117,7 +117,7 @@ def try_upgrade(resource_repo: ResourceRepository, resource_template_repo: Resou if resource_to_update.resourceType == ResourceType.UserResource: parent_service_name = resource_to_update["parentWorkspaceServiceId"] - resource_template_to_send = resource_template_repo.get_template_by_name_and_version(resource_to_update.templateName, resource_to_update.templateVersion, resource_to_update.resourceType, parent_service_name) + resource_template_to_send = resource_template_repo.get_current_template(resource_to_update.templateName, resource_to_update.resourceType, parent_service_name) # create the patch patch = ResourcePatch( diff --git a/api_app/tests_ma/test_service_bus/test_resource_request_sender.py b/api_app/tests_ma/test_service_bus/test_resource_request_sender.py index f26f4117a1..a6f5679209 100644 --- a/api_app/tests_ma/test_service_bus/test_resource_request_sender.py +++ b/api_app/tests_ma/test_service_bus/test_resource_request_sender.py @@ -105,7 +105,7 @@ async def test_multi_step_document_sends_first_step( temp_workspace_service, basic_shared_service, ] - resource_template_repo.get_template_by_name_and_version.side_effect = [ + resource_template_repo.get_current_template.side_effect = [ multi_step_resource_template, basic_shared_service_template, ] From cbe65ed0a88b8ae9a8088f565a063d80108fdabd Mon Sep 17 00:00:00 2001 From: David Moore Date: Mon, 31 Oct 2022 21:18:21 +0000 Subject: [PATCH 07/10] rolling back changes --- api_app/api/routes/api.py | 6 ++++-- api_app/api/routes/workspace_templates.py | 10 +++++----- api_app/resources/strings.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/api_app/api/routes/api.py b/api_app/api/routes/api.py index 4da2ab0183..78cbaf4d0f 100644 --- a/api_app/api/routes/api.py +++ b/api_app/api/routes/api.py @@ -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 = [ @@ -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"]) @@ -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] } ) diff --git a/api_app/api/routes/workspace_templates.py b/api_app/api/routes/workspace_templates.py index 8879628927..e7386f982c 100644 --- a/api_app/api/routes/workspace_templates.py +++ b/api_app/api/routes/workspace_templates.py @@ -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: diff --git a/api_app/resources/strings.py b/api_app/resources/strings.py index 00a58d26b0..a8ba22dfb8 100644 --- a/api_app/resources/strings.py +++ b/api_app/resources/strings.py @@ -1,9 +1,9 @@ PONG = "pong" # API Descriptions -API_GET_METADATA = "Get public API metadata (e.g. to support the UI and CLI)" 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" From f5d1719631dfc214f4a619562cbbab24d1a4ad9d Mon Sep 17 00:00:00 2001 From: David Moore Date: Tue, 1 Nov 2022 11:11:49 +0000 Subject: [PATCH 08/10] changelog --- CHANGELOG.md | 1 + .../components/shared/create-update-resource/ResourceForm.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44444f6051..06f3b94d44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ENHANCEMENTS: * Show error message when Review VMs are not configured in the current workspace * CLI: Add missing endpoints and minor bug fixes (#2784) * Added optional parameter to allow a client to retrieve a template by name and version ([#2802](https://github.com/microsoft/AzureTRE/pull/2802)) +* Added support for `allOf` usage in Resource Templates - both across the API and the UI. This allows a template author to specify certain fields as being conditionally present / conditionally required, and means we can tidy up some of the resource creation forms substantially. ([#2795](https://github.com/microsoft/AzureTRE/pull/2795)) BUG FIXES: * Show the correct createdBy value for airlock requests in UI and in API queries ([#2779](https://github.com/microsoft/AzureTRE/pull/2779)) diff --git a/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx b/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx index baed955144..b4f367d190 100644 --- a/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx +++ b/ui/app/src/components/shared/create-update-resource/ResourceForm.tsx @@ -31,7 +31,7 @@ export const ResourceForm: React.FunctionComponent = (props: const getFullTemplate = async () => { try { // Get the full resource template containing the required parameters - const templateResponse = (await apiCall(props.updateResource ? `${props.templatePath}/${props.updateResource.templateVersion}?is_update=true` : props.templatePath, HttpMethod.Get)) as ResourceTemplate; + const templateResponse = (await apiCall(props.updateResource ? `${props.templatePath}?is_update=true&version=${props.updateResource.templateVersion}` : props.templatePath, HttpMethod.Get)) as ResourceTemplate; // if it's an update, populate the form with the props that are available in the template if (props.updateResource) { From 07f83ee30c99bc8cb797e9219a764a0accba4d2b Mon Sep 17 00:00:00 2001 From: David Moore Date: Tue, 1 Nov 2022 11:52:11 +0000 Subject: [PATCH 09/10] updated e2e tests with auth_type --- api_app/models/schemas/workspace.py | 1 + e2e_tests/test_airlock.py | 1 + e2e_tests/test_performance.py | 2 ++ e2e_tests/test_workspace_services.py | 1 + 4 files changed, 5 insertions(+) diff --git a/api_app/models/schemas/workspace.py b/api_app/models/schemas/workspace.py index 6d692c377a..424c82b46a 100644 --- a/api_app/models/schemas/workspace.py +++ b/api_app/models/schemas/workspace.py @@ -82,6 +82,7 @@ class Config: "properties": { "display_name": "the workspace display name", "description": "workspace description", + "auth_type": "Manual", "client_id": "", "client_secret": "", "address_space_size": "small" diff --git a/e2e_tests/test_airlock.py b/e2e_tests/test_airlock.py index e0f2311ca6..34399ce1af 100644 --- a/e2e_tests/test_airlock.py +++ b/e2e_tests/test_airlock.py @@ -39,6 +39,7 @@ async def test_airlock_flow(verify) -> None: "display_name": "E2E test airlock flow", "description": "workspace for E2E airlock flow", "address_space_size": "small", + "auth_type": "Manual", "client_id": f"{config.TEST_WORKSPACE_APP_ID}", "client_secret": f"{config.TEST_WORKSPACE_APP_SECRET}", } diff --git a/e2e_tests/test_performance.py b/e2e_tests/test_performance.py index 0a0e97be2f..4f714cd810 100644 --- a/e2e_tests/test_performance.py +++ b/e2e_tests/test_performance.py @@ -26,6 +26,7 @@ async def test_parallel_resource_creations(verify) -> None: "display_name": f'Perf Test Workspace {i}', "description": "workspace for perf test", "address_space_size": "small", + "auth_type": "Manual", "client_id": f"{config.TEST_WORKSPACE_APP_ID}" } } @@ -67,6 +68,7 @@ async def test_bulk_updates_to_ensure_each_resource_updated_in_series(verify) -> "display_name": "E2E test guacamole service", "description": "", "address_space_size": "small", + "auth_type": "Manual", "client_id": f"{config.TEST_WORKSPACE_APP_ID}", "client_secret": f"{config.TEST_WORKSPACE_APP_SECRET}" } diff --git a/e2e_tests/test_workspace_services.py b/e2e_tests/test_workspace_services.py index caee085409..6495d4b222 100644 --- a/e2e_tests/test_workspace_services.py +++ b/e2e_tests/test_workspace_services.py @@ -20,6 +20,7 @@ async def test_create_guacamole_service_into_base_workspace(verify) -> None: "display_name": "E2E test guacamole service", "description": "workspace for E2E", "address_space_size": "small", + "auth_type": "Manual", "client_id": f"{config.TEST_WORKSPACE_APP_ID}", "client_secret": f"{config.TEST_WORKSPACE_APP_SECRET}", } From e96af7c20bb9c6b1487710f7419e5afbda717b8c Mon Sep 17 00:00:00 2001 From: David Moore Date: Tue, 1 Nov 2022 11:53:34 +0000 Subject: [PATCH 10/10] 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 db55ef1921..57f9f92e6f 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.5.10" +__version__ = "0.5.11"