Skip to content

Instantly share code, notes, and snippets.

@mrik23
Last active June 7, 2023 04:23
Show Gist options
  • Save mrik23/2ed37ce0c7c4a79605bdcf052e29b391 to your computer and use it in GitHub Desktop.
Save mrik23/2ed37ce0c7c4a79605bdcf052e29b391 to your computer and use it in GitHub Desktop.
Remove in bulk direct assigned license to users who have group assigned license with Azure AD PowerShell v1
<#
Modified version of the script from Microsoft Documentation.
Removed the part that checks if the users is assigned more products than the group assigned license.
Added connection part and help to find Sku and Group Object ID.
This script requires Azure AD (aks MSOL) PowerShell v1. It doesn't seem possible to do so with v2.
Ref: https://docs.microsoft.com/en-us/azure/active-directory/active-directory-licensing-ps-examples
#>
Import-Module MSOnline
$UserCredential = Get-Credential
Connect-MsolService -Credential $UserCredential
#Get License Sku for the tenant
Get-MsolAccountSku
#license to be removed
$skuId = "Contoso:STANDARDPACK"
#add here the group with license assignment to be processed
$LicensedGroup = "Licensed_Group"
#Get the group Object ID
$groupId = (Get-MsolGroup -SearchString $LicensedGroup).ObjectId
#Helper functions used by the script
#Returns TRUE if the user has the license assigned directly
function UserHasLicenseAssignedDirectly
{
Param([Microsoft.Online.Administration.User]$user, [string]$skuId)
$license = GetUserLicense $user $skuId
if ($license -ne $null)
{
#GroupsAssigningLicense contains a collection of IDs of objects assigning the license
#This could be a group object or a user object (contrary to what the name suggests)
#If the collection is empty, this means the license is assigned directly - this is the case for users who have never been licensed via groups in the past
if ($license.GroupsAssigningLicense.Count -eq 0)
{
return $true
}
#If the collection contains the ID of the user object, this means the license is assigned directly
#Note: the license may also be assigned through one or more groups in addition to being assigned directly
foreach ($assignmentSource in $license.GroupsAssigningLicense)
{
if ($assignmentSource -ieq $user.ObjectId)
{
return $true
}
}
return $false
}
return $false
}
#Returns TRUE if the user is inheriting the license from a specific group
function UserHasLicenseAssignedFromThisGroup
{
Param([Microsoft.Online.Administration.User]$user, [string]$skuId, [Guid]$groupId)
$license = GetUserLicense $user $skuId
if ($license -ne $null)
{
#GroupsAssigningLicense contains a collection of IDs of objects assigning the license
#This could be a group object or a user object (contrary to what the name suggests)
foreach ($assignmentSource in $license.GroupsAssigningLicense)
{
#If the collection contains at least one ID not matching the user ID this means that the license is inherited from a group.
#Note: the license may also be assigned directly in addition to being inherited
if ($assignmentSource -ieq $groupId)
{
return $true
}
}
return $false
}
return $false
}
#Returns the license object corresponding to the skuId. Returns NULL if not found
function GetUserLicense
{
Param([Microsoft.Online.Administration.User]$user, [string]$skuId, [Guid]$groupId)
#we look for the specific license SKU in all licenses assigned to the user
foreach($license in $user.Licenses)
{
if ($license.AccountSkuId -ieq $skuId)
{
return $license
}
}
return $null
}
#process staging removal for only 20 members in the group first
Get-MsolGroupMember -MaxResults 20 -GroupObjectId $groupId |
#get full info about each user in the group
Get-MsolUser -ObjectId {$_.ObjectId} |
Foreach {
$user = $_;
$operationResult = "";
#check if Direct license exists on the user
if (UserHasLicenseAssignedDirectly $user $skuId)
{
#check if the license is assigned from this group, as expected
if (UserHasLicenseAssignedFromThisGroup $user $skuId $groupId)
{
#remove the direct license from user
Set-MsolUserLicense -ObjectId $user.ObjectId -RemoveLicenses $skuId
$operationResult = "Removed direct license from user."
}
else
{
$operationResult = "User does not inherit this license from this group. License removal was skipped."
}
}
else
{
$operationResult = "User has no direct license to remove. Skipping."
}
#format output
New-Object Object |
Add-Member -NotePropertyName UserId -NotePropertyValue $user.ObjectId -PassThru |
Add-Member -NotePropertyName OperationResult -NotePropertyValue $operationResult -PassThru
} | Format-Table
<#You can then process all members in the group if the result of staging is OK
Get-MsolGroupMember -All -GroupObjectId $groupId |
#get full info about each user in the group
Get-MsolUser -ObjectId {$_.ObjectId} |
Foreach {
$user = $_;
$operationResult = "";
#check if Direct license exists on the user
if (UserHasLicenseAssignedDirectly $user $skuId)
{
#check if the license is assigned from this group, as expected
if (UserHasLicenseAssignedFromThisGroup $user $skuId $groupId)
{
#remove the direct license from user
Set-MsolUserLicense -ObjectId $user.ObjectId -RemoveLicenses $skuId
$operationResult = "Removed direct license from user."
}
else
{
$operationResult = "User does not inherit this license from this group. License removal was skipped."
}
}
else
{
$operationResult = "User has no direct license to remove. Skipping."
}
#format output
New-Object Object |
Add-Member -NotePropertyName UserId -NotePropertyValue $user.ObjectId -PassThru |
Add-Member -NotePropertyName OperationResult -NotePropertyValue $operationResult -PassThru
} | Format-Table
#>
@tsullyman
Copy link

