Azure AD direct to group license assignment migration scripts

Azure AD logo

PowerShell scripts to automate the migration from direct license assignment (in Office 365/Azure AD) to group based licensing. This assumes you have on-prem AD and will be adding users to AD based groups, although the scripts could be tweaked to use AAD groups.

I wrote these scripts to automate the migration of users, they are a bit rough and ready so make sure you understand and test these well before using in a live environment.

The scripts will do the following:

1st script assigns users to AD groups for individual plans e.g. components of E3 such as Teams or Planner. It will check which direct assigned licenses the user has, and add them to the appropriate group.

2nd script assigns users to AD groups for complete SKUs e.g. EMS.

3rd script removes any direct assigned SKUs, leaving just the group assigned plans and SKUs (make sure replication has completed, I would leave at least an hour for AAD to make the license changes).

You may only need script 1 or 2 depending on how you organised your AD groups, I would normally recommend one group per service in the plan e.g. Exchange P2, Teams etc, but sometimes it also make sense to have a complete SKU e.g. Visio or EMS.

Firstly, the scripts use a connection script:


1
2
3
4
5
6
write-host "Checking connection"
try 
{ $var = Get-AzureADTenantDetail } 

catch [Microsoft.Open.Azure.AD.CommonLibrary.AadNeedAuthenticationException] 
{ Write-Host "Not connected, authenticate in other window"; Connect-AzureAD}

Script 1:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# Script to remove direct assigned licenses
# 1st script assigns users to AD groups for individual plans e.g. components of E3 such as Teams or Planner.
# 2nd script assigns users to AD groups for SKUs e.g. EMS
# 3rd script removes any direct assigned SKUs, leaving just the group assigned plans and SKUs
# Check if a user has both direct and inherited license see https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-ps-examples
# For Service plan IDs see https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-service-plan-reference

$ErrorActionPreference = "Continue"

# Variables
$CSVfile = "$PSScriptRoot\LicenseToGroupUsers.csv"

# Connect to AzureAD
."$PSScriptRoot\..\Connections\ConnectAzureAD.ps1"

# Enter planID and group, use plan IDs from below.
# For Dynamics choose DYN365_ENTERPRISE_SALES or DYN365_ENTERPRISE_TEAM_MEMBERS (plans not SKUs with same name)
# $SKUs = Get-AzureADSubscribedSku | Where-Object {$_.SkuPartNumber -eq "ENTERPRISEPACK"}
# $SKUs.ServicePlans

#Hash table to map service plans to AD groups, these are just the E3 components that users should be allowed to use. Any others not in this list will be removed.
$ServicePlanToGroup = @{
    "57ff2da0-773e-42df-b2af-ffb7a2317929"="O365_Teams_Users"
    "b737dad2-2f6c-4c65-90e3-ca563267e8b9"="O365_Planner_Users"
    "43de0ff5-c92c-492b-9116-175376d08c38"="O365_OfficeProPlus_Users"
    "e95bec33-7c88-4a70-8e19-b10bd9d0c014"="O365_OfficeWeb_Users"
    "efb87545-963c-4e0d-99df-69c6916d9eb0"="O365_ExchangeP2_Users"
    "5dbe027f-2339-4123-9542-606e4d348a72"="O365_Sharepoint_Users"
}

# $SKUs = Get-AzureADSubscribedSku | Where-Object {$_.SkuPartNumber -eq "ENTERPRISEPACK"}
# $SKUs.ServicePlans
# Also see https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-service-plan-reference
# Only including E3 licenses
# This is just for reference
$ServicePlans = @{
    "aebd3021-9f8f-4bf8-bbe3-0ed2f4f047a1"="KAIZALA_O365_P3"
    "94065c59-bc8e-4e8b-89e5-5138d471eaff"="MICROSOFT_SEARCH"
    "94a54592-cd8b-425e-87c6-97868b000b91"="WHITEBOARD_PLAN2"
    "5136a095-5cf0-4aff-bec3-e84448b38ea5"="MIP_S_CLP1"
    "33c4f319-9bdd-48d6-9c4d-410b750a4a5a"="MYANALYTICS_P2"
    "c87f142c-d1e9-4363-8630-aaea9c4d9ae5"="BPOS_S_TODO_2"
    "2789c901-c14e-48ab-a76a-be334d9d793a"="FORMS_PLAN_E3"
    "9e700747-8b1d-45e5-ab8d-ef187ceec156"="STREAM_O365_E3"
    "8c7d2df8-86f0-4902-b2ed-a0458298f3b3"="Deskless"
    "76846ad7-7776-4c40-a281-a386362dd1b9"="FLOW_O365_P2"
    "c68f8d98-5534-41c8-bf36-22fa496fa792"="POWERAPPS_O365_P2"
    "57ff2da0-773e-42df-b2af-ffb7a2317929"="TEAMS1"
    "b737dad2-2f6c-4c65-90e3-ca563267e8b9"="PROJECTWORKMANAGEMENT"
    "a23b959c-7ce8-4e57-9140-b90eb88a9e97"="SWAY"
    "882e1d05-acd1-4ccb-8708-6ee03664b117"="INTUNE_O365"
    "7547a3fe-08ee-4ccb-b430-5077c5041653"="YAMMER_ENTERPRISE"
    "43de0ff5-c92c-492b-9116-175376d08c38"="OFFICESUBSCRIPTION"
    "0feaeb32-d00e-4d66-bd5a-43b5b83db82c"="MCOSTANDARD"
    "e95bec33-7c88-4a70-8e19-b10bd9d0c014"="SHAREPOINTWAC"
    "5dbe027f-2339-4123-9542-606e4d348a72"="SHAREPOINTENTERPRISE"
    "efb87545-963c-4e0d-99df-69c6916d9eb0"="EXCHANGE_S_ENTERPRISE"
  }

  #These are not in E3 so ignore them
  $ServicePlansIgnore = @{
    "932ad362-64a8-4783-9106-97849a1a30b9"="CLOUD APP SECURITY DISCOVERY"
    "6c57d4b6-3b23-47a5-9bc9-69f17b4947b3"="AZURE INFORMATION PROTECTION PREMIUM P1"
    "c1ec4a95-1f05-45b3-a911-aa3fa01094f5"="MICROSOFT INTUNE"
    "8a256a2b-b617-496d-b51b-e76466e88db0"="MICROSOFT AZURE MULTI-FACTOR AUTHENTICATION"
    "41781fb2-bc02-4b7c-bd55-b576c07bb09d"="AZURE ACTIVE DIRECTORY PREMIUM P1"
    "6a54b05e-4fab-40e7-9828-428db3b336fa"="DYN365_ENTERPRISE_TEAM_MEMBERS"
    "2da8e897-7791-486b-b08f-cc63c8129df7"="DYN365_ENTERPRISE_SALES"
    "f5aa7b45-8a36-4cd1-bc37-5d06dea98645"="DYNAMICS_365_FOR_OPERATIONS_TEAM_MEMBERS"
    "f2f49eef-4b3f-4853-809a-a055c6103fe0"="DYNAMICS 365 FOR TALENT - ONBOARD EXPERIENCE"
    "52e619e2-2730-439a-b0d3-d09ab7e8b705"="POWERAPPS FOR DYNAMICS 365"
    "643d201a-9884-45be-962a-06ba97062e5e"="DYNAMICS 365 FOR TALENT - ATTRACT EXPERIENCE TEAM MEMBER"
    "d5156635-0704-4f66-8803-93258f8b2678"="DYNAMICS 365 FOR TALENT TEAM MEMBERS"
    "c0454a3d-32b5-4740-b090-78c32f48f0ad"="DYNAMICS 365 FOR RETAIL TEAM MEMBERS"
    "1ec58c70-f69c-486a-8109-4b87ce86e449"="FLOW FOR DYNAMICS 365"
    "1259157c-8581-4875-bca7-2ffb18c51bda"="PROJECT ONLINE ESSENTIALS"
    "bea4c11e-220a-4e6d-8eb8-8ea15d019f90"="MICROSOFT AZURE ACTIVE DIRECTORY RIGHTS"
    "663a804f-1c30-4ff0-9915-9db84f0d1cea"="VISIO_CLIENT_SUBSCRIPTION"
    "2bdbaf8f-738f-4ac7-9234-3c3ee2ce7d0f"="VISIOONLINE"
    "da792a53-cbc0-4184-a10d-e544dd34b3c1"="ONEDRIVE_BASIC"
    "17ab22cd-a0b3-4536-910a-cb6eb12696c0"="Microsoft Flow Free"
    "50e68c76-46c6-4674-81f9-75456511b170"="DYN365_CDS_VIRAL"
    "70d33638-9c74-4d01-bfd3-562de28bd4ba"="Power BI Pro"
    "874fc546-6efe-4d22-90b8-5c4e7aa59f4b"="POWERAPPS FOR DYNAMICS 365"
    "7e6d7d78-73de-46ba-83b1-6d25117334ba"="FLOW FOR DYNAMICS 365"
    "03acaee3-9492-4f40-aed4-bcb6b32981b6"="MICROSOFT SOCIAL ENGAGEMENT - SERVICE DISCONTINUATION"
    "8839ef0e-91f1-4085-b485-62e06e7c7987"="UNKNOWN"
    "2049e525-b859-401b-b2a0-e0a31c4b1fe4"="POWER_BI_STANDARD"
  }

