Now that we setup the logins, the key vault and the function that will do the unlocking, it’s time to make sure any unlocked logins are also automatically locked again, so we don’t leave logins open.

Elastic Jobs

The idea of elastic jobs is that the same job can be executed on multiple servers that are part of the target group of these jobs. A job can have multiple job steps, similar to what is used in SQL Agent at this time.

You’ll have to setup 1 database that will be the main control database for elastic jobs. This can be a S1 Azure SQL database, and should be empty. More info can be found here : Create and manage Elastic Database Jobs (preview) with Transact-SQL (T-SQL) – Azure SQL Database | Microsoft Learn

Make sure to give job_login also access to this new database.

First you’ll have to create an Elastic Job Agent in the Azure portal. This is where you select the previously created database to act as job database.

That’s all the portal can do for you, the creation of jobs is done through TSQL in that database.

Locking stored procedure

The below procedure needs to be present in every server in the target group (in the master database in the dba schema)

CREATE PROCEDURE dba.sp_lock_logins 
AS 
BEGIN 
     
DECLARE @sql_txt nvarchar(1000); 
DECLARE @login_name NVARCHAR(50); 
 
DECLARE open_logins CURSOR FAST_FORWARD READ_ONLY FOR  
SELECT Login_Name  
  FROM dba.[Unlocked_Logins]  
 WHERE Locked_Date IS NULL  -- still unlocked 
   AND Expiry_date < GETDATE() 
   AND Login_Name not in ('mattias','mytopadmin') 
 
OPEN open_logins 
 
FETCH NEXT FROM open_logins INTO @login_name 
 
WHILE @@FETCH_STATUS = 0 
  BEGIN 
     
SET @sql_txt = 'ALTER LOGIN ' + @login_name + ' DISABLE' 
print 'Locking ' + @login_name 
 
EXECUTE sp_executesql @sql_txt 
 
SET @sql_txt = 'UPDATE dba.[Unlocked_Logins] set Locked_Date = getdate() where Login_Name = ''' + @login_name +  
               ''' and Locked_Date is null' 
EXECUTE sp_executesql @sql_txt 
 
    FETCH NEXT FROM open_logins INTO @login_name 
  END 
 
CLOSE open_logins 
DEALLOCATE open_logins 
END 
GO

The procudure basically checks the unlocked_logins table for any records with locked_date that is NULL (meaning it is still unlocked) and that have an expiry date that is before the current time (so it expired earlier then now). I also check not to include login_names that shoudl never be locked (imagine someone entering a record in this table and locking out important logins).

It then disables the login and updates the unlocked_logins table with a final locked_date.

Permissions

grant execute on dba.sp_lock_logins to job_login

Setting up the Elastic Job

The elastic job can be defined after creating

  • Target group : the database server against which to execute the job
  • Members of the target group : adding servers to the target group
  • The Job : define against which target group this job needs to run and with what frequency
  • The job steps : the different parts of the job

Below is the TSQL with which you can define this. In Azure portal, you’ll find this setup back after you completed the steps. You can’t modify it in the portal, it’s just a read-only view. You do get execution results and durations.

-- Add a target group containing all server you want to execute the lock stored procedure on
EXEC jobs.sp_add_target_group 'TestServers'; 
 
-- Add a server target member 
EXEC jobs.sp_add_target_group_member  
@target_group_name = 'TestServers', 
@target_type = 'SqlDatabase', 
@server_name = 'axis0-eu.database.windows.net', 
@database_name = 'master' 

-- Define the job (wrapper for the different steps)
DECLARE @JobName NVARCHAR(128) = N'Lock Accounts'; 
DECLARE @JobDescription NVARCHAR(512) = N'This job will run every 10 minutes to lock accounts'; 
DECLARE @Enabled BIT = 1; 
DECLARE @ScheduleIntercalType NVARCHAR(50) = N'Minutes'; 
DECLARE @ScheduleIntervalCount INT = 10; 
DECLARE @ScheduleStart DATETIME2 = N'20230620 06:00'; 
 
EXEC jobs.sp_add_job @job_name = @JobName, 
@description = @JobDescription, 
@enabled = @Enabled, 
@schedule_interval_type = @ScheduleIntercalType, 
@schedule_interval_count = @ScheduleIntervalCount, 
@schedule_start_time = @ScheduleStart; 

-- Define the job step
DECLARE @JobName NVARCHAR(128) = N'Lock Accounts'; 
DECLARE @JobStepName NVARCHAR(128) = N'Lock Accounts'; 
DECLARE @Command NVARCHAR(MAX) = N'exec dba.sp_lock_logins;'; 
DECLARE @CredentialName NVARCHAR(128) = N'job_login'; 
DECLARE @TargetGroupName NVARCHAR(128) = N'TestServers'; 
DECLARE @retryAttempts INT = 0; 
 
EXEC jobs.sp_add_jobstep @job_name = @JobName, 
@step_name = @JobStepName, 
@command = @Command, 
@credential_name = @CredentialName, 
@target_group_name = @TargetGroupName, 
@retry_attempts = @retryAttempts; 

And with this, the tedious setup of Elastic Jobs is also done. Time to join everything together.