diff --git a/Deployment/GCCH/azuredeploy.json b/Deployment/GCCH/azuredeploy.json new file mode 100644 index 000000000..868c6b685 --- /dev/null +++ b/Deployment/GCCH/azuredeploy.json @@ -0,0 +1,1419 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "baseResourceName": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "The base name to use for the resources that will be provisioned." + } + }, + "userClientId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "metadata": { + "description": "The client ID of the user bot Azure AD app, e.g., 123e4567-e89b-12d3-a456-426655440000." + } + }, + "userClientSecret": { + "type": "securestring", + "minLength": 1, + "metadata": { + "description": "The client secret of the user bot Azure AD app." + } + }, + "authorClientId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "metadata": { + "description": "The client ID of the author bot Azure AD app, e.g., 123e4567-e89b-12d3-a456-426655440000." + } + }, + "authorClientSecret": { + "type": "securestring", + "minLength": 1, + "metadata": { + "description": "The client secret of the author bot Azure AD app." + } + }, + "graphAppId": { + "type": "string", + "minLength": 36, + "maxLength": 36, + "metadata": { + "description": "The client ID of the Microsoft Graph Azure AD app, e.g., 123e4567-e89b-12d3-a456-426655440000." + } + }, + "graphAppSecret": { + "type": "securestring", + "minLength": 1, + "metadata": { + "description": "The client secret of the Microsoft Graph Azure AD app." + } + }, + "senderUPNList": { + "type": "string", + "minLength": 1, + "metadata": { + "description": "Semicolon-delimited list of the user principal names (UPNs) allowed to send messages." + } + }, + "ProactivelyInstallUserApp": { + "defaultValue": true, + "type": "Bool", + "metadata": { + "description": "If proactive app installation should be enabled." + } + }, + "UserAppExternalId": { + "defaultValue": "148a66bb-e83d-425a-927d-09f4299a9274", + "minLength": 1, + "type": "String", + "metadata": { + "description": "User app external ID." + } + }, + "DefaultCulture": { + "defaultValue": "en-US", + "allowedValues": [ + "ar-SA", + "de-DE", + "en-US", + "es-ES", + "fr-FR", + "he-IL", + "ja-JP", + "ko-KR", + "pt-BR", + "ru-RU", + "zh-CN", + "zh-TW" + ], + "minLength": 1, + "type": "String", + "metadata": { + "description": "Default culture." + } + }, + "SupportedCultures": { + "defaultValue": "ar-SA,de-DE,en-US,es-ES,fr-FR,he-IL,ja-JP,ko-KR,pt-BR,ru-RU,zh-CN,zh-TW", + "minLength": 1, + "type": "String", + "metadata": { + "description": "Comma-delimited list of the supported cultures." + } + }, + "customDomainOption": { + "type": "string", + "allowedValues": [ + "Custom domain name (recommended)", + "Azure Front Door" + ], + "defaultValue": "Azure Front Door", + "metadata": { + "description": "How the app will be hosted on a domain that is not *.azurewebsites.net. Azure Front Door is an easy option that the template can set up automatically, but it comes with ongoing monthly costs. " + } + }, + "appDisplayName": { + "type": "string", + "defaultValue": "Company Communicator", + "minLength": 1, + "metadata": { + "description": "The app (and bot) display name." + } + }, + "appDescription": { + "type": "string", + "defaultValue": "Broadcast messages to multiple teams and people in one go", + "minLength": 1, + "metadata": { + "description": "The app (and bot) description." + } + }, + "appIconUrl": { + "type": "string", + "minLength": 1, + "defaultValue": "https://raw.githubusercontent.com/OfficeDev/microsoft-teams-company-communicator-app/main/Manifest/color.png", + "metadata": { + "description": "The link to the icon for the app. It must resolve to a PNG file." + } + }, + "headerText": { + "type": "string", + "defaultValue": "Company Communicator", + "minLength": 1, + "metadata": { + "description": "The header banner text." + } + }, + "headerLogoUrl": { + "type": "string", + "minLength": 1, + "defaultValue": "https://raw.githubusercontent.com/OfficeDev/microsoft-teams-company-communicator-app/main/Source/CompanyCommunicator/ClientApp/src/assets/Images/mslogo.png", + "metadata": { + "description": "The link to the logo for the app. It must resolve to a PNG file." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "minLength": 1, + "maxLength": 36, + "metadata": { + "description": "The ID of the tenant to which the app will be deployed." + } + }, + "hostingPlanSku": { + "type": "string", + "allowedValues": [ + "Basic", + "Standard", + "Premium" + ], + "defaultValue": "Standard", + "metadata": { + "description": "The pricing tier for the hosting plan." + } + }, + "hostingPlanSize": { + "type": "string", + "allowedValues": [ + "1", + "2", + "3" + ], + "defaultValue": "2", + "metadata": { + "description": "The size of the hosting plan (small, medium, or large)." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + }, + "gitRepoUrl": { + "type": "string", + "metadata": { + "description": "The URL to the GitHub repository to deploy." + }, + "defaultValue": "https://github.com/OfficeDev/microsoft-teams-company-communicator-app.git" + }, + "gitBranch": { + "type": "string", + "metadata": { + "description": "The branch of the GitHub repository to deploy." + }, + "defaultValue": "main" + }, + "serviceBusWebAppRoleNameGuid": { + "defaultValue": "958380b3-630d-4823-b933-f59d92cdcada", + "minLength": 1, + "type": "String", + "metadata": { + "description": "A GUID used to identify the role assignment. This is Default value." + } + }, + "serviceBusPrepFuncRoleNameGuid": { + "defaultValue": "ce6ca916-08e9-4639-bfbe-9d098baf42ca", + "minLength": 1, + "type": "String", + "metadata": { + "description": "A GUID used to identify the role assignment. This is Default value." + } + }, + "serviceBusSendFuncRoleNameGuid": { + "defaultValue": "960365a2-c7bf-4ff3-8887-efa86fe4a163", + "minLength": 1, + "type": "String", + "metadata": { + "description": "A GUID used to identify the role assignment. This is Default value." + } + }, + "serviceBusDataFuncRoleNameGuid": { + "defaultValue": "d42703bc-421d-4d98-bc4d-cd2bb16e5b0a", + "minLength": 1, + "type": "String", + "metadata": { + "description": "A GUID used to identify the role assignment. This is Default value." + } + }, + "storageAccountWebAppRoleNameGuid": { + "defaultValue": "edd0cc48-2cf7-490e-99e8-131311e42030", + "minLength": 1, + "type": "String", + "metadata": { + "description": "A GUID used to identify the role assignment. This is Default value." + } + }, + "storageAccountPrepFuncRoleNameGuid": { + "defaultValue": "9332a9e9-93f4-48d9-8121-d279f30a732e", + "minLength": 1, + "type": "String", + "metadata": { + "description": "A GUID used to identify the role assignment. This is Default value." + } + }, + "storageAccountDataFuncRoleNameGuid": { + "defaultValue": "5b67af51-4a98-47e1-9d22-745069f51a13", + "minLength": 1, + "type": "String", + "metadata": { + "description": "A GUID used to identify the role assignment. This is Default value." + } + } + }, + "variables": { + "botName": "[parameters('baseResourceName')]", + "authorBotName": "[concat(parameters('baseResourceName'), '-author')]", + "botAppName": "[parameters('baseResourceName')]", + "botAppDomain": "[toLower(concat(variables('botAppName'), '.azurewebsites.us'))]", + "botAppUrl": "[concat('https://', variables('botAppDomain'))]", + "hostingPlanName": "[parameters('baseResourceName')]", + "storageAccountName": "[uniquestring(concat(resourceGroup().id, parameters('baseResourceName')))]", + "appInsightsName": "[parameters('baseResourceName')]", + "prepFunctionAppName": "[concat(parameters('baseResourceName'), '-prep-function')]", + "sendFunctionAppName": "[concat(parameters('baseResourceName'), '-function')]", + "dataFunctionAppName": "[concat(parameters('baseResourceName'), '-data-function')]", + "serviceBusNamespaceName": "[parameters('baseResourceName')]", + "serviceBusSendQueueName": "company-communicator-send", + "serviceBusDataQueueName": "company-communicator-data", + "serviceBusPrepareToSendQueueName": "company-communicator-prep", + "serviceBusExportQueueName": "company-communicator-export", + "defaultSASKeyName": "RootManageSharedAccessKey", + "authRuleResourceId": "[resourceId('Microsoft.ServiceBus/namespaces/authorizationRules', variables('serviceBusNamespaceName'), variables('defaultSASKeyName'))]", + "sharedSkus": [ + "Free", + "Shared" + ], + "isSharedPlan": "[contains(variables('sharedSkus'), parameters('hostingPlanSku'))]", + "skuFamily": "[if(equals(parameters('hostingPlanSku'), 'Shared'), 'D', take(parameters('hostingPlanSku'), 1))]", + "useFrontDoor": "[equals(parameters('customDomainOption'), 'Azure Front Door')]", + "frontDoorName": "[parameters('baseResourceName')]", + "frontDoorDomain": "[toLower(concat(variables('frontDoorName'), '.azurefd.us'))]", + "ProactivelyInstallUserApp": "[parameters('ProactivelyInstallUserApp')]", + "UserAppExternalId": "[parameters('UserAppExternalId')]", + "i18n:DefaultCulture": "[parameters('DefaultCulture')]", + "i18n:SupportedCultures": "[parameters('SupportedCultures')]", + "AzureserviceBusDataOwner": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', '090c5cfd-751d-490a-894a-3ce6f1109419')]", + "StorageBlobDataContributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')]", + "keyvaultName": "[concat(variables('botAppName'), 'vault')]", + "keyVaultUrl": "[concat('https://',variables('keyvaultName'), '.vault.usgovcloudapi.net/')]", + "subscriptionTenantId": "[subscription().tenantId]", + "StorageAccountSecretName": "[concat(variables('keyVaultName'), 'StorageAccountConnectionString')]", + "ServiceBusSecretName": "[concat(variables('keyVaultName'), 'ServiceBusConnectionString')]", + "AppInsightsSecretName": "[concat(variables('keyVaultName'),'AppInsightsKey')]", + "UserAppSecretName": "[concat(variables('keyVaultName'),'UserAppPassword')]", + "AuthorAppSecretName": "[concat(variables('keyVaultName'),'AuthorAppPassword')]", + "GraphAppSecretName": "[concat(variables('keyVaultName'),'GraphAppPassword')]", + "StorageAccountSecretResourceId": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('StorageAccountSecretName'))]", + "ServiceBusSecretResourceId": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('ServiceBusSecretName'))]", + "AppInsightsSecretResourceId": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('AppInsightsSecretName'))]", + "UserAppSecretResourceId": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('UserAppSecretName'))]", + "AuthorAppSecretResourceId": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('AuthorAppSecretName'))]", + "GraphAppSecretResourceId": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('GraphAppSecretName'))]", + "netFrameworkVersion": "v6.0", + "ChannelService": "https:botframework.azure.us", + "ForceCompleteMessageDelayInSeconds": "86400", + "GraphBaseUrl": "https://graph.microsoft.us/v1.0", + "GroupsGraphScope": "GroupMember.Read.All", + "OAuthUrl": "https://tokengcch.botframework.azure.us/", + "ToBotFromChannelOpenIdMetadataUrl": "https://login.botframework.azure.us/v1/.well-known/openidconfiguration", + "ToBotFromChannelTokenIssuer": "https://api.botframework.us", + "ToBotFromEmulatorOpenIdMetadataUrl": "https://login.microsoftonline.us/cab8a31a-1906-4287-a0d8-4eef66b95f6e/v2.0/.well-known/openid-configuration", + "ToChannelFromBotLoginUrl": "https://login.microsoftonline.us/MicrosoftServices.onmicrosoft.us", + "ToChannelFromBotOAuthScope": "https://api.botframework.us", + "Instance": "https://login.microsoftonline.us/", + "ValidIssuers": "https://login.microsoftonline.us/TENANT_ID/v2.0,https://sts.windows.net/TENANT_ID/" + + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "apiVersion": "2019-06-01", + "location": "[parameters('location')]", + "kind": "Storage", + "properties": { + "supportsHttpsTrafficOnly": true, + "allowBlobPublicAccess": false, + "minimumTlsVersion": "TLS1_2" + }, + "sku": { + "name": "Standard_LRS" + } + }, + { + "type": "Microsoft.ServiceBus/namespaces", + "apiVersion": "2017-04-01", + "name": "[variables('serviceBusNamespaceName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Basic", + "tier": "Basic" + }, + "resources": [ + { + "type": "Queues", + "apiVersion": "2017-04-01", + "name": "[variables('serviceBusSendQueueName')]", + "dependsOn": [ + "[concat('Microsoft.ServiceBus/namespaces/', variables('serviceBusNamespaceName'))]" + ], + "properties": { + "lockDuration": "PT5M", + "maxSizeInMegabytes": 1024, + "requiresDuplicateDetection": false, + "requiresSession": false, + "defaultMessageTimeToLive": "P14D", + "deadLetteringOnMessageExpiration": false, + "enableBatchedOperations": true, + "duplicateDetectionHistoryTimeWindow": "PT10M", + "maxDeliveryCount": 10, + "status": "Active", + "enablePartitioning": false, + "enableExpress": false + } + }, + { + "type": "Queues", + "apiVersion": "2017-04-01", + "name": "[variables('serviceBusDataQueueName')]", + "dependsOn": [ + "[concat('Microsoft.ServiceBus/namespaces/', variables('serviceBusNamespaceName'))]" + ], + "properties": { + "lockDuration": "PT5M", + "maxSizeInMegabytes": 1024, + "requiresDuplicateDetection": false, + "requiresSession": false, + "defaultMessageTimeToLive": "P14D", + "deadLetteringOnMessageExpiration": false, + "enableBatchedOperations": true, + "duplicateDetectionHistoryTimeWindow": "PT10M", + "maxDeliveryCount": 10, + "status": "Active", + "enablePartitioning": false, + "enableExpress": false + } + }, + { + "type": "Queues", + "apiVersion": "2017-04-01", + "name": "[variables('serviceBusPrepareToSendQueueName')]", + "dependsOn": [ + "[concat('Microsoft.ServiceBus/namespaces/', variables('serviceBusNamespaceName'))]" + ], + "properties": { + "lockDuration": "PT5M", + "maxSizeInMegabytes": 1024, + "requiresDuplicateDetection": false, + "requiresSession": false, + "defaultMessageTimeToLive": "P14D", + "deadLetteringOnMessageExpiration": false, + "enableBatchedOperations": true, + "duplicateDetectionHistoryTimeWindow": "PT10M", + "maxDeliveryCount": 10, + "status": "Active", + "enablePartitioning": false, + "enableExpress": false + } + }, + { + "type": "Queues", + "apiVersion": "2017-04-01", + "name": "[variables('serviceBusExportQueueName')]", + "dependsOn": [ + "[concat('Microsoft.ServiceBus/namespaces/', variables('serviceBusNamespaceName'))]" + ], + "properties": { + "lockDuration": "PT5M", + "maxSizeInMegabytes": 1024, + "requiresDuplicateDetection": false, + "requiresSession": false, + "defaultMessageTimeToLive": "P14D", + "deadLetteringOnMessageExpiration": false, + "enableBatchedOperations": true, + "duplicateDetectionHistoryTimeWindow": "PT10M", + "maxDeliveryCount": 10, + "status": "Active", + "enablePartitioning": false, + "enableExpress": false + } + } + ] + }, + { + "type": "Microsoft.Web/serverfarms", + "apiVersion": "2016-09-01", + "name": "[variables('hostingPlanName')]", + "location": "[parameters('location')]", + "properties": { + "name": "[variables('hostingPlanName')]", + "hostingEnvironment": "", + "numberOfWorkers": 1 + }, + "sku": { + "name": "[if(variables('isSharedPlan'), concat(variables('skuFamily'), '1'), concat(variables('skuFamily'), parameters('hostingPlanSize')))]", + "tier": "[parameters('hostingPlanSku')]", + "size": "[concat(variables('skuFamily'), parameters('hostingPlanSize'))]", + "family": "[variables('skuFamily')]", + "capacity": 0 + } + }, + { + "apiVersion": "2015-05-01", + "name": "[variables('appInsightsName')]", + "type": "Microsoft.Insights/components", + "location": "[parameters('location')]", + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', variables('botAppName'))]": "Resource" + }, + "properties": { + "Application_Type": "web", + "Request_Source": "rest" + } + }, + { + "apiVersion": "2021-03-01", + "name": "[variables('authorBotName')]", + "type": "Microsoft.BotService/botServices", + "location": "global", + "sku": { + "name": "F0" + }, + "kind": "azurebot", + "properties": { + "displayName": "[concat(parameters('appDisplayName'),'-author')]", + "description": "[parameters('appDescription')]", + "iconUrl": "[parameters('appIconUrl')]", + "msaAppId": "[parameters('authorClientId')]", + "endpoint": "[concat(variables('botAppUrl'), '/api/messages/author')]", + "developerAppInsightKey": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2015-05-01').InstrumentationKey]" + }, + "resources": [ + { + "name": "[concat(variables('authorBotName'), '/MsTeamsChannel')]", + "type": "Microsoft.BotService/botServices/channels", + "apiVersion": "2021-03-01", + "location": "global", + "sku": { + "name": "F0" + }, + "properties": { + "channelName": "MsTeamsChannel", + "location": "global", + "properties": { + "isEnabled": true + } + }, + "dependsOn": [ + "[concat('Microsoft.BotService/botServices/', variables('authorBotName'))]" + ] + } + ] + }, + { + "apiVersion": "2021-03-01", + "name": "[variables('botName')]", + "type": "Microsoft.BotService/botServices", + "location": "global", + "sku": { + "name": "F0" + }, + "kind": "azurebot", + "properties": { + "displayName": "[parameters('appDisplayName')]", + "description": "[parameters('appDescription')]", + "iconUrl": "[parameters('appIconUrl')]", + "msaAppId": "[parameters('userClientId')]", + "endpoint": "[concat(variables('botAppUrl'), '/api/messages/user')]", + "developerAppInsightKey": "[reference(resourceId('Microsoft.Insights/components', variables('appInsightsName')), '2015-05-01').InstrumentationKey]" + }, + "resources": [ + { + "name": "[concat(variables('botName'), '/MsTeamsChannel')]", + "type": "Microsoft.BotService/botServices/channels", + "apiVersion": "2021-03-01", + "location": "global", + "sku": { + "name": "F0" + }, + "properties": { + "channelName": "MsTeamsChannel", + "location": "global", + "properties": { + "isEnabled": true + } + }, + "dependsOn": [ + "[concat('Microsoft.BotService/botServices/', variables('botName'))]" + ] + } + ] + }, + { + "apiVersion": "2016-08-01", + "type": "Microsoft.Web/sites", + "name": "[variables('botAppName')]", + "location": "[parameters('location')]", + "kind": "app", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "name": "[variables('botAppName')]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "enabled": true, + "reserved": false, + "clientAffinityEnabled": true, + "clientCertEnabled": false, + "hostNamesDisabled": false, + "containerSize": 0, + "dailyMemoryTimeQuota": 0, + "httpsOnly": true, + "siteConfig": { + "ftpsState": "Disabled", + "alwaysOn": "[not(variables('isSharedPlan'))]", + "cors": { + "supportCredentials": true, + "allowedOrigins": [ + "[concat('https://', variables('frontDoorDomain'))]" + ] + }, + "metadata": [ + { + "name": "CURRENT_STACK", + "value": "dotnet" + } + ], + "netFrameworkVersion": "v6.0.100" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]", + "[resourceId('Microsoft.ServiceBus/namespaces', variables('serviceBusNamespaceName'))]", + "[resourceId('Microsoft.Web/sites', variables('prepFunctionAppName'))]", + "[resourceId('Microsoft.Web/sites', variables('sendFunctionAppName'))]", + "[resourceId('Microsoft.Web/sites', variables('dataFunctionAppName'))]" + ], + "resources": [ + { + "apiVersion": "2015-08-01", + "name": "appsettings", + "type": "config", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('botAppName'))]", + "[resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]", + "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('StorageAccountSecretName'))]", + "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('ServiceBusSecretName'))]" + ], + "properties": { + "PROJECT": "Source/CompanyCommunicator/Microsoft.Teams.Apps.CompanyCommunicator.csproj", + "SITE_ROLE": "app", + "i18n:DefaultCulture": "[variables('i18n:DefaultCulture')]", + "i18n:SupportedCultures": "[variables('i18n:SupportedCultures')]", + "ProactivelyInstallUserApp": "[variables('ProactivelyInstallUserApp')]", + "UserAppExternalId": "[variables('UserAppExternalId')]", + "AzureAd:TenantId": "[parameters('tenantId')]", + "AzureAd:ClientId": "[parameters('graphAppId')]", + "AzureAd:ClientSecret": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('GraphAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "AzureAd:ApplicationIdURI": "[if(variables('useFrontDoor'), concat('api://', variables('frontDoorDomain')), '')]", + "AzureAd:Instance": "[variables('Instance')]", + "AzureAd:ValidIssuers": "[variables('ValidIssuers')]", + "UserAppId": "[parameters('userClientId')]", + "UserAppPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('UserAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "AuthorAppId": "[parameters('authorClientId')]", + "AuthorAppPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('AuthorAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "GraphAppId": "[parameters('graphAppId')]", + "GraphAppPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('GraphAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "StorageAccountConnectionString": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('StorageAccountSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "ServiceBusConnection": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('ServiceBusSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "ServiceBusNamespace": "[concat(variables('serviceBusNamespaceName'),'.servicebus.usgovcloudapi.net')]", + "StorageAccountName": "[variables('storageAccountName')]", + "UseManagedIdentity": "true", + "AllowedTenants": "[parameters('tenantId')]", + "DisableTenantFilter": "false", + "AuthorizedCreatorUpns": "[parameters('senderUPNList')]", + "UseCertificate": "false", + "DisableAuthentication": "false", + "DisableCreatorUpnCheck": "false", + "WEBSITE_LOAD_CERTIFICATES": "*", + "APPINSIGHTS_INSTRUMENTATIONKEY": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('AppInsightsSecretResourceId'), '2015-06-01').secretUriWithVersion, ')')]", + "WEBSITE_NODE_DEFAULT_VERSION": "16.13.0", + "WEBSITE_NPM_DEFAULT_VERSION": "8.19.2", + "KeyVault:Url": "[variables('keyVaultUrl')]", + "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH": "false", + "REACT_APP_HEADERTEXT": "[parameters('headerText')]", + "REACT_APP_HEADERIMAGE": "[parameters('headerLogoUrl')]", + "APPLICATIONINSIGHTS_CONNECTION_STRING": "[concat('InstrumentationKey=',reference(resourceId('microsoft.insights/components/', variables('appInsightsName')), '2015-05-01').InstrumentationKey,';EndpointSuffix=applicationinsights.us;IngestionEndpoint=https://usgovvirginia-0.in.applicationinsights.azure.us/;AADAudience=https://monitor.azure.us/')]", + "ForceCompleteMessageDelayInSeconds": "[variables('ForceCompleteMessageDelayInSeconds')]", + "ChannelService": "[variables('ChannelService')]", + "GraphBaseUrl": "[variables('GraphBaseUrl')]", + "GroupsGraphScope": "[variables('GroupsGraphScope')]", + "OAuthUrl": "[variables('OAuthUrl')]", + "TeamsEnvironment": "GCCH", + "ToBotFromChannelOpenIdMetadataUrl": "[variables('ToBotFromChannelOpenIdMetadataUrl')]", + "ToBotFromChannelTokenIssuer": "[variables('ToBotFromChannelTokenIssuer')]", + "ToBotFromEmulatorOpenIdMetadataUrl": "[variables('ToBotFromEmulatorOpenIdMetadataUrl')]", + "ToChannelFromBotLoginUrl": "[variables('ToChannelFromBotLoginUrl')]", + "ToChannelFromBotOAuthScope": "[variables('ToChannelFromBotOAuthScope')]", + "ValidateAuthority": "true" + } + }, + { + "apiVersion": "2016-08-01", + "name": "web", + "type": "sourcecontrols", + "condition": "[not(empty(parameters('gitRepoUrl')))]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('botAppName'))]", + "[resourceId('Microsoft.Web/sites/config', variables('botAppName'), 'appsettings')]" + ], + "properties": { + "RepoUrl": "[parameters('gitRepoUrl')]", + "branch": "[parameters('gitBranch')]", + "IsManualIntegration": true + } + } + ] + }, + { + "apiVersion": "2016-08-01", + "type": "Microsoft.Web/sites", + "name": "[variables('prepFunctionAppName')]", + "location": "[parameters('location')]", + "kind": "functionapp", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "name": "[variables('prepFunctionAppName')]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "hostingEnvironment": "", + "clientAffinityEnabled": false, + "httpsOnly": true, + "siteConfig": { + "ftpsState": "Disabled", + "alwaysOn": "[not(variables('isSharedPlan'))]", + "netFrameworkVersion": "[variables('netFrameworkVersion')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.ServiceBus/namespaces', variables('serviceBusNamespaceName'))]" + ], + "resources": [ + { + "apiVersion": "2015-08-01", + "name": "appsettings", + "type": "config", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('prepFunctionAppName'))]", + "[resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]", + "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('StorageAccountSecretName'))]", + "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('ServiceBusSecretName'))]" + ], + "properties": { + "PROJECT": "Source\\CompanyCommunicator.Prep.Func\\Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.csproj", + "SITE_ROLE": "function", + "i18n:DefaultCulture": "[variables('i18n:DefaultCulture')]", + "i18n:SupportedCultures": "[variables('i18n:SupportedCultures')]", + "ProactivelyInstallUserApp": "[variables('ProactivelyInstallUserApp')]", + "UserAppExternalId": "[variables('UserAppExternalId')]", + "AzureAd:TenantId": "[parameters('tenantId')]", + "TenantId": "[parameters('tenantId')]", + "UserAppId": "[parameters('userClientId')]", + "UserAppPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('UserAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "AuthorAppId": "[parameters('authorClientId')]", + "AuthorAppPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('AuthorAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "GraphAppId": "[parameters('graphAppId')]", + "GraphAppPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('GraphAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "StorageAccountConnectionString": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('StorageAccountSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "ServiceBusConnection": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('ServiceBusSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "ServiceBusNamespace": "[concat(variables('serviceBusNamespaceName'),'.servicebus.usgovcloudapi.net')]", + "StorageAccountName": "[variables('storageAccountName')]", + "UseManagedIdentity": "true", + "UseCertificate": "false", + "WEBSITE_LOAD_CERTIFICATES": "*", + "APPINSIGHTS_INSTRUMENTATIONKEY": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('AppInsightsSecretResourceId'), '2015-06-01').secretUriWithVersion, ')')]", + "KeyVault:Url": "[variables('keyVaultUrl')]", + "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1, ';EndpointSuffix=','core.usgovcloudapi.net')]", + "AzureWebJobsDashboard": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('StorageAccountSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "FUNCTIONS_EXTENSION_VERSION": "~4", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1, ';EndpointSuffix=','core.usgovcloudapi.net')]", + "WEBSITE_CONTENTSHARE": "[toLower(variables('prepFunctionAppName'))]", + "AzureFunctionsJobHost__extensions__durableTask__maxConcurrentOrchestratorFunctions": "3", + "AzureFunctionsJobHost__extensions__durableTask__maxConcurrentActivityFunctions": "10", + "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH": "false", + "WEBSITE_NODE_DEFAULT_VERSION": "16.13.0", + "APPLICATIONINSIGHTS_CONNECTION_STRING": "[concat('InstrumentationKey=',reference(resourceId('microsoft.insights/components/', variables('appInsightsName')), '2015-05-01').InstrumentationKey,';EndpointSuffix=applicationinsights.us;IngestionEndpoint=https://usgovvirginia-0.in.applicationinsights.azure.us/;AADAudience=https://monitor.azure.us/')]", + "ChannelService": "[variables('ChannelService')]", + "GraphBaseUrl": "[variables('GraphBaseUrl')]", + "GroupsGraphScope": "[variables('GroupsGraphScope')]", + "OAuthUrl": "[variables('OAuthUrl')]", + "ToBotFromChannelOpenIdMetadataUrl": "[variables('ToBotFromChannelOpenIdMetadataUrl')]", + "ToBotFromChannelTokenIssuer": "[variables('ToBotFromChannelTokenIssuer')]", + "ToBotFromEmulatorOpenIdMetadataUrl": "[variables('ToBotFromEmulatorOpenIdMetadataUrl')]", + "ToChannelFromBotLoginUrl": "[variables('ToChannelFromBotLoginUrl')]", + "ToChannelFromBotOAuthScope": "[variables('ToChannelFromBotOAuthScope')]", + "TeamsEnvironment": "GCCH", + "ValidateAuthority": "true" + } + }, + { + "apiVersion": "2015-08-01", + "name": "web", + "type": "sourcecontrols", + "condition": "[not(empty(parameters('gitRepoUrl')))]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('prepFunctionAppName'))]", + "[resourceId('Microsoft.Web/sites/config', variables('prepFunctionAppName'), 'appsettings')]" + ], + "properties": { + "RepoUrl": "[parameters('gitRepoUrl')]", + "branch": "[parameters('gitBranch')]", + "IsManualIntegration": true + } + } + ] + }, + { + "apiVersion": "2016-08-01", + "type": "Microsoft.Web/sites", + "name": "[variables('sendFunctionAppName')]", + "location": "[parameters('location')]", + "kind": "functionapp", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "name": "[variables('sendFunctionAppName')]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "hostingEnvironment": "", + "clientAffinityEnabled": false, + "httpsOnly": true, + "siteConfig": { + "ftpsState": "Disabled", + "alwaysOn": "[not(variables('isSharedPlan'))]", + "netFrameworkVersion": "[variables('netFrameworkVersion')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.ServiceBus/namespaces', variables('serviceBusNamespaceName'))]" + ], + "resources": [ + { + "apiVersion": "2015-08-01", + "name": "appsettings", + "type": "config", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('sendFunctionAppName'))]", + "[resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]", + "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('StorageAccountSecretName'))]", + "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('ServiceBusSecretName'))]" + ], + "properties": { + "PROJECT": "Source\\CompanyCommunicator.Send.Func\\Microsoft.Teams.Apps.CompanyCommunicator.Send.Func.csproj", + "SITE_ROLE": "function", + "i18n:DefaultCulture": "[variables('i18n:DefaultCulture')]", + "i18n:SupportedCultures": "[variables('i18n:SupportedCultures')]", + "ProactivelyInstallUserApp": "[variables('ProactivelyInstallUserApp')]", + "UserAppId": "[parameters('userClientId')]", + "UserAppPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('UserAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "StorageAccountConnectionString": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('StorageAccountSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "ServiceBusConnection": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('ServiceBusSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "ServiceBusNamespace": "[concat(variables('serviceBusNamespaceName'),'.servicebus.usgovcloudapi.net')]", + "StorageAccountName": "[variables('storageAccountName')]", + "UseManagedIdentity": "true", + "UseCertificate": "false", + "MaxNumberOfAttempts": "5", + "WEBSITE_LOAD_CERTIFICATES": "*", + "APPINSIGHTS_INSTRUMENTATIONKEY": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('AppInsightsSecretResourceId'), '2015-06-01').secretUriWithVersion, ')')]", + "KeyVault:Url": "[variables('keyVaultUrl')]", + "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1, ';EndpointSuffix=','core.usgovcloudapi.net')]", + "AzureWebJobsDashboard": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('StorageAccountSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "FUNCTIONS_EXTENSION_VERSION": "~4", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1, ';EndpointSuffix=','core.usgovcloudapi.net')]", + "WEBSITE_CONTENTSHARE": "[toLower(variables('sendFunctionAppName'))]", + "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH": "false", + "WEBSITE_NODE_DEFAULT_VERSION": "16.13.0", + "APPLICATIONINSIGHTS_CONNECTION_STRING": "[concat('InstrumentationKey=',reference(resourceId('microsoft.insights/components/', variables('appInsightsName')), '2015-05-01').InstrumentationKey,';EndpointSuffix=applicationinsights.us;IngestionEndpoint=https://usgovvirginia-0.in.applicationinsights.azure.us/;AADAudience=https://monitor.azure.us/')]", + "ChannelService": "[variables('ChannelService')]", + "GraphBaseUrl": "[variables('GraphBaseUrl')]", + "GroupsGraphScope": "[variables('GroupsGraphScope')]", + "OAuthUrl": "[variables('OAuthUrl')]", + "ToBotFromChannelOpenIdMetadataUrl": "[variables('ToBotFromChannelOpenIdMetadataUrl')]", + "ToBotFromChannelTokenIssuer": "[variables('ToBotFromChannelTokenIssuer')]", + "ToBotFromEmulatorOpenIdMetadataUrl": "[variables('ToBotFromEmulatorOpenIdMetadataUrl')]", + "ToChannelFromBotLoginUrl": "[variables('ToChannelFromBotLoginUrl')]", + "ToChannelFromBotOAuthScope": "[variables('ToChannelFromBotOAuthScope')]", + "TeamsEnvironment": "GCCH", + "ValidateAuthority": "true" + } + }, + { + "apiVersion": "2015-08-01", + "name": "web", + "type": "sourcecontrols", + "condition": "[not(empty(parameters('gitRepoUrl')))]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('sendFunctionAppName'))]", + "[resourceId('Microsoft.Web/sites/config', variables('sendFunctionAppName'), 'appsettings')]" + ], + "properties": { + "RepoUrl": "[parameters('gitRepoUrl')]", + "branch": "[parameters('gitBranch')]", + "IsManualIntegration": true + } + } + ] + }, + { + "apiVersion": "2016-08-01", + "type": "Microsoft.Web/sites", + "name": "[variables('dataFunctionAppName')]", + "location": "[parameters('location')]", + "kind": "functionapp", + "identity": { + "type": "SystemAssigned" + }, + "properties": { + "name": "[variables('dataFunctionAppName')]", + "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "hostingEnvironment": "", + "clientAffinityEnabled": false, + "httpsOnly": true, + "siteConfig": { + "ftpsState": "Disabled", + "alwaysOn": "[not(variables('isSharedPlan'))]", + "netFrameworkVersion": "[variables('netFrameworkVersion')]" + } + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms', variables('hostingPlanName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.ServiceBus/namespaces', variables('serviceBusNamespaceName'))]" + ], + "resources": [ + { + "apiVersion": "2015-08-01", + "name": "appsettings", + "type": "config", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('dataFunctionAppName'))]", + "[resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]", + "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('StorageAccountSecretName'))]", + "[resourceId('Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('ServiceBusSecretName'))]" + ], + "properties": { + "PROJECT": "Source\\CompanyCommunicator.Data.Func\\Microsoft.Teams.Apps.CompanyCommunicator.Data.Func.csproj", + "SITE_ROLE": "function", + "i18n:DefaultCulture": "[variables('i18n:DefaultCulture')]", + "i18n:SupportedCultures": "[variables('i18n:SupportedCultures')]", + "ProactivelyInstallUserApp": "[variables('ProactivelyInstallUserApp')]", + "UserAppId": "[parameters('userClientId')]", + "UserAppPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('UserAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "AuthorAppId": "[parameters('authorClientId')]", + "AuthorAppPassword": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('AuthorAppSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "StorageAccountConnectionString": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('StorageAccountSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "ServiceBusConnection": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('ServiceBusSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "ServiceBusNamespace": "[concat(variables('serviceBusNamespaceName'),'.servicebus.usgovcloudapi.net')]", + "StorageAccountName": "[variables('storageAccountName')]", + "UseManagedIdentity": "true", + "UseCertificate": "false", + "WEBSITE_LOAD_CERTIFICATES": "*", + "APPINSIGHTS_INSTRUMENTATIONKEY": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('AppInsightsSecretResourceId'), '2015-06-01').secretUriWithVersion, ')')]", + "KeyVault:Url": "[variables('keyVaultUrl')]", + "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1, ';EndpointSuffix=','core.usgovcloudapi.net')]", + "AzureWebJobsDashboard": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('StorageAccountSecretResourceId'),'2015-06-01').secretUriWithVersion, ')')]", + "FUNCTIONS_EXTENSION_VERSION": "~4", + "FUNCTIONS_WORKER_RUNTIME": "dotnet", + "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1, ';EndpointSuffix=','core.usgovcloudapi.net')]", + "WEBSITE_CONTENTSHARE": "[toLower(variables('dataFunctionAppName'))]", + "CleanUpScheduleTriggerTime": "30 23 * * *", + "CleanUpFile": "1", + "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH": "false", + "WEBSITE_NODE_DEFAULT_VERSION": "16.13.0", + "APPLICATIONINSIGHTS_CONNECTION_STRING": "[concat('InstrumentationKey=',reference(resourceId('microsoft.insights/components/', variables('appInsightsName')), '2015-05-01').InstrumentationKey,';EndpointSuffix=applicationinsights.us;IngestionEndpoint=https://usgovvirginia-0.in.applicationinsights.azure.us/;AADAudience=https://monitor.azure.us/')]", + "ChannelService": "[variables('ChannelService')]", + "GraphBaseUrl": "[variables('GraphBaseUrl')]", + "GroupsGraphScope": "[variables('GroupsGraphScope')]", + "OAuthUrl": "[variables('OAuthUrl')]", + "ToBotFromChannelOpenIdMetadataUrl": "[variables('ToBotFromChannelOpenIdMetadataUrl')]", + "ToBotFromChannelTokenIssuer": "[variables('ToBotFromChannelTokenIssuer')]", + "ToBotFromEmulatorOpenIdMetadataUrl": "[variables('ToBotFromEmulatorOpenIdMetadataUrl')]", + "ToChannelFromBotLoginUrl": "[variables('ToChannelFromBotLoginUrl')]", + "ToChannelFromBotOAuthScope": "[variables('ToChannelFromBotOAuthScope')]", + "TeamsEnvironment": "GCCH", + "ValidateAuthority": "true" + } + }, + { + "apiVersion": "2015-08-01", + "name": "web", + "type": "sourcecontrols", + "condition": "[not(empty(parameters('gitRepoUrl')))]", + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('dataFunctionAppName'))]", + "[resourceId('Microsoft.Web/sites/config', variables('dataFunctionAppName'), 'appsettings')]" + ], + "properties": { + "RepoUrl": "[parameters('gitRepoUrl')]", + "branch": "[parameters('gitBranch')]", + "IsManualIntegration": true + } + } + ] + }, + { + "type": "Microsoft.KeyVault/vaults", + "name": "[variables('keyvaultName')]", + "apiVersion": "2016-10-01", + "location": "[parameters('location')]", + + "tags": { + "displayName": "KeyVault" + }, + "properties": { + "tenantId": "[variables('subscriptionTenantId')]", + "enableSoftDelete": true, + "enabledForDeployment": false, + "enabledForDiskEncryption": false, + "enabledForTemplateDeployment": true, + "accessPolicies": [ + { + "tenantId": "[reference(concat('Microsoft.Web/sites/', variables('botAppName')), '2018-02-01', 'Full').identity.tenantId]", + "objectId": "[reference(concat('Microsoft.Web/sites/', variables('botAppName')), '2018-02-01', 'Full').identity.principalId]", + "permissions": { + "keys": [], + "secrets": [ + "Get", + "Set", + "Restore" + ], + "certificates": [] + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('botAppName'))]" + ] + }, + { + "tenantId": "[reference(concat('Microsoft.Web/sites/', variables('prepFunctionAppName')), '2018-02-01', 'Full').identity.tenantId]", + "objectId": "[reference(concat('Microsoft.Web/sites/', variables('prepFunctionAppName')), '2018-02-01', 'Full').identity.principalId]", + "permissions": { + "keys": [], + "secrets": [ + "Get" + ], + "certificates": [ + "Get" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('prepFunctionAppName'))]" + ] + }, + { + "tenantId": "[reference(concat('Microsoft.Web/sites/', variables('sendFunctionAppName')), '2018-02-01', 'Full').identity.tenantId]", + "objectId": "[reference(concat('Microsoft.Web/sites/', variables('sendFunctionAppName')), '2018-02-01', 'Full').identity.principalId]", + "permissions": { + "keys": [], + "secrets": [ + "Get" + ], + "certificates": [ + "Get" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('sendFunctionAppName'))]" + ] + }, + { + "tenantId": "[reference(concat('Microsoft.Web/sites/', variables('dataFunctionAppName')), '2018-02-01', 'Full').identity.tenantId]", + "objectId": "[reference(concat('Microsoft.Web/sites/', variables('dataFunctionAppName')), '2018-02-01', 'Full').identity.principalId]", + "permissions": { + "keys": [], + "secrets": [ + "Get" + ], + "certificates": [ + "Get" + ] + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites', variables('dataFunctionAppName'))]" + ] + } + ], + "sku": { + "name": "Standard", + "family": "A" + }, + "networkAcls": { + "bypass": "AzureServices", + "defaultAction": "Allow" + } + }, + "resources": [ + { + "type": "secrets", + "apiVersion": "2016-10-01", + "name": "[variables('StorageAccountSecretName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "properties": { + "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storageAccountName'), ';AccountKey=', listKeys(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')),'2015-05-01-preview').key1, ';EndpointSuffix=','core.usgovcloudapi.net')]", + "attributes": { + "enabled": true + } + } + }, + { + "type": "secrets", + "apiVersion": "2016-10-01", + "name": "[variables('ServiceBusSecretName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('keyVaultName'))]", + "[resourceId('Microsoft.ServiceBus/namespaces', variables('serviceBusNamespaceName'))]" + ], + "properties": { + "value": "[listkeys(variables('authRuleResourceId'), '2017-04-01').primaryConnectionString]", + "attributes": { + "enabled": true + } + } + }, + { + "type": "secrets", + "apiVersion": "2016-10-01", + "name": "[variables('UserAppSecretName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]" + ], + "properties": { + "value": "[parameters('userClientSecret')]", + "attributes": { + "enabled": true + } + } + }, + { + "type": "secrets", + "apiVersion": "2016-10-01", + "name": "[variables('AuthorAppSecretName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]" + ], + "properties": { + "value": "[parameters('authorClientSecret')]", + "attributes": { + "enabled": true + } + } + }, + { + "type": "secrets", + "apiVersion": "2016-10-01", + "name": "[variables('GraphAppSecretName')]", + "location": "[parameters('location')]", + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults', variables('keyvaultName'))]" + ], + "properties": { + "value": "[parameters('graphAppSecret')]", + "attributes": { + "enabled": true + } + } + }, + { + "type": "secrets", + "name": "[variables('AppInsightsSecretName')]", + "location": "[parameters('location')]", + "apiVersion": "2018-02-14", + "dependsOn": [ + "[resourceId('Microsoft.KeyVault/vaults/', variables('keyVaultName'))]", + "[resourceId('Microsoft.Insights/components', variables('appInsightsName'))]" + ], + "properties": { + "value": "[reference(resourceId('Microsoft.Insights/components/', variables('appInsightsName')), '2015-05-01').InstrumentationKey]", + "attributes": { + "enabled": true + } + } + } + ] + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[parameters('ServiceBusWebAppRoleNameGuid')]", + "scope": "[concat('Microsoft.ServiceBus/namespaces', '/', variables('serviceBusNamespaceName'))]", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', variables('serviceBusNamespaceName'))]", + "[resourceId('Microsoft.Web/sites', variables('botAppName'))]" + ], + "properties": { + "roleDefinitionId": "[variables('AzureserviceBusDataOwner')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('botAppName')), '2019-08-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[parameters('ServiceBusPrepFuncRoleNameGuid')]", + "scope": "[concat('Microsoft.ServiceBus/namespaces', '/', variables('serviceBusNamespaceName'))]", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', variables('serviceBusNamespaceName'))]", + "[resourceId('Microsoft.Web/sites', variables('prepFunctionAppName'))]" + ], + "properties": { + "roleDefinitionId": "[variables('AzureserviceBusDataOwner')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('prepFunctionAppName')), '2019-08-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[parameters('ServiceBusSendFuncRoleNameGuid')]", + "scope": "[concat('Microsoft.ServiceBus/namespaces', '/', variables('serviceBusNamespaceName'))]", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', variables('serviceBusNamespaceName'))]", + "[resourceId('Microsoft.Web/sites', variables('sendFunctionAppName'))]" + ], + "properties": { + "roleDefinitionId": "[variables('AzureserviceBusDataOwner')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('sendFunctionAppName')), '2019-08-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[parameters('ServiceBusDataFuncRoleNameGuid')]", + "scope": "[concat('Microsoft.ServiceBus/namespaces', '/', variables('serviceBusNamespaceName'))]", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', variables('serviceBusNamespaceName'))]", + "[resourceId('Microsoft.Web/sites', variables('dataFunctionAppName'))]" + ], + "properties": { + "roleDefinitionId": "[variables('AzureserviceBusDataOwner')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('dataFunctionAppName')), '2019-08-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[parameters('StorageAccountWebAppRoleNameGuid')]", + "scope": "[concat('Microsoft.Storage/storageAccounts', '/', variables('storageAccountName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.Web/sites', variables('botAppName'))]" + ], + "properties": { + "roleDefinitionId": "[variables('StorageBlobDataContributor')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('botAppName')), '2019-08-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[parameters('StorageAccountPrepFuncRoleNameGuid')]", + "scope": "[concat('Microsoft.Storage/storageAccounts', '/', variables('storageAccountName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.Web/sites', variables('prepFunctionAppName'))]" + ], + "properties": { + "roleDefinitionId": "[variables('StorageBlobDataContributor')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('prepFunctionAppName')), '2019-08-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2020-04-01-preview", + "name": "[parameters('StorageAccountDataFuncRoleNameGuid')]", + "scope": "[concat('Microsoft.Storage/storageAccounts', '/', variables('storageAccountName'))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "[resourceId('Microsoft.Web/sites', variables('dataFunctionAppName'))]" + ], + "properties": { + "roleDefinitionId": "[variables('StorageBlobDataContributor')]", + "principalId": "[reference(resourceId('Microsoft.Web/sites', variables('dataFunctionAppName')), '2019-08-01', 'Full').identity.principalId]", + "principalType": "ServicePrincipal" + } + }, + { + "type": "Microsoft.Network/frontDoors", + "apiVersion": "2021-06-01", + "name": "[variables('frontDoorName')]", + "location": "Global", + "properties": { + "backendPools": [ + { + "id": "[concat(resourceId('Microsoft.Network/frontdoors',variables('frontDoorName')), '/BackendPools/backendPool1')]", + "name": "backendPool1", + "properties": { + "backends": [ + { + "address": "[variables('botAppDomain')]", + "backendHostHeader": "[variables('botAppDomain')]", + "httpPort": 80, + "httpsPort": 443, + "priority": 1, + "weight": 50, + "enabledState": "Enabled" + } + ], + "healthProbeSettings": { + "id": "[concat(resourceId('Microsoft.Network/frontdoors', variables('frontDoorName')), '/healthProbeSettings/healthProbeSettings1')]" + }, + "loadBalancingSettings": { + "id": "[concat(resourceId('Microsoft.Network/frontdoors', variables('frontDoorName')), '/loadBalancingSettings/loadBalancingSettings1')]" + }, + "resourceState": "Enabled" + } + } + ], + "healthProbeSettings": [ + { + "id": "[concat(resourceId('Microsoft.Network/frontdoors', variables('frontDoorName')), '/HealthProbeSettings/healthProbeSettings1')]", + "name": "healthProbeSettings1", + "properties": { + "intervalInSeconds": 255, + "path": "/health", + "protocol": "Https", + "resourceState": "Enabled" + } + } + ], + "frontendEndpoints": [ + { + "id": "[concat(resourceId('Microsoft.Network/frontdoors', variables('frontDoorName')), concat('/frontendEndpoints/', variables('frontDoorName'), '-azurefd-us'))]", + "name": "[concat(variables('frontDoorName'), '-azurefd-us')]", + "properties": { + "hostName": "[concat(variables('frontDoorName'), '.azurefd.us')]", + "sessionAffinityEnabledState": "Disabled", + "sessionAffinityTtlSeconds": 0 + } + } + ], + "loadBalancingSettings": [ + { + "name": "loadBalancingSettings1", + "properties": { + "additionalLatencyMilliseconds": 0, + "sampleSize": 4, + "successfulSamplesRequired": 2 + } + } + ], + "routingRules": [ + { + "name": "routingRule1", + "properties": { + "frontendEndpoints": [ + { + "id": "[concat(resourceId('Microsoft.Network/frontdoors', variables('frontDoorName')), concat('/frontendEndpoints/', variables('frontDoorName'), '-azurefd-us'))]" + } + ], + "acceptedProtocols": [ + "Https" + ], + "patternsToMatch": [ + "/*" + ], + "routeConfiguration": { + "@odata.type": "#Microsoft.Azure.FrontDoor.Models.FrontdoorForwardingConfiguration", + "forwardingProtocol": "HttpsOnly", + "backendPool": { + "id": "[resourceId('Microsoft.Network/frontDoors/backendPools', variables('frontDoorName'), 'backendPool1')]" + } + }, + "enabledState": "Enabled" + } + }, + { + "name": "routingRule2", + "properties": { + "frontendEndpoints": [ + { + "id": "[concat(resourceId('Microsoft.Network/frontdoors', variables('frontDoorName')), concat('/frontendEndpoints/', variables('frontDoorName'), '-azurefd-us'))]" + } + ], + "acceptedProtocols": [ + "Https" + ], + "patternsToMatch": [ + "/api/*" + ], + "routeConfiguration": { + "@odata.type": "#Microsoft.Azure.FrontDoor.Models.FrontdoorRedirectConfiguration", + "customFragment": null, + "customHost": "[variables('botAppDomain')]", + "customPath": "", + "redirectProtocol": "HttpsOnly", + "customQueryString": null, + "redirectType": "PermanentRedirect" + }, + "enabledState": "Enabled" + } + } + ], + "backendPoolsSettings": { + "enforceCertificateNameCheck": "Enabled", + "sendRecvTimeoutSeconds": 30 + }, + "enabledState": "Enabled", + "friendlyName": "[variables('frontDoorName')]", + "resourceState": "Enabled" + + }, + "condition": "[variables('useFrontDoor')]" + } + ], + "outputs": { + "keyVaultName": { + "type": "string", + "value": "[variables('keyvaultName')]" + }, + "authorBotId": { + "type": "string", + "value": "[parameters('authorClientId')]" + }, + "userBotId": { + "type": "string", + "value": "[parameters('userClientId')]" + }, + "appDomain": { + "type": "string", + "value": "[if(variables('useFrontDoor'), variables('frontDoorDomain'), concat('Please create a custom domain name for ', variables('botAppDomain'), ' and use that in the manifest'))]" + } + } +} \ No newline at end of file diff --git a/Deployment/azuredeploy.json b/Deployment/azuredeploy.json index aef89b9e9..18375a26e 100644 --- a/Deployment/azuredeploy.json +++ b/Deployment/azuredeploy.json @@ -317,7 +317,7 @@ "UserAppSecretResourceId": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('UserAppSecretName'))]", "AuthorAppSecretResourceId": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('AuthorAppSecretName'))]", "GraphAppSecretResourceId": "[resourceId(resourceGroup().name, 'Microsoft.KeyVault/vaults/secrets', variables('keyVaultName'), variables('GraphAppSecretName'))]", - "netFrameworkVersion":"v6.0" + "netFrameworkVersion": "v6.0" }, "resources": [ { @@ -633,6 +633,7 @@ "WEBSITE_LOAD_CERTIFICATES": "*", "APPINSIGHTS_INSTRUMENTATIONKEY": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('AppInsightsSecretResourceId'), '2015-06-01').secretUriWithVersion, ')')]", "WEBSITE_NODE_DEFAULT_VERSION": "16.13.0", + "WEBSITE_NPM_DEFAULT_VERSION": "8.19.2", "KeyVault:Url": "[variables('keyVaultUrl')]", "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH": "false", "REACT_APP_HEADERTEXT": "[parameters('headerText')]", diff --git a/Deployment/azuredeploywithcert.json b/Deployment/azuredeploywithcert.json index 578dea8b2..551a8f084 100644 --- a/Deployment/azuredeploywithcert.json +++ b/Deployment/azuredeploywithcert.json @@ -633,6 +633,7 @@ "WEBSITE_LOAD_CERTIFICATES": "*", "APPINSIGHTS_INSTRUMENTATIONKEY": "[concat('@Microsoft.KeyVault(SecretUri=', reference(variables('AppInsightsSecretResourceId'), '2015-06-01').secretUriWithVersion, ')')]", "WEBSITE_NODE_DEFAULT_VERSION": "16.13.0", + "WEBSITE_NPM_DEFAULT_VERSION": "8.19.2", "KeyVault:Url": "[variables('keyVaultUrl')]", "DOTNET_ADD_GLOBAL_TOOLS_TO_PATH": "false", "REACT_APP_HEADERTEXT": "[parameters('headerText')]", diff --git a/Manifest/manifest_authors.json b/Manifest/manifest_authors.json index f67ba56dd..94d3b1139 100644 --- a/Manifest/manifest_authors.json +++ b/Manifest/manifest_authors.json @@ -1,7 +1,7 @@ { "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", "manifestVersion": "1.5", - "version": "5.3.0", + "version": "5.4.0", "id": "1c07cd26-a088-4db8-8928-ace382fa219f", "packageName": "com.microsoft.teams.companycommunicator.authors", "developer": { diff --git a/Manifest/manifest_users.json b/Manifest/manifest_users.json index 311e3b198..74d76e7fc 100644 --- a/Manifest/manifest_users.json +++ b/Manifest/manifest_users.json @@ -1,7 +1,7 @@ { "$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json", "manifestVersion": "1.5", - "version": "5.3.0", + "version": "5.4.0", "id": "148a66bb-e83d-425a-927d-09f4299a9274", "packageName": "com.microsoft.teams.companycommunicator", "developer": { diff --git a/Source/CompanyCommunicator.Common/Adapter/CCBotAdapter.cs b/Source/CompanyCommunicator.Common/Adapter/CCBotAdapter.cs new file mode 100644 index 000000000..916e929f4 --- /dev/null +++ b/Source/CompanyCommunicator.Common/Adapter/CCBotAdapter.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// + +namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Adapter +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Bot.Builder; + using Microsoft.Bot.Connector.Authentication; + using Microsoft.Bot.Schema; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Secrets; + + /// + /// Bot framework http adapter instance. + /// + public class CCBotAdapter : CCBotAdapterBase + { + private readonly ICertificateProvider certificateProvider; + + /// + /// Initializes a new instance of the class. + /// + /// credential provider. + public CCBotAdapter( + ICertificateProvider certificateProvider, + BotFrameworkAuthentication botFrameworkAuthentication) + : base(botFrameworkAuthentication) + { + this.certificateProvider = certificateProvider; + } + + /// + public override async Task CreateConversationUsingCertificateAsync(string channelId, string serviceUrl, AppCredentials appCredentials, ConversationParameters conversationParameters, BotCallbackHandler callback, CancellationToken cancellationToken) + { + var cert = await this.certificateProvider.GetCertificateAsync(appCredentials.MicrosoftAppId); + var options = new CertificateAppCredentialsOptions() + { + AppId = appCredentials.MicrosoftAppId, + ClientCertificate = cert, + }; + + await this.CreateConversationAsync(appCredentials.MicrosoftAppId, channelId, serviceUrl, null/*audience*/, conversationParameters, callback, cancellationToken); + } + + /// + public override async Task CreateConversationUsingSecretAsync(string channelId, string serviceUrl, MicrosoftAppCredentials credentials, ConversationParameters conversationParameters, BotCallbackHandler callback, CancellationToken cancellationToken) + { + await this.CreateConversationAsync(credentials.MicrosoftAppId, channelId, serviceUrl, null/*audience*/, conversationParameters, callback, cancellationToken); + } + } +} diff --git a/Source/CompanyCommunicator.Common/Adapter/CCBotAdapterBase.cs b/Source/CompanyCommunicator.Common/Adapter/CCBotAdapterBase.cs new file mode 100644 index 000000000..42a32cbf2 --- /dev/null +++ b/Source/CompanyCommunicator.Common/Adapter/CCBotAdapterBase.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// + +namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Adapter +{ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Bot.Builder; + using Microsoft.Bot.Builder.Integration.AspNet.Core; + using Microsoft.Bot.Connector.Authentication; + using Microsoft.Bot.Schema; + using Microsoft.Extensions.Logging; + + /// + /// Bot Framework Http Adapter interface. + /// + public abstract class CCBotAdapterBase : CloudAdapter + { + /// + /// Initializes a new instance of the class. + /// + /// Bot Framework Authentication. + /// Logger + protected CCBotAdapterBase(BotFrameworkAuthentication botFrameworkAuthentication, ILogger logger = null) + : base(botFrameworkAuthentication, logger) + { + } + + /// + /// Creates a conversation using app secret on the specified channel. + /// + /// The ID for the channel. + /// The channel's service URL endpoint. + /// The application credentials for the bot. + /// The conversation information to use to create the conversation. + /// The method to call for the resulting bot turn. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + public abstract Task CreateConversationUsingSecretAsync(string channelId, string serviceUrl, MicrosoftAppCredentials credentials, ConversationParameters conversationParameters, BotCallbackHandler callback, CancellationToken cancellationToken); + + /// + /// Creates a conversation using app certificate on the specified channel. + /// This method can be used to use certificates for authentication. + /// + /// The ID for the channel. + /// The channel's service URL endpoint. + /// The application credentials for the bot. + /// The conversation information to use to create the conversation. + /// The method to call for the resulting bot turn. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + public abstract Task CreateConversationUsingCertificateAsync(string channelId, string serviceUrl, AppCredentials appCredentials, ConversationParameters conversationParameters, BotCallbackHandler callback, CancellationToken cancellationToken); + } +} diff --git a/Source/CompanyCommunicator.Common/Adapter/CCBotFrameworkHttpAdapter.cs b/Source/CompanyCommunicator.Common/Adapter/CCBotFrameworkHttpAdapter.cs deleted file mode 100644 index dbd5a955c..000000000 --- a/Source/CompanyCommunicator.Common/Adapter/CCBotFrameworkHttpAdapter.cs +++ /dev/null @@ -1,80 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. -// - -namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Adapter -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Bot.Builder; - using Microsoft.Bot.Builder.Integration.AspNet.Core; - using Microsoft.Bot.Connector.Authentication; - using Microsoft.Bot.Schema; - using Microsoft.Teams.Apps.CompanyCommunicator.Common.Secrets; - - /// - /// Bot framework http adapter instance. - /// - public class CCBotFrameworkHttpAdapter : BotFrameworkHttpAdapter, ICCBotFrameworkHttpAdapter - { - private readonly ICertificateProvider certificateProvider; - - /// - /// Initializes a new instance of the class. - /// - /// credential provider. - /// certificate provider. - public CCBotFrameworkHttpAdapter( - ICredentialProvider credentialProvider, - ICertificateProvider certificateProvider) - : base(credentialProvider) - { - this.certificateProvider = certificateProvider; - } - - /// - public async Task CreateConversationUsingCertificateAsync(string channelId, string serviceUrl, AppCredentials appCredentials, ConversationParameters conversationParameters, BotCallbackHandler callback, CancellationToken cancellationToken) - { - var cert = await this.certificateProvider.GetCertificateAsync(appCredentials.MicrosoftAppId); - var options = new CertificateAppCredentialsOptions() - { - AppId = appCredentials.MicrosoftAppId, - ClientCertificate = cert, - }; - - await this.CreateConversationAsync(channelId, serviceUrl, new CertificateAppCredentials(options) as AppCredentials, conversationParameters, callback, cancellationToken); - } - - /// - public async Task CreateConversationUsingSecretAsync(string channelId, string serviceUrl, MicrosoftAppCredentials credentials, ConversationParameters conversationParameters, BotCallbackHandler callback, CancellationToken cancellationToken) - { - await this.CreateConversationAsync(channelId, serviceUrl, credentials, conversationParameters, callback, cancellationToken); - } - - /// - protected override async Task BuildCredentialsAsync(string appId, string oAuthScope = null) - { - appId = appId ?? throw new ArgumentNullException(nameof(appId)); - - if (this.certificateProvider.IsCertificateAuthenticationEnabled()) - { - var cert = await this.certificateProvider.GetCertificateAsync(appId); - var options = new CertificateAppCredentialsOptions() - { - AppId = appId, - ClientCertificate = cert, - OauthScope = oAuthScope, - }; - - var certificateAppCredentials = new CertificateAppCredentials(options) as AppCredentials; - return certificateAppCredentials; - } - else - { - return await base.BuildCredentialsAsync(appId, oAuthScope); - } - } - } -} diff --git a/Source/CompanyCommunicator.Common/Configuration/CommericalConfiguration.cs b/Source/CompanyCommunicator.Common/Configuration/CommericalConfiguration.cs new file mode 100644 index 000000000..4310e1f1b --- /dev/null +++ b/Source/CompanyCommunicator.Common/Configuration/CommericalConfiguration.cs @@ -0,0 +1,40 @@ +namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration +{ + /// + /// App configuration for commercial environment. + /// + public class CommericalConfiguration : IAppConfiguration + { + private readonly string tenantId; + + /// + /// Initializes a new instance of the class. + /// + /// Tenant id. + public CommericalConfiguration(string tenantId) + { + this.tenantId = tenantId ?? throw new System.ArgumentNullException(nameof(tenantId)); + } + + /// + public string AzureAd_Instance => "https://login.microsoftonline.com"; + + /// + public string AzureAd_ValidIssuers => "https://login.microsoftonline.com/TENANT_ID/v2.0,https://sts.windows.net/TENANT_ID/"; + + /// + public string AuthorityUri => $"https://login.microsoftonline.com/{this.tenantId}"; + + /// + public string GraphBaseUrl => "https://graph.microsoft.com/v1.0"; + + /// + public string GraphDefaultScope => "https://graph.microsoft.com/.default"; + + /// + public string GraphUserReadScope => "https://graph.microsoft.com/User.Read openid profile"; + + /// + public string TeamsLicenseId => "57ff2da0-773e-42df-b2af-ffb7a2317929"; + } +} diff --git a/Source/CompanyCommunicator.Common/Configuration/ConfigurationFactory.cs b/Source/CompanyCommunicator.Common/Configuration/ConfigurationFactory.cs new file mode 100644 index 000000000..28a6c1e9e --- /dev/null +++ b/Source/CompanyCommunicator.Common/Configuration/ConfigurationFactory.cs @@ -0,0 +1,38 @@ +namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration +{ + /// + /// Configuration factory returns relevant configuration for a given environment. + /// + public class ConfigurationFactory + { + private readonly string tenantId; + + /// + /// Initializes a new instance of the class. + /// + /// Tenant id. + public ConfigurationFactory(string tenantId) + { + this.tenantId = tenantId ?? throw new System.ArgumentNullException(nameof(tenantId)); + } + + /// + /// Configuration factory returns relevant configuration for a given environment. + /// + /// Teams environment. + /// App configurstion. + public IAppConfiguration GetAppConfiguration(TeamsEnvironment env) + { + switch (env) + { + case TeamsEnvironment.Commercial: + case TeamsEnvironment.GCC: + return new CommericalConfiguration(this.tenantId); + case TeamsEnvironment.GCCH: + return new GCCHConfiguration(this.tenantId); + default: + return new CommericalConfiguration(this.tenantId); + } + } + } +} diff --git a/Source/CompanyCommunicator.Common/Configuration/GCCHConfiguration.cs b/Source/CompanyCommunicator.Common/Configuration/GCCHConfiguration.cs new file mode 100644 index 000000000..30c28c9dd --- /dev/null +++ b/Source/CompanyCommunicator.Common/Configuration/GCCHConfiguration.cs @@ -0,0 +1,40 @@ +namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration +{ + /// + /// App configuration for GCCH environment. + /// + internal class GCCHConfiguration : IAppConfiguration + { + private readonly string tenantId; + + /// + /// Initializes a new instance of the class. + /// + /// TenantId. + public GCCHConfiguration(string tenantId) + { + this.tenantId = tenantId ?? throw new System.ArgumentNullException(nameof(tenantId)); + } + + /// + public string AzureAd_Instance => "https://login.microsoftonline.us/"; + + /// + public string AzureAd_ValidIssuers => $"https://login.microsoftonline.us/{this.tenantId}/v2.0,https://sts.windows.net/{this.tenantId}/"; + + /// + public string AuthorityUri => $"https://login.microsoftonline.us/{this.tenantId}"; + + /// + public string GraphBaseUrl => "https://graph.microsoft.us/v1.0"; + + /// + public string GraphDefaultScope => "https://graph.microsoft.us/.default"; + + /// + public string GraphUserReadScope => "https://graph.microsoft.us/User.Read openid profile"; + + /// + public string TeamsLicenseId => "9953b155-8aef-4c56-92f3-72b0487fce41"; + } +} diff --git a/Source/CompanyCommunicator.Common/Configuration/IAppConfiguration.cs b/Source/CompanyCommunicator.Common/Configuration/IAppConfiguration.cs new file mode 100644 index 000000000..53320ac34 --- /dev/null +++ b/Source/CompanyCommunicator.Common/Configuration/IAppConfiguration.cs @@ -0,0 +1,27 @@ +// +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// + +namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration +{ + /// + /// App configuration interface. + /// + public interface IAppConfiguration + { + public string AzureAd_Instance { get; } + + public string AzureAd_ValidIssuers { get; } + + public string AuthorityUri { get; } + + public string GraphBaseUrl { get; } + + public string GraphDefaultScope { get; } + + public string GraphUserReadScope { get; } + + public string TeamsLicenseId { get; } + } +} diff --git a/Source/CompanyCommunicator.Common/Configuration/TeamsEnvironment.cs b/Source/CompanyCommunicator.Common/Configuration/TeamsEnvironment.cs new file mode 100644 index 000000000..cd76b208a --- /dev/null +++ b/Source/CompanyCommunicator.Common/Configuration/TeamsEnvironment.cs @@ -0,0 +1,23 @@ +namespace Microsoft.Teams.Apps.CompanyCommunicator.Common +{ + /// + /// Teams environment. + /// + public enum TeamsEnvironment + { + /// + /// Commmercial environment. + /// + Commercial, + + /// + /// GCC - https://learn.microsoft.com/en-us/office365/servicedescriptions/office-365-platform-service-description/office-365-us-government/gcc + /// + GCC, + + /// + /// GCCH - https://learn.microsoft.com/en-us/office365/servicedescriptions/office-365-platform-service-description/office-365-us-government/gcc-high-and-dod + /// + GCCH + } +} diff --git a/Source/CompanyCommunicator.Common/Constants.cs b/Source/CompanyCommunicator.Common/Constants.cs index 4076d1a71..a0acde3ce 100644 --- a/Source/CompanyCommunicator.Common/Constants.cs +++ b/Source/CompanyCommunicator.Common/Constants.cs @@ -60,11 +60,6 @@ public static class Constants /// public const string PermissionTypeKey = "x-api-permission"; - /// - /// get the default graph scope. - /// - public const string ScopeDefault = "https://graph.microsoft.com/.default"; - /// /// get the OData next page link. /// diff --git a/Source/CompanyCommunicator.Common/Extensions/ServiceCollectionExtension.cs b/Source/CompanyCommunicator.Common/Extensions/ServiceCollectionExtension.cs index 7124bdecb..a5dd28ac6 100644 --- a/Source/CompanyCommunicator.Common/Extensions/ServiceCollectionExtension.cs +++ b/Source/CompanyCommunicator.Common/Extensions/ServiceCollectionExtension.cs @@ -14,6 +14,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Extensions using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Identity.Client; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Secrets; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.CommonBot; @@ -41,7 +42,7 @@ public static void AddBlobClient(this IServiceCollection services, bool useManag { // Add using managed identities. services.AddSingleton(sp => new BlobContainerClient( - GetBlobContainerUri(sp.GetService().GetValue("StorageAccountName")), + GetBlobContainerUri(sp.GetService().GetValue("TeamsEnvironment", "Commerical"/*default*/), sp.GetService().GetValue("StorageAccountName")), new DefaultAzureCredential(), options)); } @@ -90,11 +91,12 @@ public static void AddConfidentialClient(this IServiceCollection services, bool { var options = provider.GetRequiredService>(); var certificateProvider = provider.GetRequiredService(); + var appConfiguration = provider.GetRequiredService(); var cert = certificateProvider.GetCertificateAsync(options.Value.ClientId).Result; return ConfidentialClientApplicationBuilder .Create(options.Value.ClientId) .WithCertificate(cert) - .WithAuthority(new Uri($"https://login.microsoftonline.com/{options.Value.TenantId}")) + .WithAuthority(new Uri(appConfiguration.AuthorityUri)) .Build(); }); } @@ -103,21 +105,77 @@ public static void AddConfidentialClient(this IServiceCollection services, bool services.AddSingleton(provider => { var options = provider.GetRequiredService>(); + var appConfiguration = provider.GetRequiredService(); return ConfidentialClientApplicationBuilder .Create(options.Value.ClientId) .WithClientSecret(options.Value.ClientSecret) - .WithAuthority(new Uri($"https://login.microsoftonline.com/{options.Value.TenantId}")) + .WithAuthority(new Uri(appConfiguration.AuthorityUri)) .Build(); }); } } - private static Uri GetBlobContainerUri(string storageAccountName) + /// + /// Adds relevant App configurations for Teams environment. + /// + /// Serivce collection. + /// Configuration. + public static void AddAppConfiguration(this IServiceCollection services, IConfiguration configuration) + { + var tenantId = configuration.GetValue("TenantId") ?? configuration.GetValue("AzureAd:TenantId"); + var env = configuration.GetTeamsEnvironment(); + services.AddSingleton(new ConfigurationFactory(tenantId).GetAppConfiguration(env)); + } + + public static AzureCloudInstance GetAzureCloudInstance(this IConfiguration configuration) + { + var teamsEnv = configuration.GetTeamsEnvironment(); + switch (teamsEnv) + { + case TeamsEnvironment.Commercial: + case TeamsEnvironment.GCC: + return AzureCloudInstance.AzurePublic; + case TeamsEnvironment.GCCH: + return AzureCloudInstance.AzureUsGovernment; + default: + return AzureCloudInstance.AzurePublic; + } + } + + /// + /// Reads Teams environment from the configuration. + /// + /// Configuration. + /// Teams environemnt. + public static TeamsEnvironment GetTeamsEnvironment(this IConfiguration configuration) + { + var envString = configuration.GetValue("TeamsEnvironment", "Commerical"/*default*/); + Enum.TryParse(envString, out TeamsEnvironment teamsEnvironment); + return teamsEnvironment; + } + + private static Uri GetBlobContainerUri(string envString, string storageAccountName) { - return new Uri(string.Format( + Enum.TryParse(envString, out TeamsEnvironment teamsEnvironment); + switch (teamsEnvironment) + { + case TeamsEnvironment.Commercial: + case TeamsEnvironment.GCC: + return new Uri(string.Format( "https://{0}.blob.core.windows.net/{1}", storageAccountName, Common.Constants.BlobContainerName)); + case TeamsEnvironment.GCCH: + return new Uri(string.Format( + "https://{0}.blob.core.usgovcloudapi.net/{1}", + storageAccountName, + Common.Constants.BlobContainerName)); + default: + return new Uri(string.Format( + "https://{0}.blob.core.windows.net/{1}", + storageAccountName, + Common.Constants.BlobContainerName)); + } } } } diff --git a/Source/CompanyCommunicator.Common/Microsoft.Teams.Apps.CompanyCommunicator.Common.csproj b/Source/CompanyCommunicator.Common/Microsoft.Teams.Apps.CompanyCommunicator.Common.csproj index 07d1ba09b..1d5a383fb 100644 --- a/Source/CompanyCommunicator.Common/Microsoft.Teams.Apps.CompanyCommunicator.Common.csproj +++ b/Source/CompanyCommunicator.Common/Microsoft.Teams.Apps.CompanyCommunicator.Common.csproj @@ -25,10 +25,10 @@ - + - + diff --git a/Source/CompanyCommunicator.Common/Services/CommonBot/ConfigurationCredentialProvider.cs b/Source/CompanyCommunicator.Common/Services/CommonBot/ConfigurationCredentialProvider.cs index 92b7d3e1e..77010b78d 100644 --- a/Source/CompanyCommunicator.Common/Services/CommonBot/ConfigurationCredentialProvider.cs +++ b/Source/CompanyCommunicator.Common/Services/CommonBot/ConfigurationCredentialProvider.cs @@ -8,35 +8,50 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.CommonBot using System; using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.Connector.Authentication; using Microsoft.Extensions.Options; + using Microsoft.Rest; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Secrets; /// /// This class implements ICredentialProvider, which is used by the bot framework to retrieve credential info. /// - public class ConfigurationCredentialProvider : ICredentialProvider + public class ConfigurationCredentialProvider : ServiceClientCredentialsFactory { - private readonly Dictionary credentials; + private readonly Dictionary credentials; + private readonly ICertificateProvider certificateProvider; /// /// Initializes a new instance of the class. /// A constructor that accepts a map of bot id list and credentials. /// /// bot options. - public ConfigurationCredentialProvider(IOptions botOptions) + /// Cert provider. + public ConfigurationCredentialProvider( + IOptions botOptions, + ICertificateProvider certificateProvider) { botOptions = botOptions ?? throw new ArgumentNullException(nameof(botOptions)); - this.credentials = new Dictionary(); + this.credentials = new Dictionary(); if (!string.IsNullOrEmpty(botOptions.Value.UserAppId)) { - this.credentials.Add(botOptions.Value.UserAppId, botOptions.Value.UserAppPassword); + var appId = botOptions.Value.UserAppId; + var password = botOptions.Value.UserAppPassword; + var credFactory = new PasswordServiceClientCredentialFactory(appId, password, string.Empty, null, null); + this.credentials.Add(appId, credFactory); } if (!string.IsNullOrEmpty(botOptions.Value.AuthorAppId)) { - this.credentials.Add(botOptions.Value.AuthorAppId, botOptions.Value.AuthorAppPassword); + var appId = botOptions.Value.AuthorAppId; + var password = botOptions.Value.AuthorAppPassword; + var credFactory = new PasswordServiceClientCredentialFactory(appId, password, string.Empty, null, null); + this.credentials.Add(appId, credFactory); } + + this.certificateProvider = certificateProvider ?? throw new ArgumentNullException(nameof(certificateProvider)); } /// @@ -46,34 +61,53 @@ public ConfigurationCredentialProvider(IOptions botOptions) /// A task that represents the work queued to execute. /// If the task is successful, the result is true if /// is valid for the controller; otherwise, false. - public Task IsValidAppIdAsync(string appId) + public override Task IsValidAppIdAsync(string appId, CancellationToken cancellationToken) { return Task.FromResult(this.credentials.ContainsKey(appId)); } /// - /// Gets the app password for a given bot app ID. + /// Checks whether bot authentication is disabled. /// - /// The ID of the app to get the password for. /// A task that represents the work queued to execute. - /// If the task is successful and the app ID is valid, the result - /// contains the password; otherwise, null. + /// If the task is successful and bot authentication is disabled, the result + /// is true; otherwise, false. /// - public Task GetAppPasswordAsync(string appId) + public override Task IsAuthenticationDisabledAsync(CancellationToken cancellationToken) { - return Task.FromResult(this.credentials.ContainsKey(appId) ? this.credentials[appId] : null); + return Task.FromResult(!this.credentials.Any()); } /// /// Checks whether bot authentication is disabled. /// /// A task that represents the work queued to execute. - /// If the task is successful and bot authentication is disabled, the result - /// is true; otherwise, false. + /// If the task is successful and bot authentication is disabled, the result is true; + /// otherwise, false.This method is async to enable custom implementations that need to call out + /// to serviced to validate the appId / password pair. /// - public Task IsAuthenticationDisabledAsync() + public async override Task CreateCredentialsAsync(string appId, string audience, string loginEndpoint, bool validateAuthority, CancellationToken cancellationToken) { - return Task.FromResult(!this.credentials.Any()); + appId = appId ?? throw new ArgumentNullException(nameof(appId)); + + if (this.certificateProvider.IsCertificateAuthenticationEnabled()) + { + var cert = await this.certificateProvider.GetCertificateAsync(appId); + var options = new CertificateAppCredentialsOptions() + { + AppId = appId, + ClientCertificate = cert, + OauthScope = null, + }; + + var certificateAppCredentials = new CertificateAppCredentials(options) as AppCredentials; + return certificateAppCredentials; + } + else + { + this.credentials.TryGetValue(appId, out ServiceClientCredentialsFactory factory); + return await factory.CreateCredentialsAsync(appId, audience, loginEndpoint, validateAuthority, cancellationToken); + } } } } diff --git a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Authentication/MsalAuthenticationProvider.cs b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Authentication/MsalAuthenticationProvider.cs index 9d236b88a..6da389618 100644 --- a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Authentication/MsalAuthenticationProvider.cs +++ b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Authentication/MsalAuthenticationProvider.cs @@ -12,6 +12,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGrap using System.Threading.Tasks; using Microsoft.Graph; using Microsoft.Identity.Client; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; /// /// MSAL Authentication provider for graph calls. @@ -19,14 +20,19 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGrap public class MsalAuthenticationProvider : IAuthenticationProvider { private readonly IConfidentialClientApplication clientApplication; + private readonly IAppConfiguration appConfiguration; /// /// Initializes a new instance of the class. /// /// MSAL.NET token acquisition service for confidential clients. - public MsalAuthenticationProvider(IConfidentialClientApplication clientApplication) + /// App configuration. + public MsalAuthenticationProvider( + IConfidentialClientApplication clientApplication, + IAppConfiguration appConfiguration) { this.clientApplication = clientApplication ?? throw new ArgumentNullException(nameof(clientApplication)); + this.appConfiguration = appConfiguration ?? throw new ArgumentNullException(nameof(appConfiguration)); } /// @@ -45,7 +51,7 @@ public async Task AuthenticateRequestAsync(HttpRequestMessage request) /// The access token. private async Task GetAccesTokenAsync() { - var scopes = new List { Common.Constants.ScopeDefault, }; + var scopes = new List { this.appConfiguration.GraphDefaultScope, }; var result = await this.clientApplication.AcquireTokenForClient(scopes) .ExecuteAsync(); diff --git a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Factory/GraphServiceFactory.cs b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Factory/GraphServiceFactory.cs index 61388069f..6d23ca423 100644 --- a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Factory/GraphServiceFactory.cs +++ b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Factory/GraphServiceFactory.cs @@ -7,6 +7,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGrap { using System; using Microsoft.Graph; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; /// /// Graph Service Factory. @@ -14,21 +15,25 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGrap public class GraphServiceFactory : IGraphServiceFactory { private readonly IGraphServiceClient serviceClient; + private readonly IAppConfiguration appConfiguration; /// /// Initializes a new instance of the class. /// /// V1 Graph service client. + /// App configuration. public GraphServiceFactory( - IGraphServiceClient serviceClient) + IGraphServiceClient serviceClient, + IAppConfiguration appConfiguration) { this.serviceClient = serviceClient ?? throw new ArgumentNullException(nameof(serviceClient)); + this.appConfiguration = appConfiguration ?? throw new ArgumentNullException(nameof(appConfiguration)); } /// public IUsersService GetUsersService() { - return new UsersService(this.serviceClient); + return new UsersService(this.serviceClient, this.appConfiguration); } /// @@ -52,7 +57,7 @@ public IChatsService GetChatsService() /// public IAppManagerService GetAppManagerService() { - return new AppManagerService(this.serviceClient); + return new AppManagerService(this.serviceClient, this.appConfiguration); } /// diff --git a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/GraphConstants.cs b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/GraphConstants.cs index 1d0029752..8e91d0088 100644 --- a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/GraphConstants.cs +++ b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/GraphConstants.cs @@ -10,16 +10,6 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGrap /// public class GraphConstants { - /// - /// Microsoft Graph version 1.0 base Url. - /// - public const string V1BaseUrl = "https://graph.microsoft.com/v1.0"; - - /// - /// Microsoft Graph Beta base url. - /// - public const string BetaBaseUrl = "https://graph.microsoft.com/beta"; - /// /// Max page size. /// diff --git a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/TeamWork/AppManagerService.cs b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/TeamWork/AppManagerService.cs index 854efd2d0..d69d4af4f 100644 --- a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/TeamWork/AppManagerService.cs +++ b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/TeamWork/AppManagerService.cs @@ -10,6 +10,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGrap using System.Linq; using System.Threading.Tasks; using Microsoft.Graph; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Policies; /// @@ -18,15 +19,19 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGrap internal class AppManagerService : IAppManagerService { private readonly IGraphServiceClient graphServiceClient; + private readonly IAppConfiguration appConfiguration; /// /// Initializes a new instance of the class. /// /// V1 Graph service client. + /// App configuration. internal AppManagerService( - IGraphServiceClient graphServiceClient) + IGraphServiceClient graphServiceClient, + IAppConfiguration appConfiguration) { this.graphServiceClient = graphServiceClient ?? throw new ArgumentNullException(nameof(graphServiceClient)); + this.appConfiguration = appConfiguration ?? throw new ArgumentNullException(nameof(appConfiguration)); } /// @@ -46,7 +51,7 @@ public async Task InstallAppForUserAsync(string appId, string userId) { AdditionalData = new Dictionary() { - { "teamsApp@odata.bind", $"{GraphConstants.V1BaseUrl}/appCatalogs/teamsApps/{appId}" }, + { "teamsApp@odata.bind", $"{this.appConfiguration.GraphBaseUrl}/appCatalogs/teamsApps/{appId}" }, }, }; @@ -77,7 +82,7 @@ public async Task InstallAppForTeamAsync(string appId, string teamId) { AdditionalData = new Dictionary() { - { "teamsApp@odata.bind", $"{GraphConstants.V1BaseUrl}/appCatalogs/teamsApps/{appId}" }, + { "teamsApp@odata.bind", $"{this.appConfiguration.GraphBaseUrl}/appCatalogs/teamsApps/{appId}" }, }, }; diff --git a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Users/UsersService.cs b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Users/UsersService.cs index 95cd325b1..3ddf77813 100644 --- a/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Users/UsersService.cs +++ b/Source/CompanyCommunicator.Common/Services/MicrosoftGraph/Users/UsersService.cs @@ -12,6 +12,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGrap using System.Text; using System.Threading.Tasks; using Microsoft.Graph; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; using Newtonsoft.Json.Linq; /// @@ -19,17 +20,20 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGrap /// internal class UsersService : IUsersService { - private const string TeamsLicenseId = "57ff2da0-773e-42df-b2af-ffb7a2317929"; - private readonly IGraphServiceClient graphServiceClient; + private readonly IAppConfiguration appConfiguration; /// /// Initializes a new instance of the class. /// /// graph service client. - internal UsersService(IGraphServiceClient graphServiceClient) + /// App configuration. + internal UsersService( + IGraphServiceClient graphServiceClient, + IAppConfiguration appConfiguration) { this.graphServiceClient = graphServiceClient ?? throw new ArgumentNullException(nameof(graphServiceClient)); + this.appConfiguration = appConfiguration ?? throw new ArgumentNullException(nameof(appConfiguration)); } /// @@ -256,7 +260,7 @@ private IEnumerable GetBatchRequest(IEnumerable string.Equals(sp.ServicePlanId?.ToString(), TeamsLicenseId))) + if (license.ServicePlans.Any(sp => string.Equals(sp.ServicePlanId?.ToString(), this.appConfiguration.TeamsLicenseId))) { return true; } diff --git a/Source/CompanyCommunicator.Common/Services/Teams/Conversations/ConversationService.cs b/Source/CompanyCommunicator.Common/Services/Teams/Conversations/ConversationService.cs index a213b75ea..931df8fd4 100644 --- a/Source/CompanyCommunicator.Common/Services/Teams/Conversations/ConversationService.cs +++ b/Source/CompanyCommunicator.Common/Services/Teams/Conversations/ConversationService.cs @@ -25,7 +25,7 @@ public class ConversationService : IConversationService { private static readonly string MicrosoftTeamsChannelId = "msteams"; - private readonly ICCBotFrameworkHttpAdapter botAdapter; + private readonly CCBotAdapterBase botAdapter; private readonly UserAppCredentials userAppCredentials; private readonly AuthorAppCredentials authorAppCredentials; @@ -36,7 +36,7 @@ public class ConversationService : IConversationService /// The user Microsoft app credentials. /// The author Microsoft app credentials. public ConversationService( - ICCBotFrameworkHttpAdapter botAdapter, + CCBotAdapterBase botAdapter, UserAppCredentials userAppCredentials, AuthorAppCredentials authorAppCredentials) { diff --git a/Source/CompanyCommunicator.Common/Services/Teams/Messages/MessageService.cs b/Source/CompanyCommunicator.Common/Services/Teams/Messages/MessageService.cs index 14bda0836..3b4d342ae 100644 --- a/Source/CompanyCommunicator.Common/Services/Teams/Messages/MessageService.cs +++ b/Source/CompanyCommunicator.Common/Services/Teams/Messages/MessageService.cs @@ -24,7 +24,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.Teams public class MessageService : IMessageService { private readonly string microsoftAppId; - private readonly ICCBotFrameworkHttpAdapter botAdapter; + private readonly CCBotAdapterBase botAdapter; /// /// Initializes a new instance of the class. @@ -33,7 +33,7 @@ public class MessageService : IMessageService /// The bot adapter. public MessageService( IOptions botOptions, - ICCBotFrameworkHttpAdapter botAdapter) + CCBotAdapterBase botAdapter) { this.microsoftAppId = botOptions?.Value?.UserAppId ?? throw new ArgumentNullException(nameof(botOptions)); this.botAdapter = botAdapter ?? throw new ArgumentNullException(nameof(botAdapter)); @@ -83,7 +83,7 @@ public async Task SendMessageAsync( }; await this.botAdapter.ContinueConversationAsync( - botId: this.microsoftAppId, + botAppId: this.microsoftAppId, reference: conversationReference, callback: async (turnContext, cancellationToken) => { diff --git a/Source/CompanyCommunicator.Common/Services/Teams/TeamMembers/TeamMembersService.cs b/Source/CompanyCommunicator.Common/Services/Teams/TeamMembers/TeamMembersService.cs index 186897b35..206bc0079 100644 --- a/Source/CompanyCommunicator.Common/Services/Teams/TeamMembers/TeamMembersService.cs +++ b/Source/CompanyCommunicator.Common/Services/Teams/TeamMembers/TeamMembersService.cs @@ -25,7 +25,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.Teams /// public class TeamMembersService : ITeamMembersService { - private readonly ICCBotFrameworkHttpAdapter botAdapter; + private readonly CCBotAdapterBase botAdapter; private readonly string userAppId; private readonly string authorAppId; @@ -35,7 +35,7 @@ public class TeamMembersService : ITeamMembersService /// Bot adapter. /// Bot options. public TeamMembersService( - ICCBotFrameworkHttpAdapter botAdapter, + CCBotAdapterBase botAdapter, IOptions botOptions) { this.botAdapter = botAdapter ?? throw new ArgumentNullException(nameof(botAdapter)); diff --git a/Source/CompanyCommunicator.Data.Func/Services/FileCardServices/FileCardService.cs b/Source/CompanyCommunicator.Data.Func/Services/FileCardServices/FileCardService.cs index 0de1a6166..ae0cf8ab5 100644 --- a/Source/CompanyCommunicator.Data.Func/Services/FileCardServices/FileCardService.cs +++ b/Source/CompanyCommunicator.Data.Func/Services/FileCardServices/FileCardService.cs @@ -25,7 +25,7 @@ public class FileCardService : IFileCardService { private readonly IUserDataRepository userDataRepository; private readonly string authorAppId; - private readonly ICCBotFrameworkHttpAdapter botAdapter; + private readonly CCBotAdapterBase botAdapter; private readonly IStringLocalizer localizer; /// @@ -37,7 +37,7 @@ public class FileCardService : IFileCardService /// Localization service. public FileCardService( IOptions botOptions, - ICCBotFrameworkHttpAdapter botAdapter, + CCBotAdapterBase botAdapter, IUserDataRepository userDataRepository, IStringLocalizer localizer) { @@ -79,7 +79,7 @@ public async Task DeleteAsync(string userId, string fileConsentId) int maxNumberOfAttempts = 10; await this.botAdapter.ContinueConversationAsync( - botId: this.authorAppId, + botAppId: this.authorAppId, reference: conversationReference, callback: async (turnContext, cancellationToken) => { diff --git a/Source/CompanyCommunicator.Data.Func/Startup.cs b/Source/CompanyCommunicator.Data.Func/Startup.cs index 55271ed20..f0dcbcbd1 100644 --- a/Source/CompanyCommunicator.Data.Func/Startup.cs +++ b/Source/CompanyCommunicator.Data.Func/Startup.cs @@ -94,9 +94,9 @@ public override void Configure(IFunctionsHostBuilder builder) // Add bot services. builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Add Secrets. var keyVaultUrl = Environment.GetEnvironmentVariable("KeyVault:Url"); diff --git a/Source/CompanyCommunicator.Prep.Func/Export/Activities/HandleExportFailureActivity.cs b/Source/CompanyCommunicator.Prep.Func/Export/Activities/HandleExportFailureActivity.cs index 9337df16b..f68f2af18 100644 --- a/Source/CompanyCommunicator.Prep.Func/Export/Activities/HandleExportFailureActivity.cs +++ b/Source/CompanyCommunicator.Prep.Func/Export/Activities/HandleExportFailureActivity.cs @@ -34,7 +34,7 @@ public class HandleExportFailureActivity private readonly IStorageClientFactory storageClientFactory; private readonly IUserDataRepository userDataRepository; private readonly string authorAppId; - private readonly ICCBotFrameworkHttpAdapter botAdapter; + private readonly CCBotAdapterBase botAdapter; private readonly IStringLocalizer localizer; /// @@ -50,7 +50,7 @@ public HandleExportFailureActivity( IExportDataRepository exportDataRepository, IStorageClientFactory storageClientFactory, IOptions botOptions, - ICCBotFrameworkHttpAdapter botAdapter, + CCBotAdapterBase botAdapter, IUserDataRepository userDataRepository, IStringLocalizer localizer) { @@ -114,7 +114,7 @@ private async Task SendFailureMessageAsync(string userId) int maxNumberOfAttempts = 10; await this.botAdapter.ContinueConversationAsync( - botId: this.authorAppId, + botAppId: this.authorAppId, reference: conversationReference, callback: async (turnContext, cancellationToken) => { diff --git a/Source/CompanyCommunicator.Prep.Func/Export/Activities/SendFileCardActivity.cs b/Source/CompanyCommunicator.Prep.Func/Export/Activities/SendFileCardActivity.cs index b402b1eda..c59856a07 100644 --- a/Source/CompanyCommunicator.Prep.Func/Export/Activities/SendFileCardActivity.cs +++ b/Source/CompanyCommunicator.Prep.Func/Export/Activities/SendFileCardActivity.cs @@ -32,7 +32,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.Export.Activities public class SendFileCardActivity { private readonly string authorAppId; - private readonly ICCBotFrameworkHttpAdapter botAdapter; + private readonly CCBotAdapterBase botAdapter; private readonly IUserDataRepository userDataRepository; private readonly IConversationService conversationService; private readonly TeamsConversationOptions options; @@ -51,7 +51,7 @@ public class SendFileCardActivity /// Localization service. public SendFileCardActivity( IOptions botOptions, - ICCBotFrameworkHttpAdapter botAdapter, + CCBotAdapterBase botAdapter, IUserDataRepository userDataRepository, IConversationService conversationService, IOptions options, @@ -105,7 +105,7 @@ public async Task SendFileCardActivityAsync( int maxNumberOfAttempts = 10; string consentId = string.Empty; await this.botAdapter.ContinueConversationAsync( - botId: this.authorAppId, + botAppId: this.authorAppId, reference: conversationReference, callback: async (turnContext, cancellationToken) => { diff --git a/Source/CompanyCommunicator.Prep.Func/Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.csproj b/Source/CompanyCommunicator.Prep.Func/Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.csproj index e0fe1906f..46116aec7 100644 --- a/Source/CompanyCommunicator.Prep.Func/Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.csproj +++ b/Source/CompanyCommunicator.Prep.Func/Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.csproj @@ -21,7 +21,7 @@ - + diff --git a/Source/CompanyCommunicator.Prep.Func/PreparingToSend/Activities/StoreMessageActivity.cs b/Source/CompanyCommunicator.Prep.Func/PreparingToSend/Activities/StoreMessageActivity.cs index a125e6a89..0190ab4a3 100644 --- a/Source/CompanyCommunicator.Prep.Func/PreparingToSend/Activities/StoreMessageActivity.cs +++ b/Source/CompanyCommunicator.Prep.Func/PreparingToSend/Activities/StoreMessageActivity.cs @@ -39,7 +39,6 @@ public StoreMessageActivity( this.sendingNotificationDataRepository = notificationRepo ?? throw new ArgumentNullException(nameof(notificationRepo)); this.adaptiveCardCreator = cardCreator ?? throw new ArgumentNullException(nameof(cardCreator)); this.memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); - } /// @@ -75,7 +74,6 @@ public async Task RunAsync( } notification.ImageLink += imageLink; - } var serializedContent = this.adaptiveCardCreator.CreateAdaptiveCard(notification).ToJson(); diff --git a/Source/CompanyCommunicator.Prep.Func/Startup.cs b/Source/CompanyCommunicator.Prep.Func/Startup.cs index dc27824b5..6d5cf5c38 100644 --- a/Source/CompanyCommunicator.Prep.Func/Startup.cs +++ b/Source/CompanyCommunicator.Prep.Func/Startup.cs @@ -17,8 +17,10 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func using Microsoft.Extensions.DependencyInjection; using Microsoft.Graph; using Microsoft.Identity.Client; + using Microsoft.Teams.Apps.CompanyCommunicator.Common; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Adapter; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Clients; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Extensions; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Repositories; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Repositories.ExportData; @@ -62,6 +64,9 @@ public override void Configure(IFunctionsHostBuilder builder) repositoryOptions.EnsureTableExists = !configuration.GetValue("IsItExpectedThatTableAlreadyExists", false); }); + + builder.Services.AddAppConfiguration(builder.GetContext().Configuration); + builder.Services.AddOptions() .Configure((botOptions, configuration) => { @@ -115,9 +120,9 @@ public override void Configure(IFunctionsHostBuilder builder) // Add bot services. builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Add repositories. builder.Services.AddSingleton(); @@ -167,6 +172,7 @@ private void AddGraphServices(IFunctionsHostBuilder builder) builder.Services.AddOptions(). Configure((confidentialClientApplicationOptions, configuration) => { + confidentialClientApplicationOptions.AzureCloudInstance = configuration.GetAzureCloudInstance(); confidentialClientApplicationOptions.ClientId = configuration.GetValue("GraphAppId"); confidentialClientApplicationOptions.ClientSecret = configuration.GetValue("GraphAppPassword", string.Empty); confidentialClientApplicationOptions.TenantId = configuration.GetValue("TenantId"); @@ -182,7 +188,9 @@ private void AddGraphServices(IFunctionsHostBuilder builder) // Add Graph Clients. builder.Services.AddSingleton( serviceProvider => - new GraphServiceClient(serviceProvider.GetRequiredService())); + new GraphServiceClient( + serviceProvider.GetRequiredService().GraphBaseUrl, + serviceProvider.GetRequiredService())); // Add Service Factory builder.Services.AddSingleton(); diff --git a/Source/CompanyCommunicator.Send.Func/Microsoft.Teams.Apps.CompanyCommunicator.Send.Func.csproj b/Source/CompanyCommunicator.Send.Func/Microsoft.Teams.Apps.CompanyCommunicator.Send.Func.csproj index c80457578..01e7e0a00 100644 --- a/Source/CompanyCommunicator.Send.Func/Microsoft.Teams.Apps.CompanyCommunicator.Send.Func.csproj +++ b/Source/CompanyCommunicator.Send.Func/Microsoft.Teams.Apps.CompanyCommunicator.Send.Func.csproj @@ -14,7 +14,7 @@ - + diff --git a/Source/CompanyCommunicator.Send.Func/SendFunction.cs b/Source/CompanyCommunicator.Send.Func/SendFunction.cs index 46d19163d..22f28d271 100644 --- a/Source/CompanyCommunicator.Send.Func/SendFunction.cs +++ b/Source/CompanyCommunicator.Send.Func/SendFunction.cs @@ -81,7 +81,6 @@ public SendFunction( this.sendQueue = sendQueue ?? throw new ArgumentNullException(nameof(sendQueue)); this.localizer = localizer ?? throw new ArgumentNullException(nameof(localizer)); this.memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); - } /// diff --git a/Source/CompanyCommunicator.Send.Func/Startup.cs b/Source/CompanyCommunicator.Send.Func/Startup.cs index 05686d872..f41006ace 100644 --- a/Source/CompanyCommunicator.Send.Func/Startup.cs +++ b/Source/CompanyCommunicator.Send.Func/Startup.cs @@ -84,9 +84,9 @@ public override void Configure(IFunctionsHostBuilder builder) // Add bot services. builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Add teams services. builder.Services.AddTransient(); diff --git a/Source/CompanyCommunicator/Authentication/AuthenticationServiceCollectionExtensions.cs b/Source/CompanyCommunicator/Authentication/AuthenticationServiceCollectionExtensions.cs index 2032ed1a8..1af8b109f 100644 --- a/Source/CompanyCommunicator/Authentication/AuthenticationServiceCollectionExtensions.cs +++ b/Source/CompanyCommunicator/Authentication/AuthenticationServiceCollectionExtensions.cs @@ -13,8 +13,11 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Authentication using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Identity.Client; using Microsoft.Identity.Web; using Microsoft.IdentityModel.Tokens; + using Microsoft.Teams.Apps.CompanyCommunicator.Common; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Extensions; /// /// Extension class for registering auth services in DI container. @@ -89,6 +92,7 @@ private static void RegisterAuthenticationServicesWithCertificate( confidentialClientApplicationOptions => { configuration.Bind("AzureAd", confidentialClientApplicationOptions); + confidentialClientApplicationOptions.AzureCloudInstance = configuration.GetAzureCloudInstance(); }) .AddInMemoryTokenCaches(); } diff --git a/Source/CompanyCommunicator/Authentication/GraphTokenProvider.cs b/Source/CompanyCommunicator/Authentication/GraphTokenProvider.cs index b858cfdc6..aca9b1a73 100644 --- a/Source/CompanyCommunicator/Authentication/GraphTokenProvider.cs +++ b/Source/CompanyCommunicator/Authentication/GraphTokenProvider.cs @@ -12,6 +12,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Authentication using System.Threading.Tasks; using Microsoft.Graph; using Microsoft.Identity.Web; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGraph; /// @@ -20,14 +21,19 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Authentication public class GraphTokenProvider : IAuthenticationProvider { private readonly ITokenAcquisition tokenAcquisition; + private readonly IAppConfiguration appConfiguration; /// /// Initializes a new instance of the class. /// /// MSAL.NET token acquisition service. - public GraphTokenProvider(ITokenAcquisition tokenAcquisition) + /// App configuration. + public GraphTokenProvider( + ITokenAcquisition tokenAcquisition, + IAppConfiguration appConfiguration) { this.tokenAcquisition = tokenAcquisition ?? throw new ArgumentNullException(nameof(tokenAcquisition)); + this.appConfiguration = appConfiguration ?? throw new ArgumentNullException(nameof(appConfiguration)); } /// @@ -52,12 +58,12 @@ private async Task GetAccessToken(string permissionType) if (permissionType.Equals(GraphPermissionType.Application.ToString(), StringComparison.CurrentCultureIgnoreCase)) { // we use MSAL.NET to get a token to call the API for application - accessToken = await this.tokenAcquisition.GetAccessTokenForAppAsync(Common.Constants.ScopeDefault); + accessToken = await this.tokenAcquisition.GetAccessTokenForAppAsync(this.appConfiguration.GraphDefaultScope); } else { // we use MSAL.NET to get a token to call the API On Behalf Of the current user - accessToken = await this.tokenAcquisition.GetAccessTokenForUserAsync(new string[] { Common.Constants.ScopeDefault }); + accessToken = await this.tokenAcquisition.GetAccessTokenForUserAsync(new string[] { this.appConfiguration.GraphDefaultScope }); } return accessToken; diff --git a/Source/CompanyCommunicator/Bot/CompanyCommunicatorBotAdapter.cs b/Source/CompanyCommunicator/Bot/CompanyCommunicatorBotAdapter.cs index 1f320f70a..8bfff0824 100644 --- a/Source/CompanyCommunicator/Bot/CompanyCommunicatorBotAdapter.cs +++ b/Source/CompanyCommunicator/Bot/CompanyCommunicatorBotAdapter.cs @@ -6,57 +6,28 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Bot { using System; - using System.Threading.Tasks; using Microsoft.Bot.Builder.Integration.AspNet.Core; using Microsoft.Bot.Connector.Authentication; - using Microsoft.Teams.Apps.CompanyCommunicator.Common.Secrets; /// /// The Company Communicator Bot Adapter. /// - public class CompanyCommunicatorBotAdapter : BotFrameworkHttpAdapter + public class CompanyCommunicatorBotAdapter : CloudAdapter { - private readonly ICertificateProvider certificateProvider; - /// /// Initializes a new instance of the class. /// - /// Credential provider service instance. /// Teams message filter middleware instance. - /// Certificate provider service instance. + /// Bot framework authentication. public CompanyCommunicatorBotAdapter( - ICredentialProvider credentialProvider, CompanyCommunicatorBotFilterMiddleware companyCommunicatorBotFilterMiddleware, - ICertificateProvider certificateProvider) - : base(credentialProvider) + BotFrameworkAuthentication botFrameworkAuthentication) + : base(botFrameworkAuthentication) { companyCommunicatorBotFilterMiddleware = companyCommunicatorBotFilterMiddleware ?? throw new ArgumentNullException(nameof(companyCommunicatorBotFilterMiddleware)); - this.certificateProvider = certificateProvider ?? throw new ArgumentNullException(nameof(certificateProvider)); // Middleware this.Use(companyCommunicatorBotFilterMiddleware); } - - /// - protected override async Task BuildCredentialsAsync(string appId, string oAuthScope = null) - { - appId = appId ?? throw new ArgumentNullException(nameof(appId)); - - if (!this.certificateProvider.IsCertificateAuthenticationEnabled()) - { - return await base.BuildCredentialsAsync(appId, oAuthScope); - } - - var cert = await this.certificateProvider.GetCertificateAsync(appId); - var options = new CertificateAppCredentialsOptions() - { - AppId = appId, - ClientCertificate = cert, - OauthScope = oAuthScope, - }; - - var certificateAppCredentials = new CertificateAppCredentials(options) as AppCredentials; - return certificateAppCredentials; - } } } diff --git a/Source/CompanyCommunicator/ClientApp/package-lock.json b/Source/CompanyCommunicator/ClientApp/package-lock.json index d6a8b4c9b..258e336a7 100644 --- a/Source/CompanyCommunicator/ClientApp/package-lock.json +++ b/Source/CompanyCommunicator/ClientApp/package-lock.json @@ -34869,4 +34869,4 @@ "version": "0.1.0" } } -} +} \ No newline at end of file diff --git a/Source/CompanyCommunicator/ClientApp/package.json b/Source/CompanyCommunicator/ClientApp/package.json index d8becf979..c33a7e384 100644 --- a/Source/CompanyCommunicator/ClientApp/package.json +++ b/Source/CompanyCommunicator/ClientApp/package.json @@ -1,6 +1,6 @@ { "name": "company-communicator", - "version": "5.3.0", + "version": "5.4.0", "private": true, "dependencies": { "@fluentui/react-northstar": "^0.52.0", diff --git a/Source/CompanyCommunicator/Controllers/AuthenticationMetadataController.cs b/Source/CompanyCommunicator/Controllers/AuthenticationMetadataController.cs index 303d83f2b..a13b9b4bb 100644 --- a/Source/CompanyCommunicator/Controllers/AuthenticationMetadataController.cs +++ b/Source/CompanyCommunicator/Controllers/AuthenticationMetadataController.cs @@ -12,6 +12,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Controllers using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Microsoft.Teams.Apps.CompanyCommunicator.Authentication; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; /// /// Controller for the authentication sign in data. @@ -21,12 +22,16 @@ public class AuthenticationMetadataController : ControllerBase { private readonly string tenantId; private readonly string clientId; + private readonly IAppConfiguration appConfiguration; /// /// Initializes a new instance of the class. /// /// The authentication options. - public AuthenticationMetadataController(IOptions authenticationOptions) + /// App configuration. + public AuthenticationMetadataController( + IOptions authenticationOptions, + IAppConfiguration appConfiguration) { if (authenticationOptions is null) { @@ -35,6 +40,7 @@ public AuthenticationMetadataController(IOptions authenti this.tenantId = authenticationOptions.Value.AzureAdTenantId; this.clientId = authenticationOptions.Value.AzureAdClientId; + this.appConfiguration = appConfiguration ?? throw new ArgumentNullException(nameof(appConfiguration)); } /// @@ -64,7 +70,7 @@ public string GetConsentUrl( ["client_id"] = this.clientId, ["response_type"] = "id_token", ["response_mode"] = "fragment", - ["scope"] = "https://graph.microsoft.com/User.Read openid profile", + ["scope"] = this.appConfiguration.GraphUserReadScope, ["nonce"] = Guid.NewGuid().ToString(), ["state"] = Guid.NewGuid().ToString(), ["login_hint"] = loginHint, @@ -73,7 +79,7 @@ public string GetConsentUrl( .Select(p => $"{p.Key}={HttpUtility.UrlEncode(p.Value)}") .ToList(); - var consentUrlPrefix = $"https://login.microsoftonline.com/{this.tenantId}/oauth2/v2.0/authorize?"; + var consentUrlPrefix = $"{this.appConfiguration.AzureAd_Instance}/{this.tenantId}/oauth2/v2.0/authorize?"; var consentUrlString = consentUrlPrefix + string.Join('&', consentUrlComponentList); diff --git a/Source/CompanyCommunicator/Microsoft.Teams.Apps.CompanyCommunicator.csproj b/Source/CompanyCommunicator/Microsoft.Teams.Apps.CompanyCommunicator.csproj index fa0cc8899..a4730ab36 100644 --- a/Source/CompanyCommunicator/Microsoft.Teams.Apps.CompanyCommunicator.csproj +++ b/Source/CompanyCommunicator/Microsoft.Teams.Apps.CompanyCommunicator.csproj @@ -12,13 +12,13 @@ - - + + - + diff --git a/Source/CompanyCommunicator/Startup.cs b/Source/CompanyCommunicator/Startup.cs index 883b4dc8b..568aeb7f7 100644 --- a/Source/CompanyCommunicator/Startup.cs +++ b/Source/CompanyCommunicator/Startup.cs @@ -7,7 +7,6 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator { using System; using System.Net; - using System.Net.Http; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; @@ -24,6 +23,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator using Microsoft.Teams.Apps.CompanyCommunicator.Bot; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Adapter; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Clients; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Extensions; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Repositories; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Repositories.ExportData; @@ -78,6 +78,9 @@ public void ConfigureServices(IServiceCollection services) { Startup.FillAuthenticationOptionsProperties(authenticationOptions, configuration); }); + + services.AddAppConfiguration(this.Configuration); + services.AddOptions() .Configure((botOptions, configuration) => { @@ -156,10 +159,10 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); // Add repositories. services.AddSingleton(); @@ -184,7 +187,11 @@ public void ConfigureServices(IServiceCollection services) // Add microsoft graph services. services.AddScoped(); services.AddScoped(); - services.AddScoped(sp => new GraphServiceClient(sp.GetService(), sp.GetService())); + + services.AddScoped(sp => new GraphServiceClient( + sp.GetService().GraphBaseUrl, + sp.GetService(), + sp.GetService())); services.AddScoped(); services.AddScoped(sp => sp.GetRequiredService().GetGroupsService()); services.AddScoped(sp => sp.GetRequiredService().GetAppCatalogService()); @@ -198,7 +205,7 @@ public void ConfigureServices(IServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); } diff --git a/Source/CompanyCommunicator/appsettings.json b/Source/CompanyCommunicator/appsettings.json index ab6f3f1b1..5bdc5dc9e 100644 --- a/Source/CompanyCommunicator/appsettings.json +++ b/Source/CompanyCommunicator/appsettings.json @@ -14,6 +14,7 @@ "Microsoft.Bot": "Warning" } }, + "TeamsEnvironment" : "{TeamsEnvironment}", "AllowedHosts": "*", "UserAppId": "[UserAppId]", "UserAppPassword": "[UserAppPassword]", diff --git a/Source/Test/CompanyCommunicator.Common.Test/Services/MicrosoftGraph/UsersServiceTests.cs b/Source/Test/CompanyCommunicator.Common.Test/Services/MicrosoftGraph/UsersServiceTests.cs index badf69515..f4fb07bb1 100644 --- a/Source/Test/CompanyCommunicator.Common.Test/Services/MicrosoftGraph/UsersServiceTests.cs +++ b/Source/Test/CompanyCommunicator.Common.Test/Services/MicrosoftGraph/UsersServiceTests.cs @@ -12,6 +12,7 @@ namespace Microsoft.Teams.App.CompanyCommunicator.Common.Test.Services.Microsoft using FluentAssertions; using Microsoft.Graph; using Microsoft.Teams.App.CompanyCommunicator.Common.Test.Services.Mock; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.MicrosoftGraph; using Moq; using Xunit; @@ -29,7 +30,9 @@ public void CreateInstance_AllParameters_ShouldBeSuccess() { // Arrange Mock graphServiceClientMock = new Mock(); - Action action = () => new UsersService(graphServiceClientMock.Object); + Mock appConfigurationMock = new Mock(); + + Action action = () => new UsersService(graphServiceClientMock.Object, appConfigurationMock.Object); // Act and Assert. action.Should().NotThrow(); @@ -42,7 +45,7 @@ public void CreateInstance_AllParameters_ShouldBeSuccess() public void CreateInstance_NullParameters_ThrowsArgumentNullException() { // Arrange - Action action = () => new UsersService(null); + Action action = () => new UsersService(null, null); // Act and Assert. action.Should().Throw(); @@ -211,7 +214,7 @@ private UsersService GetUsersService() }); GraphServiceClient client = new GraphServiceClient(new MockAuthenticationHelper(), mockHttpProvider); - return new UsersService(client); + return new UsersService(client, new CommericalConfiguration("tenant Id")); } } } diff --git a/Source/Test/CompanyCommunicator.Prep.Func.Test/Export/Activities/HandleExportFailureActivityTest.cs b/Source/Test/CompanyCommunicator.Prep.Func.Test/Export/Activities/HandleExportFailureActivityTest.cs index be20171c7..78d7b62a5 100644 --- a/Source/Test/CompanyCommunicator.Prep.Func.Test/Export/Activities/HandleExportFailureActivityTest.cs +++ b/Source/Test/CompanyCommunicator.Prep.Func.Test/Export/Activities/HandleExportFailureActivityTest.cs @@ -13,6 +13,8 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.Test.Export.Activit using global::Azure.Storage.Blobs; using global::Azure.Storage.Blobs.Models; using Microsoft.Bot.Builder; + using Microsoft.Bot.Builder.Integration.AspNet.Core; + using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; @@ -22,6 +24,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.Test.Export.Activit using Microsoft.Teams.Apps.CompanyCommunicator.Common.Repositories.ExportData; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Repositories.UserData; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Resources; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Secrets; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.CommonBot; using Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.Export.Activities; using Moq; @@ -36,9 +39,8 @@ public class HandleExportFailureActivityTest private readonly Mock storageClientFactory = new Mock(); private readonly Mock userDataRepository = new Mock(); private readonly Mock> botOptions = new Mock>(); - private readonly Mock botAdapter = new Mock(); private readonly Mock> localizer = new Mock>(); - private readonly Mock blobContainerClient = new Mock(); + private readonly Mock botAdapter = new Mock(new Mock().Object, new Mock().Object); /// /// Constructor test for all parameters. @@ -50,6 +52,7 @@ public void CreateInstance_AllParameters_ShouldBeSuccess() this.botOptions.Setup(x => x.Value).Returns(new BotOptions() { AuthorAppId = "AuthorAppId" }); Action action = () => new HandleExportFailureActivity(this.exportDataRepository.Object, this.storageClientFactory.Object, this.botOptions.Object, this.botAdapter.Object, this.userDataRepository.Object, this.localizer.Object); + // Act and Assert. action.Should().NotThrow(); } diff --git a/Source/Test/CompanyCommunicator.Prep.Func.Test/Export/Activities/SendFileCardActivityTest.cs b/Source/Test/CompanyCommunicator.Prep.Func.Test/Export/Activities/SendFileCardActivityTest.cs index 2ee6a011c..975f327a7 100644 --- a/Source/Test/CompanyCommunicator.Prep.Func.Test/Export/Activities/SendFileCardActivityTest.cs +++ b/Source/Test/CompanyCommunicator.Prep.Func.Test/Export/Activities/SendFileCardActivityTest.cs @@ -10,6 +10,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.Test.Export.Activit using System.Threading.Tasks; using FluentAssertions; using Microsoft.Bot.Builder; + using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; @@ -18,6 +19,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.Test.Export.Activit using Microsoft.Teams.Apps.CompanyCommunicator.Common.Repositories.NotificationData; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Repositories.UserData; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Resources; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Secrets; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.CommonBot; using Microsoft.Teams.Apps.CompanyCommunicator.Common.Services.Teams; using Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.Export.Activities; @@ -30,7 +32,6 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Prep.Func.Test.Export.Activit public class SendFileCardActivityTest { private readonly Mock> botOptions = new Mock>(); - private readonly Mock botAdapter = new Mock(); private readonly Mock userDataRepository = new Mock(); private readonly Mock conversationService = new Mock(); private readonly Mock> options = new Mock>(); @@ -38,6 +39,7 @@ public class SendFileCardActivityTest private readonly Mock> localizer = new Mock>(); private readonly Mock log = new Mock(); private readonly Mock turnContext = new Mock(); + private readonly Mock botAdapter = new Mock(new Mock().Object, new Mock().Object); /// /// Constructor test for all parameters. diff --git a/Source/Test/CompanyCommunicator.Test/Controllers/AuthenticationMetadataControllerTest.cs b/Source/Test/CompanyCommunicator.Test/Controllers/AuthenticationMetadataControllerTest.cs index 480154084..3e387c6be 100644 --- a/Source/Test/CompanyCommunicator.Test/Controllers/AuthenticationMetadataControllerTest.cs +++ b/Source/Test/CompanyCommunicator.Test/Controllers/AuthenticationMetadataControllerTest.cs @@ -11,6 +11,7 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Test.Controllers using FluentAssertions; using Microsoft.Extensions.Options; using Microsoft.Teams.Apps.CompanyCommunicator.Authentication; + using Microsoft.Teams.Apps.CompanyCommunicator.Common.Configuration; using Microsoft.Teams.Apps.CompanyCommunicator.Controllers; using Moq; using Xunit; @@ -21,6 +22,8 @@ namespace Microsoft.Teams.Apps.CompanyCommunicator.Test.Controllers public class AuthenticationMetadataControllerTest { private readonly Mock> options = new Mock>(); + private readonly Mock appConfigurationMock = new Mock(); + private readonly string tenantId = "tenantId"; private readonly string clientId = "clientId"; @@ -47,7 +50,7 @@ public void CreateInstance_AllParameters_ShouldBeSuccess() { // Arrange this.options.Setup(x => x.Value).Returns(new AuthenticationOptions() { AzureAdTenantId = this.tenantId, AzureAdClientId = this.clientId }); - Action action = () => new AuthenticationMetadataController(this.options.Object); + Action action = () => new AuthenticationMetadataController(this.options.Object, this.appConfigurationMock.Object); // Act and Assert. action.Should().NotThrow(); @@ -60,7 +63,7 @@ public void CreateInstance_AllParameters_ShouldBeSuccess() public void CreateInstance_NullParameter_ThrowsArgumentNullException() { // Arrange - Action action = () => new AuthenticationMetadataController(null /*authenticationOptions*/); + Action action = () => new AuthenticationMetadataController(null /*authenticationOptions*/, null); // Act and Assert. action.Should().Throw("authenticationOptions is null."); @@ -201,7 +204,7 @@ public void Check_consentUrl_JoinCharaterAmpersand() public AuthenticationMetadataController GetAuthenticationMetadataController() { this.options.Setup(x => x.Value).Returns(new AuthenticationOptions() { AzureAdTenantId = this.tenantId, AzureAdClientId = this.clientId }); - return new AuthenticationMetadataController(this.options.Object); + return new AuthenticationMetadataController(this.options.Object, new CommericalConfiguration("tenant Id")); } private List GetComponents() diff --git a/Wiki/Deployment-guide-gcch.md b/Wiki/Deployment-guide-gcch.md new file mode 100644 index 000000000..cb7c1b2cb --- /dev/null +++ b/Wiki/Deployment-guide-gcch.md @@ -0,0 +1,323 @@ +- Deployment Guide + - [Prerequisites](#prerequisites) + - [Steps](#Deployment-Steps) + - [Register AD Application](#1-register-azure-ad-application) + - [Deploy to Azure subscription](#2-deploy-to-your-azure-subscription) + - [Set-up Authentication](#3-set-up-authentication) + - [Add Permissions to your app](#4-add-permissions-to-your-app) + - [Create the Teams app packages](#5-create-the-teams-app-packages) + - [Install the apps in Microsoft Teams](#6-install-the-apps-in-microsoft-teams) + - [Troubleshooting](#troubleshooting) +- - - + +# Prerequisites + +To begin, you will need: +* An Azure subscription where you can create the following kinds of resources: + * App Service + * App Service Plan + * Bot Channels Registration + * Azure Function + * Azure Storage Account + * Service Bus + * Application Insights + * Azure Key vault +* A role to assign roles in Azure RBAC. To check if you have permission to do this, + * Goto the subscription page in Azure portal. Then, goto Access Control(IAM) and click on `View my access` button. + * Click on your `role` and in search permissions text box, search for `Microsoft.Authorization/roleAssignments/Write`. + * If your current role does not have the permission, then you can grant yourself the built in role `User Access Administrator` or create a custom role. + * Please follow this [link](https://docs.microsoft.com/en-us/azure/role-based-access-control/custom-roles#steps-to-create-a-custom-role) to create a custom role. Use this action `Microsoft.Authorization/roleAssignments/Write` in the custom role to assign roles in Azure RBAC. +* A team with the users who will be sending messages with this app. (You can add or remove team members later!) +* A copy of the Company Communicator app GitHub repo (https://github.com/OfficeDev/microsoft-teams-company-communicator-app) + + +- - - + +# Deployment Steps + +## 1. Register Azure AD application + +Register three Azure AD application in your tenant's directory: one for author bot, one for user bot and another for graph app. + +1. Log in to the Azure Portal for your subscription, and go to the [App registrations](https://portal.azure.us/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) blade. + +1. Click **New registration** to create an Azure AD application. + - **Name**: Name of your Teams App - if you are following the template for a default deployment, we recommend "Company Communicator User". + - **Supported account types**: Select "Accounts in any organizational directory" (*refer image below*). + - Leave the "Redirect URI" field blank for now. + + ![Azure AD app registration page](images/multitenant_app_creation.png) + +1. Click **Register** to complete the registration. + +1. When the app is registered, you'll be taken to the app's "Overview" page. Copy the **Application (client) ID**; we will need it later. Verify that the "Supported account types" is set to **Multiple organizations**. + + ![Azure AD app overview page](images/multitenant_app_overview_1.png) + +1. On the side rail in the Manage section, navigate to the "Certificates & secrets" section. In the Client secrets section, click on "+ New client secret". Add a description for the secret, and choose when the secret will expire. Click "Add". + + ![Azure AD app secret](images/multitenant_app_secret.png) + +1. Once the client secret is created, copy its **Value**; we will need it later. + +1. Go back to "App registrations", then repeat steps 2-5 to create another Azure AD application for the author bot. + - **Name**: Name of your Teams App - if you are following the template for a default deployment, we recommend "Company Communicator Author". + - **Supported account types**: Select "Accounts in any organizational directory". + - Leave the "Redirect URI" field blank for now. + +1. Go back to "App registrations", then repeat steps 2-5 to create another Azure AD application for the Microsoft Graph app. + - **Name**: Name of your Teams App - if you are following the template for a default deployment, we recommend "Company Communicator App". + - **Supported account types**: Select "Accounts in this organizational directory only(Default Directory only - Single tenant)". + - Leave the "Redirect URI" field blank for now. + + + At this point you should have the following 7 values: + 1. Application (client) ID for the user bot. + 2. Client secret for the user bot. + 3. Directory (tenant) ID. + 4. Application (client) Id for the author bot. + 5. Client secret for the author bot. + 6. Application (client) Id for the Microsoft Graph App. + 7. Client secret for the Microsoft Graph App. + + We recommend that you copy the values, we will need them later. + + ![Azure AD app overview page](images/multitenant_app_overview_2.png) + +## 2. Deploy to your Azure subscription +1. Click on the **Deploy to Azure** button below. + + [![Deploy to Azure](images/deploybutton.png)](https://portal.azure.us/#create/Microsoft.Template/uri/https%3A%2F%2Fraw.githubusercontent.com%2FOfficeDev%2Fmicrosoft-teams-apps-company-communicator%2Fmain%2FDeployment%2FGCCH%2Fazuredeploy.json) + +1. When prompted, log in to your Azure subscription. + +1. Azure will create a "Custom deployment" based on the Company Communicator ARM template and ask you to fill in the template parameters. + + > **Note:** Please ensure that you don't use underscore (_) or space in any of the field values otherwise the deployment may fail. + +1. Select a subscription and a resource group. + * We recommend creating a new resource group. + * The resource group location MUST be in a datacenter that supports all the following: + * Storage Accounts + * Application Insights + * Azure Functions + * Service Bus + * App Service + +1. Enter a **Base Resource Name**, which the template uses to generate names for the other resources. + * The `[Base Resource Name]` must be available. For example, if you select `contosocommunicator` as the base name, the name `contosocommunicator` must be available (not taken); otherwise, the deployment will fail with a Conflict error. + * Please make sure to limit the base resource name with maximum of 18 characters. + * Remember the base resource name that you selected. We will need it later. + +1. Update the following fields in the template: + 1. **User Client ID**: The application (client) ID of the Microsoft Teams user bot app. (from Step 1) + 2. **User Client Secret**: The client secret of the Microsoft Teams user bot app. (from Step 1) + 3. **Tenant Id**: The tenant ID. (from Step 1) + 4. **Author Client ID**: The application (client) ID of the Microsoft Teams author bot app. (from Step 1) + 5. **Author Client Secret**: The client secret of the Microsoft Teams author bot app. (from Step 1) + 6. **Microsoft Graph App Client ID**: The application (client) ID of the Microsoft Graph Azure AD app. (from Step 1) + 7. **Microsoft Graph App Secret**: The client secret of the Microsoft Graph Azure AD app. (from Step 1) + 8. **Proactively Install User App [Optional]**: Default value is `true`. You may set it to `false` if you want to disable the feature. + 9. **User App ExternalId [Optional]**: Default value is `148a66bb-e83d-425a-927d-09f4299a9274`. This **MUST** be the same `id` that is in the Teams app manifest for the user app. + 10. **Header Text [Optional]**: Default value is `Company Communicator`. This is the banner text that will appear starting v5.2 and later, you will have the option to modify later. + 11. **Header Logo URL [Optional]**: Default image is Microsoft logo. You will have the option to modify later. + 12. **Hosting Plan SKU [Optional]**: The pricing tier for the hosting plan. Default value is `Standard`. You may choose between Basic, Standard and Premium. + 13. **Hosting Plan Size [Optional]**: The size of the hosting plan (small - 1, medium - 2, or large - 3). Default value is `2`. + + > **Note:** The default value is 2 to minimize the chances of an error during app deployment. After deployment you can choose to change the size of the hosting plan. + 14. **Service Bus Web App Role Name Guid [Optional]**: Default value is `958380b3-630d-4823-b933-f59d92cdcada`. This **MUST** be the same `id` per app deployment. + + > **Note:** Make sure to keep the same values for an upgrade. Please change the role name GUIDs in case of another Company Communicator Deployment in same subscription. + + 15. **Service Bus Prep Func Role Name Guid [Optional]**: Default value is `ce6ca916-08e9-4639-bfbe-9d098baf42ca`. This **MUST** be the same `id` per app deployment. + 16. **Service Bus Send Func Role Name Guid [Optional]**: Default value is `960365a2-c7bf-4ff3-8887-efa86fe4a163`. This **MUST** be the same `id` per app deployment. + 17. **Service Bus Data Func Role Name Guid [Optional]**: Default value is `d42703bc-421d-4d98-bc4d-cd2bb16e5b0a`. This **MUST** be the same `id` per app deployment. + 18. **Storage Account Web App Role Name Guid [Optional]**: Default value is `edd0cc48-2cf7-490e-99e8-131311e42030`. This **MUST** be the same `id` per app deployment. + 19. **Storage Account Prep Func Role Name Guid [Optional]**: Default value is `9332a9e9-93f4-48d9-8121-d279f30a732e`. This **MUST** be the same `id` per app deployment. + 20. **Storage Account Data Func Role Name Guid [Optional]**: Default value is `5b67af51-4a98-47e1-9d22-745069f51a13`. This **MUST** be the same `id` per app deployment. + 21. **DefaultCulture [Optional]**: By default the application uses `en-US` locale. You can choose the locale from the list, if you wish to use the app in different locale. Also, you may add/update the resources for other locales and update this configuration if desired. + 22. **SupportedCultures [Optional]**: This is the list of locales that application supports currently. You may add/update the resources for other locales and update this configuration if desired. + + > **Note:** Make sure that the values are copied as-is, with no extra spaces. The template checks that GUIDs are exactly 36 characters. + +1. Update the "Sender UPN List", which is a semicolon-delimited list of users (Authors) who will be allowed to send messages using the Company Communicator. + * For example, to allow Megan Bowen (meganb@contoso.com) and Adele Vance (adelev@contoso.com) to send messages, set this parameter to `meganb@contoso.com;adelev@contoso.com`. + * You can change this list later by going to the App Service's "Configuration" blade. + +1. If you wish to change the app name, description, and icon from the defaults, modify the corresponding template parameters. + +1. If you wish to change the header/banner text and logo, refer to FAQ in the Wiki. + +1. Agree to the Azure terms and conditions by clicking on the check box "I agree to the terms and conditions stated above" located at the bottom of the page. + +1. Click on "Purchase" to start the deployment. + + > **Note:** The Bot Service will fail initially. Follow the steps below to resolve the issue. + +1. While the deployment is still in progress, go to the Resource group. Search for Author Bot Service created by ARM template. Name of Bot Service will be base resource name which was provided earlier. + +1. Click on Channels in Settings section of Bot Services and click on "Microsoft Teams" under Available Channels. + + ![Add channel to bot](images/botchannelgovernment_3.png) + +1. Accept the Terms of Service. + + ![Add channel to bot](images/botchannelgovernment_2.png) + +1. Select "Microsoft Teams for GCC High" and click on Apply. Repeat the above steps for User Bot Service. + + ![Add channel to bot](images/botchannelgovernment_1.png) + +1. Wait for the deployment to finish. You can check the progress of the deployment from the "Notifications" pane of the Azure Portal. It may take **up to an hour** for the deployment to finish. + + > If the deployment fails, see [this section](https://github.com/OfficeDev/microsoft-teams-company-communicator-app/wiki/Troubleshooting#1-code-deployment-failure) of the Troubleshooting guide. + +1. Once the deployment is successfully completed, go to the deployment's "Outputs" tab, and note down the following values. We will need them later. + * **authorBotId:** This is the Microsoft Application ID for the Company Communicator app. For the following steps, it will be referred to as `%authorBotId%`. + * **userBotId:** This is the Microsoft Application ID for the Company Communicator app. For the following steps, it will be referred to as `%userBotId%`. + * **appDomain:** This is the base domain for the Company Communicator app. For the following steps, it will be referred to as `%appDomain%`. + +## 3. Set-up Authentication + +1. Note that you have the `%authorBotId%`, `%userBotId%` and `%appDomain%` values from the previous step (Step 2). + + > If do not have these values, refer [this section](https://github.com/OfficeDev/microsoft-teams-company-communicator-app/wiki/Troubleshooting#2-forgetting-the-botId-or-appDomain) of the Troubleshooting guide for steps to get these values. + +1. Go to **App Registrations** page [here](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) and open the Microsoft Graph Azure AD app you created (in Step 1) from the application list. + + > NOTE: This step is to set-up authentication for Microsoft Graph Azure AD app. + +1. Under **Manage**, click on **Authentication** to bring up authentication settings. + + 1. Add a new entry to **Redirect URIs**: + - **Type**: Web + - **Redirect URI**: Enter `https://%appDomain%/signin-simple-end` for the URL e.g. `https://appName.azurefd.us/signin-simple-end` + + 1. Under **Implicit grant**, check **ID tokens**. + + 1. Click **Save** to commit your changes. + +1. Back under **Manage**, click on **Expose an API**. + + 1. Click on the **Set** link next to **Application ID URI**, and change the value to `api://%appDomain%` e.g. `api://appName.azurefd.us`. + + 1. Click **Save** to commit your changes. + + 1. Click on **Add a scope**, under **Scopes defined by this API**. In the flyout that appears, enter the following values: + * **Scope name:** access_as_user + * **Who can consent?:** Admins and users + * **Admin and user consent display name:** Access the API as the current logged-in user + * **Admin and user consent description:** Access the API as the current logged-in user + + 1. Click **Add scope** to commit your changes. + + 1. Click **Add a client application**, under **Authorized client applications**. In the flyout that appears, enter the following values: + * **Client ID**: `5e3ce6c0-2b1f-4285-8d4b-75ee78787346` + * **Authorized scopes**: Select the scope that ends with `access_as_user`. (There should only be 1 scope in this list.) + + 1. Click **Add application** to commit your changes. + + 1. **Repeat the previous two steps**, but with client ID = `1fec8e78-bce4-4aaf-ab1b-5451cc387264`. After this step you should have **two** client applications (`5e3ce6c0-2b1f-4285-8d4b-75ee78787346` and `1fec8e78-bce4-4aaf-ab1b-5451cc387264`) listed under **Authorized client applications**. + +1. Back under **Manage**, click on **Manifest**. + + 1. In the editor that appears, find the `optionalClaims` property in the JSON Azure AD application manifest, and replace it with the following block: + ``` + "optionalClaims": { + "idToken": [], + "accessToken": [ + { + "name": "upn", + "source": null, + "essential": false, + "additionalProperties": [] + } + ], + "saml2Token": [] + }, + ``` + + 2. Click **Save** to commit your changes. + +## 4. Add Permissions to your Microsoft Graph Azure AD app + +Continuing from the Microsoft Graph Azure AD app registration page where we ended Step 3. + +1. Select **API Permissions** blade from the left hand side. + +2. Click on **Add a permission** button to add permission to your app. + +3. In Microsoft APIs under Select an API label, select the particular service and give the following permissions, + + * Under **Commonly used Microsoft APIs**, + + * Select Microsoft Graph, then select **Delegated permissions** and check the following permissions, + 1. **GroupMember.Read.All** + 2. **AppCatalog.Read.All** + + * then select **Application permissions** and check the following permissions, + 1. **GroupMember.Read.All** + 2. **User.Read.All** + 3. **TeamsAppInstallation.ReadWriteForUser.All** + + * Click on **Add Permissions** to commit your changes. + + ![Azure AD API permissions](images/multitenant_app_permissions_1.png) + ![Azure AD API permissions](images/multitenant_app_permissions_2.png) + + > Please refer to [Solution overview](https://github.com/OfficeDev/microsoft-teams-company-communicator-app/wiki/Solution-overview#microsoft-graph-api) for more details about the above permissions. + +4. If you are logged in as the Global Administrator, click on the Grant admin consent for %tenant-name% button to grant admin consent, else inform your Admin to do the same through the portal. + +## 5. Create the Teams app packages + +Company communicator app comes with 2 applications Author, User. The Author application is intended for employees who create and send messages in the organization, and the User application is intended for employees who receive the messages. + +Create two Teams app packages: one to be installed to an Authors team and other for recipient’s to install personally and/or to teams. + +1. Make sure you have cloned the app repository locally. + +1. Open the `Manifest\manifest_authors.json` file in a text editor. + +1. Change the placeholder fields in the manifest to values appropriate for your organization. + * `developer.name` ([What's this?](https://docs.microsoft.com/en-us/microsoftteams/platform/resources/schema/manifest-schema#developer)) + * `developer.websiteUrl` + * `developer.privacyUrl` + * `developer.termsOfUseUrl` + +1. Change the `<>` placholder in the configurationUrl setting to be the `%appDomain%` value e.g. "`https://appName.azurefd.us/configtab`". + +1. Change the `<>` placeholder in the botId setting to be the `%authorBotId%` value - this is your author Azure AD application's ID from above. This is the same GUID that you entered in the template under "Author Client ID". Please note that there are two places in the manifest (for authors) where you will need to update Bot ID. + +1. Change the `<>` placeholder in the validDomains setting to be the `%appDomain%` value e.g. "`appName.azurefd.us`". + +1. Change the `<>` placeholder in the id setting of the webApplicationInfo section to be the `%authorBotId%` value. Change the `<>` placeholder in the resource setting of the webApplicationInfo section to be the `%appDomain%` value e.g. "`api://appName.azurefd.us`". + +1. Copy the `manifest_authors.json` file to a file named `manifest.json`. + +1. Create a ZIP package with the `manifest.json`,`color.png`, and `outline.png`. The two image files are the icons for your app in Teams. + * Name this package `company-communicator-authors.zip`, so you know that this is the app for the author teams. + * Make sure that the 3 files are the _top level_ of the ZIP package, with no nested folders. + ![image10](images/file-explorer.png) + +1. Delete the `manifest.json` file. + +Repeat the steps above but with the file `Manifest\manifest_users.json` and use `%userBotId%` for `<>` placeholder. Note: you will not need to change anything for the configurationUrl or webApplicationInfo section because the recipients app does not have the configurable tab. Name the resulting package `company-communicator-users.zip`, so you know that this is the app for the recipients. + +## 6. Install the apps in Microsoft Teams + +1. Install the authors app (the `company-communicator-authors.zip` package) to your team of message authors. + * Note that even if non-authors install the app, the UPN list in the app configuration will prevent them from accessing the message authoring experience. Only the users in the sender UPN list will be able to compose and send messages. + * If your tenant has sideloading apps enabled, you can install your app by following the instructions [here](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/apps/apps-upload#load-your-package-into-teams). + +2. Add the configurable tab to the team of authors, so that they can compose and send messages. + +3. Upload the User app to your tenant's app catalog so that it is available for everyone in your tenant to install. +> **IMPORTANT:** Proactive app installation will work only if you upload the User app to your tenant's app catalog. + +4. Install the User app (the `company-communicator-users.zip` package) to the users and teams that will be the target audience. +> If `proactiveAppInstallation` is enabled, you may skip this step. The service will install the app for all the recipients when authors send a message. +--- + +# Troubleshooting +Please check the [Troubleshooting](Troubleshooting) guide. diff --git a/Wiki/Deployment-guide.md b/Wiki/Deployment-guide.md index c96a89009..6edaec6f1 100644 --- a/Wiki/Deployment-guide.md +++ b/Wiki/Deployment-guide.md @@ -24,7 +24,7 @@ To begin, you will need: * Service Bus * Application Insights * Azure Key vault -* An role to assign roles in Azure RBAC. To check if you have permission to do this, +* A role to assign roles in Azure RBAC. To check if you have permission to do this, * Goto the subscription page in Azure portal. Then, goto Access Control(IAM) and click on `View my access` button. * Click on your `role` and in search permissions text box, search for `Microsoft.Authorization/roleAssignments/Write`. * If your current role does not have the permission, then you can grant yourself the built in role `User Access Administrator` or create a custom role. @@ -140,8 +140,8 @@ Register three Azure AD application in your tenant's directory: one for author b 18. **Storage Account Web App Role Name Guid [Optional]**: Default value is `edd0cc48-2cf7-490e-99e8-131311e42030`. This **MUST** be the same `id` per app deployment. 19. **Storage Account Prep Func Role Name Guid [Optional]**: Default value is `9332a9e9-93f4-48d9-8121-d279f30a732e`. This **MUST** be the same `id` per app deployment. 20. **Storage Account Data Func Role Name Guid [Optional]**: Default value is `5b67af51-4a98-47e1-9d22-745069f51a13`. This **MUST** be the same `id` per app deployment. - 21. **DefaultCulture [Optional]**: By default the application uses `en-US` locale. You can choose the locale from the list, if you wish to use the app in different locale.Also, you may add/update the resources for other locales and update this configuration if desired. - 22. **SupportedCultures [Optional]**: This is the list of locales that application supports currently.You may add/update the resources for other locales and update this configuration if desired. + 21. **DefaultCulture [Optional]**: By default the application uses `en-US` locale. You can choose the locale from the list, if you wish to use the app in different locale. Also, you may add/update the resources for other locales and update this configuration if desired. + 22. **SupportedCultures [Optional]**: This is the list of locales that application supports currently. You may add/update the resources for other locales and update this configuration if desired. > **Note:** Make sure that the values are copied as-is, with no extra spaces. The template checks that GUIDs are exactly 36 characters. @@ -163,7 +163,7 @@ Register three Azure AD application in your tenant's directory: one for author b > If the deployment fails, see [this section](https://github.com/OfficeDev/microsoft-teams-company-communicator-app/wiki/Troubleshooting#1-code-deployment-failure) of the Troubleshooting guide. -1. Once the deployment is successfully completed, go to the deployment's "Outputs" tab, and note down the follwing values. We will need them later. +1. Once the deployment is successfully completed, go to the deployment's "Outputs" tab, and note down the following values. We will need them later. * **authorBotId:** This is the Microsoft Application ID for the Company Communicator app. For the following steps, it will be referred to as `%authorBotId%`. * **userBotId:** This is the Microsoft Application ID for the Company Communicator app. For the following steps, it will be referred to as `%userBotId%`. * **appDomain:** This is the base domain for the Company Communicator app. For the following steps, it will be referred to as `%appDomain%`. @@ -270,7 +270,7 @@ Continuing from the Microsoft Graph Azure AD app registration page where we ende Company communicator app comes with 2 applications – Author, User. The Author application is intended for employees who create and send messages in the organization, and the User application is intended for employees who receive the messages. -Create two Teams app packages: one to be installed to an Authors team and other for recipients to install personally and/or to teams. +Create two Teams app packages: one to be installed to an Authors team and other for recipient’s to install personally and/or to teams. 1. Make sure you have cloned the app repository locally. diff --git a/Wiki/FAQ.md b/Wiki/FAQ.md index e84feb711..2479c0fcf 100644 --- a/Wiki/FAQ.md +++ b/Wiki/FAQ.md @@ -70,9 +70,11 @@ You can update the banner title and the logo by updating the configuration in Az ![Sync changes](images/sync_changes.png) ### 13. Is it possible to format the message in the summary field? -Yes, you can use markdown tags for formatting the message in the summary. CC v5.2 supports this feature, you can refer [here](https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features) to know the list of styles supported. +Yes, you can use markdown tags for formatting the message in the summary. CC v5.2 and above versions support this feature, you can refer [here](https://docs.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features) to know the list of styles supported. -### 14. Latest changes does not reflect after upgrading the app to v5.3.0? +### 14. Latest changes does not reflect after upgrading the app to latest versions? Teams client default behavior is to use client app resources (js/css/ images) from teams cache. To reflect the latest changes please sign out and sign in again to the teams client or clear the teams cache with the steps mentioned [here](https://docs.microsoft.com/en-us/microsoftteams/troubleshoot/teams-administration/clear-teams-cache). +### 15. Does the app works in GCC/GCCH tenant? +Yes, Company Communicator v5.4 works in Commercial, GCC and GCCH tenants. diff --git a/Wiki/Release-notes.md b/Wiki/Release-notes.md index f8374a58d..cb10ec116 100644 --- a/Wiki/Release-notes.md +++ b/Wiki/Release-notes.md @@ -4,8 +4,9 @@ Cumulative improvements in Company Communicator App. ### Version history -|Release |Published to
Microsoft Store | +|Version |Release Date | |---|---| +| 5.4 | May 10, 2023 | 5.3 | Dec 14, 2022 | 5.2 | Jul 26, 2022 | 5.1 | Apr 28, 2022 @@ -24,6 +25,10 @@ Cumulative improvements in Company Communicator App. | 1.0 | Dec 20, 2019 ### Company Communicator feature release notes +#### 5.4 (May 10, 2023) +##### Changes introduced +- Code changes to support Government Community Cloud High (GCCH) environment. + #### 5.3 (Dec 14, 2022) ##### Changes introduced - .NET upgrade from .NET Core 3.1 to .NET 6.0 diff --git a/Wiki/images/botchannelgovernment_1.png b/Wiki/images/botchannelgovernment_1.png new file mode 100644 index 000000000..d0ba49a53 Binary files /dev/null and b/Wiki/images/botchannelgovernment_1.png differ diff --git a/Wiki/images/botchannelgovernment_2.png b/Wiki/images/botchannelgovernment_2.png new file mode 100644 index 000000000..61291d76b Binary files /dev/null and b/Wiki/images/botchannelgovernment_2.png differ diff --git a/Wiki/images/botchannelgovernment_3.png b/Wiki/images/botchannelgovernment_3.png new file mode 100644 index 000000000..24eb88cdb Binary files /dev/null and b/Wiki/images/botchannelgovernment_3.png differ diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 37ff861ed..531f959c2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,6 +13,26 @@ variables: steps: - checkout: self +- task: NodeTool@0 + displayName: 'Use Node =16.x' + inputs: + versionSpec: '=16.x' + +- task: Npm@1 + displayName: 'npm install' + inputs: + command: custom + workingDir: Source\CompanyCommunicator\ClientApp + verbose: false + customCommand: 'install --save --legacy-peer-deps' + +- task: Npm@1 + displayName: 'npm build' + inputs: + command: custom + workingDir: Source\CompanyCommunicator\ClientApp + verbose: false + customCommand: 'run build' - task: UseDotNet@2 displayName: 'Use .NET 6.0.x' inputs: