From e25c05cf7998cdf54b7665a0eaa9be722546a379 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sat, 8 Jun 2024 16:11:56 +0200 Subject: [PATCH 01/14] update Entra integration cmdlets Based on the latest Microsoft recommendation for the Entra integration (https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/dev-tools/secure-developer-vm#external-integrations), the following cmdlets were updated: - New-D365EntraIntegration - remove `-AddAppRegistrationToWifConfig` switch, this is now always done - clear cached LCS configuration in AxDB - restart IIS - Test-D365EntraIntegration - A failing test for wif.config now results in an error fixed #832 --- .../functions/new-d365entraintegration.ps1 | 87 ++++++++++--------- .../functions/test-d365entraintegration.ps1 | 5 +- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index 7a07cd8f..04906d1b 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -14,12 +14,15 @@ - 2) Install the certificate to the "LocalMachine" certificate store. - 3) Grant NetworkService READ permission to the certificate (only on cloud-hosted environments). - 4) Update the web.config with the application ID and the thumbprint of the certificate. - - 5) (Optional) Add the application registration to the WIF config. + - 5) Add the application registration to the WIF config. + - 6) Clear cached LCS configuration in AxDB. + - 7) Restart the IIS service. To execute the steps, the id of an Azure application must be provided. The application must have the following API permissions: - Dynamics ERP - This permission is required to access finance and operations environments. - Microsoft Graph (User.Read.All and Group.Read.All permissions of the Application type). + - Dynamics Lifecylce service (permission of type Delegated) The URL of the finance and operations environment must also be added to the RedirectURI in the Authentication section of the Azure application. Finally, after running the cmdlet, if a new certificate was created, it must be uploaded to the Azure application. @@ -28,10 +31,10 @@ The Azure Registered Application Id / Client Id obtained while creating a Registered App inside the Azure Portal. It is assumed that an application with this id already exists in Azure. - .Parameter ExistingCertificateFile + .PARAMETER ExistingCertificateFile The path to a certificate file. If this parameter is provided, the cmdlet will not create a new certificate. - .Parameter ExistingCertificatePrivateKeyFile + .PARAMETER ExistingCertificatePrivateKeyFile The path to a certificate private key file. If this parameter is not provided, the certificate can be installed to the certificate store, but the NetworkService cannot be granted READ permission. @@ -55,15 +58,12 @@ .PARAMETER Force Forces the execution of some of the steps. For example, if a certificate with the same name already exists, it will be deleted and recreated. - .Parameter AddAppRegistrationToWifConfig - Adds the application registration to the WIF config. This is not part of the official Microsoft documentation to enable the Entra ID integration. It is however highly recommended to fix additional issues with the missing entry integration. - - .Parameter WhatIf + .PARAMETER WhatIf Executes the cmdlet until the first operation that would change the state of the system, without executing that operation. Subsequent operations are likely to fail. This is currently not fully implemented and should not be used. - .Parameter Confirm + .PARAMETER Confirm Prompts for confirmation before each operation of the cmdlet that changes the state of the system. .OUTPUTS @@ -140,9 +140,7 @@ function New-D365EntraIntegration { [Security.SecureString] $CertificatePassword, - [switch] $Force, - - [switch] $AddAppRegistrationToWifConfig + [switch] $Force ) if (-not ($Script:IsAdminRuntime)) { @@ -335,35 +333,46 @@ function New-D365EntraIntegration { $xml.Save($webConfigFile) Write-PSFMessage -Level Host -Message "web.config was updated with the application ID and the thumbprint of the certificate." - if ($PSCmdlet.ParameterSetName -eq "NewCertificate") { - Write-PSFMessage -Level Host -Message "The certificate file $NewCertificateFile must be uploaded to the Azure application, see https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#add-a-certificate." - } - # Step 5: Add app registration to Wif.config - if ($AddAppRegistrationToWifConfig) { - Write-PSFMessage -Level Verbose -Message "Step 5: Starting adding app registration to Wif.config" - $wifConfigBackup = Join-Path $Script:DefaultTempPath "WifConfigBackup" - $wifConfigFileBackup = Join-Path $wifConfigBackup $Script:WifConfig - if (Test-PathExists -Path $wifConfigFileBackup -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { - Write-PSFMessage -Level Warning -Message "Backup of Wif.config already exists." - if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a backup of Wif.config already exists" - return - } - Write-PSFMessage -Level Host -Message "Backup of Wif.config will be overwritten." - } - $null = Backup-D365WifConfig -Force:$Force - $wifConfigFile = Join-Path -Path $Script:AOSPath $Script:WifConfig - if (-not (Test-PathExists -Path $wifConfigFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { - Write-PSFMessage -Level Host -Message "Unable to find the Wif.config file." - Stop-PSFFunction -Message "Stopping because the Wif.config file could not be found" + Write-PSFMessage -Level Verbose -Message "Step 5: Starting adding app registration to Wif.config" + $wifConfigBackup = Join-Path $Script:DefaultTempPath "WifConfigBackup" + $wifConfigFileBackup = Join-Path $wifConfigBackup $Script:WifConfig + if (Test-PathExists -Path $wifConfigFileBackup -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { + Write-PSFMessage -Level Warning -Message "Backup of Wif.config already exists." + if (-not $Force) { + Stop-PSFFunction -Message "Stopping because a backup of Wif.config already exists" + return } - [xml]$xml = Get-Content $wifConfigFile - $audienceUris = $xml.'system.identityModel'.identityConfiguration.securityTokenHandlers.securityTokenHandlerConfiguration.audienceUris - $audienceUriElement = $xml.CreateElement('add') - $audienceUriElement.SetAttribute('value', "spn:$ClientId") - $audienceUris.PrependChild($audienceUriElement) - $xml.Save($wifConfigFile) - Write-PSFMessage -Level Host -Message "Wif.config was updated with the audience URIs." + Write-PSFMessage -Level Host -Message "Backup of Wif.config will be overwritten." + } + $null = Backup-D365WifConfig -Force:$Force + $wifConfigFile = Join-Path -Path $Script:AOSPath $Script:WifConfig + if (-not (Test-PathExists -Path $wifConfigFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { + Write-PSFMessage -Level Host -Message "Unable to find the Wif.config file." + Stop-PSFFunction -Message "Stopping because the Wif.config file could not be found" + } + [xml]$xml = Get-Content $wifConfigFile + $audienceUris = $xml.'system.identityModel'.identityConfiguration.securityTokenHandlers.securityTokenHandlerConfiguration.audienceUris + $audienceUriElement = $xml.CreateElement('add') + $audienceUriElement.SetAttribute('value', "spn:$ClientId") + $audienceUris.AppendChild($audienceUriElement) + $xml.Save($wifConfigFile) + Write-PSFMessage -Level Host -Message "Wif.config was updated with the audience URIs." + + # Step 6: Clear cached LCS configuration in AxDB + Write-PSFMessage -Level Verbose -Message "Step 6: Starting clearing cached LCS configuration in AxDB" + Invoke-D365SqlScript -Command "DELETE FROM SYSOAUTHCONFIGURATION where SECURERESOURCE = 'https://lcsapi.lcs.dynamics.com'" + Invoke-D365SqlScript -Command "DELETE FROM SYSOAUTHUSERTOKENS where SECURERESOURCE = 'https://lcsapi.lcs.dynamics.com'" + Write-PSFMessage -Level Host -Message "Cached LCS configuration in AxDB was cleared." + + # Step 7: Restart IIS + Write-PSFMessage -Level Verbose -Message "Step 7: Starting restarting IIS" + Restart-D365Environment -Aos + Write-PSFMessage -Level Host -Message "IIS was restarted." + + Test-D365EntraIntegration + + if ($PSCmdlet.ParameterSetName -eq "NewCertificate") { + Write-PSFMessage -Level Host -Message "The certificate file $NewCertificateFile must be uploaded to the Azure application, see https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#add-a-certificate." } } \ No newline at end of file diff --git a/d365fo.tools/functions/test-d365entraintegration.ps1 b/d365fo.tools/functions/test-d365entraintegration.ps1 index 0903fb81..67123d00 100644 --- a/d365fo.tools/functions/test-d365entraintegration.ps1 +++ b/d365fo.tools/functions/test-d365entraintegration.ps1 @@ -88,10 +88,11 @@ function Test-D365EntraIntegration { $config.AudienceUri = $nodes | Where-Object { $_.value -like "*$($config.AadRealm)*" } | Select-Object -First 1 -ExpandProperty value if ([System.String]::IsNullOrWhiteSpace($config.AudienceUri)) { - Write-PSFMessage -Level Host -Message "The 'AudienceUri' value is empty. This may not be needed, but in case of issues, try running the 'New-D365EntraIntegration' cmdlet with the -AddAppRegistrationToWifConfig switch." + Write-PSFMessage -Level Host -Message "The 'AudienceUri' value is empty. This indicates that you have a corrupted configuration. Try running the 'New-D365EntraIntegration' cmdlet to fix the configuration." + Stop-PSFFunction -Message "Stopping because the 'AudienceUri' value is empty" } elseif ($config.AadRealm -ne $config.AudienceUri) { - Write-PSFMessage -Level Host -Message "The 'Aad.Realm' and the 'AudienceUri' value do not match each other. This indicates that you have a corrupted configuration. Running the 'New-D365EntraIntegration' cmdlet with the -AddAppRegistrationToWifConfig switch could assist with fixing the configuration." + Write-PSFMessage -Level Host -Message "The 'Aad.Realm' and the 'AudienceUri' value do not match each other. This indicates that you have a corrupted configuration. Try running the 'New-D365EntraIntegration' cmdlet to fix the configuration." Stop-PSFFunction -Message "Stopping because the 'Aad.Realm' and 'AudienceUri' values do not match" } From 7ecd75b024fb282d1b69040d11c7529b6c89ca19 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 13:45:47 +0200 Subject: [PATCH 02/14] remove obsolete example for -AddAppRegistrationToWifConfig switch --- d365fo.tools/functions/new-d365entraintegration.ps1 | 5 ----- 1 file changed, 5 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index 04906d1b..94b3d89d 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -75,11 +75,6 @@ Enables the Entra ID integration with a new self-signed certificate named "CHEAuth" which expires after 2 years. - .EXAMPLE - PS C:\> New-D365EntraIntegration -ClientId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -AddAppRegistrationToWifConfig - - Enables the Entra ID integration with a new self-signed certificate named "CHEAuth" which expires after 2 years and adds the application registration to the wif.config. - .EXAMPLE PS C:\> New-D365EntraIntegration -ClientId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -CertificateName "SelfsignedCert" From 4420db626221ddb40036c5a713d6e521eba4ffae Mon Sep 17 00:00:00 2001 From: FH-Inway Date: Sun, 9 Jun 2024 11:56:00 +0000 Subject: [PATCH 03/14] =?UTF-8?q?=F0=9F=A4=96=20Fix=20best=20practice=20de?= =?UTF-8?q?viations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request was automatically created by the d365fo.tools-Generate-Text action' --- d365fo.tools/bin/d365fo.tools-index.json | 14 ++----- .../New-D365EntraIntegration.Tests.ps1 | 17 +------- docs/New-D365EntraIntegration.md | 41 +++++-------------- 3 files changed, 15 insertions(+), 57 deletions(-) diff --git a/d365fo.tools/bin/d365fo.tools-index.json b/d365fo.tools/bin/d365fo.tools-index.json index c94b987f..1eb591a2 100644 --- a/d365fo.tools/bin/d365fo.tools-index.json +++ b/d365fo.tools/bin/d365fo.tools-index.json @@ -9597,7 +9597,7 @@ }, { "CommandName": "New-D365EntraIntegration", - "Description": "Enable the Microsoft Entra ID integration by executing some of the steps described in https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/dev-tools/secure-developer-vm#external-integrations.\nThe integration can either be enabled with an existing certificate or a new self-signed certificate can be created.\nIf a new certificate is created and the integration is also to be enabled on other environments with the same certificate, a certificate password must be specified in order to create a certificate private key file.\n\nThe steps executed are:\n\n- 1) Create a self-signed certificate and save it to Desktop or use a provided certificate.\n- 2) Install the certificate to the \"LocalMachine\" certificate store.\n- 3) Grant NetworkService READ permission to the certificate (only on cloud-hosted environments).\n- 4) Update the web.config with the application ID and the thumbprint of the certificate.\n- 5) (Optional) Add the application registration to the WIF config.\n\nTo execute the steps, the id of an Azure application must be provided. The application must have the following API permissions:\n\n- Dynamics ERP - This permission is required to access finance and operations environments.\n- Microsoft Graph (User.Read.All and Group.Read.All permissions of the Application type).\n\nThe URL of the finance and operations environment must also be added to the RedirectURI in the Authentication section of the Azure application.\nFinally, after running the cmdlet, if a new certificate was created, it must be uploaded to the Azure application.", + "Description": "Enable the Microsoft Entra ID integration by executing some of the steps described in https://learn.microsoft.com/en-us/dynamics365/fin-ops-core/dev-itpro/dev-tools/secure-developer-vm#external-integrations.\nThe integration can either be enabled with an existing certificate or a new self-signed certificate can be created.\nIf a new certificate is created and the integration is also to be enabled on other environments with the same certificate, a certificate password must be specified in order to create a certificate private key file.\n\nThe steps executed are:\n\n- 1) Create a self-signed certificate and save it to Desktop or use a provided certificate.\n- 2) Install the certificate to the \"LocalMachine\" certificate store.\n- 3) Grant NetworkService READ permission to the certificate (only on cloud-hosted environments).\n- 4) Update the web.config with the application ID and the thumbprint of the certificate.\n- 5) Add the application registration to the WIF config.\n- 6) Clear cached LCS configuration in AxDB.\n- 7) Restart the IIS service.\n\nTo execute the steps, the id of an Azure application must be provided. The application must have the following API permissions:\n\n- Dynamics ERP - This permission is required to access finance and operations environments.\n- Microsoft Graph (User.Read.All and Group.Read.All permissions of the Application type).\n- Dynamics Lifecylce service (permission of type Delegated)\n\nThe URL of the finance and operations environment must also be added to the RedirectURI in the Authentication section of the Azure application.\nFinally, after running the cmdlet, if a new certificate was created, it must be uploaded to the Azure application.", "Params": [ [ "ClientId", @@ -9671,14 +9671,6 @@ "false", "False" ], - [ - "AddAppRegistrationToWifConfig", - "Adds the application registration to the WIF config. This is not part of the official Microsoft documentation to enable the Entra ID integration. It is however highly recommended to fix additional \r\nissues with the missing entry integration.", - "", - false, - "false", - "False" - ], [ "WhatIf", "Executes the cmdlet until the first operation that would change the state of the system, without executing that operation.\r\nSubsequent operations are likely to fail.\r\nThis is currently not fully implemented and should not be used.", @@ -9701,8 +9693,8 @@ "Synopsis": "Enable the Microsoft Entra ID integration on a cloud hosted environment (CHE).", "Name": "New-D365EntraIntegration", "Links": null, - "Examples": "-------------------------- EXAMPLE 1 --------------------------\nPS C:\\\u003eNew-D365EntraIntegration -ClientId e70cac82-6a7c-4f9e-a8b9-e707b961e986\nEnables the Entra ID integration with a new self-signed certificate named \"CHEAuth\" which expires after 2 years.\n-------------------------- EXAMPLE 2 --------------------------\nPS C:\\\u003eNew-D365EntraIntegration -ClientId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -AddAppRegistrationToWifConfig\nEnables the Entra ID integration with a new self-signed certificate named \"CHEAuth\" which expires after 2 years and adds the application registration to the wif.config.\n-------------------------- EXAMPLE 3 --------------------------\nPS C:\\\u003eNew-D365EntraIntegration -ClientId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -CertificateName \"SelfsignedCert\"\nEnables the Entra ID integration with a new self-signed certificate with the name \"Selfsignedcert\" that expires after 2 years.\n-------------------------- EXAMPLE 4 --------------------------\nPS C:\\\u003eNew-D365EntraIntegration -AppId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -CertificateName \"SelfsignedCert\" -CertificateExpirationYears 1\nEnables the Entra ID integration with a new self-signed certificate with the name \"SelfsignedCert\" that expires after 1 year.\n-------------------------- EXAMPLE 5 --------------------------\nPS C:\\\u003e$securePassword = Read-Host -AsSecureString -Prompt \"Enter the certificate password\"\nPS C:\\\u003e New-D365EntraIntegration -AppId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -CertificatePassword $securePassword\nEnables the Entra ID integration with a new self-signed certificate with the name \"CHEAuth\" that expires after 2 years, using the provided password to generate the private key of the certificate.\r\nThe certificate file and the private key file are saved to the Desktop of the current user.\n-------------------------- EXAMPLE 6 --------------------------\nPS C:\\\u003e$securePassword = Read-Host -AsSecureString -Prompt \"Enter the certificate password\"\nPS C:\\\u003e New-D365EntraIntegration -AppId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -ExistingCertificateFile \"C:\\Temp\\SelfsignedCert.cer\" -ExistingCertificatePrivateKeyFile \"C:\\Temp\\SelfsignedCert.pfx\" \r\n-CertificatePassword $securePassword\nEnables the Entra ID integration with the certificate file \"C:\\Temp\\SelfsignedCert.cer\", the private key file \"C:\\Temp\\SelfsignedCert.pfx\" and the provided password to install it.", - "Syntax": "New-D365EntraIntegration -ClientId \u003cString\u003e [-CertificateName \u003cString\u003e] [-CertificateExpirationYears \u003cInt32\u003e] [-NewCertificateFile \u003cString\u003e] [-NewCertificatePrivateKeyFile \u003cString\u003e] [-CertificatePassword \u003cSecureString\u003e] [-Force] [-AddAppRegistrationToWifConfig] [-WhatIf] [-Confirm] [\u003cCommonParameters\u003e]\nNew-D365EntraIntegration -ClientId \u003cString\u003e -ExistingCertificateFile \u003cString\u003e [-ExistingCertificatePrivateKeyFile \u003cString\u003e] [-CertificatePassword \u003cSecureString\u003e] [-Force] [-AddAppRegistrationToWifConfig] [-WhatIf] [-Confirm] [\u003cCommonParameters\u003e]" + "Examples": "-------------------------- EXAMPLE 1 --------------------------\nPS C:\\\u003eNew-D365EntraIntegration -ClientId e70cac82-6a7c-4f9e-a8b9-e707b961e986\nEnables the Entra ID integration with a new self-signed certificate named \"CHEAuth\" which expires after 2 years.\n-------------------------- EXAMPLE 2 --------------------------\nPS C:\\\u003eNew-D365EntraIntegration -ClientId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -CertificateName \"SelfsignedCert\"\nEnables the Entra ID integration with a new self-signed certificate with the name \"Selfsignedcert\" that expires after 2 years.\n-------------------------- EXAMPLE 3 --------------------------\nPS C:\\\u003eNew-D365EntraIntegration -AppId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -CertificateName \"SelfsignedCert\" -CertificateExpirationYears 1\nEnables the Entra ID integration with a new self-signed certificate with the name \"SelfsignedCert\" that expires after 1 year.\n-------------------------- EXAMPLE 4 --------------------------\nPS C:\\\u003e$securePassword = Read-Host -AsSecureString -Prompt \"Enter the certificate password\"\nPS C:\\\u003e New-D365EntraIntegration -AppId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -CertificatePassword $securePassword\nEnables the Entra ID integration with a new self-signed certificate with the name \"CHEAuth\" that expires after 2 years, using the provided password to generate the private key of the certificate.\r\nThe certificate file and the private key file are saved to the Desktop of the current user.\n-------------------------- EXAMPLE 5 --------------------------\nPS C:\\\u003e$securePassword = Read-Host -AsSecureString -Prompt \"Enter the certificate password\"\nPS C:\\\u003e New-D365EntraIntegration -AppId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -ExistingCertificateFile \"C:\\Temp\\SelfsignedCert.cer\" -ExistingCertificatePrivateKeyFile \"C:\\Temp\\SelfsignedCert.pfx\" \r\n-CertificatePassword $securePassword\nEnables the Entra ID integration with the certificate file \"C:\\Temp\\SelfsignedCert.cer\", the private key file \"C:\\Temp\\SelfsignedCert.pfx\" and the provided password to install it.", + "Syntax": "New-D365EntraIntegration -ClientId \u003cString\u003e [-CertificateName \u003cString\u003e] [-CertificateExpirationYears \u003cInt32\u003e] [-NewCertificateFile \u003cString\u003e] [-NewCertificatePrivateKeyFile \u003cString\u003e] [-CertificatePassword \u003cSecureString\u003e] [-Force] [-WhatIf] [-Confirm] [\u003cCommonParameters\u003e]\nNew-D365EntraIntegration -ClientId \u003cString\u003e -ExistingCertificateFile \u003cString\u003e [-ExistingCertificatePrivateKeyFile \u003cString\u003e] [-CertificatePassword \u003cSecureString\u003e] [-Force] [-WhatIf] [-Confirm] [\u003cCommonParameters\u003e]" }, { "CommandName": "New-D365ISVLicense", diff --git a/d365fo.tools/tests/functions/New-D365EntraIntegration.Tests.ps1 b/d365fo.tools/tests/functions/New-D365EntraIntegration.Tests.ps1 index 20476e02..e18f3928 100644 --- a/d365fo.tools/tests/functions/New-D365EntraIntegration.Tests.ps1 +++ b/d365fo.tools/tests/functions/New-D365EntraIntegration.Tests.ps1 @@ -128,31 +128,18 @@ $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False } - It 'Should have the expected parameter AddAppRegistrationToWifConfig' { - $parameter = (Get-Command New-D365EntraIntegration).Parameters['AddAppRegistrationToWifConfig'] - $parameter.Name | Should -Be 'AddAppRegistrationToWifConfig' - $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.SwitchParameter - $parameter.IsDynamic | Should -Be $False - $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' - $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' - $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be -2147483648 - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False - } } Describe "Testing parameterset NewCertificate" { <# NewCertificate -ClientId - NewCertificate -ClientId -CertificateName -CertificateExpirationYears -NewCertificateFile -NewCertificatePrivateKeyFile -CertificatePassword -Force -AddAppRegistrationToWifConfig + NewCertificate -ClientId -CertificateName -CertificateExpirationYears -NewCertificateFile -NewCertificatePrivateKeyFile -CertificatePassword -Force #> } Describe "Testing parameterset ExistingCertificate" { <# ExistingCertificate -ClientId -ExistingCertificateFile - ExistingCertificate -ClientId -ExistingCertificateFile -ExistingCertificatePrivateKeyFile -CertificatePassword -Force -AddAppRegistrationToWifConfig + ExistingCertificate -ClientId -ExistingCertificateFile -ExistingCertificatePrivateKeyFile -CertificatePassword -Force #> } diff --git a/docs/New-D365EntraIntegration.md b/docs/New-D365EntraIntegration.md index 94792417..4fdd73f5 100644 --- a/docs/New-D365EntraIntegration.md +++ b/docs/New-D365EntraIntegration.md @@ -16,14 +16,14 @@ Enable the Microsoft Entra ID integration on a cloud hosted environment (CHE). ``` New-D365EntraIntegration -ClientId [-CertificateName ] [-CertificateExpirationYears ] [-NewCertificateFile ] [-NewCertificatePrivateKeyFile ] [-CertificatePassword ] - [-Force] [-AddAppRegistrationToWifConfig] [-WhatIf] [-Confirm] [] + [-Force] [-WhatIf] [-Confirm] [] ``` ### ExistingCertificate ``` New-D365EntraIntegration -ClientId -ExistingCertificateFile - [-ExistingCertificatePrivateKeyFile ] [-CertificatePassword ] [-Force] - [-AddAppRegistrationToWifConfig] [-WhatIf] [-Confirm] [] + [-ExistingCertificatePrivateKeyFile ] [-CertificatePassword ] [-Force] [-WhatIf] + [-Confirm] [] ``` ## DESCRIPTION @@ -37,13 +37,16 @@ The steps executed are: - 2) Install the certificate to the "LocalMachine" certificate store. - 3) Grant NetworkService READ permission to the certificate (only on cloud-hosted environments). - 4) Update the web.config with the application ID and the thumbprint of the certificate. -- 5) (Optional) Add the application registration to the WIF config. +- 5) Add the application registration to the WIF config. +- 6) Clear cached LCS configuration in AxDB. +- 7) Restart the IIS service. To execute the steps, the id of an Azure application must be provided. The application must have the following API permissions: - Dynamics ERP - This permission is required to access finance and operations environments. - Microsoft Graph (User.Read.All and Group.Read.All permissions of the Application type). +- Dynamics Lifecylce service (permission of type Delegated) The URL of the finance and operations environment must also be added to the RedirectURI in the Authentication section of the Azure application. Finally, after running the cmdlet, if a new certificate was created, it must be uploaded to the Azure application. @@ -59,26 +62,19 @@ Enables the Entra ID integration with a new self-signed certificate named "CHEAu ### EXAMPLE 2 ``` -New-D365EntraIntegration -ClientId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -AddAppRegistrationToWifConfig -``` - -Enables the Entra ID integration with a new self-signed certificate named "CHEAuth" which expires after 2 years and adds the application registration to the wif.config. - -### EXAMPLE 3 -``` New-D365EntraIntegration -ClientId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -CertificateName "SelfsignedCert" ``` Enables the Entra ID integration with a new self-signed certificate with the name "Selfsignedcert" that expires after 2 years. -### EXAMPLE 4 +### EXAMPLE 3 ``` New-D365EntraIntegration -AppId e70cac82-6a7c-4f9e-a8b9-e707b961e986 -CertificateName "SelfsignedCert" -CertificateExpirationYears 1 ``` Enables the Entra ID integration with a new self-signed certificate with the name "SelfsignedCert" that expires after 1 year. -### EXAMPLE 5 +### EXAMPLE 4 ``` $securePassword = Read-Host -AsSecureString -Prompt "Enter the certificate password" ``` @@ -88,7 +84,7 @@ PS C:\\\> New-D365EntraIntegration -AppId e70cac82-6a7c-4f9e-a8b9-e707b961e986 - Enables the Entra ID integration with a new self-signed certificate with the name "CHEAuth" that expires after 2 years, using the provided password to generate the private key of the certificate. The certificate file and the private key file are saved to the Desktop of the current user. -### EXAMPLE 6 +### EXAMPLE 5 ``` $securePassword = Read-Host -AsSecureString -Prompt "Enter the certificate password" ``` @@ -244,23 +240,6 @@ Accept pipeline input: False Accept wildcard characters: False ``` -### -AddAppRegistrationToWifConfig -Adds the application registration to the WIF config. -This is not part of the official Microsoft documentation to enable the Entra ID integration. -It is however highly recommended to fix additional issues with the missing entry integration. - -```yaml -Type: SwitchParameter -Parameter Sets: (All) -Aliases: - -Required: False -Position: Named -Default value: False -Accept pipeline input: False -Accept wildcard characters: False -``` - ### -WhatIf Executes the cmdlet until the first operation that would change the state of the system, without executing that operation. Subsequent operations are likely to fail. From 91ebffe371e59775f93327db6c35b6493b033775 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 14:31:03 +0200 Subject: [PATCH 04/14] check for existing entry in wif.config --- .../functions/new-d365entraintegration.ps1 | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index 94b3d89d..ccb53a89 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -1,4 +1,4 @@ - + <# .SYNOPSIS Enable the Microsoft Entra ID integration on a cloud hosted environment (CHE). @@ -348,11 +348,16 @@ function New-D365EntraIntegration { } [xml]$xml = Get-Content $wifConfigFile $audienceUris = $xml.'system.identityModel'.identityConfiguration.securityTokenHandlers.securityTokenHandlerConfiguration.audienceUris - $audienceUriElement = $xml.CreateElement('add') - $audienceUriElement.SetAttribute('value', "spn:$ClientId") - $audienceUris.AppendChild($audienceUriElement) - $xml.Save($wifConfigFile) - Write-PSFMessage -Level Host -Message "Wif.config was updated with the audience URIs." + $existingAudienceUri = $audienceUris.ChildNodes | Where-Object {$_.value -eq "spn:$ClientId"} + if (-not $existingAudienceUri) { + $audienceUriElement = $xml.CreateElement('add') + $audienceUriElement.SetAttribute('value', "spn:$ClientId") + $audienceUris.AppendChild($audienceUriElement) + $xml.Save($wifConfigFile) + Write-PSFMessage -Level Host -Message "Wif.config was updated with the audience URI." + } else { + Write-PSFMessage -Level Host -Message "Audience URI already exists in Wif.config." + } # Step 6: Clear cached LCS configuration in AxDB Write-PSFMessage -Level Verbose -Message "Step 6: Starting clearing cached LCS configuration in AxDB" From da7eff8a41e35dfdcf3a7f5650f708b8af981a24 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 14:31:56 +0200 Subject: [PATCH 05/14] refactor installation of existing certificate in separate function --- .../functions/new-d365entraintegration.ps1 | 83 ++++++++++++------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index ccb53a89..1128310b 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -1,4 +1,4 @@ - + <# .SYNOPSIS Enable the Microsoft Entra ID integration on a cloud hosted environment (CHE). @@ -152,38 +152,12 @@ function New-D365EntraIntegration { # Check and install provided certificate file if ($PSCmdlet.ParameterSetName -eq "ExistingCertificate") { Write-PSFMessage -Level Verbose -Message "Steps 1+2: Starting installation of existing certificate" - if (-not (Test-PathExists -Path $ExistingCertificateFile -Type Leaf)) { - Write-PSFMessage -Level Host -Message "The provided certificate file $ExistingCertificateFile does not exist." - Stop-PSFFunction -Message "Stopping because the provided certificate file does not exist" - return - } - - if ($CertificatePassword -and -not (Test-PathExists -Path $ExistingCertificatePrivateKeyFile -Type Leaf)) { - Write-PSFMessage -Level Host -Message "The provided certificate private key file $ExistingCertificatePrivateKeyFile does not exist." - Stop-PSFFunction -Message "Stopping because the provided certificate private key file does not exist" - return + $params = @{ + CertificateFile = $ExistingCertificateFile + PrivateKeyFile = $ExistingCertificatePrivateKeyFile + CertificatePassword = $CertificatePassword } - - $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($ExistingCertificateFile) - # Check for existing certificate that has the same thumbprint as the provided certificate - $existingCertificate = Get-ChildItem -Path $certificateStoreLocation -ErrorAction SilentlyContinue | Where-Object {$_.Thumbprint -eq $certificate.Thumbprint} - if ($existingCertificate) { - Write-PSFMessage -Level Warning -Message "A certificate with the same thumbprint as the provided certificate $ExistingCertificateFile already exists in $certificateStoreLocation." - if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a certificate with the same thumbprint as the provided certificate already exists" - return - - } - Write-PSFMessage -Level Host -Message "Deleting and installing the provided certificate." - $existingCertificate | Remove-Item - } - # Install certificate - if ($CertificatePassword) { - $null = Import-PfxCertificate -FilePath $ExistingCertificatePrivateKeyFile -CertStoreLocation $certificateStoreLocation -Password $CertificatePassword - } - $certificate = Import-Certificate -FilePath $ExistingCertificateFile -CertStoreLocation $certificateStoreLocation - $certificateThumbprint = $certificate.Thumbprint - Write-PSFMessage -Level Host -Message "Certificate $ExistingCertificateFile installed to $certificateStoreLocation." + $certificateThumbprint = CheckAndInstallCertificate @params } # Create and install certificate @@ -375,4 +349,49 @@ function New-D365EntraIntegration { if ($PSCmdlet.ParameterSetName -eq "NewCertificate") { Write-PSFMessage -Level Host -Message "The certificate file $NewCertificateFile must be uploaded to the Azure application, see https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#add-a-certificate." } +} + +function CheckAndInstallCertificate { + param ( + [Parameter(Mandatory = $true)] + [string] $CertificateFile, + + [Parameter(Mandatory = $false)] + [string] $PrivateKeyFile, + + [Parameter(Mandatory = $false)] + [Security.SecureString] $CertificatePassword + ) + + if (-not (Test-PathExists -Path $CertificateFile -Type Leaf)) { + Write-PSFMessage -Level Host -Message "The provided certificate file $CertificateFile does not exist." + Stop-PSFFunction -Message "Stopping because the provided certificate file does not exist" + return + } + + if ($CertificatePassword -and -not (Test-PathExists -Path $PrivateKeyFile -Type Leaf)) { + Write-PSFMessage -Level Host -Message "The provided certificate private key file $PrivateKeyFile does not exist." + Stop-PSFFunction -Message "Stopping because the provided certificate private key file does not exist" + return + } + + $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($CertificateFile) + # Check for existing certificate that has the same thumbprint as the provided certificate + $existingCertificate = Get-ChildItem -Path $certificateStoreLocation -ErrorAction SilentlyContinue | Where-Object {$_.Thumbprint -eq $certificate.Thumbprint} + if ($existingCertificate) { + Write-PSFMessage -Level Warning -Message "A certificate with the same thumbprint as the provided certificate $CertificateFile already exists in $certificateStoreLocation." + if (-not $Force) { + Stop-PSFFunction -Message "Stopping because a certificate with the same thumbprint as the provided certificate already exists" + return + } + Write-PSFMessage -Level Host -Message "Deleting and installing the provided certificate." + $existingCertificate | Remove-Item + } + # Install certificate + if ($CertificatePassword) { + $null = Import-PfxCertificate -FilePath $PrivateKeyFile -CertStoreLocation $certificateStoreLocation -Password $CertificatePassword + } + $certificate = Import-Certificate -FilePath $CertificateFile -CertStoreLocation $certificateStoreLocation + Write-PSFMessage -Level Host -Message "Certificate $CertificateFile installed to $certificateStoreLocation." + $certificate.Thumbprint } \ No newline at end of file From 1f1b3eaaa6a08d8d1718a1a8de43ebc89745344b Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 15:11:40 +0200 Subject: [PATCH 06/14] add missing force parameter --- d365fo.tools/functions/new-d365entraintegration.ps1 | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index 1128310b..3d6ceaae 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -156,8 +156,9 @@ function New-D365EntraIntegration { CertificateFile = $ExistingCertificateFile PrivateKeyFile = $ExistingCertificatePrivateKeyFile CertificatePassword = $CertificatePassword + Force = $Force } - $certificateThumbprint = CheckAndInstallCertificate @params + $certificateThumbprint = CheckAndInstallExistingCertificate @params } # Create and install certificate @@ -351,16 +352,16 @@ function New-D365EntraIntegration { } } -function CheckAndInstallCertificate { +function CheckAndInstallExistingCertificate { param ( - [Parameter(Mandatory = $true)] + [Parameter(Mandatory)] [string] $CertificateFile, - [Parameter(Mandatory = $false)] [string] $PrivateKeyFile, - [Parameter(Mandatory = $false)] - [Security.SecureString] $CertificatePassword + [Security.SecureString] $CertificatePassword, + + [switch] $Force ) if (-not (Test-PathExists -Path $CertificateFile -Type Leaf)) { From 48ccfe893971a57cc31cf9984039008d2ef61b81 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 15:13:50 +0200 Subject: [PATCH 07/14] refactor creation of new certificate into separate function --- .../functions/new-d365entraintegration.ps1 | 129 +++++++++++------- 1 file changed, 78 insertions(+), 51 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index 3d6ceaae..40a0768f 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -148,7 +148,6 @@ function New-D365EntraIntegration { $certificateThumbprint = "" # Steps 1 and 2: Create or use existing certificate and install it to the certificate store - # Check and install provided certificate file if ($PSCmdlet.ParameterSetName -eq "ExistingCertificate") { Write-PSFMessage -Level Verbose -Message "Steps 1+2: Starting installation of existing certificate" @@ -160,59 +159,18 @@ function New-D365EntraIntegration { } $certificateThumbprint = CheckAndInstallExistingCertificate @params } - - # Create and install certificate + # Create and install new certificate if ($PSCmdlet.ParameterSetName -eq "NewCertificate") { Write-PSFMessage -Level Verbose -Message "Steps 1+2: Starting creation of new certificate" - #Check for existing certificate - $existingCertificate = Get-ChildItem -Path $certificateStoreLocation -ErrorAction SilentlyContinue | Where-Object {$_.Subject -Match "$CertificateName"} - if ($existingCertificate) { - Write-PSFMessage -Level Warning -Message "A certificate with name $CertificateName already exists in $certificateStoreLocation with expiration date $($existingCertificate.NotAfter)." - if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a certificate with the same name already exists" - return - } - Write-PSFMessage -Level Host -Message "Deleting and re-creating the certificate." - $existingCertificate | Remove-Item - } - - # Check for existing certificate file - if (Test-PathExists -Path $NewCertificateFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { - Write-PSFMessage -Level Warning -Message "A certificate file with the same name as the new certificate file $NewCertificateFile already exists." - if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a certificate file with the same name already exists" - return - } - Write-PSFMessage -Level Host -Message "The existing certificate file will be overwritten." - } - if ($CertificatePassword -and (Test-PathExists -Path $NewCertificatePrivateKeyFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { - Write-PSFMessage -Level Warning -Message "A certificate private key file with the same name as the new certificate private key file $NewCertificatePrivateKeyFile already exists." - if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a certificate private key file with the same name already exists" - return - } - Write-PSFMessage -Level Host -Message "The existing certificate private key file will be overwritten." - } - - # Create certificate - $certificateParams = @{ - Subject = "CN=$CertificateName" - CertStoreLocation = $certificateStoreLocation - KeyExportPolicy = 'Exportable' - KeySpec = 'Signature' - KeyLength = 2048 - KeyAlgorithm = 'RSA' - HashAlgorithm = 'SHA256' - NotAfter = (Get-Date).AddYears($CertificateExpirationYears) - } - $certificate = New-SelfSignedCertificate @certificateParams - $certificateThumbprint = $certificate.Thumbprint - $null = Export-Certificate -Cert $certificate -FilePath $NewCertificateFile -Force:$Force - Write-PSFMessage -Level Host -Message "Certificate $CertificateName created and saved to $NewCertificateFile." - if ($CertificatePassword) { - $null = Export-PfxCertificate -Cert $certificate -FilePath $NewCertificatePrivateKeyFile -Password $CertificatePassword -Force:$Force - Write-PSFMessage -Level Host -Message "Certificate private key file $NewCertificatePrivateKeyFile created." + $params = @{ + CertificateName = $CertificateName + CertificateExpirationYears = $CertificateExpirationYears + NewCertificateFile = $NewCertificateFile + NewCertificatePrivateKeyFile = $NewCertificatePrivateKeyFile + CertificatePassword = $CertificatePassword + Force = $Force } + $certificateThumbprint = CreateAndInstallNewCertificate @params } # Sanity checks before next steps @@ -395,4 +353,73 @@ function CheckAndInstallExistingCertificate { $certificate = Import-Certificate -FilePath $CertificateFile -CertStoreLocation $certificateStoreLocation Write-PSFMessage -Level Host -Message "Certificate $CertificateFile installed to $certificateStoreLocation." $certificate.Thumbprint +} + +function CreateAndInstallNewCertificate { + param ( + [Parameter(Mandatory)] + [string] $CertificateName, + + [Parameter(Mandatory)] + [int] $CertificateExpirationYears, + + [Parameter(Mandatory)] + [string] $NewCertificateFile, + + [string] $NewCertificatePrivateKeyFile, + + [Security.SecureString] $CertificatePassword, + + [switch] $Force + ) + + # Check for existing certificate + $existingCertificate = Get-ChildItem -Path $certificateStoreLocation -ErrorAction SilentlyContinue | Where-Object {$_.Subject -Match "$CertificateName"} + if ($existingCertificate) { + Write-PSFMessage -Level Warning -Message "A certificate with name $CertificateName already exists in $certificateStoreLocation with expiration date $($existingCertificate.NotAfter)." + if (-not $Force) { + Stop-PSFFunction -Message "Stopping because a certificate with the same name already exists" + return + } + Write-PSFMessage -Level Host -Message "Deleting and re-creating the certificate." + $existingCertificate | Remove-Item + } + + # Check for existing certificate file + if (Test-PathExists -Path $NewCertificateFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { + Write-PSFMessage -Level Warning -Message "A certificate file with the same name as the new certificate file $NewCertificateFile already exists." + if (-not $Force) { + Stop-PSFFunction -Message "Stopping because a certificate file with the same name already exists" + return + } + Write-PSFMessage -Level Host -Message "The existing certificate file will be overwritten." + } + if ($CertificatePassword -and (Test-PathExists -Path $NewCertificatePrivateKeyFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { + Write-PSFMessage -Level Warning -Message "A certificate private key file with the same name as the new certificate private key file $NewCertificatePrivateKeyFile already exists." + if (-not $Force) { + Stop-PSFFunction -Message "Stopping because a certificate private key file with the same name already exists" + return + } + Write-PSFMessage -Level Host -Message "The existing certificate private key file will be overwritten." + } + + # Create certificate + $certificateParams = @{ + Subject = "CN=$CertificateName" + CertStoreLocation = $certificateStoreLocation + KeyExportPolicy = 'Exportable' + KeySpec = 'Signature' + KeyLength = 2048 + KeyAlgorithm = 'RSA' + HashAlgorithm = 'SHA256' + NotAfter = (Get-Date).AddYears($CertificateExpirationYears) + } + $certificate = New-SelfSignedCertificate @certificateParams + $null = Export-Certificate -Cert $certificate -FilePath $NewCertificateFile -Force:$Force + Write-PSFMessage -Level Host -Message "Certificate $CertificateName created and saved to $NewCertificateFile." + if ($CertificatePassword) { + $null = Export-PfxCertificate -Cert $certificate -FilePath $NewCertificatePrivateKeyFile -Password $CertificatePassword -Force:$Force + Write-PSFMessage -Level Host -Message "Certificate private key file $NewCertificatePrivateKeyFile created." + } + $certificate.Thumbprint } \ No newline at end of file From f366e6e514ca3959e72437758fffb35973aa0723 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 15:17:21 +0200 Subject: [PATCH 08/14] refactor granting network service read permission into separate function --- .../functions/new-d365entraintegration.ps1 | 100 ++++++++++-------- 1 file changed, 55 insertions(+), 45 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index 40a0768f..d26522f8 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -186,51 +186,9 @@ function New-D365EntraIntegration { return } - # Step 3: Grant NetworkService READ permission to the certificate by setting ACL rights - # Check if on cloud-hosted environment - if ($Script:EnvironmentType -eq [EnvironmentType]::AzureHostedTier1) { - Write-PSFMessage -Level Verbose -Message "Step 3: Starting granting NetworkService READ permission to the certificate" - # Get private key container name - $privatekey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificateObject) - $containerName = "" - if ($privateKey.GetType().Name -ieq "RSACng") { - $containerName = $privateKey.Key.UniqueName - } - else { - $containerName = $privateKey.CspKeyContainerInfo.UniqueKeyContainerName - } - $keyFullPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\" + $containerName; - if (-not (Test-PathExists -Path $keyFullPath -Type Leaf)) { - Write-PSFMessage -Level Host -Message "Unable to get the private key container to set read permission for NetworkService." - Stop-PSFFunction -Message "Stopping because the private key container to set read permission for NetworkService could not be retrieved" - } - # Grant NetworkService account access to certificate if it does not already have it - $networkServiceSidType = [System.Security.Principal.WellKnownSidType]::NetworkServiceSid - $readFileSystemRight = [System.Security.AccessControl.FileSystemRights]::Read - $allowAccessControlType = [System.Security.AccessControl.AccessControlType]::Allow - $networkServiceSID = New-Object System.Security.Principal.SecurityIdentifier($networkServiceSidType, $null) - $permissions = (Get-Item $keyFullPath).GetAccessControl() - $newRuleSet = 0 - $identityNetwork = $permissions.access ` - | Where-Object {$_.identityreference -eq "$($networkServiceSID.Translate([System.Security.Principal.NTAccount]).value)"} ` - | Select-Object - if ($identityNetwork.IdentityReference -ne "$($networkServiceSID.Translate([System.Security.Principal.NTAccount]).value)") { - $rule1 = New-Object Security.AccessControl.FileSystemAccessRule($networkServiceSID, $readFileSystemRight, $allowAccessControlType) - $permissions.AddAccessRule($rule1) - $newRuleSet = 1 - Write-PSFMessage -Level Verbose -Message "Added NetworkService with READ access to certificate" - } - elseif ($identityNetwork.FileSystemRights -ne $readFileSystemRight) { - $rule1 = New-Object Security.AccessControl.FileSystemAccessRule($networkServiceSID, $readFileSystemRight, $allowAccessControlType) - $permissions.AddAccessRule($rule1) - $newRuleSet = 1 - Write-PSFMessage -Level Verbose -Message "Gave NetworkService READ access to certificate" - } - if ($newRuleSet -eq 1){ - Set-Acl -Path $keyFullPath -AclObject $permissions - Write-PSFMessage -Level Host -Message "NetworkService was granted READ permission to the certificate." - } - } + # Step 3: Grant NetworkService READ permission to the certificate + Write-PSFMessage -Level Verbose -Message "Step 3: Starting granting NetworkService READ permission to the certificate" + Grant-NetworkServiceReadPermissionToCertificate -certificateObject $certificateObject # Step 4: Update web.config Write-PSFMessage -Level Verbose -Message "Step 4: Starting updating web.config" @@ -422,4 +380,56 @@ function CreateAndInstallNewCertificate { Write-PSFMessage -Level Host -Message "Certificate private key file $NewCertificatePrivateKeyFile created." } $certificate.Thumbprint +} + +function Grant-NetworkServiceReadPermissionToCertificate { + param ( + [Parameter(Mandatory)] + [System.Security.Cryptography.X509Certificates.X509Certificate2] $certificateObject + ) + + # Get private key container name + $privateKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($certificateObject) + $containerName = "" + if ($privateKey.GetType().Name -ieq "RSACng") { + $containerName = $privateKey.Key.UniqueName + } + else { + $containerName = $privateKey.CspKeyContainerInfo.UniqueKeyContainerName + } + $keyFullPath = $env:ProgramData + "\Microsoft\Crypto\RSA\MachineKeys\" + $containerName + + if (-not (Test-PathExists -Path $keyFullPath -Type Leaf)) { + Write-PSFMessage -Level Host -Message "Unable to get the private key container to set read permission for NetworkService." + Stop-PSFFunction -Message "Stopping because the private key container to set read permission for NetworkService could not be retrieved" + } + + # Grant NetworkService account access to certificate if it does not already have it + $networkServiceSidType = [System.Security.Principal.WellKnownSidType]::NetworkServiceSid + $readFileSystemRight = [System.Security.AccessControl.FileSystemRights]::Read + $allowAccessControlType = [System.Security.AccessControl.AccessControlType]::Allow + $networkServiceSID = New-Object System.Security.Principal.SecurityIdentifier($networkServiceSidType, $null) + $permissions = (Get-Item $keyFullPath).GetAccessControl() + $newRuleSet = 0 + $identityNetwork = $permissions.access ` + | Where-Object {$_.identityreference -eq "$($networkServiceSID.Translate([System.Security.Principal.NTAccount]).value)"} ` + | Select-Object + + if ($identityNetwork.IdentityReference -ne "$($networkServiceSID.Translate([System.Security.Principal.NTAccount]).value)") { + $rule1 = New-Object Security.AccessControl.FileSystemAccessRule($networkServiceSID, $readFileSystemRight, $allowAccessControlType) + $permissions.AddAccessRule($rule1) + $newRuleSet = 1 + Write-PSFMessage -Level Verbose -Message "Added NetworkService with READ access to certificate" + } + elseif ($identityNetwork.FileSystemRights -ne $readFileSystemRight) { + $rule1 = New-Object Security.AccessControl.FileSystemAccessRule($networkServiceSID, $readFileSystemRight, $allowAccessControlType) + $permissions.AddAccessRule($rule1) + $newRuleSet = 1 + Write-PSFMessage -Level Verbose -Message "Gave NetworkService READ access to certificate" + } + + if ($newRuleSet -eq 1){ + Set-Acl -Path $keyFullPath -AclObject $permissions + Write-PSFMessage -Level Host -Message "NetworkService was granted READ permission to the certificate." + } } \ No newline at end of file From 50efaad05e2b43ea582b3fc71098c1eb91b16dca Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 15:18:54 +0200 Subject: [PATCH 09/14] refactor update of web.config into separate function --- .../functions/new-d365entraintegration.ps1 | 82 +++++++++++++------ 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index d26522f8..ae1175ab 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -190,34 +190,16 @@ function New-D365EntraIntegration { Write-PSFMessage -Level Verbose -Message "Step 3: Starting granting NetworkService READ permission to the certificate" Grant-NetworkServiceReadPermissionToCertificate -certificateObject $certificateObject - # Step 4: Update web.config - Write-PSFMessage -Level Verbose -Message "Step 4: Starting updating web.config" - $webConfigBackup = Join-Path $Script:DefaultTempPath "WebConfigBackup" - $webConfigFileBackup = Join-Path $webConfigBackup $Script:WebConfig - if (Test-PathExists -Path $webConfigFileBackup -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { - Write-PSFMessage -Level Warning -Message "Backup of web.config already exists." - if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a backup of web.config already exists" - return - } - Write-PSFMessage -Level Host -Message "Backup of web.config will be overwritten." + # Step 4: Update web.config with application ID and certificate thumbprint + Write-PSFMessage -Level Verbose -Message "Step 4: Starting updating web.config with application ID and certificate thumbprint" + $params = @{ + AOSPath = $Script:AOSPath + WebConfig = $Script:WebConfig + ClientId = $ClientId + CertificateThumbprint = $certificateThumbprint + Force = $Force } - $null = Backup-D365WebConfig -Force:$Force - $webConfigFile = Join-Path -Path $Script:AOSPath $Script:WebConfig - if (-not (Test-PathExists -Path $webConfigFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { - Write-PSFMessage -Level Host -Message "Unable to find the web.config file." - Stop-PSFFunction -Message "Stopping because the web.config file could not be found" - } - [xml]$xml = Get-Content $webConfigFile - $nodes = ($xml.configuration.appSettings).ChildNodes - $aadRealm = $nodes | Where-Object -Property Key -eq "Aad.Realm" - $aadRealm.value = "spn:$ClientId" - $infraThumb = $nodes | Where-Object -Property Key -eq "Infrastructure.S2SCertThumbprint" - $infraThumb.value = $certificateThumbprint - $graphThumb = $nodes | Where-Object -Property Key -eq "GraphApi.GraphAPIServicePrincipalCert" - $graphThumb.value = $certificateThumbprint - $xml.Save($webConfigFile) - Write-PSFMessage -Level Host -Message "web.config was updated with the application ID and the thumbprint of the certificate." + Update-WebConfig -AOSPath @params # Step 5: Add app registration to Wif.config Write-PSFMessage -Level Verbose -Message "Step 5: Starting adding app registration to Wif.config" @@ -432,4 +414,50 @@ function Grant-NetworkServiceReadPermissionToCertificate { Set-Acl -Path $keyFullPath -AclObject $permissions Write-PSFMessage -Level Host -Message "NetworkService was granted READ permission to the certificate." } +} + +function Update-WebConfig { + param ( + [Parameter(Mandatory)] + [string] $AOSPath, + + [Parameter(Mandatory)] + [string] $WebConfig, + + [Parameter(Mandatory)] + [string] $ClientId, + + [Parameter(Mandatory)] + [string] $CertificateThumbprint, + + [switch] $Force + ) + + Write-PSFMessage -Level Verbose -Message "Starting updating web.config" + $webConfigBackup = Join-Path $Script:DefaultTempPath "WebConfigBackup" + $webConfigFileBackup = Join-Path $webConfigBackup $WebConfig + if (Test-PathExists -Path $webConfigFileBackup -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { + Write-PSFMessage -Level Warning -Message "Backup of web.config already exists." + if (-not $Force) { + Stop-PSFFunction -Message "Stopping because a backup of web.config already exists" + return + } + Write-PSFMessage -Level Host -Message "Backup of web.config will be overwritten." + } + $null = Backup-D365WebConfig -Force:$Force + $webConfigFile = Join-Path -Path $AOSPath $WebConfig + if (-not (Test-PathExists -Path $webConfigFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { + Write-PSFMessage -Level Host -Message "Unable to find the web.config file." + Stop-PSFFunction -Message "Stopping because the web.config file could not be found" + } + [xml]$xml = Get-Content $webConfigFile + $nodes = ($xml.configuration.appSettings).ChildNodes + $aadRealm = $nodes | Where-Object -Property Key -eq "Aad.Realm" + $aadRealm.value = "spn:$ClientId" + $infraThumb = $nodes | Where-Object -Property Key -eq "Infrastructure.S2SCertThumbprint" + $infraThumb.value = $CertificateThumbprint + $graphThumb = $nodes | Where-Object -Property Key -eq "GraphApi.GraphAPIServicePrincipalCert" + $graphThumb.value = $CertificateThumbprint + $xml.Save($webConfigFile) + Write-PSFMessage -Level Host -Message "web.config was updated with the application ID and the thumbprint of the certificate." } \ No newline at end of file From f9333227d07c9a6cedd41ee5b8f0f8c7ba5ead49 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 15:20:49 +0200 Subject: [PATCH 10/14] refactor update of wif.config into separate function --- .../functions/new-d365entraintegration.ps1 | 74 ++++++++++++------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index ae1175ab..c4aae6d4 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -203,34 +203,7 @@ function New-D365EntraIntegration { # Step 5: Add app registration to Wif.config Write-PSFMessage -Level Verbose -Message "Step 5: Starting adding app registration to Wif.config" - $wifConfigBackup = Join-Path $Script:DefaultTempPath "WifConfigBackup" - $wifConfigFileBackup = Join-Path $wifConfigBackup $Script:WifConfig - if (Test-PathExists -Path $wifConfigFileBackup -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { - Write-PSFMessage -Level Warning -Message "Backup of Wif.config already exists." - if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a backup of Wif.config already exists" - return - } - Write-PSFMessage -Level Host -Message "Backup of Wif.config will be overwritten." - } - $null = Backup-D365WifConfig -Force:$Force - $wifConfigFile = Join-Path -Path $Script:AOSPath $Script:WifConfig - if (-not (Test-PathExists -Path $wifConfigFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { - Write-PSFMessage -Level Host -Message "Unable to find the Wif.config file." - Stop-PSFFunction -Message "Stopping because the Wif.config file could not be found" - } - [xml]$xml = Get-Content $wifConfigFile - $audienceUris = $xml.'system.identityModel'.identityConfiguration.securityTokenHandlers.securityTokenHandlerConfiguration.audienceUris - $existingAudienceUri = $audienceUris.ChildNodes | Where-Object {$_.value -eq "spn:$ClientId"} - if (-not $existingAudienceUri) { - $audienceUriElement = $xml.CreateElement('add') - $audienceUriElement.SetAttribute('value', "spn:$ClientId") - $audienceUris.AppendChild($audienceUriElement) - $xml.Save($wifConfigFile) - Write-PSFMessage -Level Host -Message "Wif.config was updated with the audience URI." - } else { - Write-PSFMessage -Level Host -Message "Audience URI already exists in Wif.config." - } + Update-WifConfig -AOSPath $Script:AOSPath -WifConfig $Script:WifConfig -ClientId $ClientId -Force:$Force # Step 6: Clear cached LCS configuration in AxDB Write-PSFMessage -Level Verbose -Message "Step 6: Starting clearing cached LCS configuration in AxDB" @@ -460,4 +433,49 @@ function Update-WebConfig { $graphThumb.value = $CertificateThumbprint $xml.Save($webConfigFile) Write-PSFMessage -Level Host -Message "web.config was updated with the application ID and the thumbprint of the certificate." +} + +function Update-WifConfig { + param ( + [Parameter(Mandatory)] + [string] $AOSPath, + + [Parameter(Mandatory)] + [string] $WifConfig, + + [Parameter(Mandatory)] + [string] $ClientId, + + [switch] $Force + ) + + Write-PSFMessage -Level Verbose -Message "Step 5: Starting adding app registration to Wif.config" + $wifConfigBackup = Join-Path $Script:DefaultTempPath "WifConfigBackup" + $wifConfigFileBackup = Join-Path $wifConfigBackup $WifConfig + if (Test-PathExists -Path $wifConfigFileBackup -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { + Write-PSFMessage -Level Warning -Message "Backup of Wif.config already exists." + if (-not $Force) { + Stop-PSFFunction -Message "Stopping because a backup of Wif.config already exists" + return + } + Write-PSFMessage -Level Host -Message "Backup of Wif.config will be overwritten." + } + $null = Backup-D365WifConfig -Force:$Force + $wifConfigFile = Join-Path -Path $AOSPath $WifConfig + if (-not (Test-PathExists -Path $wifConfigFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { + Write-PSFMessage -Level Host -Message "Unable to find the Wif.config file." + Stop-PSFFunction -Message "Stopping because the Wif.config file could not be found" + } + [xml]$xml = Get-Content $wifConfigFile + $audienceUris = $xml.'system.identityModel'.identityConfiguration.securityTokenHandlers.securityTokenHandlerConfiguration.audienceUris + $existingAudienceUri = $audienceUris.ChildNodes | Where-Object {$_.value -eq "spn:$ClientId"} + if (-not $existingAudienceUri) { + $audienceUriElement = $xml.CreateElement('add') + $audienceUriElement.SetAttribute('value', "spn:$ClientId") + $audienceUris.AppendChild($audienceUriElement) + $xml.Save($wifConfigFile) + Write-PSFMessage -Level Host -Message "Wif.config was updated with the audience URI." + } else { + Write-PSFMessage -Level Host -Message "Audience URI already exists in Wif.config." + } } \ No newline at end of file From 3b9b94c3fe15fe82f30986aa62b40627e1148971 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 15:42:41 +0200 Subject: [PATCH 11/14] restore check for CHE environment --- d365fo.tools/functions/new-d365entraintegration.ps1 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index c4aae6d4..7545c140 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -1,4 +1,4 @@ - + <# .SYNOPSIS Enable the Microsoft Entra ID integration on a cloud hosted environment (CHE). @@ -187,8 +187,11 @@ function New-D365EntraIntegration { } # Step 3: Grant NetworkService READ permission to the certificate - Write-PSFMessage -Level Verbose -Message "Step 3: Starting granting NetworkService READ permission to the certificate" - Grant-NetworkServiceReadPermissionToCertificate -certificateObject $certificateObject + # Check if on cloud-hosted environment + if ($Script:EnvironmentType -eq [EnvironmentType]::AzureHostedTier1) { + Write-PSFMessage -Level Verbose -Message "Step 3: Starting granting NetworkService READ permission to the certificate" + Grant-NetworkServiceReadPermissionToCertificate -certificateObject $certificateObject + } # Step 4: Update web.config with application ID and certificate thumbprint Write-PSFMessage -Level Verbose -Message "Step 4: Starting updating web.config with application ID and certificate thumbprint" From 8994b4ccfde538fc923ec722ee3fe779c23a52c1 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 15:42:50 +0200 Subject: [PATCH 12/14] fix parameter issue --- d365fo.tools/functions/new-d365entraintegration.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index 7545c140..a4ccc9aa 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -1,4 +1,4 @@ - + <# .SYNOPSIS Enable the Microsoft Entra ID integration on a cloud hosted environment (CHE). @@ -202,7 +202,7 @@ function New-D365EntraIntegration { CertificateThumbprint = $certificateThumbprint Force = $Force } - Update-WebConfig -AOSPath @params + Update-WebConfig @params # Step 5: Add app registration to Wif.config Write-PSFMessage -Level Verbose -Message "Step 5: Starting adding app registration to Wif.config" From 8e1df7a9920cd217839ad4ebdb3e51f08f851baf Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 16:16:32 +0200 Subject: [PATCH 13/14] restore previous error flow --- .../functions/new-d365entraintegration.ps1 | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index a4ccc9aa..e0143694 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -174,6 +174,7 @@ function New-D365EntraIntegration { } # Sanity checks before next steps + if (Test-PSFFunctionInterrupt) { return } if (-not $certificateThumbprint) { Write-PSFMessage -Level Host -Message "Unable to get the certificate thumbprint." Stop-PSFFunction -Message "Stopping because the certificate thumbprint could not be retrieved" @@ -191,6 +192,7 @@ function New-D365EntraIntegration { if ($Script:EnvironmentType -eq [EnvironmentType]::AzureHostedTier1) { Write-PSFMessage -Level Verbose -Message "Step 3: Starting granting NetworkService READ permission to the certificate" Grant-NetworkServiceReadPermissionToCertificate -certificateObject $certificateObject + if (Test-PSFFunctionInterrupt) { return } } # Step 4: Update web.config with application ID and certificate thumbprint @@ -203,10 +205,12 @@ function New-D365EntraIntegration { Force = $Force } Update-WebConfig @params + if (Test-PSFFunctionInterrupt) { return } # Step 5: Add app registration to Wif.config Write-PSFMessage -Level Verbose -Message "Step 5: Starting adding app registration to Wif.config" Update-WifConfig -AOSPath $Script:AOSPath -WifConfig $Script:WifConfig -ClientId $ClientId -Force:$Force + if (Test-PSFFunctionInterrupt) { return } # Step 6: Clear cached LCS configuration in AxDB Write-PSFMessage -Level Verbose -Message "Step 6: Starting clearing cached LCS configuration in AxDB" @@ -240,13 +244,13 @@ function CheckAndInstallExistingCertificate { if (-not (Test-PathExists -Path $CertificateFile -Type Leaf)) { Write-PSFMessage -Level Host -Message "The provided certificate file $CertificateFile does not exist." - Stop-PSFFunction -Message "Stopping because the provided certificate file does not exist" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because the provided certificate file does not exist" return } if ($CertificatePassword -and -not (Test-PathExists -Path $PrivateKeyFile -Type Leaf)) { Write-PSFMessage -Level Host -Message "The provided certificate private key file $PrivateKeyFile does not exist." - Stop-PSFFunction -Message "Stopping because the provided certificate private key file does not exist" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because the provided certificate private key file does not exist" return } @@ -256,7 +260,7 @@ function CheckAndInstallExistingCertificate { if ($existingCertificate) { Write-PSFMessage -Level Warning -Message "A certificate with the same thumbprint as the provided certificate $CertificateFile already exists in $certificateStoreLocation." if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a certificate with the same thumbprint as the provided certificate already exists" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because a certificate with the same thumbprint as the provided certificate already exists" return } Write-PSFMessage -Level Host -Message "Deleting and installing the provided certificate." @@ -294,7 +298,7 @@ function CreateAndInstallNewCertificate { if ($existingCertificate) { Write-PSFMessage -Level Warning -Message "A certificate with name $CertificateName already exists in $certificateStoreLocation with expiration date $($existingCertificate.NotAfter)." if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a certificate with the same name already exists" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because a certificate with the same name already exists" return } Write-PSFMessage -Level Host -Message "Deleting and re-creating the certificate." @@ -305,7 +309,7 @@ function CreateAndInstallNewCertificate { if (Test-PathExists -Path $NewCertificateFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { Write-PSFMessage -Level Warning -Message "A certificate file with the same name as the new certificate file $NewCertificateFile already exists." if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a certificate file with the same name already exists" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because a certificate file with the same name already exists" return } Write-PSFMessage -Level Host -Message "The existing certificate file will be overwritten." @@ -313,7 +317,7 @@ function CreateAndInstallNewCertificate { if ($CertificatePassword -and (Test-PathExists -Path $NewCertificatePrivateKeyFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { Write-PSFMessage -Level Warning -Message "A certificate private key file with the same name as the new certificate private key file $NewCertificatePrivateKeyFile already exists." if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a certificate private key file with the same name already exists" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because a certificate private key file with the same name already exists" return } Write-PSFMessage -Level Host -Message "The existing certificate private key file will be overwritten." @@ -359,7 +363,8 @@ function Grant-NetworkServiceReadPermissionToCertificate { if (-not (Test-PathExists -Path $keyFullPath -Type Leaf)) { Write-PSFMessage -Level Host -Message "Unable to get the private key container to set read permission for NetworkService." - Stop-PSFFunction -Message "Stopping because the private key container to set read permission for NetworkService could not be retrieved" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because the private key container to set read permission for NetworkService could not be retrieved" + return } # Grant NetworkService account access to certificate if it does not already have it @@ -415,7 +420,7 @@ function Update-WebConfig { if (Test-PathExists -Path $webConfigFileBackup -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { Write-PSFMessage -Level Warning -Message "Backup of web.config already exists." if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a backup of web.config already exists" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because a backup of web.config already exists" return } Write-PSFMessage -Level Host -Message "Backup of web.config will be overwritten." @@ -424,7 +429,8 @@ function Update-WebConfig { $webConfigFile = Join-Path -Path $AOSPath $WebConfig if (-not (Test-PathExists -Path $webConfigFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { Write-PSFMessage -Level Host -Message "Unable to find the web.config file." - Stop-PSFFunction -Message "Stopping because the web.config file could not be found" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because the web.config file could not be found" + return } [xml]$xml = Get-Content $webConfigFile $nodes = ($xml.configuration.appSettings).ChildNodes @@ -458,7 +464,7 @@ function Update-WifConfig { if (Test-PathExists -Path $wifConfigFileBackup -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue) { Write-PSFMessage -Level Warning -Message "Backup of Wif.config already exists." if (-not $Force) { - Stop-PSFFunction -Message "Stopping because a backup of Wif.config already exists" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because a backup of Wif.config already exists" return } Write-PSFMessage -Level Host -Message "Backup of Wif.config will be overwritten." @@ -467,7 +473,8 @@ function Update-WifConfig { $wifConfigFile = Join-Path -Path $AOSPath $WifConfig if (-not (Test-PathExists -Path $wifConfigFile -Type Leaf -ErrorAction SilentlyContinue -WarningAction SilentlyContinue)) { Write-PSFMessage -Level Host -Message "Unable to find the Wif.config file." - Stop-PSFFunction -Message "Stopping because the Wif.config file could not be found" + Stop-PSFFunction -StepsUpward 1 -Message "Stopping because the Wif.config file could not be found" + return } [xml]$xml = Get-Content $wifConfigFile $audienceUris = $xml.'system.identityModel'.identityConfiguration.securityTokenHandlers.securityTokenHandlerConfiguration.audienceUris From ae555095c3c7ddc8f4bdfa42e5265277c001686d Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sun, 9 Jun 2024 16:38:10 +0200 Subject: [PATCH 14/14] add minimal SupportsShouldProcess to satisfy automatic tests --- .../functions/new-d365entraintegration.ps1 | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/d365fo.tools/functions/new-d365entraintegration.ps1 b/d365fo.tools/functions/new-d365entraintegration.ps1 index e0143694..7eb9300f 100644 --- a/d365fo.tools/functions/new-d365entraintegration.ps1 +++ b/d365fo.tools/functions/new-d365entraintegration.ps1 @@ -231,6 +231,7 @@ function New-D365EntraIntegration { } function CheckAndInstallExistingCertificate { + [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [string] $CertificateFile, @@ -276,6 +277,7 @@ function CheckAndInstallExistingCertificate { } function CreateAndInstallNewCertificate { + [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [string] $CertificateName, @@ -345,6 +347,7 @@ function CreateAndInstallNewCertificate { } function Grant-NetworkServiceReadPermissionToCertificate { + [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [System.Security.Cryptography.X509Certificates.X509Certificate2] $certificateObject @@ -398,6 +401,7 @@ function Grant-NetworkServiceReadPermissionToCertificate { } function Update-WebConfig { + [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [string] $AOSPath, @@ -440,11 +444,14 @@ function Update-WebConfig { $infraThumb.value = $CertificateThumbprint $graphThumb = $nodes | Where-Object -Property Key -eq "GraphApi.GraphAPIServicePrincipalCert" $graphThumb.value = $CertificateThumbprint - $xml.Save($webConfigFile) - Write-PSFMessage -Level Host -Message "web.config was updated with the application ID and the thumbprint of the certificate." + if ($PSCmdlet.ShouldProcess($WebConfig, "Update")) { + $xml.Save($webConfigFile) + Write-PSFMessage -Level Host -Message "web.config was updated with the application ID and the thumbprint of the certificate." + } } function Update-WifConfig { + [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory)] [string] $AOSPath, @@ -483,8 +490,10 @@ function Update-WifConfig { $audienceUriElement = $xml.CreateElement('add') $audienceUriElement.SetAttribute('value', "spn:$ClientId") $audienceUris.AppendChild($audienceUriElement) - $xml.Save($wifConfigFile) - Write-PSFMessage -Level Host -Message "Wif.config was updated with the audience URI." + if ($PSCmdlet.ShouldProcess($WifConfig, "Update")) { + $xml.Save($wifConfigFile) + Write-PSFMessage -Level Host -Message "Wif.config was updated with the audience URI." + } } else { Write-PSFMessage -Level Host -Message "Audience URI already exists in Wif.config." }