Skip to content

Conversation

@FH-Inway
Copy link
Member

@FH-Inway FH-Inway commented Oct 4, 2025

This pull request adds the new PowerShell cmdlet Request-D365DatabaseJITAccess for requesting just-in-time (JIT) database access for D365 Finance and Operations unified development environments (UDE). The cmdlet allows users to obtain temporary credentials for direct database access, supporting multiple authentication methods and integration with SQL Server Management Studio. The changes also register the new cmdlet in the module manifest.

Resolves #897

New experience

$params = @{
    Url = "https://my-unified-dev.crm4.dynamics.com/"
    Tenant = "e674da86-7ee5-40a7-b777-1111111111111"
    RawOutput = $true
}
Request-D365DatabaseJITAccess @params
image

Open SSMS

This is the simplest way to use the new command, but not necessarily the best. One quality of life parameter is -SQLServerManagementStudioPath, which lets you specify the path to the SQL Server Management Studio ssms.exe (usually the path looks similar to C:\Program Files (x86)\Microsoft SQL Server Management Studio 18\Common7\IDE\ssms.exe). This lets the command open SSMS with the connection information. Unfortunately, the password cannot be provided in newer SSMS versions. So instead, it is copied to the clipboard so the user can paste it in the connection form.

Unfortunately, getting to the connection form without the password is a bit tricky. First, you get a warning dialog asking if it should connect.
image
Since the password is missing at that point, this fails with an error dialog.
image
After that, you get to the connection form where the password can be pasted and the connection be established.
image
May take a few tries until the firewall accepts the IP address (the command will automatically determine the IP address, but can also be provided with the -ClientIPAddress parameter).

Export bacpac

The access information can also be used to export the database .bacpac file like so:

$params = @{
    Url = "https://my-unified-dev.crm4.dynamics.com/"
    Tenant = "e674da86-7ee5-40a7-b777-1111111111111"
    # Note no -RawOutput parameter is provided here. This causes the SQL JIT user name and password to be wrapped in a PSCredential property of the returned object, which provides additional security for the password.
}
$dbaccess = Request-D365DatabaseJITAccess @params

$bacpacParams = @{
    ExportModeTier2 = $true # Only with ExportModeTier2 can a SqlUser and SqlPwd be provided. We should add a switch ExportModeUDE with the same behavior to make it clearer that this can be used for UDE as well.
    ExportOnly = $true # Haven't tested the additional stuff around the bacpac export, it very likely will need some changes to work with UDE
    DatabaseServer = $dbaccess.ServerName
    DatabaseName = $dbaccess.DatabaseName
    SqlUser = $dbaccess.SQLJITCredential.UserName
    SqlPwd = $dbaccess.SQLJITCredential.GetNetworkCredential().Password # To connect to the database, the password is required again in plain text
    BacpacFile = "C:\Temp\weber-unified-dev.bacpac"
    Verbose = $true
}
New-D365Bacpac @bacpacParams

@FH-Inway FH-Inway requested a review from Splaxi October 4, 2025 17:01
@akasparas
Copy link

akasparas commented Oct 5, 2025

There is a better way to pass the password to SSMS--Credential Manager.
To use it in PowerShell you may need to Install and Include module for it. But then, you create New-StoredCredential and call SSMS.

$ssmsVersion = 19
$sqlServerGUID = "8c91a03d-f9b4-46c0-a305-b5dcc79ff907"
$newCreds = New-StoredCredential  -UserName $username -Password $password -Target "Microsoft:SSMS:{ssmsVersion}:${serverName}:${username}:${sqlServerGUID}:1" 
& $SQLServerManagementStudioPath -S $serverName -d $databaseName -U $username

@FH-Inway
Copy link
Member Author

FH-Inway commented Oct 5, 2025

Thank you @akasparas , I will look into that. Appreciate the hint!

@akasparas
Copy link

Aparently, Import of CredentialManager module is little different depending on Powershell version. Please take a look at Troy's comment https://learn.microsoft.com/en-us/answers/questions/459844/get-storedcredential-not-recognized-even-though-it?source=docs before someone complains that code is not working.

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 6, 2025

Awesome work!

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 6, 2025

New-StoredCredential -UserName $username -Password $password -Target

Below works - I just confirmed with locally on my machine, with static values from a Tier2.

New-StoredCredential -UserName $username -Password $password -Target "Microsoft:SSMS:${ssmsVersion}:${serverName}:${username}:${sqlServerGUID}:1" > $null

The ${} is a C# string interpolation - we should aim to stick with the known PowerShell way

New-StoredCredential -UserName $username -Password $password -Target "Microsoft:SSMS:$($ssmsVersion):$($serverName):$($username):$($sqlServerGUID):1" > $null

All my testing was done in PowerShell 5.1, with a set of Install-Module CredentialManager & Import-Module CredentialManager. The Install-Module was done, and then I started a fresh PowerShell session - from there the auto-import functionality worked as expected.

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 6, 2025

Here is a hack to find all SSMS installed on the machine:

