Tag Archives: Azure Automation

License your Office 365/Azure AD users with Azure Automation

A while ago I wrote a post regarding schedule your Office 365 automation jobs in Azure Automation. At that time, the Azure AD PowerShell module did not support Azure Automation due to the Online Services Sign in Assistant dependency.

The preview Azure AD PowerShell module released in september do however use ADAL for authentication which solves this problem.
This is great news for all currently running licensing scripts in regular servers as scheduled tasks!

Since many probably will move from their scheduled tasks to Automation for their licensing scripts, I thought it was a good idea sharing on how to make the switch. I will use the “new” Azure portal and assume that you already have an Automation account.

1. Since the MSOnline module is not included by default in Azure Automation you need to download and install the module on your computer. When that is done you need to add the module folders (MSOnline and MSOnlineExtended) to a zip file each. The easiest way to do that is of course with PowerShell 🙂
The following code assumes that you have installed the modules in the default location and the zip files will be put on your desktop.

Add-Type -AssemblyName 'system.io.compression.filesystem'

Get-ChildItem -Path $PSHOME\Modules\MSonline* | ForEach-Object -Process {
    $Source = $_.FullName
    $Destination = '{0}\Desktop\{1}.zip' -f $env:USERPROFILE,$_.Name  
    [Io.compression.zipfile]::CreateFromDirectory($Source, $Destination) 
}

2. Upload the zipped modules files as assets to your automation account.
2015-11-28_17-38-18
2015-11-28_17-43-302015-11-28_17-42-44
3. Now add your license service account as an asset to your automation account.
If you haven’t created one you can use the below PowerShell lines to do it. Note that the account only needs to have the “User management administrator” role.

#Create the service account
New-MsolUser -UserPrincipalName licenserobot@acme333.onmicrosoft.com `
             -DisplayName "LicenseRobot" `
             -FirstName "License" `
             -LastName "Robot" `
             -Password 'password' `
             -ForceChangePassword $false `
             -PasswordNeverExpires $true 
