Microsoft Secure Tech Accelerator
Apr 03 2024, 07:00 AM - 11:00 AM (PDT)
Microsoft Tech Community
SOLVED

Azure AD group-based license management for Office 365 and more

Silver Contributor

This looks awesome - simplify licence management for Office 365, EMS, Dynamics 365 and more with the new group-based licensing preview in Azure AD:

 

Microsoft cloud services such as Office 365, Enterprise Mobility + Security, Dynamics CRM, and other similar products require licenses to be assigned to each user who needs access to these services. Until now, licenses could only be assigned at individual user level, which can male large-scale management difficult for our customers.

 

all-products-assign.png

 

We have introduced a new capability of the Azure AD license management system: group-based licensing. It is now possible to assign one or more product licenses to a group. Azure AD will make sure that the licenses are assigned to all members of the group. Any new members joining the group will be assigned the appropriate licenses and when they leave the group those licenses will be removed. This eliminates the need for automating license management via PowerShell to reflect changes in the organization and departmental structure on a per-user basis.

 

select-a-group2.png

 

Here is the documentation with the steps to get started - What is group-based licensing in Azure Active Directory?

38 Replies
So I have set up a few AD groups that we will use to apply the licenses.

I have also set up a powershell script set up that will clear membership of those groups and refresh them every hour or so to account for changes (new users, changed situations, etc).

If I am clearing those groups out and replacing all the users frequently, is there anything to be concerned with from the group-based licensing process perspective? Or other gotchas?

Or would this be a pretty low risk process (given the code is built to properly populate the groups)?

Not a good idea as when you clear the membership GBL will trigger a remove of the license and then you would have to re-apply them and hope that your timing matches that of GBL updating the assignments in Office for example. You will likely get some very unpredictable results if you keep running this on your groups. 

 

I understand that you are doing this as a simple version of dynamic groups which is an Azure AD Premium feature but you have to change the logic to not remove member unless he/she is really removed. 

 

Brjann

 

That's disappointing, will have to experiment with how to handle delta changes to achieve same goal :(

Brent, we do something similar here, but we do delta changes to group membership using powershell, instead of a wipe and replace. It does rely on an extra step using MS Access or SQL Server to hold your combined AD/Azure data - for example, we have a scheduled task to powershell export the current Azure group listings/memberships and import into SQL Express. Another task to powershell export our local Active Directory info into same SQL Express. Then a query to find the new AD people, and another query to find the removed AD people. Export those 2 queries to a text file, and use those 2 to powershell the delta changes up to Azure.  It sounds like a lot but once you get it built, it's very quick and easy to run, and it sounds like you're almost doing that now. 

Ya, I've already got it pretty much implemented, just a simple delta comparison check of the groups instead of a wipe and replace, just took a bit more extra thought than I wanted to have :)

Not going to the complexity of tracking in Access or SQL, just powershell looking at existing AD groups we have set up and existing users.
So I just got this fully rolled out for us, and it works great! Took a bit more thought on the PowerShell automation to account for all of our scenarios, but pretty nice. I have alleviated the need for almost 10 global employees, who were managing licenses for various regions, to ever have to touch licensing at all, except for vary rare one offs that we maybe havent thought of yet, so they can focus on their other stuff now :)

Would you be willing to share your PowerShell scripts, or the relevants parts with your personal info stripped out? I'm always looking for better/faster ways to do things, but I understand that some people may not want to provide that info due to potential security reasons.

Glad to share.  Below is a sanitized version, the only thing you really have to do is set your AD domain (line 7), and then create your Groups as necessary.

 

May take a bit to disect the different scenarios I had to account for.

 

 

The main workhorse is the deltaSync function which adds and removes users as needed (instead of repopulating the license groups).

 

The getADGroupMembers function gets all users in an AD group and adds them to an array variable

 

What I am doing is basically building arrays of users:

  1. Iterate through all users (I am looking for users that have an Employee ID attribute which is connected to our HR system)
  2. Bouncing that list of users off of different Groups which will determine if they get E3's versus E5's.  
  3. Also bouncing that list of users of a few other license groups
  4. Then use the deltaSync functions to update O365 License Groups which are used directly in AAD License Templates.

I have one OU with Groups that our ID Administrators can update to account for specific scenarios.

Then I have another OU with Groups that are specifically for licenses (that will be used in AAD).

 

We are specifically applying licenses for E3's, E5's (with S4B phone), E5's (without S4B phone), Advanced Threat Protection (to E3 users), Project Online, Project Pro, Visio, PSTN Conferencing, EMS, Exchange Plan 2, and maybe one or two others.

 

Our AADConnect runs every 30 minutes, and this script runs every 30 minutes offset by 15 minutes from the AADConnect sync job.

 

 

