Create Exchange Administrators in all customer Office 365 tenants using Azure Functions and Delegated Administration

Azure Functions Creating Exchange Administrators

This is the second in a 3 part series on integrating Azure Functions, Microsoft Flow and Office 365 to automate Office 365 administration and reporting across multiple Office 365 tenants. See part 1 here on how to retrieve Office 365 user details using Azure Functions, Microsoft Flow and SharePoint Online.

In this post we’ll be creating Exchange Service Administrators in each one of our Customers Office 365 tenants using delegated administration and Azure functions.

To manage these Exchange Administrators, we’ll create three separate Azure Functions.

  • Function 1 will create the Exchange Administrators with blocked credentials
  • Function 2 will unblock a specific Exchange Administrator when it’s required
  • Function 3 will block any unblocked admins when they’re not required by another function

Function 1 Overview: Creating Exchange Administrators in all delegated tenants

Timer Triggered – PowerShell – Azure Function

We’ll be able to use these Exchange Admins in later posts to run tasks that don’t work under an Office 365 delegated admin account.

To prevent unauthorised usage of these Exchange Administrators, the accounts are created with blocked credentials and are unblocked when required by another Azure function. Each day when this function is run, it performs the following actions:

  • Check whether the Exchange Administrator exists, and if it doesn’t, create it and block it’s credentials.
  • If it does exist, check whether the credentials are still blocked and that it’s still an Exchange admin.
  • If credentials aren’t blocked, we wait a few minutes to give any running functions enough time to complete, then block the credentials again.

We’ll also set up another Azure Function that can be used for…

Function 2 Overview: Unblocking an Exchange Administrator when it’s required

HTTP Triggered – PowerShell – Azure Function

We want to be able to unblock our Exchange Administrators on demand. In our case, we’ll be using these admins for a few tasks, so we’re unblocking them with a separate function that we can call from other functions or flows.

This function can be called via a HTTP request from Microsoft Flow. It’ll unblock the reporting user, then update an Azure Storage Table with a time thirty minutes in the future (TimeToBlock). The reason for this is that we may be requiring this user multiple times in quick succession. If the function automatically blocks the user on a timer, it’ll interrupt the running of any following functions that require the user too.
Each time the function is called, the time that the user will be blocked is updated to the current time plus thirty minutes.

This brings us to our third Azure Function

Function 3 Overview: Check for unblocked reporting users

Timer Triggered – PowerShell – Azure Function

We don’t want these Exchange admins to remain unblocked for long. Every thirty minutes, this function will check the Azure Storage Table that the previous function updated. If the TimeToBlock value is in the past, this function will block the user and update the table.

Building Function 1: Creating Exchange Administrators in all delegated tenants

Each of the Azure functions we’ll be creating will need to connect to Office 365.

Follow the instructions in our knowledge base article here to create an Azure function with the following properties:

  • Name it whatever you like – ours is called CreateReportingUsersOnTimer
  • Set it to run once each weekday using the following CRON schedule:
    0 30 9 * * 1-5
  • Make sure you’ve uploaded the encrypted key and the MSOnline PowerShell module via FTP.

Create and encrypt credentials for your Exchange Administrators

When you set up your Azure Function, you encrypted credentials for your Office 365 delegated administator. We’ll need to do the same for the credentials you want to use for your exchange admins.

The steps to do this are the same as before, though we’ll call the encryption file ReportingUserPassEncryptkey.key

  1. Copy and paste the following commands into a PowerShell session on your local computer:
