Use-AzSqlGui.ps1
<#PSScriptInfo
.VERSION 0.0.3 .GUID 4c3a3f4b-1351-488d-88f4-fcda52e93278 .AUTHOR alex.rudolph.1987@gmail.com .COMPANYNAME .COPYRIGHT (c) 2021 Alex Rudolph. All rights reserved. .TAGS Public .LICENSEURI .PROJECTURI .ICONURI .REQUIREDMODULES .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES #> <# .SYNOPSIS .DESCRIPTION This script is designed to use powershell to call azure using your azure credentials and SSO. Some prerequisite configurations are needed. .EXAMPLE .NOTES To allow pulling credentials from KV make sure that the context for the KV is set prior to executing the script as well as update the Get-Auth call in in the "Globals" section of the script. #> #requires -version 5.1 # ============================================================== # FUNCTIONS # ============================================================== #this is here because using .Requiredmodules can be a little hinky If(-not $installedModules){Write-Host "Getting installed modules"; $installedModules = Get-InstalledModule} Write-Host "Validating existing modules and installing missing modules" If(-not ($installedModules | Where-Object {$_.Name -eq "Az"})){Install-Module Az -force} Write-Host "Importing required modules" Import-Module Az Function Get-AllSqlServers { Write-Verbose "Loading SQL Servers" $Global:SQLServerList = @() $Contexts = Get-AzContext -ListAvailable ForEach($Context in $Contexts) { Write-Verbose "Checking subscription $($Context.Name)" $Context | Select-AzContext | out-null $temp = Get-AzSqlServer | Select-Object ServerName, ResourceGroupName $temp | Add-Member -MemberType NoteProperty "Subscription" -Value $Context.Name $Global:SQLServerList += $temp Remove-Variable temp } $Global:selServer.Items.AddRange(($Global:SQLServerList.ServerName | Sort-Object)) } Function Get-AllSqlDbs { $Global:selDatabase.Clear() $subscription = $Global:SQLServerList | Where-Object {$_.ServerName -eq $Global:SelectedServer} Write-Verbose "Selecting subscription $($subscription.subscription)" #Get-AzSubscription -SubscriptionName $subscription | Select-AzSubscription Get-AzContext -name $subscription.Subscription | Set-AzContext $ResourceGroupName = ($Global:SQLServerList | Where-Object {$_.ServerName -eq $Global:SelectedServer.ToString()}).ResourceGroupName Write-Verbose "Checking $ResourceGroupName" $Global:SQLDBList = (Get-AzSqlDatabase -ServerName $Global:SelectedServer -ResourceGroupName $ResourceGroupName).DatabaseName $Global:DBColumn = $Global:selDatabase.Columns.Add('Database',205) $Global:selDatabase.Items.AddRange(($Global:SQLDBList)) } Function User-Selector { $userList = Get-AzADUser | Out-GridView -PassThru -Title "User Selection" Return $UserList } Function AAD-Selector { $GroupList = Get-AzADGroup | Out-GridView -PassThru -Title "Group Selection" Return $GroupList } Function Add-AzSqlPermission { Param($users) If($users.count -gt 0) { If($users.UserPrincipalName){$Looper = $users.UserPrincipalName} ElseIf($users.DisplayName){$Looper = $users.DisplayName} ForEach($user in $Looper) { $UserQuery = "IF not exists(select name from sys.database_principals where name = '$user')`r`n CREATE USER [$user] FROM EXTERNAL PROVIDER;" $splatParams = @{ ServerInstance = "$($Global:SelectedServer).database.windows.net" Database = 'master' Query = $UserQuery Credential = $Global:cred UseADAuth = $true } $QueryObject = Invoke-AzSqlcmd @splatParams $splatParams.Query +="`r`nALTER ROLE db_datareader ADD MEMBER [$user];" $splatParams.Database = $Global:SelectedDatabase $QueryObject = Invoke-AzSqlcmd @splatParams } Set-UserList } Else { write-Verbose "nothing select... moving on..." } } Function Remove-AzSqlPermission { $splatParams = @{ ServerInstance = "$($Global:SelectedServer).database.windows.net" Database = $Global:SelectedDatabase Query = "IF exists(select name from sys.database_principals where name = '$global:SelectedUser')`r`n DROP USER [$global:SelectedUser];" Credential = $Global:cred UseADAuth = $true } If($Global:selUser.SelectedItems.count -gt 1) { ForEach($item in $Global:selUser.SelectedItems) { $splatParams.Query = "`r`nDROP USER [$($item.Text)];" $QueryObject = Invoke-AzSqlcmd @splatParams } } Else { $QueryObject = Invoke-AzSqlcmd @splatParams } Set-UserList } Function Get-AzSqlPermission { $UserQuery = "Select * from sys.database_principals where type not in ('A', 'G', 'R') and type_desc in ('EXTERNAL_USER','EXTERNAL_GROUP') and name not in ('dbo','guest','sys','INFORMATION_SCHEMA') order by name;" $splatParams = @{ ServerInstance = "$($Global:SelectedServer).database.windows.net" Database = $Global:SelectedDatabase Query = $UserQuery Credential = $Global:cred UseADAuth = $true } $QueryObject = Invoke-AzSqlcmd @splatParams $PermissionsObjs += $QueryObject.dataset.tables[0] return $PermissionsObjs } Function Set-UserList { $Global:selUser.Clear() $users = Get-AzSqlPermission if(-not $users) { [System.Windows.MessageBox]::Show('No assigned permissions found') } $Global:selUser.Columns.Add('UserName',400) $Global:selUser.Items.AddRange(($users.name)) } Function Get-KeyVaultSQLCredentials { <# .EXAMPLE Get-KeyVaultSQLCredentials -keyVaultName ExampleKeyVault -sqlKeyName sqlUserName -passKeyName sqlPassword #> Param ( [Parameter(Mandatory=$true)] $keyVaultName, [Parameter(Mandatory=$true)] $sqlKeyName, [Parameter(Mandatory=$true)] $passKeyName ) [string]$SQLuser = (Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $sqlKeyName).SecretValueText [string]$SQLpass = (Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $passKeyName).SecretValueText #generate random #$Key = Get-RandomExtended -length 20 -type "Hex" #build PS Credential $secpasswd = ConvertTo-SecureString $SQLpass -AsPlainText -Force $SqlCred = New-Object System.Management.Automation.PSCredential ($SQLuser, $secpasswd) $return = @{cred = $sqlCred; String = $Key; UN = $SQLuser; PW = $SQLpass} return $return } Function Invoke-AzSqlRunAsUser { $sUsername = $Global:SelectedUser [void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic') $title = "Running as $sUsername" $msg = 'Enter your Query:' $text = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title) If (-not $text){Return} $UserQuery = "EXECUTE AS USER = '$sUsername'; $text;" $splatParams = @{ ServerInstance = "$($Global:SelectedServer).database.windows.net" Database = $Global:SelectedDatabase Query = $UserQuery Credential = $Global:cred UseADAuth = $true } $QueryObject = Invoke-AzSqlcmd @splatParams $PermissionsObjs += $QueryObject.dataset.tables[0] $PermissionsObjs | Out-GridView -Title "Results for $sUsername" } Function Invoke-AzSqlGetUserPermissions { $sUsername = $Global:SelectedUser $UserQuery = "EXECUTE AS USER = '$sUsername'; SELECT * FROM fn_my_permissions (NULL, 'DATABASE');" $splatParams = @{ ServerInstance = "$($Global:SelectedServer).database.windows.net" Database = $Global:SelectedDatabase Query = $UserQuery Credential = $Global:cred UseADAuth = $true } $QueryObject = Invoke-AzSqlcmd @splatParams $PermissionsObjs += $QueryObject.dataset.tables[0] $PermissionsObjs | Out-GridView -Title "Permissions for $sUsername" } Function Invoke-AzSqlCmd { Param ( $Query, $Credential, $SecureKey, $Database, $ServerInstance, $Port = 1433, $connectionTimeout = 30, $CommandTimeout = 120, [switch]$UseADAuth, [switch]$persistConnection, $cxn, $forceRetry = $true, $ErrorAction = "Continue" ) #modded from invoke-sqlcmd because of AAD login #reference https://social.msdn.microsoft.com/Forums/vstudio/en-US/15686e28-293b-4150-805f-1c25d2432d9a/invokesqlcmd-fails-with-quotcannot-open-server-quotdomaincomquot-requested-by-the-login?forum=ssdsgetstarted $Error.Clear() If($Credential.GetType().Name -eq 'PSCredential') { $Username = $Credential.UserName $Password = $Credential.GetNetworkCredential().Password } If(-not $Password){$Password=$global:PlainPWD} If(-not $Username -or -not $Password) { Write-Error "Unable to return username from credential" Exit } If(-not $cxn) { #MultipleActiveResultSets=true is because of the error "There is already an open DataReader associated with this Command which must be closed first" #$cxnString = "Server=tcp:$ServerInstance,$Port;Database=$Database;UID='$UserName';PWD='$Password';Trusted_Connection=False;Encrypt=True;Connection Timeout=$connectionTimeout;MultipleActiveResultSets=true" #https://stackoverflow.com/questions/6062192/there-is-already-an-open-datareader-associated-with-this-command-which-must-be-c $cxnString = "Server=tcp:$ServerInstance,$Port;Database=$Database;UID='$UserName';PWD='$Password';Trusted_Connection=False;Encrypt=True;Connection Timeout=$connectionTimeout;" Write-Verbose "Connection string: `n $cxnString" If($UseADAuth) { $cxnString += "Authentication=Active Directory Password;" } $cxn = New-Object System.Data.SqlClient.SqlConnection($cxnString) Try { $cxn.Open() } Catch { $Err = $_ switch ($ErrorAction) { {'SilentlyContinue','Ignore' -contains $_} {} 'Stop' { Throw $Err } 'Continue' { Throw $Err; Continue} Default { Throw $Err; Continue} } } } Write-Verbose "SQL Query: `n$Query" $cmd = New-Object System.Data.SqlClient.SqlCommand($Query, $cxn) $cmd.CommandTimeout = $CommandTimeout Try { $cmd.ExecuteReader() } Catch { $Err = $_ switch ($ErrorAction) { {'SilentlyContinue','Ignore' -contains $_} {} 'Stop' { Throw $Err } 'Continue' { Throw $Err; Continue} Default { Throw $Err; Continue} } } $ds = New-Object system.Data.DataSet $da = New-Object system.Data.SqlClient.SqlDataAdapter($cmd) #this intermittently fails... not sure why. Put a microsleep in there for the time being Start-Sleep -Milliseconds 150 Try { [void]$da.fill($ds) } Catch [System.Data.SqlClient.SqlException] # For SQL exception { $Err = $_ Write-Verbose "Capture SQL Error" if ($PSBoundParameters.Verbose) {Write-Verbose "SQL Error: $Err"} #Shiyang, add the verbose output of exception switch ($ErrorAction) { {'SilentlyContinue','Ignore' -contains $_} {} 'Stop' { Throw $Err } 'Continue' { Throw $Err} Default { Throw $Err} } } Catch # For other exception { Write-Verbose "Capture Other Error" $Err = $_ Write-Verbose $Err.Exception If($Err.Exception -like "*There is already an open DataReader associated with this Command which must be closed first*") { If($forceRetry){$retry = 0}Else{$retry = 5} Do { Start-Sleep -Seconds 1 $Error.Clear() Try { $PassthruSplat = @{ Query = $Query Credential = $Credential SecureKey = $SecureKey Database = $Database ServerInstance = $ServerInstance Port = $Port connectionTimeout = $connectionTimeout CommandTimeout = $CommandTimeout UseADAuth = $UseADAuth persistConnection = $persistConnection cxn = $cxn forceRetry = $false } $results = Invoke-AzSqlCmd @PassthruSplat Return $results } Catch { } $retry += 1 } Until ($retry -eq 5 -or -not $Error) If(-not $error) { Continue } Else { Write-Verbose 'A timeout occoured due to a previously open connection. Please try again.' } } if ($PSBoundParameters.Verbose) {Write-Verbose "Other Error: $Err"} switch ($ErrorAction) { {'SilentlyContinue','Ignore' -contains $_} {} 'Stop' { Throw $Err} 'Continue' { Throw $Err} Default { Throw $Err} } } If(-not $persistConnection) { Write-Verbose " Cleaning up connection..." $cxn.Close() $results = @{dataset = $ds; connection = $null} remove-variable ds,da,cxn,cmd } Else { Write-Verbose " Persisting connection..." $results = @{dataset = $ds; connection = $cxn} remove-variable ds,da,cmd } Return $results } Function Get-Auth { Param($sqlUserKeyName, $sqlKeyPassword) # Add-Type -AssemblyName PresentationFramework $error.Clear() If((Get-AzContext)){Disconnect-AzAccount} Do { $Global:cred = Get-Credential -Message "Please enter your Azure Credentials" If(-not $Global:Cred) { If($forceAzLogin) { $msgBoxInput = [System.Windows.Forms.MessageBox]::Show('You did not specify the credentials. The Servers and Databases will use your local CLI Connect-AzAccount so the credentials are required.','Missing credentials','OkCancel','Error') } Else { $msgBoxInput = [System.Windows.Forms.MessageBox]::Show('You did not specify the credentials. Would you like to try again? Select no if you would like to use keyvault.','Missing credentials','YesNoCancel','Error') } switch ($msgBoxInput) { 'Yes' {} 'Ok' {} 'No' {$continue = $true} 'Cancel' {exit} } } } Until($continue -or $Global:cred) $Global:PlainPWD = $Global:cred.GetNetworkCredential().Password If($Global:cred) { Write-Verbose "Forcing Connect-AzAccount..." Connect-AzAccount -Credential $Global:cred -ErrorAction Stop | Out-null } If($Global:cred){Write-Verbose "Manual credential created"; return} [void][Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic') $title = 'Keyvault' $msg = 'Enter your Keyvault Name:' $text = [Microsoft.VisualBasic.Interaction]::InputBox($msg, $title) If($text -eq ""){Write-Error -Message "No Keyvault specified... exiting" ;Exit} $Global:cred = (Get-KeyVaultSQLCredentials -keyVaultName $text -sqlKeyName $sqlUserKeyName -passKeyName $sqlKeyPassword).cred if($Error){Write-Error -Message $Error.Exception; exit} } # ============================================================== # GLOBALS # ============================================================== #$ErrorActionPreference = 'Stop' $VerbosePreference = 'Continue' $error.Clear() $stopWatch = [System.Diagnostics.Stopwatch]::StartNew() Add-Type -AssemblyName System.Windows.Forms [System.Windows.Forms.Application]::EnableVisualStyles() Get-Auth # ============================================================== # INIT # ============================================================== $Global:Form = New-Object system.Windows.Forms.Form $Global:Form.Icon = [Drawing.Icon]::ExtractAssociatedIcon((Get-Command powershell).Path) $Global:Form.ClientSize = New-Object System.Drawing.Point(724,441) $Global:Form.text = "Azure SQL User Manager" $Global:Form.TopMost = $false $labServer = New-Object system.Windows.Forms.Label $labServer.text = "Server:" $labServer.AutoSize = $true $labServer.width = 25 $labServer.height = 10 $labServer.location = New-Object System.Drawing.Point(6,9) $labServer.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $Global:selServer = New-Object system.Windows.Forms.ComboBox $Global:selServer.width = 372 $Global:selServer.height = 20 $Global:selServer.location = New-Object System.Drawing.Point(56,5) $Global:selServer.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $Global:selDatabase = New-Object system.Windows.Forms.ListView $Global:selDatabase.View = "Details" $Global:selDatabase.width = 206 $Global:selDatabase.height = 371 $Global:selDatabase.location = New-Object System.Drawing.Point(13,57) # Add items to the ListView Get-AllSqlServers $Global:selUser = New-Object system.Windows.Forms.ListView $Global:selUser.View = "Details" $Global:selUser.width = 477 $Global:selUser.height = 332 $Global:selUser.location = New-Object System.Drawing.Point(235,57) $btnAdd = New-Object system.Windows.Forms.Button $btnAdd.Enabled = $false $btnAdd.text = "Add User" $btnAdd.width = 90 $btnAdd.height = 30 $btnAdd.location = New-Object System.Drawing.Point(237,399) $btnAdd.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $btnAddGrp = New-Object system.Windows.Forms.Button $btnAddGrp.Enabled = $false $btnAddGrp.text = "Add Group" $btnAddGrp.width = 90 $btnAddGrp.height = 30 $btnAddGrp.location = New-Object System.Drawing.Point(337,399) $btnAddGrp.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $btnTest = New-Object system.Windows.Forms.Button $btnTest.Enabled = $true $btnTest.text = "Test" $btnTest.width = 60 $btnTest.height = 30 $btnTest.location = New-Object System.Drawing.Point(400,399) $btnTest.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $btnUserPermissions = New-Object system.Windows.Forms.Button $btnUserPermissions.Enabled = $false $btnUserPermissions.text = "Permissions" $btnUserPermissions.width = 90 $btnUserPermissions.height = 30 $btnUserPermissions.location = New-Object System.Drawing.Point(488,399) $btnUserPermissions.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $btnRunAsUser = New-Object system.Windows.Forms.Button $btnRunAsUser.Enabled = $false $btnRunAsUser.text = "RunAs" $btnRunAsUser.width = 65 $btnRunAsUser.height = 30 $btnRunAsUser.location = New-Object System.Drawing.Point(580,399) $btnRunAsUser.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $btnRemove = New-Object system.Windows.Forms.Button $btnRemove.Enabled = $false $btnRemove.text = "Remove" $btnRemove.width = 65 $btnRemove.height = 30 $btnRemove.location = New-Object System.Drawing.Point(647,399) $btnRemove.Font = New-Object System.Drawing.Font('Microsoft Sans Serif',10) $Global:Form.controls.AddRange(@($labServer,$selServer,$selDatabase,$selUser,$btnAdd,$btnAddGrp,$btnRemove,$btnUserPermissions,$btnRunAsUser)) $btnRunAsUser.Add_Click({ If(-not $Global:SelectedUser){[System.Windows.Forms.MessageBox]::Show('Please select a user to run as first')}Else{Invoke-AzSqlRunAsUser} }) $btnUserPermissions.Add_Click({ If(-not $Global:SelectedUser){[System.Windows.Forms.MessageBox]::Show('Please select a user to get permissions first')}Else{Invoke-AzSqlGetUserPermissions} }) $btnRemove.Add_Click({ If(-not $Global:SelectedUser){[System.Windows.Forms.MessageBox]::Show('Please select a user to remove first')}Else{Remove-AzSqlPermission} }) $btnAdd.Add_Click({ $ADUsers=User-Selector; If(-not $ADUsers){[System.Windows.Forms.MessageBox]::Show('No user selected')}Else{Add-AzSqlPermission -Users $ADUsers} }) $btnAddGrp.Add_Click({ $ADGroups=AAD-Selector; If(-not $ADGroups){[System.Windows.Forms.MessageBox]::Show('No group selected')}Else{Add-AzSqlPermission -Users $ADGroups} }) If($debugEnabled){ $Global:Form.controls.Add($btnTEST) ;$btnTEST.Add_Click({ If(-not $Global:SelectedUser){[System.Windows.Forms.MessageBox]::Show('Please select a user to remove first')}Else{TESTFUNCTION} }) } $Global:selServer.Add_SelectedValueChanged({ $Global:SelectedServer = $Global:selServer.SelectedItem; Get-AllSqlDbs; write-host $SelectedServer }) $Global:selDatabase.Add_Click({ $Global:SelectedDatabase = $Global:selDatabase.SelectedItems[0].Text; $btnAdd.Enabled=$True; $btnAddGrp.Enabled=$True; Set-UserList}) $Global:selUser.Add_Click({ $Global:SelectedUser = $Global:selUser.SelectedItems[0].Text; $btnRemove.Enabled=$True; $btnRunAsUser.Enabled=$True; $btnUserPermissions.Enabled=$True; }) $Global:Form.ShowDialog() |