# Import the CSV file
try {
    $users = import-csv $CSVfile
    }
    catch {
        $errorZero = $Error[0]
        write-host "Error: " $errorZero -ForegroundColor Red  #Writes the latest error
        Break
    }


    
write-warning "About to add the following users to license groups:"
foreach ($user in $users){
write-host $user.UserPrincipalName
}
Read-Host -Prompt "Press Enter to continue or CTRL+C to quit"

foreach ($user in $users){
    $groupsToAdd = @()
    $groupsToRemove = @()
    write-host "Processing" $user.UserPrincipalName

# Get licensed plans for the user
$aaduser = get-azureaduser -objectID $user.UserPrincipalName
$plans = $aaduser | Select UserPrincipalName,ImmutableID -ExpandProperty AssignedPlans
$enabledPlans = $plans | Where-Object {$_.CapabilityStatus -eq "Enabled" }
write-host "Enabled plans:"
$enabledPlans | ft ServicePlanID,Service,CapabilityStatus,UserPrincipalName

#Get the AD ObjectGuid for the group add (cannot use UPN)
$ImmutableID = "" #Null these out otherwise gets reused from previous 
#Have to match using the guid
$ImmutableID = $aaduser.ImmutableID
if ($ImmutableID) {$objectGUID = ([GUID][System.Convert]::FromBase64String($ImmutableID)).Guid}
else {
    write-warning "Error getting ImmutableID for $UPN, user is likely cloud only, skipping"
    Break
    }
foreach ($plan in $enabledPlans) {
# Check if there is a matching AD group, otherwise plan will be removed
if ($ADGroup = $ServicePlanToGroup[$plan.ServicePlanId]) {
    write-host "Adding" $user.UserPrincipalName" to $ADGroup" -ForegroundColor Green
    try {
        Add-ADGroupMember -Identity $ADGroup -members $objectGUID #-whatif #Note that this will not warn if existing member if the domain is server 2008 R2 or lower
        }
        catch {
            $errorZero = $Error[0]
            write-host "Error: " $errorZero -ForegroundColor Red  #Writes the latest error
            #Break
        }
}
elseif ($ServicePlansIgnore[$plan.ServicePlanId]) {
    write-host "Plan part of another SKU and will not be affected:" $ServicePlansIgnore[$plan.ServicePlanId]
}
else {
    write-host "Direct assigned plan will be removed:" $ServicePlans[$plan.ServicePlanId] $plan.ServicePlanId -ForegroundColor Yellow
}
}

}