#Add the user as an "User Account Administrator"
Add-MsolRoleMember -RoleName "User Account Administrator" `
                   -RoleMemberEmailAddress "licenserobot@acme333.onmicrosoft.com"

2015-11-28_18-06-16
4. Create your runbook and import the script. In this case I am using a slight modified version of this script, which you will find if you expand the script below below. This version are using the GroupObjectGuid to identify the groups instead of the DisplayName. Since the script is using Write-Output and Write-Warning for logging, Azure Automation will automatically handle basic logging.

LicenseAzureADUsers

#Connect to Azure AD
$Credentials = Get-AutomationPSCredential -Name 'LicenseRobot'
	
Connect-MsolService -Credential $Credentials

$Licenses = @{
                 'E1' = @{ 
                          LicenseSKU = 'tenant:STANDARDPACK'
                          Group = 'cb41a390-4312-4ecf-896e-086f64652690'
                          DisabledPlans = "SHAREPOINTSTANDARD"
                        }                        
                
                 'E3' = @{ 
                          LicenseSKU = 'tenant:ENTERPRISEPACK'
                          Group = '4549bd70-232a-4b71-8afe-4d431e5464ae'
                          DisabledPlans = "SHAREPOINTENTERPRISE","SHAREPOINTWAC"
                        }
                 'EMS' = @{
                         LicenseSKU = 'tenant:EMS'
                         Group = '9abc1736-049b-4c97-9fb6-8b6b02f9e7d5'
                 }
            
            }
    
$UsageLocation = 'SE'
    
#Get all currently licensed users and put them in a custom object
$LicensedUserDetails = Get-MsolUser -All -Synchronized | Where-Object {$_.IsLicensed -eq 'True'} | ForEach-Object {
    [pscustomobject]@{
                UserPrincipalName = $_.UserPrincipalName
                License = $_.Licenses.AccountSkuId
                DisabledPlans = $_.Licenses.Servicestatus | Where-Object {$_.Provisioningstatus -contains "Disabled"}
    }
}
   
#Create array for users to change or delete
$UsersToChangeOrDelete = @()
    
foreach ($license in $Licenses.Keys) {
      
    #Get current group name and ObjectID from Hashtable
    $GroupID = $Licenses[$license].Group
    $AccountSKU = Get-MsolAccountSku | Where-Object {$_.AccountSKUID -eq $Licenses[$license].LicenseSKU}
    $DisabledPlans =  $licenses[$license].DisabledPlans
    Write-Output "Checking for unlicensed $license users in group $GroupID..."
    #Get all members of the group in current scope
    $GroupMembers = ''
    $GroupMembers = (Get-MsolGroupMember -GroupObjectId $GroupID -All).EmailAddress
    #Get all already licensed users in current scope
    $ActiveUsers = ($LicensedUserDetails | Where-Object {$_.License -eq $licenses[$license].LicenseSKU}).UserPrincipalName 
    $UsersToHandle = ''
    $UsersToAdd = ''

    if ($GroupMembers) {  
        
        if ($ActiveUsers) {  
            #Compare $Groupmembers and $Activeusers
            #Users which are in the group but not licensed, will be added
            #Users licensed, but not, will be evaluated for deletion or change of license 
            $UsersToHandle = Compare-Object -ReferenceObject $GroupMembers -DifferenceObject $ActiveUsers -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
            $UsersToAdd = ($UsersToHandle | Where-Object {$_.SideIndicator -eq '<='}).InputObject
            $UsersToChangeOrDelete += ($UsersToHandle | Where-Object {$_.SideIndicator -eq '=>'}).InputObject  
        } else {  
            #No licenses currently assigned for the license in scope, assign licenses to all group members.
            $UsersToAdd = $GroupMembers
        }
    
    } else {  
        Write-Warning  "Group $GroupID is empty - will process removal or move of all users with license $($AccountSKU.AccountSkuId)"
        #If no users are a member in the group, add them for deletion or change of license.
        $UsersToChangeOrDelete += $ActiveUsers
    }
            
    if ($UsersToAdd -match "[.]") {
  
        foreach ($User in $UsersToAdd){
    
            #Process all users for license assignment, if not already licensed with the SKU in order.  
            $MsolUser = Get-MsolUser -UserPrincipalName $User
           
            #Assign licenses for users
            if ($MsolUser.Licenses.AccountSkuId -notcontains $AccountSku.AccountSkuId) {
                try {  
                    #Assign UsageLocation and License.
                    $LicenseAssignmentHt = @{
                        UserPrincipalname = $User
                        AddLicenses = $AccountSKU.AccountSkuId
                    }
                    #Set custom license options to not enable SharePoint / OneDrive by Default
                    
                    if ($DisabledPlans) {
                        $LicenseOptions = New-MsolLicenseOptions -AccountSkuId $AccountSKU.AccountSkuId -DisabledPlans $DisabledPlans
                        $LicenseAssignmentHt["LicenseOptions"] = $LicenseOptions
                    }
                    Set-MsolUser -UserPrincipalName $user -UsageLocation $UsageLocation -ErrorAction Stop -WarningAction Stop
                    Set-MsolUserLicense @LicenseAssignmentHt -ErrorAction Stop -WarningAction Stop
                    Write-Output "SUCCESS: Licensed $User with $license"
                } catch {  
                    Write-Warning "Error when licensing $User$_"
    
                }
            
            }  
        }
    }
}
    
#Process users for change or deletion
if ($UsersToChangeOrDelete -ne $null) {
        foreach ($User in $UsersToChangeOrDelete) {
          if ($user -ne $null) {
    
            #Fetch users old license for later usage
            $OldLicense = ($LicensedUserDetails | Where-Object {$_.UserPrincipalName -eq $User}).License
                 
             #Loop through to check if the user group assignment has been changed, and put the old and the new license in a custom object.
             #Only one license group per user is currently supported.
             $ChangeLicense = $Licenses.Keys | ForEach-Object {
                  $GroupID = $Licenses[$_].Group
                  if (Get-MsolGroupMember -All -GroupObjectId $GroupID | Where-Object {$_.EmailAddress -eq $User}) {
                     [pscustomobject]@{
                        OldLicense = $OldLicense
                        NewLicense = $Licenses[$_].LicenseSKU
                     }
                  } 
    
              }
    
              if ($ChangeLicense) {
                    #The user were assigned to another group, switch license to the new one.
                    try {  
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $ChangeLicense.OldLicense -AddLicenses $ChangeLicense.NewLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Changed license for user $User from $($ChangeLicense.OldLicense) to $($ChangeLicense.NewLicense)"
                    } catch {  
                          Write-Warning "Error when changing license on $User`r`n$_"
                    }
                      
              } else {  
                               
                    #The user is no longer a member of any license group, remove license
                    Write-Warning "$User is not a member of any group, license will be removed... "
                    try {  
                          Set-MsolUserLicense -UserPrincipalName $User -RemoveLicenses $OldLicense -ErrorAction Stop -WarningAction Stop
                          Write-Output "SUCCESS: Removed $OldLicense for $User"
                    } catch {  
                          Write-Warning "Error when removing license on user`r`n$_"
                    }
              }
         }
    }
}

2016-01-03_14-33-11
2016-01-03_15-29-22
5. Verify that the runbook is working as intended by starting it in the test pane. 2016-01-03_14-54-40
Make sure to solve eventual errors before you publish and schedule the runbook. The most common error is login errors for the automation service account.
6. Publish and schedule the runbook. In my case I will run it one time every hour.
2016-01-03_15-21-34
2016-01-03_15-19-28

You are now all set and can hopefully disable a scheduled task in your on premises environment. If thinking about running this in production, keep in mind that the PowerShell module is still in preview (I haven’t experienced any problems, but…).

Let me know if you have any questions or suggestions, and happy automation!

/Johan

Advertisements

Get rid of your Office 365 Scheduled tasks with Azure Automation!

Implementing Office 365 in a production environment you most often end up with quite a few scheduled scripts for licensing, shared mailbox maintenance and other similiar tasks. When moving more and more services to the cloud, moving from infrastructure services to platform services is the way of nature.

One service that can help you to get more efficient when it comes to scheduling scripts is Azure Automation. Instead of having a server where you schedule your scripts, you simply schedule them within the automation service that takes care of the rest. As of now, Azure Automation does not support use of the Azure Active Directory Powershell module, which means we cannot use the service for our licensing scripts. Make sure to vote for the suggestion to fix that here.

Exchange Online, Lync Online and SharePoint Online do however work well, so that’s what my example is going to be about. To make it easy for me I’m using a script from one of my older posts about Dynamically adding mailbox permissions through groups, with some adjustments to fit Azure Automation.

1. First of all, if you don’t already have one, you need an Azure Automation account. I’m choosing to create mine in the West Europe region. By default, it will use a free automation plan that gives you 500 minutes of job runtime per month.
2015-04-05_16-56-21
2. Now create an asset/setting that will store your Exchange Online admin credentials in a secure way. We will call the asset ‘EXOCreds’ so we easliy can pick them up later in the script.2015-04-05_17-03-15
2015-04-05_17-04-13
2015-04-05_17-05-11
3. Create a runbook in the automation account you created earlier, in my case I called it ‘Sync-EXOSharedMailboxPermissions’.
2015-04-05_17-15-35
4. Now it’s time to author the runbook, in my example I’ve just changed some minor things with parameters and credentials. If you’re a PowerShell WF Pro, there might be a lot of things you can do to improve the script.
2015-04-05_19-11-27
Below you’ll find the code I’ve used:

Sync-EXOSharedMailboxPermissions

workflow Sync-EXOSharedMailboxPermissions {
<#
    .SYNOPSIS
    The script will automatically assign mailbox and recipient permissions on shared mailboxes based on groups.
    Modified 2015-04-05 to support Azure automation and PowerShell workflows
    .NOTES
    File Name: SharedMailboxViaGroups.ps1
    Author   : Johan Dahlbom, johan[at]dahlbom.eu
    Blog     : 365lab.net
    The script are provided “AS IS” with no guarantees, no warranties, and they confer no rights.
    Requires PowerShell Version 3.0!
#>
    #Credentials to connect to Exchange Online
    $Credentials = Get-AutomationPSCredential -Name 'EXOCreds'
    #Prefix to search for
    $Prefix = 'SM-'
    function Connect-ExchangeOnline {
    param (
        $Creds
    )
        #Clean up existing PowerShell Sessions
        Get-PSSession | Remove-PSSession
        #Connect to Exchange Online
        $Session = New-PSSession –ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Creds -Authentication Basic -AllowRedirection
        $Commands = @("Add-MailboxPermission","Add-RecipientPermission","Remove-RecipientPermission","Remove-MailboxPermission","Get-MailboxPermission","Get-User","Get-DistributionGroupMember","Get-DistributionGroup","Get-Mailbox")
        Import-PSSession -Session $Session -Prefix "Cloud" -DisableNameChecking:$true -AllowClobber:$true -CommandName $Commands | Out-Null
    }
    Connect-ExchangeOnline -Creds $Credentials
    inlineScript {
        function Add-JDMailboxPermission {
            param(
                [string]$Identity,
                [string]$SharedMailboxName
            )
            try {
                Add-CloudMailboxPermission -Identity $SharedMailboxName -User $Identity -AccessRights FullAccess -ErrorAction stop | Out-Null
                Add-CloudRecipientPermission -Identity $SharedMailboxName -Trustee $Identity -AccessRights SendAs -Confirm:$False -ErrorAction stop | Out-Null
                Write-Output "INFO: Successfully added $Identity to $SharedMailboxName"
            } catch {
                Write-Warning "Cannot add $Identity to $SharedMailboxName`r`n$_"
            }
        }
        function Remove-JDMailboxPermission {
            param(
                [string]$Identity,
                [string]$SharedMailboxName
            )
            try {
                Remove-CloudMailboxPermission -Identity $SharedMailboxName -User $Identity -AccessRights FullAccess -Confirm:$False -ErrorAction stop -WarningAction ignore | Out-Null
                Remove-CloudRecipientPermission -Identity $SharedMailboxName -Trustee $Identity -AccessRights SendAs -Confirm:$False -ErrorAction stop -WarningAction ignore  | Out-Null
                Write-Output "INFO: Successfully removed $Identity from $SharedMailboxName"
            } catch {
                Write-Warning "Cannot remove $Identity from $SharedMailboxName`r`n$_"
            }
        }
        function Sync-EXOResourceGroup {
            [CmdletBinding(SupportsShouldProcess=$true)]
            param(
                [string]$Prefix = 'SM-'
            )
            #Get All groups to process mailboxes for
            $MasterGroups = Get-CloudDistributionGroup -ResultSize Unlimited -Identity "$Prefix*"
            foreach ($Group in $MasterGroups) {
                #Remove prefix to get the mailbox name
                $MbxName = $Group.Name.Replace("$Prefix",'')
                $SharedMailboxName =  (Get-CloudMailbox -Identity $MbxName -ErrorAction ignore -WarningAction ignore).WindowsLiveID
                if ($SharedMailboxName) {
                    Write-Verbose -Message "Processing group $($Group.Name) and mailbox $SharedMailboxName"
                    #Get all users with explicit permissions on the mailbox
                    $SharedMailboxDelegates = Get-CloudMailboxPermission -Identity $SharedMailboxName -ErrorAction Stop -ResultSize Unlimited | Where-Object {$_.IsInherited -eq $false -and $_.User -ne "NT AUTHORITY\SELF" -and $_.User -notmatch 'S-\d-\d-\d+-\d+-\d+-\d+-\w+' -and $_.User -notlike "$Prefix*"} |  Select-Object @{Name="User";Expression={(Get-CloudUser -identity $_.User).WindowsLiveID }}
                    #Get all group members
                    $SharedMailboxMembers = Get-CloudDistributionGroupMember -Identity $Group.Identity -ResultSize Unlimited
                    #Remove users if group is empty
                    if (-not($SharedMailboxMembers) -and $SharedMailboxDelegates) {
                        Write-Warning "The group $Group is empty, will remove explicit permissions from $SharedMailboxName"
                        foreach ($user in $SharedMailboxDelegates.User) {
                            Remove-JDMailboxPermission -Identity $user -SharedMailboxName $SharedMailboxName
                        }
                        #Add users if no permissions are present
                    } elseif (-not($SharedMailboxDelegates)) {
                        foreach ($user in $SharedMailboxMembers.WindowsLiveID) {
                            Add-JDMailboxPermission -Identity $user -SharedMailboxName $SharedMailboxName
                        }
                        #Process removals and adds
                    } else {
                        #Compare the group with the users that have actual access
                        $Users = Compare-Object -ReferenceObject $SharedMailboxDelegates.User -DifferenceObject $SharedMailboxMembers.WindowsLiveID 

                        #Add users that are members of the group but do not have access to the shared mailbox
                        foreach ($user in ($users | Where-Object {$_.SideIndicator -eq "=>"})) {
                            Add-JDMailboxPermission -Identity $user.InputObject -SharedMailboxName $SharedMailboxName
                        }
                        #Remove users that have access to the shared mailbox but are not members of the group
                        foreach ($user in ($users | Where-Object {$_.SideIndicator -eq "<="})) {
                            Remove-JDMailboxPermission -Identity $user.InputObject -SharedMailboxName $SharedMailboxName
                        }
                    }
                } else {
                    Write-Warning "Could not find the mailbox $MbxName"
                }
            }
        }
        #Start Processing groups and mailboxes
        Sync-EXOResourceGroup -Prefix $Using:Prefix -Verbose
    }
}

5. Test the runbook by clicking the test button. You will be asked to save the runbook before you test. Hopefully your output will be as nice looking as mine. 🙂
2015-04-05_20-12-47
6. If all went good, you’re now ready to publish and schedule the runbook. I’m choosing to schedule mine to run every three hours. Depending on your script runtime, you might want to change this due to cost or other factors. (Remember your free 500 minutes!)
2015-04-05_20-12-472015-04-06_13-40-14
2015-04-06_13-45-53
2015-04-06_13-46-56
2015-04-06_13-47-44
2015-04-06_13-48-13
2015-04-06_13-48-39

My runbook is now published and scheduled, now it’s just to wait for the magic to happen. Hope this gives you an idea what Azure Automation can do for you!

Additional resources to get started can be found below:
Get Started with Azure Automation
Automating the Cloud with Azure Automation (MVA)

Until next time! 🙂

/Johan