# Oneliner
Get-ChildItem -Path Registry::HKEY_CLASSES_ROOT\ssms.c2s*\shell\Open\Command | Select-Object -ExpandProperty PsPath | ForEach-Object {(Get-ItemProperty -Path $_)."(Default)"}  | Select-String -Pattern '^[\"]?(.*ssms\.exe)["]?\s*"%1"' | ForEach-Object { $_.Matches.Groups[1].Value }

#Indented
Get-ChildItem `
    -Path Registry::HKEY_CLASSES_ROOT\ssms.c2s*\shell\Open\Command | `
    Select-Object -ExpandProperty PsPath | `
        ForEach-Object {(Get-ItemProperty -Path $_)."(Default)"} | `
            Select-String -Pattern '^[\"]?(.*ssms\.exe)["]?\s*"%1"' | `
                ForEach-Object { $_.Matches.Groups[1].Value }

#Formatted
Get-ChildItem `
    -Path Registry::HKEY_CLASSES_ROOT\ssms.c2s*\shell\Open\Command | `
    Select-Object -ExpandProperty PsPath | `
    ForEach-Object { (Get-ItemProperty -Path $_)."(Default)" } | `
    Select-String -Pattern '^[\"]?(.*ssms\.exe)["]?\s*"%1"' | `
    ForEach-Object { $_.Matches.Groups[1].Value }

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 6, 2025

Just tested with 20 and 21 of SSMS - without issues.

Just ran the New-StoredCredential cmdlet for 20 and 21, without issues. Started both ssms.exe versions from powershell and they connected as expected.

@akasparas
Copy link

If you have to search for registered SSMS paths anyway, maybe it makes sense to have parameter -ssms. Without arguments--latest, with argument--particular version? SSMS.exe usually have long path, so it would be less typing for admin.

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 6, 2025

If you have to search for registered SSMS paths anyway, maybe it makes sense to have parameter -ssms. Without arguments--latest, with argument--particular version? SSMS.exe usually have long path, so it would be less typing for admin.

I'm not entirely sure I fully understand. Please share more details how you would envision using cmdlets like this.

@FH-Inway
Copy link
Member Author

FH-Inway commented Oct 6, 2025

Thanks for the research, the hack to find the installed SSMS should be useful.

If you have to search for registered SSMS paths anyway, maybe it makes sense to have parameter -ssms. Without arguments--latest, with argument--particular version? SSMS.exe usually have long path, so it would be less typing for admin.

If I understand correctly, the ask is to instead of the -SQLServerManagementStudioPath parameter have a -SSMS switch parameter and then the command would figure out itself (for example by using the hack) where the ssms.exe resides. I think it would be a good quality of life feature, but I will keep the -SQLServerManagementStudioPath parameter around in case the script cannot determine the ssms.exe.

Regarding the stored credential: @Splaxi do we want to take a dependency on the CredentialManager module? My vote would be no, at least in regards to a dependency at installation time. The overhead and risk that comes with it does not justify the relatively minor gain in quality of life for this rather rare use case. I would be more open to a run time dependency, i.e. have the command check if the module is available. If not, it falls back to the current behavior and tells the user to install the module. If yes, it would create the credential.

I would also recommend that instead of CredentialManager (updated last in 2016 and source code archived in 2021) we test TUN.CredentialManager instead. It seems to be the successor to CredentialManager and compatible with PowerShell Core.

Another option could be to write our own internal functions for creating a stored credential, since we only need a small part of the full CredentialManager functionality.

Yet another option could be to write a How-To in the wiki that explains how to use Request-D365DatabaseJITAccess with credential manager to open SSMS.

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 7, 2025

I would like the following be possible:

Get an JIT, persist it on my local machine and have SSMS open - in 1 operation / flow. Be that by pipes or other fancy tricks.

I would like to list the current Stored Credentials, with server + database + username. On top of that - I would like to clear them out, when they have rendered obsolete, in the background.

I'm willing to put in any effort to make that happen, as I feel we are very close to make it happen. I'll dive into the TUN.CredentialManager and see where it takes me.

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 7, 2025

With the TUN module, we have some neat stuff available:

$(Get-StoredCredential -AsCredentialObject -WarningAction SilentlyContinue) | Where-Object TargetName -like "*SSMS*"

Flags          : 0
Type           : Generic
TargetName     : LegacyGeneric:target=Microsoft:SSMS:21:spartan-srv-emea-d365opsprod-f2231d90b92d.database.windows.net:
                 JIT-d365fo-abc......:8c91a03d-f9b4-46c0-a305-b5dcc79ff907:1
Comment        :
LastWritten    : 10/6/2025 3:13:51 PM
PaswordSize    : 180
Password       : _tOUblpWxctSTKe3hpgy........
SecurePassword :
Persist        : LocalMachine
AttributeCount : 0
Attributes     : 0
TargetAlias    :
UserName       : JIT-d365fo-abc......

With that - We can build some cmdlets, and features - what will make the life with UDE easier...

If we use the Comment for that database name, e.g. "db:db_d365opsprod_ax_97ce;" - then we should have all the capabilities directly from the Credential Manager store.