$ScriptStart = (Get-Date)
Add-Type -AssemblyName System.DirectoryServices.AccountManagement

function getADGroupMembers($adGroupName){
    $adGroupArray = @()

    $domain='' #Enter your AD domain here
    $pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext([System.DirectoryServices.AccountManagement.ContextType]::Domain, $domain)
    $group2 = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($pc, [System.DirectoryServices.AccountManagement.IdentityType]::Name, $adGroupName)
    $group2.Members.GetEnumerator() | % { 
        #Write-Host $_.DistinguishedName
        if($adGroupName -like "O365 License*"){        
            $adGroupArray += "$($_.DistinguishedName)"
        } else {
            if($_.DistinguishedName -notlike "*Disabled Objects*"){
                $adGroupArray += "$($_.DistinguishedName)"
            }
        }    
    }

    if($adGroupArray.Length -gt 0){
        return $adGroupArray
    } else {
        return $null
    }

}

function checkMembership($user, $array){
    return $array.contains($user)
}

function checkMembershipCount($checkGroup, $checkName){
    $count = 0
    foreach($checkGroupItem in $checkGroup){
        if($checkGroupItem.contains($checkName)){
            $count += 1
        }

    }    
    return $count
}

function removeArray($array1, $array2){
    if($array2){
        $array3 = @()
        foreach($item in $array1){
            if(!$array2.Contains($item)){
                $array3 += $item
            }
        }
        return $array3
    } else {
        return $array1
    }
}

function deltaSync($adGroupName, $replaceWith){

    $replaceWith = $replaceWith | select -uniq

    Write-Host "`nProcessing Target Group:" $adGroupName -ForegroundColor Cyan
    $adGroupArray = getADGroupMembers -adGroupName "$adGroupName"

    if($replaceWith.Length -eq 0 -and $adGroupArray.Length -eq 0){
        return $false
    }

    if($replaceWith.Length -eq 0 -and $adGroupArray.Length -ne 0){
        Write-Host "Removing all users"
        Remove-ADGroupMember "$adGroupName" -Members $adGroupArray -Confirm:$false
        return $false
    }

    if($replaceWith.Length -ne 0 -and $adGroupArray.Length -eq 0){
        Write-Host "Adding all users"
        Add-ADGroupMember "$adGroupName" -Members $replaceWith -Confirm:$false
        return $false
    }

    # Compare the differences between the two groups
    $arrayDiff = Compare-Object -ReferenceObject $adGroupArray -DifferenceObject $replaceWith 

    # Iterate the differences and determine Adds / Removes
    $usersToAdd = @()
    $usersToRemove = @()
    foreach($arrayItem in $arrayDiff){      
        if($arrayItem.SideIndicator -eq "=>"){
            Write-Host "Add to Array" $arrayItem.InputObject -ForegroundColor Yellow
            $usersToAdd += "$($arrayItem.InputObject)"
        } else {
            Write-Host "Remove from Group" $arrayItem.InputObject -ForegroundColor Red
            $usersToRemove += "$($arrayItem.InputObject)"            
        }
    }

    # Add users to target Group
    if($usersToAdd.Length -gt 0){
        Write-Host "`nAdd Users Now" -ForegroundColor Yellow
        Add-ADGroupMember "$adGroupName" -Members $usersToAdd -Confirm:$false
    }

    # Remove users from target Group
    if($usersToRemove.Length -gt 0){
        Write-Host "`nRemove Users Now" -ForegroundColor Yellow
        Remove-ADGroupMember "$adGroupName" -Members $usersToRemove -Confirm:$false
    }

    return $true

}

# Define E5 Groups to Check
$groups = getADGroupMembers -adGroupName "Groups with E5 Licenses (O365 Groups)"

# Build array of Users that get E5's based on Group Membership
$E5UserArray = @()
foreach($group in $groups){
    #$group | get-member
    $adGroup = Get-ADGroup $group    
    #$zzz = Get-ADGroup $adGroup -Properties *
    #$zzz.DisplayName
    #Write-Host "Processing $($adGroup.DisplayName)"
    $members = getADGroupMembers -adGroupName "$($adGroup.Name)"
    foreach($member in $members){
        #Write-Host "  $member"
        $E5UserArray += "$($member)" 
    }
}


$Users_S4BCloud = getADGroupMembers -adGroupName "Users with S4B Phone - Cloud"
$Users_S4BOnPrem = getADGroupMembers -adGroupName "Users with S4B Phone - On Prem"
$Devices_S4B = getADGroupMembers -adGroupName "Devices with S4B Conferencing"


# Get all Users from AD
$Users = Get-ADUser -Filter * -Properties userprincipalname,msRTCSIP-PrimaryUserAddress,Company,Created,displayName,employeeNumber,c,proxyAddresses,mail,sAMAccountType,userAccountControl,enabled