Is it possible to add an export to the script so that it can in the first stage export to csv all users in the tenant and whether or not they are Direct Assigned or Inherited? So it can be reviewed first!

@flyRV
Copy link

flyRV commented Aug 8, 2020

Well done! I had zero issues with this script.

@mmenzie
Copy link

mmenzie commented Oct 28, 2020

i am trying to get this script to work but when i run it it stops with this error:

Get-MsolGroupMember : Cannot convert 'System.Object[]' to the type 'System.Guid' required by parameter 'GroupObjectId'. Specified method is not
supported.
At C:\Scripts\MSOL-BulkRemoveDirectAssignedLicense.ps1:99 char:51

  • Get-MsolGroupMember -MaxResults 20 -GroupObjectId $groupId |
  •                                               ~~~~~~~~
    
    • CategoryInfo : InvalidArgument: (:) [Get-MsolGroupMember], ParameterBindingException
    • FullyQualifiedErrorId : CannotConvertArgument,Microsoft.Online.Administration.Automation.GetGroupMember

can i get some help please?

@neil-sabol
Copy link

Hello mmenzie - this looks like the $LicensedGroup you are specifying may be returning multiple groups (because -SearchString does "starts with" matching). That would result in $groupId being an object vs. the Guid of a single group. What is the output if you run only these 2 commands (replacing "Licensed_Group" with your actual value)?

$LicensedGroup = "Licensed_Group"
(Get-MsolGroup -SearchString $LicensedGroup).ObjectId

@ArshSidhu2612
Copy link

Hello @neil-sabo,
I am getting the same error as mmenzie is i.e Get-MsolGroupMember : Cannot convert 'System.Object[]' to the type 'System.Guid' required by parameter 'GroupObjectId'. Specified method is not
supported.
At C:\Scripts\MSOL-BulkRemoveDirectAssignedLicense.ps1:99 char:51

Get-MsolGroupMember -MaxResults 20 -GroupObjectId $groupId |
~~~~~~~~
CategoryInfo : InvalidArgument: (:) [Get-MsolGroupMember], ParameterBindingException
FullyQualifiedErrorId : CannotConvertArgument,Microsoft.Online.Administration.Automation.GetGroupMember

If I run the cmdlets:-
$LicensedGroup = "Licensed_Group"
(Get-MsolGroup -SearchString $LicensedGroup).ObjectId

It displays me no result!

@neil-sabol
Copy link

Hello ArshSidhu2612 - sorry to hear. Just to confirm, when running the following cmdlets, you are replacing "Licensed_Group" with a group name (or prefix) for a group that exists in your tenant, right?

$LicensedGroup = "Licensed_Group"
(Get-MsolGroup -SearchString $LicensedGroup).ObjectId

IE