LastWritten will be use to validate that a given username / password combination has expired (9+ hours), and when our module loads, we can start a background tasks, that will remove expired. We can expose a setting, in the Get-PSFConfigValue style, so people can opt-in for the auto-clean mechanism - depending on their needs.

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 7, 2025

@FH-Inway

Can we do Ude - just like we did Lcs in the cmdlets? As a grouping function?

Request-D365UdeDatabaseJitAccess
Set-D365UdeDatabaseCredential

image

@FH-Inway
Copy link
Member Author

FH-Inway commented Oct 7, 2025

Can we do Ude - just like we did Lcs in the cmdlets? As a grouping function?

Yes, good idea 👍

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 8, 2025

@FH-Inway

Set-D365UdeDatabaseCredential
Get-D365UdeDatabaseCredential
Start-D365UdeSsmsSession

They are ready for some initial testing...

@FH-Inway
Copy link
Member Author

FH-Inway commented Oct 8, 2025

Set-D365UdeDatabaseCredential
Get-D365UdeDatabaseCredential
Start-D365UdeSsmsSession

They are ready for some initial testing...

Have not been able to find them, let me know once you push them to a public repo, then I will take a look.

Here is a hack to find all SSMS installed on the machine:

I looked into the hack. It does not work on older SSMS versions, since the registry path is different there. The question is how many versions backwards we want to support. We will also have to check with each new SSMS versions if the registry paths still work and update them if not.

Also not sure if we need the ForEach-Object loops, I ended up with the following (haven't checked the SSMS 19 registry yet):

$registryPathDMX = "Registry::HKEY_CLASSES_ROOT\ssms.dmx*\shell\Open\Command" # SSMS 18
$registryPathC2S = "Registry::HKEY_CLASSES_ROOT\ssms.c2s*\shell\Open\Command" # SSMS 20 + 21
$registryEntry = Get-ItemProperty -Path $registryPathDMX
if (-not $registryEntry) {
    $registryEntry = Get-ItemProperty -Path $registryPathC2S
}
if (-not $registryEntry) {
    throw "SSMS installation not found in registry."
}
$ssmsCommand = $registryEntry."(Default)"
$ssmsPathMatches = $ssmsCommand | Select-String -Pattern '^[\"]?(.*ssms\.exe)["]?\s*"%1"' 
$ssmsPath = $ssmsPathMatches.Matches.Groups[1].Value

Based on that, I had GitHub Copilot (GPT-5) vibe code an internal function to determine the ssms.exe path: https://github.com/FH-Inway/d365fo.tools/blob/695897325bec8cd280630358cfae034f8b977e0d/d365fo.tools/internal/functions/get-ssmspath.ps1
Have not fully reviewed it yet, it does some additional stuff to the registry values.

@Splaxi
Copy link
Collaborator

Splaxi commented Oct 9, 2025

Set-D365UdeDatabaseCredential
Get-D365UdeDatabaseCredential
Start-D365UdeSsmsSession
They are ready for some initial testing...

Have not been able to find them, let me know once you push them to a public repo, then I will take a look.

Here is a hack to find all SSMS installed on the machine:

I looked into the hack. It does not work on older SSMS versions, since the registry path is different there. The question is how many versions backwards we want to support. We will also have to check with each new SSMS versions if the registry paths still work and update them if not.

Also not sure if we need the ForEach-Object loops, I ended up with the following (haven't checked the SSMS 19 registry yet):

$registryPathDMX = "Registry::HKEY_CLASSES_ROOT\ssms.dmx*\shell\Open\Command" # SSMS 18
$registryPathC2S = "Registry::HKEY_CLASSES_ROOT\ssms.c2s*\shell\Open\Command" # SSMS 20 + 21
$registryEntry = Get-ItemProperty -Path $registryPathDMX
if (-not $registryEntry) {
    $registryEntry = Get-ItemProperty -Path $registryPathC2S
}
if (-not $registryEntry) {
    throw "SSMS installation not found in registry."
}
$ssmsCommand = $registryEntry."(Default)"
$ssmsPathMatches = $ssmsCommand | Select-String -Pattern '^[\"]?(.*ssms\.exe)["]?\s*"%1"' 
$ssmsPath = $ssmsPathMatches.Matches.Groups[1].Value

Based on that, I had GitHub Copilot (GPT-5) vibe code an internal function to determine the ssms.exe path: https://github.com/FH-Inway/d365fo.tools/blob/695897325bec8cd280630358cfae034f8b977e0d/d365fo.tools/internal/functions/get-ssmspath.ps1 Have not fully reviewed it yet, it does some additional stuff to the registry values.

Maybe this could make it simpler, and support multiple versions over time

Get-ChildItem `
    -Path Registry::HKEY_CLASSES_ROOT\ssms.*\shell\Open\Command | `
    Select-Object -ExpandProperty PsPath | `
    ForEach-Object { (Get-ItemProperty -Path $_)."(Default)" } | `
    Select-String -Pattern '^[\"]?(.*ssms\.exe)["]?\s*"%1"' | `
    ForEach-Object { $_.Matches.Groups[1].Value } | Select-Object -Unique

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New command to request just in time (JIT) database access for a unified development environment (UDE)

3 participants