Friday, November 11, 2022

Create AAD Licensing groups by Graph API

Group based licensing in Microsoft 365 is not a new feature, but still a feature a lot of organizations is missing out on. Assigning licenses to groups instead of directly to users provides advantages related to automation, overview and more. Information of this is easily available on net, but I have been missing an automated way of providing uniform groups for the purpose.

Assigning licenses to users by group membership in Azure Active directory is consistently documented at Microsoft Learn and at several other online locations. The advantages of this are therefore not specifically mentioned in this blog post. 


This blog post will focus on the creation of the groups in a uniform and automated way. Repeating manual tasks are not desired in a larger environment. This leads to small differences and configuration drift. By automating, we arrive at a uniform standard platform as quickly as possible.

There is no requirement to use dedicated groups. It is possible to use existing groups for assigning licenses. You can even nest groups. This blogpost will cover how I in an automated way can create a licensing group for each available SKU in the tenant by use of Microsoft Graph API.

When connected to MgGraph, I can list all SKUs in the tenant by using the following command:

Get-MgSubscribedSku

There might be some SKUs coming out of that query where you don't want to create a belonging license group. I have taken this into account in my script and created a table where I can add SKUs to be excluded from the routine:

# Insert SKUs not manageable by groups
$SKUsNotToManage = @(
    "WINDOWS_STORE"
    "RMSBASIC"
)

This gives me the opportunity to retrieve only a selection of SKUs from the tenant:

# Gather all SKUs in Tenant
$SKUsToManage = Get-MgSubscribedSku | Where-Object { $_.SkuPartNumber -notin $SKUsNotToManage }

Naming SKUs in Tenant is also something I want to enrich related to the corresponding group names. I have seen many people solve this using manuallly maintained tables. I have instead chosen to import a CSV from Microsoft at run time. This CSV is regularly updated by Microsoft with friendly display names for each SKU part number.

# Import Microsoft CSV file with friendly display name and SKU Partnumber
$licenseCsvURL = 'https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv'
 
$skuHashTable = @{}
(Invoke-WebRequest -Uri $licenseCsvURL).ToString() | ConvertFrom-Csv | ForEach-Object {
    $skuHashTable[$_.String_Id] = @{
        "SkuId"         = $_.GUID
        "SkuPartNumber" = $_.String_Id
        "DisplayName"   = $_.Product_Display_Name
    }
}

With this information available, I can construct the DisplayName for my groups:

    $SKUpartno = $SKU.SkuPartNumber
    $GroupDisplayName = RenameDisplayName -DisplayName "AZ-LIC-$(($skuHashTable["$SKUpartno"]).DisplayName)"

"RenameDisplayName" is a function I created to make some replacements of common abbreviations in the name. These are defined in a hash table:

# Insert translations used to shorten group names
$DisplayNameTranslations = @{
    "Azure Active Directory" = 'AAD'
    "Microsoft 365"          = 'M365'
}

The Function will then use information from this hash table to do text replacements in the group name:

function RenameDisplayName {
    Param (
        [Parameter(Mandatory = $true)]
        [string]$DisplayName
    )
    foreach ($Translation in $DisplayNameTranslations.GetEnumerator()) {
        $DisplayName = $DisplayName.Replace($Translation.Name, $Translation.Value)
    }
    $DisplayName

Here is an example on how this will work out:

  • SKU name received from tenant: SPE_E3
  • Matched displayname from CSV: Microsoft 365 E3
  • Displayname after my fynction: M365 E3

This should give a dynamic method for creating uniform group names in a preferred pattern based of available SKUs in the tenant and assign the corresponding SKU as assigned license to the group.

Putting this together, the final script will look like the following (also available on my GitHub):

<#
.SYNOPSIS
  Script to create Azure AD License groups.
.DESCRIPTION
    Script to create Azure AD License groups for available SKUs used for assigning licenses to users in the tenant.
    The script has a table of SKUs not managed by groups, and a table of translations to apply common abbreviations.
.EXAMPLE
   
.NOTES
    Version:        1.0
    Author:         Simon Skotheimsvik
    Info:           https://skotheimsvik.blogspot.com/2022/11/create-aad-licensing-groups-by-graph-api.html        
    Creation Date:  08.11.2022
    Updated:              03.11.2022
    Version history:
    1.0 - (08.11.2022) Script released

#>

#region functions
function RenameDisplayName {
    Param (
        [Parameter(Mandatory = $true)]
        [string]$DisplayName
    )
    foreach ($Translation in $DisplayNameTranslations.GetEnumerator()) {
        $DisplayName = $DisplayName.Replace($Translation.Name, $Translation.Value)
    }
    $DisplayName
}# end function

#endregion functions

#region Variables

# Insert SKUs not manageable by groups
$SKUsNotToManage = @(
    "WINDOWS_STORE"
    "RMSBASIC"
)

# Insert translations used to shorten group names
$DisplayNameTranslations = @{
    "Azure Active Directory" = 'AAD'
    "Microsoft 365"          = 'M365'
}

# Import Microsoft CSV file with friendly display name and SKU Partnumber
$licenseCsvURL = 'https://download.microsoft.com/download/e/3/e/e3e9faf2-f28b-490a-9ada-c6089a1fc5b0/Product%20names%20and%20service%20plan%20identifiers%20for%20licensing.csv'
 
$skuHashTable = @{}
(Invoke-WebRequest -Uri $licenseCsvURL).ToString() | ConvertFrom-Csv | ForEach-Object {
    $skuHashTable[$_.String_Id] = @{
        "SkuId"         = $_.GUID
        "SkuPartNumber" = $_.String_Id
        "DisplayName"   = $_.Product_Display_Name
    }
}

#endregion Variables

#region connect
Select-MgProfile beta
Connect-MgGraph -Scopes "Directory.Read.All", "Group.ReadWrite.All" -ForceRefresh
Import-Module Microsoft.Graph.Groups
#endregion connect

#region script

# Gather all SKUs in Tenant
$SKUsToManage = Get-MgSubscribedSku | Where-Object { $_.SkuPartNumber -notin $SKUsNotToManage }

# Create groups for SKUs in Tenant
foreach ($SKU in $SKUsToManage) {
    # Get friendly shortened GroupDisplayName
    $SKUpartno = $SKU.SkuPartNumber
    $GroupDisplayName = RenameDisplayName -DisplayName "AZ-LIC-$(($skuHashTable["$SKUpartno"]).DisplayName)"

    # Finds group, create it if not existing
    if ($Group = Get-MgGroup | Where-Object { $_.DisplayName -like $GroupDisplayName }) {
        Write-Output """$GroupDisplayName"" exists"
    }
    else {
        Write-Output """$GroupDisplayName"" does not exist"
        $Group = New-MgGroup -DisplayName $GroupDisplayName -Description "This group is used to assign $(($skuHashTable["$SKUpartno"]).DisplayName) licenses." -MailEnabled:$false -SecurityEnabled -MailNickName ($GroupDisplayName).Replace(" ", "") #-IsAssignableToRole:$true
    }

    # Add license to group
    $params = @{
        AddLicenses = @(
            @{
                SkuId = $Sku.SkuId
            }
        )
        RemoveLicenses = @(
        )
    }
    Set-MgGroupLicense -GroupId $Group.Id -BodyParameter $params
}
#endregion script

When adding a user to one of these groups, a license will be assigned (as long as there are free licenses available in the tenant). As a bonus tip you can use the same group to assign software installations to the user's endpoints in Microsoft Intune!


No comments:

Post a Comment