Дано:
- An Azure SQL Сервер -
MyAzureSqlServer
- Субъект-служба -
MyServicePrincipal
- Субъект-служба настроена в качестве администратора AD сервера Azure SQL. (Azure Модуль Portal и Az Powershell не допускает этого, но Azure CLI и REST API позволяют)
У меня есть код Powershell, который запускает SELECT 1
в данной базе данных в вышеупомянутом Azure SQL Сервер:
param($db)
$AzContext = Get-AzContext # Assume this returns the Az Context for MyServicePrincipal
$TenantId = $AzContext.Tenant.Id
$ClientId = $AzContext.Account.Id
$SubscriptionId = $AzContext.Subscription.Id
$ClientSecret = $AzContext.Account.ExtendedProperties.ServicePrincipalSecret
$token = Get-AzureAuthenticationToken -TenantID $TenantId -ClientID $ClientId -ClientSecret $ClientSecret -ResourceAppIDUri "https://database.windows.net/"
Invoke-SqlQueryThruAdoNet -ConnectionString "Server=MyAzureSqlServer.database.windows.net;database=$db" -AccessToken $token -Query "SELECT 1"
Где Get-AzureAuthenticationToken
это:
function Get-AzureAuthenticationToken(
[Parameter(Mandatory)][String]$TenantID,
[Parameter(Mandatory)][String]$ClientID,
[Parameter(Mandatory)][String]$ClientSecret,
[Parameter(Mandatory)][String]$ResourceAppIDUri)
{
$tokenResponse = Invoke-RestMethod -Method Post -UseBasicParsing `
-Uri "https://login.windows.net/$TenantID/oauth2/token" `
-Body @{
resource = $ResourceAppIDUri
client_id = $ClientID
grant_type = 'client_credentials'
client_secret = $ClientSecret
} -ContentType 'application/x-www-form-urlencoded'
Write-Verbose "Access token type is $($tokenResponse.token_type), expires $($tokenResponse.expires_on)"
$tokenResponse.access_token
}
И Invoke-SqlQueryThruAdoNet
это:
function Invoke-SqlQueryThruAdoNet(
[parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]$ConnectionString,
[parameter(Mandatory=$true)]
[string]$Query,
$QueryTimeout = 30,
[string]$AccessToken
)
{
$SqlConnection = New-Object System.Data.SqlClient.SqlConnection
$SqlCmd = New-Object System.Data.SqlClient.SqlCommand
$SqlAdapter = New-Object System.Data.SqlClient.SqlDataAdapter
try
{
$SqlConnection.ConnectionString = $ConnectionString
if ($AccessToken)
{
$SqlConnection.AccessToken = $AccessToken
}
$SqlConnection.Open()
$SqlCmd.CommandTimeout = $QueryTimeout
$SqlCmd.CommandText = $Query
$SqlCmd.Connection = $SqlConnection
$DataSet = New-Object System.Data.DataSet
$SqlAdapter.SelectCommand = $SqlCmd
[void]$SqlAdapter.Fill($DataSet)
$res = $null
if ($DataSet.Tables.Count)
{
$res = $DataSet.Tables[$DataSet.Tables.Count - 1]
}
$res
}
finally
{
$SqlAdapter.Dispose()
$SqlCmd.Dispose()
$SqlConnection.Dispose()
}
}
И это работает отлично для любой базы данных, кроме главной, для которой я получаю:
[MyAzureSqlServer.database. windows .net \ master] Ошибка входа для пользователя '4 ... 1@2...b' . (SqlError 18456, LineNumber = 65536, ClientConnectionId = b8f4f657-2772-4306-b222-4533013227d1)
, где 4...1
- это идентификатор клиента MyServicePrincipal
, а 2...b
- наш Azure Идентификатор клиента AD
Итак, я знаю, что токен доступа в порядке, потому что я могу выполнять запросы к другим базам данных. Именно master
является проблемой c. Есть ли решение для этого? Конечно, он должен работать с субъектом-службой, являющимся администратором AD.
EDIT 1
Как я уже упоминал, существует 2 способа настроить субъект-службу как Администратор AD:
- Используя Azure CLI. На самом деле это просто:
az sql server ad-admin create --resource-group {YourAzureSqlResourceGroupName} `
--server-name {YourAzureSqlServerName} `
--display-name {ADAdminName} `
--object-id {ServicePrincipalObjectId}
{ADAdminName}
может быть любым, но мы передаем отображаемое имя субъекта-службы.
Теперь, пока это работает, мы отказались от Azure CLI в пользу Az Powershell, поскольку последний не сохраняет учетные данные участника-службы на диске в виде открытого текста. Однако функция Az Powershell Set-AzSqlServerActiveDirectoryAdministrator не принимает участника-службы. Тем не менее, Azure REST API позволяет это, поэтому у нас есть следующая настраиваемая функция PS, выполняющая эту работу:
function Set-MyAzSqlServerActiveDirectoryAdministrator
{
[CmdLetBinding(DefaultParameterSetName = 'NoObjectId')]
param(
[Parameter(Mandatory, Position = 0)][string]$ResourceGroupName,
[Parameter(Mandatory, Position = 1)][string]$ServerName,
[Parameter(ParameterSetName = 'ObjectId', Mandatory)][ValidateNotNullOrEmpty()]$ObjectId,
[Parameter(ParameterSetName = 'ObjectId', Mandatory)][ValidateNotNullOrEmpty()]$DisplayName
)
$AzContext = Get-AzContext
if (!$AzContext)
{
throw "No Az context is found."
}
$TenantId = $AzContext.Tenant.Id
$ClientId = $AzContext.Account.Id
$SubscriptionId = $AzContext.Subscription.Id
$ClientSecret = $AzContext.Account.ExtendedProperties.ServicePrincipalSecret
if ($PsCmdlet.ParameterSetName -eq 'NoObjectId')
{
$sp = Get-AzADServicePrincipal -ApplicationId $ClientId
$DisplayName = $sp.DisplayName
$ObjectId = $sp.Id
}
$path = "/subscriptions/$SubscriptionId/resourceGroups/$ResourceGroupName/providers/Microsoft.Sql/servers/$ServerName/administrators/activeDirectory"
$apiUrl = "https://management.azure.com${path}?api-version=2014-04-01"
$jsonBody = @{
id = $path
name = 'activeDirectory'
properties = @{
administratorType = 'ActiveDirectory'
login = $DisplayName
sid = $ObjectId
tenantId = $TenantId
}
} | ConvertTo-Json -Depth 99
$token = Get-AzureAuthenticationToken -TenantID $TenantId -ClientID $ClientId -ClientSecret $ClientSecret -ResourceAppIDUri "https://management.core.windows.net/"
$headers = @{
"Authorization" = "Bearer $token"
"Content-Type" = "application/json"
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-RestMethod $apiUrl -Method Put -Headers $headers -Body $jsonBody
}
Она использует уже знакомую (см. Выше) функцию Get-AzureAuthenticationToken
. Для наших нужд он устанавливает текущего вошедшего в систему участника-службы как администратора AD.