From a0a5ee7b909b53d100f0a5748ce8429118d675fc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:04:31 +0000 Subject: [PATCH 1/8] Implement Request-D365DatabaseJITAccess function with tests and module exports Co-authored-by: FH-Inway <33372796+FH-Inway@users.noreply.github.com> --- d365fo.tools/d365fo.tools.psd1 | 2 + .../request-d365databasejitaccess.ps1 | 190 ++++++++++++++++++ .../Request-D365DatabaseJITAccess.Tests.ps1 | 139 +++++++++++++ 3 files changed, 331 insertions(+) create mode 100644 d365fo.tools/functions/request-d365databasejitaccess.ps1 create mode 100644 d365fo.tools/tests/functions/Request-D365DatabaseJITAccess.Tests.ps1 diff --git a/d365fo.tools/d365fo.tools.psd1 b/d365fo.tools/d365fo.tools.psd1 index 8f7981cd..787d6f45 100644 --- a/d365fo.tools/d365fo.tools.psd1 +++ b/d365fo.tools/d365fo.tools.psd1 @@ -272,6 +272,8 @@ 'Remove-D365Model', 'Remove-D365User', + 'Request-D365DatabaseJITAccess', + 'Rename-D365Instance', 'Rename-D365ComputerName', diff --git a/d365fo.tools/functions/request-d365databasejitaccess.ps1 b/d365fo.tools/functions/request-d365databasejitaccess.ps1 new file mode 100644 index 00000000..083b12e7 --- /dev/null +++ b/d365fo.tools/functions/request-d365databasejitaccess.ps1 @@ -0,0 +1,190 @@ + +<# + .SYNOPSIS + Request just in time (JIT) database access for a unified development environment (UDE) + + .DESCRIPTION + Utilize the D365FO OData API to request just in time access (JIT) to a UDE database + + This will allow you to get temporary database credentials for connecting to the database directly + + .PARAMETER Url + URL / URI for the D365FO environment you want to access + + If you are working against a D365FO instance, it will be the URL / URI for the instance itself + + This should be the full URL, e.g. "https://operations-acme-uat.crm4.dynamics.com/" + + .PARAMETER ClientId + The ClientId obtained from the Azure Portal when you created a Registered Application + + .PARAMETER ClientSecret + The ClientSecret obtained from the Azure Portal when you created a Registered Application + + .PARAMETER Tenant + Azure Active Directory (AAD) tenant id (Guid) that the D365FO environment is connected to, that you want to access + + .PARAMETER ClientIPAddress + The IP address of the client that needs database access + + Default value is "127.0.0.1" for localhost access + + .PARAMETER Role + The database role to assign to the JIT access + + Valid options are "Reader" and "Writer" + + Default value is "Reader" + + .PARAMETER Reason + The reason for requesting JIT database access + + This is logged for audit purposes + + Default value is "Administrative access via d365fo.tools" + + .PARAMETER RawOutput + Instructs the cmdlet to include the outer structure of the response received from the endpoint + + The output will still be a PSCustomObject + + .PARAMETER OutputAsJson + Instructs the cmdlet to convert the output to a Json string + + .EXAMPLE + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" + + This will request JIT database access for the D365FO environment. + It will use the default client IP address "127.0.0.1", role "Reader", and reason "Administrative access via d365fo.tools". + It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". + It will authenticate against the "https://login.microsoftonline.com/e674da86-7ee5-40a7-b777-1111111111111/oauth2/token" url with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". + It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". + It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + + .EXAMPLE + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -ClientIPAddress "192.168.1.100" -Role "Writer" -Reason "Development work" + + This will request JIT database access for the D365FO environment with Writer privileges. + It will use the client IP address "192.168.1.100", role "Writer", and reason "Development work". + It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". + It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". + It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". + It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + + .EXAMPLE + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -OutputAsJson + + This will request JIT database access for the D365FO environment and display the result as json. + It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". + It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". + It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". + It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + + .NOTES + Tags: JIT, Database, Access, UDE, OData, RestApi + + Author: Mötz Jensen (@Splaxi) + + This cmdlet is inspired by the PowerShell script provided in GitHub issue for d365fo.tools + +#> +function Request-D365DatabaseJITAccess { + [CmdletBinding()] + [OutputType([System.String])] + param ( + [Parameter(Mandatory = $true)] + [string] $Url, + + [Parameter(Mandatory = $true)] + [string] $ClientId, + + [Parameter(Mandatory = $true)] + [string] $ClientSecret, + + [Parameter(Mandatory = $true)] + [string] $Tenant, + + [string] $ClientIPAddress = "127.0.0.1", + + [ValidateSet("Reader", "Writer")] + [string] $Role = "Reader", + + [string] $Reason = "Administrative access via d365fo.tools", + + [switch] $RawOutput, + + [switch] $OutputAsJson + ) + + begin { + # Clean up the URL to ensure it ends with a slash + if (-not $Url.EndsWith('/')) { + $Url = $Url + '/' + } + } + + process { + $bearerParms = @{ + Resource = $Url + ClientId = $ClientId + ClientSecret = $ClientSecret + AuthProviderUri = "https://login.microsoftonline.com/$Tenant/oauth2/token" + } + + $bearer = Invoke-ClientCredentialsGrant @bearerParms | Get-BearerToken + + $headers = @{ + 'Authorization' = $bearer + 'Accept' = 'application/json' + 'Content-Type' = 'application/json; charset=utf-8' + } + + $requestUrl = $Url + "api/data/v9.2/msprov_getfinopssqljitaccessasync" + + $body = @{ + "sqljitclientipaddress" = $ClientIPAddress + "sqljitreason" = $Reason + "sqljitrole" = $Role + } | ConvertTo-Json -Depth 3 + + try { + Write-PSFMessage -Level Verbose -Message "Requesting JIT database access from endpoint: $requestUrl" + Write-PSFMessage -Level Verbose -Message "Request body: $body" + + $response = Invoke-RestMethod -Uri $requestUrl -Method Post -Headers $headers -Body $body + + if ($RawOutput) { + $result = $response + } + else { + # Extract the relevant information from the response + $result = [PSCustomObject]@{ + Username = $response.sqljitusername + Password = $response.sqljitpassword + ServerName = $response.servername + DatabaseName = $response.databasename + DatabaseType = $response.databasetype + IPAddress = $response.ipaddress + Role = $response.sqljitrole + ExpirationTime = $response.sqljitexpiration + OperationId = $response.operationhistoryid + } + } + + if ($OutputAsJson) { + $result | ConvertTo-Json -Depth 10 + } + else { + $result + } + } + catch { + Write-PSFMessage -Level Host -Message "Something went wrong while requesting JIT database access" -Exception $PSItem.Exception + Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 + return + } + } + + end { + } +} \ No newline at end of file diff --git a/d365fo.tools/tests/functions/Request-D365DatabaseJITAccess.Tests.ps1 b/d365fo.tools/tests/functions/Request-D365DatabaseJITAccess.Tests.ps1 new file mode 100644 index 00000000..a2504dc1 --- /dev/null +++ b/d365fo.tools/tests/functions/Request-D365DatabaseJITAccess.Tests.ps1 @@ -0,0 +1,139 @@ +Describe "Request-D365DatabaseJITAccess Unit Tests" -Tag "Unit" { + BeforeAll { + # Place here all things needed to prepare for the tests + } + AfterAll { + # Here is where all the cleanup tasks go + } + + Describe "Ensuring unchanged command signature" { + It "should have the expected parameter sets" { + (Get-Command Request-D365DatabaseJITAccess).ParameterSets.Name | Should -Be '__AllParameterSets' + } + + It 'Should have the expected parameter Url' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['Url'] + $parameter.Name | Should -Be 'Url' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter ClientId' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['ClientId'] + $parameter.Name | Should -Be 'ClientId' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 1 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter ClientSecret' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['ClientSecret'] + $parameter.Name | Should -Be 'ClientSecret' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 2 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter Tenant' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['Tenant'] + $parameter.Name | Should -Be 'Tenant' + $parameter.ParameterType.ToString() | Should -Be System.String + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' + $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' + $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True + $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 3 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter ClientIPAddress' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['ClientIPAddress'] + $parameter.Name | Should -Be 'ClientIPAddress' + $parameter.ParameterType.ToString() | Should -Be System.String + $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 4 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter Role' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['Role'] + $parameter.Name | Should -Be 'Role' + $parameter.ParameterType.ToString() | Should -Be System.String + $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 5 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter Reason' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['Reason'] + $parameter.Name | Should -Be 'Reason' + $parameter.ParameterType.ToString() | Should -Be System.String + $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 6 + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter RawOutput' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['RawOutput'] + $parameter.Name | Should -Be 'RawOutput' + $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 + } + It 'Should have the expected parameter OutputAsJson' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['OutputAsJson'] + $parameter.Name | Should -Be 'OutputAsJson' + $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 "Ensuring ValidateSet works on Role parameter" { + It "should only allow 'Reader' and 'Writer' for Role parameter" { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['Role'] + $parameter.Attributes.ValidValues | Should -Be @('Reader', 'Writer') + } + } +} \ No newline at end of file From 422c9cf92ec46f5c0defc686bb0fc41f084670b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 27 Sep 2025 13:08:28 +0000 Subject: [PATCH 2/8] Fix trailing whitespaces and add documentation for Request-D365DatabaseJITAccess Co-authored-by: FH-Inway <33372796+FH-Inway@users.noreply.github.com> --- .../request-d365databasejitaccess.ps1 | 58 ++--- docs/Request-D365DatabaseJITAccess.md | 230 ++++++++++++++++++ 2 files changed, 259 insertions(+), 29 deletions(-) create mode 100644 docs/Request-D365DatabaseJITAccess.md diff --git a/d365fo.tools/functions/request-d365databasejitaccess.ps1 b/d365fo.tools/functions/request-d365databasejitaccess.ps1 index 083b12e7..ac9e7d9a 100644 --- a/d365fo.tools/functions/request-d365databasejitaccess.ps1 +++ b/d365fo.tools/functions/request-d365databasejitaccess.ps1 @@ -2,91 +2,91 @@ <# .SYNOPSIS Request just in time (JIT) database access for a unified development environment (UDE) - + .DESCRIPTION Utilize the D365FO OData API to request just in time access (JIT) to a UDE database - + This will allow you to get temporary database credentials for connecting to the database directly - + .PARAMETER Url URL / URI for the D365FO environment you want to access - + If you are working against a D365FO instance, it will be the URL / URI for the instance itself - + This should be the full URL, e.g. "https://operations-acme-uat.crm4.dynamics.com/" - + .PARAMETER ClientId The ClientId obtained from the Azure Portal when you created a Registered Application - + .PARAMETER ClientSecret The ClientSecret obtained from the Azure Portal when you created a Registered Application - + .PARAMETER Tenant Azure Active Directory (AAD) tenant id (Guid) that the D365FO environment is connected to, that you want to access - + .PARAMETER ClientIPAddress The IP address of the client that needs database access - + Default value is "127.0.0.1" for localhost access - + .PARAMETER Role The database role to assign to the JIT access - + Valid options are "Reader" and "Writer" - + Default value is "Reader" - + .PARAMETER Reason The reason for requesting JIT database access - + This is logged for audit purposes - + Default value is "Administrative access via d365fo.tools" - + .PARAMETER RawOutput Instructs the cmdlet to include the outer structure of the response received from the endpoint - + The output will still be a PSCustomObject - + .PARAMETER OutputAsJson Instructs the cmdlet to convert the output to a Json string - + .EXAMPLE PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" - + This will request JIT database access for the D365FO environment. It will use the default client IP address "127.0.0.1", role "Reader", and reason "Administrative access via d365fo.tools". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the "https://login.microsoftonline.com/e674da86-7ee5-40a7-b777-1111111111111/oauth2/token" url with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". - + .EXAMPLE PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -ClientIPAddress "192.168.1.100" -Role "Writer" -Reason "Development work" - + This will request JIT database access for the D365FO environment with Writer privileges. It will use the client IP address "192.168.1.100", role "Writer", and reason "Development work". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". - + .EXAMPLE PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -OutputAsJson - + This will request JIT database access for the D365FO environment and display the result as json. It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". - + .NOTES Tags: JIT, Database, Access, UDE, OData, RestApi - + Author: Mötz Jensen (@Splaxi) - + This cmdlet is inspired by the PowerShell script provided in GitHub issue for d365fo.tools - + #> function Request-D365DatabaseJITAccess { [CmdletBinding()] diff --git a/docs/Request-D365DatabaseJITAccess.md b/docs/Request-D365DatabaseJITAccess.md new file mode 100644 index 00000000..0ce349c1 --- /dev/null +++ b/docs/Request-D365DatabaseJITAccess.md @@ -0,0 +1,230 @@ +--- +external help file: d365fo.tools-help.xml +Module Name: d365fo.tools +online version: +schema: 2.0.0 +--- + +# Request-D365DatabaseJITAccess + +## SYNOPSIS +Request just in time (JIT) database access for a unified development environment (UDE) + +## SYNTAX + +``` +Request-D365DatabaseJITAccess [-Url] [-ClientId] [-ClientSecret] + [-Tenant] [[-ClientIPAddress] ] [[-Role] ] [[-Reason] ] [-RawOutput] + [-OutputAsJson] [] +``` + +## DESCRIPTION +Utilize the D365FO OData API to request just in time access (JIT) to a UDE database + +This will allow you to get temporary database credentials for connecting to the database directly + +## EXAMPLES + +### EXAMPLE 1 +``` +Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" +``` + +This will request JIT database access for the D365FO environment. +It will use the default client IP address "127.0.0.1", role "Reader", and reason "Administrative access via d365fo.tools". +It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". +It will authenticate against the "https://login.microsoftonline.com/e674da86-7ee5-40a7-b777-1111111111111/oauth2/token" url with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". +It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". +It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + +### EXAMPLE 2 +``` +Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -ClientIPAddress "192.168.1.100" -Role "Writer" -Reason "Development work" +``` + +This will request JIT database access for the D365FO environment with Writer privileges. +It will use the client IP address "192.168.1.100", role "Writer", and reason "Development work". +It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". +It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". +It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". +It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + +### EXAMPLE 3 +``` +Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -OutputAsJson +``` + +This will request JIT database access for the D365FO environment and display the result as json. +It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". +It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". +It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". +It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + +## PARAMETERS + +### -Url +URL / URI for the D365FO environment you want to access + +If you are working against a D365FO instance, it will be the URL / URI for the instance itself + +This should be the full URL, e.g. "https://operations-acme-uat.crm4.dynamics.com/" + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ClientId +The ClientId obtained from the Azure Portal when you created a Registered Application + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ClientSecret +The ClientSecret obtained from the Azure Portal when you created a Registered Application + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 2 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Tenant +Azure Active Directory (AAD) tenant id (Guid) that the D365FO environment is connected to, that you want to access + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: True +Position: 3 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ClientIPAddress +The IP address of the client that needs database access + +Default value is "127.0.0.1" for localhost access + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 4 +Default value: 127.0.0.1 +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Role +The database role to assign to the JIT access + +Valid options are "Reader" and "Writer" + +Default value is "Reader" + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 5 +Default value: Reader +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Reason +The reason for requesting JIT database access + +This is logged for audit purposes + +Default value is "Administrative access via d365fo.tools" + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 6 +Default value: Administrative access via d365fo.tools +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -RawOutput +Instructs the cmdlet to include the outer structure of the response received from the endpoint + +The output will still be a PSCustomObject + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -OutputAsJson +Instructs the cmdlet to convert the output to a Json string + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +## NOTES +Tags: JIT, Database, Access, UDE, OData, RestApi + +Author: Mötz Jensen (@Splaxi) + +This cmdlet is inspired by the PowerShell script provided in GitHub issue for d365fo.tools + +## RELATED LINKS \ No newline at end of file From 87f0482c43d3382d949b14cc1efe1c6787cf3548 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sat, 27 Sep 2025 15:48:49 +0000 Subject: [PATCH 3/8] improve copilot draft --- .../request-d365databasejitaccess.ps1 | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/d365fo.tools/functions/request-d365databasejitaccess.ps1 b/d365fo.tools/functions/request-d365databasejitaccess.ps1 index ac9e7d9a..60922c57 100644 --- a/d365fo.tools/functions/request-d365databasejitaccess.ps1 +++ b/d365fo.tools/functions/request-d365databasejitaccess.ps1 @@ -4,16 +4,16 @@ Request just in time (JIT) database access for a unified development environment (UDE) .DESCRIPTION - Utilize the D365FO OData API to request just in time access (JIT) to a UDE database + Utilize the D365FO Power Platform OData API to request just in time access (JIT) to a UDE database This will allow you to get temporary database credentials for connecting to the database directly .PARAMETER Url - URL / URI for the D365FO environment you want to access + URL / URI for the D365FO Power Platform environment that provides the JIT access API. - If you are working against a D365FO instance, it will be the URL / URI for the instance itself + Note: This is not the URL of the D365FO environment itself (aka the Finance and Operations URL). Instead, it is the URL of the Power Platform environment (aka the Environment URL) that the D365FO environment is integrated with. - This should be the full URL, e.g. "https://operations-acme-uat.crm4.dynamics.com/" + For example: "https://operations-acme-uat.crm4.dynamics.com/" .PARAMETER ClientId The ClientId obtained from the Azure Portal when you created a Registered Application @@ -83,9 +83,7 @@ .NOTES Tags: JIT, Database, Access, UDE, OData, RestApi - Author: Mötz Jensen (@Splaxi) - - This cmdlet is inspired by the PowerShell script provided in GitHub issue for d365fo.tools + Author: Florian Hopfner (@FH-Inway) #> function Request-D365DatabaseJITAccess { @@ -102,7 +100,7 @@ function Request-D365DatabaseJITAccess { [string] $ClientSecret, [Parameter(Mandatory = $true)] - [string] $Tenant, + [string] $Tenant, # TODO This could be preset from $Script.TenantId once UDE support is added (see https://github.com/d365collaborative/d365fo.tools/pull/868) [string] $ClientIPAddress = "127.0.0.1", @@ -121,6 +119,17 @@ function Request-D365DatabaseJITAccess { if (-not $Url.EndsWith('/')) { $Url = $Url + '/' } + + # Replace default IP address with IP address from icanhazip.com + if ($ClientIPAddress -eq "127.0.0.1") { + try { + $ClientIPAddress = (Invoke-RestMethod -Uri "https://icanhazip.com" -UseBasicParsing).Trim() + Write-PSFMessage -Level Verbose -Message "Detected public IP address: $ClientIPAddress" + } + catch { + Write-PSFMessage -Level Warning -Message "Could not determine public IP address from icanhazip.com. Falling back to default IP address: $ClientIPAddress" + } + } } process { @@ -153,30 +162,31 @@ function Request-D365DatabaseJITAccess { $response = Invoke-RestMethod -Uri $requestUrl -Method Post -Headers $headers -Body $body - if ($RawOutput) { - $result = $response - } - else { + $result = $response + if (-not $RawOutput) { # Extract the relevant information from the response - $result = [PSCustomObject]@{ - Username = $response.sqljitusername - Password = $response.sqljitpassword - ServerName = $response.servername - DatabaseName = $response.databasename - DatabaseType = $response.databasetype - IPAddress = $response.ipaddress - Role = $response.sqljitrole - ExpirationTime = $response.sqljitexpiration - OperationId = $response.operationhistoryid + $selectParams = @{ + TypeName = "D365FO.TOOLS.UDE.JITDatabaseAccess" + Property = @{Name = "SQLJITCredential"; Expression = { + $password = $_.sqljitpassword | ConvertTo-SecureString -AsPlainText -Force + New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($_.sqljitusername, $password) + }}, + "servername as ServerName", + "databasename as DatabaseName", + "databasetype as DatabaseType", + "ipaddress as IPAddress", + "sqljitrole as SQLJITRole", + "sqljitexpiration as SQLJITExpirationTime to DateTime", + "operationhistoryid as OperationHistoryId" } + $result = $response | Select-PSFObject @selectParams } if ($OutputAsJson) { - $result | ConvertTo-Json -Depth 10 - } - else { - $result + $result = $result | ConvertTo-Json -Depth 10 } + + $result } catch { Write-PSFMessage -Level Host -Message "Something went wrong while requesting JIT database access" -Exception $PSItem.Exception From d4b70bbb9d4552ae4ff051adbfbd16b87ea814f1 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sat, 4 Oct 2025 12:06:02 +0000 Subject: [PATCH 4/8] add more secure authentication options with client secret as Secure.String or a PSCredential object --- .../request-d365databasejitaccess.ps1 | 98 +++++++++++++++---- 1 file changed, 80 insertions(+), 18 deletions(-) diff --git a/d365fo.tools/functions/request-d365databasejitaccess.ps1 b/d365fo.tools/functions/request-d365databasejitaccess.ps1 index 60922c57..e2a1b0e2 100644 --- a/d365fo.tools/functions/request-d365databasejitaccess.ps1 +++ b/d365fo.tools/functions/request-d365databasejitaccess.ps1 @@ -1,5 +1,4 @@ - -<# +<# .SYNOPSIS Request just in time (JIT) database access for a unified development environment (UDE) @@ -18,16 +17,35 @@ .PARAMETER ClientId The ClientId obtained from the Azure Portal when you created a Registered Application - .PARAMETER ClientSecret + .PARAMETER ClientSecretAsPlainString The ClientSecret obtained from the Azure Portal when you created a Registered Application + This is the plain text version of the ClientSecret parameter. + + Either ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided. + + .PARAMETER ClientSecretAsSecureString + The ClientSecret obtained from the Azure Portal when you created a Registered Application + + This is the secure string version of the ClientSecret parameter. + + Either ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided. + + .PARAMETER Credential + The Credential object containing Username (ClientId) and Password (ClientSecret) + + The Username will be used as ClientId + The Password will be used as ClientSecret + + Either ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided. + .PARAMETER Tenant Azure Active Directory (AAD) tenant id (Guid) that the D365FO environment is connected to, that you want to access .PARAMETER ClientIPAddress The IP address of the client that needs database access - Default value is "127.0.0.1" for localhost access + Default value is "127.0.0.1" which will be replaced with the public IP address of the client as determined by querying "https://icanhazip.com" .PARAMETER Role The database role to assign to the JIT access @@ -39,8 +57,6 @@ .PARAMETER Reason The reason for requesting JIT database access - This is logged for audit purposes - Default value is "Administrative access via d365fo.tools" .PARAMETER RawOutput @@ -52,33 +68,56 @@ Instructs the cmdlet to convert the output to a Json string .EXAMPLE - PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" This will request JIT database access for the D365FO environment. - It will use the default client IP address "127.0.0.1", role "Reader", and reason "Administrative access via d365fo.tools". + It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the "https://login.microsoftonline.com/e674da86-7ee5-40a7-b777-1111111111111/oauth2/token" url with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". - It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". .EXAMPLE - PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -ClientIPAddress "192.168.1.100" -Role "Writer" -Reason "Development work" + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -ClientIPAddress "192.168.1.100" -Role "Writer" -Reason "Development work" This will request JIT database access for the D365FO environment with Writer privileges. It will use the client IP address "192.168.1.100", role "Writer", and reason "Development work". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". - It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". .EXAMPLE - PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -OutputAsJson + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -OutputAsJson This will request JIT database access for the D365FO environment and display the result as json. It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". - It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + + .EXAMPLE + PS C:\> $clientSecretSecure = Read-Host -AsSecureString "Enter the Client Secret" + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsSecureString $clientSecretSecure + + This will prompt the user to enter the client secret securely (the input will be masked). + Then it will request JIT database access for the D365FO environment using the secure string for authentication. + It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". + It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". + It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". + It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". + It will authenticate with the client secret provided through the secure prompt. + + .EXAMPLE + PS C:\> $credential = Get-Credential -UserName "dea8d7a9-1602-4429-b138-111111111111" -Message "Enter the Client Secret" + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -Credential $credential + + This will prompt the user to enter the client secret through a secure credential dialog (using the ClientId as the username). + Then it will request JIT database access for the D365FO environment using the credential for authentication. + It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". + It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". + It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". + It will authenticate with the client id and secret provided through the credential object. .NOTES Tags: JIT, Database, Access, UDE, OData, RestApi @@ -87,17 +126,24 @@ #> function Request-D365DatabaseJITAccess { - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'ByClientSecretAsPlainString')] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] [string] $Url, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ParameterSetName = 'ByClientSecretAsPlainString')] + [Parameter(Mandatory = $true, ParameterSetName = 'ByClientSecretAsSecureString')] [string] $ClientId, - [Parameter(Mandatory = $true)] - [string] $ClientSecret, + [Parameter(Mandatory = $true, ParameterSetName = 'ByClientSecretAsPlainString')] + [string] $ClientSecretAsPlainString, + + [Parameter(Mandatory = $true, ParameterSetName = 'ByClientSecretAsSecureString')] + [System.Security.SecureString] $ClientSecretAsSecureString, + + [Parameter(Mandatory = $true, ParameterSetName = 'ByCredential')] + [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $true)] [string] $Tenant, # TODO This could be preset from $Script.TenantId once UDE support is added (see https://github.com/d365collaborative/d365fo.tools/pull/868) @@ -115,6 +161,18 @@ function Request-D365DatabaseJITAccess { ) begin { + # Extract ClientId and ClientSecret from credential if provided + if ($PSCmdlet.ParameterSetName -eq 'ByCredential') { + $ClientId = $Credential.UserName + $ClientSecretAsPlainString = $Credential.GetNetworkCredential().Password + } + # Convert SecureString to plain text if ClientSecretAsSecureString is provided + elseif ($PSCmdlet.ParameterSetName -eq 'ByClientSecretAsSecureString') { + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ClientSecretAsSecureString) + $ClientSecretAsPlainString = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) + } + # Clean up the URL to ensure it ends with a slash if (-not $Url.EndsWith('/')) { $Url = $Url + '/' @@ -136,7 +194,7 @@ function Request-D365DatabaseJITAccess { $bearerParms = @{ Resource = $Url ClientId = $ClientId - ClientSecret = $ClientSecret + ClientSecret = $ClientSecretAsPlainString AuthProviderUri = "https://login.microsoftonline.com/$Tenant/oauth2/token" } @@ -196,5 +254,9 @@ function Request-D365DatabaseJITAccess { } end { + if ($PSCmdlet.ParameterSetName -ne 'ByClientSecretAsPlainString') { + # Clear sensitive variables + $ClientSecretAsPlainString = $null + } } } \ No newline at end of file From 5220f7ca82321bd39d7c4752a4104e0b86a72563 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sat, 4 Oct 2025 14:11:41 +0000 Subject: [PATCH 5/8] add interactive authentication --- .../request-d365databasejitaccess.ps1 | 72 ++++++++++++++++--- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/d365fo.tools/functions/request-d365databasejitaccess.ps1 b/d365fo.tools/functions/request-d365databasejitaccess.ps1 index e2a1b0e2..61f2f380 100644 --- a/d365fo.tools/functions/request-d365databasejitaccess.ps1 +++ b/d365fo.tools/functions/request-d365databasejitaccess.ps1 @@ -7,6 +7,8 @@ This will allow you to get temporary database credentials for connecting to the database directly + If no credentials are provided (ClientId/ClientSecret or Credential), the function will automatically use interactive authentication via Azure PowerShell. + .PARAMETER Url URL / URI for the D365FO Power Platform environment that provides the JIT access API. @@ -67,6 +69,21 @@ .PARAMETER OutputAsJson Instructs the cmdlet to convert the output to a Json string + .PARAMETER EnableException + This parameters disables user-friendly warnings and enables the throwing of exceptions + This is less user friendly, but allows catching exceptions in calling scripts + Usually this parameter is not used directly, but via the Enable-D365Exception cmdlet + See https://github.com/d365collaborative/d365fo.tools/wiki/Exception-handling#what-does-the--enableexception-parameter-do for further information + + .EXAMPLE + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" + + This will request JIT database access for the D365FO environment using interactive authentication. + It will prompt you to sign in with your Azure AD credentials if not already signed in. + It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". + It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". + It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". + .EXAMPLE PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" @@ -126,7 +143,7 @@ #> function Request-D365DatabaseJITAccess { - [CmdletBinding(DefaultParameterSetName = 'ByClientSecretAsPlainString')] + [CmdletBinding(DefaultParameterSetName = 'ByInteractiveLogin')] [OutputType([System.String])] param ( [Parameter(Mandatory = $true)] @@ -157,7 +174,9 @@ function Request-D365DatabaseJITAccess { [switch] $RawOutput, - [switch] $OutputAsJson + [switch] $OutputAsJson, + + [switch] $EnableException ) begin { @@ -191,14 +210,47 @@ function Request-D365DatabaseJITAccess { } process { - $bearerParms = @{ - Resource = $Url - ClientId = $ClientId - ClientSecret = $ClientSecretAsPlainString - AuthProviderUri = "https://login.microsoftonline.com/$Tenant/oauth2/token" - } + # Get authentication token based on the parameter set + if ($PSCmdlet.ParameterSetName -ne 'ByInteractiveLogin') { + $bearerParms = @{ + Resource = $Url + ClientId = $ClientId + ClientSecret = $ClientSecretAsPlainString + AuthProviderUri = "https://login.microsoftonline.com/$Tenant/oauth2/token" + } + + $bearer = Invoke-ClientCredentialsGrant @bearerParms | Get-BearerToken + } + else { + try { + # Check if already connected + $context = Get-AzContext + if (-not $context) { + Write-PSFMessage -Level Verbose -Message "Not connected to Azure. Initiating login..." + Connect-AzAccount -Tenant $Tenant -ErrorAction Stop + } + elseif ($context.Tenant.Id -ne $Tenant) { + Write-PSFMessage -Level Verbose -Message "Connected to different tenant. Reconnecting to specified tenant..." + Connect-AzAccount -Tenant $Tenant -ErrorAction Stop + } + else { + Write-PSFMessage -Level Verbose -Message "Already connected to Azure tenant $Tenant" + } - $bearer = Invoke-ClientCredentialsGrant @bearerParms | Get-BearerToken + # Get access token for the Power Platform API + Write-PSFMessage -Level Verbose -Message "Requesting access token for resource: $Url" + $token = Get-AzAccessToken -ResourceUrl $Url -AsSecureString -ErrorAction Stop + $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($token.Token) + $tokenAsPlainString = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($BSTR) + $bearer = "Bearer $($tokenAsPlainString)" + } + catch { + Write-PSFMessage -Level Host -Message "Failed to authenticate using interactive login" -Exception $PSItem.Exception + Stop-PSFFunction -Message "Stopping because of authentication errors" + return + } + } $headers = @{ 'Authorization' = $bearer @@ -248,7 +300,7 @@ function Request-D365DatabaseJITAccess { } catch { Write-PSFMessage -Level Host -Message "Something went wrong while requesting JIT database access" -Exception $PSItem.Exception - Stop-PSFFunction -Message "Stopping because of errors" -StepsUpward 1 + Stop-PSFFunction -Message "Stopping because of errors" return } } From d3f47935c8afd4ddc292a31687e6f7590b7731c2 Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sat, 4 Oct 2025 14:12:19 +0000 Subject: [PATCH 6/8] add parameter for automatic start of SQL Server Management Studio --- .../request-d365databasejitaccess.ps1 | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/d365fo.tools/functions/request-d365databasejitaccess.ps1 b/d365fo.tools/functions/request-d365databasejitaccess.ps1 index 61f2f380..e1d264f9 100644 --- a/d365fo.tools/functions/request-d365databasejitaccess.ps1 +++ b/d365fo.tools/functions/request-d365databasejitaccess.ps1 @@ -61,6 +61,19 @@ Default value is "Administrative access via d365fo.tools" + .PARAMETER SQLServerManagementStudioPath + The full path to the SQL Server Management Studio executable (ssms.exe) + + If provided, the function will automatically open SQL Server Management Studio and connect to the database using the obtained credentials. + + Example: "C:\Program Files\Microsoft SQL Server Management Studio 21\Release\Common7\IDE\SSMS.exe" + + Note: Since version 18, SQL Server Management Studio does no longer allow providing the password directly in the command line. The password will be copied to clipboard instead for easy pasting. It will be cleared from clipboard after 60 seconds. + + Note: After SQL Server Management Studio has been started this way, it will display a "Connect to the following server?" warning dialog. Confirm it with "Yes". Next, because of the missing password, a "Connect to server" error dialog will be shown. Confirm it with "OK". Finally, a "Connect to server" form will be shown where the password can be pasted and the connection be established with the "Connect" button. Answering "No" on the first warning dialog will take you directly to the "Connect to server" form, but the database information will not be pre-filled. + + Note: The connection may fail at first because it takes some time until the client's IP address is whitelisted in the Azure SQL Database firewall rules. If that happens, just try again after a minute or so. + .PARAMETER RawOutput Instructs the cmdlet to include the outer structure of the response received from the endpoint @@ -136,6 +149,15 @@ It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the client id and secret provided through the credential object. + .EXAMPLE + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -SQLServerManagementStudioPath "C:\Program Files\Microsoft SQL Server Management Studio 21\Release\Common7\IDE\SSMS.exe" + + This will request JIT database access for the D365FO environment using interactive authentication. + It will open SQL Server Management Studio and connect to the database using the obtained credentials. + It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". + It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". + It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". + .NOTES Tags: JIT, Database, Access, UDE, OData, RestApi @@ -172,6 +194,8 @@ function Request-D365DatabaseJITAccess { [string] $Reason = "Administrative access via d365fo.tools", + [string] $SQLServerManagementStudioPath, + [switch] $RawOutput, [switch] $OutputAsJson, @@ -272,6 +296,25 @@ function Request-D365DatabaseJITAccess { $response = Invoke-RestMethod -Uri $requestUrl -Method Post -Headers $headers -Body $body + if ($SQLServerManagementStudioPath -and (Test-Path -Path $SQLServerManagementStudioPath)) { + # Open SQL Management Studio and connect to the database using the obtained credentials + # Use the server name, database name, username, and password from the $response object + $serverName = $response.servername + $databaseName = $response.databasename + $username = $response.sqljitusername + $password = $response.sqljitpassword + + # Copy the password to clipboard for easy pasting + $password | Set-Clipboard + Read-Host "Password copied to clipboard. Once you confirm this message, SQL Server Management Studio will be started with the database connection. Read the -SQLServerManagementStudioPath parameter description before you confirm with Enter. Press Enter to continue." + & $SQLServerManagementStudioPath -S $serverName -d $databaseName -U $username + for ($i = 60; $i -gt 0; $i--) { + Write-Progress -Activity "JIT Database Access" -Status "Waiting for $i seconds before clearing clipboard..." -PercentComplete ((60 - $i) / 60 * 100) + Start-Sleep -Seconds 1 + } + Set-Clipboard " " + } + $result = $response if (-not $RawOutput) { # Extract the relevant information from the response From b98897f754a025b120a9ac069738a39c54fce161 Mon Sep 17 00:00:00 2001 From: FH-Inway <33372796+FH-Inway@users.noreply.github.com> Date: Sat, 4 Oct 2025 14:32:57 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=A4=96=20Fix=20best=20practice=20devi?= =?UTF-8?q?ations?= 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 | 125 ++++++++++ .../request-d365databasejitaccess.ps1 | 103 ++++---- .../Request-D365DatabaseJITAccess.Tests.ps1 | 133 +++++++--- docs/Request-D365DatabaseJITAccess.md | 235 +++++++++++++++--- 4 files changed, 483 insertions(+), 113 deletions(-) diff --git a/d365fo.tools/bin/d365fo.tools-index.json b/d365fo.tools/bin/d365fo.tools-index.json index 96f0d762..84a14cee 100644 --- a/d365fo.tools/bin/d365fo.tools-index.json +++ b/d365fo.tools/bin/d365fo.tools-index.json @@ -10635,6 +10635,131 @@ "Examples": "-------------------------- EXAMPLE 1 --------------------------\nPS C:\\\u003eRepair-D365BacpacModelFile -Path C:\\Temp\\Base.xml -PathRepairSimple \u0027\u0027 -PathRepairQualifier \u0027\u0027 -PathRepairReplace \u0027C:\\Temp\\RepairBacpac.Replace.Custom.json\u0027\nThis will only process the Replace section, as the other repair paths are empty - indicating to skip them.\r\nIt will load the instructions from the \u0027C:\\Temp\\RepairBacpac.Replace.Custom.json\u0027 file and run those in the Replace section.\n-------------------------- EXAMPLE 2 --------------------------\nPS C:\\\u003eRepair-D365BacpacModelFile -Path C:\\Temp\\Base.xml -KeepFiles -Force\nThis will process all repair sections.\r\nIt will keep the files in the temporary work directory, for the user to analyze the files further.\r\nIt will Force overwrite the output file, if it exists already.", "Syntax": "Repair-D365BacpacModelFile [-Path] \u003cString\u003e [[-OutputPath] \u003cString\u003e] [[-PathRepairSimple] \u003cString\u003e] [[-PathRepairQualifier] \u003cString\u003e] [[-PathRepairReplace] \u003cString\u003e] [-KeepFiles] [-Force] [\u003cCommonParameters\u003e]" }, + { + "CommandName": "Request-D365DatabaseJITAccess", + "Description": "Utilize the D365FO Power Platform OData API to request just in time access (JIT) to a UDE database\n\nThis will allow you to get temporary database credentials for connecting to the database directly\n\nIf no credentials are provided (ClientId/ClientSecret or Credential), the function will automatically use interactive authentication via Azure PowerShell.", + "Tags": [ + "JIT", + "Database", + "Access", + "UDE", + "OData", + "RestApi" + ], + "Params": [ + [ + "Url", + "URL / URI for the D365FO Power Platform environment that provides the JIT access API.\nNote: This is not the URL of the D365FO environment itself (aka the Finance and Operations URL). Instead, it is the URL of the Power Platform environment (aka the Environment URL) that the D365FO \r\nenvironment is integrated with.\nFor example: \"https://operations-acme-uat.crm4.dynamics.com/\"", + "", + true, + "false", + "" + ], + [ + "ClientId", + "The ClientId obtained from the Azure Portal when you created a Registered Application", + "", + true, + "false", + "" + ], + [ + "ClientSecretAsPlainString", + "The ClientSecret obtained from the Azure Portal when you created a Registered Application\nThis is the plain text version of the ClientSecret parameter.\nEither ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided.", + "", + true, + "false", + "" + ], + [ + "ClientSecretAsSecureString", + "The ClientSecret obtained from the Azure Portal when you created a Registered Application\nThis is the secure string version of the ClientSecret parameter.\nEither ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided.", + "", + true, + "false", + "" + ], + [ + "Credential", + "The Credential object containing Username (ClientId) and Password (ClientSecret)\nThe Username will be used as ClientId\r\nThe Password will be used as ClientSecret\nEither ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided.", + "", + true, + "false", + "" + ], + [ + "Tenant", + "Azure Active Directory (AAD) tenant id (Guid) that the D365FO environment is connected to, that you want to access", + "", + true, + "false", + "" + ], + [ + "ClientIPAddress", + "The IP address of the client that needs database access\nDefault value is \"127.0.0.1\" which will be replaced with the public IP address of the client as determined by querying \"https://icanhazip.com\"", + "", + false, + "false", + "127.0.0.1" + ], + [ + "Role", + "The database role to assign to the JIT access\nValid options are \"Reader\" and \"Writer\"\nDefault value is \"Reader\"", + "", + false, + "false", + "Reader" + ], + [ + "Reason", + "The reason for requesting JIT database access\nDefault value is \"Administrative access via d365fo.tools\"", + "", + false, + "false", + "Administrative access via d365fo.tools" + ], + [ + "SQLServerManagementStudioPath", + "The full path to the SQL Server Management Studio executable (ssms.exe)\nIf provided, the function will automatically open SQL Server Management Studio and connect to the database using the obtained credentials.\nExample: \"C:\\Program Files\\Microsoft SQL Server Management Studio 21\\Release\\Common7\\IDE\\SSMS.exe\"\nNote: Since version 18, SQL Server Management Studio does no longer allow providing the password directly in the command line. The password will be copied to clipboard instead for easy pasting. It \r\nwill be cleared from clipboard after 60 seconds.\nNote: After SQL Server Management Studio has been started this way, it will display a \"Connect to the following server?\" warning dialog. Confirm it with \"Yes\". Next, because of the missing password, \r\na \"Connect to server\" error dialog will be shown. Confirm it with \"OK\". Finally, a \"Connect to server\" form will be shown where the password can be pasted and the connection be established with the \r\n\"Connect\" button. Answering \"No\" on the first warning dialog will take you directly to the \"Connect to server\" form, but the database information will not be pre-filled.\nNote: The connection may fail at first because it takes some time until the client\u0027s IP address is whitelisted in the Azure SQL Database firewall rules. If that happens, just try again after a minute \r\nor so.", + "", + false, + "false", + "" + ], + [ + "RawOutput", + "Instructs the cmdlet to include the outer structure of the response received from the endpoint\nThe output will still be a PSCustomObject", + "", + false, + "false", + "False" + ], + [ + "OutputAsJson", + "Instructs the cmdlet to convert the output to a Json string", + "", + false, + "false", + "False" + ], + [ + "EnableException", + "This parameters disables user-friendly warnings and enables the throwing of exceptions\r\nThis is less user friendly, but allows catching exceptions in calling scripts\r\nUsually this parameter is not used directly, but via the Enable-D365Exception cmdlet\r\nSee https://github.com/d365collaborative/d365fo.tools/wiki/Exception-handling#what-does-the--enableexception-parameter-do for further information", + "", + false, + "false", + "False" + ] + ], + "Alias": "", + "Author": "Florian Hopfner (@FH-Inway)", + "Synopsis": "Request just in time (JIT) database access for a unified development environment (UDE)", + "Name": "Request-D365DatabaseJITAccess", + "Links": null, + "Examples": "-------------------------- EXAMPLE 1 --------------------------\nPS C:\\\u003eRequest-D365DatabaseJITAccess -Url \"https://operations-acme-uat.crm4.dynamics.com/\" -Tenant \"e674da86-7ee5-40a7-b777-1111111111111\"\nThis will request JIT database access for the D365FO environment using interactive authentication.\r\nIt will prompt you to sign in with your Azure AD credentials if not already signed in.\r\nIt will use the client\u0027s IP address, role \"Reader\", and reason \"Administrative access via d365fo.tools\".\r\nIt will contact the D365FO instance specified in the Url parameter: \"https://operations-acme-uat.crm4.dynamics.com/\".\r\nIt will authenticate against the Azure Active Directory with the specified Tenant parameter: \"e674da86-7ee5-40a7-b777-1111111111111\".\n-------------------------- EXAMPLE 2 --------------------------\nPS C:\\\u003eRequest-D365DatabaseJITAccess -Url \"https://operations-acme-uat.crm4.dynamics.com/\" -Tenant \"e674da86-7ee5-40a7-b777-1111111111111\" -ClientId \"dea8d7a9-1602-4429-b138-111111111111\" \r\n-ClientSecretAsPlainString \"Vja/VmdxaLOPR+alkjfsadffelkjlfw234522\"\nThis will request JIT database access for the D365FO environment.\r\nIt will use the client\u0027s IP address, role \"Reader\", and reason \"Administrative access via d365fo.tools\".\r\nIt will contact the D365FO instance specified in the Url parameter: \"https://operations-acme-uat.crm4.dynamics.com/\".\r\nIt will authenticate against the \"https://login.microsoftonline.com/e674da86-7ee5-40a7-b777-1111111111111/oauth2/token\" url with the specified Tenant parameter: \r\n\"e674da86-7ee5-40a7-b777-1111111111111\".\r\nIt will authenticate with the specified ClientId parameter: \"dea8d7a9-1602-4429-b138-111111111111\".\r\nIt will authenticate with the specified ClientSecretAsPlainString parameter: \"Vja/VmdxaLOPR+alkjfsadffelkjlfw234522\".\n-------------------------- EXAMPLE 3 --------------------------\nPS C:\\\u003eRequest-D365DatabaseJITAccess -Url \"https://operations-acme-uat.crm4.dynamics.com/\" -Tenant \"e674da86-7ee5-40a7-b777-1111111111111\" -ClientId \"dea8d7a9-1602-4429-b138-111111111111\" \r\n-ClientSecretAsPlainString \"Vja/VmdxaLOPR+alkjfsadffelkjlfw234522\" -ClientIPAddress \"192.168.1.100\" -Role \"Writer\" -Reason \"Development work\"\nThis will request JIT database access for the D365FO environment with Writer privileges.\r\nIt will use the client IP address \"192.168.1.100\", role \"Writer\", and reason \"Development work\".\r\nIt will contact the D365FO instance specified in the Url parameter: \"https://operations-acme-uat.crm4.dynamics.com/\".\r\nIt will authenticate against the Azure Active Directory with the specified Tenant parameter: \"e674da86-7ee5-40a7-b777-1111111111111\".\r\nIt will authenticate with the specified ClientId parameter: \"dea8d7a9-1602-4429-b138-111111111111\".\r\nIt will authenticate with the specified ClientSecretAsPlainString parameter: \"Vja/VmdxaLOPR+alkjfsadffelkjlfw234522\".\n-------------------------- EXAMPLE 4 --------------------------\nPS C:\\\u003eRequest-D365DatabaseJITAccess -Url \"https://operations-acme-uat.crm4.dynamics.com/\" -Tenant \"e674da86-7ee5-40a7-b777-1111111111111\" -ClientId \"dea8d7a9-1602-4429-b138-111111111111\" \r\n-ClientSecretAsPlainString \"Vja/VmdxaLOPR+alkjfsadffelkjlfw234522\" -OutputAsJson\nThis will request JIT database access for the D365FO environment and display the result as json.\r\nIt will contact the D365FO instance specified in the Url parameter: \"https://operations-acme-uat.crm4.dynamics.com/\".\r\nIt will authenticate against the Azure Active Directory with the specified Tenant parameter: \"e674da86-7ee5-40a7-b777-1111111111111\".\r\nIt will authenticate with the specified ClientId parameter: \"dea8d7a9-1602-4429-b138-111111111111\".\r\nIt will authenticate with the specified ClientSecretAsPlainString parameter: \"Vja/VmdxaLOPR+alkjfsadffelkjlfw234522\".\n-------------------------- EXAMPLE 5 --------------------------\nPS C:\\\u003e$clientSecretSecure = Read-Host -AsSecureString \"Enter the Client Secret\"\nPS C:\\\u003e Request-D365DatabaseJITAccess -Url \"https://operations-acme-uat.crm4.dynamics.com/\" -Tenant \"e674da86-7ee5-40a7-b777-1111111111111\" -ClientId \"dea8d7a9-1602-4429-b138-111111111111\" \r\n-ClientSecretAsSecureString $clientSecretSecure\nThis will prompt the user to enter the client secret securely (the input will be masked).\r\nThen it will request JIT database access for the D365FO environment using the secure string for authentication.\r\nIt will use the client\u0027s IP address, role \"Reader\", and reason \"Administrative access via d365fo.tools\".\r\nIt will contact the D365FO instance specified in the Url parameter: \"https://operations-acme-uat.crm4.dynamics.com/\".\r\nIt will authenticate against the Azure Active Directory with the specified Tenant parameter: \"e674da86-7ee5-40a7-b777-1111111111111\".\r\nIt will authenticate with the specified ClientId parameter: \"dea8d7a9-1602-4429-b138-111111111111\".\r\nIt will authenticate with the client secret provided through the secure prompt.\n-------------------------- EXAMPLE 6 --------------------------\nPS C:\\\u003e$credential = Get-Credential -UserName \"dea8d7a9-1602-4429-b138-111111111111\" -Message \"Enter the Client Secret\"\nPS C:\\\u003e Request-D365DatabaseJITAccess -Url \"https://operations-acme-uat.crm4.dynamics.com/\" -Tenant \"e674da86-7ee5-40a7-b777-1111111111111\" -Credential $credential\nThis will prompt the user to enter the client secret through a secure credential dialog (using the ClientId as the username).\r\nThen it will request JIT database access for the D365FO environment using the credential for authentication.\r\nIt will use the client\u0027s IP address, role \"Reader\", and reason \"Administrative access via d365fo.tools\".\r\nIt will contact the D365FO instance specified in the Url parameter: \"https://operations-acme-uat.crm4.dynamics.com/\".\r\nIt will authenticate against the Azure Active Directory with the specified Tenant parameter: \"e674da86-7ee5-40a7-b777-1111111111111\".\r\nIt will authenticate with the client id and secret provided through the credential object.\n-------------------------- EXAMPLE 7 --------------------------\nPS C:\\\u003eRequest-D365DatabaseJITAccess -Url \"https://operations-acme-uat.crm4.dynamics.com/\" -Tenant \"e674da86-7ee5-40a7-b777-1111111111111\" -SQLServerManagementStudioPath \"C:\\Program Files\\Microsoft \r\nSQL Server Management Studio 21\\Release\\Common7\\IDE\\SSMS.exe\"\nThis will request JIT database access for the D365FO environment using interactive authentication.\r\nIt will open SQL Server Management Studio and connect to the database using the obtained credentials.\r\nIt will use the client\u0027s IP address, role \"Reader\", and reason \"Administrative access via d365fo.tools\".\r\nIt will contact the D365FO instance specified in the Url parameter: \"https://operations-acme-uat.crm4.dynamics.com/\".\r\nIt will authenticate against the Azure Active Directory with the specified Tenant parameter: \"e674da86-7ee5-40a7-b777-1111111111111\".", + "Syntax": "Request-D365DatabaseJITAccess -Url \u003cString\u003e -Tenant \u003cString\u003e [-ClientIPAddress \u003cString\u003e] [-Role \u003cString\u003e] [-Reason \u003cString\u003e] [-SQLServerManagementStudioPath \u003cString\u003e] [-RawOutput] [-OutputAsJson] [-EnableException] [\u003cCommonParameters\u003e]\nRequest-D365DatabaseJITAccess -Url \u003cString\u003e -ClientId \u003cString\u003e -ClientSecretAsSecureString \u003cSecureString\u003e -Tenant \u003cString\u003e [-ClientIPAddress \u003cString\u003e] [-Role \u003cString\u003e] [-Reason \u003cString\u003e] [-SQLServerManagementStudioPath \u003cString\u003e] [-RawOutput] [-OutputAsJson] [-EnableException] [\u003cCommonParameters\u003e]\nRequest-D365DatabaseJITAccess -Url \u003cString\u003e -ClientId \u003cString\u003e -ClientSecretAsPlainString \u003cString\u003e -Tenant \u003cString\u003e [-ClientIPAddress \u003cString\u003e] [-Role \u003cString\u003e] [-Reason \u003cString\u003e] [-SQLServerManagementStudioPath \u003cString\u003e] [-RawOutput] [-OutputAsJson] [-EnableException] [\u003cCommonParameters\u003e]\nRequest-D365DatabaseJITAccess -Url \u003cString\u003e -Credential \u003cPSCredential\u003e -Tenant \u003cString\u003e [-ClientIPAddress \u003cString\u003e] [-Role \u003cString\u003e] [-Reason \u003cString\u003e] [-SQLServerManagementStudioPath \u003cString\u003e] [-RawOutput] [-OutputAsJson] [-EnableException] [\u003cCommonParameters\u003e]" + }, { "CommandName": "Restart-D365Environment", "Description": "Restart the different services in a Dynamics 365 Finance \u0026 Operations environment", diff --git a/d365fo.tools/functions/request-d365databasejitaccess.ps1 b/d365fo.tools/functions/request-d365databasejitaccess.ps1 index e1d264f9..01bde9bf 100644 --- a/d365fo.tools/functions/request-d365databasejitaccess.ps1 +++ b/d365fo.tools/functions/request-d365databasejitaccess.ps1 @@ -1,135 +1,136 @@ -<# + +<# .SYNOPSIS Request just in time (JIT) database access for a unified development environment (UDE) - + .DESCRIPTION Utilize the D365FO Power Platform OData API to request just in time access (JIT) to a UDE database - + This will allow you to get temporary database credentials for connecting to the database directly - + If no credentials are provided (ClientId/ClientSecret or Credential), the function will automatically use interactive authentication via Azure PowerShell. - + .PARAMETER Url URL / URI for the D365FO Power Platform environment that provides the JIT access API. - + Note: This is not the URL of the D365FO environment itself (aka the Finance and Operations URL). Instead, it is the URL of the Power Platform environment (aka the Environment URL) that the D365FO environment is integrated with. - + For example: "https://operations-acme-uat.crm4.dynamics.com/" - + .PARAMETER ClientId The ClientId obtained from the Azure Portal when you created a Registered Application - + .PARAMETER ClientSecretAsPlainString The ClientSecret obtained from the Azure Portal when you created a Registered Application - + This is the plain text version of the ClientSecret parameter. - + Either ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided. - + .PARAMETER ClientSecretAsSecureString The ClientSecret obtained from the Azure Portal when you created a Registered Application - + This is the secure string version of the ClientSecret parameter. - + Either ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided. - + .PARAMETER Credential The Credential object containing Username (ClientId) and Password (ClientSecret) - + The Username will be used as ClientId The Password will be used as ClientSecret - + Either ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided. - + .PARAMETER Tenant Azure Active Directory (AAD) tenant id (Guid) that the D365FO environment is connected to, that you want to access - + .PARAMETER ClientIPAddress The IP address of the client that needs database access - + Default value is "127.0.0.1" which will be replaced with the public IP address of the client as determined by querying "https://icanhazip.com" - + .PARAMETER Role The database role to assign to the JIT access - + Valid options are "Reader" and "Writer" - + Default value is "Reader" - + .PARAMETER Reason The reason for requesting JIT database access - + Default value is "Administrative access via d365fo.tools" - + .PARAMETER SQLServerManagementStudioPath The full path to the SQL Server Management Studio executable (ssms.exe) - + If provided, the function will automatically open SQL Server Management Studio and connect to the database using the obtained credentials. - + Example: "C:\Program Files\Microsoft SQL Server Management Studio 21\Release\Common7\IDE\SSMS.exe" - + Note: Since version 18, SQL Server Management Studio does no longer allow providing the password directly in the command line. The password will be copied to clipboard instead for easy pasting. It will be cleared from clipboard after 60 seconds. Note: After SQL Server Management Studio has been started this way, it will display a "Connect to the following server?" warning dialog. Confirm it with "Yes". Next, because of the missing password, a "Connect to server" error dialog will be shown. Confirm it with "OK". Finally, a "Connect to server" form will be shown where the password can be pasted and the connection be established with the "Connect" button. Answering "No" on the first warning dialog will take you directly to the "Connect to server" form, but the database information will not be pre-filled. - + Note: The connection may fail at first because it takes some time until the client's IP address is whitelisted in the Azure SQL Database firewall rules. If that happens, just try again after a minute or so. - + .PARAMETER RawOutput Instructs the cmdlet to include the outer structure of the response received from the endpoint - + The output will still be a PSCustomObject - + .PARAMETER OutputAsJson Instructs the cmdlet to convert the output to a Json string - + .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions This is less user friendly, but allows catching exceptions in calling scripts Usually this parameter is not used directly, but via the Enable-D365Exception cmdlet See https://github.com/d365collaborative/d365fo.tools/wiki/Exception-handling#what-does-the--enableexception-parameter-do for further information - + .EXAMPLE PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" - + This will request JIT database access for the D365FO environment using interactive authentication. It will prompt you to sign in with your Azure AD credentials if not already signed in. It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". - + .EXAMPLE PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" - + This will request JIT database access for the D365FO environment. It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the "https://login.microsoftonline.com/e674da86-7ee5-40a7-b777-1111111111111/oauth2/token" url with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". - + .EXAMPLE PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -ClientIPAddress "192.168.1.100" -Role "Writer" -Reason "Development work" - + This will request JIT database access for the D365FO environment with Writer privileges. It will use the client IP address "192.168.1.100", role "Writer", and reason "Development work". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". - + .EXAMPLE PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -OutputAsJson - + This will request JIT database access for the D365FO environment and display the result as json. It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". - + .EXAMPLE PS C:\> $clientSecretSecure = Read-Host -AsSecureString "Enter the Client Secret" PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsSecureString $clientSecretSecure - + This will prompt the user to enter the client secret securely (the input will be masked). Then it will request JIT database access for the D365FO environment using the secure string for authentication. It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". @@ -137,32 +138,32 @@ It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". It will authenticate with the client secret provided through the secure prompt. - + .EXAMPLE PS C:\> $credential = Get-Credential -UserName "dea8d7a9-1602-4429-b138-111111111111" -Message "Enter the Client Secret" PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -Credential $credential - + This will prompt the user to enter the client secret through a secure credential dialog (using the ClientId as the username). Then it will request JIT database access for the D365FO environment using the credential for authentication. It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the client id and secret provided through the credential object. - + .EXAMPLE PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -SQLServerManagementStudioPath "C:\Program Files\Microsoft SQL Server Management Studio 21\Release\Common7\IDE\SSMS.exe" - + This will request JIT database access for the D365FO environment using interactive authentication. It will open SQL Server Management Studio and connect to the database using the obtained credentials. It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". - + .NOTES Tags: JIT, Database, Access, UDE, OData, RestApi - + Author: Florian Hopfner (@FH-Inway) - + #> function Request-D365DatabaseJITAccess { [CmdletBinding(DefaultParameterSetName = 'ByInteractiveLogin')] diff --git a/d365fo.tools/tests/functions/Request-D365DatabaseJITAccess.Tests.ps1 b/d365fo.tools/tests/functions/Request-D365DatabaseJITAccess.Tests.ps1 index a2504dc1..fb546156 100644 --- a/d365fo.tools/tests/functions/Request-D365DatabaseJITAccess.Tests.ps1 +++ b/d365fo.tools/tests/functions/Request-D365DatabaseJITAccess.Tests.ps1 @@ -8,7 +8,7 @@ Describe "Ensuring unchanged command signature" { It "should have the expected parameter sets" { - (Get-Command Request-D365DatabaseJITAccess).ParameterSets.Name | Should -Be '__AllParameterSets' + (Get-Command Request-D365DatabaseJITAccess).ParameterSets.Name | Should -Be 'ByInteractiveLogin', 'ByClientSecretAsSecureString', 'ByClientSecretAsPlainString', 'ByCredential' } It 'Should have the expected parameter Url' { @@ -19,7 +19,7 @@ $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True - $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 0 + $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 @@ -29,26 +29,58 @@ $parameter.Name | Should -Be 'ClientId' $parameter.ParameterType.ToString() | Should -Be System.String $parameter.IsDynamic | Should -Be $False - $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' - $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' - $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True - $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 1 - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be 'ByClientSecretAsSecureString', 'ByClientSecretAsPlainString' + $parameter.ParameterSets.Keys | Should -Contain 'ByClientSecretAsSecureString' + $parameter.ParameterSets['ByClientSecretAsSecureString'].IsMandatory | Should -Be $True + $parameter.ParameterSets['ByClientSecretAsSecureString'].Position | Should -Be -2147483648 + $parameter.ParameterSets['ByClientSecretAsSecureString'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['ByClientSecretAsSecureString'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['ByClientSecretAsSecureString'].ValueFromRemainingArguments | Should -Be $False + $parameter.ParameterSets.Keys | Should -Contain 'ByClientSecretAsPlainString' + $parameter.ParameterSets['ByClientSecretAsPlainString'].IsMandatory | Should -Be $True + $parameter.ParameterSets['ByClientSecretAsPlainString'].Position | Should -Be -2147483648 + $parameter.ParameterSets['ByClientSecretAsPlainString'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['ByClientSecretAsPlainString'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['ByClientSecretAsPlainString'].ValueFromRemainingArguments | Should -Be $False } - It 'Should have the expected parameter ClientSecret' { - $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['ClientSecret'] - $parameter.Name | Should -Be 'ClientSecret' + It 'Should have the expected parameter ClientSecretAsPlainString' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['ClientSecretAsPlainString'] + $parameter.Name | Should -Be 'ClientSecretAsPlainString' $parameter.ParameterType.ToString() | Should -Be System.String $parameter.IsDynamic | Should -Be $False - $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' - $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' - $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True - $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 2 - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipeline | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False - $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be 'ByClientSecretAsPlainString' + $parameter.ParameterSets.Keys | Should -Contain 'ByClientSecretAsPlainString' + $parameter.ParameterSets['ByClientSecretAsPlainString'].IsMandatory | Should -Be $True + $parameter.ParameterSets['ByClientSecretAsPlainString'].Position | Should -Be -2147483648 + $parameter.ParameterSets['ByClientSecretAsPlainString'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['ByClientSecretAsPlainString'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['ByClientSecretAsPlainString'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter ClientSecretAsSecureString' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['ClientSecretAsSecureString'] + $parameter.Name | Should -Be 'ClientSecretAsSecureString' + $parameter.ParameterType.ToString() | Should -Be System.Security.SecureString + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be 'ByClientSecretAsSecureString' + $parameter.ParameterSets.Keys | Should -Contain 'ByClientSecretAsSecureString' + $parameter.ParameterSets['ByClientSecretAsSecureString'].IsMandatory | Should -Be $True + $parameter.ParameterSets['ByClientSecretAsSecureString'].Position | Should -Be -2147483648 + $parameter.ParameterSets['ByClientSecretAsSecureString'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['ByClientSecretAsSecureString'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['ByClientSecretAsSecureString'].ValueFromRemainingArguments | Should -Be $False + } + It 'Should have the expected parameter Credential' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['Credential'] + $parameter.Name | Should -Be 'Credential' + $parameter.ParameterType.ToString() | Should -Be System.Management.Automation.PSCredential + $parameter.IsDynamic | Should -Be $False + $parameter.ParameterSets.Keys | Should -Be 'ByCredential' + $parameter.ParameterSets.Keys | Should -Contain 'ByCredential' + $parameter.ParameterSets['ByCredential'].IsMandatory | Should -Be $True + $parameter.ParameterSets['ByCredential'].Position | Should -Be -2147483648 + $parameter.ParameterSets['ByCredential'].ValueFromPipeline | Should -Be $False + $parameter.ParameterSets['ByCredential'].ValueFromPipelineByPropertyName | Should -Be $False + $parameter.ParameterSets['ByCredential'].ValueFromRemainingArguments | Should -Be $False } It 'Should have the expected parameter Tenant' { $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['Tenant'] @@ -58,7 +90,7 @@ $parameter.ParameterSets.Keys | Should -Be '__AllParameterSets' $parameter.ParameterSets.Keys | Should -Contain '__AllParameterSets' $parameter.ParameterSets['__AllParameterSets'].IsMandatory | Should -Be $True - $parameter.ParameterSets['__AllParameterSets'].Position | Should -Be 3 + $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 @@ -71,7 +103,7 @@ $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 4 + $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 @@ -84,7 +116,7 @@ $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 5 + $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 @@ -97,7 +129,20 @@ $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 6 + $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 + } + It 'Should have the expected parameter SQLServerManagementStudioPath' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['SQLServerManagementStudioPath'] + $parameter.Name | Should -Be 'SQLServerManagementStudioPath' + $parameter.ParameterType.ToString() | Should -Be System.String + $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 @@ -128,12 +173,44 @@ $parameter.ParameterSets['__AllParameterSets'].ValueFromPipelineByPropertyName | Should -Be $False $parameter.ParameterSets['__AllParameterSets'].ValueFromRemainingArguments | Should -Be $False } + It 'Should have the expected parameter EnableException' { + $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['EnableException'] + $parameter.Name | Should -Be 'EnableException' + $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 "Ensuring ValidateSet works on Role parameter" { - It "should only allow 'Reader' and 'Writer' for Role parameter" { - $parameter = (Get-Command Request-D365DatabaseJITAccess).Parameters['Role'] - $parameter.Attributes.ValidValues | Should -Be @('Reader', 'Writer') - } + Describe "Testing parameterset ByInteractiveLogin" { + <# + ByInteractiveLogin -Url -Tenant + ByInteractiveLogin -Url -Tenant -ClientIPAddress -Role -Reason -SQLServerManagementStudioPath -RawOutput -OutputAsJson -EnableException + #> + } + Describe "Testing parameterset ByClientSecretAsSecureString" { + <# + ByClientSecretAsSecureString -Url -ClientId -ClientSecretAsSecureString -Tenant + ByClientSecretAsSecureString -Url -ClientId -ClientSecretAsSecureString -Tenant -ClientIPAddress -Role -Reason -SQLServerManagementStudioPath -RawOutput -OutputAsJson -EnableException + #> + } + Describe "Testing parameterset ByClientSecretAsPlainString" { + <# + ByClientSecretAsPlainString -Url -ClientId -ClientSecretAsPlainString -Tenant + ByClientSecretAsPlainString -Url -ClientId -ClientSecretAsPlainString -Tenant -ClientIPAddress -Role -Reason -SQLServerManagementStudioPath -RawOutput -OutputAsJson -EnableException + #> + } + Describe "Testing parameterset ByCredential" { + <# + ByCredential -Url -Credential -Tenant + ByCredential -Url -Credential -Tenant -ClientIPAddress -Role -Reason -SQLServerManagementStudioPath -RawOutput -OutputAsJson -EnableException + #> } + } \ No newline at end of file diff --git a/docs/Request-D365DatabaseJITAccess.md b/docs/Request-D365DatabaseJITAccess.md index 0ce349c1..60d2701a 100644 --- a/docs/Request-D365DatabaseJITAccess.md +++ b/docs/Request-D365DatabaseJITAccess.md @@ -1,4 +1,4 @@ ---- +--- external help file: d365fo.tools-help.xml Module Name: d365fo.tools online version: @@ -12,34 +12,69 @@ Request just in time (JIT) database access for a unified development environment ## SYNTAX +### ByInteractiveLogin (Default) +``` +Request-D365DatabaseJITAccess -Url -Tenant [-ClientIPAddress ] [-Role ] + [-Reason ] [-SQLServerManagementStudioPath ] [-RawOutput] [-OutputAsJson] [-EnableException] + [] +``` + +### ByClientSecretAsSecureString ``` -Request-D365DatabaseJITAccess [-Url] [-ClientId] [-ClientSecret] - [-Tenant] [[-ClientIPAddress] ] [[-Role] ] [[-Reason] ] [-RawOutput] - [-OutputAsJson] [] +Request-D365DatabaseJITAccess -Url -ClientId -ClientSecretAsSecureString + -Tenant [-ClientIPAddress ] [-Role ] [-Reason ] + [-SQLServerManagementStudioPath ] [-RawOutput] [-OutputAsJson] [-EnableException] [] +``` + +### ByClientSecretAsPlainString +``` +Request-D365DatabaseJITAccess -Url -ClientId -ClientSecretAsPlainString + -Tenant [-ClientIPAddress ] [-Role ] [-Reason ] + [-SQLServerManagementStudioPath ] [-RawOutput] [-OutputAsJson] [-EnableException] [] +``` + +### ByCredential +``` +Request-D365DatabaseJITAccess -Url -Credential -Tenant + [-ClientIPAddress ] [-Role ] [-Reason ] [-SQLServerManagementStudioPath ] + [-RawOutput] [-OutputAsJson] [-EnableException] [] ``` ## DESCRIPTION -Utilize the D365FO OData API to request just in time access (JIT) to a UDE database +Utilize the D365FO Power Platform OData API to request just in time access (JIT) to a UDE database This will allow you to get temporary database credentials for connecting to the database directly +If no credentials are provided (ClientId/ClientSecret or Credential), the function will automatically use interactive authentication via Azure PowerShell. + ## EXAMPLES ### EXAMPLE 1 ``` -Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" +Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" +``` + +This will request JIT database access for the D365FO environment using interactive authentication. +It will prompt you to sign in with your Azure AD credentials if not already signed in. +It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". +It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". +It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". + +### EXAMPLE 2 +``` +Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" ``` This will request JIT database access for the D365FO environment. -It will use the default client IP address "127.0.0.1", role "Reader", and reason "Administrative access via d365fo.tools". +It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the "https://login.microsoftonline.com/e674da86-7ee5-40a7-b777-1111111111111/oauth2/token" url with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". -It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". +It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". -### EXAMPLE 2 +### EXAMPLE 3 ``` -Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -ClientIPAddress "192.168.1.100" -Role "Writer" -Reason "Development work" +Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -ClientIPAddress "192.168.1.100" -Role "Writer" -Reason "Development work" ``` This will request JIT database access for the D365FO environment with Writer privileges. @@ -47,27 +82,68 @@ It will use the client IP address "192.168.1.100", role "Writer", and reason "De It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". -It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". +It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". -### EXAMPLE 3 +### EXAMPLE 4 ``` -Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecret "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -OutputAsJson +Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -OutputAsJson ``` This will request JIT database access for the D365FO environment and display the result as json. It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". -It will authenticate with the specified ClientSecret parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". +It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + +### EXAMPLE 5 +``` +$clientSecretSecure = Read-Host -AsSecureString "Enter the Client Secret" +``` + +PS C:\\\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsSecureString $clientSecretSecure + +This will prompt the user to enter the client secret securely (the input will be masked). +Then it will request JIT database access for the D365FO environment using the secure string for authentication. +It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". +It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". +It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". +It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". +It will authenticate with the client secret provided through the secure prompt. + +### EXAMPLE 6 +``` +$credential = Get-Credential -UserName "dea8d7a9-1602-4429-b138-111111111111" -Message "Enter the Client Secret" +``` + +PS C:\\\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -Credential $credential + +This will prompt the user to enter the client secret through a secure credential dialog (using the ClientId as the username). +Then it will request JIT database access for the D365FO environment using the credential for authentication. +It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". +It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". +It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". +It will authenticate with the client id and secret provided through the credential object. + +### EXAMPLE 7 +``` +Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -SQLServerManagementStudioPath "C:\Program Files\Microsoft SQL Server Management Studio 21\Release\Common7\IDE\SSMS.exe" +``` + +This will request JIT database access for the D365FO environment using interactive authentication. +It will open SQL Server Management Studio and connect to the database using the obtained credentials. +It will use the client's IP address, role "Reader", and reason "Administrative access via d365fo.tools". +It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". +It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". ## PARAMETERS ### -Url -URL / URI for the D365FO environment you want to access +URL / URI for the D365FO Power Platform environment that provides the JIT access API. -If you are working against a D365FO instance, it will be the URL / URI for the instance itself +Note: This is not the URL of the D365FO environment itself (aka the Finance and Operations URL). +Instead, it is the URL of the Power Platform environment (aka the Environment URL) that the D365FO environment is integrated with. -This should be the full URL, e.g. "https://operations-acme-uat.crm4.dynamics.com/" +For example: "https://operations-acme-uat.crm4.dynamics.com/" ```yaml Type: String @@ -75,7 +151,7 @@ Parameter Sets: (All) Aliases: Required: True -Position: 0 +Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -86,26 +162,69 @@ The ClientId obtained from the Azure Portal when you created a Registered Applic ```yaml Type: String -Parameter Sets: (All) +Parameter Sets: ByClientSecretAsSecureString, ByClientSecretAsPlainString Aliases: Required: True -Position: 1 +Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False ``` -### -ClientSecret +### -ClientSecretAsPlainString The ClientSecret obtained from the Azure Portal when you created a Registered Application +This is the plain text version of the ClientSecret parameter. + +Either ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided. + ```yaml Type: String -Parameter Sets: (All) +Parameter Sets: ByClientSecretAsPlainString Aliases: Required: True -Position: 2 +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ClientSecretAsSecureString +The ClientSecret obtained from the Azure Portal when you created a Registered Application + +This is the secure string version of the ClientSecret parameter. + +Either ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided. + +```yaml +Type: SecureString +Parameter Sets: ByClientSecretAsSecureString +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Credential +The Credential object containing Username (ClientId) and Password (ClientSecret) + +The Username will be used as ClientId +The Password will be used as ClientSecret + +Either ClientSecretAsPlainString, ClientSecretAsSecureString, or Credential must be provided. + +```yaml +Type: PSCredential +Parameter Sets: ByCredential +Aliases: + +Required: True +Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -120,7 +239,7 @@ Parameter Sets: (All) Aliases: Required: True -Position: 3 +Position: Named Default value: None Accept pipeline input: False Accept wildcard characters: False @@ -129,7 +248,7 @@ Accept wildcard characters: False ### -ClientIPAddress The IP address of the client that needs database access -Default value is "127.0.0.1" for localhost access +Default value is "127.0.0.1" which will be replaced with the public IP address of the client as determined by querying "https://icanhazip.com" ```yaml Type: String @@ -137,7 +256,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 4 +Position: Named Default value: 127.0.0.1 Accept pipeline input: False Accept wildcard characters: False @@ -156,7 +275,7 @@ Parameter Sets: (All) Aliases: Required: False -Position: 5 +Position: Named Default value: Reader Accept pipeline input: False Accept wildcard characters: False @@ -165,8 +284,6 @@ Accept wildcard characters: False ### -Reason The reason for requesting JIT database access -This is logged for audit purposes - Default value is "Administrative access via d365fo.tools" ```yaml @@ -175,12 +292,45 @@ Parameter Sets: (All) Aliases: Required: False -Position: 6 +Position: Named Default value: Administrative access via d365fo.tools Accept pipeline input: False Accept wildcard characters: False ``` +### -SQLServerManagementStudioPath +The full path to the SQL Server Management Studio executable (ssms.exe) + +If provided, the function will automatically open SQL Server Management Studio and connect to the database using the obtained credentials. + +Example: "C:\Program Files\Microsoft SQL Server Management Studio 21\Release\Common7\IDE\SSMS.exe" + +Note: Since version 18, SQL Server Management Studio does no longer allow providing the password directly in the command line. +The password will be copied to clipboard instead for easy pasting. +It will be cleared from clipboard after 60 seconds. + +Note: After SQL Server Management Studio has been started this way, it will display a "Connect to the following server?" warning dialog. +Confirm it with "Yes". +Next, because of the missing password, a "Connect to server" error dialog will be shown. +Confirm it with "OK". +Finally, a "Connect to server" form will be shown where the password can be pasted and the connection be established with the "Connect" button. +Answering "No" on the first warning dialog will take you directly to the "Connect to server" form, but the database information will not be pre-filled. + +Note: The connection may fail at first because it takes some time until the client's IP address is whitelisted in the Azure SQL Database firewall rules. +If that happens, just try again after a minute or so. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### -RawOutput Instructs the cmdlet to include the outer structure of the response received from the endpoint @@ -213,6 +363,24 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -EnableException +This parameters disables user-friendly warnings and enables the throwing of exceptions +This is less user friendly, but allows catching exceptions in calling scripts +Usually this parameter is not used directly, but via the Enable-D365Exception cmdlet +See https://github.com/d365collaborative/d365fo.tools/wiki/Exception-handling#what-does-the--enableexception-parameter-do for further information + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). @@ -220,11 +388,10 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## OUTPUTS +### System.String ## NOTES Tags: JIT, Database, Access, UDE, OData, RestApi -Author: Mötz Jensen (@Splaxi) - -This cmdlet is inspired by the PowerShell script provided in GitHub issue for d365fo.tools +Author: Florian Hopfner (@FH-Inway) -## RELATED LINKS \ No newline at end of file +## RELATED LINKS From 2f3ed02b49db5f2ff46c7037dc161beee202fc1f Mon Sep 17 00:00:00 2001 From: Florian Hopfner Date: Sat, 4 Oct 2025 15:40:17 +0000 Subject: [PATCH 8/8] fix best practice deviations --- .../functions/request-d365databasejitaccess.ps1 | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/d365fo.tools/functions/request-d365databasejitaccess.ps1 b/d365fo.tools/functions/request-d365databasejitaccess.ps1 index 01bde9bf..913b276c 100644 --- a/d365fo.tools/functions/request-d365databasejitaccess.ps1 +++ b/d365fo.tools/functions/request-d365databasejitaccess.ps1 @@ -127,6 +127,15 @@ It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + .EXAMPLE + PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsPlainString "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522" -RawOutput + + This will request JIT database access for the D365FO environment and display the result as object with the content as it was received from the endpoint. + It will contact the D365FO instance specified in the Url parameter: "https://operations-acme-uat.crm4.dynamics.com/". + It will authenticate against the Azure Active Directory with the specified Tenant parameter: "e674da86-7ee5-40a7-b777-1111111111111". + It will authenticate with the specified ClientId parameter: "dea8d7a9-1602-4429-b138-111111111111". + It will authenticate with the specified ClientSecretAsPlainString parameter: "Vja/VmdxaLOPR+alkjfsadffelkjlfw234522". + .EXAMPLE PS C:\> $clientSecretSecure = Read-Host -AsSecureString "Enter the Client Secret" PS C:\> Request-D365DatabaseJITAccess -Url "https://operations-acme-uat.crm4.dynamics.com/" -Tenant "e674da86-7ee5-40a7-b777-1111111111111" -ClientId "dea8d7a9-1602-4429-b138-111111111111" -ClientSecretAsSecureString $clientSecretSecure @@ -166,6 +175,9 @@ #> function Request-D365DatabaseJITAccess { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingConvertToSecureStringWithPlainText', '', + Justification = 'Converting plain text to Secure.String provides protection against accidental exposure in logs etc. when used correctly. Encrypting the plain text first would make it too difficult to use.')] [CmdletBinding(DefaultParameterSetName = 'ByInteractiveLogin')] [OutputType([System.String])] param ( @@ -245,7 +257,7 @@ function Request-D365DatabaseJITAccess { } $bearer = Invoke-ClientCredentialsGrant @bearerParms | Get-BearerToken - } + } else { try { # Check if already connected @@ -321,7 +333,7 @@ function Request-D365DatabaseJITAccess { # Extract the relevant information from the response $selectParams = @{ TypeName = "D365FO.TOOLS.UDE.JITDatabaseAccess" - Property = @{Name = "SQLJITCredential"; Expression = { + Property = @{Name = "SQLJITCredential"; Expression = { $password = $_.sqljitpassword | ConvertTo-SecureString -AsPlainText -Force New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($_.sqljitusername, $password) }},