Script 2:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# This script is used when there are groups for complete SKUs e.g. EMS or VISIO
# Wait for AD sync before removing the direct assigned license for the SKU
# Check if a user has both direct and inherited license see https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-ps-examples
# For Service plan IDs see https://docs.microsoft.com/en-us/azure/active-directory/users-groups-roles/licensing-service-plan-reference

$ErrorActionPreference = "Continue"

# Variables
$CSVfile = "$PSScriptRoot\LicenseToGroupUsers.csv"

# Connect to AzureAD
."$PSScriptRoot\..\Connections\ConnectAzureAD.ps1"

# Enter planID and group, use plan IDs from below.
# For Dynamics choose DYN365_ENTERPRISE_SALES or DYN365_ENTERPRISE_TEAM_MEMBERS (plans not SKUs with same name)
# $SKUs = Get-AzureADSubscribedSku | Where-Object {$_.SkuPartNumber -eq "ENTERPRISEPACK"}
# $SKUs.ServicePlans

#Hash table to map service plans to AD groups, these are just the SKUs that users should be allowed to use. Any others not in this list will be removed.
$SKUToGroup = @{
    "1e1a282c-9c54-43a2-9310-98ef728faace"="O365_Dynamics_Sales"
    "53818b1b-4a27-454b-8896-0dba576410e6"="O365_ProjectP3_Users"
    "8e7a3d30-d97d-43ab-837c-d7701cef83dc"="O365_Dynamics_Team"
    "c5928f49-12ba-48f7-ada3-0d743a3601d5"="O365_VisioP2_Users"
    "efccb6f7-5641-4e0e-bd10-b4976e1bf68e"="O365_EMS_Users"
    "f8a1db68-be16-40ed-86d5-cb42ce701560"="O365_PowerBIPro_Users"
    "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235"="O365_PowerBIFree_Users"
}
#These SKUs are licensed using individual components (plans) so ignoring here. Should have been done in 1st script.
$SKUToIgnore = @{
    "6fd2c87f-b296-42f0-b197-1e91e994b900"="ENTERPRISEPACK"
}
# $SKUs = Get-AzureADSubscribedSku | Where-Object {$_.SkuPartNumber -eq "ENTERPRISEPACK"}
# $SKUs.ServicePlans
# Build a hash table with SkuPartNumber and SkuId
$AllSKUs = @{
    "c5928f49-12ba-48f7-ada3-0d743a3601d5"="VISIOCLIENT"
    "1f2f344a-700d-42c9-9427-5cea1d5d7ba6"="STREAM"
    "f8a1db68-be16-40ed-86d5-cb42ce701560"="POWER_BI_PRO"
    "6fd2c87f-b296-42f0-b197-1e91e994b900"="ENTERPRISEPACK"
    "f30db892-07e9-47e9-837c-80727f46fd3d"="FLOW_FREE"
    "726a0894-2c77-4d65-99da-9775ef05aad1"="MICROSOFT_BUSINESS_CENTER"
    "dcb1a3ae-b33f-4487-846a-a640262fadf4"="POWERAPPS_VIRAL"
    "74fbf1bb-47c6-4796-9623-77dc7371723b"="MS_TEAMS_IW"
    "e06abcc2-7ec5-4a79-b08b-d9c282376f72"="CRMTESTINSTANCE"
    "6070a4c8-34c6-4937-8dfb-39bbc6397a60"="MEETING_ROOM"
    "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235"="POWER_BI_STANDARD"
    "efccb6f7-5641-4e0e-bd10-b4976e1bf68e"="EMS"
    "078d2b04-f1bd-4111-bbd4-b4b1b354cef4"="AAD_PREMIUM"
    "9d776713-14cb-4697-a21d-9a52455c738a"="CRMINSTANCE"
    "53818b1b-4a27-454b-8896-0dba576410e6"="PROJECTPROFESSIONAL"
    "29a2f828-8f39-4837-b8ff-c957e86abe3c"="TEAMS_COMMERCIAL_TRIAL"
    "1e1a282c-9c54-43a2-9310-98ef728faace"="DYN365_ENTERPRISE_SALES"
    "8e7a3d30-d97d-43ab-837c-d7701cef83dc"="DYN365_ENTERPRISE_TEAM_MEMBERS"
    }
