Azure AD direct to group license assignment migration scripts

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
# 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 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

Your email address will not be published. Required fields are marked *

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

%d bloggers like this: