From 37f5b19105569413f1b38a8966a8f1b953c5bc32 Mon Sep 17 00:00:00 2001 From: AdminDroid <49208841+admindroid-community@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:23:31 +0530 Subject: [PATCH] Direct License Cleanup for Group-based Licensed Users --- .../DirectLicenseCleanup.ps1 | 248 ++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 Identify and Remove Overlapped Direct Licenses from Group-based Licensed Users/DirectLicenseCleanup.ps1 diff --git a/Identify and Remove Overlapped Direct Licenses from Group-based Licensed Users/DirectLicenseCleanup.ps1 b/Identify and Remove Overlapped Direct Licenses from Group-based Licensed Users/DirectLicenseCleanup.ps1 new file mode 100644 index 0000000..660ad0f --- /dev/null +++ b/Identify and Remove Overlapped Direct Licenses from Group-based Licensed Users/DirectLicenseCleanup.ps1 @@ -0,0 +1,248 @@ +<# +============================================================================================= +Name: Remove Direct Licenses for Group-Licensed Users Using PowerShell +Version: 1.0 +website: o365reports.com + +Script Highlights: +~~~~~~~~~~~~~~~~~ + +1. This script generates reports on users with overlapping direct & group-based license assignments.  +2. The script removes direct assigned license(s) if the same license inherited via groups too.   +3. This script installs MS Graph PowerShell SDK (if not installed already) upon your confirmation.  +4. The script can be executed with an MFA-enabled account too.  +5. The script is schedular-friendly.  +6. It can be executed with certificate-based authentication (CBA) too.  + +For detailed Script execution: https://o365reports.com/2024/08/27/remove-direct-licenses-for-group-licensed-users-using-powershell/ +============================================================================================ +#> +Param +( + [switch]$GenerateReportOnly, + [switch]$CreateSession, + [switch]$Force, + [string]$TenantId, + [string]$ClientId, + [string]$CertificateThumbprint +) +Function Connect_MgGraph +{ + #Check for module installation + $Module=Get-Module -Name microsoft.graph.beta -ListAvailable + if($Module.count -eq 0) + { + Write-Host Microsoft Graph PowerShell SDK is not available -ForegroundColor yellow + $Confirm= Read-Host Are you sure you want to install module? [Y] Yes [N] No + if($Confirm -match "[yY]") + { + Write-host "Installing Microsoft Graph PowerShell module..." + Install-Module Microsoft.Graph.beta -Repository PSGallery -Scope CurrentUser -AllowClobber -Force + } + else + { + Write-Host "Microsoft Graph Beta PowerShell module is required to run this script. Please install module using Install-Module Microsoft.Graph cmdlet." + Exit + } + } + #Disconnect Existing MgGraph session + if($CreateSession.IsPresent) + { + Disconnect-MgGraph + } + + + Write-Host Connecting to Microsoft Graph... + if(($TenantId -ne "") -and ($ClientId -ne "") -and ($CertificateThumbprint -ne "")) + { + Connect-MgGraph -TenantId $TenantId -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome + } + else + { + Connect-MgGraph -Scopes "User.Read.All","AuditLog.read.All" -NoWelcome + } +} + +Function Convert-FrndlyName { + param( + [Parameter(Mandatory=$true)] + [Array]$InputIds + ) + $EasyName = $FriendlyNameHash[$SkuName] + if(!($EasyName)) + {$NamePrint = $SkuName} + else + {$NamePrint = $EasyName} + return $NamePrint +} +Connect_MgGraph +$Location=Get-Location +$ExportCSV = "$Location\M365Users_Overlapping_License_Report_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm-ss` tt).ToString()).csv" +$LogFile="$Location\LicenseRemoval_LogFile_$((Get-Date -format yyyy-MMM-dd-ddd` hh-mm-ss` tt).ToString()).csv" +$ExportResult="" +$ExportResults=@() +$PrintedUser=0 +$Count=0 + +#Get license in the organization and saving it as hash table +$SKUHashtable=@{} +Get-MgBetaSubscribedSku –All | foreach{ +$SKUHashtable[$_.skuid]=$_.Skupartnumber} + +#Get friendly name of license plan from external file +$FriendlyNameHash=Get-Content -Raw -Path "$Location\LicenseFriendlyName.txt" -ErrorAction Stop | ConvertFrom-StringData + +#Confiramtion for removing duplicate license assignment +$LicenseRemovalConfirmation=$false +if($Force.IsPresent) +{ + $LicenseRemovalConfirmation=$true +} +elseif(!($GenerateReportOnly.IsPresent)) +{ + Write-Host "Do you want to remove the direct license(s) that inherited via groups? [Y] Yes [N] No " -ForegroundColor Yellow + $Confirm= Read-Host + if($Confirm -match "[Y]") + { + $LicenseRemovalConfirmation=$true + } + else + { + Write-Host "Proceeding report generation without removing license(s) from users" -ForegroundColor Cyan + } +} + +#Process users +$RequiredProperties=@('UserPrincipalName','DisplayName','EmployeeId','CreatedDateTime','AccountEnabled','Department','JobTitle','LicenseAssignmentStates') +Get-MgBetaUser -Filter "assignedLicenses/`$count ne 0" -All -Property $RequiredProperties -ConsistencyLevel eventual -CountVariable Records | select $RequiredProperties | ForEach-Object { + $Count++ + $Print=1 + $DirectlyAssignedLicense=@() + $GroupBasedLicense=@() + $DirectlyAssignedLicense_FrndlyName=@() + $GroupBasedLicense_FrndlyName=@() + $DirectlyAssignedSKUs=@() + $InheritedSKUs=@() + $DuplicateAssignment=@() + $UPN=$_.UserPrincipalName + Write-Progress -Activity "`n Processed user: $Count - $UPN" + $DisplayName=$_.DisplayName + $AccountEnabled=$_.AccountEnabled + $Department=$_.Department + $JobTitle=$_.JobTitle + $LicenseAssignmentStates=$_.LicenseAssignmentStates + + + if($AccountEnabled -eq $true) + { + $AccountStatus='Enabled' + } + else + { + $AccountStatus='Disabled' + } +foreach($License in $LicenseAssignmentStates) +{ + $SKU=$License.SkuId + $SkuName=$SkuHashtable[$SKU] + $FriendlyName=Convert-FrndlyName -InputIds $SkuName + if($License.AssignedByGroup -eq $null) + { + $DirectlyAssignedLicense += $SkuHashtable[$SKU] + $DirectlyAssignedLicense_FrndlyName += $FriendlyName + $DirectlyAssignedSKUs +=$SKU + + } + elseif($License.AssignedByGroup -ne $null -and $License.State -eq "Active") + { + $GroupBasedLicense += $SkuHashtable[$SKU] + $GroupBasedLicense_FrndlyName +=$FriendlyName + $InheritedSKUs +=$SKU + } +} + #Check for duplicate license assignment + if($DirectlyAssignedLicense.Count -ne 0 -and $GroupBasedLicense.Count -ne 0) + { + $IsDuplicateLicenseFound = Compare-Object -ReferenceObject $DirectlyAssignedSKUs -DifferenceObject $InheritedSKUs -IncludeEqual -ExcludeDifferent + if($IsDuplicateLicenseFound.inputObject -ne $null) + { + $DuplicateLicenses=$IsDuplicateLicenseFound.inputObject + $DuplicateLicenseCount=$DuplicateLicenses.count + foreach ($DuplicateLicense in $DuplicateLicenses) + { + $DuplicateAssignment += $SkuHashtable[$DuplicateLicense] + } + + $Print=0 + } + } + + if($Print -ne 0) + { + return + } + + + $DirectlyAssignedLicense=$DirectlyAssignedLicense -join "," + $GroupBasedLicense=$GroupBasedLicense -join "," + $DirectlyAssignedLicense_FrndlyName=$DirectlyAssignedLicense_FrndlyName -join "," + $GroupBasedLicense_FrndlyName=$GroupBasedLicense_FrndlyName -join "," + $DuplicateAssignment=$DuplicateAssignment -join "," + + #Export output to CSV file + $PrintedUser++ + $ExportResult=[PSCustomObject]@{'Display Name'=$DisplayName;'UPN'=$UPN;'Directly Assigned Licenses'=$DirectlyAssignedLicense;'Group Based Licenses'=$GroupBasedLicense;'Duplicate License(s)'=$DuplicateAssignment;'Duplicate License Count'=$DuplicateLicenseCount;'Directly Assigned Licenses(Frndly_Name)'=$DirectlyAssignedLicense_FrndlyName;'Group based Licenses(Frndly_Name)'=$GroupBasedLicense_FrndlyName;'Account Status'=$AccountStatus;'Department'=$Department;'Job Title'=$JobTitle} + $ExportResult | Export-Csv -Path $ExportCSV -Notype -Append + + + + + #Remove Direct License Assignement if the same license assigned via group too + if(($LicenseRemovalConfirmation -eq $true)) + { + foreach($DupLicense in $DuplicateLicenses) + { + $FriendlyName=Convert-FrndlyName -InputIds $DupLicense + Write-Progress "Removing $FriendlyName license from the user $UPN" + Set-MgBetaUserLicense –UserId $UPN -RemoveLicenses @($DupLicense) -AddLicenses @() | Out-Null + If($?) + { + "Removing $FriendlyName license from the user $UPN successful" | Out-File $LogFile -Append + } + Else + { + "Removing $FriendlyName license from the user $UPN failed" | Out-File $LogFile -Append + } + } + } +} + + +#Open output file after execution +Write-Host `nScript execution completed +if((Test-Path -Path $ExportCSV) -eq "True") +{ + Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green + Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n`n + Write-Host "Exported report has $PrintedUser user(s)" + + Write-Host "Detailed report available in:" -NoNewline -ForegroundColor Yellow; Write-Host $ExportCSV + if($LicenseRemovalConfirmation -eq $true) + { + Write-Host "License removal log file available in:" -NoNewline -ForegroundColor Yellow; Write-Host $LogFile + } + $Prompt = New-Object -ComObject wscript.shell + $UserInput = $Prompt.popup("Do you want to open output file?",` 0,"Open Output File",4) + if ($UserInput -eq 6) + { + Invoke-Item "$ExportCSV" + } + +} +else +{ + Write-Host "No user found" -ForegroundColor Red + Write-Host `n~~ Script prepared by AdminDroid Community ~~`n -ForegroundColor Green + Write-Host "~~ Check out " -NoNewline -ForegroundColor Green; Write-Host "admindroid.com" -ForegroundColor Yellow -NoNewline; Write-Host " to get access to 1800+ Microsoft 365 reports. ~~" -ForegroundColor Green `n`n + +} \ No newline at end of file