# Import the CSV file
try {
    $users = import-csv $CSVfile
    }
    catch {
        $errorZero = $Error[0]
        write-host "Error: " $errorZero -ForegroundColor Red  #Writes the latest error
        Break
    }


    
write-warning "About to add the following users to license groups for complete SKU:"
foreach ($user in $users){
write-host $user.UserPrincipalName
}
Read-Host -Prompt "Press Enter to continue or CTRL+C to quit"

foreach ($user in $users){
    $groupsToAdd = @()
    $groupsToRemove = @()
    write-host "Processing" $user.UserPrincipalName

# Get licensed SKUs for the user
$aaduser = get-azureaduser -objectID $user.UserPrincipalName
$SKUs = $aaduser | Select UserPrincipalName,ImmutableID -ExpandProperty AssignedLicenses


#Get the AD ObjectGuid for the group add (cannot use UPN)
$ImmutableID = "" #Null these out otherwise gets reused from previous 
#Have to match using the guid
$ImmutableID = $aaduser.ImmutableID
if ($ImmutableID) {$objectGUID = ([GUID][System.Convert]::FromBase64String($ImmutableID)).Guid}
else {
    write-warning "Error getting ImmutableID for $UPN, user is likely cloud only, skipping"
    Break
    }
    foreach ($SKU in $SKUs) {
        if($ADGroup = $SKUToGroup[$SKU.SkuId]) {
            write-host "Adding" $user.UserPrincipalName"to $ADGroup" -ForegroundColor Green
            try {
                Add-ADGroupMember -Identity $ADGroup -members $objectGUID #-whatif #Note that this will not warn if existing member if the domain is server 2008 R2 or lower
                }
                catch {
                    $errorZero = $Error[0]
                    write-host "Error: " $errorZero -ForegroundColor Red  #Writes the latest error
                }
        }
        elseif($ADGroup = $SKUToIgnore[$SKU.SkuId]) {
            write-host "Should have already been assigned individual plans:"$SKUToIgnore[$SKU.SkuId]
        }
        else {
            write-host "SKU not used so will be removed during next script:"$AllSKUs[$SKU.SkuId] $SKU.SkuId -ForegroundColor Yellow
        }
     }
}

Script 3:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# This script will remove a complete SKU e.g. EMS, E3 etc, along with all components, unless they have been assigned via group.
# This will remove all direct licenses leaving only inherited (group based) licenses, so make sure the users have been added all relevant groups for the SKU first
# Will have no effect if license is group assigned (inherited) only

# Connect to AzureAD
."$PSScriptRoot\..\Connections\ConnectAzureAD.ps1"

#
#$ErrorActionPreference = "SilentlyContinue"

# Variables
$CSVfile = "$PSScriptRoot\LicenseToGroupUsers.csv"

# Enter SKUID and group, use plan IDs from below.
# Get-AzureADSubscribedSku | select SkuID,SKuPartNumber

