Use-AzSqlGui.ps1

<#PSScriptInfo
.VERSION 0.0.5
.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
            }
            $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];"
    }

    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
    }

    $QueryObject = Invoke-AzSqlcmd @splatParams
    $PermissionsObjs = $QueryObject
    
    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
    }

    $QueryObject = Invoke-AzSqlcmd @splatParams
    $PermissionsObjs = $QueryObject
    
    $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
    }

    $QueryObject = Invoke-AzSqlcmd @splatParams
    $PermissionsObjs = $QueryObject
    
    $PermissionsObjs | Out-GridView -Title "Permissions for $sUsername"
}

Function Invoke-AzSqlCmd
{
    Param($serverInstance
         ,$Database
         ,$Query
         ,$context
    )
    # Build off this article https://thomasthornton.cloud/2020/10/06/query-azure-sql-database-using-service-principal-with-powershell/

    $ConnectionString="Data Source=$serverInstance; Initial Catalog=$Database;"

    if (-not $context){$context = Get-Azcontext}

    $dexResourceUrl='https://database.windows.net/'
    $token = [Microsoft.Azure.Commands.Common.Authentication.AzureSession]::Instance.AuthenticationFactory.Authenticate($context.Account, `
                                    $context.Environment, 
                                    $context.Tenant.Id.ToString(),
                                    $null, 
                                    [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::Never, 
                                    $null, $dexResourceUrl).AccessToken
    $SqlConnection = New-Object System.Data.SqlClient.SqlConnection                
    $SqlCmd = New-Object System.Data.SqlClient.SqlCommand
    try 
    {
        $SqlConnection.ConnectionString = $ConnectionString
        if ($token)
        {
            $SqlConnection.AccessToken = $token
        }
        $SqlConnection.Open() 
         
        $SqlCmd.Connection = $SqlConnection 
        
        $SqlCmd.CommandText = $Query
        $SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
        $SqlAdapter.SelectCommand = $SqlCmd
        $DataSet = New-Object System.Data.DataSet
        $SqlAdapter.Fill($DataSet) | Out-Null

        # based on https://www.stefanroth.net/2018/04/11/powershell-create-clean-customobjects-from-datatable-object/
        $result = @()
        ForEach ($Row in $DataSet.Tables[0])
        {
            $Properties = @{}
            For($i = 0;$i -le $Row.ItemArray.Count - 1;$i++)
            {
                $Properties.Add($DataSet[0].Tables[0].Columns[$i], $Row.ItemArray[$i])
            }
            $result += New-Object -TypeName PSObject -Property $Properties  
        }
    }
    Catch
    {
        If($_.exception -like '*Client with IP address *')
        {
            [System.Windows.MessageBox]::Show("A firewall rule has not been set for $database on $serverInstance")
            Write-Error "A firewall rule has not been set for $database"
        }
    }

    $SqlAdapter.Dispose()
    $SqlCmd.Dispose()
    $SqlConnection.Dispose()

    return $result
}

Function Get-Auth
{
    Param($sqlUserKeyName, $sqlKeyPassword)
# Add-Type -AssemblyName PresentationFramework
    $error.Clear()
    If((Get-AzContext))
    {
        $disconnect = Read-Host -Prompt "You are already connected. Would you like to reconnect? Enter 'Yes' to force reconnect" 
        If($disconnect -eq 'Yes')
        {
            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()