$Users_E5 = @()
$Users_E5_CloudPBX = @()
$Users_E3 = @()
$Users_EMS = @()
$count = 0

foreach($user in $users){
    if(($User.EmployeeNumber) -and ($User.DistinguishedName -like "*OU=Users*") -and ($User.DistinguishedName -notlike "*OU=_Disabled Objects*")){

        $count += 1

        if(checkMembership -array $E5UserArray -user "$user"){ 
            if(checkMembership -array $Users_S4BCloud -user "$user"){
                $Users_E5_CloudPBX += "$($user.DistinguishedName)"
            } else {
                $Users_E5 += "$($user.DistinguishedName)"
            }
        } else {
            $Users_E3 += "$($user.DistinguishedName)"
        }
        $Users_EMS += "$($user.DistinguishedName)"
    }
}

Write-Host "`nFound $count Users`n"


# Build array of Users that will receive no license
Write-Host "`n****`nExclusion List  `n****" -ForegroundColor Green
$ExclusionList = getADGroupMembers -adGroupName "Users with No License"
Write-Host $ExclusionList

Write-Host "`n****`nDevices with S4B Conferencing  `n****" -ForegroundColor Green
$Devices_S4B = getADGroupMembers -adGroupName "Devices with S4B Conferencing"
deltaSync -adGroup "O365 License Users with E5 (Devices)" -replaceWith $Devices_S4B

Write-Host "`n****`nUsers with Visio  `n****" -ForegroundColor Green
$Users_Visio = getADGroupMembers -adGroupName "Users with Visio"
$Users_Visio = removeArray -array1 $Users_Visio -array2 $ExclusionList
deltaSync -adGroup "O365 License Users with Visio" -replaceWith $Users_Visio

Write-Host "`n****`nUsers and Devices with Exchange Only  `n****" -ForegroundColor Green
$Users_ExchangeOnly = getADGroupMembers -adGroupName "Service Accounts with Email Only"
$Users_VM = getADGroupMembers -adGroupName "Devices with Voicemail"
$Users_ExchangeOnly = $Users_ExchangeOnly + $Users_VM
$Users_ExchangeOnly = removeArray -array1 $Users_ExchangeOnly -array2 $ExclusionList
deltaSync -adGroup "O365 License Users with Exchange Only" -replaceWith $Users_ExchangeOnly

Write-Host "`n****`nE5 (Regular)  `n****" -ForegroundColor Green
$Users_E5 = removeArray -array1 $Users_E5 -array2 $ExclusionList
deltaSync -adGroup "O365 License Users with E5 (Regular)" -replaceWith $Users_E5

Write-Host "`n****`nE5 (Phone)  `n****" -ForegroundColor Green
$Users_E5_CloudPBX = $Users_E5_CloudPBX
$Users_E5_CloudPBX = removeArray -array1 $Users_E5_CloudPBX -array2 $ExclusionList
deltaSync -adGroup "O365 License Users with E5 (Phone)" -replaceWith $Users_E5_CloudPBX

Write-Host "`n****`nE3 (Temporary)  `n****" -ForegroundColor Green
$Users_E3_Temporary = getADGroupMembers -adGroupName "Users with E3 Limited Licenses"
deltaSync -adGroup "O365 License Users with E3 (Temporary)" -replaceWith $Users_E3_Temporary

Write-Host "`n****`nE3 (Service Accounts)  `n****" -ForegroundColor Green
$Users_E3_ServiceAccounts = getADGroupMembers -adGroupName "Service Accounts with E3 Licenses"
deltaSync -adGroup "O365 License Users with E3 (Service Accounts)" -replaceWith $Users_E3_ServiceAccounts

$Users_E3_Manual = getADGroupMembers -adGroupName "Users (Non-Buckman) with E3 Licenses"
$Users_E3 = $Users_E3 + $Users_E3_Manual
$Users_E3 = removeArray -array1 $Users_E3 -array2 $ExclusionList
$Users_E3 = removeArray -array1 $Users_E3 -array2 $Users_E3_Temporary
Write-Host "`n****`nE3 / ATP  `n****" -ForegroundColor Green
deltaSync -adGroup "O365 License Users with E3" -replaceWith $Users_E3

Write-Host "`n****`nATP  `n****" -ForegroundColor Green
$Users_ATP = $Users_E3 + $Users_E3_Temporary
deltaSync -adGroup "O365 License Users with ATP" -replaceWith $Users_ATP

Write-Host "`n****`nEMS  `n****" -ForegroundColor Green
$Users_EMS = $Users_E3 + $Users_E5_CloudPBX + $Users_E5
$Users_EMS = removeArray -array1 $Users_EMS -array2 $Devices_S4B
$Users_EMS = removeArray -array1 $Users_EMS -array2 $ExclusionList
deltaSync -adGroup "O365 License Users with EMS" -replaceWith $Users_EMS