$LicensedGroup = "Neils-E1-License-Group"
(Get-MsolGroup -SearchString $LicensedGroup).ObjectId

Also, what version of the MSOL module are you running? My testing is done with 1.1.183.57.

Get-Module -ListAvailable -Name MSOnline

@ArshSidhu2612
Copy link

Thank you so much neil-sabol for responding back. And thank you for the example you just commented. I realized that the instead of license group, i mentioned the Object Id. Now since i mentioned the Licensed group, it worked perfectly fine for me!! Thanks once again Neil!!
Have a great rest of the day!

@Ntinsky
Copy link

Ntinsky commented Sep 17, 2021

Thank you for sharing such a useful script. Is it possible to skip the $LicensedGroup and just remove a specific type of license declared in $skuId?

I have like 700+ users with direct and inherited license assignments. It would be very convenient if i could just remove only a specific type of license like eg xxx:ENTERPRISEPACK or xxx:OFFICESUBSCRIPTION

Then i will be able to see which of these users receive any of the above licenses mentioned via group inheritance.

Thank you in advance for your time and keep up the good work!!!

@neil-sabol
Copy link

neil-sabol commented Sep 21, 2021

Hello Ntinsky - although I did not create this script initially (all credit to mrik23 for that), I believe you can achieve what you are asking with the following:

Import-Module MSOnline
$UserCredential = Get-Credential
Connect-MsolService -Credential $UserCredential

#Get License Sku for the tenant
Get-MsolAccountSku

#license to be removed
$skuId = "Contoso:STANDARDPACK"

#Helper functions used by the script

#Returns TRUE if the user has the license assigned directly
function UserHasLicenseAssignedDirectly
{
    Param([Microsoft.Online.Administration.User]$user, [string]$skuId)

    $license = GetUserLicense $user $skuId

    if ($license -ne $null)
    {
        #GroupsAssigningLicense contains a collection of IDs of objects assigning the license
        #This could be a group object or a user object (contrary to what the name suggests)
        #If the collection is empty, this means the license is assigned directly - this is the case for users who have never been licensed via groups in the past
        if ($license.GroupsAssigningLicense.Count -eq 0)
        {
            return $true
        }

        #If the collection contains the ID of the user object, this means the license is assigned directly
        #Note: the license may also be assigned through one or more groups in addition to being assigned directly
        foreach ($assignmentSource in $license.GroupsAssigningLicense)
        {
            if ($assignmentSource -ieq $user.ObjectId) 
            {
                return $true
            }
        }
        return $false
    }
    return $false
}

#Returns the license object corresponding to the skuId. Returns NULL if not found
function GetUserLicense
{
    Param([Microsoft.Online.Administration.User]$user, [string]$skuId)
    #we look for the specific license SKU in all licenses assigned to the user
    foreach($license in $user.Licenses)
    {
        if ($license.AccountSkuId -ieq $skuId)
        {
            return $license
        }
    }
    return $null
}

#process staging removal for only 20 members in the directory first and get full info about each user
Get-MsolUser -MaxResults 20 | 
  Foreach { 
      $user = $_;
      $operationResult = "";

      #check if Direct license exists on the user
      if (UserHasLicenseAssignedDirectly $user $skuId) {
          #remove the direct license from user
          #Set-MsolUserLicense -ObjectId $user.ObjectId -RemoveLicenses $skuId
          $operationResult = "Removed direct license from user."   
      } else {
          $operationResult = "User has no direct license to remove. Skipping."
      }
      #format output
      New-Object Object | 
                  Add-Member -NotePropertyName UserId -NotePropertyValue $user.ObjectId -PassThru |
                  Add-Member -NotePropertyName OperationResult -NotePropertyValue $operationResult -PassThru 
  } | Format-Table

This variant removes references to license groups and removes the specified, directly assigned SKU from ALL users in the tenant.

NOTE: the version above is "staging" (an abundance of caution, in case I misunderstood what you are asking):

  • If you want to remove direct license assignments, uncomment the Set-MsolUserLicense -ObjectId $user.ObjectId -RemoveLicenses $skuId line.
  • There is also a limit (20) on the number of users returned by Get-MsolUser. To process all users in the tenant, change Get-MsolUser -MaxResults 20 | to Get-MsolUser -All |.

