Add Set-AzureSQLPermissions script and CSV for managing Azure SQL permissions
This commit is contained in:
@@ -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
|
||||||
|
@@ -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
|
||||||
Reference in New Issue
Block a user