diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 new file mode 100644 index 0000000000..f7c529af72 --- /dev/null +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -0,0 +1,114 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All Rights Reserved. +# Licensed under the MIT License. See License in the project root for license information. +# ------------------------------------------------------------------------------ +function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { + [CmdletBinding(SupportsShouldProcess, + ConfirmImpact = 'High', + DefaultParameterSetName = 'AllInvitedGuests')] + param ( + [Parameter(ParameterSetName = 'ByUsers', HelpMessage = "The Unique ID of the User (User ID).")] + [String[]] $UserId, + + [Parameter(ParameterSetName = 'AllInvitedGuests', HelpMessage = "A Flag indicating whether to include all invited guests.")] + [switch] $All + ) + + process { + $guestFilter = "(CreationType eq 'Invitation')" + $expand = "sponsors" + $customHeaders = New-EntraBetaCustomHeaders -Command $MyInvocation.MyCommand + $environment = (Get-EntraContext).Environment + $baseUri = (Get-EntraEnvironment -Name $environment).GraphEndpoint + + if ((-not $UserId -or $UserId.Count -eq 0) -and -not $All) { + throw "Please specify either -UserId or -All" + } + + $invitedUsers = @() + $uri = "$baseUri/beta/users?`$filter=$guestFilter&`$expand=sponsors" + + if ($All) { + $invitedUsers = (Invoke-GraphRequest -Method GET -Uri $uri).value + } + else { + foreach ($user in $UserId) { + $userUri = $baseUri+"/beta/users/$user`?\$expand=sponsors" + $invitedUsers += (Invoke-GraphRequest -Method GET -Uri $userUri).value + } + } + + if (-not $invitedUsers) { + Write-Error "No guest users to process" + return + } + + foreach ($invitedUser in $invitedUsers) { + $invitedByUri = "$baseUri/beta/users/$($invitedUser.id)/invitedBy" + $invitedBy = Invoke-GraphRequest -Method GET -Uri $invitedByUri -Headers $customHeaders + + Write-Verbose ($invitedBy | ConvertTo-Json -Depth 10) + + if ($invitedBy -and $invitedBy.value -and $invitedBy.value.id) { + $inviterId = $invitedBy.value.id + Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $invitedUser.displayName, $inviterId) + + # Get current sponsors + $currentSponsorIds = @() + if ($invitedUser.sponsors) { + foreach ($s in $invitedUser.sponsors) { + if ($s.id) { + $currentSponsorIds += $s.id + } + } + } + + if (-not ($currentSponsorIds -contains $inviterId)) { + Write-Verbose "Sponsors does not contain the user who invited them!" + + if ($PSCmdlet.ShouldProcess("$($invitedUser.displayName) ($($invitedUser.userPrincipalName) - $($invitedUser.id))", "Update Sponsors")) { + try { + $sponsorUrl = "https://graph.microsoft.com/beta/users/$inviterId" + $dirObj = @{ + "sponsors@odata.bind" = @($sponsorUrl) + } + $sponsorsRequestBody = $dirObj | ConvertTo-Json -Depth 5 + + $updateSponsorUri = $baseUri+"/beta/users/$($invitedUser.Id)" + Invoke-GraphRequest -Method PATCH -Uri $updateSponsorUri -Body $sponsorsRequestBody -Headers $customHeaders -ContentType "application/json" + + Write-Output "$($invitedUser.userPrincipalName) - Sponsor updated successfully." + } + catch { + $errorMessage = $_.Exception.Message + $responseContent = $_.ErrorDetails + + if ($responseContent -match "One or more added object references already exist for the following modified properties: 'sponsors'" -or $responseContent -match "One or more added object references already exist for the following modified properties: 'sponsors'") { + Write-Warning "$($invitedUser.userPrincipalName) - Sponsor already set. Skipping." + } + elseif ($_.Exception.Response.StatusCode.Value__ -eq 400) { + Write-Warning "$($invitedUser.userPrincipalName) - Bad request: $responseContent" + } + else { + Write-Error "$($invitedUser.userPrincipalName) - Unexpected error: $errorMessage" + } + } + } + } + else { + Write-Output "$($invitedUser.userPrincipalName) - Sponsor already exists." + } + } + else { + Write-Output "$($invitedUser.userPrincipalName) - InvitedBy info not found." + } + } + } + + end { + Write-Verbose "Complete!" + } +} + + + diff --git a/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md new file mode 100644 index 0000000000..884a96f25b --- /dev/null +++ b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md @@ -0,0 +1,158 @@ +--- +title: Update-EntraBetaInvitedUserSponsorsFromInvitedBy +description: This article provides details on the Update-EntraBetaInvitedUserSponsorsFromInvitedBy command. + +ms.topic: reference +ms.date: 02/11/2025 +ms.author: eunicewaweru +ms.reviewer: stevemutungi +manager: CelesteDG +author: msewaweru + +external help file: Microsoft.Entra.Beta.Users-Help.xml +Module Name: Microsoft.Entra.Beta +online version: https://learn.microsoft.com/powershell/module/Microsoft.Beta.Entra/Update-EntraBetaInvitedUserSponsorsFromInvitedBy + +schema: 2.0.0 +--- + +# Update-EntraBetaInvitedUserSponsorsFromInvitedBy + +## Synopsis + +Update the Sponsors attribute to include the user who initially invited them to the tenant using the InvitedBy property. While new guests are sponsored automatically, the feature was only rolled out last year and did not backfill the sponsor info for previous guests that were invited. + +## Syntax + +```powershell +Update-EntraBetaInvitedUserSponsorsFromInvitedBy + [-UserId ] + [-All] + [] +``` + +## Description + +The `Update-EntraBetaInvitedUserSponsorsFromInvitedBy` cmdlet updates the Sponsors attribute to include the user who initially invited them to the tenant using the InvitedBy property. This script can be used to backfill Sponsors attribute for existing users. + +The calling user must be assigned at least one of the following Microsoft Entra roles: + +- User Administrator +- Privileged Authentication Administrator + +## Examples + +### Example 1: Enumerate all invited users in the Tenant and update Sponsors using InvitedBy value + +```powershell + Connect-Entra -Scopes 'User.ReadWrite.All' + Update-EntraBetaInvitedUserSponsorsFromInvitedBy +``` + +```Output +Confirm +Are you sure you want to perform this action? +Performing the operation "Update Sponsors" on target "externaluser_externaldomain.com" +(externaluser_externaldomain.com#EXT#@contoso.com - 00aa00aa-bb11-cc22-dd33-44ee44ee44ee)". +[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): A + +externaluser1_externaldomain.com#EXT#@contoso.com - Sponsor updated successfully for this user. +externaluser1_externaldomain#EXT#@contoso - Sponsor updated successfully for this user. +externaluser1_externaldomain#EXT#@contoso - Sponsor updated successfully for this user. +``` + +Enumerate all invited users in the Tenant and update Sponsors using InvitedBy value + +### Example 2: Update sponsors for a specific guest user + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All' +Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId 'externaluser1_externaldomain.com','externaluser1_externaldomain.com' +``` + +```Output +Confirm +Are you sure you want to perform this action? +Performing the operation "Update Sponsors" on target "externaluser_externaldomain.com" +(externaluser_externaldomain.com#EXT#@contoso.com - 00aa00aa-bb11-cc22-dd33-44ee44ee44ee)". +[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): A + +externaluser1_externaldomain.com#EXT#@contoso.com - Sponsor updated successfully for this user. +``` + +This command updates the sponsors for the specified guest user in Microsoft Entra ID. + +### Example 3: Update sponsors for all invited guest users + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All' +Update-EntraBetaInvitedUserSponsorsFromInvitedBy -All +``` + +```Output +Confirm +Are you sure you want to perform this action? +Performing the operation "Update Sponsors" on target "externaluser_externaldomain.com" +(externaluser_externaldomain.com#EXT#@contoso.com - 00aa00aa-bb11-cc22-dd33-44ee44ee44ee)". +[Y] Yes [A] Yes to All [N] No [L] No to All [S] Suspend [?] Help (default is "Y"): A + +externaluser1_externaldomain.com#EXT#@contoso.com - Sponsor updated successfully for this user. +externaluser1_externaldomain#EXT#@contoso - Sponsor updated successfully for this user. +externaluser1_externaldomain#EXT#@contoso - Sponsor updated successfully for this user. +``` + +This command updates the sponsors for all invited guest users in Microsoft Entra ID. + +## Parameters + +### -UserId + +Specifies the ID of one or more guest users (as UPNs or User IDs) in Microsoft Entra ID. + +```yaml +Type: System.String[] +Parameter Sets: ByUsers +Aliases: None + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -All + +Specifies that the cmdlet should update sponsors for all invited guest users. + +```yaml +Type: SwitchParameter +Parameter Sets: AllInvitedGuests +Aliases: None + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: `-Debug`, `-ErrorAction`, `-ErrorVariable`, `-InformationAction`, `-InformationVariable`, `-OutVariable`, `-OutBuffer`, `-PipelineVariable`, `-Verbose`, `-WarningAction`, and `-WarningVariable`. For more information, see [about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## Inputs + +## Outputs + +## Notes + +- If neither `-UserId` nor `-All` is specified, the cmdlet returns an error. +- The cmdlet retrieves invited users and their inviter information before updating the sponsors. +- The `-All` switch processes all guest users in the tenant. + +## Related Links + +[Get-EntraUser](Get-EntraBetaUser.md) + +[Set-EntraUser](Set-EntraBetaUser.md) diff --git a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 new file mode 100644 index 0000000000..f85e805da8 --- /dev/null +++ b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 @@ -0,0 +1,121 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All Rights Reserved. +# Licensed under the MIT License. See License in the project root for license information. +# ------------------------------------------------------------------------------ + +BeforeAll { + if ((Get-Module -Name Microsoft.Entra.Beta.Users) -eq $null) { + Import-Module Microsoft.Entra.Beta.Users + } + Import-Module (Join-Path $PSScriptRoot "..\..\Common-Functions.ps1") -Force + + Mock -CommandName Get-EntraContext -MockWith { return @{ Environment = "Public" } } -ModuleName Microsoft.Entra.Beta.Users + + Mock -CommandName Get-EntraEnvironment -MockWith { return @{ GraphEndpoint = "https://graph.microsoft.com" } } -ModuleName Microsoft.Entra.Beta.Users + + $guestFilter = "(CreationType eq 'Invitation')" + $expand = "sponsors" + + # Mock Invoke-GraphRequest for GET with the user filter and expand parameters + Mock -CommandName Invoke-GraphRequest -MockWith { + @{ + value = @( + @{ + id = "user1"; + userPrincipalName = "user1@example.com"; + displayName = "User One"; + sponsors = @() + }, + @{ + id = "user2"; + userPrincipalName = "user2@example.com"; + displayName = "User Two"; + sponsors = @(@{ id = "sponsor1" }) + } + ) + Headers = @{ + 'User-Agent' = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" + } + } + } -ModuleName Microsoft.Entra.Beta.Users -ParameterFilter { $Method -eq 'GET' -and $Uri -match "/users?`$filter=$guestFilter&`$expand=sponsors" } + + + # Mock Invoke-GraphRequest for GET with the invitedBy endpoint + Mock -CommandName Invoke-GraphRequest -MockWith { + @{ + value = @{ id = "inviter1" } + } + } -ModuleName Microsoft.Entra.Beta.Users -ParameterFilter { $Method -eq 'GET' -and $Uri -match '/users/.+/invitedBy' } + + # Mock Invoke-GraphRequest for PATCH to update sponsors + Mock -CommandName Invoke-GraphRequest -MockWith { + Write-Output "Sponsor updated successfully" + } -ModuleName Microsoft.Entra.Beta.Users -ParameterFilter { $Method -eq 'PATCH' -and $Uri -match '/users/.+' } + + # Mock Invoke-GraphRequest for GET with all users + Mock -CommandName Invoke-GraphRequest -MockWith { + return @{value = @( + @{ + id = "user1"; + userPrincipalName = "user1@example.com"; + displayName = "User One"; + sponsors = @() + }, + @{ + id = "user2"; + userPrincipalName = "user2@example.com"; + displayName = "User Two"; + sponsors = @(@{ id = "sponsor1" }) + } + ) + Headers = @{ + 'User-Agent' = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" + } + } + } -ModuleName Microsoft.Entra.Beta.Users -ParameterFilter {$Method -eq 'GET' -and $Uri -match '/users/'} + + Mock -CommandName New-EntraBetaCustomHeaders -MockWith { + return @{ + 'User-Agent' = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" + } + } -ModuleName Microsoft.Entra.Beta.Users +} + +Describe "Update-EntraBetaInvitedUserSponsorsFromInvitedBy" { + Context "Valid Inputs" { + It "Should update sponsor for a single user" { + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor updated successfully" + } + + It "Should process all invited users when -All is specified" { + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -All -Confirm:$false | Should -Match "Sponsor updated successfully" + } + } + + Context "Invalid Inputs" { + It "Should throw an error when neither -UserId nor -All is provided" { + { Update-EntraBetaInvitedUserSponsorsFromInvitedBy -Confirm:$false} | Should -Throw "Please specify either -UserId or -All" + } + } + + Context "Edge Cases" { + It "Should handle missing invitedBy information" { + Mock -CommandName Invoke-GraphRequest -MockWith { @{ value =@() } } -ModuleName Microsoft.Entra.Beta.Users + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor updated successfully" + } + } + + Context "User-Agent Header" { + It "Should contain 'User-Agent' header" { + $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" + + # Call the function + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false + + # Verify that Invoke-GraphRequest was called with the correct User-Agent header + Should -Invoke -CommandName Invoke-GraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 -ParameterFilter { + $Headers.'User-Agent' -eq $userAgentHeaderValue + } + } + } +}