在Azure SQL Server中,作为服务主体的AD管理员能否在主数据库上运行查询?

在Azure SQL Server中,作为服务主体的AD管理员能否在主数据库上运行查询?,azure,azure-sql-database,azure-sql-server,Azure,Azure Sql Database,Azure Sql Server,鉴于: Azure SQL Server-MyAzureSQL Server 服务主体-MyServicePrincipal 服务主体被配置为Azure SQL Server的AD管理员。(Azure Portal和Az Powershell模块不允许,但Azure CLI和REST API允许) 我的Powershell代码在上述Azure SQL Server中的给定数据库上运行SELECT 1: param($db) $AzContext = Get-AzContext

鉴于:

  • Azure SQL Server-
    MyAzureSQL Server
  • 服务主体-
    MyServicePrincipal
  • 服务主体被配置为Azure SQL Server的AD管理员。(Azure Portal和Az Powershell模块不允许,但Azure CLI和REST API允许)
  • 我的Powershell代码在上述Azure SQL Server中的给定数据库上运行
    SELECT 1

    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"
    
    其中
    获取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
    }
    
    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()
        }
    }
    
    调用SqlQueryThruAdoNet是:

    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
    }
    
    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()
        }
    }
    
    它在任何数据库上都能正常工作,除了主数据库,我得到:

    用户“4”的[MyAzureSqlServer.database.windows.net\master]登录失败。。。1@2...b'. (SqlError 18456,行号=65536,客户端连接ID=b8f4f657-2772-4306-b222-453301327D1)

    其中
    4…1
    MyServicePrincipal
    的客户端Id,
    2…b
    是我们的Azure AD租户Id

    所以我知道访问令牌是可以的,因为我可以在其他数据库上运行查询。尤其是
    主机
    有问题。有解决办法吗?当然,它必须与作为广告管理员的服务主体一起工作

    编辑1

    正如我所提到的,有两种方法可以将服务主体配置为AD管理员:

    • 使用Azure CLI。其实很简单:
    {ADAdminName}
    可以是任何内容,但我们传递服务主体的显示名称

    现在,虽然这样做有效,但我们还是放弃了Azure CLI,转而使用Az Powershell,因为后者不会以明文形式在磁盘上持久保存服务主体凭据。但是,Az Powershell的函数不接受服务主体。但是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
    }
    

    它使用已经熟悉的(见上文)函数
    获取AzureAuthenticationToken
    。根据我们的需要,它将当前登录的服务主体设置为AD admin。

    根据我的测试,当我们直接将Azure服务主体设置为Azure SQL AD admin时,它将导致一些问题。我们无法使用服务原则登录
    master
    数据库。因为Azure广告管理员登录名应该是Azure广告用户或Azure广告组。有关更多详细信息,请参阅

    因此,如果您想将Azure服务主体设置为Azure SQL AD admin,我们需要创建一个Azure AD安全组,将服务主体添加为组的成员,将Azure AD组设置为Azure SQL AD admin

    比如说

  • 配置Azure AD管理员
  • 连接AzaAccount
    $group=New AzADGroup-DisplayName SQLADADmin-Mail昵称SQLADADmin
    $sp=获取AzADServicePrincipal-显示名称“TodoListService-OBO-sample-v2”
    添加AzADGroupMember-MemberObjectId$sp.Id-TargetGroupObjectId$group.Id
    $sp=获取AzADServicePrincipal-DisplayName“”
    删除AZSSQLServerActiveDirectoryAdministrator-ResourceGroupName”“-ServerName”“-force
    设置AzSqlServerActiveDirectoryAdministrator-ResourceGroupName”“-ServerName”“-DisplayName$group.DisplayName-ObjectId$group.id
    

    质疑

    $appId=“”
    $password=“”
    $secpasswd=converttosecurestring$password-AsPlainText-Force
    $mycreds=New Object System.Management.Automation.PSCredential($appId,$secpasswd)
    连接AzaAccount-ServicePrincipal-凭据$mycreds-租户“”
    #领取代币
    $context=获取上下文
    $dexResourceUrl=https://database.windows.net/'
    $token=[Microsoft.Azure.Commands.Common.Authentication.AzureSession]::实例.AuthenticationFactory.Authentication($context.Account,
    $context.Environment,
    $context.Tenant.Id.ToString(),
    $null,
    [Microsoft.Azure.Commands.Common.Authentication.ShowDialog]::从不,
    $null,$dexResourceUrl).AccessToken
    $SqlConnection=新对象System.Data.SqlClient.SqlConnection
    $SqlCmd=New Object System.Data.SqlClient.SqlCommand
    $ConnectionString=“数据源=testsql08.database.windows.net;初始目录=master;”
    #查询当前数据库名称
    $Query=“选择数据库名称()
    尝试
    {
    $SqlConnection.ConnectionString=$ConnectionString
    如果($token)
    {
    $SqlConnection.AccessToken=$token
    }
    $SqlConnection.Open()
    $SqlCmd.CommandText=$Query
    $SqlCmd.Connection=$SqlConnection
    $DataSet=新对象System.Data.DataSet
    $SqlAdapter.SelectCommand=$SqlCmd
    [void]$SqlAdapter.Fill($DataSet)
    $res=$null
    if($DataSet.Tables.Count)
    {
    $res=$DataSet.Tables[$DataSet.Tables.Count-1]
    }
    $res
    }
    最后
    {
    $SqlAdapter.Dispose()
    $SqlCmd.Dispose()
    $SqlConnection.Dispose()
    }
    

    您能告诉我如何将服务主体配置为Azure AD AD admin吗?请参阅编辑1。它对您有用吗?让我对答案进行评论。+1,但我将等待其他答案,因为这里的AD admin是一个组,而不是服务主体。我们不能使用这种方法,因为每个pod都部署了一个专用的服务主体。创建SP的代码没有修改广告组的权限。@mark您能告诉我为什么需要修改广告组吗?因为当我们引导一个新pod时,会创建一个专用的服务主体。引导代码具有创建新服务主体的权限,但不具有修改任何现有广告组或创建新广告组的权限。
    $appId = "<your sp app id>"
    $password = "<your sp password>"
    $secpasswd = ConvertTo-SecureString $password -AsPlainText -Force
    $mycreds = New-Object System.Management.Automation.PSCredential ($appId, $secpasswd)
    Connect-AzAccount -ServicePrincipal -Credential $mycreds -Tenant "<your AD tenant id>"
    #get token
    $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
    $ConnectionString="Data Source=testsql08.database.windows.net; Initial Catalog=master;"
    
    # query the current database name
    $Query="SELECT DB_NAME()"
    
        try 
        {
            $SqlConnection.ConnectionString = $ConnectionString
            if ($token)
            {
                $SqlConnection.AccessToken = $token
            }
            $SqlConnection.Open()
    
            $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()
        }