diff --git a/README.md b/README.md index a70b98e..7b0c642 100644 --- a/README.md +++ b/README.md @@ -10,23 +10,7 @@ Because this solution uses Cosmos DB it is highly scalable and because it uses K The solution is fully deployed within your own tenant. All data processing and data storage is done in whichever tenant you deploy the application. -1. [High Level Architecture](./docs/high-level-architecture.md) - -1. [Requirements](./docs/requirements.md) - -1. [Deployment](./docs/deployment.md) - -1. [Validation & Verification](./docs/deployment.md#deployment-validation--verification) - -1. [Admin Functions](./docs/admin-functions.md) - -1. [CosmosDB, Kusto Functions, Views and Queries](./docs/data-explorer-functions-views-queries.md) - -1. [Configuration Settings](./docs/function-configuration-settings.md) - -1. [Multi Tenant](./docs/multi-tenant-deployment.md) - -1. [Troubleshooting](./docs/troubleshooting.md) +[Documentation](../../wiki) ## Contributing diff --git a/deploy/bicep/GraphChangeNotificationSubnets.jsonc b/deploy/bicep/GraphChangeNotificationSubnets.jsonc index 3eb8c68..27ca911 100644 --- a/deploy/bicep/GraphChangeNotificationSubnets.jsonc +++ b/deploy/bicep/GraphChangeNotificationSubnets.jsonc @@ -4,54 +4,97 @@ "52.159.23.209/32", "52.159.17.84/32", "13.78.204.0/32", + "52.148.24.136/32", + "52.148.27.39/32", "52.147.213.251/32", "52.147.213.181/32", "20.127.53.125/32", + "40.76.162.99/32", + "40.76.162.42/32", "70.37.95.92/32", "70.37.95.11/32", "70.37.92.195/32", + "70.37.93.191/32", + "70.37.90.219/32", "20.9.36.45/32", "20.9.35.166/32", "20.9.36.128/32", + "20.9.37.73/32", + "20.9.37.76/32", "20.96.21.67/32", "20.69.245.215/32", "104.46.117.15/32", + "20.96.21.98/32", + "20.96.21.115/32", "137.135.11.161/32", "137.135.11.116/32", "20.253.156.113/32", + "137.135.11.222/32", + "137.135.11.250/32", "52.159.107.50/32", "52.159.107.4/32", "52.159.124.33/32", + "52.159.109.205/32", + "52.159.102.72/32", "20.98.68.182/32", "20.98.68.57/32", "20.98.68.200/32", + "20.98.68.203/32", + "20.98.68.218/32", "20.171.81.121/32", "20.25.189.138/32", "20.171.82.192/32", + "20.171.83.146/32", + "20.171.83.157/32", "52.142.114.29/32", "52.142.115.31/32", "20.223.139.245/32", + "51.104.159.213/32", + "51.104.159.181/32", "51.124.75.43/32", "51.124.73.177/32", "104.40.209.182/32", + "51.138.90.7/32", + "51.138.90.52/32", "20.199.102.157/32", "20.199.102.73/32", "20.216.150.67/32", + "20.111.9.46/32", + "20.111.9.77/32", + "13.87.81.123/32", + "13.87.81.35/32", + "20.90.99.1/32", + "13.87.81.133/32", + "13.87.81.141/32", "20.91.212.211/32", "20.91.212.136/32", "20.91.213.57/32", + "20.91.208.88/32", + "20.91.209.147/32", "20.44.210.83/32", "20.44.210.146/32", "20.212.153.162/32", + "52.148.115.48/32", + "52.148.114.238/32", "40.80.232.177/32", "40.80.232.118/32", "52.231.196.24/32", + "40.80.233.14/32", + "40.80.239.196/32", "20.48.12.75/32", "20.48.11.201/32", "20.89.108.161/32", + "20.48.14.35/32", + "20.48.15.147/32", "104.215.13.23/32", "104.215.6.169/32", - "20.89.240.165/32" + "20.89.240.165/32", + "104.215.18.55/32", + "104.215.12.254/32", + "20.20.32.0/19", + "20.190.128.0/18", + "20.231.128.0/19", + "40.126.0.0/18" ], "AzureUSGovernment": [ "52.244.33.45/32", @@ -69,7 +112,9 @@ "52.182.32.51/32", "52.182.32.143/32", "52.181.24.199/32", - "52.181.24.220/32" + "52.181.24.220/32", + "20.140.232.0/23", + "52.126.194.0/23" ], "AzureChinaCloud": [ "42.159.72.35/32", @@ -79,6 +124,11 @@ "40.125.138.23/32", "40.125.136.69/32", "40.72.155.199/32", - "40.72.155.216/32" + "40.72.155.216/32", + "40.72.70.0/23", + "52.130.2.32/27", + "52.130.3.64/27", + "52.130.17.192/27", + "52.130.18.32/27" ] } \ No newline at end of file diff --git a/deploy/bicep/deployFunction.bicep b/deploy/bicep/deployFunction.bicep index b0d83d3..ce2f0c0 100644 --- a/deploy/bicep/deployFunction.bicep +++ b/deploy/bicep/deployFunction.bicep @@ -296,14 +296,14 @@ var graphChangeNotificationSubnets = loadJsonContent('GraphChangeNotificationSub // Event Hub resource eventHubNamespace 'Microsoft.EventHub/namespaces@2022-10-01-preview' = { - name: baseResourceName + name: length(baseResourceName) >= 6 ? baseResourceName : '${baseResourceName}${substring(uniqueString(baseResourceName),0, 6 - length(baseResourceName))}' location: location sku: configurations[deploymentType].eventHub.sku properties: configurations[deploymentType].eventHub.properties resource graphEventHub 'eventhubs' = { name: 'graphevents' properties: configurations[deploymentType].eventHub.eventhubs.properties - resource senderAuthorizationRule 'authorizationRules' = if (!useGraphEventHubManagedIdentity) { + resource senderAuthorizationRule 'authorizationRules' = { name: 'sender' properties: { rights: [ 'Send' ] } } @@ -338,12 +338,22 @@ resource serverfarm 'Microsoft.Web/serverfarms@2022-09-01' = { sku: configurations[deploymentType].serverfarm.sku } +var eventHubFQDN = split(split(eventHubNamespace.properties.serviceBusEndpoint,'://')[1],':')[0] + var GraphNotificationUrl = useGraphEventHubManagedIdentity /* -*/ ? 'EventHub:${eventHubNamespace.properties.serviceBusEndpoint}/eventhubname/${eventHubNamespace::graphEventHub.name}' /* +*/ ? 'EventHub:https://${eventHubFQDN}/eventhubname/${eventHubNamespace::graphEventHub.name}' /* */ : useSeparateKeyVaultForGraph /* */ ? 'EventHub:${graphKeyVault::graphEventHubConnectionString.properties.secretUri}' /* */ : 'EventHub:${keyvault::graphEventHubConnectionString.properties.secretUri}' +var GraphEndpoints = { + usgovvirginia : 'graph.microsoft.us' + usgovarizona : 'graph.microsoft.us' + usgovtexas : 'graph.microsoft.us' + usdodcentral : 'dod-graph.microsoft.us' + usdodeast : 'dod-graph.microsoft.us' +} + resource functionApp 'Microsoft.Web/sites@2022-09-01' = { name: '${baseResourceName}-function' location: location @@ -352,34 +362,49 @@ resource functionApp 'Microsoft.Web/sites@2022-09-01' = { properties: configurations[deploymentType].functionApp.properties resource appSettings 'config' = { name: 'appsettings' - properties: toObject([ + properties: toObject(flatten([ + [ { key: 'RenewSubscriptionScheduleCron', value: '0 0 */2 * * *' } // CallRecords Queue Configuration { key: 'CallRecordsQueueConnection__queueServiceUri', value: storageAccount.properties.primaryEndpoints.queue } { key: 'CallRecordsQueueConnection__credential', value: 'managedidentity' } { key: 'CallRecordsToDownloadQueueName', value: storageAccount::queues::download.name } - + // Graph Subscription Manager Configuration { key: 'GraphSubscription__NotificationUrl', value: GraphNotificationUrl } { key: 'GraphSubscription__Tenants', value: tenantDomain } - + { key: 'CallRecordInsightsDb__EndpointUri', value: cosmosAccount.properties.documentEndpoint } { key: 'CallRecordInsightsDb__DatabaseName', value: cosmosAccount::database.properties.resource.id } { key: 'CallRecordInsightsDb__ProcessedContainerName', value: cosmosAccount::database::container.properties.resource.id } - + { key: 'GraphNotificationEventHubName', value: eventHubNamespace::graphEventHub.name } - { key: 'EventHubConnection__fullyQualifiedNamespace', value: split(split(eventHubNamespace.properties.serviceBusEndpoint,'://')[1],':')[0] } + { key: 'EventHubConnection__fullyQualifiedNamespace', value: eventHubFQDN } { key: 'EventHubConnection__credential', value: 'managedidentity' } - - { key: 'AzureWebJobsStorage__accountName', value: storageAccount.name } + { key: 'AzureWebJobsSecretStorageType', value: 'keyvault' } { key: 'AzureWebJobsSecretStorageKeyVaultUri', value: keyvault.properties.vaultUri } { key: 'FUNCTIONS_EXTENSION_VERSION', value: '~4' } { key: 'FUNCTIONS_WORKER_RUNTIME', value: 'dotnet' } { key: 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', value: '@Microsoft.KeyVault(VaultName=${keyvault.name};SecretName=${keyvault::storageAccountConnectionString.name})' } { key: 'WEBSITE_CONTENTSHARE', value: toLower(functionApp.name) } - ], o => o.key, o => o.value) + { key: 'SCM_COMMAND_IDLE_TIMEOUT', value: '1800' } + ] + contains(GraphEndpoints, location) ? [ // GCCH/DoD Configuration + { key: 'GraphSubscription__Endpoint', value: GraphEndpoints[location] } + { key: 'AzureAd__Instance', value: environment().authentication.loginEndpoint } + { key: 'AzureWebJobsStorage__blobServiceUri', value: storageAccount.properties.primaryEndpoints.blob } + { key: 'AzureWebJobsStorage__queueServiceUri', value: storageAccount.properties.primaryEndpoints.queue } + { key: 'AzureWebJobsStorage__tableServiceUri', value: storageAccount.properties.primaryEndpoints.table } + ] : [ // Non-GCCH/DoD Configuration + { key: 'AzureWebJobsStorage__accountName', value: storageAccount.name } + ]]), o => o.key, o => o.value) + dependsOn: [ + functionAppKeyVaultRoleAssignment // Ensure the function app has access to the key vault before reading referenced secrets + functionAppEventHubsRoleAssignment + functionAppStorageRoleAssignment + ] } } @@ -397,7 +422,7 @@ resource keyvault 'Microsoft.KeyVault/vaults@2023-02-01' = { } } - resource graphEventHubConnectionString 'secrets@2023-02-01' = if (!useGraphEventHubManagedIdentity && !useSeparateKeyVaultForGraph) { + resource graphEventHubConnectionString 'secrets' = if (!useGraphEventHubManagedIdentity && !useSeparateKeyVaultForGraph) { name: 'GraphEventHubConnectionString' properties: { value: eventHubNamespace::graphEventHub::senderAuthorizationRule.listkeys().primaryConnectionString @@ -413,7 +438,7 @@ resource graphKeyVault 'Microsoft.KeyVault/vaults@2023-02-01' = if (!useGraphEve location: location properties: configurations[deploymentType].keyvault.properties - resource graphEventHubConnectionString 'secrets' = if (!useGraphEventHubManagedIdentity && useSeparateKeyVaultForGraph) { + resource graphEventHubConnectionString 'secrets' = { name: 'GraphEventHubConnectionString' properties: { value: eventHubNamespace::graphEventHub::senderAuthorizationRule.listkeys().primaryConnectionString diff --git a/deploy/bicep/deployKusto.bicep b/deploy/bicep/deployKusto.bicep index cad690c..60dd6aa 100644 --- a/deploy/bicep/deployKusto.bicep +++ b/deploy/bicep/deployKusto.bicep @@ -22,12 +22,14 @@ param existingKustoClusterName string = 'NOEXISTINGKUSTOCLUSTER' @description('The type of identity to assign to the cluster. SystemAssigned is the default. None will not assign an identity. UserAssigned will assign the identity specified in the identity parameter.') param identity string = 'SystemAssigned' +var isGovCloud = startsWith(location, 'usgov') || startsWith(location, 'usdod') + // T-Shirt sizing var clusterConfigurations = { DevTest: { cluster: { sku: { - name: 'Dev(No SLA)_Standard_E2a_v4' + name: isGovCloud ? 'Dev(No SLA)_Standard_D11_v2' : 'Dev(No SLA)_Standard_E2a_v4' tier: 'Basic' capacity: 1 } @@ -45,7 +47,7 @@ var clusterConfigurations = { Production: { cluster: { sku: { - name: 'Standard_E2ads_v5' + name: isGovCloud ? 'Standard_D11_v2' : 'Standard_E2ads_v5' tier: 'Standard' capacity: 2 } @@ -58,11 +60,11 @@ var clusterConfigurations = { } } } - // this will eventually be locked down netowrk wise, but for now, just using the same as production + // this will eventually be locked down network wise, but for now, just using the same as production RestrictedProduction: { cluster: { sku: { - name: 'Standard_E2ads_v5' + name: isGovCloud ? 'Standard_D11_v2' : 'Standard_E2ads_v5' tier: 'Standard' capacity: 2 } diff --git a/deploy/deploy.ps1 b/deploy/deploy.ps1 index 68b337a..c71e238 100644 --- a/deploy/deploy.ps1 +++ b/deploy/deploy.ps1 @@ -23,10 +23,11 @@ param( $DeploymentSize = 'Production', [Parameter(HelpMessage = 'The Azure region that''s right for you. Not every resource is available in every region.')] - [ValidateSet('australiacentral', 'australiaeast', 'australiasoutheast', 'brazilsouth', 'canadacentral', 'canadaeast', 'centralindia', 'centralus', 'eastasia', - 'eastus', 'eastus2', 'francecentral', 'germanywestcentral', 'japaneast', 'japanwest', 'koreacentral', 'koreasouth', 'northcentralus', 'northeurope', 'norwayeast', - 'polandcentral', 'qatarcentral', 'southafricanorth', 'southcentralus', 'southeastasia', 'southindia', 'swedencentral', 'switzerlandnorth', 'uaenorth', 'uksouth', - 'ukwest', 'westcentralus', 'westeurope', 'westindia', 'westus', 'westus2', 'westus3')] + [ValidateSet('australiaeast', 'brazilsouth', 'canadacentral', 'centralindia', 'centralus', 'eastasia', 'eastus', 'eastus2', + 'francecentral', 'germanywestcentral', 'japaneast', 'koreacentral', 'northcentralus', 'northeurope', 'norwayeast', + 'polandcentral', 'southafricanorth', 'southcentralus', 'southeastasia', 'swedencentral', 'switzerlandnorth', + 'uaenorth', 'uksouth', 'westcentralus', 'westeurope', 'westus', 'westus2', 'westus3', + 'usgovvirginia', 'usgovarizona', 'usgovtexas', 'usdodcentral')] [string] $Location = 'westus', @@ -82,9 +83,21 @@ if (!(Get-Command -Name az -CommandType Application -ErrorAction SilentlyContinu return } +$GraphEndpoint = switch ($Location) { + { $_ -in @('usgovvirginia', 'usgovarizona', 'usgovtexas', 'usgoviowa') } { 'graph.microsoft.us' } + { $_ -in @('usdodcentral','usdodeast') } { 'dod-graph.microsoft.us' } + default { 'graph.microsoft.com' } +} + +$AzureCloud = switch ($GraphEndpoint) { + { $_ -in @('usgovvirginia', 'usgovarizona', 'usgovtexas', 'usgoviowa') } { 'AzureUSGovernment' } + { $_ -in @('usdodcentral','usdodeast') } { 'AzureUSGovernment' } + default { 'AzureCloud' } +} + $AzCommands = @{ - getdeployment = { az deployment group show --resource-group $args[0] --name $args[1] --query properties 2>&1 } - deploybicep = { + getdeployment = { az deployment group show --resource-group $args[0] --name $args[1] --query properties 2>&1 } + deploybicep = { param( [string] $ResourceGroupName, @@ -119,27 +132,33 @@ $AzCommands = @{ } az @parameters 2>&1 } - getdeploymentoperations = { az deployment operation group list --resource-group $args[0] --name $args[1] --query "[].{provisioningState:properties.provisioningState,targetResource:properties.targetResource.id,statusMessage:properties.statusMessage.error.message}" 2>&1 } - getazsubscription = { az account show --query id 2>&1 } - connect = { az login 2>&1; if ($LASTEXITCODE -eq 0) { az account set --subscription $args[0] 2>&1 } } - getazusername = { az ad signed-in-user show --query userPrincipalName 2>&1 } - getazoid = { az ad signed-in-user show --query id 2>&1 } - getaztenant = { az account show --query tenantId 2>&1 } - getspnobjectid = { az ad sp list --spn $args[0] --query "[].id" 2>&1 } - getresourcegroup = { az group show --name $args[0] 2>&1 } - createresourcegroup = { az group create --name $args[0] --location $args[1] 2>&1 } - getaadapproleassignments = { az rest --method get --url "https://graph.microsoft.com/v1.0/servicePrincipals/$($args[0])/appRoleAssignments" 2>&1 } - addaadapproleassignment = { + getdeploymentoperations = { az deployment operation group list --resource-group $args[0] --name $args[1] --query "[].{provisioningState:properties.provisioningState,targetResource:properties.targetResource.id,statusMessage:properties.statusMessage.error.message}" 2>&1 } + getenvironment = { az cloud show --query name 2>&1 } + setenvironment = { az cloud set --name $args[0] 2>&1 } + getazsubscription = { az account show --query id 2>&1 } + connect = { az login 2>&1; if ($LASTEXITCODE -eq 0) { az account set --subscription $args[0] 2>&1 } } + getazusername = { az ad signed-in-user show --query userPrincipalName 2>&1 } + getazoid = { az ad signed-in-user show --query id 2>&1 } + getaztenant = { az account show --query tenantId 2>&1 } + getspnobjectid = { az ad sp list --spn $args[0] --query "[].id" 2>&1 } + createspn = { az ad sp create --id $args[0] --query "[].id" 2>&1 } + getresourcegroup = { az group show --name $args[0] 2>&1 } + createresourcegroup = { az group create --name $args[0] --location $args[1] 2>&1 } + getaadapproleassignments = { az rest --method get --url "https://${GraphEndpoint}/v1.0/servicePrincipals/$($args[0])/appRoleAssignments" 2>&1 } + addaadapproleassignment = { $request = @{ principalId = $args[0] resourceId = $args[1] appRoleId = $args[2] } $body = ($request | ConvertTo-Json -Compress).Replace('"', '\"') - az rest --method post --url "https://graph.microsoft.com/v1.0/servicePrincipals/$($args[0])/appRoleAssignments" --body "$body" 2>&1 + az rest --method post --url "https://${GraphEndpoint}/v1.0/servicePrincipals/$($args[0])/appRoleAssignments" --body "$body" 2>&1 } - getwebappdeployment = { az webapp log deployment show --resource-group $args[0] --name $args[1] 2>&1 } - getmasterkey = { az functionapp keys list --resource-group $args[0] --name $args[1] --query masterKey 2>&1 } + getwebappdeployment = { az webapp log deployment show --resource-group $args[0] --name $args[1] 2>&1 } + getmasterkey = { az functionapp keys list --resource-group $args[0] --name $args[1] --query masterKey 2>&1 } + getwebappdeploymentlogs = { az webapp log deployment show --resource-group $args[0] --name $args[1] --query "[].{time:log_time,message:message,url:details_url}" 2>&1 } + getwebapppublishingcredentials = { az webapp deployment list-publishing-credentials --resource-group $args[0] --name $args[1] --query "{pwd:publishingPassword,un:publishingUserName}" 2>&1 } + getwebappdeploymentlogdetails = { az rest --uri ($args[0] -replace '(?<=://)',"$($args[1].un):$($args[1].pwd)@") --skip-authorization-header --query "[].{time:log_time,message:message,url:details_url}" 2>&1 } } function FormatDictionary { @@ -281,34 +300,56 @@ function deployifneeded { $Errors | ForEach-Object { Write-Error -ErrorRecord $_ } return $null } - $state = $Deployment.provisioningState.ToLower() - $JobStatus = "$DeploymentType deployment is $state." - if ($StatusMessages.Add($JobStatus)) { - Write-Host $JobStatus - } - $Errors, $OperationState = TryExecuteMethod getdeploymentoperations $ResourceGroupName $DeploymentName - if (!$Errors) { - @($OperationState | Sort-Object -Property provisioningState, targetResource).ForEach({ - if (!$_.targetResource) { return } - $verb = if ($_.provisioningState.EndsWith('ed')) { 'has' } else { 'is' } - $Message = "Deploy Resource: $($_.targetResource.Split('/providers/',2)[1]) $verb $($_.provisioningState)." - if ($StatusMessages.Add($Message)) { - $Color = if ($Message -match 'has Succeeded') { - 'Green' - } elseif ($Message -match 'has Failed') { - 'Red' - } else { - 'White' + + $nestedDeployments = [Stack[string]]@($DeploymentName) + $processedDeployments = [HashSet[string]]@() + + while ($nestedDeployments.Count -gt 0) { + $nestedDeploymentName = $nestedDeployments.Pop() + if (!$processedDeployments.Add($nestedDeploymentName)) { continue } + $Errors, $OperationState = TryExecuteMethod getdeploymentoperations $ResourceGroupName $nestedDeploymentName + if (!$Errors) { + $OperationState | ForEach-Object { + if (!$_.targetResource) { return } + if ($_.targetResource.Split('/')[-2] -eq 'deployments') { + $nestedDeployments.Push($_.targetResource.Split('/')[-1]) } - Write-Host $Message -ForegroundColor $Color - if ($_.provisioningState -eq 'Failed') { - Write-Warning "$($_.statusMessage)" + $verb = if ($_.provisioningState.EndsWith('ed')) { 'has' } else { 'is' } + $Message = "Deploy Resource: $($_.targetResource.Split('/providers/',2)[1]) $verb $($_.provisioningState)." + if ($StatusMessages.Add($Message)) { + $Color = if ($Message -match 'has Succeeded') { + 'Green' + } elseif ($Message -match 'has Failed') { + 'Red' + } else { + 'White' + } + Write-Host $Message -ForegroundColor $Color + if ($_.provisioningState -eq 'Failed') { + Write-Warning "$($_.statusMessage)" + } } } - }) + } + } + + # $state = $Deployment.provisioningState.ToLower() + $verb = if ($Deployment.provisioningState.EndsWith('ed')) { 'has' } else { 'is' } + + $Message = "$DeploymentType deployment $verb $($Deployment.provisioningState)." + if ($StatusMessages.Add($Message)) { + $Color = if ($Message -match 'has Succeeded') { + 'Green' + } elseif ($Message -match 'has Failed') { + 'Red' + } else { + 'White' + } + Write-Host $Message -ForegroundColor $Color } + $Errors = $null - switch ($state) { + switch ($Deployment.provisioningState) { 'cancelled' { Write-Warning "$DeploymentType in resource group '$ResourceGroupName' deployment was cancelled! Please try again." return $null @@ -329,6 +370,22 @@ Write-Host "Ensuring we are connected to subscription '$SubscriptionId'." $Errors, $ConnectedSubscriptionId = TryExecuteMethod getazsubscription if ($Errors -or $null -eq $ConnectedSubscriptionId -or $ConnectedSubscriptionId -ne $SubscriptionId) { + $Errors, $CurrentAzureCloud = TryExecuteMethod getenvironment $AzureCloud + if ($Errors) { + Write-Error "Failed to get the current environment. Please ensure you have access to the subscription and try again." + $Errors | ForEach-Object { Write-Error -ErrorRecord $_ } + return + } + if ($CurrentAzureCloud -ne $AzureCloud) { + Write-Host "Setting the environment to '$AzureCloud'." + $Errors, $null = TryExecuteMethod setenvironment $AzureCloud + if ($Errors) { + Write-Error "Failed to set the environment to '$AzureCloud'. Please ensure you have access to the subscription and try again." + $Errors | ForEach-Object { Write-Error -ErrorRecord $_ } + return + } + } + Write-Host "Connecting to subscription '$SubscriptionId'. Please login if prompted." $Errors, $null = TryExecuteMethod connect $SubscriptionId if ($Errors) { @@ -380,6 +437,15 @@ if ($Errors) { $Errors | ForEach-Object { Write-Error -ErrorRecord $_ } return } +if ($null -eq $GraphChangeTrackingSPNObjectId) { + Write-Host "Creating SPN for the Microsoft Graph Change Tracking app." + $Errors, $GraphChangeTrackingSPNObjectId = TryExecuteMethod createspn '0bf30f3b-4a52-48df-9a82-234910c4a086' + if ($Errors) { + Write-Error "Failed to create the SPN object id for the Microsoft Graph Change Tracking app. Please ensure you have access to the tenant and try again." + $Errors | ForEach-Object { Write-Error -ErrorRecord $_ } + return + } +} if ($GraphChangeTrackingSPNObjectId -isnot [string]) { $GraphChangeTrackingSPNObjectId = $GraphChangeTrackingSPNObjectId[0] } Write-Host "SPN object id for the Microsoft Graph Change Tracking app is '$GraphChangeTrackingSPNObjectId'." -ForegroundColor Green Write-Host @@ -466,8 +532,45 @@ while (!$deployed) { } $latestmessage = if ($null -ne $CurrentDeploymentLogs) { $CurrentDeploymentLogs[-1].message } else { '' } if ($latestmessage -cmatch 'Deployment Failed.') { - $detailsUrl = $CurrentDeploymentLogs.Where({$_.details_url},'First',1)[0].details_url -replace '(?<=logs)/[^/]+$' - Write-Error "Failed to deploy function app '$functionName'. See $detailsUrl for more details." + Write-Error "Failed to deploy function app '$functionName'. Getting deployment logs..." + + $Errors, $MainDeploymentLogs = TryExecuteMethod getwebappdeploymentlogs $ResourceGroupName $functionName + if ($Errors.Count -gt 0) { + $MainDeploymentLogs = $null + Write-Error "Failed to get deployment logs for function app '$functionName'." + $Errors | ForEach-Object { Write-Error $_.Exception.Message } + } + $NeedDetail = @($MainDeploymentLogs | Where-Object { $_.url }) + $DeploymentLogDetails = if ($NeedDetail.Count -gt 0) { + $Errors, $PublishingCredentials = TryExecuteMethod getwebapppublishingcredentials $ResourceGroupName $functionName + if ($Errors.Count -gt 0) { + Write-Error "Failed to get publishing credentials for function app '$functionName'." + $PublishingCredentials = $null + $Errors | ForEach-Object { Write-Error $_.Exception.Message } + return + } + @($NeedDetail | ForEach-Object { + if ($null -eq $PublishingCredentials.un -or $null -eq $PublishingCredentials.pwd) { return } + $Errors, $Details = TryExecuteMethod getwebappdeploymentlogdetails $_.url $PublishingCredentials + if ($Errors.Count -gt 0) { + Write-Error "Failed to get deployment log details for function app '$functionName'." + $Details = $null + $Errors | ForEach-Object { Write-Error $_.Exception.Message } + return + } + $Details + }) + } else { + @() + } + + $DeploymentLogs = $MainDeploymentLogs + $DeploymentLogDetails | Where-Object { $_.time } | + Sort-Object time | ForEach-Object {'{0:o}: {1}' -f $_.time, $_.message } + + if ($DeploymentLogs.Count -gt 0) { + Write-Warning "Deployment logs for function app '$functionName':`n$($DeploymentLogs -join "`n")" + } + return } $deployed = $latestmessage -match '^\s*deployment\s+successful\.\s*$' diff --git a/deploy/resourcemanager/template.json b/deploy/resourcemanager/template.json index 5e899e5..647a4e6 100644 --- a/deploy/resourcemanager/template.json +++ b/deploy/resourcemanager/template.json @@ -4,8 +4,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.25.3.34343", - "templateHash": "2662921431620975723" + "version": "0.28.1.47646", + "templateHash": "17485759625502517747" } }, "parameters": { @@ -136,8 +136,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.25.3.34343", - "templateHash": "5023612052478866940" + "version": "0.28.1.47646", + "templateHash": "2791479325930176374" } }, "parameters": { @@ -190,11 +190,12 @@ } }, "variables": { + "isGovCloud": "[or(startsWith(parameters('location'), 'usgov'), startsWith(parameters('location'), 'usdod'))]", "clusterConfigurations": { "DevTest": { "cluster": { "sku": { - "name": "Dev(No SLA)_Standard_E2a_v4", + "name": "[if(variables('isGovCloud'), 'Dev(No SLA)_Standard_D11_v2', 'Dev(No SLA)_Standard_E2a_v4')]", "tier": "Basic", "capacity": 1 }, @@ -212,7 +213,7 @@ "Production": { "cluster": { "sku": { - "name": "Standard_E2ads_v5", + "name": "[if(variables('isGovCloud'), 'Standard_D11_v2', 'Standard_E2ads_v5')]", "tier": "Standard", "capacity": 2 }, @@ -228,7 +229,7 @@ "RestrictedProduction": { "cluster": { "sku": { - "name": "Standard_E2ads_v5", + "name": "[if(variables('isGovCloud'), 'Standard_D11_v2', 'Standard_E2ads_v5')]", "tier": "Standard", "capacity": 2 }, @@ -436,8 +437,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.25.3.34343", - "templateHash": "994269997504341877" + "version": "0.28.1.47646", + "templateHash": "15512555221821624675" } }, "parameters": { @@ -645,8 +646,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.25.3.34343", - "templateHash": "7928089187257027743" + "version": "0.28.1.47646", + "templateHash": "12813007253047072709" } }, "parameters": { @@ -730,54 +731,97 @@ "52.159.23.209/32", "52.159.17.84/32", "13.78.204.0/32", + "52.148.24.136/32", + "52.148.27.39/32", "52.147.213.251/32", "52.147.213.181/32", "20.127.53.125/32", + "40.76.162.99/32", + "40.76.162.42/32", "70.37.95.92/32", "70.37.95.11/32", "70.37.92.195/32", + "70.37.93.191/32", + "70.37.90.219/32", "20.9.36.45/32", "20.9.35.166/32", "20.9.36.128/32", + "20.9.37.73/32", + "20.9.37.76/32", "20.96.21.67/32", "20.69.245.215/32", "104.46.117.15/32", + "20.96.21.98/32", + "20.96.21.115/32", "137.135.11.161/32", "137.135.11.116/32", "20.253.156.113/32", + "137.135.11.222/32", + "137.135.11.250/32", "52.159.107.50/32", "52.159.107.4/32", "52.159.124.33/32", + "52.159.109.205/32", + "52.159.102.72/32", "20.98.68.182/32", "20.98.68.57/32", "20.98.68.200/32", + "20.98.68.203/32", + "20.98.68.218/32", "20.171.81.121/32", "20.25.189.138/32", "20.171.82.192/32", + "20.171.83.146/32", + "20.171.83.157/32", "52.142.114.29/32", "52.142.115.31/32", "20.223.139.245/32", + "51.104.159.213/32", + "51.104.159.181/32", "51.124.75.43/32", "51.124.73.177/32", "104.40.209.182/32", + "51.138.90.7/32", + "51.138.90.52/32", "20.199.102.157/32", "20.199.102.73/32", "20.216.150.67/32", + "20.111.9.46/32", + "20.111.9.77/32", + "13.87.81.123/32", + "13.87.81.35/32", + "20.90.99.1/32", + "13.87.81.133/32", + "13.87.81.141/32", "20.91.212.211/32", "20.91.212.136/32", "20.91.213.57/32", + "20.91.208.88/32", + "20.91.209.147/32", "20.44.210.83/32", "20.44.210.146/32", "20.212.153.162/32", + "52.148.115.48/32", + "52.148.114.238/32", "40.80.232.177/32", "40.80.232.118/32", "52.231.196.24/32", + "40.80.233.14/32", + "40.80.239.196/32", "20.48.12.75/32", "20.48.11.201/32", "20.89.108.161/32", + "20.48.14.35/32", + "20.48.15.147/32", "104.215.13.23/32", "104.215.6.169/32", - "20.89.240.165/32" + "20.89.240.165/32", + "104.215.18.55/32", + "104.215.12.254/32", + "20.20.32.0/19", + "20.190.128.0/18", + "20.231.128.0/19", + "40.126.0.0/18" ], "AzureUSGovernment": [ "52.244.33.45/32", @@ -795,7 +839,9 @@ "52.182.32.51/32", "52.182.32.143/32", "52.181.24.199/32", - "52.181.24.220/32" + "52.181.24.220/32", + "20.140.232.0/23", + "52.126.194.0/23" ], "AzureChinaCloud": [ "42.159.72.35/32", @@ -805,7 +851,12 @@ "40.125.138.23/32", "40.125.136.69/32", "40.72.155.199/32", - "40.72.155.216/32" + "40.72.155.216/32", + "40.72.70.0/23", + "52.130.2.32/27", + "52.130.3.64/27", + "52.130.17.192/27", + "52.130.18.32/27" ] }, "tenantId": "[subscription().tenantId]", @@ -1000,6 +1051,13 @@ "Custom": {} }, "graphChangeNotificationSubnets": "[variables('$fxv#0')]", + "GraphEndpoints": { + "usgovvirginia": "graph.microsoft.us", + "usgovarizona": "graph.microsoft.us", + "usgovtexas": "graph.microsoft.us", + "usdodcentral": "dod-graph.microsoft.us", + "usdodeast": "dod-graph.microsoft.us" + }, "roleDefinitionId": { "StorageAccount": { "Contributor": "17d1049b-9a84-46fb-8f53-869881c3d3ab", @@ -1119,26 +1177,25 @@ ] }, { - "condition": "[not(parameters('useGraphEventHubManagedIdentity'))]", "type": "Microsoft.EventHub/namespaces/eventhubs/authorizationRules", "apiVersion": "2022-10-01-preview", - "name": "[format('{0}/{1}/{2}', parameters('baseResourceName'), 'graphevents', 'sender')]", + "name": "[format('{0}/{1}/{2}', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents', 'sender')]", "properties": { "rights": [ "Send" ] }, "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces/eventhubs', parameters('baseResourceName'), 'graphevents')]" + "[resourceId('Microsoft.EventHub/namespaces/eventhubs', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents')]" ] }, { "type": "Microsoft.EventHub/namespaces/eventhubs", "apiVersion": "2022-10-01-preview", - "name": "[format('{0}/{1}', parameters('baseResourceName'), 'graphevents')]", + "name": "[format('{0}/{1}', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents')]", "properties": "[variables('configurations')[parameters('deploymentType')].eventHub.eventhubs.properties]", "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces', parameters('baseResourceName'))]", + "[resourceId('Microsoft.EventHub/namespaces', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))))]", "[resourceId('Microsoft.Network/virtualNetworks/subnets', format('vnet-{0}', resourceGroup().name), 'fnapp-subnet')]" ] }, @@ -1146,7 +1203,7 @@ "condition": "[startsWith(parameters('deploymentType'), 'Restricted')]", "type": "Microsoft.EventHub/namespaces/networkRuleSets", "apiVersion": "2022-10-01-preview", - "name": "[format('{0}/{1}', parameters('baseResourceName'), 'default')]", + "name": "[format('{0}/{1}', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'default')]", "properties": { "publicNetworkAccess": "SecuredByPerimeter", "trustedServiceAccessEnabled": true, @@ -1161,7 +1218,7 @@ "ipRules": "[map(variables('graphChangeNotificationSubnets')[environment().name], lambda('s', createObject('action', 'Allow', 'ipMask', lambdaVariables('s'))))]" }, "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces', parameters('baseResourceName'))]", + "[resourceId('Microsoft.EventHub/namespaces', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))))]", "[resourceId('Microsoft.Network/virtualNetworks/subnets', format('vnet-{0}', resourceGroup().name), 'fnapp-subnet')]" ] }, @@ -1169,12 +1226,15 @@ "type": "Microsoft.Web/sites/config", "apiVersion": "2022-09-01", "name": "[format('{0}/{1}', format('{0}-function', parameters('baseResourceName')), 'appsettings')]", - "properties": "[toObject(createArray(createObject('key', 'RenewSubscriptionScheduleCron', 'value', '0 0 */2 * * *'), createObject('key', 'CallRecordsQueueConnection__queueServiceUri', 'value', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').primaryEndpoints.queue), createObject('key', 'CallRecordsQueueConnection__credential', 'value', 'managedidentity'), createObject('key', 'CallRecordsToDownloadQueueName', 'value', variables('downloadQueueName')), createObject('key', 'GraphSubscription__NotificationUrl', 'value', if(parameters('useGraphEventHubManagedIdentity'), format('EventHub:{0}/eventhubname/{1}', reference(resourceId('Microsoft.EventHub/namespaces', parameters('baseResourceName')), '2022-10-01-preview').serviceBusEndpoint, 'graphevents'), if(parameters('useSeparateKeyVaultForGraph'), format('EventHub:{0}', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('graphKeyVaultName'), 'GraphEventHubConnectionString'), '2023-02-01').secretUri), format('EventHub:{0}', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyvaultName'), 'GraphEventHubConnectionString'), '2023-02-01').secretUri)))), createObject('key', 'GraphSubscription__Tenants', 'value', variables('tenantDomain')), createObject('key', 'CallRecordInsightsDb__EndpointUri', 'value', reference(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosAccountName')), '2023-09-15').documentEndpoint), createObject('key', 'CallRecordInsightsDb__DatabaseName', 'value', reference(resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('cosmosAccountName'), parameters('callRecordsDatabaseName')), '2023-09-15').resource.id), createObject('key', 'CallRecordInsightsDb__ProcessedContainerName', 'value', reference(resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('cosmosAccountName'), parameters('callRecordsDatabaseName'), parameters('callRecordsContainerName')), '2023-09-15').resource.id), createObject('key', 'GraphNotificationEventHubName', 'value', 'graphevents'), createObject('key', 'EventHubConnection__fullyQualifiedNamespace', 'value', split(split(reference(resourceId('Microsoft.EventHub/namespaces', parameters('baseResourceName')), '2022-10-01-preview').serviceBusEndpoint, '://')[1], ':')[0]), createObject('key', 'EventHubConnection__credential', 'value', 'managedidentity'), createObject('key', 'AzureWebJobsStorage__accountName', 'value', variables('storageAccountName')), createObject('key', 'AzureWebJobsSecretStorageType', 'value', 'keyvault'), createObject('key', 'AzureWebJobsSecretStorageKeyVaultUri', 'value', reference(resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName')), '2023-02-01').vaultUri), createObject('key', 'FUNCTIONS_EXTENSION_VERSION', 'value', '~4'), createObject('key', 'FUNCTIONS_WORKER_RUNTIME', 'value', 'dotnet'), createObject('key', 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1})', variables('keyvaultName'), 'StorageAccountConnectionString')), createObject('key', 'WEBSITE_CONTENTSHARE', 'value', toLower(format('{0}-function', parameters('baseResourceName'))))), lambda('o', lambdaVariables('o').key), lambda('o', lambdaVariables('o').value))]", + "properties": "[toObject(flatten(createArray(createArray(createObject('key', 'RenewSubscriptionScheduleCron', 'value', '0 0 */2 * * *'), createObject('key', 'CallRecordsQueueConnection__queueServiceUri', 'value', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').primaryEndpoints.queue), createObject('key', 'CallRecordsQueueConnection__credential', 'value', 'managedidentity'), createObject('key', 'CallRecordsToDownloadQueueName', 'value', variables('downloadQueueName')), createObject('key', 'GraphSubscription__NotificationUrl', 'value', if(parameters('useGraphEventHubManagedIdentity'), format('EventHub:https://{0}/eventhubname/{1}', split(split(reference(resourceId('Microsoft.EventHub/namespaces', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName'))))))), '2022-10-01-preview').serviceBusEndpoint, '://')[1], ':')[0], 'graphevents'), if(parameters('useSeparateKeyVaultForGraph'), format('EventHub:{0}', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('graphKeyVaultName'), 'GraphEventHubConnectionString'), '2023-02-01').secretUri), format('EventHub:{0}', reference(resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyvaultName'), 'GraphEventHubConnectionString'), '2023-02-01').secretUri)))), createObject('key', 'GraphSubscription__Tenants', 'value', variables('tenantDomain')), createObject('key', 'CallRecordInsightsDb__EndpointUri', 'value', reference(resourceId('Microsoft.DocumentDB/databaseAccounts', parameters('cosmosAccountName')), '2023-09-15').documentEndpoint), createObject('key', 'CallRecordInsightsDb__DatabaseName', 'value', reference(resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases', parameters('cosmosAccountName'), parameters('callRecordsDatabaseName')), '2023-09-15').resource.id), createObject('key', 'CallRecordInsightsDb__ProcessedContainerName', 'value', reference(resourceId('Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers', parameters('cosmosAccountName'), parameters('callRecordsDatabaseName'), parameters('callRecordsContainerName')), '2023-09-15').resource.id), createObject('key', 'GraphNotificationEventHubName', 'value', 'graphevents'), createObject('key', 'EventHubConnection__fullyQualifiedNamespace', 'value', split(split(reference(resourceId('Microsoft.EventHub/namespaces', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName'))))))), '2022-10-01-preview').serviceBusEndpoint, '://')[1], ':')[0]), createObject('key', 'EventHubConnection__credential', 'value', 'managedidentity'), createObject('key', 'AzureWebJobsSecretStorageType', 'value', 'keyvault'), createObject('key', 'AzureWebJobsSecretStorageKeyVaultUri', 'value', reference(resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName')), '2023-02-01').vaultUri), createObject('key', 'FUNCTIONS_EXTENSION_VERSION', 'value', '~4'), createObject('key', 'FUNCTIONS_WORKER_RUNTIME', 'value', 'dotnet'), createObject('key', 'WEBSITE_CONTENTAZUREFILECONNECTIONSTRING', 'value', format('@Microsoft.KeyVault(VaultName={0};SecretName={1})', variables('keyvaultName'), 'StorageAccountConnectionString')), createObject('key', 'WEBSITE_CONTENTSHARE', 'value', toLower(format('{0}-function', parameters('baseResourceName')))), createObject('key', 'SCM_COMMAND_IDLE_TIMEOUT', 'value', '1800')), if(contains(variables('GraphEndpoints'), parameters('location')), createArray(createObject('key', 'GraphSubscription__Endpoint', 'value', variables('GraphEndpoints')[parameters('location')]), createObject('key', 'AzureAd__Instance', 'value', environment().authentication.loginEndpoint), createObject('key', 'AzureWebJobsStorage__blobServiceUri', 'value', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').primaryEndpoints.blob), createObject('key', 'AzureWebJobsStorage__queueServiceUri', 'value', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').primaryEndpoints.queue), createObject('key', 'AzureWebJobsStorage__tableServiceUri', 'value', reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2022-09-01').primaryEndpoints.table)), createArray(createObject('key', 'AzureWebJobsStorage__accountName', 'value', variables('storageAccountName')))))), lambda('o', lambdaVariables('o').key), lambda('o', lambdaVariables('o').value))]", "dependsOn": [ "[resourceId('Microsoft.Storage/storageAccounts/queueServices/queues', variables('storageAccountName'), 'default', variables('downloadQueueName'))]", - "[resourceId('Microsoft.EventHub/namespaces', parameters('baseResourceName'))]", + "[resourceId('Microsoft.EventHub/namespaces', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))))]", "[resourceId('Microsoft.Web/sites', format('{0}-function', parameters('baseResourceName')))]", - "[resourceId('Microsoft.EventHub/namespaces/eventhubs', parameters('baseResourceName'), 'graphevents')]", + "functionAppEventHubsRoleAssignment", + "functionAppKeyVaultRoleAssignment", + "functionAppStorageRoleAssignment", + "[resourceId('Microsoft.EventHub/namespaces/eventhubs', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents')]", "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('graphKeyVaultName'), 'GraphEventHubConnectionString')]", "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyvaultName'), 'GraphEventHubConnectionString')]", "[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]", @@ -1204,7 +1264,7 @@ "apiVersion": "2023-02-01", "name": "[format('{0}/{1}', variables('keyvaultName'), 'GraphEventHubConnectionString')]", "properties": { - "value": "[listkeys(resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', parameters('baseResourceName'), 'graphevents', 'sender'), '2022-10-01-preview').primaryConnectionString]", + "value": "[listkeys(resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents', 'sender'), '2022-10-01-preview').primaryConnectionString]", "attributes": { "enabled": true }, @@ -1212,16 +1272,16 @@ }, "dependsOn": [ "[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]", - "[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', parameters('baseResourceName'), 'graphevents', 'sender')]" + "[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents', 'sender')]" ] }, { - "condition": "[and(and(not(parameters('useGraphEventHubManagedIdentity')), parameters('useSeparateKeyVaultForGraph')), and(not(parameters('useGraphEventHubManagedIdentity')), parameters('useSeparateKeyVaultForGraph')))]", + "condition": "[and(not(parameters('useGraphEventHubManagedIdentity')), parameters('useSeparateKeyVaultForGraph'))]", "type": "Microsoft.KeyVault/vaults/secrets", "apiVersion": "2023-02-01", "name": "[format('{0}/{1}', variables('graphKeyVaultName'), 'GraphEventHubConnectionString')]", "properties": { - "value": "[listkeys(resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', parameters('baseResourceName'), 'graphevents', 'sender'), '2022-10-01-preview').primaryConnectionString]", + "value": "[listkeys(resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents', 'sender'), '2022-10-01-preview').primaryConnectionString]", "attributes": { "enabled": true }, @@ -1229,7 +1289,7 @@ }, "dependsOn": [ "[resourceId('Microsoft.KeyVault/vaults', variables('graphKeyVaultName'))]", - "[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', parameters('baseResourceName'), 'graphevents', 'sender')]" + "[resourceId('Microsoft.EventHub/namespaces/eventhubs/authorizationRules', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents', 'sender')]" ] }, { @@ -1261,7 +1321,7 @@ { "type": "Microsoft.EventHub/namespaces", "apiVersion": "2022-10-01-preview", - "name": "[parameters('baseResourceName')]", + "name": "[if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName'))))))]", "location": "[parameters('location')]", "sku": "[variables('configurations')[parameters('deploymentType')].eventHub.sku]", "properties": "[variables('configurations')[parameters('deploymentType')].eventHub.properties]", @@ -1338,7 +1398,7 @@ }, "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.EventHub/namespaces/{0}/eventhubs/{1}', parameters('baseResourceName'), 'graphevents')]", + "scope": "[format('Microsoft.EventHub/namespaces/{0}/eventhubs/{1}', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents')]", "name": "[guid(resourceId('Microsoft.Web/sites', format('{0}-function', parameters('baseResourceName'))), variables('neededRoles').functionApp.EventHubs[copyIndex()], resourceGroup().id)]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('neededRoles').functionApp.EventHubs[copyIndex()])]", @@ -1347,7 +1407,7 @@ }, "dependsOn": [ "[resourceId('Microsoft.Web/sites', format('{0}-function', parameters('baseResourceName')))]", - "[resourceId('Microsoft.EventHub/namespaces/eventhubs', parameters('baseResourceName'), 'graphevents')]" + "[resourceId('Microsoft.EventHub/namespaces/eventhubs', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents')]" ] }, { @@ -1393,7 +1453,7 @@ }, "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", - "scope": "[format('Microsoft.EventHub/namespaces/{0}/eventhubs/{1}', parameters('baseResourceName'), 'graphevents')]", + "scope": "[format('Microsoft.EventHub/namespaces/{0}/eventhubs/{1}', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents')]", "name": "[guid(parameters('graphChangeTrackingAppObjectId'), variables('neededRoles').graphChangeTrackingApp.EventHubs[copyIndex()], resourceGroup().id)]", "properties": { "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('neededRoles').graphChangeTrackingApp.EventHubs[copyIndex()])]", @@ -1401,7 +1461,7 @@ "principalType": "ServicePrincipal" }, "dependsOn": [ - "[resourceId('Microsoft.EventHub/namespaces/eventhubs', parameters('baseResourceName'), 'graphevents')]" + "[resourceId('Microsoft.EventHub/namespaces/eventhubs', if(greaterOrEquals(length(parameters('baseResourceName')), 6), parameters('baseResourceName'), format('{0}{1}', parameters('baseResourceName'), substring(uniqueString(parameters('baseResourceName')), 0, sub(6, length(parameters('baseResourceName')))))), 'graphevents')]" ] }, { @@ -1495,8 +1555,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.25.3.34343", - "templateHash": "7102274191347685" + "version": "0.28.1.47646", + "templateHash": "8805216685510205529" } }, "parameters": { @@ -1664,8 +1724,8 @@ "metadata": { "_generator": { "name": "bicep", - "version": "0.25.3.34343", - "templateHash": "14633294426575691402" + "version": "0.28.1.47646", + "templateHash": "774527224550770436" } }, "parameters": { diff --git a/docs/admin-functions.md b/docs/admin-functions.md deleted file mode 100644 index 530459d..0000000 --- a/docs/admin-functions.md +++ /dev/null @@ -1,80 +0,0 @@ -# Admin Functions - -Add info here about how to disable, and notes that none are required post deployment if wanting to eliminate all HTTP Triggers - -Call Record Insights provides several HTTP Triggered functions solely for administrative purposes. - -Each admin function requires the use of the master/host key for authentication. - ---- - -## GetCallRecordAdminFunction -This admin function retrieves a raw call record from Graph - -### URL -`https://cridemo1-function.azurewebsites.net/api/callRecords/ca111dca-111d-ca11-1dca-11ca111dca11/contoso.com?code=` - -### Method -`GET` - ---- - -## AddSubscriptionOrRenewIfExpiredFunction -This admin function creates/renews the subscription to Graph - -### URL -`https://cridemo1-function.azurewebsites.net/api/subscription/contoso.com?code=` - -### Method -`POST` - ---- - -## ManuallyProcessCallIdsFunction -This admin function process a call record with a provided Call-Id. - -### URL -`https://cridemo1-function.azurewebsites.net/api/callRecords?code=` - -### Method -`POST` - -### Body -`["ca111dca-111d-ca11-1dca-11ca111dca11"]` - -### Content-Type -`application/json` - ---- - -## GetCallRecordInsightsHealthFunction -Gets the health state of each component - -### URL -`https://cridemo1-function.azurewebsites.net/api/health?code=` - -### Method -`GET` - ---- - -## GetSubscriptionIdFunction -Gets the Azure subscription Id the deployment is in - -### URL -`https://cridemo1-function.azurewebsites.net/api/subscription/contoso.com?code=` - -### Method -`GET` - -### Result -```json -{ - "id": "0ddba11c-a11a-b1ec-ab00-5eca55e77e1d", - "expirationDateTime": "1/20/2024 10:30:03 AM", - "tenantId": "c0ffee60-0dde-cafc-0ffe-ebadcaffe14e", - "resource": "communications/callRecords", - "changeType": "created,updated", - "notificationUrl": "EventHub:https://kvcridemo1.vault.azure.net/secrets/GraphEventHubConnectionString?tenantId=c0ffee60-0dde-cafc-0ffe-ebadcaffe14e" -} -``` diff --git a/docs/data-explorer-functions-views-queries.md b/docs/data-explorer-functions-views-queries.md deleted file mode 100644 index 88ee87e..0000000 --- a/docs/data-explorer-functions-views-queries.md +++ /dev/null @@ -1,3 +0,0 @@ -# CosmosDB, Kusto Functions, Views and Queries - -\ \ No newline at end of file diff --git a/docs/deployment.md b/docs/deployment.md deleted file mode 100644 index 5af067a..0000000 --- a/docs/deployment.md +++ /dev/null @@ -1,174 +0,0 @@ -# Deployment -Follow the steps below to deploy the application: - -## Retrieve Deployment Scripts - -### Fork Repo (optional) - -> [!WARNING] -> This step is required if you want to make any customizations to the solution being deployed - -![](./media/github-fork.png) - -### Download the zip file containing the source and deployment scripts - -> [!NOTE] -> If you performed the [step above](#fork-repo-optional), this should be done from your new repository - -![](./media/github-download-code.png) - -### Unblock the zip file -This is required to prevent issues with script ps1 script executions. - -![](./media/unblock-file.png) - -### Extract the zip file -The deployment script(s) will be executed from this directory. Make note of where this was stored. - -## Define deployment parameters - -### `[-ResourceGroupName]` \ **Required** -This is the name that is used for the Azure Resource Group that Call Record Insights resources will be deployed to. If this does not exist it will be created. - -### `[-BaseResourceName]` \ -This is the name of which all resources are based upon. If BaseResourceName is not specified ResourceGroupName is used. - -**Default**: [ResourceGroupName](#resourcegroupname-string-required) - -### `[-SubscriptionId]` \ **Required** -This is the subscription id of the subscription to which the application will be deployed. - -### `[-DeploymentSize]` \ -This defines the SKUs of each component (Kusto, Function App, Storage Account). - -Options are: -- DevTest -- Production -- ProductionRestricted - -**Default**: `Production` - -### `[-Location]` \ -This is the Azure region location where the application (and all associated resources) will be deployed. - -[Choose the Right Azure Region for You \| Microsoft Azure](https://azure.microsoft.com/en-us/explore/global-infrastructure/geographies/#overview) - -*Example*: `centralus` - -**Default**: `westus` - -### `[-GitRepoUrl]` \ -This is the GIT Repo URL that you are deploying. - -If you [forked](#fork-repo-optional) the repo, then this is **Required** and must be set to the url hosting your repository - -*Example*: `https://github.com/{organization}/callrecord-insights.git` - -If you are using a private repository, then a Personal Access Token must be created and passed in this parameter - -*Example*: `https://{username}:{PAT}@github.com/{organization}/callrecord-insights.git` - -**Default**: `https://github.com/OfficeDev/microsoft-teams-apps-callrecord-insights.git` - -### `[-GitBranch]` \ -This is the name of the branch of the repository you wish to deploy - -**Default**: `main` - -### `[-TenantDomain]` \ **Required** -This is the domain name associated with your Tenant. - -*Example*: `contoso.com` - -### `[-CosmosAccountName]` \ -The account name used for Cosmos DB. - -**Default**: [BaseResourceName](#baseresourcename-string) + `cdb` - -### `[-CosmosCallRecordsDatabaseName]` \ -The database name used for the Call Records Database in Cosmos DB. - -**Default**: `CallRecordInsights` - -### `[-CosmosCallRecordsContainerName]` \ -The CallRecords container name in Cosmos DB. - -**Default**: `records` - -### `[-ExistingKustoClusterName]` \ -The Kusto cluster name if deploying to an existing KustoCluster - -### `[-KustoCallRecordsDatabaseName]` \ -The database name inside of the Kusto cluster of the Call Records database - -### `[-KustoCallRecordsTableName]` \ -The table name inside of the Kusto database - -### `[-KustoCallRecordsViewName]` \ -The name of the view that de-duplicates records in case of duplication due to updated call record ingestion. - -### `[-UseEventHubManagedIdentity]` \ -Reserved for future use, currently breaks all functionality. - -> [!CAUTION] -> Do not use this parameter - -## Execute deploy.ps1 -Using the parameters defined [above](#define-deployment-parameters) - -*Example:* -```powershell -$DeploymentParameters = @{ - ResourceGroupName = 'cridemo1rg' - BaseResourceName = 'cridemo1' - SubscriptionId = 'ba5eba11-beef-ba5e-ba11-beefba5eba11' - TenantDomain = 'contoso.com' -} -.\deploy.ps1 @DeploymentParameters -``` - -Wait for deployment script to complete - -# Deployment Validation & Verification - -Once deployment steps complete (successfully or unsuccessfully) the deployment script calls the admin function to get the health state of the deployment. The output indicates whether all required services are up and healthy. - -## Deployment Validation -Once the deployment script is completed, the final output will be the deployment health. - -If the output of `$HealthState` is healthy, then you will receive the following message: -**`App deployment is healthy.`** - -### Deployment Health Details -`$HealthState` is the variable which will contain details regarding the health state of the deployment - -![](./media/deploy-ps1-health-state.png) - -#### healthy -This indicates if the deployment is overall healthy or unhealthy. - -#### eventHub -This provides the health status of the EventHub and the EventHub Url - -#### cosmos -This provides the health status of the Cosmos DB and the Cosmos DB Url for the call records db. - -#### downloadQueue -This provides the health status of the Azure Storage Queue and the Azure Storage Queue Url for the download queue. - -#### subscriptions -This provides the health status of the subscription to the Call Records Graph API endpoint and the expiration date of the subscription. - -#### unhealthyServices -Lists all services above that were found to be unhealthy. - -### Check Cosmos DB Ingestion/Data Connection - -From portal.azure.com, -Navigate to Resource Group -> Kusto Cluster -> Databases -> Database -> CosmosDB - -![](./media/data-explorer-portal-database.png) -![](./media/data-explorer-portal-data-connections.png) - -### Check Subscription Status in Graph -![](./media/graph-explorer-list-subscriptions.png) diff --git a/docs/function-configuration-settings.md b/docs/function-configuration-settings.md deleted file mode 100644 index 6de0d2f..0000000 --- a/docs/function-configuration-settings.md +++ /dev/null @@ -1,127 +0,0 @@ -# Configuration Settings - -> [!NOTE] -> This application was designed to use Identity-Based connections wherever possible, and as such, some of the configuration entries look different than other examples of Azure Function implementations. -> See [this](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference#configure-an-identity-based-connection) for more information - -## `RenewSubscriptionScheduleCron` -This is Cron string for the frequency of renewing the Call Records Notification Subscription from Graph. - -This defaults to `0 0 */2 * * *` or every 2 hours. - -## `CallRecordsQueueConnection__queueServiceUri` -This is the Queue Service Uri of the storage account which contains the download/processing queue. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to the primary queue endpoint of the storage account the same template creates. - -## `CallRecordsQueueConnection__credential` -This is the identity used to connect to the [download/processing queue](#callrecordsqueueconnection__queueserviceuri) - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to `managedidentity`. This should not be changed. - -## `CallRecordsToDownloadQueueName` -This is the name of the storage queue to be used as the download/processing queue. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to [KustoCallRecordsDatabaseName](#kustocallrecordsdatabasename-string) + `download` - -## `GraphSubscription__NotificationUrl` -This is the `notificationUrl` used in the Call Records [Subscription](https://learn.microsoft.com/en-us/graph/api/resources/subscription#properties) used by the deployment - -This will be in the form of the Uri for the Key Vault secret holding the Event Hub connection string for use by the Graph Event Notification Service. - -See [Receive change notifications through Azure Event Hubs](https://learn.microsoft.com/en-us/graph/change-notifications-delivery-event-hubs#creating-the-subscription) for more info - -## `GraphSubscription__Tenants` -This is a list of tenants the application is configured to monitor. It can be either a tenantId GUID or configured tenant domain. - -By default, this is set to [TenantDomain](deployment.md#tenantdomain-string-required) - -If [Multi/Cross Tenant](./multi-tenant-deployment.md) deployment is desired, this will be all the monitored tenants, separated by a semi-colon (`;`) - -## `AzureAd` -This section will not be present unless configured manually. -This section is for configuring the app service principal to be used when calling Graph API. -This is only required for [Multi/Cross Tenant](./multi-tenant-deployment.md) deployments, otherwise the managed identity of the function app will be used for all Graph API calls. - -This section should be a valid [MicrosoftIdentityOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.identity.web.microsoftidentityoptions) configuration. - -The fields `TenantId`, `ClientId`, `Instance`, and either `ClientCredentials` or `ClientCertificates` are required - -*Example*: -``` -AzureAd__Instance: https://login.microsoftonline.com -AzureAd__TenantId: c0ffee60-0dde-cafc-0ffe-ebadcaffe14e -AzureAd__ClientId: c1d71d1d-ca11-ca11-ca11-defa177ca115 -AzureAd__ClientCredentials__0__SourceType: KeyVault -AzureAd__ClientCredentials__0__KeyVaultUrl: https://kvcridemo1.vault.azure.net -AzureAd__ClientCredentials__0__KeyVaultCertificateName: CRI-DEMO-1-SPN-CERT -``` - -## `CallRecordInsightsDb__EndpointUri` -This is the Document Endpoint of the Cosmos DB Account used to store the flattened call records. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to the documentEndpoint of the Cosmos DB Account that was created in [deployCosmos.bicep](../deploy/bicep/deployCosmos.bicep) - -## `CallRecordInsightsDb__DatabaseName` -This is the name of the Cosmos DB database used to store the flattened call records. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to the name of the database that was created in [deployCosmos.bicep](../deploy/bicep/deployCosmos.bicep) - -## `CallRecordInsightsDb__ProcessedContainerName` -This is the container in the Cosmos DB database used to store the flattened call records. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to the name of the container in the Cosmos DB database that was created in [deployCosmos.bicep](../deploy/bicep/deployCosmos.bicep) - -## `GraphNotificationEventHubName` -This is the Event Hub to which the Graph Event Notifications are sent. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to the name of the Event Hub configured in the same template. - -## `EventHubConnection__fullyQualifiedNamespace` -This is the namespace of the Event Hub associated with [GraphNotificationEventHubName](#graphnotificationeventhubname) - -This is used for the identity-based connection from the function app. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) from the underlying serviceBusEndpoint of the Event Hub configured in the same template. - -## `EventHubConnection__credential` -This is the identity used to connect to the [Event Hub](#eventhubconnection__fullyqualifiednamespace) - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to `managedidentity`. This should not be changed. - -## `AzureWebJobsStorage__accountName` -This is the name of the storage account used for the function app. - -This is used for the identity-based connection from the function app. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) and should not be changed. - -## `AzureWebJobsSecretStorageType` -This is the secret storage entry to allow secrets management to occur in Key Vault instead of in the underlying storage account. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to `keyvault`. This should not be changed. - -## `AzureWebJobsSecretStorageKeyVaultUri` -This is the secret storage entry to allow secrets management to occur in Key Vault instead of in the underlying storage account. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to the Key Vault Uri created for internal use in the same template. - -## `FUNCTIONS_EXTENSION_VERSION` -This is a mandatory Azure Functions configuration entry. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to `~4`. This should not be changed. - -## `FUNCTIONS_WORKER_RUNTIME` -This is a mandatory Azure Functions configuration entry. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to `dotnet`. This should not be changed. - -## `WEBSITE_CONTENTAZUREFILECONNECTIONSTRING` -This is a mandatory Azure Functions configuration entry. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to a key vault reference to the secret containing the storage account connection string, both of which were created in the same template. - -## `WEBSITE_CONTENTSHARE` -This is a mandatory Azure Functions configuration entry. - -This is set by [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) to the name of the function app. \ No newline at end of file diff --git a/docs/high-level-architecture.md b/docs/high-level-architecture.md deleted file mode 100644 index 8420f4d..0000000 --- a/docs/high-level-architecture.md +++ /dev/null @@ -1,3 +0,0 @@ -# High Level Architecture - -![](./media/architecture-diagram.png) \ No newline at end of file diff --git a/docs/media/architecture-diagram.png b/docs/media/architecture-diagram.png deleted file mode 100644 index 897545e..0000000 Binary files a/docs/media/architecture-diagram.png and /dev/null differ diff --git a/docs/media/data-explorer-portal-data-connections.png b/docs/media/data-explorer-portal-data-connections.png deleted file mode 100644 index 170da47..0000000 Binary files a/docs/media/data-explorer-portal-data-connections.png and /dev/null differ diff --git a/docs/media/data-explorer-portal-database.png b/docs/media/data-explorer-portal-database.png deleted file mode 100644 index 59c5252..0000000 Binary files a/docs/media/data-explorer-portal-database.png and /dev/null differ diff --git a/docs/media/deploy-ps1-health-state.png b/docs/media/deploy-ps1-health-state.png deleted file mode 100644 index 51ad287..0000000 Binary files a/docs/media/deploy-ps1-health-state.png and /dev/null differ diff --git a/docs/media/example-azure-resources.png b/docs/media/example-azure-resources.png deleted file mode 100644 index b0a4d13..0000000 Binary files a/docs/media/example-azure-resources.png and /dev/null differ diff --git a/docs/media/github-download-code.png b/docs/media/github-download-code.png deleted file mode 100644 index c109e54..0000000 Binary files a/docs/media/github-download-code.png and /dev/null differ diff --git a/docs/media/github-fork.png b/docs/media/github-fork.png deleted file mode 100644 index 84be8a7..0000000 Binary files a/docs/media/github-fork.png and /dev/null differ diff --git a/docs/media/graph-explorer-list-subscriptions.png b/docs/media/graph-explorer-list-subscriptions.png deleted file mode 100644 index 8e547d9..0000000 Binary files a/docs/media/graph-explorer-list-subscriptions.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-1.png b/docs/media/multi-tenant-create-application-1.png deleted file mode 100644 index a27c292..0000000 Binary files a/docs/media/multi-tenant-create-application-1.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-10.png b/docs/media/multi-tenant-create-application-10.png deleted file mode 100644 index 5bbd1ec..0000000 Binary files a/docs/media/multi-tenant-create-application-10.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-11.png b/docs/media/multi-tenant-create-application-11.png deleted file mode 100644 index eb671ab..0000000 Binary files a/docs/media/multi-tenant-create-application-11.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-12.png b/docs/media/multi-tenant-create-application-12.png deleted file mode 100644 index a6cf40a..0000000 Binary files a/docs/media/multi-tenant-create-application-12.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-13.png b/docs/media/multi-tenant-create-application-13.png deleted file mode 100644 index 0818d98..0000000 Binary files a/docs/media/multi-tenant-create-application-13.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-2.png b/docs/media/multi-tenant-create-application-2.png deleted file mode 100644 index 8f2d449..0000000 Binary files a/docs/media/multi-tenant-create-application-2.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-3.png b/docs/media/multi-tenant-create-application-3.png deleted file mode 100644 index c5980cd..0000000 Binary files a/docs/media/multi-tenant-create-application-3.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-4.png b/docs/media/multi-tenant-create-application-4.png deleted file mode 100644 index 4d99861..0000000 Binary files a/docs/media/multi-tenant-create-application-4.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-5.png b/docs/media/multi-tenant-create-application-5.png deleted file mode 100644 index 9bb2763..0000000 Binary files a/docs/media/multi-tenant-create-application-5.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-6.png b/docs/media/multi-tenant-create-application-6.png deleted file mode 100644 index f636f6b..0000000 Binary files a/docs/media/multi-tenant-create-application-6.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-7.png b/docs/media/multi-tenant-create-application-7.png deleted file mode 100644 index 8ce8c99..0000000 Binary files a/docs/media/multi-tenant-create-application-7.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-8.png b/docs/media/multi-tenant-create-application-8.png deleted file mode 100644 index 2b3890d..0000000 Binary files a/docs/media/multi-tenant-create-application-8.png and /dev/null differ diff --git a/docs/media/multi-tenant-create-application-9.png b/docs/media/multi-tenant-create-application-9.png deleted file mode 100644 index 30b3f27..0000000 Binary files a/docs/media/multi-tenant-create-application-9.png and /dev/null differ diff --git a/docs/media/multi-tenant-grant-consent-1.png b/docs/media/multi-tenant-grant-consent-1.png deleted file mode 100644 index 0dd17c7..0000000 Binary files a/docs/media/multi-tenant-grant-consent-1.png and /dev/null differ diff --git a/docs/media/multi-tenant-grant-consent-2.png b/docs/media/multi-tenant-grant-consent-2.png deleted file mode 100644 index 6a225f0..0000000 Binary files a/docs/media/multi-tenant-grant-consent-2.png and /dev/null differ diff --git a/docs/media/multi-tenant-grant-consent-3.png b/docs/media/multi-tenant-grant-consent-3.png deleted file mode 100644 index 5df9a46..0000000 Binary files a/docs/media/multi-tenant-grant-consent-3.png and /dev/null differ diff --git a/docs/media/multi-tenant-grant-consent-4.png b/docs/media/multi-tenant-grant-consent-4.png deleted file mode 100644 index 3748ad0..0000000 Binary files a/docs/media/multi-tenant-grant-consent-4.png and /dev/null differ diff --git a/docs/media/multi-tenant-grant-consent-5.png b/docs/media/multi-tenant-grant-consent-5.png deleted file mode 100644 index b0abe86..0000000 Binary files a/docs/media/multi-tenant-grant-consent-5.png and /dev/null differ diff --git a/docs/media/multi-tenant-register-service-principal-1.png b/docs/media/multi-tenant-register-service-principal-1.png deleted file mode 100644 index f7ef04e..0000000 Binary files a/docs/media/multi-tenant-register-service-principal-1.png and /dev/null differ diff --git a/docs/media/unblock-file.png b/docs/media/unblock-file.png deleted file mode 100644 index 3bf9b09..0000000 Binary files a/docs/media/unblock-file.png and /dev/null differ diff --git a/docs/multi-tenant-deployment.md b/docs/multi-tenant-deployment.md deleted file mode 100644 index 9f515a9..0000000 --- a/docs/multi-tenant-deployment.md +++ /dev/null @@ -1,103 +0,0 @@ -# Multi/Cross Tenant Deployment - -For the default (and most common) deployment, Call Record Insights is deployed to an Azure Subscription associated with the same Microsoft Entra ID Tenant that is being monitored. - -However, when that shared tenant context is not possible, it is possible to deploy Call Record Insights to work in either a Cross-Tenant or Multi-Tenant configuration. - -Example: - -- Development deployment with Production data being monitored - -- Single deployment with multiple subsidiary tenants being monitored - -Since the default deployment uses Managed Identity for all Microsoft Graph authentication, this will not work, as the identity would only have permissions to access data from the tenant which owns the Identity. - -By using Microsoft Entra ID App authentication, we can authenticate against external tenants that have been properly configured and consented. - -## Create Microsoft Entra ID App Registration (In Deployed Tenant) -### Create a New registration - - - -> [!Note] -> Be sure to select *Accounts in any organizational directory (Any Microsoft Entra ID - Multitenant)* - - - - - -### Configure Microsoft Graph API Permissions - - - - - - - -### Grant admin consent (Optional) - -> [!NOTE] -> This is only necessary if the Microsoft Entra ID Tenant where Call Record Insights is deployed is being monitored -> -> This is unneeded if Call Records for this tenant are unwanted - - - - - -### Create Client Secret - -> [!NOTE] -> Client Certificates can also be used -> -> It is recommended that this is not done via the portal but instead managed and stored within the configured Azure Key Vault used by Call Record Insights - -> [!NOTE] -> If this Secret or Certificate Expires or is revoked, Call Record Insights will no longer be able to monitor Call Records until renewed. - - - - - -## Register Service Principal (In All Monitored Tenants) - -### Create New Service Principal for the newly created App Registration - -```powershell -$MultiTenantAppClientId = 'c0ffee60-0dde-cafc-0ffe-ebadcaffe14e' # This should be the Application (client) ID for the app registration - -Connect-MgGraph -Scopes Application.ReadWrite.All -$NewSPN = New-MgServicePrincipal -AppId $MultiTenantAppClientId - -Write-Host "Go To https://portal.azure.com/#view/Microsoft_AAD_IAM/ManagedAppMenuBlade/~/Permissions/objectId/$($NewSPN.Id)/appId/$MultiTenantAppClientId to grant consent to the application for your tenant." -``` - - - -### Grant consent for the new Service Principal - - - -#### Sign as a Global Administrator - - -#### Verify the Application and Permissions are correct - - -#### After Accepting the Permissions requested - - -#### Refresh To Verify the permissions are consented - - -## Update Call Record Insights Configuration - -### Ensure all monitored domains are present in the [Monitored Tenant List](./function-configuration-settings.md#graphsubscription__tenants). - -If a domain is not listed in this configuration, it **will not be monitored** even if consent has been granted - -If the local tenant is also monitored, be sure to include it in the configuration in addition to any external Tenants. - -### Enable Call Record Insights to use the [App Registration](#create-microsoft-entra-id-app-registration-in-deployed-tenant) - -This is done in the [Graph App Configuration](./function-configuration-settings.md#azuread), refer there for more information and example(s) diff --git a/docs/requirements.md b/docs/requirements.md deleted file mode 100644 index 3c12805..0000000 --- a/docs/requirements.md +++ /dev/null @@ -1,342 +0,0 @@ -# Requirements - -## Pre-Deployment Requirements -The Call Record Insights application requires the following steps to be completed before deploying. - -- Existing Azure Subscription to deploy to -- Rights to create new resources in Azure Subscription - - See [Deployment Permissions](#deployment-permissions) for details. -- Teams Enabled Users to generate call records - -## Deployment Client Requirements -If using [deploy.ps1](../deploy/deploy.ps1), The workstation performing the deployment requires - -- PowerShell 7.2 (or greater) or Windows PowerShell 5.1 -- Azure Cli 2.56.0 or greater - -## Azure Service Requirements -The following Azure Service components are required. - -They will be created automatically as part of the deployment, either via [PowerShell](../deploy/deploy.ps1) or [ARM](../deploy/resourcemanager/template.json)) - -- 1 Azure Function app -- 1 Azure Storage Account -- 1 Azure Cosmos DB NoSQL Account -- 2 Azure Key vault minimum -- 1 Azure Event Hub -- 1 Azure Data Explorer Database -- 1 App Service Plan - -*Example Resource Group (Post-Deployment):* - -![](./media/example-azure-resources.png) - -## Permissions - -### Runtime Permissions - -| **Account Needing Permissions** | **Role** | **Minimum Required Scope** | **Automated Assignment Source** | **Reason For Requirement** | | -|---|---|---|---|---|---| -| Kusto Managed Identity | Cosmos DB Account Reader | Cosmos DB Account | [configureKusto.bicep](../deploy/bicep/configureKusto.bicep) | Configuring Kusto Ingestion | [ref](https://learn.microsoft.com/en-us/azure/data-explorer/ingest-data-cosmos-db-connection?tabs=arm&tabpanel_1_arm) | -| | Cosmos DB Data Reader | Cosmos DB "records" container | [configureKusto.bicep](../deploy/bicep/configureKusto.bicep) | Reading data from "records" to ingest | [ref](https://learn.microsoft.com/en-us/azure/data-explorer/ingest-data-cosmos-db-connection?tabs=arm&tabpanel_1_arm) | -| Function App Managed Identity | Storage Account Contributor | Storage Account | [configureKusto.bicep](../deploy/bicep/configureKusto.bicep) | | [ref](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference?tabs=eventhubs&pivots=programming-language-csharp&connecting-to-host-storage-with-an-identity) | -| | Storage Account Queue Data Contributor | Storage Account | [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) | Queue Trigger | [ref](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference?tabs=queue&pivots=programming-language-csharp&tabpanel_1_queue) | -| | Storage Account Blob Data Owner | Storage Account | [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) | | [ref](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference?tabs=eventhubs&pivots=programming-language-csharp&connecting-to-host-storage-with-an-identity) | -| | Event Hubs Data Receiver | Event Hub | [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) | Event Hubs Trigger | [ref](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference?tabs=eventhubs&pivots=programming-language-csharp&tabpanel_1_eventhubs) | -| | Key Vault Secrets Officer | Key Vault | [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) | Function Secrets | [ref](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference?tabs=eventhubs&pivots=programming-language-csharp&connecting-to-host-storage-with-an-identity) | -| | Cosmos DB Data Contributor | Cosmos DB Database | [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) | Read/Upsert Processed Data | [ref](https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-setup-rbac#built-in-role-definitions) | -| | CallRecords.Read.All | Microsoft Graph (Tenant) | [deploy.ps1](../deploy/deploy.ps1) | Configure Subscription Retrieve Call Records | [ref](https://learn.microsoft.com/en-us/graph/permissions-reference#callrecordsreadall) | -| Microsoft Graph Change Tracking Service Principal | Key Vault Secrets User | Graph Event Hub Connection String Secret | [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) | Read Event Hub connection string for event publishing | [ref](https://learn.microsoft.com/en-us/graph/change-notifications-delivery-event-hubs#receiving-notifications) | - -### Deployment Permissions - -The recommendation is to grant all of these permissions to a single user account performing the deployment, and to use [deploy.ps1](../deploy/deploy.ps1) - -> [!Note] -> Regarding Multiple Administrator Deployment -> -> This can be done, however, this will result in errors on each run of [deploy.ps1](../deploy/deploy.ps1) where the running account does not have all permissions -> - The parameters passed to [deploy.ps1](../deploy/deploy.ps1) **MUST** be the same for each run, regardless of the admin account -> - When the [deploy.ps1](../deploy/deploy.ps1) script writes an error regarding checking permissions, then the current account does not have the required permissions to continue -> - The step which failed will be noted as the last line before the error -> - So long as the final run of the [deploy.ps1](../deploy/deploy.ps1) script results in no errors, then the application will have deployed successfully - -#### Permissions Required for deploy.ps1 - -> [!Note] -> All Commands tested using Azure Cli version 2.56.0 - -Below is a break down of each underlying Azure Cli command which is used in the script. Each Command has the list of Possible Errors you may see if permissions are not set properly for the currently signed-in user. - -##### Command - -`az account show --query id` - -###### Possible Errors - -- `Failed to connect to subscription '$SubscriptionId'. Please ensure you have access to the subscription and try again.` - -###### API Calls - -- None - -###### Permissions - -- No additional permissions - -##### Command - -`az ad signed-in-user show --query userPrincipalName az ad signed-in-user show --query id` - -###### Possible Errors - -- `Failed to get identity of signed in user! Please ensure you are signed in and try again.` - -###### API Calls - -- GET https://graph.microsoft.com/v1.0/me - -###### Permissions - -- Microsoft Graph Scopes ([ref](https://learn.microsoft.com/en-us/graph/api/user-get#permissions)) - - User.Read - - **OR** - - User.ReadWrite - - **OR** - - User.ReadBasic.All - - **OR** - - User.Read.All - - **OR** - - User.ReadWrite.All - - **OR** - - Directory.Read.All - - **OR** - - Directory.ReadWrite.All - -##### Command - -`az account show --query tenantId` - -###### Possible Errors - -- `Failed to get tenant id for subscription '$SubscriptionId'. Please ensure you have access to the subscription and try again.` - -###### API Calls - -- None - -###### Permissions - -- Must Be User in Tenant associated with Subscription -- No additional permissions - -##### Command - -`az ad sp list --spn $APP_ID --query "[].id"` - -###### Possible Errors - -- `Failed to get the SPN object id for the Microsoft Graph Change Tracking app. Please ensure you have access to the tenant and try again.` - -- `Failed to get the SPN object id for Microsoft Graph. Please ensure you have access to the tenant and try again.` - -###### API Calls - -- GET https://graph.microsoft.com/v1.0/servicePrincipals?$filter=servicePrincipalNames/any(c:c/id+eq+'$APP_ID') - -###### Permissions - -- Microsoft Graph Scopes ([ref](https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list#permissions)) - - Application.Read.All - - **OR** - - Application.ReadWrite.All - - **OR** - - Directory.Read.All - - **OR** - - Directory.ReadWrite.All - -##### Command - -`az group show --name $RESOURCE_GROUP` - -###### Possible Errors - -- `Failed to create resource group '$ResourceGroupName' in location '$Location'. Please ensure you have access to the subscription and try again.` - -###### API Calls - -- GET https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP?api-version=2022-09-01 - -###### Permissions - -- Azure RBAC Roles - - Microsoft.Resources/subscriptions/resourceGroups/read - -##### Command - -`az group create --name $RESOURCE_GROUP --location $LOCATION` - -###### Possible Errors - -- `Failed to create resource group '$ResourceGroupName' in location '$Location'. Please ensure you have access to the subscription and try again.` - -###### API Calls - -- PUT https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP?api-version=2022-09-01 - -###### Permissions - -- Azure RBAC Roles - - Microsoft.Resources/subscriptions/resourceGroups/read - - **AND** - - Microsoft.Resources/subscriptions/resourceGroups/write - -##### Command - -`az rest --method get --url "https://graph.microsoft.com/v1.0/servicePrincipals/$SPN_ID/appRoleAssignments"` - -###### Possible Errors - -- `Failed to add app role '$perm' to Service Principal '$appPrincipalprincipalId'. Please ensure you have access to the tenant and try again.` - -###### API Calls - -- GET https://graph.microsoft.com/v1.0/servicePrincipals/$SPN_ID/appRoleAssignments - -###### Permissions - -- Microsoft Graph Scopes ([ref](https://learn.microsoft.com/en-us/graph/api/serviceprincipal-list-approleassignments#permissions)) - - Application.Read.All - - **OR** - - Application.ReadWrite.All - - **OR** - - Directory.Read.All - - **OR** - - Directory.ReadWrite.All - -##### Command - -`az rest --method post --url "https://graph.microsoft.com/v1.0/servicePrincipals/$SPN_ID/appRoleAssignments" --body "$body"` - -###### Possible Errors - -- `Failed to add app role '$perm' to Service Principal '$appPrincipalprincipalId'. Please ensure you have access to the tenant and try again.` - -###### API Calls - -- POST https://graph.microsoft.com/v1.0/servicePrincipals/$SPN_ID/appRoleAssignments - -###### Permissions - -- Microsoft Graph Scopes ([ref](https://learn.microsoft.com/en-us/graph/api/serviceprincipal-post-approleassignments#permissions)) - - AppRoleAssignment.ReadWrite.All - - **AND** - - Application.Read.All - - **OR** - - Directory.Read.All - -##### Command - -`az webapp log deployment show --resource-group $RESOURCE_GROUP --name $FUNCTION_APP` - -###### Possible Errors - -- `Failed to get deployment logs for function app '$functionName'. Please ensure you have access to the subscription and try again.` - -###### API Calls - -- GET https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/sites/$FUNCTION_APP?api-version=2023-01-01 -- GET https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/sites/$FUNCTION_APP/basicPublishingCredentialsPolicies/scm?api-version=2023-01-01 -- POST https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/sites/$FUNCTION_APP/config/publishingcredentials/list?api-version=2023-01-01 - -###### Permissions - -- Azure RBAC Roles - - Microsoft.Web/sites/Read - - **AND** - - Microsoft.Web/sites/basicPublishingCredentialsPolicies/scm/Read - - **AND** - - Microsoft.Web/sites/config/list/Action - -##### Command - -`az functionapp keys list --resource-group $RESOURCE_GROUP --name $FUNCTION_APP --query masterKey` - -###### Possible Errors - -- `Failed to get master key for function app '$functionName'.` - -###### API Calls - -- POST https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/sites/$FUNCTION_APP/host/default/listkeys?api-version=2023-01-01 - -###### Permissions - -- Azure RBAC Roles - - Microsoft.Web/sites/host/listkeys/action - -##### Command - -`az deployment group show --resource-group $RESOURCE_GROUP --name $DEPLOYMENT_NAME --query properties` - -###### Possible Errors - -- `Could not get $DeploymentType deployment in resource group '$ResourceGroupName'. Please ensure you have access to the subscription and try again.` -- `Failed to create $DeploymentType in resource group '$ResourceGroupName'. Please ensure you have access to the subscription and try again.` - -###### API Calls - -- GET https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP/providers/Microsoft.Resources/deployments/$DEPLOYMENT_NAME?api-version=2022-09-01 - -###### Permissions - -- Azure RBAC Roles - - Microsoft.Resources/deployments/read - -##### Command - -`az deployment group create --resource-group $RESOURCE_GROUP --name $DEPLOYMENT_NAME --mode Incremental --template-file $TemplateFile --no-prompt true --no-wait --query properties --parameters ...` - -###### Possible Errors - -- `Failed to create $DeploymentType in resource group '$ResourceGroupName'. Please ensure you have access to the subscription and try again.` - -###### API Calls - -- POST https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP/providers/Microsoft.Resources/deployments/$DEPLOYMENT_NAME?api-version=2022-09-01 - -###### Permissions - -- Azure RBAC Roles - - Microsoft.Resources/deployments/write - -##### Command - -`az deployment operation group list --resource-group $RESOURCE_GROUP --name $DEPLOYMENT_NAME --query "[].{provisioningState:properties.provisioningState,targetResource:properties.targetResource.id,statusMessage:properties.statusMessage.error.message}"` - -###### Possible Errors - -- `Failed to create $DeploymentType in resource group '$ResourceGroupName'. Please ensure you have access to the subscription and try again.` - -###### API Calls - -- GET https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourcegroups/$RESOURCE_GROUP/providers/Microsoft.Resources/deployments/$DEPLOYMENT_NAME/operations?api-version=2022-09-01 - -###### Permissions - -- Azure RBAC Roles - - Microsoft.Resources/deployments/operations/read - -#### Permissions for Resource Deployment - -> [!Note] -> Each role requires at least resource group level scoped assignment - -> [!Note] -> These roles are required whether deploying via [deploy.ps1](../deploy/deploy.ps1) **OR** [ARM](../deploy/resourcemanager/template.json) - -| **Bicep Template** | **Azure RBAC Roles** | -|---|---| -| [deployKusto.bicep](../deploy/bicep/deployKusto.bicep) | Microsoft.Kusto/clusters/read
Microsoft.Kusto/clusters/create
Microsoft.Kusto/clusters/databases/create | -| [deployCosmos.bicep](../deploy/bicep/deployCosmos.bicep) | Microsoft.DocumentDB/databaseAccounts/create
Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/create
Microsoft.DocumentDB/databaseAccounts/sqlDatabases/create | -| [deployFunction.bicep](../deploy/bicep/deployFunction.bicep) | Microsoft.DocumentDB/databaseAccounts/read
Microsoft.DocumentDB/databaseAccounts/sqlDatabases/read
Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/read
Microsoft.EventHub/namespaces/read
Microsoft.KeyVault/vaults/read
Microsoft.KeyVault/vaults/secrets/read
Microsoft.Storage/storageAccounts/read
Microsoft.Web/sites/read
Microsoft.Authorization/roleAssignments/create
Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments/create
Microsoft.EventHub/namespaces/create
Microsoft.EventHub/namespaces/eventhubs/authorizationRules/action
Microsoft.EventHub/namespaces/eventhubs/authorizationRules/create
Microsoft.EventHub/namespaces/eventhubs/create
Microsoft.KeyVault/vaults/create
Microsoft.KeyVault/vaults/secrets/create
Microsoft.Storage/storageAccounts/action
Microsoft.Storage/storageAccounts/create
Microsoft.Storage/storageAccounts/queueServices/create
Microsoft.Storage/storageAccounts/queueServices/queues/create
Microsoft.Web/serverfarms/create
Microsoft.Web/sites/config/create
Microsoft.Web/sites/create | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index 0dafa26..0000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,26 +0,0 @@ -# Troubleshooting - -## Deployment - -### Error - No SPN for Graph Change Tracking -The required Service Principal for the First Party Application 'Microsoft Graph Change Tracking' does not exist in the tenant where Call Record Insights is deployed - -[More Information](https://learn.microsoft.com/en-us/graph/change-notifications-delivery-event-hubs#what-if-the-microsoft-graph-change-tracking-application-is-missing) - -#### Resolution -Register the Service Principal for the First Party Application in the tenant where Call Record Insights is deployed - -*Example*: -```powershell -Connect-MgGraph -Scopes 'Application.ReadWrite.All' -$GraphChangeTrackingAppId = '0bf30f3b-4a52-48df-9a82-234910c4a086' -$Existing = Get-MgServicePrincipal -Filter "appId eq '$GraphChangeTrackingAppId'" -ErrorAction SilentlyContinue -if (!$Existing) { - Write-Warning "SPN not found, creating new..." - New-MgServicePrincipal -AppId \$GraphChangeTrackingAppId -} -else { - Write-Information "SPN already created..." - $Existing -} -``` diff --git a/src/Functions/Extensions/GraphRequestBuilderAppOnlyExtensions.cs b/src/Functions/Extensions/GraphRequestBuilderAppOnlyExtensions.cs index 2bd1702..ad17e17 100644 --- a/src/Functions/Extensions/GraphRequestBuilderAppOnlyExtensions.cs +++ b/src/Functions/Extensions/GraphRequestBuilderAppOnlyExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.Identity.Web; using Microsoft.Kiota.Abstractions; +using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options; using System.Collections.Generic; using System.Linq; @@ -17,7 +18,7 @@ public static class GraphRequestBuilderAppOnlyExtensions public static IList AsAppForTenant(this IList options, string tenant = null) { var graphAuthenticationOptions = options.OfType().FirstOrDefault(); - if (graphAuthenticationOptions == null) + if (graphAuthenticationOptions is null) { graphAuthenticationOptions = new GraphAuthenticationOptions(); options.Add(graphAuthenticationOptions); @@ -27,7 +28,30 @@ public static IList AsAppForTenant(this IList op graphAuthenticationOptions.AcquireTokenOptions.Tenant = tenant?.TryGetValidTenantIdGuid(out var tenantIdGuid) == true ? tenantIdGuid.ToString() : null; + return options.WithUserAgent(); + } + + /// + /// Specifies the user agent for the request. + /// + /// Options to modify. + /// + public static IList WithUserAgent(this IList options) + { + var userAgentHandlerOption = options.OfType().FirstOrDefault(); + if (userAgentHandlerOption is null) + { + userAgentHandlerOption = new UserAgentHandlerOption(); + options.Add(userAgentHandlerOption); + } + userAgentHandlerOption.ProductName = APP_NAME; + userAgentHandlerOption.ProductVersion = APP_VERSION; + userAgentHandlerOption.Enabled = true; + return options; } + + private const string APP_NAME = "CallRecordInsights"; + private const string APP_VERSION = "1.1.0"; } } diff --git a/src/Functions/Extensions/ServiceCollectionExtensions.cs b/src/Functions/Extensions/ServiceCollectionExtensions.cs index 3527447..6576c87 100644 --- a/src/Functions/Extensions/ServiceCollectionExtensions.cs +++ b/src/Functions/Extensions/ServiceCollectionExtensions.cs @@ -41,9 +41,10 @@ public static IServiceCollection AddCallRecordsGraphContext( this IServiceCollection services, string sectionName = "GraphSubscription") { - return services.AddMicrosoftGraphAsApplication() + return services .AddScoped(serviceProvider => new CallRecordsGraphOptions( serviceProvider.GetRequiredService().GetSection(sectionName))) + .AddMicrosoftGraphAsApplication() .AddScoped(); } @@ -66,7 +67,6 @@ public static IServiceCollection AddCallRecordsDataContext( ); } - /// /// Adds the to the service collection using the . /// @@ -80,10 +80,13 @@ public static IServiceCollection AddMicrosoftGraphAsApplication( return services .AddTokenCredential(sectionName) .AddAzureMultiTenantGraphAuthenticationProvider() - .AddScoped(sp => new GraphServiceClient(sp.GetRequiredService())); + .AddScoped(sp => + new GraphServiceClient( + authenticationProvider: sp.GetRequiredService(), + baseUrl: $"https://{sp.GetRequiredService().Endpoint}/v1.0" + )); } - /// /// Adds the to the service collection. /// @@ -95,7 +98,6 @@ public static IServiceCollection AddAzureMultiTenantGraphAuthenticationProvider( .AddScoped(); } - /// /// Adds the to the service collection using the /// and for the @@ -118,6 +120,10 @@ public static IServiceCollection AddTokenCredential(this IServiceCollection serv ManagedIdentityClientId = identityOptions?.UserAssignedManagedIdentityClientId, TenantId = identityOptions?.TenantId, }; + + if (identityOptions?.Authority != null) + defaultAzureCredentialOptions.AuthorityHost = new Uri(identityOptions.Authority); + defaultAzureCredentialOptions.AdditionallyAllowedTenants.Add("*"); var clientCertificateCredentialOptions = new ClientCertificateCredentialOptions(); diff --git a/src/Functions/Services/AzureIdentityMultiTenantGraphAuthenticationProvider.cs b/src/Functions/Services/AzureIdentityMultiTenantGraphAuthenticationProvider.cs index e8605dc..353c6d8 100644 --- a/src/Functions/Services/AzureIdentityMultiTenantGraphAuthenticationProvider.cs +++ b/src/Functions/Services/AzureIdentityMultiTenantGraphAuthenticationProvider.cs @@ -23,7 +23,7 @@ public class AzureIdentityMultiTenantGraphAuthenticationProvider : IAuthenticati private readonly GraphServiceClientOptions _defaultAuthenticationOptions; private readonly TokenCredential _credential; private readonly ILogger logger; - private static readonly string[] AppOnlyScopes = new[] { "https://graph.microsoft.com/.default" }; + private static readonly string[] AppOnlyScopes = new[] { "00000003-0000-0000-c000-000000000000/.default" }; public AzureIdentityMultiTenantGraphAuthenticationProvider( TokenCredential credential, diff --git a/src/Functions/Services/CallRecordsGraphContext.cs b/src/Functions/Services/CallRecordsGraphContext.cs index c0fc0ad..7f4659f 100644 --- a/src/Functions/Services/CallRecordsGraphContext.cs +++ b/src/Functions/Services/CallRecordsGraphContext.cs @@ -514,7 +514,7 @@ private List TenantListFactory() private string DefaultTenantFactory() { var token = credential.GetToken( - new TokenRequestContext(new[] { $"https://{graphOptions.Endpoint}/.default" }), + new TokenRequestContext(new[] { "00000003-0000-0000-c000-000000000000/.default" }), default) .Token; var tenantId = new JwtSecurityToken(token)