# Build a hash table with SkuPartNumber and SkuId
$AllSKUs = @{
    "c5928f49-12ba-48f7-ada3-0d743a3601d5"="VISIOCLIENT"
    "1f2f344a-700d-42c9-9427-5cea1d5d7ba6"="STREAM"
    "f8a1db68-be16-40ed-86d5-cb42ce701560"="POWER_BI_PRO"
    "6fd2c87f-b296-42f0-b197-1e91e994b900"="ENTERPRISEPACK"
    "f30db892-07e9-47e9-837c-80727f46fd3d"="FLOW_FREE"
    "726a0894-2c77-4d65-99da-9775ef05aad1"="MICROSOFT_BUSINESS_CENTER"
    "dcb1a3ae-b33f-4487-846a-a640262fadf4"="POWERAPPS_VIRAL"
    "74fbf1bb-47c6-4796-9623-77dc7371723b"="MS_TEAMS_IW"
    "e06abcc2-7ec5-4a79-b08b-d9c282376f72"="CRMTESTINSTANCE"
    "6070a4c8-34c6-4937-8dfb-39bbc6397a60"="MEETING_ROOM"
    "a403ebcc-fae0-4ca2-8c8c-7a907fd6c235"="POWER_BI_STANDARD"
    "efccb6f7-5641-4e0e-bd10-b4976e1bf68e"="EMS"
    "078d2b04-f1bd-4111-bbd4-b4b1b354cef4"="AAD_PREMIUM"
    "9d776713-14cb-4697-a21d-9a52455c738a"="CRMINSTANCE"
    "53818b1b-4a27-454b-8896-0dba576410e6"="PROJECTPROFESSIONAL"
    "29a2f828-8f39-4837-b8ff-c957e86abe3c"="TEAMS_COMMERCIAL_TRIAL"
    "1e1a282c-9c54-43a2-9310-98ef728faace"="DYN365_ENTERPRISE_SALES"
    "8e7a3d30-d97d-43ab-837c-d7701cef83dc"="DYN365_ENTERPRISE_TEAM_MEMBERS"
    }

$users = @()


# Import the CSV file

try {
    $users = import-csv $CSVfile
    }
    catch {
        $errorZero = $Error[0]
        write-host "Error: " $errorZero -ForegroundColor Red  # Writes the latest error
        Break
    }

# Could add a check here to check for AD group membership

foreach ($user in $users){
    write-host $user.UserPrincipalName
    }

write-warning "These users will have all SKU licenses removed, make sure they have been added to AD groups"
write-host "Make sure you have waited 30 minutes after adding to groups!" -ForegroundColor Red
write-host "Note: This will not affect licenses which have been assigned by group"
Read-Host -Prompt "Press Enter to continue or CTRL+C to quit"

foreach ($user in $users){$user.UserPrincipalName}
$userSKUs = @()
$userSKUIDs = @()


foreach ($user in $users){
$aaduser = get-azureaduser -ObjectId $user.UserPrincipalName
$SKUs = $aaduser | Select UserPrincipalName,ImmutableID -ExpandProperty AssignedLicenses
# Get all the SKUs
foreach ($SKU in $SKUs) {
    if($SKUname = $AllSKUs[$SKU.SkuId]) { 
        # Add the users SKUs to an array
        write-host "Removing" $SKUname $SKU.SkuId "from" $user.UserPrincipalName  

        $LicensesToAssign = New-Object -TypeName Microsoft.Open.AzureAD.Model.AssignedLicenses
        $LicensesToAssign.AddLicenses = @() # We aren't adding any
        $LicensesToAssign.RemoveLicenses = $SKU.SkuId # Get the ID from the hash table.
        try {
            $aaduser | Set-AzureADUserLicense  -AssignedLicenses $LicensesToAssign
            }
            catch {
                $errorZero = $Error[0]
                write-host "Error: " $errorZero -ForegroundColor Yellow  # Writes the latest error
            }
    }
    else {
        write-host $SKU.SkuId "is not listed in the hash table" -ForegroundColor Red
        exit
    }

}

}

Posted in Azure AD, Office 365

Related Posts

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: