diff --git a/Makefile b/Makefile index 942156346a..a5849acde1 100644 --- a/Makefile +++ b/Makefile @@ -325,13 +325,13 @@ setup-local-debugging: && . ./devops/scripts/load_env.sh ./templates/core/private.env \ && . ./scripts/setup_local_debugging.sh -register-aad-workspace: - $(call target_title,"Registering AAD Workspace") \ +auth: + $(call target_title,"Setting up Azure Active Directory") \ && . ./devops/scripts/check_dependencies.sh nodocker \ && . ./devops/scripts/load_env.sh ./templates/core/.env \ && pushd ./templates/core/terraform/ > /dev/null && . ./outputs.sh && popd > /dev/null \ && . ./devops/scripts/load_env.sh ./templates/core/private.env \ - && . ./devops/scripts/register-aad-workspace.sh + && . ./scripts/create_aad_assets.sh show-core-output: $(call target_title,"Display TRE core output") \ diff --git a/devops/scripts/register-aad-workspace.sh b/devops/scripts/register-aad-workspace.sh deleted file mode 100755 index c10caaa6d4..0000000000 --- a/devops/scripts/register-aad-workspace.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -e - -: ${AAD_TENANT_ID?"You have not set your AAD_TENANT_ID in ./templates/core/.env"} - -LOGGED_IN_TENANT_ID=$(az account show --query tenantId -o tsv) -CHANGED_TENANT=0 - -if [ "${LOGGED_IN_TENANT_ID}" != "${AAD_TENANT_ID}" ]; then - echo "Attempting to sign you onto ${AAD_TENANT_ID} to add an App Registration." - - # First we need to login to the AAD tenant (as it is different to the subscription tenant) - az login --tenant ${AAD_TENANT_ID} --allow-no-subscriptions - CHANGED_TENANT=1 -fi - -# Then register an App -./scripts/aad-app-reg.sh \ --n "${TRE_ID} - Workspace One" \ --r "https://${TRE_ID}.${RESOURCE_LOCATION}.cloudapp.azure.com/api/docs/oauth2-redirect" \ --w - -if [ "${CHANGED_TENANT}" -ne 0 ]; then - echo "Attempting to sign you back into ${LOGGED_IN_TENANT_ID}." - - # Log back into the tenant the user started on. - az login --tenant ${LOGGED_IN_TENANT_ID} --allow-no-subscriptions -fi diff --git a/docs/tre-admins/auth.md b/docs/tre-admins/auth.md index 78bea1c4d2..e69f4e4d4f 100644 --- a/docs/tre-admins/auth.md +++ b/docs/tre-admins/auth.md @@ -4,6 +4,20 @@ AAD holds the identities of all the TRE/workspace users, including administrators, and connects the identities with applications which define the permissions for each user role. +## Pre-requisites +The following values are needed to be in place to run the script. (`/templates/core/.env`) +| Key | Description | +| ----------- | ----------- | +|TRE_ID|This is needed to build up the redirect URI for the Swagger App| +|AAD_TENANT_ID|The tenant id of where your AAD identities will be placed. This can be different to the tenant where your Azure resources are created. + +## Create Authentication assets +You can build all of the Identity assets by running the following at the command line +```bash +make auth +``` +Follow the instructions and prompts in the script. It will ask you to confirm at various stages, so don't go and make a coffee! This will create the 4 parts of authentication outlined below, and if succesful you will not need to do anything apart from copy some values into `/templates/core/.env`. The details below are for your understanding. + ## App registrations App registrations (represented by service principals) define the various access permissions to the TRE system. There are a total of four main Applications of interest. diff --git a/scripts/aad-app-reg.sh b/scripts/aad-app-reg.sh index 8df59a1a56..cab294385b 100755 --- a/scripts/aad-app-reg.sh +++ b/scripts/aad-app-reg.sh @@ -2,7 +2,7 @@ # Setup Script set -euo pipefail -AZURE_CORE_OUTPUT=jsonc # force CLI output to JSON for the script (user can still change default for interactive usage in the dev container) +# AZURE_CORE_OUTPUT=jsonc # force CLI output to JSON for the script (user can still change default for interactive usage in the dev container) function show_usage() { @@ -46,10 +46,12 @@ if ! command -v az &> /dev/null; then exit 1 fi +# Get the directory that this script is in +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" declare grantAdminConsent=0 -declare swaggerSpId="" +declare swaggerAppId="" declare workspace=0 declare appName="" declare replyUrl="" @@ -75,7 +77,7 @@ while [[ $# -gt 0 ]]; do shift 1 ;; -s|--swaggerui-clientid) - swaggerSpId=$2 + swaggerAppId=$2 shift 2 ;; -r|--swaggerui-redirecturl) @@ -98,14 +100,16 @@ while [[ $# -gt 0 ]]; do esac done - +################################### +# CHECK INCOMMING PARAMETERS # +################################### if [[ -z "$appName" ]]; then echo "Please specify the application name" 1>&2 show_usage fi # if admin consent & workspace, but not swagger client id, show error -if [[ $workspace -ne 0 && $grantAdminConsent -eq 1 && ! -n "$swaggerSpId" ]]; then +if [[ $workspace -eq 1 && $grantAdminConsent -eq 1 && -z "$swaggerAppId" ]]; then echo "When specifying --admin-consent and --workspace, please specify the swagger application client ID option" 1>&2 show_usage fi @@ -116,54 +120,6 @@ if [[ $(az account list --only-show-errors -o json | jq 'length') -eq 0 ]]; then fi -# This function polls looking for an app registration with the given ID. -# If after the number of retries no app registration is found, the function exits. -function wait_for_new_app_registration() -{ - appId=$1 - retries=10 - counter=0 - objectId=$(az ad app list --filter "appId eq '${appId}'" --query '[0].objectId' --output tsv) - - while [[ -z $objectId && $counter -lt $retries ]]; do - counter=$((counter+1)) - echo "Waiting for app registration with ID ${appId} to show up (${counter}/${retries})..." - sleep 5 - objectId=$(az ad app list --filter "appId eq '${appId}'" --query '[0].objectId' --output tsv) - done - - if [[ -z $objectId ]]; then - echo "Failed" - exit 1 - fi - - echo "App registration with ID ${appId} found" -} - -# This function polls looking for a service principal with the given ID. -# If after the number of retries no app registration is found, the function exits. -function wait_for_new_service_principal() -{ - servicePrincipalId=$1 - retries=10 - counter=0 - output=$(az rest --method GET --uri https://graph.microsoft.com/v1.0/servicePrincipals/${servicePrincipalId} 2>/dev/null || true) - - while [[ -z $output && $counter -lt $retries ]]; do - counter=$((counter+1)) - echo "Waiting for service principal with ID ${servicePrincipalId} to show up (${counter}/${retries})..." - sleep 5 - output=$(az rest --method GET --uri https://graph.microsoft.com/v1.0/servicePrincipals/${servicePrincipalId} 2>/dev/null || true) - done - - if [[ -z $output ]]; then - echo "Failed" - exit 1 - fi - - echo "Service principal with ID ${servicePrincipalId} found" -} - # Grants admin consent for the given app permission. # # Parameters: @@ -177,7 +133,9 @@ function grant_admin_consent() appRoleId=$3 # test if enabled to avoid "Permission being assigned already exists on the object" error - is_enabled=$(az rest --method GET --uri ${msGraphUri}/servicePrincipals/${principalId}/appRoleAssignments -o json | jq -r ".value | map( select(.appRoleId==\"${appRoleId}\") ) | length") + is_enabled=$(az rest --method GET \ + --uri "${msGraphUri}/servicePrincipals/${principalId}/appRoleAssignments" -o json \ + | jq -r ".value | map( select(.appRoleId==\"${appRoleId}\") ) | length") if [[ "$is_enabled" != "1" ]]; then data=$(jq -c . << JSON @@ -188,11 +146,11 @@ function grant_admin_consent() } JSON ) - az rest --method POST --uri ${msGraphUri}/servicePrincipals/${principalId}/appRoleAssignments --body ${data} + az rest --method POST --uri "${msGraphUri}/servicePrincipals/${principalId}/appRoleAssignments" --body "${data}" fi } -declare tenant=$(az rest -m get -u ${msGraphUri}/domains -o json | jq -r '.value[] | select(.isDefault == true) | .id') +tenant=$(az rest -m get -u "${msGraphUri}/domains" -o json | jq -r '.value[] | select(.isDefault == true) | .id') echo "You are about to create app registrations in the Azure AD tenant \"${tenant}\"." read -p "Do you want to continue? (y/N) " -n 1 -r @@ -205,24 +163,23 @@ fi currentUserId=$(az ad signed-in-user show --query 'objectId' --output tsv) # Generate GUIDS -declare userRoleId=$(cat /proc/sys/kernel/random/uuid) -declare adminRoleId=$(cat /proc/sys/kernel/random/uuid) -declare researcherRoleId=$(cat /proc/sys/kernel/random/uuid) -declare ownerRoleId=$(cat /proc/sys/kernel/random/uuid) - -declare apiUserImpersonationScopeID=$(cat /proc/sys/kernel/random/uuid) - -declare apiAppObjectId="" +userRoleId=$(cat /proc/sys/kernel/random/uuid) +adminRoleId=$(cat /proc/sys/kernel/random/uuid) +researcherRoleId=$(cat /proc/sys/kernel/random/uuid) +ownerRoleId=$(cat /proc/sys/kernel/random/uuid) +apiUserImpersonationScopeID=$(cat /proc/sys/kernel/random/uuid) +apiAppObjectId="" function get_existing_app() { - local existingApiApps=$(az ad app list --display-name "$1" -o json) + existingApiApps=$(az ad app list --display-name "$1" -o json) + number_of_apps=$(echo "${existingApiApps}" | jq 'length') - if [[ $(echo ${existingApiApps} | jq 'length') -gt 1 ]]; then + if [[ "${number_of_apps}" -gt 1 ]]; then echo "There are more than one applications with the name \"$1\" already." exit 1 fi - if [[ $(echo ${existingApiApps} | jq 'length') -eq 1 ]]; then + if [[ "${number_of_apps}" -eq 1 ]]; then echo "${existingApiApps}" | jq -c '.[0]' return 0 fi @@ -231,14 +188,15 @@ function get_existing_app() { } function get_existing_app_by_id() { - local existingApiApps=$(az ad app list --app-id "$1" -o json) + existingApiApps=$(az ad app list --app-id "$1" -o json) + number_of_apps=$(echo "${existingApiApps}" | jq 'length') - if [[ $(echo ${existingApiApps} | jq 'length') -ne 1 ]]; then + if [[ "${number_of_apps}" -ne 1 ]]; then echo "There are no applications with id \"$1\"." exit 1 fi - if [[ $(echo ${existingApiApps} | jq 'length') -eq 1 ]]; then + if [ "${number_of_apps}" -eq 1 ]; then echo "${existingApiApps}" | jq -c '.[0]' return 0 fi @@ -246,10 +204,10 @@ function get_existing_app_by_id() { return 1 } -declare existingApiApp=$(get_existing_app "${appName} API") +existingApiApp=$(get_existing_app "${appName} API") if [[ -n ${existingApiApp} ]]; then - apiAppObjectId=$(echo ${existingApiApp} | jq -r '.objectId') + apiAppObjectId=$(echo "${existingApiApp}" | jq -r '.objectId') # Get existing IDs of roles and scopes. if [[ $workspace -eq 0 ]]; then @@ -269,7 +227,7 @@ if [[ -n ${existingApiApp} ]]; then if [[ -z "${apiUserImpersonationScopeID}" ]]; then apiUserImpersonationScopeID=$(cat /proc/sys/kernel/random/uuid); fi fi -declare appRoles=$(jq -c . << JSON +appRoles=$(jq -c . << JSON [ { "id": "${userRoleId}", @@ -293,7 +251,7 @@ declare appRoles=$(jq -c . << JSON JSON ) -declare workspaceAppRoles=$(jq -c . << JSON +workspaceAppRoles=$(jq -c . << JSON [ { "id": "${ownerRoleId}", @@ -317,7 +275,7 @@ declare workspaceAppRoles=$(jq -c . << JSON JSON ) -declare oauth2PermissionScopes=$(jq -c . << JSON +oauth2PermissionScopes=$(jq -c . << JSON [ { "adminConsentDescription": "Allow the app to access the TRE API on behalf of the signed-in user.", @@ -333,7 +291,7 @@ declare oauth2PermissionScopes=$(jq -c . << JSON JSON ) -declare workspaceOauth2PermissionScopes=$(jq -c . << JSON +workspaceOauth2PermissionScopes=$(jq -c . << JSON [ { "adminConsentDescription": "Allow the app to access the Workspace API on behalf of the signed-in user.", @@ -349,38 +307,40 @@ declare workspaceOauth2PermissionScopes=$(jq -c . << JSON JSON ) -declare msGraphAppId="00000003-0000-0000-c000-000000000000" -declare msGraphEmailScopeId="64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0" -declare msGraphOpenIdScopeId="37f7f235-527c-4136-accd-4a02d197296e" -declare msGraphProfileScopeId="14dad69e-099b-42c9-810b-d002981feec1" -declare msGraphObjectId=$(az ad sp show --id ${msGraphAppId} --query "objectId" --output tsv) -declare directoryReadAllId=$(az ad sp show --id ${msGraphAppId} --query "appRoles[?value=='Directory.Read.All'].id" --output tsv) -declare userReadAllId=$(az ad sp show --id ${msGraphAppId} --query "appRoles[?value=='User.Read.All'].id" --output tsv) +msGraphAppId="00000003-0000-0000-c000-000000000000" +msGraphEmailScopeId="64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0" +msGraphOpenIdScopeId="37f7f235-527c-4136-accd-4a02d197296e" +msGraphProfileScopeId="14dad69e-099b-42c9-810b-d002981feec1" +msGraphObjectId=$(az ad sp show --id ${msGraphAppId} --query "objectId" --output tsv) +directoryReadAllId=$(az ad sp show --id ${msGraphAppId} --query "appRoles[?value=='Directory.Read.All'].id" --output tsv) +userReadAllId=$(az ad sp show --id ${msGraphAppId} --query "appRoles[?value=='User.Read.All'].id" --output tsv) +applicationReadWriteOwnedById=$(az ad sp show --id ${msGraphAppId} --query "appRoles[?value=='Application.ReadWrite.OwnedBy'].id" --output tsv) function get_msgraph_scope() { - local scope=$(az ad sp show --id ${msGraphAppId} --query "oauth2Permissions[?value=='$1'].id | [0]" --output tsv) + oauthScope=$(az ad sp show --id ${msGraphAppId} --query "oauth2Permissions[?value=='$1'].id | [0]" --output tsv) jq -c . <<- JSON { - "id": "${scope}", + "id": "${oauthScope}", "type": "Scope" } JSON } function get_msgraph_role() { - local scope=$(az ad sp show --id ${msGraphAppId} --query "appRoles[?value=='$1'].id | [0]" --output tsv) + appRoleScope=$(az ad sp show --id ${msGraphAppId} --query "appRoles[?value=='$1'].id | [0]" --output tsv) jq -c . <<- JSON { - "id": "${scope}", + "id": "${appRoleScope}", "type": "Role" } JSON } -declare roleUserReadAll=$(get_msgraph_role "User.Read.All" ) -declare roleDirectoryReadAll=$(get_msgraph_role "Directory.Read.All" ) +roleUserReadAll=$(get_msgraph_role "User.Read.All" ) +roleDirectoryReadAll=$(get_msgraph_role "Directory.Read.All" ) +roleApplicationReadWriteOwnedBy=$(get_msgraph_role "Application.ReadWrite.OwnedBy" ) -declare apiRequiredResourceAccess=$(jq -c . << JSON +apiRequiredResourceAccess=$(jq -c . << JSON [ { "resourceAppId": "${msGraphAppId}", @@ -393,8 +353,20 @@ declare apiRequiredResourceAccess=$(jq -c . << JSON JSON ) +workspaceRequiredResourceAccess=$(jq -c . << JSON +[ + { + "resourceAppId": "${msGraphAppId}", + "resourceAccess": [ + ${roleApplicationReadWriteOwnedBy} + ] + } +] +JSON +) + if [[ $workspace -eq 0 ]]; then - declare apiApp=$(jq -c . << JSON + apiApp=$(jq -c . << JSON { "displayName": "${appName} API", "api": { @@ -409,7 +381,7 @@ JSON ) else - declare apiApp=$(jq -c . << JSON + apiApp=$(jq -c . << JSON { "displayName": "${appName} API", "api": { @@ -418,6 +390,7 @@ else }, "appRoles": ${workspaceAppRoles}, "signInAudience": "AzureADMyOrg", + "requiredResourceAccess": ${workspaceRequiredResourceAccess}, "web":{ "implicitGrantSettings":{ "enableIdTokenIssuance":true, @@ -447,12 +420,11 @@ JSON ) fi - # Is the API app already registered? if [[ -n ${apiAppObjectId} ]]; then echo "Updating API app registration with ID ${apiAppObjectId}" az rest --method PATCH --uri "${msGraphUri}/applications/${apiAppObjectId}" --headers Content-Type=application/json --body "${apiApp}" - apiAppId=$(az ad app show --id ${apiAppObjectId} --query "appId" --output tsv) + apiAppId=$(az ad app show --id "${apiAppObjectId}" --query "appId" --output tsv) echo "API app registration with ID ${apiAppId} updated" else echo "Creating a new API app registration, ${appName} API" @@ -460,16 +432,16 @@ else echo "AppId: ${apiAppId}" # Poll until the app registration is found in the listing. - echo "Waiting for the new app registration" - wait_for_new_app_registration $apiAppId + "${DIR}"/aad/wait_for_new_app_registration.sh "${apiAppId}" # Update to set the identifier URI. - echo "Updating identifier URI" - az ad app update --id ${apiAppId} --identifier-uris "api://${apiAppId}" + echo "Updating identifier URI 'api://${apiAppId}'" + az ad app update --id "${apiAppId}" --identifier-uris "api://${apiAppId}" fi echo "Setting API permissions (email / profile / ipaddr)" -az ad app permission add --id ${apiAppId} --api ${msGraphAppId} --api-permissions ${msGraphEmailScopeId}=Scope ${msGraphOpenIdScopeId}=Scope ${msGraphProfileScopeId}=Scope +az ad app permission add --id "${apiAppId}" --api "${msGraphAppId}" \ + --api-permissions ${msGraphEmailScopeId}=Scope ${msGraphOpenIdScopeId}=Scope ${msGraphProfileScopeId}=Scope # todo: [Issue 1352](https://github.com/microsoft/AzureTRE/issues/1352) # echo "Updating redirect uri" @@ -479,7 +451,7 @@ az ad app permission add --id ${apiAppId} --api ${msGraphAppId} --api-permission # --body '{"spa":{"redirectUris":["https://localhost:8080"]}}' # Make the current user an owner of the application. -az ad app owner add --id ${apiAppId} --owner-object-id $currentUserId +az ad app owner add --id "${apiAppId}" --owner-object-id "$currentUserId" # See if a service principal already exists spId=$(az ad sp list --filter "appId eq '${apiAppId}'" --query '[0].objectId' --output tsv) @@ -488,9 +460,10 @@ resetPassword=0 # If not, create a new service principal if [[ -z "$spId" ]]; then - spId=$(az ad sp create --id ${apiAppId} --query 'objectId' --output tsv) - echo "Creating a new service principal, for ${appName} API app, with ID $spId" - wait_for_new_service_principal $spId + spId=$(az ad sp create --id "${apiAppId}" --query 'objectId' --output tsv) + echo "Creating a new service principal, for '${appName} API' app, with ID ${spId}" + "${DIR}"/aad/wait_for_new_service_principal.sh "${spId}" + az ad app owner add --id "${apiAppId}" --owner-object-id "${spId}" resetPassword=1 else echo "Service principal for the app already exists." @@ -507,37 +480,89 @@ spPassword="" if [[ "$resetPassword" == 1 ]]; then # Reset the app password (client secret) and display it - spPassword=$(az ad sp credential reset --name ${apiAppId} --query 'password' --output tsv) - echo "${appName} API app password (client secret): ${spPassword}" + spPassword=$(az ad sp credential reset --name "${apiAppId}" --query 'password' --output tsv) + echo "'${appName} API' app password (client secret): ${spPassword}" fi # This tag ensures the app is listed in "Enterprise applications" -az ad sp update --id $spId --set tags="['WindowsAzureActiveDirectoryIntegratedApp']" +az ad sp update --id "$spId" --set tags="['WindowsAzureActiveDirectoryIntegratedApp']" # needed to make the API permissions change effective, this must be done after SP creation... echo "running 'az ad app permission grant' to make changes effective" -az ad app permission grant --id ${apiAppId} --api ${msGraphAppId} +az ad app permission grant --id "${apiAppId}" --api "${msGraphAppId}" # If a TRE core app reg -if [[ $workspace -ne 0 ]]; then +if [[ "$workspace" -ne 0 ]]; then # Grant admin consent for the delegated workspace scopes - if [[ $grantAdminConsent -eq 1 ]]; then - echo "Granting admin consent for ${appName} Swagger UI app (service principal ID ${swaggerSpId})" - az ad app permission grant --id $swaggerSpId --api $apiAppId --scope "user_impersonation" + if [[ "$grantAdminConsent" -eq 1 ]]; then + echo "Granting admin consent for '${appName}' Workspace app (service principal ID ${spId}) - NOTE: Directory admin privileges required for this step" + "$DIR"/aad/wait_for_new_service_principal.sh "$spId" + grant_admin_consent "$spId" "$msGraphObjectId" "$applicationReadWriteOwnedById" fi + + # The Swagger UI (which was created as part of the API) needs to also have access to this Workspace + echo "Searching for existing Swagger application (${swaggerAppId})." + existingSwaggerApp=$(get_existing_app_by_id "${swaggerAppId}") + swaggerObjectId=$(echo "${existingSwaggerApp}" | jq -r .objectId) + + # Get the existing required resource access from the swagger app, + # but remove the access that we are about to add for idempotency. We cant use + # the response from az cli as it returns an 'AdditionalProperties' element in + # the json + existingResourceAccess=$(az rest \ + --method GET \ + --uri "${msGraphUri}/applications/${swaggerObjectId}" \ + --headers Content-Type=application/json \ + | jq -r --arg apiAppId "${apiAppId}" \ + 'del(.requiredResourceAccess[] | select(.resourceAppId==$apiAppId)) | .requiredResourceAccess' \ + ) + + # Add the existing resource access so we don't remove any existing permissions. + swaggerWorkspaceAccess=$(jq -c . << JSON +{ + "requiredResourceAccess": [ + { + "resourceAccess": [ + { + "id": "${apiUserImpersonationScopeID}", + "type": "Scope" + } + ], + "resourceAppId": "${apiAppId}" + } + ], + "existingAccess": ${existingResourceAccess} +} +JSON +) + + # Manipulate the json (add existingAccess into requiredResourceAccess and then remove it) + requiredResourceAccess=$(echo "${swaggerWorkspaceAccess}" | \ + jq '.requiredResourceAccess += .existingAccess | {requiredResourceAccess}') + + az rest --method PATCH \ + --uri "${msGraphUri}/applications/${swaggerObjectId}" \ + --headers Content-Type=application/json \ + --body "${requiredResourceAccess}" + + echo "Grant Swagger UI delegated access '${appName} API' (Client ID ${swaggerAppId})" + az ad app permission grant --id "${swaggerAppId}" --api "${apiAppId}" --scope "user_impersonation" + else # Grant admin consent on the required resource accesses (Graph API) if [[ $grantAdminConsent -eq 1 ]]; then - echo "Granting admin consent for ${appName} API app (service principal ID ${spId}) - NOTE: Directory admin privileges required for this step" - grant_admin_consent $spId $msGraphObjectId $directoryReadAllId - grant_admin_consent $spId $msGraphObjectId $userReadAllId + echo "Granting admin consent for '${appName} API' app (service principal ID ${spId}) - NOTE: Directory admin privileges required for this step" + "$DIR"/aad/wait_for_new_service_principal.sh "${spId}" + grant_admin_consent "${spId}" "$msGraphObjectId" "${directoryReadAllId}" + "$DIR"/aad/wait_for_new_service_principal.sh "${spId}" + grant_admin_consent "${spId}" "${msGraphObjectId}" "${userReadAllId}" fi # Now create the app for the Swagger UI - declare scope_openid=$(get_msgraph_scope "openid") - declare scope_offline_access=$(get_msgraph_scope "offline_access") + scope_openid=$(get_msgraph_scope "openid") + scope_offline_access=$(get_msgraph_scope "offline_access") - declare swaggerRequiredResourceAccess=$(jq -c . << JSON + swaggerRequiredResourceAccess=$(jq -c . << JSON [ { "resourceAppId": "00000003-0000-0000-c000-000000000000", @@ -566,7 +591,7 @@ JSON redirectUris="${redirectUris}, \"${replyUrl}\"" fi - declare swaggerUIApp=$(jq -c . << JSON + swaggerUIApp=$(jq -c . << JSON { "displayName": "${appName} Swagger UI", "signInAudience": "AzureADMyOrg", @@ -581,44 +606,41 @@ JSON ) # Is the Swagger UI app already registered? - declare existingSwaggerUIApp=$(get_existing_app "${appName} Swagger UI") + existingSwaggerUIApp=$(get_existing_app "${appName} Swagger UI") if [[ -n ${existingSwaggerUIApp} ]]; then swaggerUIAppObjectId=$(echo "${existingSwaggerUIApp}" | jq -r '.objectId') echo "Updating Swagger UI app with ID ${swaggerUIAppObjectId}" az rest --method PATCH --uri "${msGraphUri}/applications/${swaggerUIAppObjectId}" --headers Content-Type=application/json --body "${swaggerUIApp}" - swaggerAppId=$(az ad app show --id ${swaggerUIAppObjectId} --query "appId" --output tsv) + swaggerAppId=$(az ad app show --id "${swaggerUIAppObjectId}" --query "appId" --output tsv) echo "Swagger UI app registration with ID ${swaggerAppId} updated" else swaggerAppId=$(az rest --method POST --uri "${msGraphUri}/applications" --headers Content-Type=application/json --body "${swaggerUIApp}" --output tsv --query "appId") echo "Creating a new app registration, ${appName} Swagger UI, with ID ${swaggerAppId}" # Poll until the app registration is found in the listing. - wait_for_new_app_registration $swaggerAppId + "${DIR}"/aad/wait_for_new_app_registration.sh "${swaggerAppId}" fi # Make the current user an owner of the application. - az ad app owner add --id ${swaggerAppId} --owner-object-id $currentUserId + az ad app owner add --id "${swaggerAppId}" --owner-object-id "${currentUserId}" # See if a service principal already exists swaggerSpId=$(az ad sp list --filter "appId eq '${swaggerAppId}'" --query '[0].objectId' --output tsv) # If not, create a new service principal if [[ -z "$swaggerSpId" ]]; then - swaggerSpId=$(az ad sp create --id ${swaggerAppId} --query 'objectId' --output tsv) + swaggerSpId=$(az ad sp create --id "${swaggerAppId}" --query 'objectId' --output tsv) echo "Creating a new service principal, for ${appName} Swagger UI app, with ID $swaggerSpId" - wait_for_new_service_principal $swaggerSpId + "${DIR}"/aad/wait_for_new_service_principal.sh "${swaggerSpId}" fi - # Grant admin consent for the delegated scopes - if [[ $grantAdminConsent -eq 1 ]]; then - echo "Granting admin consent for ${appName} Swagger UI app (service principal ID ${swaggerSpId})" - az ad app permission grant --id $swaggerSpId --api $msGraphObjectId --scope "offline_access openid" - az ad app permission grant --id $swaggerSpId --api $apiAppId --scope "user_impersonation" - fi + echo "Granting delegated access for ${appName} Swagger UI app (service principal ID ${swaggerSpId})" + az ad app permission grant --id "$swaggerSpId" --api "$msGraphObjectId" --scope "offline_access openid" + az ad app permission grant --id "$swaggerSpId" --api "$apiAppId" --scope "user_impersonation" fi -declare automationApp=$(jq -c . << JSON +automationApp=$(jq -c . << JSON { "displayName": "${appName} Automation Admin App", "api": { @@ -644,12 +666,12 @@ declare automationApp=$(jq -c . << JSON JSON ) -if [[ -n $automationAppId ]]; then - echo "Searching for existing Automation application ($automationAppId)." - declare existingAutomationApp=$(get_existing_app_by_id "${automationAppId}") +if [[ -n ${automationAppId} ]]; then + echo "Searching for existing Automation application (${automationAppId})." + existingAutomationApp=$(get_existing_app_by_id "${automationAppId}") - automationAppObjectId=$(echo ${existingAutomationApp} | jq -r .objectId) - automationAppName=$(echo ${existingAutomationApp} | jq -r .displayName) + automationAppObjectId=$(echo "${existingAutomationApp}" | jq -r .objectId) + automationAppName=$(echo "${existingAutomationApp}" | jq -r .displayName) echo "Found '${automationAppName}' with ObjectId: '${automationAppObjectId}'" # Get the existing required resource access from the automation app, @@ -660,12 +682,12 @@ if [[ -n $automationAppId ]]; then --method GET \ --uri "${msGraphUri}/applications/${automationAppObjectId}" \ --headers Content-Type=application/json \ - | jq -r --arg apiAppId ${apiAppId} \ + | jq -r --arg apiAppId "${apiAppId}" \ 'del(.requiredResourceAccess[] | select(.resourceAppId==$apiAppId)) | .requiredResourceAccess' \ ) # Add the existing resource access so we don't remove any existing permissions. - declare automationWorkspaceAccess=$(jq -c . << JSON + automationWorkspaceAccess=$(jq -c . << JSON { "requiredResourceAccess": [ { @@ -688,7 +710,7 @@ JSON ) # Manipulate the json (add existingAccess into requiredResourceAccess and then remove it) - requiredResourceAccess=$(echo ${automationWorkspaceAccess} | \ + requiredResourceAccess=$(echo "${automationWorkspaceAccess}" | \ jq '.requiredResourceAccess += .existingAccess | {requiredResourceAccess}') az rest --method PATCH \ @@ -696,10 +718,13 @@ JSON --headers Content-Type=application/json \ --body "${requiredResourceAccess}" + # We've just updated a required resource. Wait for the update to complete. + "${DIR}"/aad/wait_for_new_app_registration.sh "${automationAppId}" + # Grant admin consent for the delegated workspace scopes if [[ $grantAdminConsent -eq 1 ]]; then echo "Granting admin consent for ${automationAppName} (App ID ${automationAppId})" - az ad app permission admin-consent --id $automationAppId + az ad app permission admin-consent --id "${automationAppId}" fi fi @@ -707,12 +732,12 @@ if [[ $createAutomationAccount -ne 0 ]]; then # Create an App Registration to allow automation to authenticate to the API # E.g. to register bundles - declare existingAutomationApp=$(get_existing_app "${appName} Automation Admin App") + existingAutomationApp=$(get_existing_app "${appName} Automation Admin App") if [[ -n ${existingAutomationApp} ]]; then echo "Automation app exists - updating..." - automationAppObjectId=$(echo ${existingAutomationApp} | jq -r .objectId) - automationAppId=$(echo ${existingAutomationApp} | jq -r .appId) + automationAppObjectId=$(echo "${existingAutomationApp}" | jq -r .objectId) + automationAppId=$(echo "${existingAutomationApp}" | jq -r .appId) az rest --method PATCH --uri "${msGraphUri}/applications/${automationAppObjectId}" --headers Content-Type=application/json --body "${automationApp}" else echo "Automation app doesn't exist - creating..." @@ -721,10 +746,10 @@ if [[ $createAutomationAccount -ne 0 ]]; then echo "Creating a new automation admin app registration, '${appName} Automation Admin App', with ID ${automationAppId}" # Poll until the app registration is found in the listing. - wait_for_new_app_registration $automationAppId + "${DIR}"/aad/wait_for_new_app_registration.sh "${automationAppId}" # Make the current user an owner of the application. - az ad app owner add --id ${automationAppId} --owner-object-id $currentUserId + az ad app owner add --id "${automationAppId}" --owner-object-id "${currentUserId}" fi # See if a service principal already exists @@ -733,9 +758,9 @@ if [[ $createAutomationAccount -ne 0 ]]; then resetPassword=0 # If not, create a new service principal if [[ -z "$automationSpId" ]]; then - automationSpId=$(az ad sp create --id ${automationAppId} --query 'objectId' --output tsv) - echo "Creating a new service principal, for ${appName} Automation Admin App, with ID $automationSpId" - wait_for_new_service_principal $automationSpId + automationSpId=$(az ad sp create --id "${automationAppId}" --query 'objectId' --output tsv) + echo "Creating a new service principal, for '${appName}' Automation Admin App, with ID '$automationSpId'" + "${DIR}"/aad/wait_for_new_service_principal.sh "${automationSpId}" resetPassword=1 else echo "Service principal for the app already exists." @@ -752,20 +777,26 @@ if [[ $createAutomationAccount -ne 0 ]]; then if [[ "$resetPassword" == 1 ]]; then # Reset the app password (client secret) and display it - automationSpPassword=$(az ad sp credential reset --name ${automationAppId} --query 'password' --output tsv) + automationSpPassword=$(az ad sp credential reset --name "${automationAppId}" --query 'password' --output tsv) echo "${appName} Automation Admin App password (client secret): ${automationSpPassword}" fi # This tag ensures the app is listed in "Enterprise applications" - az ad sp update --id $automationSpId --set tags="['WindowsAzureActiveDirectoryIntegratedApp']" + az ad sp update --id "${automationSpId}" --set tags="['WindowsAzureActiveDirectoryIntegratedApp']" # Grant admin consent for the delegated workspace scopes # https://github.com/microsoft/AzureTRE/issues/1513 # I've noticed that there can sometimes be a delay in the app having the permissions set # before we give admin-consent. If this occurs - rerun and it will work. if [[ $grantAdminConsent -eq 1 ]]; then - echo "Granting admin consent for ${appName} Automation Admin App (service principal ID ${automationSpId})" - az ad app permission admin-consent --id $automationAppId + echo "Granting admin consent for ${appName} Automation Admin App (ClientID ${automationAppId})" + "${DIR}"/aad/wait_for_new_app_registration.sh "${automationAppId}" + adminConsentResponse=$(az ad app permission admin-consent --id "${automationAppId}") + echo "${adminConsentResponse}" + if [ -z "${adminConsentResponse}" ]; then + echo "Admin consent failed, trying once more: ${adminConsentResponse}" + az ad app permission admin-consent --id "${automationAppId}" + fi fi fi @@ -776,41 +807,39 @@ echo "Done" if [[ $workspace -eq 0 ]]; then cat << ENV_VARS +AAD_TENANT_ID="$(az account show --output json | jq -r '.tenantId')" + ** Please copy the following variables to /templates/core/.env ** -AAD_TENANT_ID=$(az account show --output json | jq -r '.tenantId') -API_CLIENT_ID=${apiAppId} -API_CLIENT_SECRET=${spPassword} -SWAGGER_UI_CLIENT_ID=${swaggerAppId} +API_CLIENT_ID="${apiAppId}" +API_CLIENT_SECRET="${spPassword}" +SWAGGER_UI_CLIENT_ID="${swaggerAppId}" ENV_VARS else cat << ENV_VARS +AAD_TENANT_ID="$(az account show --output json | jq -r '.tenantId')" + ** Please copy the following variables to /templates/core/.env ** -AAD_TENANT_ID=$(az account show --output json | jq -r '.tenantId') -WORKSPACE_API_CLIENT_ID=${apiAppId} -WORKSPACE_API_CLIENT_SECRET=${spPassword} +WORKSPACE_API_OBJECT_ID=$(az ad app show --id "${apiAppId}" --query objectId) +WORKSPACE_API_CLIENT_ID="${apiAppId}" +WORKSPACE_API_CLIENT_SECRET="${spPassword}" ENV_VARS fi if [[ $createAutomationAccount -eq 1 ]]; then cat << ENV_VARS - -TEST_ACCOUNT_CLIENT_ID=${automationAppId} -TEST_ACCOUNT_CLIENT_SECRET=${automationSpPassword} - -** Please copy this to /templates/core/.env and set it to be https://fqdn for your TRE ** -TRE_URL=__CHANGE_ME___ +TEST_ACCOUNT_CLIENT_ID="${automationAppId}" +TEST_ACCOUNT_CLIENT_SECRET="${automationSpPassword}" ENV_VARS fi - if [[ $grantAdminConsent -eq 0 ]]; then echo "NOTE: Make sure the API permissions of the app registrations have admin consent granted." echo "Run this script with flag -a to grant admin consent or configure the registrations in Azure Portal." diff --git a/scripts/aad/add_redirect_url.sh b/scripts/aad/add_redirect_url.sh new file mode 100755 index 0000000000..d08865ed34 --- /dev/null +++ b/scripts/aad/add_redirect_url.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# This script is called by a bundle to interact with the TRE API + +function usage() { + cat </dev/null || true) + +while [[ -z $output && $counter -lt $retries ]]; do + counter=$((counter+1)) + echo "Waiting for service principal with ID ${servicePrincipalId} to show up (${counter}/${retries})..." + sleep 5 + output=$(az rest --method GET --uri "https://graph.microsoft.com/v1.0/servicePrincipals/${servicePrincipalId}" 2>/dev/null || true) +done + +if [[ -z $output ]]; then + echo "Failed" + exit 1 +fi + +echo "Service principal with ID ${servicePrincipalId} found" diff --git a/scripts/create_aad_assets.sh b/scripts/create_aad_assets.sh new file mode 100755 index 0000000000..496451b5d3 --- /dev/null +++ b/scripts/create_aad_assets.sh @@ -0,0 +1,50 @@ +#!/bin/bash +set -e + +: "${AAD_TENANT_ID?'You have not set your AAD_TENANT_ID in ./templates/core/.env'}" + +LOGGED_IN_TENANT_ID=$(az account show --query tenantId -o tsv) +CHANGED_TENANT=0 + +if [ "${LOGGED_IN_TENANT_ID}" != "${AAD_TENANT_ID}" ]; then + echo "Attempting to sign you onto ${AAD_TENANT_ID} to add an App Registration." + + # First we need to login to the AAD tenant (as it is different to the subscription tenant) + az login --tenant "${AAD_TENANT_ID}" --allow-no-subscriptions --use-device-code + CHANGED_TENANT=1 +fi + +# Then register an App +./scripts/aad-app-reg.sh \ + --name "${TRE_ID}" \ + --swaggerui-redirecturl "https://${TRE_ID}.${RESOURCE_LOCATION}.cloudapp.azure.com/api/docs/oauth2-redirect" \ + --admin-consent --automation-account + +echo "Please copy the values above into your /templates/core/.env." +read -p "Please confirm you have done this? (y/N) " -n 1 -r +echo +if [[ ! $REPLY =~ ^[Yy]$ ]] +then + exit 0 +fi + +# Load the new values back in +set -a +# shellcheck disable=SC1091 +. ./templates/core/.env + +echo "Please check that the following value is the same as above to check you have copied yoru keys." +echo "Test account client id is : $TEST_ACCOUNT_CLIENT_ID" + +./scripts/aad-app-reg.sh \ + --name "${TRE_ID} - workspace 1" \ + --workspace --admin-consent \ + --swaggerui-clientid "${SWAGGER_UI_CLIENT_ID}" \ + --automation-clientid "${TEST_ACCOUNT_CLIENT_ID}" + +if [ "${CHANGED_TENANT}" -ne 0 ]; then + echo "Attempting to sign you back into ${LOGGED_IN_TENANT_ID}." + + # Log back into the tenant the user started on. + az login --tenant "${LOGGED_IN_TENANT_ID}" --allow-no-subscriptions +fi