Reporting Azure AD Group Licensing
Azure AD Group based licensing is a pretty awesome Office365 feature. It automatically assigns licenses based on the groups of a user. This group can be a security group synced from your on-premises Active Directory or an Azure AD Group.
I like to use this feature in hybrid environments. This way you can add a user to a specific group in your AD and he will receive the correct licenses in the cloud.
There is only one problem with this approach: you can easily overprovision your licenses. Azure AD won’t give you any type of warning when the amount of members exceed the amount of licenses you have. They even say it in their documentation:
For example, you might have run out of licenses, causing some users to be in an error state. To free up the available seat count, you can remove some directly assigned licenses from other users. However, the system does not automatically react to this change and fix users in that error state.
(Source: https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-group-advanced)
To use this efficiently I want to be notified when I overprovision licenses, this is where Powershell comes in.
With the following command you can receive the groups that have licensing errors.
Get-MSOLgroup -HasLicenseErrorsOnly $true
Using this command it is easy to get the users whom have licensing errors.
The following PowerShell scripts reads out all the licensing errors and sends a formatted email report with all the errors that you currently have.
It also prints out a handy legend that shows more information about the current errors.
The full script (with synopsis and an example) can be found on my GitHub page – https://github.com/LeThijs/Azure-AD/blob/master/Get-AADLicenseErrors.ps1
Function Connect-MSOL(){ Write-Log "[INFO] - Starting Function Connect-MSO" Try{ $Cred = (Get-Credential) $null = Connect-MsolService -Credential $cred Write-Log "[INFO] - Connected to MSOL service" return $cred } Catch{ Write-Log "[ERROR] - Error connecting to MSOL, exiting" Write-Log "$($_.Exception.Message)" Exit } Write-Log "[INFO] - Exiting Function Connect-MSO" } Function Populate-ErrorMesages { Write-Log "[INFO] - Starting Function Populate-ErrorMesages" $errormessages = @() $errormessages += @{Name="CountViolation";Description="There aren't enough available licenses for one of the products that's specified in the group. You need to either purchase more licenses for the product or free up unused licenses from other users or groups."} $errormessages += @{Name="MutuallyExclusiveViolation";Description="One of the products that's specified in the group contains a service plan that conflicts with another service plan that's already assigned to the user via a different product. Some service plans are configured in a way that they can't be assigned to the same user as another, related service plan."} $errormessages += @{Name="DependencyViolation";Description="One of the products that's specified in the group contains a service plan that must be enabled for another service plan, in another product, to function. This error occurs when Azure AD attempts to remove the underlying service plan. For example, this can happen when you remove the user from the group."} $errormessages += @{Name="ProhibitedInUsageLocationViolation";Description="Some Microsoft services aren't available in all locations because of local laws and regulations."} Write-Log "[INFO] - Added $($errormessages.count) errormessages" Write-Log "[INFO] - Ending Function Populate-ErrorMesages" return $errormessages } Function Get-ErrorGroups(){ Write-Log "[INFO] - Starting Get-ErrorGroups" #find groups with license errors $errorgroups = Get-MSOLgroup -HasLicenseErrorsOnly $true Write-Log "[INFO] Found $($errorgroups.count) groups with errors" Write-Log "[INFO] - Ending Get-ErrorGroups" return $errorgroups } Function Get-LicenseErrors($errorgroups){ Write-Log "[INFO] - Starting Get-LicenseErrors" $errors = @() foreach($errorgroup in $errorgroups){ #get all user members of the group Write-Log "[INFO] - Checking members of $errorgroup.Name" Try{ $groupMembers = Get-MsolGroupMember -All -GroupObjectId $errorgroup.objectid Write-Log "[INFO] - Got groupmembers" } Catch{ Write-Log "[ERROR] - Getting groupmembers" Write-Log "$(_.Exception.Message)" } Write-Log "[INFO] - Found $($groupMembers.count) members" foreach($member in $groupMembers){ Try{ #Get user object $user = Get-MsolUser -ObjectId $member.ObjectId Write-Log "[INFO] - Got user full details $($user.UserPrincipalName)" } Catch{ Write-Log "[ERROR] - Error getting user" Write-Log "$(_.Exception.Message)" } #Check if user has errors and if errors are because of gropu we are currently checking if($user.IndirectLicenseErrors -and $user.IndirectLicenseErrors.ReferencedObjectId -eq $($errorgroup.ObjectId)){ Write-Log "[INFO] - Found user with error - $($user.UserPrincipalName)" $errors += @{UPN="$($user.UserPrincipalName)";Error="$($user.IndirectLicenseErrors.Error)";GroupName="$($errorgroup.DisplayName)";GroupDescription="$($errorgroup.Description)"} } } } Write-Log "[INFO] - Ending Get-LicenseErrors" return $errors } Function Send-Mail($errors, $Credential, $errormessages){ Write-Log "[INFO] - Starting Send-Mail" #Check if there are errors if($errors.count){ #Create body of mail, add all errors $body = "<html><body> <table> <h3>Licensing Errors</h3> <tr> <th align='left'>UPN Username</th> <th align='left'>License Error</th> <th align='left'>Group Name</th> <th align='left'>Group Description</th> </tr> " foreach($error in $errors){ $body += " <tr> <td>$($error.UPN)</td> <td>$($error.Error)</td> <td>$($error.GroupName)</td> <td>$($error.GroupDescription)</td> </tr> " } $body += "</table> " #Populate body with error messages, but only show explanation for current errors foreach($message in $errormessages){ if($errors.Error -contains $message.Name){ $body += " <h4>$($message.Name)</h4> <div>$($message.description)</div> " } } $body += "</body></html>" } else{ $body += "<html><body> <h4>There are currently no licensing errors</4></body></html>" } Try{ #Check if SSL should be used if($SMTPSSL){ Send-MailMessage -To $ReportRecipient -From $ReportSender -Subject ("Azure AD Group Licensing Errors " + (Get-Date -format d)) -Credential $Credential -Body $body -BodyAsHtml -smtpserver $SMTPServer -usessl -Port $SMTPPort } else{ Send-MailMessage -To $ReportRecipient -From $ReportSender -Subject ("Azure AD Group Licensing Errors " + (Get-Date -format d)) -Credential $Credential -Body $body -BodyAsHtml -smtpserver $SMTPServer -Port $SMTPPort } } Catch{ Write-Log "[ERROR] - Sending message" Write-Log "$(_.Exception.Message)" } Write-Log "[INFO] - Ending Send-Mail" } #endregion #region Operational Script Write-Log "[INFO] - Starting script" Try{ $Credential = Connect-MSOL $groups = Get-ErrorGroups $errors = Get-LicenseErrors -errorgroups $groups } Catch{ Write-Log "[ERROR] - Signing into Exchange Online" Write-Log "$($_.Exception.Message)" } Finally{ #Remove all current PS Sessions Get-PSSession | Remove-PSSession } $errormessages = Populate-ErrorMesages Send-Mail -errors $errors -Credential $Credential -errormessages $errormessages Write-Log "[INFO] - Stopping script"
I am a 22-year old cloud and automation enthusiast. My main focus is EMS, Powershell and Azure. My scripts can be found through my GitHub account: https://github.com/thijslecomte. I am currently blogging at http://365bythijs.be
Nice article Thijs. One small detail. Try to add hyperlink under URL, so readers can click them directly instead of having to copy/paste.
Hi Benjamin,
I would like to run this but instead of sending an email I would like it to be exported to CSV. Could you please give me a hand?
Possible that this doesnt work anymore?