Write-Host "`n****`nProject Pro  `n****" -ForegroundColor Green
$Users_ProjectPro = getADGroupMembers -adGroupName "Users with Project Pro"
deltaSync -adGroup "O365 License Users with Project Pro" -replaceWith $Users_ProjectPro

Write-Host "`n****`nProject Online  `n****" -ForegroundColor Green
$Users_ProjectOnline = getADGroupMembers -adGroupName "Users with Project Online"
deltaSync -adGroup "O365 License Users with Project Online" -replaceWith $Users_ProjectOnline

Write-Host "`n****`nUsers with PSTN Conferencing  `n****" -ForegroundColor Green
$Users_PSTN = getADGroupMembers -adGroupName "Users with PSTN Conferencing"
$Users_PSTN = removeArray -array1 $Users_PSTN -array2 $ExclusionList
deltaSync -adGroup "O365 License Users with PSTN Conferencing" -replaceWith $Users_PSTN

$ALL_E5_1 = getADGroupMembers -adGroupName "O365 License Users with E5 (Phone)"
$ALL_E5_2 = getADGroupMembers -adGroupName "O365 License Users with E5 (Regular)"
$ALL_E5 = $ALL_E5_2 + $ALL_E5_1 
$ALL_E3 = getADGroupMembers -adGroupName "O365 License Users with E3"
$Users_PSTN_E3 = removeArray -array1 $Users_PSTN -array2 $ALL_E5
$Users_PSTN_E3 = removeArray -array1 $Users_PSTN_E3 -array2 $ExclusionList
deltaSync -adGroup "O365 License Users with PSTN Conferencing (E3)" -replaceWith $Users_PSTN_E3
$Users_PSTN_E5 = removeArray -array1 $Users_PSTN -array2 $ALL_E3
$Users_PSTN_E5 = removeArray -array1 $Users_PSTN_E5 -array2 $ExclusionList
deltaSync -adGroup "O365 License Users with PSTN Conferencing (E5)" -replaceWith $Users_PSTN_E5


$ScriptEnd = (Get-Date)
$RunTime = New-Timespan -Start $ScriptStart -End $ScriptEnd
"`nElapsed Time: {0}:{1}:{2}" -f $RunTime.Hours,$Runtime.Minutes,$RunTime.Seconds
 

 

Wow! Thank you!! I'm going to dig into this and see what I can re-use for my environment, which looks like it will end up saving us more time here too. I really appreciate your post!!
Spoiler
 

Is it also possible to get an export from for example all the users with the E3 license?

Hello, 

as I understand it is still in public preview. So my question, do you have a timeline when group-based license management will be GA? And how quick will it be available (GA) in the German Cloud?

 

Regards Thomas 

I'm keen to understand when this is going GA as well.

I concur, when is this going GA?

We have just changed our licensing to Office 365 E3 to Office 365 E5. 

And Kiosk to F1 licensing is there any reason not to use group based licensing?

This would help flip all my users properly and also remove the services that we didn't want to go live quite yet.

 

When is this going GA?

We found that the “Azure AD group-based license management” (in public preview) is not currently smart enough to recognize a single user license between E3 and E5. It “double dips”, so a user who has an E5 license (direct or inherited) and an E3 license (direct or inherited) takes up two license; one E3 and one E5. This scenario did not create any warning or alert from the system. Is there a UserVoice style area to communicate with folks evaluating what will be GA?

@Deleted, here's where you could add in to Azure AD ideas on UserVoice: https://feedback.azure.com/forums/169401-azure-active-directory

 

There's some Group Based Licensing requests in there already.

The problem here isn't the AD Group based implementation. it honours whatever licensing rules are applied by the platform. Therefore if you can apply the two license templates in the Office 365 UI, then you can do the same in the Group Based templates.

 

In this instance, it's a viable solution to apply elements from both E3 and E5 to a single user (Note I said viable.. not sensible!). You'll find that you can tick both E3 and E5 in the Office 365 UI. If you tried to do the same using and F1 and E3 or F1 and E5 it would throw an error in the UI and also in the Group Based licensing interfaces. 

@Paul Hunt - Cimares I like your quantifier "(Note I said viable.. not sensible!)"

The problem, of course is, if a thing is not sensible, someone will still try to do it at the expense of others around them.

I do understand what you are saying though.

We - large scale corporate implementation - will need a reasonable way of reporting on it or preventing it.

Pulling the data per user per license per service down from the tenant via PowerShell then republishing it via PowerBI is also viable but not sensible. ;)

My tests of the group-based license management is going well. Its value is clear especially given Microsoft's gross propensity to force service plans out as "Enabled by default". (another viable not sensible example)