In order to unlock one of the deploy logins, you need a mechanism that does this without providing access to someone to the database to actually unlock that login.

The goal is to issue an ALTER LOGIN xyz-deploy ENABLE statement. You also want to keep track that this login was enabled, and for how long this should remain opened. For this last part, we’ll use an audit table and wrap this login in a stored procedure

Unlocking objects

Tracking table

Keep track of all the logins and their status on a specific database server with a tracking table. This table needs to be available in every server in the master database, in the dba schema (custom schema).

CREATE SCHEMA dba 
GO
 
CREATE TABLE [dba].[Unlocked_Logins]( 
[Login_Name] [nvarchar](50) NOT NULL, 
[Unlocked_Date] [datetime] NOT NULL, 
[Expiry_Date] [datetime] NOT NULL, 
[Locked_Date] [datetime] NULL 
) 

Keep track of each request in a record. When unlocking the time of unlock will be logged, the requested expiry date and a field is foreseen to log when the login was locked again. Initially this will be blank.

Unlock stored procedure

The following procedure will insert a request for a specific login and it’s requested expiry time. It needs to be present on every server in the master database in the dba schema.

CREATE PROCEDURE dba.sp_unlock_login @login_name nvarchar(50), @expiry int = NULL 
AS 
BEGIN 
    DECLARE @sql_txt nvarchar(1000); 
DECLARE @unlock_date datetime; 
    DECLARE @expiry_date datetime; 
 
set @expiry = coalesce(@expiry,60) 
    set @unlock_date = getdate() 
set @expiry_date = DATEADD(mi,@expiry,@unlock_date)  
 
SET @sql_txt = 'INSERT INTO dba.[Unlocked_Logins] ' + 
  '(Login_Name, Unlocked_Date, Expiry_Date) VALUES ' + 
  '(''' + @login_name + ''',''' + 
          CONVERT(varchar, @unlock_date, 120) + ''',''' +  
  CONVERT(varchar, @expiry_date, 120) + ''')'  
 
EXECUTE sp_executesql @sql_txt 
 
SET @sql_txt = 'ALTER LOGIN ' + @login_name + ' ENABLE' 
print 'Unlocking ' + @login_name 
 
EXECUTE sp_executesql @sql_txt 
 
END 
GO 

Permissions

To make sure the job_login can modify the table, add following permissions to the tracking table.

grant select on dba.unlocked_logins to job_login 
grant update on dba.unlocked_logins to job_login 
grant insert on dba.unlocked_logins to job_login 

grant execute on dba.sp_unlock_login to job_login 

Azure Function

Let’s go ahead and create the function that can be called to unlock a specific login.

After you created a function app, go to the Functions blade and click Create.

Choose to Develop this in the portal and pick the HTTP trigger template, with authorization level Function.

In the run.ps1 script, you use a logic like below (this is a function only for the test environment, you’ll have a separate function for Prod

using namespace System.Net

# Input bindings are passed in via param block.  
param($Request, $TriggerMetadata)
    $instance_info = @{
       "brazil" = @{server="testeu"; database="brazil_db"; login="brdeploy"}
       "germany" = @{server="testeu"; database="germany_db"; login="dedeploy"}
       "spain" = @{server="testeu"; database="spain_db"; login="esdeploy"}
       "france" = @{server="testeu"; database="france_db"; login="frdeploy"}
       "japan"= @{server="testeu"; database="japan_db"; login="jpdeploy"}
       "us"= @{server="testeu"; database="us_db"; login="nadeploy"}
       "india"= @{server="testeu"; database="india_db"; login="indeploy"}
    }

$body = "Function didn't execute, missing arguments."
$statusCode = [HttpStatusCode]::BadRequest

$instanceName = $Request.Query.InstanceName
$environmentName = $Request.Query.EnvironmentName
if ($instanceName -and ($environmentName -eq "UAT")) {
    $expiryTime = $Request.Query.ExpiryTime
    if(-not $expiryTime) {
        $expiryTime=60
    }
    
    # Required parameters for unlocking
    $SQLServer = $instance_info[$instanceName].server
    $SQLDBName = $instance_info[$instanceName].database
    $loginName = $instance_info[$instanceName].login
    $pwd = $env:job_login_pwd
    if($connString -and $loginName){
        $sqlConn = New-Object System.Data.SqlClient.SqlConnection
        $sqlConn.ConnectionString = "Server = $SQLServer; Database = $SQLDBName; Integrated Security = False; User ID = job_login; Password = $pwd;"
        $sqlConn.Open()
        $sqlcmd = $sqlConn.CreateCommand()
        $sqlcmd.Connection = $sqlConn
        $query = "exec dba.sp_unlock_login @login_name = '$loginName', @expiry = $expiryTime "
        $sqlcmd.CommandText = $query
        $sqlcmd.ExecuteNonQuery()
        $body = "Function executed successfully."
        $statusCode = [HttpStatusCode]::OK
    }
}

# Associate values to output bindings
Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{
    StatusCode = $statusCode
    Body = $body
})

In short, the above function has an array of configuration values specific to each database. It will check if it’s being executed for the correct environment and a default expiry time of 60 minutes is set.

A connection is made to the right database and the procedure sp_unlock_login is executed for a certain login and setting a specific expiry time.

Get a copy of the function URL that looks like this : https://axisi0-af01.azurewebsites.net/api/UnlockUser?code=xxxxxxxxxbEboAmetqd8LmUGOsKJ3AzFufnJvjA==

Configuration

In the Function App configuration, you’ll supply the job_login password in the app settings, with a reference to the KeyVault

This is done by adding the configuration and for the value provide

@Microsoft.KeyVault(SecretUri=https://axis0-secrets.vault.azure.net/secrets/joblogin/xxxx03f8ab4xxxxxxxa64ca97d1)

Any time you change the KeyVault, this parameter will also have the latest version.

Permissions

You’ll have to give permissions to the azure function app that should be able to read the secrets.

I’ve configured this with Access Policies:

The function only needs Get on Secrets

For the principal name, select your azure function app e.g. test-axis0-af from the list. You can only select it, when you already created the function app.