$AESKey = New-Object Byte[] 32
$Path = "C:\Temp\ReportingUserPassEncryptKey.key"
$EncryptedPasswordPath = "C:\Temp\ReportingUserEncryptedPassword.txt"
[Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($AESKey)
Set-Content $Path $AESKey
$Password = Read-Host "Please enter the password"
$secPw = ConvertTo-SecureString -AsPlainText $Password -Force
$AESKey = Get-content $Path
$Encryptedpassword = $secPw | ConvertFrom-SecureString -Key $AESKey
$Encryptedpassword | Out-File -filepath $EncryptedPasswordPath
  1. Enter your desired Exchange Administrator password when prompted by PowerShell
  2. Upload ReportingUserPassEncryptKey.key via FTP into the Bin folder of your Azure Function. (Reminder: this will be created in the C:\Temp folder of your local computer)
  3. Return to Azure Functions and navigate to the Application Settings for your function app. This is the same place you added the username and password for your own Delegated Administrator account.
  4. Copy and paste the contents of C:\Temp\ReportingUserEncryptedPassword.txt into the Application Settings section of your Azure Function under a key called reportingpassword.
  5. Create another key called reportinguser and enter a prefix for your reporting user usernames. Our reporting users will be created as [email protected], so we’ll enter gcitsreports into this key value.
  6. Create another key called reportinguserdisplayname and enter a value for your reporting user’s display name. We’ve called ours ‘Service Account for GCITS Reports’Add Exchange Administrator Details

Create Azure Storage Account for status tracking

We’ll also be logging the status of each delegated admin in Azure Table Storage. To do this, we’ll create an Azure Storage account, then add the details to your Azure Function’s application settings.

You can follow the instructions on this page to create a storage account, retrieve the account key and secret, and add them to your Azure Function application settings. Once you’ve completed this, return to this article. Your Applications Settings values should look something like this:App Settings For Exchange Administrators in Azure Functions

Return to your function, remove any existing code and paste in the following PowerShell script:

Write-Output "PowerShell Timer trigger function executed at:$(get-date)";

$FunctionName = 'CreateReportingUsersOnTimer'
$ModuleName = 'MSOnline'
$ModuleVersion = '1.1.166.0'
$username = $Env:user
$pw = $Env:password
$reportingUserDisplayName = $Env:reportinguserdisplayname
$reportingusername = $Env:reportinguser 
$reportingpw = $Env:reportingpassword
$storageAccount = $Env:storageAccount
$primaryKey = $Env:storageAccountKey
$UsersCreatedTableName = "ReportingUsersCreated"
$adminUserKeyPath = "D:\home\site\wwwroot\$FunctionName\bin\keys\PassEncryptKey.key"
$reportingUserKeyPath = "D:\home\site\wwwroot\$FunctionName\bin\keys\ReportingUserPassEncryptKey.key"

# Create storage account context
$Ctx = New-AzureStorageContext $storageAccount -StorageAccountKey $primaryKey

# Get Logging Table or create it if it doesn't exist.
$logTable = Get-AzureStorageTable –Name $UsersCreatedTableName -Context $Ctx -ErrorAction Ignore

if ($logtable -eq $null) {
    $logTable = New-AzureStorageTable –Name $UsersCreatedTableName -Context $Ctx
}

# Define Table Entity and add it to a table

function Add-Entity() {
    [CmdletBinding()]
    param(
        $table,
        [String]$partitionKey,
        [String]$rowKey,
        [String]$tenantId,
        [String]$companyName,
        [String]$domainName,
        [String]$userName,
        [String]$blockCredential,
        [String]$whenCreated

    )

    $entity = New-Object -TypeName Microsoft.WindowsAzure.Storage.Table.DynamicTableEntity -ArgumentList $partitionKey, $rowKey
    $entity.Properties.Add("TenantId", $tenantId)
    $entity.Properties.Add("CompanyName", $companyName)
    $entity.Properties.Add("DomainName", $domainName)
    $entity.Properties.Add("Username", $userName)
    $entity.Properties.Add("BlockCredential", $blockCredential)
    $entity.Properties.Add("WhenCreated", $whenCreated)
    $table.CloudTable.Execute([Microsoft.WindowsAzure.Storage.Table.TableOperation]::InsertOrReplace($entity))
}

#import PS module
$PSModulePath = "D:\home\site\wwwroot\$FunctionName\bin\$ModuleName\$ModuleVersion\$ModuleName.psd1"

Import-module $PSModulePath

# Build Credentials
$secpassword = $pw | ConvertTo-SecureString -Key (Get-Content $adminUserKeyPath)
$credential = New-Object System.Management.Automation.PSCredential ($username, $secpassword)

# Connect to MSOnline
Connect-MsolService -Credential $credential

# Get all customers
$Customers = Get-MsolPartnerContract -All

foreach ($Customer in $Customers) {

    Write-Output $Customer.Name.ToUpper()
    
    # Define the username of the reporting user
    $InitialDomain = Get-MsolDomain -TenantId $Customer.TenantId | Where {$_.IsInitial -eq $true}
    $NewAdminUPN = -join ($reportingusername, "@", $($InitialDomain.Name))
    Write-Output $NewAdminUPN
    
    # Check whether the reporting user exists
    $ReportingUserSearch = Get-MsolUser -TenantId $Customer.TenantId | Where-Object {$_.UserPrincipalName -match $NewAdminUPN}
    
    if ($ReportingUserSearch.count -eq 0) {
        Write-Output "No user found, creating user"

        # Unpack reporting user password
        $reportingsecpassword = $reportingpw | ConvertTo-SecureString -Key (Get-Content $reportingUserKeyPath)
        $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($reportingsecpassword)
        $reportingPassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

        # Create reporting user and add to Exchange Administrators Role
        New-MsolUser -UserPrincipalName $NewAdminUPN -DisplayName $reportingUserDisplayName -Password $reportingPassword -TenantId $Customer.TenantId -ForceChangePassword $false 
        Add-MsolRoleMember -TenantId $Customer.TenantId -RoleName "Exchange Service Administrator" -RoleMemberEmailAddress $NewAdminUPN
        Write-Output "Created $NewAdminUPN and added to Exchange Administrators Role"
        Set-MsolUser -UserPrincipalName $NewAdminUPN -TenantId $Customer.TenantId -BlockCredential $true

        $UserDetails = Get-MsolUser -UserPrincipalName $NewAdminUPN -TenantId $Customer.TenantId

        Add-Entity -Table $logTable -PartitionKey $Customer.TenantId -RowKey $NewAdminUPN -TenantId $Customer.TenantId -CompanyName $Customer.Name `
            -DomainName $Customer.DefaultDomainName -UserName $NewAdminUPN -BlockCredential $UserDetails.BlockCredential -WhenCreated $UserDetails.WhenCreated
    }
    else {
        $UserDetails = Get-MsolUser -UserPrincipalName $NewAdminUPN -TenantId $Customer.TenantId

        if ($UserDetails.BlockCredential -contains $false) {
            Set-MsolUser -UserPrincipalName $NewAdminUPN -TenantId $Customer.TenantId -BlockCredential $true
        }
        $role = Get-MsolRole -tenantid $Customer.TenantId | Where-Object {$_.name -contains "Exchange Service Administrator"}
        $checkrolemember = Get-MsolRoleMember -RoleObjectId $role.ObjectId -TenantId $Customer.TenantId | Where-Object {$_.emailaddress -contains $NewAdminUPN}
        
        if (!$checkrolemember) {
            Add-MsolRoleMember -TenantId $Customer.TenantId -RoleName "Exchange Service Administrator" -RoleMemberEmailAddress $NewAdminUPN
            Write-Output "Added $NewAdminUPN to Exchange Service Administrator Role"
        }
        else {
            Write-Output "$NewAdminUPN is still an Exchange Service Administrator"
        }
        
        Add-Entity -Table $logTable -PartitionKey $Customer.TenantId -RowKey $NewAdminUPN -TenantId $Customer.TenantId -CompanyName $Customer.Name `
            -DomainName $Customer.DefaultDomainName -UserName $NewAdminUPN -BlockCredential $UserDetails.BlockCredential -WhenCreated $UserDetails.WhenCreatedQ 
    }
}

Building Function 2 – Unblock a reporting user on demand

This function will be used to unblock a specific reporting user when required. You can call it from a Microsoft flow or another function via it’s Function URL. This function will also update the Azure Storage table we created earlier with the block status and WhenToBlock value for each reporting user.

Create a new HTTP triggered Azure Function with the following properties:

  • Call it whatever you like. This example is called UnblockReportingUser
  • Set it up to connect to Office 365 with the MSOnline module and your encrypted Delegated Administrator credentials – if you like, you can just copy the bin folder via FTP from the first function into the second function.
  • Retrieve the function URL from the top right of the function and save it somewhere.

Copy and paste the following code into your function, then save it:

# POST method: $req
$requestBody = Get-Content $req -Raw | ConvertFrom-Json
$tenantid = $requestBody.tenantid

$FunctionName = 'UnblockReportingUser'
$ModuleName = 'MSOnline'
$ModuleVersion = '1.1.166.0'
$reportinguseralias = $Env:reportinguser
$username = $Env:user
$pw = $Env:password
$storageAccount = $Env:storageAccount
$primaryKey = $Env:storageAccountKey
$UsersCreatedTableName = "ReportingUsersCreated"
$QueueName = "reportinguserblockstatus"


Write-Output $tenantid

# Create storage account context
$Ctx = New-AzureStorageContext $storageAccount -StorageAccountKey $primaryKey


# Get Logging Table or create it if it doesn't exist.
$logTable = Get-AzureStorageTable –Name $UsersCreatedTableName -Context $Ctx -ErrorAction Ignore

if ($logtable -eq $null) {
    $logTable = New-AzureStorageTable –Name $UsersCreatedTableName -Context $Ctx
}

# Get Reporting User Status Queue or create it if it doesn't exist.
$Queue = Get-AzureStorageQueue –Name $QueueName -Context $Ctx -ErrorAction Ignore
 
if ($Queue -eq $null) {
$Queue = New-AzureStorageQueue –Name $QueueName -Context $Ctx
}

function Add-Entity() {
    [CmdletBinding()]
    param(
        $table,
        [String]$PartitionKey,
        [String]$RowKey,
        [String]$WhenToBlock,
        [String]$BlockCredential

    )

    $entity = New-Object -TypeName Microsoft.WindowsAzure.Storage.Table.DynamicTableEntity -ArgumentList $partitionKey, $rowKey
    $entity.Properties.Add("WhenToBlock", $whenToBlock)
    $entity.Properties.Add("BlockCredential", $BlockCredential)

    $table.CloudTable.Execute([Microsoft.WindowsAzure.Storage.Table.TableOperation]::InsertOrMerge($entity))
}

# Import PS module
 
$PSModulePath = "D:\home\site\wwwroot\$FunctionName\bin\$ModuleName\$ModuleVersion\$ModuleName.psd1"

Import-module $PSModulePath
 
# Build Credentials

$keypath = "D:\home\site\wwwroot\$FunctionName\bin\keys\PassEncryptKey.key"
$secpassword = $pw | ConvertTo-SecureString -Key (Get-Content $keypath)
$credential = New-Object System.Management.Automation.PSCredential ($username, $secpassword)
 
# Connect to MSOnline
Connect-MsolService -Credential $credential

$InitialDomain = Get-MsolDomain -TenantId $tenantId | Where-Object {$_.IsInitial -eq $true}
$reportinguser = -join ($reportinguseralias, "@", $InitialDomain.Name)

Write-Output $reportinguser

# Unblock Reporting User
Set-MsolUser -TenantId $tenantId -UserPrincipalName $reportinguser -BlockCredential $false

$userResult = Get-MsolUser -TenantId $tenantid -UserPrincipalName $reportinguser

# Add 30 minutes to time
$ts = New-TimeSpan -Minutes 30
$whenToBlock = (get-date) + $ts

Add-Entity -Table $logTable -PartitionKey $TenantId -RowKey $reportinguser -WhenToBlock $whenToBlock -BlockCredential $userResult.BlockCredential

# Add 10 hours for Queensland Time
$ts = New-TimeSpan -Hours 10
$whenToBlock = $WhenToBlock + $ts

if (!$userResult.blockcredential) {
    $QueueMessageText = "$reportinguser will be unblocked until up to 30 minutes after $WhenToBlock"
 
    if ($Queue -ne $null) {

        $QueueMessage = New-Object -TypeName Microsoft.WindowsAzure.Storage.Queue.CloudQueueMessage -ArgumentList $QueueMessageText
        $Queue.CloudQueue.AddMessage($QueueMessage)
    }
}

Out-File -Encoding Ascii -FilePath $res -inputObject "We'll block $reportinguser at $whenToBlock"

Building Function 3 – Check Reporting User Status

This function will run every 30 minutes to check for unblocked reporting users. It connects to your Azure Storage table and looks for unblocked users whose WhenToBlock value is in the past. If it finds one, it’ll block the user and update the storage table.

Create a new Timer Triggered PowerShell function with the following values:

  • Name it whatever you like, I called ours CheckReportingUserStatus
  • Set it to run every 30 minutes using the following CRON schedule:
    0 */30 * * * *
  • Set it up to connect to Office 365 by copying the bin directory from the previous functions into it via FTP.

Now, paste in the following code:

Write-Output "PowerShell Timer trigger function executed at:$(get-date)";

$FunctionName = 'CheckReportingUserStatus'
$ModuleName = 'MSOnline'
$ModuleVersion = '1.1.166.0'
$username = $Env:user
$pw = $Env:password
$storageAccount = $Env:storageAccount
$primaryKey = $Env:storageAccountKey
$UsersCreatedTableName = "ReportingUsersCreated"
$QueueName = "reportinguserblockstatus"

# Create storage account context
$Ctx = New-AzureStorageContext $storageAccount -StorageAccountKey $primaryKey

# Get Logging Table or create it if it doesn't exist.
$table = Get-AzureStorageTable –Name $UsersCreatedTableName -Context $Ctx -ErrorAction Ignore

if ($table -eq $null) {
    $table = New-AzureStorageTable –Name $UsersCreatedTableName -Context $Ctx
}

# Get Reporting User Status Queue or create it if it doesn't exist.
$Queue = Get-AzureStorageQueue –Name $QueueName -Context $Ctx -ErrorAction Ignore
 
if ($Queue -eq $null) {
$Queue = New-AzureStorageQueue –Name $QueueName -Context $Ctx
}

# Define entity for updating reporting users in Azure Storage

function Add-Entity() {
    [CmdletBinding()]
    param(
        $table,
        [String]$PartitionKey,
        [String]$RowKey,
        [String]$WhenToBlock,
        [Boolean]$BlockCredential
    )

    $entity = New-Object -TypeName Microsoft.WindowsAzure.Storage.Table.DynamicTableEntity -ArgumentList $partitionKey, $rowKey
    $entity.Properties.Add("WhenToBlock", $whenToBlock)
    $entity.Properties.Add("BlockCredential", $BlockCredential)
    $table.CloudTable.Execute([Microsoft.WindowsAzure.Storage.Table.TableOperation]::InsertOrMerge($entity))
}

# Retrieve entities from Azure Table Storage

$query = New-Object Microsoft.WindowsAzure.Storage.Table.TableQuery
$list = New-Object System.Collections.Generic.List[string]
$list.Add("PartitionKey")
$list.Add("RowKey")
$list.Add("CompanyName")
$list.Add("BlockCredential")
$list.Add("WhenToBlock")
$query.SelectColumns = $list
$entities = $table.CloudTable.ExecuteQuery($query)

# Import PS module
$PSModulePath = "D:\home\site\wwwroot\$FunctionName\bin\$ModuleName\$ModuleVersion\$ModuleName.psd1"
Import-module $PSModulePath
 
# Build Credentials
$keypath = "D:\home\site\wwwroot\$FunctionName\bin\keys\PassEncryptKey.key"
$secpassword = $pw | ConvertTo-SecureString -Key (Get-Content $keypath)
$credential = New-Object System.Management.Automation.PSCredential ($username, $secpassword)

# Connect to MSOnline
Connect-MsolService -Credential $credential

foreach ($entity in $entities) {

    if ($entity.Properties.BlockCredential.StringValue -match "false" -and $entity.properties.WhenToBlock.StringValue) {

        $WhenToBlock = [datetime]$($entity.properties.WhenToBlock.StringValue)

        if ($WhenToBlock -lt (get-date)) {

            Set-MsolUser -tenantid $entity.partitionkey -UserPrincipalName $entity.rowkey -BlockCredential $true
            Start-Sleep -s 5
            $userResult = Get-msoluser -tenantid $entity.partitionkey -UserPrincipalName $entity.rowkey

            if ($userResult.BlockCredential -contains $true) {

                Add-Entity -Table $table -PartitionKey $entity.partitionkey -RowKey $entity.rowkey -WhenToBlock "" -BlockCredential $true
                $QueueMessageText = "$($entity.rowkey) has been blocked again"
 
                if ($Queue -ne $null) {
 
                    $QueueMessage = New-Object -TypeName Microsoft.WindowsAzure.Storage.Queue.CloudQueueMessage -ArgumentList $QueueMessageText
                    $Queue.CloudQueue.AddMessage($QueueMessage)
                }
            }
        }
    }
}

Now that we have a series of functions to manage our Exchange admins, we can put them to work. In the next post, we’ll use them to securely and completely offboard Office 365 users. This will take advantage of the MsolUser SharePoint list that we created in the previous post.

Was this article helpful?

Related Articles