I hope that helps!

@Ntinsky
Copy link

Ntinsky commented Sep 21, 2021 via email

@Burncycle3008
Copy link

So when I run this I get (small preview - theres actually much much more as I modified the max results to 1000 to account for our enterprise):

7da7189d-cf38-41bc-aa53-1af46b3a62c5 User has no direct license to remove. Skipping.
ee774b6d-0955-4e27-818c-ba08b7f80ea9 User has no direct license to remove. Skipping.
0904b7b3-cb11-47db-adf5-8a17efe403a8 User has no direct license to remove. Skipping.
d35e541d-6a76-4497-a1a0-dc104c2432f4 User has no direct license to remove. Skipping.
934572cf-654a-4875-9bd1-844e11f422bd User has no direct license to remove. Skipping.
7b089a61-970b-486e-89d4-3cd74ad816c6 User has no direct license to remove. Skipping.
f8085c7d-04a0-49a6-a9bb-133a3466ab47 User has no direct license to remove. Skipping.
c43e43dd-99ae-4a97-aa8b-20b53f139024 User has no direct license to remove. Skipping.
0144e6e9-7954-42cb-83c8-29b10619ee83 User has no direct license to remove. Skipping.
04b765bb-a1a2-454b-82f6-166daae3526f User has no direct license to remove. Skipping.

That last userID I verified is a user with direct and group license. There's over 150 users with dual licensing and am really hoping to avoid hours of point and click work. Any help would be greatly appreciated! Thanks!

Additionally using the connect-msolservice command in the script doesnt work for me, but if I connect manually beforehand and comment out the connection it 'works' just fine.

Lastly, I modified the script to use the group-id directly instead of getting it from a AD group name, but that made no difference. See immediately below for my change.
"
#Group ID is set for FULL_OFFICE_E3_License, FULL_MS_E3_License is aae61cd4-7c1f-4452-a426-e9d01bd259fe
#Get the groupID (original version) - $groupId = (Get-MsolGroup -SearchString $LicensedGroup).ObjectId
$groupId = 7c727707-a9bd-4b15-82f9-4fd97fb3f8cd
"

@Kumarn26
Copy link

I think MS is going to depreciate it soon. It only processes 27 accounts now and then throws error:

Set-MsolUserLicense : You have exceeded the maximum number of allowable transactions. Please try again later.
At line:145 char:17

@neil-sabol
Copy link

Hello Kumarn26 - good catch. This script should be ported to MS Graph for longevity. See Assign Microsoft 365 licenses to user accounts with PowerShell. The recommended replacement for Set-MsolUserLicense is Set-MgUserLicense.

But in the interim, I suspect the "...You have exceeded the maximum number of allowable transactions..." message is a throttling issue.

You may be able to work around it by adding something like Start-Sleep -Seconds 2 after the Set-MsolUserLicense cmdlet. A one second pause may be sufficient, but both are admittedly untested. Regardless, script execution will be slowed down.

...
                    #remove the direct license from user
                    Set-MsolUserLicense -ObjectId $user.ObjectId -RemoveLicenses $skuId
                    $operationResult = "Removed direct license from user."
                    Start-Sleep -Seconds 2
...

Hope it helps.

@Kumarn26
Copy link

Kumarn26 commented May 31, 2023 via email

@neil-sabol
Copy link

Hello Nitin - yeah, understand the pain. Even a one second sleep adds A LOT of delay when processing thousands of users. If you are brave, feel free to give this a whirl:

https://gist.github.com/neil-sabol/250d43047c6c061067fca274588a4ce8

That is my first pass at porting this script to use the MgGraph (Microsoft Graph) PowerShell module instead of MSOnline. My very cursory testing suggests it works, but I cannot say 100%. Additional testing is needed. If you are able to do that, it would be appreciated.

Pending the outcome, @mrik23, would you mind incorporating the updates into your Gist since you clearly have the SEO advantage here? ;)

Thank you all,
-N

@Menz01
Copy link

Menz01 commented Jun 6, 2023

if someone test the above out and it works please report back

@Kumarn26
Copy link

Kumarn26 commented Jun 7, 2023 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment