diff --git a/README.md b/README.md index 92a7c1332..feace0e06 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ We use the [@msbotframework](https://twitter.com/msbotframework) account on twit The [Gitter Channel](https://gitter.im/Microsoft/BotBuilder) provides a place where the Community can get together and collaborate. ## Contributing and our code of conduct -We welcome contributions and suggestions. Please see our [contributing guidelines](./contributing.md) for more information. +We welcome contributions and suggestions. Please see our [contributing guidelines](./Contributing.md) for more information. This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json index cc1800c0d..92b2e84c9 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json @@ -14,8 +14,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json index eea65b7c3..eb36c03fb 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json @@ -15,7 +15,7 @@ "value": "" }, "newAppServicePlanLocation": { - "value": "" + "value": "West US" }, "newAppServicePlanSku": { "value": { @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md index f8f1d1e56..19d77be80 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md @@ -1,28 +1,48 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment group create --resource-group --template-file --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment group create --resource-group --template-file --parameters @` # parameters-for-template-BotApp-with-rg: -**appServiceName**:(required) The Name of the Bot App Service. +- **appServiceName**:(required) The Name of the Bot App Service. + +- (choose an existingAppServicePlan or create a new AppServicePlan) + - **existingAppServicePlanName**: The name of the App Service Plan. + - **existingAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanName**: The name of the App Service Plan. + - **newAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** + +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. + +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. + +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. + +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. + +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -(choose an existingAppServicePlan or create a new AppServicePlan) -**existingAppServicePlanName**: The name of the App Service Plan. -**existingAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanName**: The name of the App Service Plan. -**newAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-with-rg: -**azureBotId**:(required) The globally unique and immutable bot ID. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **azureBotId**:(required) The globally unique and immutable bot ID. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json index f7d08b75d..b2b686dcc 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json @@ -9,8 +9,8 @@ } }, "azureBotSku": { - "defaultValue": "S1", "type": "string", + "defaultValue": "S1", "metadata": { "description": "The pricing tier of the Bot Service Registration. Allowed values are: F0, S1(default)." } @@ -24,15 +24,72 @@ }, "botEndpoint": { "type": "string", + "defaultValue": "", "metadata": { "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "[variables('msiResourceId')]" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "msiResourceId": "[variables('appTypeDef')[parameters('appType')].msiResourceId]" } }, "resources": [ @@ -49,8 +106,11 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppMSIResourceId": "[variables('appType').msiResourceId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json index 9b1c79ae9..23a23b1cc 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json @@ -4,7 +4,6 @@ "parameters": { "appServiceName": { "type": "string", - "defaultValue": "", "metadata": { "description": "The globally unique name of the Web App." } @@ -18,18 +17,21 @@ }, "existingAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } }, "newAppServicePlanName": { "type": "string", + "defaultValue": "", "metadata": { "description": "The name of the new App Service Plan." } }, "newAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -47,6 +49,18 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -59,13 +73,58 @@ "metadata": { "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } } }, "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlanName')), 'createNewAppServicePlan', parameters('existingAppServicePlanName'))]", - "useExistingServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "useExistingServicePlan": "[not(empty(parameters('existingAppServicePlanName')))]", "servicePlanName": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanName'), parameters('newAppServicePlanName'))]", - "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]" + "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -89,13 +148,15 @@ "comments": "Create a Web App using an App Service Plan", "type": "Microsoft.Web/sites", "apiVersion": "2015-08-01", - "name": "[parameters('appServiceName')]", "location": "[variables('servicePlanLocation')]", "kind": "app,linux", "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" ], + "name": "[parameters('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { + "name": "[parameters('appServiceName')]", "enabled": true, "hostNameSslStates": [ { @@ -124,6 +185,10 @@ "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", "value": "true" }, + { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" @@ -131,6 +196,10 @@ { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -167,7 +236,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -201,7 +270,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json index f18061813..e51036f85 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json @@ -20,8 +20,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json index f3f07b497..de2dba051 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md index d88b160f1..4c752364b 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md @@ -1,31 +1,45 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment sub create --template-file --location --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment sub create --template-file --location --parameters @` # parameters-for-template-BotApp-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **appServiceName**:(required) The location of the App Service Plan. +- **appServicePlanName**:(required) The name of the App Service Plan. +- **appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. +- **appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appServiceName**:(required) The location of the App Service Plan. -**appServicePlanName**:(required) The name of the App Service Plan. -**appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. -**appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. -**azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource \ No newline at end of file diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json index f79264452..63fbf970d 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json @@ -41,11 +41,47 @@ "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]" } }, "resources": [ @@ -85,8 +121,10 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json index b33b21510..381b57abf 100644 --- a/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json +++ b/generators/app/templates/core/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json @@ -28,6 +28,7 @@ }, "appServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -45,6 +46,25 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -53,16 +73,56 @@ }, "appSecret": { "type": "string", + "defaultValue": "", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." + } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types." + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." } } }, "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", "appServicePlanName": "[parameters('appServicePlanName')]", "resourcesLocation": "[if(empty(parameters('appServicePlanLocation')), parameters('groupLocation'), parameters('appServicePlanLocation'))]", "appServiceName": "[parameters('appServiceName')]", - "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -114,6 +174,7 @@ "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" ], "name": "[variables('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { "name": "[variables('appServiceName')]", "hostNameSslStates": [ @@ -136,12 +197,19 @@ "value": "true" }, { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" }, { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -179,7 +247,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -213,7 +281,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json index cc1800c0d..92b2e84c9 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json @@ -14,8 +14,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json index eea65b7c3..eb36c03fb 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json @@ -15,7 +15,7 @@ "value": "" }, "newAppServicePlanLocation": { - "value": "" + "value": "West US" }, "newAppServicePlanSku": { "value": { @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md index f8f1d1e56..19d77be80 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md @@ -1,28 +1,48 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment group create --resource-group --template-file --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment group create --resource-group --template-file --parameters @` # parameters-for-template-BotApp-with-rg: -**appServiceName**:(required) The Name of the Bot App Service. +- **appServiceName**:(required) The Name of the Bot App Service. + +- (choose an existingAppServicePlan or create a new AppServicePlan) + - **existingAppServicePlanName**: The name of the App Service Plan. + - **existingAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanName**: The name of the App Service Plan. + - **newAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** + +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. + +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. + +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. + +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. + +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -(choose an existingAppServicePlan or create a new AppServicePlan) -**existingAppServicePlanName**: The name of the App Service Plan. -**existingAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanName**: The name of the App Service Plan. -**newAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-with-rg: -**azureBotId**:(required) The globally unique and immutable bot ID. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **azureBotId**:(required) The globally unique and immutable bot ID. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json index f7d08b75d..b2b686dcc 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json @@ -9,8 +9,8 @@ } }, "azureBotSku": { - "defaultValue": "S1", "type": "string", + "defaultValue": "S1", "metadata": { "description": "The pricing tier of the Bot Service Registration. Allowed values are: F0, S1(default)." } @@ -24,15 +24,72 @@ }, "botEndpoint": { "type": "string", + "defaultValue": "", "metadata": { "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "[variables('msiResourceId')]" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "msiResourceId": "[variables('appTypeDef')[parameters('appType')].msiResourceId]" } }, "resources": [ @@ -49,8 +106,11 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppMSIResourceId": "[variables('appType').msiResourceId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json index 9b1c79ae9..23a23b1cc 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json @@ -4,7 +4,6 @@ "parameters": { "appServiceName": { "type": "string", - "defaultValue": "", "metadata": { "description": "The globally unique name of the Web App." } @@ -18,18 +17,21 @@ }, "existingAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } }, "newAppServicePlanName": { "type": "string", + "defaultValue": "", "metadata": { "description": "The name of the new App Service Plan." } }, "newAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -47,6 +49,18 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -59,13 +73,58 @@ "metadata": { "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } } }, "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlanName')), 'createNewAppServicePlan', parameters('existingAppServicePlanName'))]", - "useExistingServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "useExistingServicePlan": "[not(empty(parameters('existingAppServicePlanName')))]", "servicePlanName": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanName'), parameters('newAppServicePlanName'))]", - "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]" + "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -89,13 +148,15 @@ "comments": "Create a Web App using an App Service Plan", "type": "Microsoft.Web/sites", "apiVersion": "2015-08-01", - "name": "[parameters('appServiceName')]", "location": "[variables('servicePlanLocation')]", "kind": "app,linux", "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" + "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" ], + "name": "[parameters('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { + "name": "[parameters('appServiceName')]", "enabled": true, "hostNameSslStates": [ { @@ -124,6 +185,10 @@ "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", "value": "true" }, + { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" @@ -131,6 +196,10 @@ { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -167,7 +236,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -201,7 +270,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json index f18061813..e51036f85 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json @@ -20,8 +20,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json index f3f07b497..de2dba051 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md index d88b160f1..4c752364b 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md @@ -1,31 +1,45 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment sub create --template-file --location --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment sub create --template-file --location --parameters @` # parameters-for-template-BotApp-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **appServiceName**:(required) The location of the App Service Plan. +- **appServicePlanName**:(required) The name of the App Service Plan. +- **appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. +- **appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appServiceName**:(required) The location of the App Service Plan. -**appServicePlanName**:(required) The name of the App Service Plan. -**appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. -**appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. -**azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource \ No newline at end of file diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json index f79264452..63fbf970d 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json @@ -41,11 +41,47 @@ "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]" } }, "resources": [ @@ -85,8 +121,10 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json index b33b21510..381b57abf 100644 --- a/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json +++ b/generators/app/templates/echo/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json @@ -28,6 +28,7 @@ }, "appServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -45,6 +46,25 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -53,16 +73,56 @@ }, "appSecret": { "type": "string", + "defaultValue": "", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." + } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types." + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." } } }, "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", "appServicePlanName": "[parameters('appServicePlanName')]", "resourcesLocation": "[if(empty(parameters('appServicePlanLocation')), parameters('groupLocation'), parameters('appServicePlanLocation'))]", "appServiceName": "[parameters('appServiceName')]", - "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -114,6 +174,7 @@ "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" ], "name": "[variables('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { "name": "[variables('appServiceName')]", "hostNameSslStates": [ @@ -136,12 +197,19 @@ "value": "true" }, { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" }, { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -179,7 +247,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -213,7 +281,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json index cc1800c0d..92b2e84c9 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-AzureBot-with-rg.json @@ -14,8 +14,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json index eea65b7c3..eb36c03fb 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/parameters-for-template-BotApp-with-rg.json @@ -15,7 +15,7 @@ "value": "" }, "newAppServicePlanLocation": { - "value": "" + "value": "West US" }, "newAppServicePlanSku": { "value": { @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md index f8f1d1e56..19d77be80 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/readme.md @@ -1,28 +1,48 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment group create --resource-group --template-file --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment group create --resource-group --template-file --parameters @` # parameters-for-template-BotApp-with-rg: -**appServiceName**:(required) The Name of the Bot App Service. +- **appServiceName**:(required) The Name of the Bot App Service. + +- (choose an existingAppServicePlan or create a new AppServicePlan) + - **existingAppServicePlanName**: The name of the App Service Plan. + - **existingAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanName**: The name of the App Service Plan. + - **newAppServicePlanLocation**: The location of the App Service Plan. + - **newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** + +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. + +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. + +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. + +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. + +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -(choose an existingAppServicePlan or create a new AppServicePlan) -**existingAppServicePlanName**: The name of the App Service Plan. -**existingAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanName**: The name of the App Service Plan. -**newAppServicePlanLocation**: The location of the App Service Plan. -**newAppServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-with-rg: -**azureBotId**:(required) The globally unique and immutable bot ID. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **azureBotId**:(required) The globally unique and immutable bot ID. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. + +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json index f7d08b75d..b2b686dcc 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-AzureBot-with-rg.json @@ -9,8 +9,8 @@ } }, "azureBotSku": { - "defaultValue": "S1", "type": "string", + "defaultValue": "S1", "metadata": { "description": "The pricing tier of the Bot Service Registration. Allowed values are: F0, S1(default)." } @@ -24,15 +24,72 @@ }, "botEndpoint": { "type": "string", + "defaultValue": "", "metadata": { "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "[variables('msiResourceId')]" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "msiResourceId": "[variables('appTypeDef')[parameters('appType')].msiResourceId]" } }, "resources": [ @@ -49,8 +106,11 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppMSIResourceId": "[variables('appType').msiResourceId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json index 9b1c79ae9..979ec221b 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployUseExistResourceGroup/template-BotApp-with-rg.json @@ -4,7 +4,6 @@ "parameters": { "appServiceName": { "type": "string", - "defaultValue": "", "metadata": { "description": "The globally unique name of the Web App." } @@ -18,18 +17,21 @@ }, "existingAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } }, "newAppServicePlanName": { "type": "string", + "defaultValue": "", "metadata": { "description": "The name of the new App Service Plan." } }, "newAppServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -47,6 +49,18 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -59,13 +73,58 @@ "metadata": { "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." + } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } } }, "variables": { - "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlanName')), 'createNewAppServicePlan', parameters('existingAppServicePlanName'))]", - "useExistingServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "useExistingServicePlan": "[not(empty(parameters('existingAppServicePlanName')))]", "servicePlanName": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanName'), parameters('newAppServicePlanName'))]", - "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]" + "servicePlanLocation": "[if(variables('useExistingServicePlan'), parameters('existingAppServicePlanLocation'), parameters('newAppServicePlanLocation'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -89,13 +148,15 @@ "comments": "Create a Web App using an App Service Plan", "type": "Microsoft.Web/sites", "apiVersion": "2015-08-01", - "name": "[parameters('appServiceName')]", "location": "[variables('servicePlanLocation')]", "kind": "app,linux", "dependsOn": [ "[resourceId('Microsoft.Web/serverfarms', variables('servicePlanName'))]" ], + "name": "[parameters('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { + "name": "[parameters('appServiceName')]", "enabled": true, "hostNameSslStates": [ { @@ -124,6 +185,10 @@ "name": "SCM_DO_BUILD_DURING_DEPLOYMENT", "value": "true" }, + { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" @@ -131,6 +196,10 @@ { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -167,7 +236,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -201,7 +270,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json index f18061813..e51036f85 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-AzureBot-new-rg.json @@ -20,8 +20,20 @@ "botEndpoint": { "value": "" }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" + }, + "tenantId": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json index f3f07b497..de2dba051 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/parameters-for-template-BotApp-new-rg.json @@ -26,11 +26,23 @@ "capacity": 1 } }, + "appType": { + "value": "MultiTenant" + }, "appId": { "value": "" }, "appSecret": { "value": "" + }, + "tenantId": { + "value": "" + }, + "UMSIName": { + "value": "" + }, + "UMSIResourceGroupName": { + "value": "" } } } \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md index d88b160f1..4c752364b 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/readme.md @@ -1,31 +1,45 @@ -Need deploy BotAppService before AzureBot ---- -az login -az deployment sub create --template-file --location --parameters @ ---- +# Usage +BotApp must be deployed prior to AzureBot. + +### Command line: +`az login`
+`az deployment sub create --template-file --location --parameters @` # parameters-for-template-BotApp-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **appServiceName**:(required) The location of the App Service Plan. +- **appServicePlanName**:(required) The name of the App Service Plan. +- **appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. +- **appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. -**appServiceName**:(required) The location of the App Service Plan. -**appServicePlanName**:(required) The name of the App Service Plan. -**appServicePlanLocation**: The location of the App Service Plan. Defaults to use groupLocation. -**appServicePlanSku**: The SKU of the App Service Plan. Defaults to Standard values. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . + +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. -**appSecret**:(required for MultiTenant and SingleTenant) Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. # parameters-for-template-AzureBot-new-rg: -**groupName**:(required) Specifies the name of the new Resource Group. -**groupLocation**:(required) Specifies the location of the new Resource Group. +- **groupName**:(required) Specifies the name of the new Resource Group. +- **groupLocation**:(required) Specifies the location of the new Resource Group. + +- **azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. +- **azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. +- **azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. +- **botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. -**azureBotId**:(required) The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable. -**azureBotSku**: The pricing tier of the Bot Service Registration. **Allowed values are: F0, S1(default)**. -**azureBotRegion**: Specifies the location of the new AzureBot. **Allowed values are: global(default), westeurope**. -**botEndpoint**: Use to handle client messages, Such as https://.azurewebsites.net/api/messages. +- **appType**: Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. **Allowed values are: MultiTenant(default), SingleTenant, UserAssignedMSI.** +- **appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. +- **UMSIName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource used for the Bot's Authentication. +- **UMSIResourceGroupName**:(required for UserAssignedMSI) The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. +- **tenantId**: The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to . -**appId**:(required) Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings. \ No newline at end of file +MoreInfo: https://docs.microsoft.com/en-us/azure/bot-service/tutorial-provision-a-bot?view=azure-bot-service-4.0&tabs=userassigned%2Cnewgroup#create-an-identity-resource \ No newline at end of file diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json index f79264452..63fbf970d 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-AzureBot-new-rg.json @@ -41,11 +41,47 @@ "description": "Use to handle client messages, Such as https://.azurewebsites.net/api/messages." } }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { "description": "Active Directory App ID or User-Assigned Managed Identity Client ID, set as MicrosoftAppId in the Web App's Application Settings." } + }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + } + }, + "variables": { + "botEndpoint": "[if(empty(parameters('botEndpoint')), concat('https://', parameters('azureBotId'), '.azurewebsites.net/api/messages'), parameters('botEndpoint'))]", + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "msiResourceId": "" + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "msiResourceId": "" + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]" } }, "resources": [ @@ -85,8 +121,10 @@ "name": "[parameters('azureBotId')]", "displayName": "[parameters('azureBotId')]", "iconUrl": "https://docs.botframework.com/static/devportal/client/images/bot-framework-default.png", - "endpoint": "[parameters('botEndpoint')]", + "endpoint": "[variables('botEndpoint')]", "msaAppId": "[parameters('appId')]", + "msaAppTenantId": "[variables('appType').tenantId]", + "msaAppType": "[parameters('appType')]", "luisAppIds": [], "schemaTransformationVersion": "1.3", "isCmekEnabled": false, diff --git a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json index b33b21510..381b57abf 100644 --- a/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json +++ b/generators/app/templates/empty/{{cookiecutter.bot_name}}/deploymentTemplates/deployWithNewResourceGroup/template-BotApp-new-rg.json @@ -28,6 +28,7 @@ }, "appServicePlanLocation": { "type": "string", + "defaultValue": "", "metadata": { "description": "The location of the App Service Plan." } @@ -45,6 +46,25 @@ "description": "The SKU of the App Service Plan. Defaults to Standard values." } }, + "tenantId": { + "type": "string", + "defaultValue": "[subscription().tenantId]", + "metadata": { + "description": "The Azure AD Tenant ID to use as part of the Bot's Authentication. Only used for SingleTenant and UserAssignedMSI app types. Defaults to \"Subscription Tenant ID\"." + } + }, + "appType": { + "type": "string", + "defaultValue": "MultiTenant", + "allowedValues": [ + "MultiTenant", + "SingleTenant", + "UserAssignedMSI" + ], + "metadata": { + "description": "Type of Bot Authentication. set as MicrosoftAppType in the Web App's Application Settings. Allowed values are: MultiTenant, SingleTenant, UserAssignedMSI. Defaults to \"MultiTenant\"." + } + }, "appId": { "type": "string", "metadata": { @@ -53,16 +73,56 @@ }, "appSecret": { "type": "string", + "defaultValue": "", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types. Defaults to \"\"." + } + }, + "UMSIName": { + "type": "string", + "defaultValue": "", "metadata": { - "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Required for MultiTenant and SingleTenant app types." + "description": "The User-Assigned Managed Identity Resource used for the Bot's Authentication. Defaults to \"\"." + } + }, + "UMSIResourceGroupName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The User-Assigned Managed Identity Resource Group used for the Bot's Authentication. Defaults to \"\"." } } }, "variables": { + "tenantId": "[if(empty(parameters('tenantId')), subscription().tenantId, parameters('tenantId'))]", "appServicePlanName": "[parameters('appServicePlanName')]", "resourcesLocation": "[if(empty(parameters('appServicePlanLocation')), parameters('groupLocation'), parameters('appServicePlanLocation'))]", "appServiceName": "[parameters('appServiceName')]", - "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]" + "resourceGroupId": "[concat(subscription().id, '/resourceGroups/', parameters('groupName'))]", + "msiResourceId": "[if(empty(parameters('UMSIName')), '', concat(subscription().id, '/resourceGroups/', parameters('UMSIResourceGroupName'), '/providers/', 'Microsoft.ManagedIdentity/userAssignedIdentities/', parameters('UMSIName')))]", + "appTypeDef": { + "MultiTenant": { + "tenantId": "", + "identity": { "type": "None" } + }, + "SingleTenant": { + "tenantId": "[variables('tenantId')]", + "identity": { "type": "None" } + }, + "UserAssignedMSI": { + "tenantId": "[variables('tenantId')]", + "identity": { + "type": "UserAssigned", + "userAssignedIdentities": { + "[variables('msiResourceId')]": {} + } + } + } + }, + "appType": { + "tenantId": "[variables('appTypeDef')[parameters('appType')].tenantId]", + "identity": "[variables('appTypeDef')[parameters('appType')].identity]" + } }, "resources": [ { @@ -114,6 +174,7 @@ "[concat(variables('resourceGroupId'), '/providers/Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" ], "name": "[variables('appServiceName')]", + "identity": "[variables('appType').identity]", "properties": { "name": "[variables('appServiceName')]", "hostNameSslStates": [ @@ -136,12 +197,19 @@ "value": "true" }, { + "name": "MicrosoftAppType", + "value": "[parameters('appType')]" + }, { "name": "MicrosoftAppId", "value": "[parameters('appId')]" }, { "name": "MicrosoftAppPassword", "value": "[parameters('appSecret')]" + }, + { + "name": "MicrosoftAppTenantId", + "value": "[variables('appType').tenantId]" } ], "cors": { @@ -179,7 +247,7 @@ "phpVersion": "", "pythonVersion": "", "nodeVersion": "", - "linuxFxVersion": "PYTHON|3.7", + "linuxFxVersion": "PYTHON|3.9", "requestTracingEnabled": false, "remoteDebuggingEnabled": false, "remoteDebuggingVersion": "VS2017", @@ -213,7 +281,7 @@ "autoHealEnabled": false, "vnetName": "", "minTlsVersion": "1.2", - "ftpsState": "AllAllowed", + "ftpsState": "Disabled", "reservedInstanceCount": 0 } } diff --git a/libraries/botbuilder-adapters-slack/requirements.txt b/libraries/botbuilder-adapters-slack/requirements.txt index 98af56627..50f1af767 100644 --- a/libraries/botbuilder-adapters-slack/requirements.txt +++ b/libraries/botbuilder-adapters-slack/requirements.txt @@ -1,4 +1,4 @@ -aiohttp==3.10.5 +aiohttp pyslack botbuilder-core==4.17.0 slackclient diff --git a/libraries/botbuilder-ai/setup.py b/libraries/botbuilder-ai/setup.py index e842fdc34..10bc3ee5c 100644 --- a/libraries/botbuilder-ai/setup.py +++ b/libraries/botbuilder-ai/setup.py @@ -8,7 +8,7 @@ "azure-cognitiveservices-language-luis==0.2.0", "botbuilder-schema==4.17.0", "botbuilder-core==4.17.0", - "aiohttp==3.10.5", + "aiohttp>=3.10,<4.0", ] TESTS_REQUIRES = ["aiounittest>=1.1.0"] diff --git a/libraries/botbuilder-core/botbuilder/core/__init__.py b/libraries/botbuilder-core/botbuilder/core/__init__.py index c5c038353..0769d9100 100644 --- a/libraries/botbuilder-core/botbuilder/core/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/__init__.py @@ -48,6 +48,7 @@ from .user_state import UserState from .register_class_middleware import RegisterClassMiddleware from .adapter_extensions import AdapterExtensions +from .serializer_helper import serializer_helper __all__ = [ "ActivityHandler", @@ -100,5 +101,6 @@ "TurnContext", "UserState", "UserTokenProvider", + "serializer_helper", "__version__", ] diff --git a/libraries/botbuilder-core/botbuilder/core/activity_handler.py b/libraries/botbuilder-core/botbuilder/core/activity_handler.py index be847739e..4dbf04f0b 100644 --- a/libraries/botbuilder-core/botbuilder/core/activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/activity_handler.py @@ -68,6 +68,10 @@ async def on_turn( if turn_context.activity.type == ActivityTypes.message: await self.on_message_activity(turn_context) + elif turn_context.activity.type == ActivityTypes.message_update: + await self.on_message_update_activity(turn_context) + elif turn_context.activity.type == ActivityTypes.message_delete: + await self.on_message_delete_activity(turn_context) elif turn_context.activity.type == ActivityTypes.conversation_update: await self.on_conversation_update_activity(turn_context) elif turn_context.activity.type == ActivityTypes.message_reaction: @@ -107,6 +111,34 @@ async def on_message_activity( # pylint: disable=unused-argument """ return + async def on_message_update_activity( # pylint: disable=unused-argument + self, turn_context: TurnContext + ): + """ + Override this method in a derived class to provide logic specific to activities, + such as the conversational logic. + + :param turn_context: The context object for this turn + :type turn_context: :class:`botbuilder.core.TurnContext` + + :returns: A task that represents the work queued to execute + """ + return + + async def on_message_delete_activity( # pylint: disable=unused-argument + self, turn_context: TurnContext + ): + """ + Override this method in a derived class to provide logic specific to activities, + such as the conversational logic. + + :param turn_context: The context object for this turn + :type turn_context: :class:`botbuilder.core.TurnContext` + + :returns: A task that represents the work queued to execute + """ + return + async def on_conversation_update_activity(self, turn_context: TurnContext): """ Invoked when a conversation update activity is received from the channel when the base behavior of @@ -446,12 +478,19 @@ async def on_invoke_activity( # pylint: disable=unused-argument if ( turn_context.activity.name == SignInConstants.verify_state_operation_name - or turn_context.activity.name - == SignInConstants.token_exchange_operation_name ): await self.on_sign_in_invoke(turn_context) return self._create_invoke_response() + # This is for back-compat with previous versions of Python SDK. This method does not + # exist in the C# SDK, and is not used in the Python SDK. + if ( + turn_context.activity.name + == SignInConstants.token_exchange_operation_name + ): + await self.on_teams_signin_token_exchange(turn_context) + return self._create_invoke_response() + if turn_context.activity.name == "adaptiveCard/action": invoke_value = self._get_adaptive_card_invoke_value( turn_context.activity diff --git a/libraries/botbuilder-core/botbuilder/core/teams/__init__.py b/libraries/botbuilder-core/botbuilder/core/teams/__init__.py index 9d3c4d43d..7e1f1eede 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/__init__.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/__init__.py @@ -9,6 +9,7 @@ from .teams_info import TeamsInfo from .teams_activity_extensions import ( teams_get_channel_id, + teams_get_selected_channel_id, teams_get_team_info, teams_notify_user, ) @@ -19,6 +20,7 @@ "TeamsInfo", "TeamsSSOTokenExchangeMiddleware", "teams_get_channel_id", + "teams_get_selected_channel_id", "teams_get_team_info", "teams_notify_user", ] diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py index 7b9c2fd0a..253b31f5c 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_extensions.py @@ -1,12 +1,14 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from typing import List from botbuilder.schema import Activity from botbuilder.schema.teams import ( NotificationInfo, TeamsChannelData, TeamInfo, TeamsMeetingInfo, + OnBehalfOf, ) @@ -31,6 +33,23 @@ def teams_get_channel_id(activity: Activity) -> str: return None +def teams_get_selected_channel_id(activity: Activity) -> str: + if not activity: + return None + + if activity.channel_data: + channel_data = TeamsChannelData().deserialize(activity.channel_data) + return ( + channel_data.settings.selected_channel.id + if channel_data + and channel_data.settings + and channel_data.settings.selected_channel + else None + ) + + return None + + def teams_get_team_info(activity: Activity) -> TeamInfo: if not activity: return None @@ -52,7 +71,7 @@ def teams_notify_user( activity.channel_data = {} channel_data = TeamsChannelData().deserialize(activity.channel_data) - channel_data.notification = NotificationInfo(alert=True) + channel_data.notification = NotificationInfo(alert=not alert_in_meeting) channel_data.notification.alert_in_meeting = alert_in_meeting channel_data.notification.external_resource_url = external_resource_url activity.channel_data = channel_data @@ -67,3 +86,14 @@ def teams_get_meeting_info(activity: Activity) -> TeamsMeetingInfo: return channel_data.meeting return None + + +def teams_get_team_on_behalf_of(activity: Activity) -> List[OnBehalfOf]: + if not activity: + return None + + if activity.channel_data: + channel_data = TeamsChannelData().deserialize(activity.channel_data) + return channel_data.on_behalf_of + + return None diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py index 33b4e419c..2cc2e6fb6 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_activity_handler.py @@ -28,6 +28,7 @@ TabRequest, TabSubmit, MeetingParticipantsEventDetails, + ReadReceiptInfo, ) from botframework.connector import Channels from ..serializer_helper import deserializer_helper @@ -55,13 +56,6 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: ): return await self.on_teams_card_action_invoke(turn_context) - if ( - turn_context.activity.name - == SignInConstants.token_exchange_operation_name - ): - await self.on_teams_signin_token_exchange(turn_context) - return self._create_invoke_response() - if turn_context.activity.name == "fileConsent/invoke": return await self.on_teams_file_consent( turn_context, @@ -89,6 +83,16 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: ) ) + if turn_context.activity.name == "composeExtension/anonymousQueryLink": + return self._create_invoke_response( + await self.on_teams_anonymous_app_based_link_query( + turn_context, + deserializer_helper( + AppBasedLinkQuery, turn_context.activity.value + ), + ) + ) + if turn_context.activity.name == "composeExtension/query": return self._create_invoke_response( await self.on_teams_messaging_extension_query( @@ -185,6 +189,22 @@ async def on_invoke_activity(self, turn_context: TurnContext) -> InvokeResponse: ) ) + if turn_context.activity.name == "config/fetch": + return self._create_invoke_response( + await self.on_teams_config_fetch( + turn_context, + turn_context.activity.value, + ) + ) + + if turn_context.activity.name == "config/submit": + return self._create_invoke_response( + await self.on_teams_config_submit( + turn_context, + turn_context.activity.value, + ) + ) + return await super().on_invoke_activity(turn_context) except _InvokeResponseException as invoke_exception: @@ -223,7 +243,9 @@ async def on_teams_signin_verify_state(self, turn_context: TurnContext): raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) async def on_teams_signin_token_exchange(self, turn_context: TurnContext): - raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + # This is for back-compat with previous versions of Python SDK. This method does not + # exist in the C# SDK, and is not used in the Python SDK. + return await self.on_teams_signin_verify_state(turn_context) async def on_teams_file_consent( self, @@ -314,6 +336,19 @@ async def on_teams_app_based_link_query( # pylint: disable=unused-argument """ raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_teams_anonymous_app_based_link_query( # pylint: disable=unused-argument + self, turn_context: TurnContext, query: AppBasedLinkQuery + ) -> MessagingExtensionResponse: + """ + Invoked when an anonymous app based link query activity is received from the connector. + + :param turn_context: A context object for this turn. + :param query: The invoke request body type for app-based link query. + + :returns: The Messaging Extension Response for the query. + """ + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_teams_messaging_extension_query( # pylint: disable=unused-argument self, turn_context: TurnContext, query: MessagingExtensionQuery ) -> MessagingExtensionResponse: @@ -514,6 +549,32 @@ async def on_teams_tab_submit( # pylint: disable=unused-argument """ raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_teams_config_fetch( # pylint: disable=unused-argument + self, turn_context: TurnContext, config_data: any + ): + """ + Override this in a derived class to provide logic for when a config is fetched. + + :param turn_context: A context object for this turn. + :param config_data: The config fetch invoke request value payload. + + :returns: A Config Response for the request. + """ + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + + async def on_teams_config_submit( # pylint: disable=unused-argument + self, turn_context: TurnContext, config_data: any + ): + """ + Override this in a derived class to provide logic for when a config is submitted. + + :param turn_context: A context object for this turn. + :param config_data: The config fetch invoke request value payload. + + :returns: A Config Response for the request. + """ + raise _InvokeResponseException(status_code=HTTPStatus.NOT_IMPLEMENTED) + async def on_conversation_update_activity(self, turn_context: TurnContext): """ Invoked when a conversation update activity is received from the channel. @@ -906,6 +967,10 @@ async def on_event_activity(self, turn_context: TurnContext): the scope of a channel. """ if turn_context.activity.channel_id == Channels.ms_teams: + if turn_context.activity.name == "application/vnd.microsoft.readReceipt": + return await self.on_teams_read_receipt_event( + turn_context.activity.value, turn_context + ) if turn_context.activity.name == "application/vnd.microsoft.meetingStart": return await self.on_teams_meeting_start_event( turn_context.activity.value, turn_context @@ -931,6 +996,19 @@ async def on_event_activity(self, turn_context: TurnContext): return await super().on_event_activity(turn_context) + async def on_teams_read_receipt_event( + self, read_receipt_info: ReadReceiptInfo, turn_context: TurnContext + ): # pylint: disable=unused-argument + """ + Override this in a derived class to provide logic for when the bot receives a read receipt event. + + :param read_receipt_info: Information regarding the read receipt. i.e. Id of the message last read by the user. + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return + async def on_teams_meeting_start_event( self, meeting: MeetingStartEventDetails, turn_context: TurnContext ): # pylint: disable=unused-argument @@ -982,3 +1060,73 @@ async def on_teams_meeting_participants_leave_event( :returns: A task that represents the work queued to execute. """ return + + async def on_message_update_activity(self, turn_context: TurnContext): + """ + Invoked when a message update activity is received, such as a message edit or undelete. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + if turn_context.activity.channel_id == Channels.ms_teams: + channel_data = TeamsChannelData().deserialize( + turn_context.activity.channel_data + ) + + if channel_data: + if channel_data.event_type == "editMessage": + return await self.on_teams_message_edit(turn_context) + if channel_data.event_type == "undeleteMessage": + return await self.on_teams_message_undelete(turn_context) + + return await super().on_message_update_activity(turn_context) + + async def on_message_delete_activity(self, turn_context: TurnContext): + """ + Invoked when a message delete activity is received, such as a soft delete message. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + if turn_context.activity.channel_id == Channels.ms_teams: + channel_data = TeamsChannelData().deserialize( + turn_context.activity.channel_data + ) + + if channel_data: + if channel_data.event_type == "softDeleteMessage": + return await self.on_teams_message_soft_delete(turn_context) + + return await super().on_message_delete_activity(turn_context) + + async def on_teams_message_edit(self, turn_context: TurnContext): + """ + Invoked when a Teams edit message event activity is received. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return + + async def on_teams_message_undelete(self, turn_context: TurnContext): + """ + Invoked when a Teams undo soft delete message event activity is received. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return + + async def on_teams_message_soft_delete(self, turn_context: TurnContext): + """ + Invoked when a Teams soft delete message event activity is received. + + :param turn_context: A context object for this turn. + + :returns: A task that represents the work queued to execute. + """ + return diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py index f70f6cccc..4afa50c05 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_info.py @@ -25,6 +25,8 @@ TeamsChannelAccount, TeamsPagedMembersResult, TeamsMeetingParticipant, + MeetingNotificationBase, + MeetingNotificationResponse, ) @@ -100,6 +102,31 @@ async def _legacy_send_message_to_teams_channel( ) return (result[0], result[1]) + @staticmethod + async def send_meeting_notification( + turn_context: TurnContext, + notification: MeetingNotificationBase, + meeting_id: str = None, + ) -> MeetingNotificationResponse: + meeting_id = ( + meeting_id + if meeting_id + else teams_get_meeting_info(turn_context.activity).id + ) + if meeting_id is None: + raise TypeError( + "TeamsInfo._send_meeting_notification: method requires a meeting_id or " + "TurnContext that contains a meeting id" + ) + + if notification is None: + raise TypeError("notification is required.") + + connector_client = await TeamsInfo.get_teams_connector_client(turn_context) + return await connector_client.teams.send_meeting_notification( + meeting_id, notification + ) + @staticmethod async def _create_conversation_callback( new_turn_context, diff --git a/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py b/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py index 1dec1210a..5a6fa5de6 100644 --- a/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py +++ b/libraries/botbuilder-core/botbuilder/core/teams/teams_sso_token_exchange_middleware.py @@ -26,6 +26,7 @@ StoreItem, TurnContext, ) +from botframework.connector.auth.user_token_client import UserTokenClient class _TokenStoreItem(StoreItem): @@ -147,17 +148,29 @@ async def _exchanged_token(self, turn_context: TurnContext) -> bool: token_exchange_response: TokenResponse = None aux_dict = {} if turn_context.activity.value: - for prop in ["id", "connection_name", "token", "properties"]: + for prop in ["id", "connectionName", "token", "properties"]: aux_dict[prop] = turn_context.activity.value.get(prop) token_exchange_request = TokenExchangeInvokeRequest( id=aux_dict["id"], - connection_name=aux_dict["connection_name"], + connection_name=aux_dict["connectionName"], token=aux_dict["token"], properties=aux_dict["properties"], ) try: adapter = turn_context.adapter - if isinstance(turn_context.adapter, ExtendedUserTokenProvider): + + user_token_client: UserTokenClient = turn_context.turn_state.get( + UserTokenClient.__name__, None + ) + if user_token_client: + # If the adapter has UserTokenClient, use it to exchange the token. + token_exchange_response = await user_token_client.exchange_token( + turn_context.activity.from_property.id, + token_exchange_request.connection_name, + turn_context.activity.channel_id, + TokenExchangeRequest(token=token_exchange_request.token), + ) + elif isinstance(turn_context.adapter, ExtendedUserTokenProvider): token_exchange_response = await adapter.exchange_token( turn_context, self._oauth_connection_name, diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py index 852fd1f31..72e25726c 100644 --- a/libraries/botbuilder-core/botbuilder/core/turn_context.py +++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py @@ -396,9 +396,13 @@ def remove_mention_text(activity: Activity, identifier: str) -> str: mentions = TurnContext.get_mentions(activity) for mention in mentions: if mention.additional_properties["mentioned"]["id"] == identifier: + replace_text = ( + mention.additional_properties.get("text") + or mention.additional_properties.get("mentioned")["name"] + ) mention_name_match = re.match( r"(.*?)<\/at>", - escape(mention.additional_properties["text"]), + escape(replace_text), re.IGNORECASE, ) if mention_name_match: diff --git a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py index 390df6191..257dc75f9 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_activity_handler.py @@ -5,6 +5,10 @@ from typing import List import aiounittest +import sys +import os + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from botbuilder.core import BotAdapter, TurnContext from botbuilder.core.teams import TeamsActivityHandler from botbuilder.schema import ( @@ -33,6 +37,8 @@ TabSubmit, TabContext, MeetingParticipantsEventDetails, + ReadReceiptInfo, + TeamsChannelData, ) from botframework.connector import Channels from simple_adapter import SimpleAdapter @@ -218,6 +224,14 @@ async def on_teams_messaging_extension_query( self.record.append("on_teams_messaging_extension_query") return await super().on_teams_messaging_extension_query(turn_context, query) + async def on_teams_anonymous_app_based_link_query( + self, turn_context: TurnContext, query: AppBasedLinkQuery + ): + self.record.append("on_teams_anonymous_app_based_link_query") + return await super().on_teams_anonymous_app_based_link_query( + turn_context, query + ) + async def on_teams_messaging_extension_submit_action_dispatch( self, turn_context: TurnContext, action: MessagingExtensionAction ): @@ -314,10 +328,26 @@ async def on_teams_tab_submit( self.record.append("on_teams_tab_submit") return await super().on_teams_tab_submit(turn_context, tab_submit) + async def on_teams_config_fetch(self, turn_context: TurnContext, config_data: any): + self.record.append("on_teams_config_fetch") + return await super().on_teams_config_fetch(turn_context, config_data) + + async def on_teams_config_submit(self, turn_context: TurnContext, config_data: any): + self.record.append("on_teams_config_submit") + return await super().on_teams_config_submit(turn_context, config_data) + async def on_event_activity(self, turn_context: TurnContext): self.record.append("on_event_activity") return await super().on_event_activity(turn_context) + async def on_teams_read_receipt_event( + self, read_receipt_info: ReadReceiptInfo, turn_context: TurnContext + ): + self.record.append("on_teams_read_receipt_event") + return await super().on_teams_read_receipt_event( + turn_context.activity.value, turn_context + ) + async def on_teams_meeting_start_event( self, meeting: MeetingStartEventDetails, turn_context: TurnContext ): @@ -334,6 +364,26 @@ async def on_teams_meeting_end_event( turn_context.activity.value, turn_context ) + async def on_message_update_activity(self, turn_context: TurnContext): + self.record.append("on_message_update_activity") + return await super().on_message_update_activity(turn_context) + + async def on_teams_message_edit(self, turn_context: TurnContext): + self.record.append("on_teams_message_edit") + return await super().on_teams_message_edit(turn_context) + + async def on_teams_message_undelete(self, turn_context: TurnContext): + self.record.append("on_teams_message_undelete") + return await super().on_teams_message_undelete(turn_context) + + async def on_message_delete_activity(self, turn_context: TurnContext): + self.record.append("on_message_delete_activity") + return await super().on_message_delete_activity(turn_context) + + async def on_teams_message_soft_delete(self, turn_context: TurnContext): + self.record.append("on_teams_message_soft_delete") + return await super().on_teams_message_soft_delete(turn_context) + async def on_teams_meeting_participants_join_event( self, meeting: MeetingParticipantsEventDetails, turn_context: TurnContext ): @@ -799,6 +849,25 @@ async def test_on_app_based_link_query(self): assert bot.record[0] == "on_invoke_activity" assert bot.record[1] == "on_teams_messaging_extension_query" + async def test_compose_extension_anonymous_query_link(self): + # arrange + activity = Activity( + type=ActivityTypes.invoke, + name="composeExtension/anonymousQueryLink", + value={"url": "http://www.test.com"}, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_anonymous_app_based_link_query" + async def test_on_teams_messaging_extension_bot_message_preview_edit_activity(self): # Arrange @@ -1117,6 +1186,50 @@ async def test_on_teams_tab_submit(self): assert bot.record[0] == "on_invoke_activity" assert bot.record[1] == "on_teams_tab_submit" + async def test_on_teams_config_fetch(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="config/fetch", + value={ + "data": {"key": "value", "type": "config/fetch"}, + "context": {"theme": "default"}, + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_config_fetch" + + async def test_on_teams_config_submit(self): + # Arrange + activity = Activity( + type=ActivityTypes.invoke, + name="config/submit", + value={ + "data": {"key": "value", "type": "config/submit"}, + "context": {"theme": "default"}, + }, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + assert len(bot.record) == 2 + assert bot.record[0] == "on_invoke_activity" + assert bot.record[1] == "on_teams_config_submit" + async def test_on_end_of_conversation_activity(self): activity = Activity(type=ActivityTypes.end_of_conversation) @@ -1141,6 +1254,24 @@ async def test_typing_activity(self): assert len(bot.record) == 1 assert bot.record[0] == "on_typing_activity" + async def test_on_teams_read_receipt_event(self): + activity = Activity( + type=ActivityTypes.event, + name="application/vnd.microsoft.readReceipt", + channel_id=Channels.ms_teams, + value={"lastReadMessageId": "10101010"}, + ) + + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + assert len(bot.record) == 2 + assert bot.record[0] == "on_event_activity" + assert bot.record[1] == "on_teams_read_receipt_event" + async def test_on_teams_meeting_start_event(self): activity = Activity( type=ActivityTypes.event, @@ -1175,6 +1306,124 @@ async def test_on_teams_meeting_end_event(self): assert bot.record[0] == "on_event_activity" assert bot.record[1] == "on_teams_meeting_end_event" + async def test_message_update_activity_teams_message_edit(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_update, + channel_data=TeamsChannelData(event_type="editMessage"), + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(2, len(bot.record)) + self.assertEqual("on_message_update_activity", bot.record[0]) + self.assertEqual("on_teams_message_edit", bot.record[1]) + + async def test_message_update_activity_teams_message_undelete(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_update, + channel_data=TeamsChannelData(event_type="undeleteMessage"), + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(2, len(bot.record)) + self.assertEqual("on_message_update_activity", bot.record[0]) + self.assertEqual("on_teams_message_undelete", bot.record[1]) + + async def test_message_update_activity_teams_message_undelete_no_msteams(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_update, + channel_data=TeamsChannelData(event_type="undeleteMessage"), + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(1, len(bot.record)) + self.assertEqual("on_message_update_activity", bot.record[0]) + + async def test_message_update_activity_teams_no_channel_data(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_update, + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(1, len(bot.record)) + self.assertEqual("on_message_update_activity", bot.record[0]) + + async def test_message_delete_activity_teams_message_soft_delete(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_delete, + channel_data=TeamsChannelData(event_type="softDeleteMessage"), + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(2, len(bot.record)) + self.assertEqual("on_message_delete_activity", bot.record[0]) + self.assertEqual("on_teams_message_soft_delete", bot.record[1]) + + async def test_message_delete_activity_teams_message_soft_delete_no_msteams(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_delete, + channel_data=TeamsChannelData(event_type="softDeleteMessage"), + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(1, len(bot.record)) + self.assertEqual("on_message_delete_activity", bot.record[0]) + + async def test_message_delete_activity_teams_no_channel_data(self): + # Arrange + activity = Activity( + type=ActivityTypes.message_delete, + channel_id=Channels.ms_teams, + ) + turn_context = TurnContext(SimpleAdapter(), activity) + + # Act + bot = TestingTeamsActivityHandler() + await bot.on_turn(turn_context) + + # Assert + self.assertEqual(1, len(bot.record)) + self.assertEqual("on_message_delete_activity", bot.record[0]) + async def test_on_teams_meeting_participants_join_event(self): # arrange activity = Activity( diff --git a/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py b/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py index e468526bc..324749ce5 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_channel_data.py @@ -1,11 +1,21 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from uuid import uuid4 import aiounittest from botbuilder.schema import Activity from botbuilder.schema.teams import TeamsChannelData from botbuilder.core.teams import teams_get_team_info +from botbuilder.schema.teams._models_py3 import ( + ChannelInfo, + NotificationInfo, + OnBehalfOf, + TeamInfo, + TeamsChannelDataSettings, + TeamsMeetingInfo, + TenantInfo, +) class TestTeamsChannelData(aiounittest.AsyncTestCase): @@ -28,3 +38,49 @@ def test_teams_get_team_info(self): # Assert assert team_info.aad_group_id == "teamGroup123" + + def test_teams_channel_data_inits(self): + # Arrange + channel = ChannelInfo(id="general", name="General") + event_type = "eventType" + team = TeamInfo(id="supportEngineers", name="Support Engineers") + notification = NotificationInfo(alert=True) + tenant = TenantInfo(id="uniqueTenantId") + meeting = TeamsMeetingInfo(id="BFSE Stand Up") + settings = TeamsChannelDataSettings(selected_channel=channel) + on_behalf_of = [ + OnBehalfOf( + display_name="onBehalfOfTest", + item_id=0, + mention_type="person", + mri=str(uuid4()), + ) + ] + + # Act + channel_data = TeamsChannelData( + channel=channel, + event_type=event_type, + team=team, + notification=notification, + tenant=tenant, + meeting=meeting, + settings=settings, + on_behalf_of=on_behalf_of, + ) + + # Assert + self.assertIsNotNone(channel_data) + self.assertIsInstance(channel_data, TeamsChannelData) + self.assertEqual(channel, channel_data.channel) + self.assertEqual(event_type, channel_data.event_type) + self.assertEqual(team, channel_data.team) + self.assertEqual(notification, channel_data.notification) + self.assertEqual(tenant, channel_data.tenant) + self.assertEqual(meeting, channel_data.meeting) + self.assertEqual(settings, channel_data.settings) + self.assertEqual(on_behalf_of, channel_data.on_behalf_of) + self.assertEqual(on_behalf_of[0].display_name, "onBehalfOfTest") + self.assertEqual(on_behalf_of[0].mention_type, "person") + self.assertIsNotNone(on_behalf_of[0].mri) + self.assertEqual(on_behalf_of[0].item_id, 0) diff --git a/libraries/botbuilder-core/tests/teams/test_teams_extension.py b/libraries/botbuilder-core/tests/teams/test_teams_extension.py index 98c1ee829..406d3cb39 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_extension.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_extension.py @@ -1,16 +1,22 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from uuid import uuid4 import aiounittest from botbuilder.schema import Activity from botbuilder.schema.teams import TeamInfo from botbuilder.core.teams import ( teams_get_channel_id, + teams_get_selected_channel_id, teams_get_team_info, teams_notify_user, ) -from botbuilder.core.teams.teams_activity_extensions import teams_get_meeting_info +from botbuilder.core.teams.teams_activity_extensions import ( + teams_get_meeting_info, + teams_get_team_on_behalf_of, +) +from botbuilder.schema.teams._models_py3 import OnBehalfOf class TestTeamsActivityHandler(aiounittest.AsyncTestCase): @@ -26,6 +32,35 @@ def test_teams_get_channel_id(self): # Assert assert result == "id123" + def test_teams_get_selected_channel_id(self): + # Arrange + activity = Activity( + channel_data={ + "channel": {"id": "id123", "name": "channel_name"}, + "settings": { + "selectedChannel": {"id": "id12345", "name": "channel_name"} + }, + } + ) + + # Act + result = teams_get_selected_channel_id(activity) + + # Assert + assert result == "id12345" + + def test_teams_get_selected_channel_id_with_no_selected_channel(self): + # Arrange + activity = Activity( + channel_data={"channel": {"id": "id123", "name": "channel_name"}} + ) + + # Act + result = teams_get_selected_channel_id(activity) + + # Assert + assert result is None + def test_teams_get_channel_id_with_no_channel(self): # Arrange activity = Activity( @@ -120,6 +155,17 @@ def test_teams_notify_user(self): # Assert assert activity.channel_data.notification.alert + def test_teams_notify_user_alert_in_meeting(self): + # Arrange + activity = Activity() + + # Act + teams_notify_user(activity, alert_in_meeting=True) + + # Assert + assert activity.channel_data.notification.alert_in_meeting is True + assert activity.channel_data.notification.alert is False + def test_teams_notify_user_with_no_activity(self): # Arrange activity = None @@ -160,3 +206,23 @@ def test_teams_meeting_info(self): # Assert assert meeting_id == "meeting123" + + def test_teams_channel_data_existing_on_behalf_of(self): + # Arrange + on_behalf_of_list = [ + OnBehalfOf( + display_name="onBehalfOfTest", + item_id=0, + mention_type="person", + mri=str(uuid4()), + ) + ] + + activity = Activity(channel_data={"onBehalfOf": on_behalf_of_list}) + + # Act + on_behalf_of_list = teams_get_team_on_behalf_of(activity) + + # Assert + self.assertEqual(1, len(on_behalf_of_list)) + self.assertEqual("onBehalfOfTest", on_behalf_of_list[0].display_name) diff --git a/libraries/botbuilder-core/tests/teams/test_teams_info.py b/libraries/botbuilder-core/tests/teams/test_teams_info.py index dea57030c..00f4ad8a4 100644 --- a/libraries/botbuilder-core/tests/teams/test_teams_info.py +++ b/libraries/botbuilder-core/tests/teams/test_teams_info.py @@ -1,7 +1,19 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json import aiounittest +from botbuilder.schema.teams._models_py3 import ( + ContentType, + MeetingNotificationChannelData, + MeetingStageSurface, + MeetingTabIconSurface, + OnBehalfOf, + TargetedMeetingNotification, + TargetedMeetingNotificationValue, + TaskModuleContinueResponse, + TaskModuleTaskInfo, +) from botframework.connector import Channels from botbuilder.core import TurnContext, MessageFactory @@ -234,6 +246,53 @@ async def test_get_meeting_info(self): handler = TeamsActivityHandler() await handler.on_turn(turn_context) + async def test_send_meeting_notificationt(self): + test_cases = [ + ("202", "accepted"), + ( + "207", + "if the notifications are sent only to parital number of recipients\ + because the validation on some recipients' ids failed or some\ + recipients were not found in the roster. In this case, \ + SMBA will return the user MRIs of those failed recipients\ + in a format that was given to a bot (ex: if a bot sent \ + encrypted user MRIs, return encrypted one).", + ), + ( + "400", + "when Meeting Notification request payload validation fails. For instance,\ + Recipients: # of recipients is greater than what the API allows ||\ + all of recipients' user ids were invalid, Surface: Surface list\ + is empty or null, Surface type is invalid, Duplicative \ + surface type exists in one payload", + ), + ( + "403", + "if the bot is not allowed to send the notification. In this case,\ + the payload should contain more detail error message. \ + There can be many reasons: bot disabled by tenant admin,\ + blocked during live site mitigation, the bot does not\ + have a correct RSC permission for a specific surface type, etc", + ), + ] + for status_code, expected_message in test_cases: + adapter = SimpleAdapterWithCreateConversation() + + activity = Activity( + type="targetedMeetingNotification", + text="Test-send_meeting_notificationt", + channel_id=Channels.ms_teams, + from_property=ChannelAccount( + aad_object_id="participantId-1", name=status_code + ), + service_url="https://test.coffee", + conversation=ConversationAccount(id="conversation-id"), + ) + + turn_context = TurnContext(adapter, activity) + handler = TeamsActivityHandler() + await handler.on_turn(turn_context) + class TestTeamsActivityHandler(TeamsActivityHandler): async def on_turn(self, turn_context: TurnContext): @@ -241,6 +300,8 @@ async def on_turn(self, turn_context: TurnContext): if turn_context.activity.text == "test_send_message_to_teams_channel": await self.call_send_message_to_teams(turn_context) + elif turn_context.activity.text == "test_send_meeting_notification": + await self.call_send_meeting_notification(turn_context) async def call_send_message_to_teams(self, turn_context: TurnContext): msg = MessageFactory.text("call_send_message_to_teams") @@ -251,3 +312,71 @@ async def call_send_message_to_teams(self, turn_context: TurnContext): assert reference[0].activity_id == "new_conversation_id" assert reference[1] == "reference123" + + async def call_send_meeting_notification(self, turn_context: TurnContext): + from_property = turn_context.activity.from_property + try: + # Send the meeting notification asynchronously + failed_participants = await TeamsInfo.send_meeting_notification( + turn_context, + self.get_targeted_meeting_notification(from_property), + "meeting-id", + ) + + # Handle based on the 'from_property.name' + if from_property.name == "207": + self.assertEqual( + "failingid", + failed_participants.recipients_failure_info[0].recipient_mri, + ) + elif from_property.name == "202": + assert failed_participants is None + else: + raise TypeError( + f"Expected HttpOperationException with response status code {from_property.name}." + ) + + except ValueError as ex: + # Assert that the response status code matches the from_property.name + assert from_property.name == str(int(ex.response.status_code)) + + # Deserialize the error response content to an ErrorResponse object + error_response = json.loads(ex.response.content) + + # Handle based on error codes + if from_property.name == "400": + assert error_response["error"]["code"] == "BadSyntax" + elif from_property.name == "403": + assert error_response["error"]["code"] == "BotNotInConversationRoster" + else: + raise TypeError( + f"Expected HttpOperationException with response status code {from_property.name}." + ) + + def get_targeted_meeting_notification(self, from_account: ChannelAccount): + recipients = [from_account.id] + + if from_account.name == "207": + recipients.append("failingid") + + meeting_stage_surface = MeetingStageSurface( + content=TaskModuleContinueResponse( + value=TaskModuleTaskInfo(title="title here", height=3, width=2) + ), + content_type=ContentType.Task, + ) + + meeting_tab_icon_surface = MeetingTabIconSurface( + tab_entity_id="test tab entity id" + ) + + value = TargetedMeetingNotificationValue( + recipients=recipients, + surfaces=[meeting_stage_surface, meeting_tab_icon_surface], + ) + + obo = OnBehalfOf(display_name=from_account.name, mri=from_account.id) + + channel_data = MeetingNotificationChannelData(on_behalf_of_list=[obo]) + + return TargetedMeetingNotification(value=value, channel_data=channel_data) diff --git a/libraries/botbuilder-core/tests/test_activity_handler.py b/libraries/botbuilder-core/tests/test_activity_handler.py index fedc03e96..1ee0c5414 100644 --- a/libraries/botbuilder-core/tests/test_activity_handler.py +++ b/libraries/botbuilder-core/tests/test_activity_handler.py @@ -26,6 +26,14 @@ async def on_message_activity(self, turn_context: TurnContext): self.record.append("on_message_activity") return await super().on_message_activity(turn_context) + async def on_message_update_activity(self, turn_context: TurnContext): + self.record.append("on_message_update_activity") + return await super().on_message_update_activity(turn_context) + + async def on_message_delete_activity(self, turn_context: TurnContext): + self.record.append("on_message_delete_activity") + return await super().on_message_delete_activity(turn_context) + async def on_members_added_activity( self, members_added: ChannelAccount, turn_context: TurnContext ): @@ -208,6 +216,32 @@ async def test_invoke_should_not_match(self): assert bot.record[0] == "on_invoke_activity" assert adapter.activity.value.status == int(HTTPStatus.NOT_IMPLEMENTED) + async def test_on_message_update_activity(self): + activity = Activity(type=ActivityTypes.message_update) + + adapter = TestInvokeAdapter() + turn_context = TurnContext(adapter, activity) + + # Act + bot = TestingActivityHandler() + await bot.on_turn(turn_context) + + assert len(bot.record) == 1 + assert bot.record[0] == "on_message_update_activity" + + async def test_on_message_delete_activity(self): + activity = Activity(type=ActivityTypes.message_delete) + + adapter = TestInvokeAdapter() + turn_context = TurnContext(adapter, activity) + + # Act + bot = TestingActivityHandler() + await bot.on_turn(turn_context) + + assert len(bot.record) == 1 + assert bot.record[0] == "on_message_delete_activity" + async def test_on_end_of_conversation_activity(self): activity = Activity(type=ActivityTypes.end_of_conversation) diff --git a/libraries/botbuilder-core/tests/test_turn_context.py b/libraries/botbuilder-core/tests/test_turn_context.py index 473580ef0..7247caab9 100644 --- a/libraries/botbuilder-core/tests/test_turn_context.py +++ b/libraries/botbuilder-core/tests/test_turn_context.py @@ -350,6 +350,48 @@ def test_should_remove_at_mention_with_regex_characters(self): assert text == " test activity" assert activity.text == " test activity" + def test_should_remove_custom_mention_from_activity(self): + activity = Activity( + text="Hallo", + text_format="plain", + type="message", + timestamp="2025-03-11T14:16:47.0093935Z", + id="1741702606984", + channel_id="msteams", + service_url="https://smba.trafficmanager.net/emea/REDACTED/", + from_property=ChannelAccount( + id="29:1J-K4xVh-sLpdwQ-R5GkOZ_TB0W3ec_37p710aH8qe8bITA0zxdgIGc9l-MdDdkdE_jasSfNOeWXyyL1nsrHtBQ", + name="", + aad_object_id="REDACTED", + ), + conversation=ConversationAccount( + is_group=True, + conversation_type="groupChat", + tenant_id="REDACTED", + id="19:Ql86tXNM2lTBXNKJdqKdwIF9ltGZwpvluLvnJdA0tmg1@thread.v2", + ), + recipient=ChannelAccount( + id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", name="Azure AI Agent" + ), + entities=[ + Entity().deserialize( + Mention( + type="mention", + mentioned=ChannelAccount( + id="28:c5d5fb56-a1a4-4467-a7a3-1b37905498a0", + name="Custom Agent", + ), + ).serialize() + ) + ], + channel_data={"tenant": {"id": "REDACTED"}, "productContext": "COPILOT"}, + ) + + text = TurnContext.remove_mention_text(activity, activity.recipient.id) + + assert text == "Hallo" + assert activity.text == "Hallo" + async def test_should_send_a_trace_activity(self): context = TurnContext(SimpleAdapter(), ACTIVITY) called = False diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/_user_token_access.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/_user_token_access.py index 0aa005789..ba25a0baa 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/_user_token_access.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/_user_token_access.py @@ -104,6 +104,7 @@ async def exchange_token( channel_id = turn_context.activity.channel_id return await user_token_client.exchange_token( user_id, + settings.connection_name, channel_id, token_exchange_request, ) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py index 43cfe3052..f07a8afa5 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog.py @@ -125,7 +125,7 @@ async def on_dialog_event( # Bubble as needed if (not handled) and dialog_event.bubble and dialog_context.parent: - handled = await dialog_context.parent.emit( + handled = await dialog_context.parent.emit_event( dialog_event.name, dialog_event.value, True, False ) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py index d31a0b56a..270d4f324 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/prompts/oauth_prompt.py @@ -341,6 +341,13 @@ async def _send_oauth_card( if sign_in_resource.token_exchange_resource else None ) + + json_token_ex_post = ( + sign_in_resource.token_post_resource.as_dict() + if sign_in_resource.token_post_resource + else None + ) + prompt.attachments.append( CardFactory.oauth_card( OAuthCard( @@ -355,6 +362,7 @@ async def _send_oauth_card( ) ], token_exchange_resource=json_token_ex_resource, + token_post_resource=json_token_ex_post, ) ) ) diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py index e5be50c24..d848c13c7 100644 --- a/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py +++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/skills/skill_dialog.py @@ -26,6 +26,8 @@ from .begin_skill_dialog_options import BeginSkillDialogOptions from .skill_dialog_options import SkillDialogOptions +from botbuilder.dialogs.prompts import OAuthPromptSettings +from .._user_token_access import _UserTokenAccess class SkillDialog(Dialog): @@ -275,50 +277,55 @@ async def _intercept_oauth_cards( """ Tells is if we should intercept the OAuthCard message. """ - if not connection_name or not isinstance( - context.adapter, ExtendedUserTokenProvider - ): + if not connection_name or connection_name.isspace(): # The adapter may choose not to support token exchange, in which case we fallback to # showing an oauth card to the user. return False oauth_card_attachment = next( - attachment - for attachment in activity.attachments - if attachment.content_type == ContentTypes.oauth_card + ( + attachment + for attachment in activity.attachments + if attachment.content_type == ContentTypes.oauth_card + ), + None, ) - if oauth_card_attachment: - oauth_card = oauth_card_attachment.content - if ( - oauth_card - and oauth_card.token_exchange_resource - and oauth_card.token_exchange_resource.uri - ): - try: - result = await context.adapter.exchange_token( - turn_context=context, - connection_name=connection_name, - user_id=context.activity.from_property.id, - exchange_request=TokenExchangeRequest( - uri=oauth_card.token_exchange_resource.uri - ), - ) + if oauth_card_attachment is None: + return False - if result and result.token: - # If token above is null, then SSO has failed and hence we return false. - # If not, send an invoke to the skill with the token. - return await self._send_token_exchange_invoke_to_skill( - activity, - oauth_card.token_exchange_resource.id, - oauth_card.connection_name, - result.token, - ) - except: - # Failures in token exchange are not fatal. They simply mean that the user needs - # to be shown the OAuth card. - return False - - return False + oauth_card = oauth_card_attachment.content + if ( + not oauth_card + or not oauth_card.token_exchange_resource + or not oauth_card.token_exchange_resource.uri + ): + return False + + try: + settings = OAuthPromptSettings( + connection_name=connection_name, title="Sign In" + ) + result = await _UserTokenAccess.exchange_token( + context, + settings, + TokenExchangeRequest(uri=oauth_card.token_exchange_resource.uri), + ) + + if not result or not result.token: + # If token above is null, then SSO has failed and hence we return false. + return False + + # If not, send an invoke to the skill with the token. + return await self._send_token_exchange_invoke_to_skill( + activity, + oauth_card.token_exchange_resource.id, + oauth_card.connection_name, + result.token, + ) + except: + # Failures in token exchange are not fatal. They simply mean that the user needs + # to be shown the OAuth card. + return False async def _send_token_exchange_invoke_to_skill( self, diff --git a/libraries/botbuilder-dialogs/tests/choices/test_choice_recognizers.py b/libraries/botbuilder-dialogs/tests/choices/test_choice_recognizers.py index c37243fd1..ac202d044 100644 --- a/libraries/botbuilder-dialogs/tests/choices/test_choice_recognizers.py +++ b/libraries/botbuilder-dialogs/tests/choices/test_choice_recognizers.py @@ -57,10 +57,9 @@ def assert_choice(result, value, index, score, synonym=None): resolution.score == score ), f"Invalid resolution.score of '{resolution.score}' for '{value}' choice." if synonym: - assert ( # pylint: disable=assert-on-tuple - resolution.synonym == synonym, - f"Invalid resolution.synonym of '{resolution.synonym}' for '{value}' choice.", - ) + assert ( + resolution.synonym == synonym + ), f"Invalid resolution.synonym of '{resolution.synonym}' for '{value}' choice." _color_choices: List[str] = ["red", "green", "blue"] diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py index 0f9131871..576c5125c 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/cloud_adapter.py @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +import json + from typing import Awaitable, Callable, Optional from aiohttp.web import ( @@ -17,6 +19,7 @@ Bot, CloudAdapterBase, InvokeResponse, + serializer_helper, TurnContext, ) from botbuilder.core.streaming import ( @@ -102,7 +105,8 @@ async def process( # Write the response, serializing the InvokeResponse if invoke_response: return json_response( - data=invoke_response.body, status=invoke_response.status + data=serializer_helper(invoke_response.body), + status=invoke_response.status, ) return Response(status=201) else: diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index de0f13750..d66ba0327 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -1,4 +1,4 @@ msrest== 0.7.* botframework-connector==4.17.0 botbuilder-schema==4.17.0 -aiohttp==3.10.5 +aiohttp==3.*.* diff --git a/libraries/botbuilder-integration-aiohttp/setup.py b/libraries/botbuilder-integration-aiohttp/setup.py index 1fe5d5ccc..2624c9dc8 100644 --- a/libraries/botbuilder-integration-aiohttp/setup.py +++ b/libraries/botbuilder-integration-aiohttp/setup.py @@ -10,7 +10,7 @@ "botframework-connector==4.17.0", "botbuilder-core==4.17.0", "yarl>=1.8.1", - "aiohttp==3.10.5", + "aiohttp>=3.10,<4.0", ] root = os.path.abspath(os.path.dirname(__file__)) diff --git a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py index 30c45b2f7..78c32e5eb 100644 --- a/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py +++ b/libraries/botbuilder-integration-applicationinsights-aiohttp/setup.py @@ -6,7 +6,7 @@ REQUIRES = [ "applicationinsights>=0.11.9", - "aiohttp==3.10.5", + "aiohttp>=3.10,<4.0", "botbuilder-schema==4.17.0", "botframework-connector==4.17.0", "botbuilder-core==4.17.0", diff --git a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py index 1b6a631c6..e7dd1f789 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/_models_py3.py @@ -1909,6 +1909,7 @@ class OAuthCard(Model): "connection_name": {"key": "connectionName", "type": "str"}, "buttons": {"key": "buttons", "type": "[CardAction]"}, "token_exchange_resource": {"key": "tokenExchangeResource", "type": "object"}, + "token_post_resource": {"key": "tokenPostResource", "type": "object"}, } def __init__( @@ -1918,6 +1919,7 @@ def __init__( connection_name: str = None, buttons=None, token_exchange_resource=None, + token_post_resource=None, **kwargs ) -> None: super(OAuthCard, self).__init__(**kwargs) @@ -1925,6 +1927,7 @@ def __init__( self.connection_name = connection_name self.buttons = buttons self.token_exchange_resource = token_exchange_resource + self.token_post_resource = token_post_resource class PagedMembersResult(Model): diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py index 55901f7a4..be9aa11ce 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/__init__.py @@ -59,6 +59,7 @@ from ._models_py3 import TeamDetails from ._models_py3 import TeamInfo from ._models_py3 import TeamsChannelAccount +from ._models_py3 import TeamsChannelDataSettings from ._models_py3 import TeamsChannelData from ._models_py3 import TeamsPagedMembersResult from ._models_py3 import TenantInfo @@ -80,6 +81,14 @@ from ._models_py3 import UserMeetingDetails from ._models_py3 import TeamsMeetingMember from ._models_py3 import MeetingParticipantsEventDetails +from ._models_py3 import ReadReceiptInfo +from ._models_py3 import BotConfigAuth +from ._models_py3 import ConfigAuthResponse +from ._models_py3 import ConfigResponse +from ._models_py3 import ConfigTaskResponse +from ._models_py3 import MeetingNotificationBase +from ._models_py3 import MeetingNotificationResponse +from ._models_py3 import OnBehalfOf __all__ = [ "AppBasedLinkQuery", @@ -140,6 +149,7 @@ "TeamDetails", "TeamInfo", "TeamsChannelAccount", + "TeamsChannelDataSettings", "TeamsChannelData", "TeamsPagedMembersResult", "TenantInfo", @@ -161,4 +171,12 @@ "UserMeetingDetails", "TeamsMeetingMember", "MeetingParticipantsEventDetails", + "ReadReceiptInfo", + "BotConfigAuth", + "ConfigAuthResponse", + "ConfigResponse", + "ConfigTaskResponse", + "MeetingNotificationBase", + "MeetingNotificationResponse", + "OnBehalfOf", ] diff --git a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py index 260442cf8..0b6e0e899 100644 --- a/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py +++ b/libraries/botbuilder-schema/botbuilder/schema/teams/_models_py3.py @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. +from enum import Enum from typing import List from msrest.serialization import Model from botbuilder.schema import ( @@ -87,17 +88,23 @@ class ChannelInfo(Model): :type id: str :param name: Name of the channel :type name: str + :param type: The channel type + :type type: str """ _attribute_map = { "id": {"key": "id", "type": "str"}, "name": {"key": "name", "type": "str"}, + "type": {"key": "type", "type": "str"}, } - def __init__(self, *, id: str = None, name: str = None, **kwargs) -> None: + def __init__( + self, *, id: str = None, name: str = None, type: str = None, **kwargs + ) -> None: super(ChannelInfo, self).__init__(**kwargs) self.id = id self.name = name + self.type = type class CacheInfo(Model): @@ -1820,6 +1827,8 @@ class TeamDetails(Model): :type channel_count: int :param member_count: The count of members in the team. :type member_count: int + :param type: The team type + :type type: str """ _attribute_map = { @@ -1828,6 +1837,7 @@ class TeamDetails(Model): "aad_group_id": {"key": "aadGroupId", "type": "str"}, "channel_count": {"key": "channelCount", "type": "int"}, "member_count": {"key": "memberCount", "type": "int"}, + "type": {"key": "type", "type": "str"}, } def __init__( @@ -1838,6 +1848,7 @@ def __init__( aad_group_id: str = None, member_count: int = None, channel_count: int = None, + type: str = None, **kwargs ) -> None: super(TeamDetails, self).__init__(**kwargs) @@ -1846,6 +1857,7 @@ def __init__( self.aad_group_id = aad_group_id self.channel_count = channel_count self.member_count = member_count + self.type = type class TeamInfo(Model): @@ -1958,6 +1970,26 @@ def __init__( self.members = members +class TeamsChannelDataSettings(Model): + """ + Represents the settings information for a Teams channel data. + + :param selected_channel: Information about the selected Teams channel. + :type selected_channel: ~botframework.connector.teams.models.ChannelInfo + :param additional_properties: Gets or sets properties that are not otherwise defined by the + type but that might appear in the REST JSON object. + :type additional_properties: object + """ + + _attribute_map = { + "selected_channel": {"key": "selectedChannel", "type": "ChannelInfo"}, + } + + def __init__(self, *, selected_channel=None, **kwargs) -> None: + super(TeamsChannelDataSettings, self).__init__(**kwargs) + self.selected_channel = selected_channel + + class TeamsChannelData(Model): """Channel data specific to messages received in Microsoft Teams. @@ -1974,6 +2006,10 @@ class TeamsChannelData(Model): :type tenant: ~botframework.connector.teams.models.TenantInfo :param meeting: Information about the meeting in which the message was sent :type meeting: ~botframework.connector.teams.models.TeamsMeetingInfo + :param settings: Information about the about the settings in which the message was sent + :type settings: ~botframework.connector.teams.models.TeamsChannelDataSettings + :param on_behalf_of: The OnBehalfOf list for user attribution + :type on_behalf_of: list[~botframework.connector.teams.models.OnBehalfOf] """ _attribute_map = { @@ -1983,6 +2019,8 @@ class TeamsChannelData(Model): "notification": {"key": "notification", "type": "NotificationInfo"}, "tenant": {"key": "tenant", "type": "TenantInfo"}, "meeting": {"key": "meeting", "type": "TeamsMeetingInfo"}, + "settings": {"key": "settings", "type": "TeamsChannelDataSettings"}, + "on_behalf_of": {"key": "onBehalfOf", "type": "[OnBehalfOf]"}, } def __init__( @@ -1994,6 +2032,8 @@ def __init__( notification=None, tenant=None, meeting=None, + settings: TeamsChannelDataSettings = None, + on_behalf_of: List["OnBehalfOf"] = None, **kwargs ) -> None: super(TeamsChannelData, self).__init__(**kwargs) @@ -2004,6 +2044,8 @@ def __init__( self.notification = notification self.tenant = tenant self.meeting = meeting + self.settings = settings + self.on_behalf_of = on_behalf_of if on_behalf_of is not None else [] class TenantInfo(Model): @@ -2568,3 +2610,422 @@ class MeetingParticipantsEventDetails(Model): def __init__(self, *, members: List[TeamsMeetingMember] = None, **kwargs) -> None: super(MeetingParticipantsEventDetails, self).__init__(**kwargs) self.members = members + + +class ReadReceiptInfo(Model): + """General information about a read receipt. + + :param last_read_message_id: The id of the last read message. + :type last_read_message_id: str + """ + + _attribute_map = { + "last_read_message_id": {"key": "lastReadMessageId", "type": "str"}, + } + + def __init__(self, *, last_read_message_id: str = None, **kwargs) -> None: + super(ReadReceiptInfo, self).__init__(**kwargs) + self.last_read_message_id = last_read_message_id + + @staticmethod + def is_message_read(compare_message_id, last_read_message_id): + """ + Helper method useful for determining if a message has been read. + This method converts the strings to integers. If the compare_message_id is + less than or equal to the last_read_message_id, then the message has been read. + + :param compare_message_id: The id of the message to compare. + :param last_read_message_id: The id of the last message read by the user. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + if not compare_message_id or not last_read_message_id: + return False + + try: + compare_message_id_long = int(compare_message_id) + last_read_message_id_long = int(last_read_message_id) + except ValueError: + return False + + return compare_message_id_long <= last_read_message_id_long + + def is_message_read_instance(self, compare_message_id): + """ + Helper method useful for determining if a message has been read. + If the compare_message_id is less than or equal to the last_read_message_id, + then the message has been read. + + :param compare_message_id: The id of the message to compare. + :return: True if the compare_message_id is less than or equal to the last_read_message_id. + """ + return ReadReceiptInfo.is_message_read( + compare_message_id, self.last_read_message_id + ) + + +class BotConfigAuth(Model): + """Specifies bot config auth, including type and suggestedActions. + + :param type: The type of bot config auth. + :type type: str + :param suggested_actions: The suggested actions of bot config auth. + :type suggested_actions: ~botframework.connector.models.SuggestedActions + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + "suggested_actions": {"key": "suggestedActions", "type": "SuggestedActions"}, + } + + def __init__(self, *, type: str = "auth", suggested_actions=None, **kwargs) -> None: + super(BotConfigAuth, self).__init__(**kwargs) + self.type = type + self.suggested_actions = suggested_actions + + +class ConfigResponseBase(Model): + """Specifies Invoke response base, including response type. + + :param response_type: Response type for invoke request + :type response_type: str + """ + + _attribute_map = { + "response_type": {"key": "responseType", "type": "str"}, + } + + def __init__(self, *, response_type: str = None, **kwargs) -> None: + super(ConfigResponseBase, self).__init__(**kwargs) + self.response_type = response_type + + +class ConfigResponse(ConfigResponseBase): + """Envelope for Config Response Payload. + + :param config: The response to the config message. Possible values: 'auth', 'task' + :type config: T + :param cache_info: Response cache info + :type cache_info: ~botframework.connector.teams.models.CacheInfo + """ + + _attribute_map = { + "config": {"key": "config", "type": "object"}, + "cache_info": {"key": "cacheInfo", "type": "CacheInfo"}, + } + + def __init__(self, *, config=None, cache_info=None, **kwargs) -> None: + super(ConfigResponse, self).__init__(response_type="config", **kwargs) + self.config = config + self.cache_info = cache_info + + +class ConfigTaskResponse(ConfigResponse): + """Envelope for Config Task Response. + + This class uses TaskModuleResponseBase as the type for the config parameter. + """ + + def __init__(self, *, config=None, **kwargs) -> None: + super(ConfigTaskResponse, self).__init__( + config=config or TaskModuleResponseBase(), **kwargs + ) + + +class ConfigAuthResponse(ConfigResponse): + """Envelope for Config Auth Response. + + This class uses BotConfigAuth as the type for the config parameter. + """ + + def __init__(self, *, config=None, **kwargs) -> None: + super(ConfigAuthResponse, self).__init__( + config=config or BotConfigAuth(), **kwargs + ) + + +class OnBehalfOf(Model): + """Specifies attribution for notifications. + + :param item_id: The identification of the item. Default is 0. + :type item_id: int + :param mention_type: The mention type. Default is "person". + :type mention_type: str + :param mri: Message resource identifier (MRI) of the person on whose behalf the message is sent. + :type mri: str + :param display_name: Name of the person. Used as fallback in case name resolution is unavailable. + :type display_name: str + """ + + _attribute_map = { + "item_id": {"key": "itemid", "type": "int"}, + "mention_type": {"key": "mentionType", "type": "str"}, + "mri": {"key": "mri", "type": "str"}, + "display_name": {"key": "displayName", "type": "str"}, + } + + def __init__( + self, + *, + item_id: int = 0, + mention_type: str = "person", + mri: str = None, + display_name: str = None, + **kwargs + ) -> None: + super(OnBehalfOf, self).__init__(**kwargs) + self.item_id = item_id + self.mention_type = mention_type + self.mri = mri + self.display_name = display_name + + +class SurfaceType(Enum): + """ + Defines Teams Surface type for use with a Surface object. + + :var Unknown: TeamsSurfaceType is Unknown. + :vartype Unknown: int + :var MeetingStage: TeamsSurfaceType is MeetingStage.. + :vartype MeetingStage: int + :var MeetingTabIcon: TeamsSurfaceType is MeetingTabIcon. + :vartype MeetingTabIcon: int + """ + + Unknown = 0 + + MeetingStage = 1 + + MeetingTabIcon = 2 + + +class ContentType(Enum): + """ + Defines content type. Depending on contentType, content field will have a different structure. + + :var Unknown: Content type is Unknown. + :vartype Unknown: int + :var Task: TContent type is Task. + :vartype Task: int + """ + + Unknown = 0 + + Task = 1 + + +class MeetingNotificationBase(Model): + """Specifies Bot meeting notification base including channel data and type. + + :param type: Type of Bot meeting notification. + :type type: str + """ + + _attribute_map = { + "type": {"key": "type", "type": "str"}, + } + + def __init__(self, *, type: str = None, **kwargs) -> None: + super(MeetingNotificationBase, self).__init__(**kwargs) + self.type = type + + +class MeetingNotification(MeetingNotificationBase): + """Specifies Bot meeting notification including meeting notification value. + + :param value: Teams Bot meeting notification value. + :type value: TargetedMeetingNotificationValue + """ + + _attribute_map = { + "value": {"key": "value", "type": "TargetedMeetingNotificationValue"}, + } + + def __init__( + self, *, value: "TargetedMeetingNotificationValue" = None, **kwargs + ) -> None: + super(MeetingNotification, self).__init__(**kwargs) + self.value = value + + +class MeetingNotificationChannelData(Model): + """Specify Teams Bot meeting notification channel data. + + :param on_behalf_of_list: The Teams Bot meeting notification's OnBehalfOf list. + :type on_behalf_of_list: list[~botframework.connector.teams.models.OnBehalfOf] + """ + + _attribute_map = { + "on_behalf_of_list": {"key": "OnBehalfOf", "type": "[OnBehalfOf]"} + } + + def __init__(self, *, on_behalf_of_list: List["OnBehalfOf"] = None, **kwargs): + super(MeetingNotificationChannelData, self).__init__(**kwargs) + self.on_behalf_of_list = on_behalf_of_list + + +class MeetingNotificationRecipientFailureInfo(Model): + """Information regarding failure to notify a recipient of a meeting notification. + + :param recipient_mri: The MRI for a recipient meeting notification failure. + :type recipient_mri: str + :param error_code: The error code for a meeting notification. + :type error_code: str + :param failure_reason: The reason why a participant meeting notification failed. + :type failure_reason: str + """ + + _attribute_map = { + "recipient_mri": {"key": "recipientMri", "type": "str"}, + "error_code": {"key": "errorcode", "type": "str"}, + "failure_reason": {"key": "failureReason", "type": "str"}, + } + + def __init__( + self, + *, + recipient_mri: str = None, + error_code: str = None, + failure_reason: str = None, + **kwargs + ): + super(MeetingNotificationRecipientFailureInfo, self).__init__(**kwargs) + self.recipient_mri = recipient_mri + self.error_code = error_code + self.failure_reason = failure_reason + + +class MeetingNotificationResponse(Model): + """Specifies Bot meeting notification response. + + Contains list of MeetingNotificationRecipientFailureInfo. + + :param recipients_failure_info: The list of MeetingNotificationRecipientFailureInfo. + :type recipients_failure_info: list[~botframework.connector.teams.models.MeetingNotificationRecipientFailureInfo] + """ + + _attribute_map = { + "recipients_failure_info": { + "key": "recipientsFailureInfo", + "type": "[MeetingNotificationRecipientFailureInfo]", + } + } + + def __init__( + self, + *, + recipients_failure_info: List["MeetingNotificationRecipientFailureInfo"] = None, + **kwargs + ): + super(MeetingNotificationResponse, self).__init__(**kwargs) + self.recipients_failure_info = recipients_failure_info + + +class Surface(Model): + """Specifies where the notification will be rendered in the meeting UX. + + :param type: The value indicating where the notification will be rendered in the meeting UX. + :type type: ~botframework.connector.teams.models.SurfaceType + """ + + _attribute_map = { + "type": {"key": "surface", "type": "SurfaceType"}, + } + + def __init__(self, *, type: SurfaceType = None, **kwargs): + super(Surface, self).__init__(**kwargs) + self.type = type + + +class MeetingStageSurface(Surface): + """Specifies meeting stage surface. + + :param content_type: The content type of this MeetingStageSurface. + :type content_type: ~botframework.connector.teams.models.ContentType + :param content: The content of this MeetingStageSurface. + :type content: object + """ + + _attribute_map = { + "content_type": {"key": "contentType", "type": "ContentType"}, + "content": {"key": "content", "type": "object"}, + } + + def __init__( + self, + *, + content_type: ContentType = ContentType.Task, + content: object = None, + **kwargs + ): + super(MeetingStageSurface, self).__init__(SurfaceType.MeetingStage, **kwargs) + self.content_type = content_type + self.content = content + + +class MeetingTabIconSurface(Surface): + """ + Specifies meeting tab icon surface. + + :param tab_entity_id: The tab entity Id of this MeetingTabIconSurface. + :type tab_entity_id: str + """ + + _attribute_map = { + "tab_entity_id": {"key": "tabEntityId", "type": "str"}, + } + + def __init__(self, *, tab_entity_id: str = None, **kwargs): + super(MeetingTabIconSurface, self).__init__( + SurfaceType.MeetingTabIcon, **kwargs + ) + self.tab_entity_id = tab_entity_id + + +class TargetedMeetingNotificationValue(Model): + """Specifies the targeted meeting notification value, including recipients and surfaces. + + :param recipients: The collection of recipients of the targeted meeting notification. + :type recipients: list[str] + :param surfaces: The collection of surfaces on which to show the notification. + :type surfaces: list[~botframework.connector.teams.models.Surface] + """ + + _attribute_map = { + "recipients": {"key": "recipients", "type": "[str]"}, + "surfaces": {"key": "surfaces", "type": "[Surface]"}, + } + + def __init__( + self, *, recipients: List[str] = None, surfaces: List[Surface] = None, **kwargs + ): + super(TargetedMeetingNotificationValue, self).__init__(**kwargs) + self.recipients = recipients + self.surfaces = surfaces + + +class TargetedMeetingNotification(MeetingNotification): + """Specifies Teams targeted meeting notification. + + :param value: The value of the TargetedMeetingNotification. + :type value: ~botframework.connector.teams.models.TargetedMeetingNotificationValue + :param channel_data: Teams Bot meeting notification channel data. + :type channel_data: ~botframework.connector.teams.models.MeetingNotificationChannelData + """ + + _attribute_map = { + "value": {"key": "value", "type": "TargetedMeetingNotificationValue"}, + "channel_data": { + "key": "channelData", + "type": "MeetingNotificationChannelData", + }, + } + + def __init__( + self, + *, + value: "TargetedMeetingNotificationValue" = None, + channel_data: "MeetingNotificationChannelData" = None, + **kwargs + ): + super(TargetedMeetingNotification, self).__init__(value=value, **kwargs) + self.channel_data = channel_data diff --git a/libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py b/libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py new file mode 100644 index 000000000..f6d771c4e --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_bot_config_auth.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import BotConfigAuth + + +class TestBotConfigAuth(aiounittest.AsyncTestCase): + def test_bot_config_auth_inits_with_no_args(self): + bot_config_auth_response = BotConfigAuth() + + self.assertIsNotNone(bot_config_auth_response) + self.assertIsInstance(bot_config_auth_response, BotConfigAuth) + self.assertEqual("auth", bot_config_auth_response.type) diff --git a/libraries/botbuilder-schema/tests/teams/test_config_auth_response.py b/libraries/botbuilder-schema/tests/teams/test_config_auth_response.py new file mode 100644 index 000000000..54221399d --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_config_auth_response.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ConfigAuthResponse + + +class TestConfigAuthResponse(aiounittest.AsyncTestCase): + def test_config_auth_response_init_with_no_args(self): + config_auth_response = ConfigAuthResponse() + + self.assertIsNotNone(config_auth_response) + self.assertIsInstance(config_auth_response, ConfigAuthResponse) + self.assertEqual("config", config_auth_response.response_type) diff --git a/libraries/botbuilder-schema/tests/teams/test_config_response.py b/libraries/botbuilder-schema/tests/teams/test_config_response.py new file mode 100644 index 000000000..39d2ce0d5 --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_config_response.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ConfigResponse + + +class TestConfigResponse(aiounittest.AsyncTestCase): + def test_config_response_inits_with_no_args(self): + config_response = ConfigResponse() + + self.assertIsNotNone(config_response) + self.assertIsInstance(config_response, ConfigResponse) diff --git a/libraries/botbuilder-schema/tests/teams/test_config_task_response.py b/libraries/botbuilder-schema/tests/teams/test_config_task_response.py new file mode 100644 index 000000000..53126388d --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_config_task_response.py @@ -0,0 +1,14 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ConfigTaskResponse + + +class TestConfigTaskResponse(aiounittest.AsyncTestCase): + def test_config_task_response_init_with_no_args(self): + config_task_response = ConfigTaskResponse() + + self.assertIsNotNone(config_task_response) + self.assertIsInstance(config_task_response, ConfigTaskResponse) + self.assertEqual("config", config_task_response.response_type) diff --git a/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py b/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py new file mode 100644 index 000000000..e6aad9bf3 --- /dev/null +++ b/libraries/botbuilder-schema/tests/teams/test_read_receipt_info.py @@ -0,0 +1,28 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import aiounittest +from botbuilder.schema.teams import ReadReceiptInfo + + +class TestReadReceiptInfo(aiounittest.AsyncTestCase): + def test_read_receipt_info(self): + # Arrange + test_cases = [ + ("1000", "1000", True), + ("1001", "1000", True), + ("1000", "1001", False), + ("1000", None, False), + (None, "1000", False), + ] + + for last_read, compare, is_read in test_cases: + # Act + info = ReadReceiptInfo(last_read_message_id=last_read) + + # Assert + self.assertEqual(info.last_read_message_id, last_read) + self.assertEqual(info.is_message_read_instance(compare), is_read) + self.assertEqual( + ReadReceiptInfo.is_message_read(compare, last_read), is_read + ) diff --git a/libraries/botbuilder-testing/setup.py b/libraries/botbuilder-testing/setup.py index 9fed4e3ac..9ee855a41 100644 --- a/libraries/botbuilder-testing/setup.py +++ b/libraries/botbuilder-testing/setup.py @@ -9,7 +9,7 @@ "botbuilder-core==4.17.0", "botbuilder-dialogs==4.17.0", "botbuilder-azure==4.17.0", - "pytest~=7.3.1", + "pytest~=8.3.3", ] TESTS_REQUIRES = ["aiounittest==1.3.0"] diff --git a/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py index 89dbe882d..31e845eb6 100644 --- a/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py +++ b/libraries/botframework-connector/botframework/connector/auth/certificate_app_credentials.py @@ -67,7 +67,17 @@ def get_access_token(self, force_refresh: bool = False) -> str: if not auth_token: # No suitable token exists in cache. Let's get a new one from AAD. auth_token = self.__get_msal_app().acquire_token_for_client(scopes=scopes) - return auth_token["access_token"] + if "access_token" in auth_token: + return auth_token["access_token"] + error = auth_token["error"] if "error" in auth_token else "Unknown error" + error_description = ( + auth_token["error_description"] + if "error_description" in auth_token + else "Unknown error description" + ) + raise PermissionError( + f"Failed to get access token with error: {error}, error_description: {error_description}" + ) def __get_msal_app(self): if not self.app: diff --git a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py index ff1bdb18c..6e453ae23 100644 --- a/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py +++ b/libraries/botframework-connector/botframework/connector/teams/operations/teams_operations.py @@ -266,3 +266,76 @@ def fetch_meeting( return deserialized fetch_participant.metadata = {"url": "/v1/meetings/{meetingId}"} + + def send_meeting_notification( + self, + meeting_id: str, + notification: models.MeetingNotificationBase, + custom_headers=None, + raw=False, + **operation_config + ): + """Send a teams meeting notification. + + :param meeting_id: Meeting Id, encoded as a BASE64 string. + :type meeting_id: str + :param notification: The notification to send to Teams + :type notification: ~botframework.connector.teams.models.MeetingNotificationBase + :param dict custom_headers: headers that will be added to the request + :param bool raw: returns the direct response alongside the + deserialized response + :param operation_config: :ref:`Operation configuration + overrides`. + :return: MeetingNotificationResponse or ClientRawResponse if raw=true + :rtype: ~botframework.connector.teams.models.MeetingNotificationResponse or + ~msrest.pipeline.ClientRawResponse + :raises: + :class:`HttpOperationError` + """ + + # Construct URL + url = self.send_meeting_notification.metadata["url"] + path_format_arguments = { + "meetingId": self._serialize.url("meeting_id", meeting_id, "str"), + } + url = self._client.format_url(url, **path_format_arguments) + + # Construct parameters + query_parameters = {} + + # Construct headers + header_parameters = {} + header_parameters["Accept"] = "application/json" + header_parameters["Content-Type"] = "application/json; charset=utf-8" + if custom_headers: + header_parameters.update(custom_headers) + + # Construct body + body_content = self._serialize.body(notification, "notification") + + # Construct and send request + request = self._client.post( + url, query_parameters, header_parameters, body_content + ) + response = self._client.send(request, stream=False, **operation_config) + + if response.status_code not in [200, 201, 202]: + raise models.ErrorResponseException(self._deserialize, response) + + deserialized = None + if response.status_code == 200: + deserialized = self._deserialize("MeetingNotificationResponse", response) + if response.status_code == 201: + deserialized = self._deserialize("MeetingNotificationResponse", response) + if response.status_code == 202: + deserialized = self._deserialize("MeetingNotificationResponse", response) + + if raw: + client_raw_response = ClientRawResponse(deserialized, response) + return client_raw_response + + return deserialized + + send_meeting_notification.metadata = { + "url": "/v1/meetings/{meetingId}/notification" + } diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py b/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py index f4593e21a..0f1f158da 100644 --- a/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py +++ b/libraries/botframework-connector/botframework/connector/token_api/models/__init__.py @@ -13,6 +13,7 @@ from ._models_py3 import SignInUrlResponse from ._models_py3 import TokenExchangeRequest from ._models_py3 import TokenExchangeResource + from ._models_py3 import TokenPostResource from ._models_py3 import TokenResponse from ._models_py3 import TokenStatus except (SyntaxError, ImportError): @@ -23,6 +24,7 @@ from ._models import SignInUrlResponse from ._models import TokenExchangeRequest from ._models import TokenExchangeResource + from ._models import TokenPostResource from ._models import TokenResponse from ._models import TokenStatus @@ -35,6 +37,7 @@ "SignInUrlResponse", "TokenExchangeRequest", "TokenExchangeResource", + "TokenPostResource", "TokenResponse", "TokenStatus", ] diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/_models.py b/libraries/botframework-connector/botframework/connector/token_api/models/_models.py index 5f69104cd..8b526324a 100644 --- a/libraries/botframework-connector/botframework/connector/token_api/models/_models.py +++ b/libraries/botframework-connector/botframework/connector/token_api/models/_models.py @@ -104,6 +104,9 @@ class SignInUrlResponse(Model): :param token_exchange_resource: :type token_exchange_resource: ~botframework.tokenapi.models.TokenExchangeResource + :param token_post_resource: + :type token_post_resource: + ~botframework.tokenapi.models.TokenPostResource """ _attribute_map = { @@ -112,12 +115,17 @@ class SignInUrlResponse(Model): "key": "tokenExchangeResource", "type": "TokenExchangeResource", }, + "token_post_resource": { + "key": "tokenPostResource", + "type": "TokenPostResource", + }, } def __init__(self, **kwargs): super(SignInUrlResponse, self).__init__(**kwargs) self.sign_in_link = kwargs.get("sign_in_link", None) self.token_exchange_resource = kwargs.get("token_exchange_resource", None) + self.token_exchange_resource = kwargs.get("token_post_resource", None) class TokenExchangeRequest(Model): @@ -164,6 +172,22 @@ def __init__(self, **kwargs): self.provider_id = kwargs.get("provider_id", None) +class TokenPostResource(Model): + """TokenPostResource. + + :param sas_url: + :type id: str + """ + + _attribute_map = { + "sas_url": {"key": "sasUrl", "type": "str"}, + } + + def __init__(self, **kwargs): + super(TokenPostResource, self).__init__(**kwargs) + self.sas_url = kwargs.get("sas_url", None) + + class TokenResponse(Model): """TokenResponse. diff --git a/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py b/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py index 60ab62c92..512e85356 100644 --- a/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py +++ b/libraries/botframework-connector/botframework/connector/token_api/models/_models_py3.py @@ -106,6 +106,9 @@ class SignInUrlResponse(Model): :param token_exchange_resource: :type token_exchange_resource: ~botframework.tokenapi.models.TokenExchangeResource + :param token_post_resource: + :type token_post_resource: + ~botframework.tokenapi.models.TokenPostResource """ _attribute_map = { @@ -114,14 +117,24 @@ class SignInUrlResponse(Model): "key": "tokenExchangeResource", "type": "TokenExchangeResource", }, + "token_post_resource": { + "key": "tokenPostResource", + "type": "TokenPostResource", + }, } def __init__( - self, *, sign_in_link: str = None, token_exchange_resource=None, **kwargs + self, + *, + sign_in_link: str = None, + token_exchange_resource=None, + token_post_resource=None, + **kwargs ) -> None: super(SignInUrlResponse, self).__init__(**kwargs) self.sign_in_link = sign_in_link self.token_exchange_resource = token_exchange_resource + self.token_post_resource = token_post_resource class TokenExchangeRequest(Model): @@ -170,6 +183,22 @@ def __init__( self.provider_id = provider_id +class TokenPostResource(Model): + """TokenPostResource. + + :param sas_url: + :type id: str + """ + + _attribute_map = { + "sas_url": {"key": "sasUrl", "type": "str"}, + } + + def __init__(self, *, sas_url: str = None, **kwargs) -> None: + super(TokenPostResource, self).__init__(**kwargs) + self.sas_url = sas_url + + class TokenResponse(Model): """TokenResponse. diff --git a/libraries/botframework-connector/requirements.txt b/libraries/botframework-connector/requirements.txt index 5a6d8d4e9..515030672 100644 --- a/libraries/botframework-connector/requirements.txt +++ b/libraries/botframework-connector/requirements.txt @@ -3,4 +3,4 @@ botbuilder-schema==4.17.0 requests==2.32.0 PyJWT==2.4.0 cryptography==43.0.1 -msal>=1.29.0 +msal>=1.31.1 diff --git a/libraries/botframework-connector/setup.py b/libraries/botframework-connector/setup.py index 8a99b3e19..1bfc05d49 100644 --- a/libraries/botframework-connector/setup.py +++ b/libraries/botframework-connector/setup.py @@ -11,7 +11,7 @@ # "requests>=2.23.0,<2.26", "PyJWT>=2.4.0", "botbuilder-schema==4.17.0", - "msal>=1.29.0", + "msal>=1.31.1", ] root = os.path.abspath(os.path.dirname(__file__)) diff --git a/libraries/botframework-connector/tests/requirements.txt b/libraries/botframework-connector/tests/requirements.txt index 5f0d9558d..6facda892 100644 --- a/libraries/botframework-connector/tests/requirements.txt +++ b/libraries/botframework-connector/tests/requirements.txt @@ -1,6 +1,6 @@ -pytest-cov>=2.6.0 -pytest~=7.3.1 +pytest-cov>=5.0.0 +pytest~=8.3.3 pyyaml==6.0.1 -pytest-asyncio==0.23.8 +pytest-asyncio==0.24.0 ddt==1.2.1 setuptools==72.1.0 \ No newline at end of file diff --git a/tests/skills/skills-buffered/child/requirements.txt b/tests/skills/skills-buffered/child/requirements.txt index 20f8f8fe5..9e79c8115 100644 --- a/tests/skills/skills-buffered/child/requirements.txt +++ b/tests/skills/skills-buffered/child/requirements.txt @@ -1,2 +1,2 @@ botbuilder-core>=4.7.1 -aiohttp +aiohttp==3.*.* diff --git a/tests/skills/skills-buffered/parent/requirements.txt b/tests/skills/skills-buffered/parent/requirements.txt index 20f8f8fe5..9e79c8115 100644 --- a/tests/skills/skills-buffered/parent/requirements.txt +++ b/tests/skills/skills-buffered/parent/requirements.txt @@ -1,2 +1,2 @@ botbuilder-core>=4.7.1 -aiohttp +aiohttp==3.*.*