diff --git a/Database/Set-AzureSQLPermissions/AzureSQLPermissions.csv b/Database/Set-AzureSQLPermissions/AzureSQLPermissions.csv new file mode 100644 index 0000000..77befa6 --- /dev/null +++ b/Database/Set-AzureSQLPermissions/AzureSQLPermissions.csv @@ -0,0 +1,2 @@ +ServerName,DatabaseName,ManagedIdentityName,DataReader,DataWriter,Execute +sql-flg-ebs-tst-dewc-1,ebase-data,vm-flg-management-tst-dewc-1 ,yes,yes,yes \ No newline at end of file diff --git a/Database/Set-AzureSQLPermissions/README.md b/Database/Set-AzureSQLPermissions/README.md new file mode 100644 index 0000000..7ced918 --- /dev/null +++ b/Database/Set-AzureSQLPermissions/README.md @@ -0,0 +1,227 @@ +# Azure SQL Permissions Assignment + +## Overview + +This PowerShell script automates granting Azure SQL Database permissions to Managed Identities (or other Microsoft Entra identities). + +Instead of connecting to each Azure SQL server using SQL Server Management Studio (SSMS), the script reads a CSV file containing the target databases and identities, then: + +- Creates the database user (if it does not already exist) +- Grants the **db_datareader** role (optional) +- Grants the **db_datawriter** role (optional) +- Grants **EXECUTE** permission (optional) +- Logs all operations to a timestamped log file + +The script is designed to be safely re-run multiple times. + +--- + +# Prerequisites + +## Azure CLI + +Install Azure CLI: + +https://learn.microsoft.com/cli/azure/install-azure-cli + +Login before running the script: + +```powershell +az login +``` + +If working with multiple subscriptions, select the required subscription: + +```powershell +az account set --subscription "" +``` + +--- + +## PowerShell + +PowerShell 7.x is recommended. + +--- + +## SqlServer PowerShell Module + +The script uses `Invoke-Sqlcmd`. + +Install it once: + +```powershell +Install-Module SqlServer -Scope CurrentUser -Force +``` + +Verify installation: + +```powershell +Get-Command Invoke-Sqlcmd +``` + +--- + +## Required Azure SQL Permissions + +The account running the script must be able to: + +- Connect to the Azure SQL Database using Microsoft Entra authentication +- Create database users +- Grant database permissions + +Typically this means: + +- Microsoft Entra SQL Administrator +- or a user with sufficient database permissions (for example `db_owner`). + +--- + +# Files + +## Set-AzureSQLPermissions.ps1 + +Main automation script. + +### Parameters + +| Parameter | Description | +|-----------|-------------| +| `-CsvPath` | Path to the CSV file to process. Optional, defaults to `AzureSQLPermissions.csv` next to the script. | + +The script also supports the standard PowerShell `-WhatIf` and `-Confirm` switches (via `SupportsShouldProcess`): every database change (user creation, role grants, EXECUTE grant) is gated behind them, so `-WhatIf` shows what would be changed without touching any database. + +--- + +## AzureSQLPermissions.csv + +Contains the list of permissions to assign. + +Example: + +```csv +ServerName,DatabaseName,ManagedIdentityName,DataReader,DataWriter,Execute +sql-prod-01,SalesDB,vm-sales-prod,Yes,Yes,Yes +sql-prod-01,ReportingDB,vm-reporting,Yes,No,Yes +sql-test-01,TestDB,vm-test,Yes,Yes,No +``` + +### Columns + +| Column | Description | +|---------|-------------| +| ServerName | Azure SQL logical server name (without `.database.windows.net`) | +| DatabaseName | Target database | +| ManagedIdentityName | Managed Identity or Microsoft Entra principal | +| DataReader | Yes / No | +| DataWriter | Yes / No | +| Execute | Yes / No | + +Accepted values: + +- Yes +- Y +- True +- 1 + +Any other value is treated as **No**. + +--- + +# What the script does + +For each CSV row: + +1. Connects to the Azure SQL Database +2. Creates the user if it does not exist + +```sql +CREATE USER [Identity] FROM EXTERNAL PROVIDER; +``` + +3. Optionally grants: + +- db_datareader +- db_datawriter +- EXECUTE + +--- + +# Logging + +Each execution creates a log file: + +``` +AzureSQLPermissions_YYYYMMDD_HHMMSS.log +``` + +Example: + +``` +2026-06-30 09:12:11 [INFO] Starting Azure SQL permission script +2026-06-30 09:12:13 [INFO] Server: sql-prod-01 +2026-06-30 09:12:14 [SUCCESS] Permissions successfully configured for 'vm-sales-prod' +2026-06-30 09:12:16 [ERROR] Principal could not be found. +``` + +--- + +# Running the script + +Using the default CSV file location (`AzureSQLPermissions.csv` next to the script): + +```powershell +./Set-AzureSQLPermissions.ps1 +``` + +Using a different CSV file: + +```powershell +./Set-AzureSQLPermissions.ps1 -CsvPath C:\data\perms.csv +``` + +Dry run - shows what would change without applying anything: + +```powershell +./Set-AzureSQLPermissions.ps1 -WhatIf +``` + +--- + +# Error Handling + +The script validates: + +- Azure CLI is installed +- `Invoke-Sqlcmd` is available +- CSV file exists +- Azure access token can be obtained +- Each database operation + +Errors are written to the log file. + +If processing multiple databases, the script continues with the next entry after logging the error. + +The script exits with code `0` on a completed run and `1` if a fatal error occurs before any databases are processed (missing prerequisites, missing/empty CSV, or failure to obtain an access token). Per-row failures are logged but do not change the exit code. + +--- + +# Idempotency + +The script is safe to run multiple times. + +It checks whether: + +- the user already exists +- the user is already a member of the requested database roles + +Running the script repeatedly does not create duplicate users or duplicate role memberships. + +--- + +# Notes + +- The script uses Microsoft Entra authentication. +- SQL Authentication is not required. +- SSMS is not required. +- The script is suitable for bulk permission assignment across multiple Azure SQL databases. diff --git a/Database/Set-AzureSQLPermissions/Set-AzureSQLPermissions.ps1 b/Database/Set-AzureSQLPermissions/Set-AzureSQLPermissions.ps1 new file mode 100644 index 0000000..412771c --- /dev/null +++ b/Database/Set-AzureSQLPermissions/Set-AzureSQLPermissions.ps1 @@ -0,0 +1,227 @@ +<# +.SYNOPSIS + Grants Azure SQL Database permissions to managed identities listed in a CSV file. + +.DESCRIPTION + Reads a CSV of ServerName/DatabaseName/ManagedIdentityName/DataReader/DataWriter/Execute + rows, authenticates to Azure SQL using an access token from 'az account get-access-token', + creates each managed identity as an external database user if it does not already exist, + and grants the requested db_datareader/db_datawriter/EXECUTE permissions idempotently. + Requires the Azure CLI ('az') and the SqlServer module (Invoke-Sqlcmd) to be installed, + and an active 'az login' session with rights to request database access tokens. + +.PARAMETER CsvPath + Path to the input CSV file. Defaults to 'AzureSQLPermissions.csv' next to this script. + +.EXAMPLE + PS> .\Set-AzureSQLPermissions.ps1 + Processes AzureSQLPermissions.csv from the script folder and applies the permissions. + +.EXAMPLE + PS> .\Set-AzureSQLPermissions.ps1 -CsvPath C:\data\perms.csv -WhatIf + Shows which grants would be applied without changing any database. + +.NOTES + Author: Michal Horák + Created: 2026-07-21 + Version: 1.1.0 + Changelog: + 1.1.0 - Added standard header, ShouldProcess/-WhatIf support, CsvPath parameter, + Set-StrictMode/$ErrorActionPreference, function help blocks, exit 0 on success. + 1.0.0 - Initial version +#> + +[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] +param( + [Parameter(Mandatory = $false, HelpMessage = 'Path to the CSV file containing permission rows.')] + [ValidateNotNullOrEmpty()] + [string] + $CsvPath = (Join-Path $PSScriptRoot "AzureSQLPermissions.csv") +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$LogFile = Join-Path $PSScriptRoot ("AzureSQLPermissions_{0}.log" -f (Get-Date -Format "yyyyMMdd_HHmmss")) + +function Write-Log { + <# + .SYNOPSIS + Writes a timestamped message to the console and to the run's log file. + .PARAMETER Message + The text to log. + .PARAMETER Level + Severity label prefixed to the log line (e.g. INFO, ERROR, SUCCESS). Defaults to INFO. + .EXAMPLE + Write-Log -Message "Starting" -Level INFO + #> + param( + [string]$Message, + [string]$Level = "INFO" + ) + + $Line = "{0} [{1}] {2}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Level, $Message + Write-Host $Line + Add-Content -Path $LogFile -Value $Line -ErrorAction Stop +} + +function Test-Yes { + <# + .SYNOPSIS + Interprets a CSV cell value as a boolean flag. + .PARAMETER Value + The raw CSV cell text (e.g. "Yes", "Y", "True", "1"). + .EXAMPLE + Test-Yes -Value 'Yes' + #> + param([string]$Value) + return $Value -match '^(Yes|Y|True|1)$' +} + +Write-Log "Starting Azure SQL permission script" +Write-Log "CSV file: $CsvPath" +Write-Log "Log file: $LogFile" + +try { + if (!(Test-Path $CsvPath)) { + throw "CSV file not found: $CsvPath" + } + + if (-not (Get-Command az -ErrorAction SilentlyContinue)) { + throw "Azure CLI 'az' is not available." + } + + if (-not (Get-Command Invoke-Sqlcmd -ErrorAction SilentlyContinue)) { + throw "Invoke-Sqlcmd is not available. Install it with: Install-Module SqlServer -Scope CurrentUser -Force" + } + + $Permissions = Import-Csv -Path $CsvPath -ErrorAction Stop + + if (-not $Permissions) { + throw "CSV file is empty." + } + + $AccessToken = az account get-access-token ` + --resource https://database.windows.net/ ` + --query accessToken ` + -o tsv + + if (-not $AccessToken) { + throw "Failed to obtain Azure SQL access token. Run 'az login' first." + } +} +catch { + Write-Log $_.Exception.Message "ERROR" + exit 1 +} + +foreach ($Entry in $Permissions) { + + $ServerName = $Entry.ServerName.Trim() + $DatabaseName = $Entry.DatabaseName.Trim() + $ManagedIdentityName = $Entry.ManagedIdentityName.Trim() + + $GrantDataReader = Test-Yes $Entry.DataReader + $GrantDataWriter = Test-Yes $Entry.DataWriter + $GrantExecute = Test-Yes $Entry.Execute + + Write-Log "--------------------------------------------" + Write-Log "Server: $ServerName" + Write-Log "Database: $DatabaseName" + Write-Log "Managed Identity: $ManagedIdentityName" + Write-Log "DataReader: $GrantDataReader, DataWriter: $GrantDataWriter, Execute: $GrantExecute" + + try { + if ([string]::IsNullOrWhiteSpace($ServerName) -or + [string]::IsNullOrWhiteSpace($DatabaseName) -or + [string]::IsNullOrWhiteSpace($ManagedIdentityName)) { + throw "Missing ServerName, DatabaseName or ManagedIdentityName in CSV row." + } + + $PermissionSql = "" + + if ($GrantDataReader) { + $PermissionSql += @" + +IF NOT EXISTS ( + SELECT 1 + FROM sys.database_role_members rm + JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id + JOIN sys.database_principals u ON rm.member_principal_id = u.principal_id + WHERE r.name = 'db_datareader' + AND u.name = N'$ManagedIdentityName' +) +BEGIN + ALTER ROLE db_datareader ADD MEMBER [$ManagedIdentityName]; +END; +"@ + } + + if ($GrantDataWriter) { + $PermissionSql += @" + +IF NOT EXISTS ( + SELECT 1 + FROM sys.database_role_members rm + JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id + JOIN sys.database_principals u ON rm.member_principal_id = u.principal_id + WHERE r.name = 'db_datawriter' + AND u.name = N'$ManagedIdentityName' +) +BEGIN + ALTER ROLE db_datawriter ADD MEMBER [$ManagedIdentityName]; +END; +"@ + } + + if ($GrantExecute) { + $PermissionSql += @" + +IF NOT EXISTS ( + SELECT 1 + FROM sys.database_permissions p + JOIN sys.database_principals pr + ON p.grantee_principal_id = pr.principal_id + WHERE pr.name = N'$ManagedIdentityName' + AND p.permission_name = 'EXECUTE' + AND p.state IN ('G','W') +) +BEGIN + GRANT EXECUTE TO [$ManagedIdentityName]; +END; +"@ + } + + $Query = @" +IF NOT EXISTS ( + SELECT 1 + FROM sys.database_principals + WHERE name = N'$ManagedIdentityName' +) +BEGIN + CREATE USER [$ManagedIdentityName] FROM EXTERNAL PROVIDER; +END; + +$PermissionSql +"@ + + if ($PSCmdlet.ShouldProcess("$ServerName/$DatabaseName", "Grant permissions to '$ManagedIdentityName'")) { + Invoke-Sqlcmd ` + -ServerInstance "$ServerName.database.windows.net" ` + -Database $DatabaseName ` + -AccessToken $AccessToken ` + -Query $Query ` + -ErrorAction Stop + + Write-Log "Permissions successfully configured for '$ManagedIdentityName' on $ServerName / $DatabaseName" "SUCCESS" + } + } + catch { + Write-Log "FAILED configuring permissions for '$ManagedIdentityName' on $ServerName / $DatabaseName" "ERROR" + Write-Log $_.Exception.Message "ERROR" + continue + } +} + +Write-Log "Script finished" +exit 0 \ No newline at end of file