Add Set-AzureSQLPermissions script and CSV for managing Azure SQL permissions

This commit is contained in:
Petr Štěpán
2026-07-21 13:31:15 +02:00
parent 540e245d9d
commit 486f9d9d94
3 changed files with 456 additions and 0 deletions
@@ -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
1 ServerName DatabaseName ManagedIdentityName DataReader DataWriter Execute
2 sql-flg-ebs-tst-dewc-1 ebase-data vm-flg-management-tst-dewc-1 yes yes yes
+227
View File
@@ -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 "<Subscription Name or ID>"
```
---
## 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.
@@ -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