Public/NC.Users.ps1
|
#Requires -Version 5.0 using namespace System.Management.Automation # Nebula.Core: User helpers ========================================================================================================================= function Search-EntraUser { <# .SYNOPSIS Finds Entra users by display name, user principal name, or mail. .DESCRIPTION Uses Microsoft Graph search to find users by display name, user principal name, or mail. This is especially useful for guest users whose Entra UPN contains fragments of the invited domain or tenant-specific suffixes. .PARAMETER SearchText Text to search for in display name, user principal name, and/or mail. Accepts pipeline input. .PARAMETER SearchIn Where to search: DisplayName, UserPrincipalName, Mail, or Any. .PARAMETER IndexOnly Use Microsoft Graph indexed search only. This is faster, but it can miss guest identities and other partial matches that only show up in a broader scan. .PARAMETER Detailed Show extra diagnostic columns such as match source, guest fragment, and user metadata. .PARAMETER GridView Show the result set in Out-GridView instead of returning objects. .EXAMPLE Search-EntraUser -SearchText "step" .EXAMPLE Search-EntraUser -SearchText "federica" -SearchIn DisplayName .EXAMPLE "guest" | Search-EntraUser -SearchIn Any -GridView #> [CmdletBinding()] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [Alias('Search', 'Query', 'Text', 'Name', 'DisplayName', 'UserPrincipalName', 'Mail')] [string]$SearchText, [ValidateSet('DisplayName', 'UserPrincipalName', 'Mail', 'Any')] [string]$SearchIn = 'Any', [switch]$IndexOnly, [switch]$Detailed, [switch]$GridView ) process { $resolvedUsers = @(Resolve-EntraUserSearchResults -SearchText $SearchText -SearchIn $SearchIn -IndexOnly:$IndexOnly) Add-EmptyLine Write-Verbose "Users found: $($resolvedUsers.Count) for '$SearchText'." if (-not $resolvedUsers -or $resolvedUsers.Count -eq 0) { Write-NCMessage "No users found for '$SearchText'." -Level WARNING return } $results = [System.Collections.Generic.List[object]]::new() foreach ($candidate in $resolvedUsers) { $user = $candidate.User $row = [ordered]@{ 'Display Name' = $user.DisplayName 'User Principal Name' = $user.UserPrincipalName 'Mail' = $user.Mail 'User Type' = $user.UserType } if ($Detailed.IsPresent) { $row['Matched By'] = if ($candidate.MatchedBy.Count -gt 0) { $candidate.MatchedBy -join ', ' } else { $null } $row['Guest Fragment'] = $candidate.GuestFragment $row['User Id'] = $user.Id $row['Account Enabled'] = $user.AccountEnabled $row['Department'] = $user.Department $row['Job Title'] = $user.JobTitle $row['Search Mode'] = $candidate.SearchMode } $results.Add([pscustomobject]$row) | Out-Null } if ($GridView.IsPresent) { $results | Out-NCGridView -Title "Entra Users - Search: $SearchText" } else { $results | Sort-Object 'Display Name', 'User Principal Name' } } } function Remove-EntraUser { <# .SYNOPSIS Removes an Entra user from the tenant. .DESCRIPTION Removes the user identified by user principal name (UPN) with Microsoft Graph. .PARAMETER UserPrincipalName The user principal name of the Entra user to remove. .PARAMETER PassThru Return the removed user details after deletion. .EXAMPLE Remove-EntraUser -UserPrincipalName "antonio.sala_stepsrl.org#EXT#@messita.onmicrosoft.com" .EXAMPLE "federica.arpaia_stepsrl.it#EXT#@messita.onmicrosoft.com" | Remove-EntraUser -WhatIf #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)] [Alias('UPN', 'Identity')] [string]$UserPrincipalName, [switch]$PassThru ) begin { $graphConnected = $null $queue = [System.Collections.Generic.List[string]]::new() $state = @{ StartLineWritten = $false } $flush = { if ($queue.Count -eq 0) { return } $items = @($queue) $queue.Clear() if (-not $state.StartLineWritten) { Write-NCGraphBatchNotice -Count ($items.Count) -Noun 'users' -Streaming $state.StartLineWritten = $true } for ($offset = 0; $offset -lt $items.Count; $offset += 20) { $chunk = @($items[$offset..([Math]::Min($offset + 20, $items.Count) - 1)]) # (a) Read the whole chunk. $lookups = @(for ($i = 0; $i -lt $chunk.Count; $i++) { @{ Id = "m$i" Method = 'GET' Url = "/users/$([uri]::EscapeDataString($chunk[$i]))?`$select=id,displayName,userPrincipalName,mail,userType" } }) $lookupResponses = @(Invoke-NCGraphBatch -Requests $lookups -Activity 'Resolving users') # (b) Confirm per user. $approved = [System.Collections.Generic.List[object]]::new() for ($i = 0; $i -lt $chunk.Count; $i++) { $response = $lookupResponses[$i] if (-not $response.Success) { Write-NCMessage "Unable to resolve user '$($chunk[$i])': $($response.ErrorMessage)" -Level ERROR continue } $user = [pscustomobject]$response.Body if ($PSCmdlet.ShouldProcess($user.userPrincipalName, "Remove Entra user $($user.displayName)")) { $approved.Add($user) | Out-Null } } # (c) Delete the approved users in one batch. if ($approved.Count -eq 0) { continue } $requests = @(for ($i = 0; $i -lt $approved.Count; $i++) { @{ Id = "m$i" Method = 'DELETE' Url = "/users/$([uri]::EscapeDataString([string]$approved[$i].id))" } }) $responses = @(Invoke-NCGraphBatch -Requests $requests -Activity 'Removing users') # (d) Report in input order. for ($i = 0; $i -lt $approved.Count; $i++) { $user = $approved[$i] $response = $responses[$i] if ($response.Success) { if ($PassThru.IsPresent) { [pscustomobject]@{ 'Display Name' = $user.displayName 'User Principal Name' = $user.userPrincipalName 'Mail' = $user.mail 'User Type' = $user.userType 'User Id' = $user.id } } Write-NCMessage "Removed Entra user '$($user.displayName)' ($($user.userPrincipalName))." -Level SUCCESS } else { Write-NCMessage "Unable to remove user '$($user.displayName)': $($response.ErrorMessage)" -Level ERROR } } } } } process { if ([string]::IsNullOrWhiteSpace($UserPrincipalName)) { Write-NCMessage "UserPrincipalName cannot be empty." -Level WARNING return } if ($null -eq $graphConnected) { $graphConnected = Test-MgGraphConnection -Scopes @('User.ReadWrite.All', 'Directory.Read.All') -EnsureExchangeOnline:$false if (-not $graphConnected) { Add-EmptyLine Write-NCMessage "Can't connect or use Microsoft Graph modules. Please check logs." -Level ERROR } } if (-not $graphConnected) { return } $queue.Add($UserPrincipalName.Trim()) | Out-Null if ($queue.Count -ge 20) { & $flush } } end { if ($graphConnected) { & $flush } } } |