Skip to content

Enganga/update entra invited user sponsors from invited by #1354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a141b1b
initial commit
emmanuel-karanja Feb 12, 2025
9c6b79b
initial commit
emmanuel-karanja Feb 12, 2025
6edf624
added beta cmdlet port
emmanuel-karanja Feb 12, 2025
d758558
Tests refactors, docs creation and cmdlet update to remove prerequisites
emmanuel-karanja Feb 13, 2025
d1d5563
Fixes
emmanuel-karanja Feb 13, 2025
3b242b9
Changed to use -Select
emmanuel-karanja Feb 13, 2025
1be86fb
Implement Get-ObjectPropertyValue function
emmanuel-karanja Feb 13, 2025
954cf72
Updated to -Select for Beta
emmanuel-karanja Feb 13, 2025
e49b3e3
Only better version is supported
emmanuel-karanja Feb 14, 2025
a6a6587
Updates
emmanuel-karanja Feb 20, 2025
b8db6c5
Inlined functioned
emmanuel-karanja Mar 18, 2025
54b8374
Beta updates
emmanuel-karanja Mar 18, 2025
61194f2
added examples to ocs
emmanuel-karanja Mar 20, 2025
d043f9e
added examples to ocs
emmanuel-karanja Mar 20, 2025
564adae
Removed Get-ObjectPropertyValue since we are only checking for the 'i…
emmanuel-karanja Mar 21, 2025
79e3157
Delete module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAcces…
emmanuel-karanja Mar 21, 2025
c624de3
Delete module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaComm…
emmanuel-karanja Mar 21, 2025
33627e4
Merge branch 'main' into enganga/Update-EntraInvitedUserSponsorsFromI…
emmanuel-karanja Mar 21, 2025
9c667ee
Merge branch 'main' into enganga/Update-EntraInvitedUserSponsorsFromI…
SteveMutungi254 Mar 25, 2025
2fff099
compute once
emmanuel-karanja Mar 26, 2025
7ae84d4
Migrate to use Graph API
emmanuel-karanja Mar 28, 2025
dd7c32f
update to use Graph API and update to tests
emmanuel-karanja Apr 1, 2025
d757cd7
cleanup tests
emmanuel-karanja Apr 1, 2025
2220b91
Merge branch 'main' into enganga/Update-EntraInvitedUserSponsorsFromI…
emmanuel-karanja Apr 1, 2025
330a446
Merge branch 'main' of https://github.com/microsoftgraph/entra-powers…
emmanuel-karanja Apr 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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!"
}
}



Original file line number Diff line number Diff line change
@@ -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 <String[]>]
[-All]
[<CommonParameters>]
```

## 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)
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}