From a141b1ba01e090b6ec8c134ee58497643e580be0 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:13:38 +0300 Subject: [PATCH 01/21] initial commit --- .../Get-EntraCrossTenantAccessActivity.ps1 | 500 ++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAccessActivity.ps1 diff --git a/module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAccessActivity.ps1 b/module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAccessActivity.ps1 new file mode 100644 index 0000000000..01f726d073 --- /dev/null +++ b/module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAccessActivity.ps1 @@ -0,0 +1,500 @@ +function Get-EntraCrossTenantAccessActivity { + [CmdletBinding()] + param( + + #Return events based on external tenant access direction, either 'Inbound', 'Outbound', or 'Both' + [Parameter(Position = 0)] + [ValidateSet('Inbound', 'Outbound')] + [string]$AccessDirection, + + #Return events for the supplied external tenant ID + [Parameter(Position = 1)] + [guid]$ExternalTenantId, + + #Show summary statistics by tenant + [switch]$SummaryStats, + + #Atemmpt to resolve the external tenant ID + [switch]$ResolveTenantId + + ) + + begin { + ## Initialize Critical Dependencies + function Test-MgCommandPrerequisites { + [CmdletBinding()] + [OutputType([bool])] + param ( + # The name of a command. + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] + [Alias('Command')] + [string[]] $Name, + # The service API version. + [Parameter(Mandatory = $false, Position = 2)] + [ValidateSet('v1.0')] + [string] $ApiVersion = 'v1.0', + # Specifies a minimum version. + [Parameter(Mandatory = $false)] + [version] $MinimumVersion, + # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [switch] $RequireListPermissions + ) + + begin { + [version] $MgAuthenticationModuleVersion = $null + $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" + if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { + $MgAuthenticationModuleVersion = $Matches[2] + } + else { + $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version + } + Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" + } + + process { + ## Initialize + $result = $true + + ## Get Graph Command Details + [hashtable] $MgCommandLookup = @{} + foreach ($CommandName in $Name) { + + [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion -ErrorAction Break + + if ($MgCommands.Count -eq 1) { + $MgCommand = $MgCommands[0] + } + elseif ($MgCommands.Count -gt 1) { + $MgCommand = $MgCommands[0] + ## Resolve from multiple results + [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null + [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" + [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" + if ($MgCommandsWithListPermissions -and $RequireListPermissions) { + $MgCommand = $MgCommandsWithListPermissions[0] + } + elseif ($MgCommandsWithGetPermissions) { + $MgCommand = $MgCommandsWithGetPermissions[0] + } + else { + $MgCommand = $MgCommands[0] + } + } + + if ($MgCommand) { + $MgCommandLookup[$MgCommand.Command] = $MgCommand + } + else { + Write-Error "Unable to resolve a specific command for '$CommandName'." + } + } + + ## Import Required Modules + [string[]] $MgModules = @() + foreach ($MgCommand in $MgCommandLookup.Values) { + if (!$MgModules.Contains($MgCommand.Module)) { + $MgModules += $MgCommand.Module + [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" + try { + if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { + ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + } + else { + ## Load module to match currently loaded Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + } + } + catch { + $result = $false + Write-Error -ErrorRecord $_ + } + } + } + Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) + + ## Check MgModule Connection + $MgContext = Get-MgContext + if ($MgContext) { + if ($MgContext.AuthType -eq 'Delegated') { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' + $result = $false + } + } + } + else { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" + } + } + } + } + else { + $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' + $result = $false + } + + return $result + } + } + $CriticalError = $null + if (!(Test-MgCommandPrerequisites 'Get-MgAuditLogSignIn' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } + + #External Tenant ID check + + if ($ExternalTenantId) { + + Write-Verbose -Message "$(Get-Date -f T) - Checking supplied external tenant ID - $ExternalTenantId..." + + if ($ExternalTenantId -eq (Get-MgContext).TenantId) { + + Write-Error "$(Get-Date -f T) - Supplied external tenant ID ($ExternalTenantId) cannot match connected tenant ID ($((Get-MgContext).TenantId)))" -ErrorAction Stop + + } + else { + + Write-Verbose -Message "$(Get-Date -f T) - Supplied external tenant ID OK" + } + + } + + } + + process { + ## Return Immediately On Critical Error + if ($CriticalError) { return } + + #Get filtered sign-in logs and handle parameters + + if ($AccessDirection -eq "Outbound") { + + if ($ExternalTenantId) { + + Write-Verbose -Message "$(Get-Date -f T) - Access direction 'Outbound' selected" + Write-Verbose -Message "$(Get-Date -f T) - Outbound: getting sign-ins for local users accessing external tenant ID - $ExternalTenantId" + + $SignIns = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and ResourceTenantId eq '{0}'" -f $ExternalTenantId) -All | Group-Object ResourceTenantID + + } + else { + + Write-Verbose -Message "$(Get-Date -f T) - Access direction 'Outbound' selected" + Write-Verbose -Message "$(Get-Date -f T) - Outbound: getting external tenant IDs accessed by local users" + + $SignIns = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and ResourceTenantId ne '{0}'" -f (Get-MgContext).TenantId) -All | Group-Object ResourceTenantID + + } + + } + elseif ($AccessDirection -eq 'Inbound') { + + if ($ExternalTenantId) { + + Write-Verbose -Message "$(Get-Date -f T) - Access direction 'Inbound' selected" + Write-Verbose -Message "$(Get-Date -f T) - Inbound: getting sign-ins for users accessing local tenant from external tenant ID - $ExternalTenantId" + + $SignIns = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and HomeTenantId eq '{0}' and TokenIssuerType eq 'AzureAD'" -f $ExternalTenantId) -All | Group-Object HomeTenantID + + } + else { + + Write-Verbose -Message "$(Get-Date -f T) - Access direction 'Inbound' selected" + Write-Verbose -Message "$(Get-Date -f T) - Inbound: getting external tenant IDs for external users accessing local tenant" + + $SignIns = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and HomeTenantId ne '{0}' and TokenIssuerType eq 'AzureAD'" -f (Get-MgContext).TenantId) -All | Group-Object HomeTenantID + + } + } + else { + + if ($ExternalTenantId) { + + Write-Verbose -Message "$(Get-Date -f T) - Default access direction 'Both'" + Write-Verbose -Message "$(Get-Date -f T) - Outbound: getting sign-ins for local users accessing external tenant ID - $ExternalTenantId" + + $Outbound = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and ResourceTenantId eq '{0}'" -f $ExternalTenantId) -All | Group-Object ResourceTenantID + + + Write-Verbose -Message "$(Get-Date -f T) - Inbound: getting sign-ins for users accessing local tenant from external tenant ID - $ExternalTenantId" + + $Inbound = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and HomeTenantId eq '{0}' and TokenIssuerType eq 'AzureAD'" -f $ExternalTenantId) -All | Group-Object HomeTenantID + } + else { + + Write-Verbose -Message "$(Get-Date -f T) - Default access direction 'Both'" + Write-Verbose -Message "$(Get-Date -f T) - Outbound: getting external tenant IDs accessed by local users" + + $Outbound = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and ResourceTenantId ne '{0}'" -f (Get-MgContext).TenantId) -All | Group-Object ResourceTenantID + + + Write-Verbose -Message "$(Get-Date -f T) - Inbound: getting external tenant IDs for external users accessing local tenant" + + $Inbound = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and HomeTenantId ne '{0}' and TokenIssuerType eq 'AzureAD'" -f (Get-MgContext).TenantId) -All | Group-Object HomeTenantID + } + + #Combine outbound and inbound results + + [array]$SignIns = $Outbound + $SignIns += $Inbound + } + #Analyse sign-in logs + + Write-Verbose -Message "$(Get-Date -f T) - Checking for sign-ins..." + + if ($SignIns) { + + Write-Verbose -Message "$(Get-Date -f T) - Sign-ins obtained" + Write-Verbose -Message "$(Get-Date -f T) - Iterating Sign-ins..." + + foreach ($TenantID in $SignIns) { + + #Handle resolving tenant ID + + if ($ResolveTenantId) { + + Write-Verbose -Message "$(Get-Date -f T) - Attempting to resolve external tenant - $($TenantId.Name)" + + #Nullify $ResolvedTenant value + + $ResolvedTenant = $null + + #Attempt to resolve tenant ID + + try { $ResolvedTenant = Resolve-MsIdTenant -TenantId $TenantId.Name -ErrorAction Stop } + catch { Write-Warning $_.Exception.Message; Write-Verbose -Message "$(Get-Date -f T) - Issue resolving external tenant - $($TenantId.Name)" } + + if ($ResolvedTenant) { + + if ($ResolvedTenant.Result -eq 'Resolved') { + + $ExternalTenantName = $ResolvedTenant.DisplayName + $DefaultDomainName = $ResolvedTenant.DefaultDomainName + } + else { + $ExternalTenantName = $ResolvedTenant.Result + $DefaultDomainName = $ResolvedTenant.Result + } + + if ($ResolvedTenant.oidcMetadataResult -eq 'Resolved') { + + $oidcMetadataTenantRegionScope = $ResolvedTenant.oidcMetadataTenantRegionScope + } + else { + + $oidcMetadataTenantRegionScope = 'NotFound' + + } + + } + else { + + $ExternalTenantName = "NotFound" + $DefaultDomainName = "NotFound" + $oidcMetadataTenantRegionScope = 'NotFound' + } + + } + #Handle access direction + + if (($AccessDirection -eq 'Inbound') -or ($AccessDirection -eq 'Outbound')) { + + $Direction = $AccessDirection + + } + else { + + if ($TenantID.Name -eq $TenantID.Group[0].HomeTenantId) { + + $Direction = "Inbound" + + } + elseif ($TenantID.Name -eq $TenantID.Group[0].ResourceTenantId) { + + $Direction = "Outbound" + + } + + } + + #Provide summary + + if ($SummaryStats) { + + Write-Verbose -Message "$(Get-Date -f T) - Creating summary stats for external tenant - $($TenantId.Name)" + + #Handle resolving tenant ID + + if ($ResolveTenantId) { + + $Analysis = [pscustomobject]@{ + + ExternalTenantId = $TenantId.Name + ExternalTenantName = $ExternalTenantName + ExternalTenantRegionScope = $oidcMetadataTenantRegionScope + AccessDirection = $Direction + SignIns = ($TenantId).count + SuccessSignIns = ($TenantID.Group.Status | Where-Object { $_.ErrorCode -eq 0 } | Measure-Object).count + FailedSignIns = ($TenantID.Group.Status | Where-Object { $_.ErrorCode -ne 0 } | Measure-Object).count + UniqueUsers = ($TenantID.Group | Select-Object UserId -Unique | Measure-Object).count + UniqueResources = ($TenantID.Group | Select-Object ResourceId -Unique | Measure-Object).count + + } + + } + else { + + #Build custom output object + + $Analysis = [pscustomobject]@{ + + ExternalTenantId = $TenantId.Name + AccessDirection = $Direction + SignIns = ($TenantId).count + SuccessSignIns = ($TenantID.Group.Status | Where-Object { $_.ErrorCode -eq 0 } | Measure-Object).count + FailedSignIns = ($TenantID.Group.Status | Where-Object { $_.ErrorCode -ne 0 } | Measure-Object).count + UniqueUsers = ($TenantID.Group | Select-Object UserId -Unique | Measure-Object).count + UniqueResources = ($TenantID.Group | Select-Object ResourceId -Unique | Measure-Object).count + + } + + + } + + Write-Verbose -Message "$(Get-Date -f T) - Adding stats for $($TenantId.Name) to total analysis object" + + [array]$TotalAnalysis += $Analysis + + } + else { + + #Get individual events by external tenant + + Write-Verbose -Message "$(Get-Date -f T) - Getting individual sign-in events for external tenant - $($TenantId.Name)" + + + foreach ($Event in $TenantID.group) { + + + if ($ResolveTenantId) { + + $CustomEvent = [pscustomobject]@{ + + ExternalTenantId = $TenantId.Name + ExternalTenantName = $ExternalTenantName + ExternalDefaultDomain = $DefaultDomainName + ExternalTenantRegionScope = $oidcMetadataTenantRegionScope + AccessDirection = $Direction + UserDisplayName = $Event.UserDisplayName + UserPrincipalName = $Event.UserPrincipalName + UserId = $Event.UserId + UserType = $Event.UserType + CrossTenantAccessType = $Event.CrossTenantAccessType + AppDisplayName = $Event.AppDisplayName + AppId = $Event.AppId + ResourceDisplayName = $Event.ResourceDisplayName + ResourceId = $Event.ResourceId + SignInId = $Event.Id + CreatedDateTime = $Event.CreatedDateTime + StatusCode = $Event.Status.Errorcode + StatusReason = $Event.Status.FailureReason + + + } + + $CustomEvent + + } + else { + + $CustomEvent = [pscustomobject]@{ + + ExternalTenantId = $TenantId.Name + AccessDirection = $Direction + UserDisplayName = $Event.UserDisplayName + UserPrincipalName = $Event.UserPrincipalName + UserId = $Event.UserId + UserType = $Event.UserType + CrossTenantAccessType = $Event.CrossTenantAccessType + AppDisplayName = $Event.AppDisplayName + AppId = $Event.AppId + ResourceDisplayName = $Event.ResourceDisplayName + ResourceId = $Event.ResourceId + SignInId = $Event.Id + CreatedDateTime = $Event.CreatedDateTime + StatusCode = $Event.Status.Errorcode + StatusReason = $Event.Status.FailureReason + } + + $CustomEvent + + } + } + + } + + } + + } + else { + + Write-Warning "$(Get-Date -f T) - No sign-ins matching the selected criteria found." + + } + + #Display summary table + + if ($SummaryStats) { + + #Show array of summary objects for each external tenant + + Write-Verbose -Message "$(Get-Date -f T) - Displaying total analysis object" + + if (!$AccessDirection) { + + $TotalAnalysis | Sort-Object ExternalTenantId + + } + else { + + $TotalAnalysis | Sort-Object SignIns -Descending + + } + + } + + } + + end { + if ($CriticalError) { return } + } +} + From 9c6b79b6c99690c143830d31f0a1fa9320d790fb Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Wed, 12 Feb 2025 11:46:41 +0300 Subject: [PATCH 02/21] initial commit --- ...-EntraInvitedUserSponsorsFromInvitedBy.ps1 | 248 ++++++++++++++++++ ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 242 +++++++++++++++++ 2 files changed, 490 insertions(+) create mode 100644 module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 create mode 100644 module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 diff --git a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 new file mode 100644 index 0000000000..8048a2c9af --- /dev/null +++ b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 @@ -0,0 +1,248 @@ + +# ------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All Rights Reserved. +# Licensed under the MIT License. See License in the project root for license information. +# ------------------------------------------------------------------------------ + +function Update-EntraInvitedUserSponsorsFromInvitedBy { + [CmdletBinding(SupportsShouldProcess, + ConfirmImpact = 'High', + DefaultParameterSetName = 'AllInvitedGuests')] + param ( + + # UserId of Guest User + [Parameter(ParameterSetName = 'ByUsers')] + [String[]] + $UserId, + # Enumerate and Update All Guest Users. + [Parameter(ParameterSetName = 'AllInvitedGuests')] + [switch] + $All + ) + + begin { + function Test-MgCommandPrerequisites { + [CmdletBinding()] + [OutputType([bool])] + param ( + # The name of a command. + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] + [Alias('Command')] + [string[]] $Name, + # The service API version. + [Parameter(Mandatory = $false, Position = 2)] + [ValidateSet('v1.0')] + [string] $ApiVersion = 'v1.0', + # Specifies a minimum version. + [Parameter(Mandatory = $false)] + [version] $MinimumVersion, + # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [switch] $RequireListPermissions + ) + + begin { + [version] $MgAuthenticationModuleVersion = $null + $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" + if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { + $MgAuthenticationModuleVersion = $Matches[2] + } + else { + $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version + } + Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" + } + + process { + ## Initialize + $result = $true + + ## Get Graph Command Details + [hashtable] $MgCommandLookup = @{} + foreach ($CommandName in $Name) { + + [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion + + if ($MgCommands.Count -eq 1) { + $MgCommand = $MgCommands[0] + } + elseif ($MgCommands.Count -gt 1) { + $MgCommand = $MgCommands[0] + ## Resolve from multiple results + [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null + [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" + [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" + if ($MgCommandsWithListPermissions -and $RequireListPermissions) { + $MgCommand = $MgCommandsWithListPermissions[0] + } + elseif ($MgCommandsWithGetPermissions) { + $MgCommand = $MgCommandsWithGetPermissions[0] + } + else { + $MgCommand = $MgCommands[0] + } + } + + if ($MgCommand) { + $MgCommandLookup[$MgCommand.Command] = $MgCommand + } + else { + Write-Error "Unable to resolve a specific command for '$CommandName'." + } + } + + ## Import Required Modules + [string[]] $MgModules = @() + foreach ($MgCommand in $MgCommandLookup.Values) { + if (!$MgModules.Contains($MgCommand.Module)) { + $MgModules += $MgCommand.Module + [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" + try { + if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { + ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + } + else { + ## Load module to match currently loaded Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + } + } + catch { + $result = $false + Write-Error -ErrorRecord $_ + } + } + } + Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) + + ## Check MgModule Connection + $MgContext = Get-MgContext + if ($MgContext) { + if ($MgContext.AuthType -eq 'Delegated') { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' + $result = $false + } + } + } + else { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" + } + } + } + } + else { + $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' + $result = $false + } + + return $result + } + } + ## Initialize Critical Dependencies + $CriticalError = $null + if (!(Test-MgCommandPrerequisites 'Get-Mguser', 'Update-Mguser' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } + + $guestFilter = "(CreationType eq 'Invitation')" + } + + process { + + $customHeaders = New-EntraCustomHeaders -Command $MyInvocation.MyCommand + + if ($CriticalError) { return } + if ($null -eq $UserId -and !$All) { + Write-Error "Please specify either -UserId or -All" + return + } + + if ($All) { + $InvitedUsers = Get-MgUser -Filter $guestFilter -All -ExpandProperty Sponsors + } + else { + foreach ($user in $userId) { + $InvitedUsers += Get-MgUser -UserId $user -ExpandProperty Sponsors + } + } + + if ($null -eq $InvitedUsers) { + Write-Error "No guest users to process" + } + else { + foreach ($InvitedUser in $InvitedUsers) { + $invitedBy = $null + + $splatArgumentsGetInvitedBy = @{ + Method = 'Get' + Uri = ((Get-MgEnvironment -Name (Get-MgContext).Environment).GraphEndpoint + + "/v1.0/users/" + $InvitedUser.Id + "/invitedBy") + } + + $invitedBy = Invoke-MgGraphRequest @splatArgumentsGetInvitedBy -Headers $customHeaders + + Write-Verbose ($invitedBy | ConvertTo-Json -Depth 10) + + if ($null -ne $invitedBy -and $null -ne $invitedBy.value -and $null -ne (Get-ObjectPropertyValue $invitedBy.value -Property 'id')) { + Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $InvitedUser.DisplayName, $invitedBy.value.id) + + if (($null -like $InvitedUser.Sponsors) -or ($InvitedUser.Sponsors.id -notcontains $invitedBy.value.id)) { + 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/v1.0/users/{0}" -f $invitedBy.value.id) + $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } + $sponsorsRequestBody = $dirObj | ConvertTo-Json + + Update-MgUser -UserId $InvitedUser.Id -BodyParameter $sponsorsRequestBody -Header $customHeaders + Write-Output "$($InvitedUser.UserPrincipalName) - Sponsor updated succesfully for this user." + } + catch { + Write-Output "$($InvitedUser.UserPrincipalName) - Failed updating sponsor for this user." + Write-Error $_ + } + } + } + else { + Write-Output "$($InvitedUser.UserPrincipalName) - Sponsor already exists for this user." + } + } + else { + Write-Output "$($InvitedUser.UserPrincipalName) - Invited user information not available for this user." + } + } + } + } + + end { + Write-Verbose "Complete!" + } +} + 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..030d5f4c70 --- /dev/null +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -0,0 +1,242 @@ +function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { + [CmdletBinding(SupportsShouldProcess, + ConfirmImpact = 'High', + DefaultParameterSetName = 'AllInvitedGuests')] + param ( + + # UserId of Guest User + [Parameter(ParameterSetName = 'ByUsers')] + [String[]] + $UserId, + # Enumerate and Update All Guest Users. + [Parameter(ParameterSetName = 'AllInvitedGuests')] + [switch] + $All + ) + + begin { + function Test-MgCommandPrerequisites { + [CmdletBinding()] + [OutputType([bool])] + param ( + # The name of a command. + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] + [Alias('Command')] + [string[]] $Name, + # The service API version. + [Parameter(Mandatory = $false, Position = 2)] + [ValidateSet('v1.0')] + [string] $ApiVersion = 'v1.0', + # Specifies a minimum version. + [Parameter(Mandatory = $false)] + [version] $MinimumVersion, + # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [switch] $RequireListPermissions + ) + + begin { + [version] $MgAuthenticationModuleVersion = $null + $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" + if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { + $MgAuthenticationModuleVersion = $Matches[2] + } + else { + $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version + } + Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" + } + + process { + ## Initialize + $result = $true + + ## Get Graph Command Details + [hashtable] $MgCommandLookup = @{} + foreach ($CommandName in $Name) { + + [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion + + if ($MgCommands.Count -eq 1) { + $MgCommand = $MgCommands[0] + } + elseif ($MgCommands.Count -gt 1) { + $MgCommand = $MgCommands[0] + ## Resolve from multiple results + [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null + [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" + [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" + if ($MgCommandsWithListPermissions -and $RequireListPermissions) { + $MgCommand = $MgCommandsWithListPermissions[0] + } + elseif ($MgCommandsWithGetPermissions) { + $MgCommand = $MgCommandsWithGetPermissions[0] + } + else { + $MgCommand = $MgCommands[0] + } + } + + if ($MgCommand) { + $MgCommandLookup[$MgCommand.Command] = $MgCommand + } + else { + Write-Error "Unable to resolve a specific command for '$CommandName'." + } + } + + ## Import Required Modules + [string[]] $MgModules = @() + foreach ($MgCommand in $MgCommandLookup.Values) { + if (!$MgModules.Contains($MgCommand.Module)) { + $MgModules += $MgCommand.Module + [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" + try { + if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { + ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + } + else { + ## Load module to match currently loaded Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + } + } + catch { + $result = $false + Write-Error -ErrorRecord $_ + } + } + } + Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) + + ## Check MgModule Connection + $MgContext = Get-MgContext + if ($MgContext) { + if ($MgContext.AuthType -eq 'Delegated') { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' + $result = $false + } + } + } + else { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" + } + } + } + } + else { + $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' + $result = $false + } + + return $result + } + } + ## Initialize Critical Dependencies + $CriticalError = $null + if (!(Test-MgCommandPrerequisites 'Get-MgBetaUser', 'Update-MgBetaUser' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } + + $guestFilter = "(CreationType eq 'Invitation')" + } + + process { + + $customHeaders = New-EntraBetaCustomHeaders -Command $MyInvocation.MyCommand + + if ($CriticalError) { return } + if ($null -eq $UserId -and !$All) { + Write-Error "Please specify either -UserId or -All" + return + } + + if ($All) { + $InvitedUsers = Get-MgBetaUser -Filter $guestFilter -All -ExpandProperty Sponsors + } + else { + foreach ($user in $userId) { + $InvitedUsers += Get-MgUser -UserId $user -ExpandProperty Sponsors + } + } + + if ($null -eq $InvitedUsers) { + Write-Error "No guest users to process" + } + else { + foreach ($InvitedUser in $InvitedUsers) { + $invitedBy = $null + + $splatArgumentsGetInvitedBy = @{ + Method = 'Get' + Uri = ((Get-MgEnvironment -Name (Get-MgContext).Environment).GraphEndpoint + + "/beta/users/" + $InvitedUser.Id + "/invitedBy") + } + + $invitedBy = Invoke-MgGraphRequest @splatArgumentsGetInvitedBy -Headers $customHeaders + + Write-Verbose ($invitedBy | ConvertTo-Json -Depth 10) + + if ($null -ne $invitedBy -and $null -ne $invitedBy.value -and $null -ne (Get-ObjectPropertyValue $invitedBy.value -Property 'id')) { + Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $InvitedUser.DisplayName, $invitedBy.value.id) + + if (($null -like $InvitedUser.Sponsors) -or ($InvitedUser.Sponsors.id -notcontains $invitedBy.value.id)) { + 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/{0}" -f $invitedBy.value.id) + $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } + $sponsorsRequestBody = $dirObj | ConvertTo-Json + + Update-MgUser -UserId $InvitedUser.Id -BodyParameter $sponsorsRequestBody + Write-Output "$($InvitedUser.UserPrincipalName) - Sponsor updated succesfully for this user." + } + catch { + Write-Output "$($InvitedUser.UserPrincipalName) - Failed updating sponsor for this user." + Write-Error $_ + } + } + } + else { + Write-Output "$($InvitedUser.UserPrincipalName) - Sponsor already exists for this user." + } + } + else { + Write-Output "$($InvitedUser.UserPrincipalName) - Invited user information not available for this user." + } + } + } + } + + end { + Write-Verbose "Complete!" + } +} + From 6edf6245320a404670ffc3c2489a8d0bc50ee58a Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:12:41 +0300 Subject: [PATCH 03/21] added beta cmdlet port --- .../Test-EntraBetaCommandPrerequisites.ps1 | 145 +++++++++++++++++ ...-EntraInvitedUserSponsorsFromInvitedBy.ps1 | 148 +---------------- .../Test-EntraBetaCommandPrerequisite.ps1 | 150 ++++++++++++++++++ ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 148 +---------------- ...InvitedUserSponsorsFromInvitedBy.Tests.ps1 | 93 +++++++++++ ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 90 +++++++++++ 6 files changed, 482 insertions(+), 292 deletions(-) create mode 100644 module/Entra/Microsoft.Entra/Users/Test-EntraBetaCommandPrerequisites.ps1 create mode 100644 module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaCommandPrerequisite.ps1 create mode 100644 test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 create mode 100644 test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 diff --git a/module/Entra/Microsoft.Entra/Users/Test-EntraBetaCommandPrerequisites.ps1 b/module/Entra/Microsoft.Entra/Users/Test-EntraBetaCommandPrerequisites.ps1 new file mode 100644 index 0000000000..158185c10b --- /dev/null +++ b/module/Entra/Microsoft.Entra/Users/Test-EntraBetaCommandPrerequisites.ps1 @@ -0,0 +1,145 @@ + function Test-EntraCommandPrerequisites { + [CmdletBinding()] + [OutputType([bool])] + param ( + # The name of a command. + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] + [Alias('Command')] + [string[]] $Name, + # The service API version. + [Parameter(Mandatory = $false, Position = 2)] + [ValidateSet('v1.0')] + [string] $ApiVersion = 'v1.0', + # Specifies a minimum version. + [Parameter(Mandatory = $false)] + [version] $MinimumVersion, + # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [switch] $RequireListPermissions + ) + + begin { + [version] $MgAuthenticationModuleVersion = $null + $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" + if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { + $MgAuthenticationModuleVersion = $Matches[2] + } + else { + $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version + } + Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" + } + + process { + ## Initialize + $result = $true + + ## Get Graph Command Details + [hashtable] $MgCommandLookup = @{} + foreach ($CommandName in $Name) { + + [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion + + if ($MgCommands.Count -eq 1) { + $MgCommand = $MgCommands[0] + } + elseif ($MgCommands.Count -gt 1) { + $MgCommand = $MgCommands[0] + ## Resolve from multiple results + [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null + [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" + [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" + if ($MgCommandsWithListPermissions -and $RequireListPermissions) { + $MgCommand = $MgCommandsWithListPermissions[0] + } + elseif ($MgCommandsWithGetPermissions) { + $MgCommand = $MgCommandsWithGetPermissions[0] + } + else { + $MgCommand = $MgCommands[0] + } + } + + if ($MgCommand) { + $MgCommandLookup[$MgCommand.Command] = $MgCommand + } + else { + Write-Error "Unable to resolve a specific command for '$CommandName'." + } + } + + ## Import Required Modules + [string[]] $MgModules = @() + foreach ($MgCommand in $MgCommandLookup.Values) { + if (!$MgModules.Contains($MgCommand.Module)) { + $MgModules += $MgCommand.Module + [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" + try { + if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { + ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + } + else { + ## Load module to match currently loaded Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + } + } + catch { + $result = $false + Write-Error -ErrorRecord $_ + } + } + } + Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) + + ## Check MgModule Connection + $MgContext = Get-MgContext + if ($MgContext) { + if ($MgContext.AuthType -eq 'Delegated') { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' + $result = $false + } + } + } + else { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" + } + } + } + } + else { + $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' + $result = $false + } + + return $result + } +} \ No newline at end of file diff --git a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 index 8048a2c9af..056eb2d101 100644 --- a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 @@ -21,154 +21,10 @@ function Update-EntraInvitedUserSponsorsFromInvitedBy { ) begin { - function Test-MgCommandPrerequisites { - [CmdletBinding()] - [OutputType([bool])] - param ( - # The name of a command. - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] - [Alias('Command')] - [string[]] $Name, - # The service API version. - [Parameter(Mandatory = $false, Position = 2)] - [ValidateSet('v1.0')] - [string] $ApiVersion = 'v1.0', - # Specifies a minimum version. - [Parameter(Mandatory = $false)] - [version] $MinimumVersion, - # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. - [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] - [switch] $RequireListPermissions - ) - - begin { - [version] $MgAuthenticationModuleVersion = $null - $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" - if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { - $MgAuthenticationModuleVersion = $Matches[2] - } - else { - $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version - } - Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" - } - - process { - ## Initialize - $result = $true - - ## Get Graph Command Details - [hashtable] $MgCommandLookup = @{} - foreach ($CommandName in $Name) { - - [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion - - if ($MgCommands.Count -eq 1) { - $MgCommand = $MgCommands[0] - } - elseif ($MgCommands.Count -gt 1) { - $MgCommand = $MgCommands[0] - ## Resolve from multiple results - [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null - [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" - [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" - if ($MgCommandsWithListPermissions -and $RequireListPermissions) { - $MgCommand = $MgCommandsWithListPermissions[0] - } - elseif ($MgCommandsWithGetPermissions) { - $MgCommand = $MgCommandsWithGetPermissions[0] - } - else { - $MgCommand = $MgCommands[0] - } - } - - if ($MgCommand) { - $MgCommandLookup[$MgCommand.Command] = $MgCommand - } - else { - Write-Error "Unable to resolve a specific command for '$CommandName'." - } - } - - ## Import Required Modules - [string[]] $MgModules = @() - foreach ($MgCommand in $MgCommandLookup.Values) { - if (!$MgModules.Contains($MgCommand.Module)) { - $MgModules += $MgCommand.Module - [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" - try { - if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { - ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - } - else { - ## Load module to match currently loaded Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - } - } - catch { - $result = $false - Write-Error -ErrorRecord $_ - } - } - } - Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) - - ## Check MgModule Connection - $MgContext = Get-MgContext - if ($MgContext) { - if ($MgContext.AuthType -eq 'Delegated') { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' - $result = $false - } - } - } - else { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" - } - } - } - } - else { - $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' - $result = $false - } - - return $result - } - } + ## Initialize Critical Dependencies $CriticalError = $null - if (!(Test-MgCommandPrerequisites 'Get-Mguser', 'Update-Mguser' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } + if (!(Test-EntraCommandPrerequisites 'Get-Mguser', 'Update-Mguser' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } $guestFilter = "(CreationType eq 'Invitation')" } diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaCommandPrerequisite.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaCommandPrerequisite.ps1 new file mode 100644 index 0000000000..fbe520199c --- /dev/null +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaCommandPrerequisite.ps1 @@ -0,0 +1,150 @@ + # ------------------------------------------------------------------------------ +# Copyright (c) Microsoft Corporation. All Rights Reserved. +# Licensed under the MIT License. See License in the project root for license information. +# ------------------------------------------------------------------------------ + function Test-EntraBetaCommandPrerequisites { + [CmdletBinding()] + [OutputType([bool])] + param ( + # The name of a command. + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] + [Alias('Command')] + [string[]] $Name, + # The service API version. + [Parameter(Mandatory = $false, Position = 2)] + [ValidateSet('v1.0')] + [string] $ApiVersion = 'v1.0', + # Specifies a minimum version. + [Parameter(Mandatory = $false)] + [version] $MinimumVersion, + # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. + [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] + [switch] $RequireListPermissions + ) + + begin { + [version] $MgAuthenticationModuleVersion = $null + $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" + if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { + $MgAuthenticationModuleVersion = $Matches[2] + } + else { + $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version + } + Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" + } + + process { + ## Initialize + $result = $true + + ## Get Graph Command Details + [hashtable] $MgCommandLookup = @{} + foreach ($CommandName in $Name) { + + [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion + + if ($MgCommands.Count -eq 1) { + $MgCommand = $MgCommands[0] + } + elseif ($MgCommands.Count -gt 1) { + $MgCommand = $MgCommands[0] + ## Resolve from multiple results + [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null + [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" + [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" + if ($MgCommandsWithListPermissions -and $RequireListPermissions) { + $MgCommand = $MgCommandsWithListPermissions[0] + } + elseif ($MgCommandsWithGetPermissions) { + $MgCommand = $MgCommandsWithGetPermissions[0] + } + else { + $MgCommand = $MgCommands[0] + } + } + + if ($MgCommand) { + $MgCommandLookup[$MgCommand.Command] = $MgCommand + } + else { + Write-Error "Unable to resolve a specific command for '$CommandName'." + } + } + + ## Import Required Modules + [string[]] $MgModules = @() + foreach ($MgCommand in $MgCommandLookup.Values) { + if (!$MgModules.Contains($MgCommand.Module)) { + $MgModules += $MgCommand.Module + [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" + try { + if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { + ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) + } + } + else { + ## Load module to match currently loaded Microsoft.Graph.Authentication module + try { + Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false + } + catch [System.IO.FileLoadException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + catch [System.IO.FileNotFoundException] { + $result = $false + Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) + } + } + } + catch { + $result = $false + Write-Error -ErrorRecord $_ + } + } + } + Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) + + ## Check MgModule Connection + $MgContext = Get-MgContext + if ($MgContext) { + if ($MgContext.AuthType -eq 'Delegated') { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' + $result = $false + } + } + } + else { + ## Check MgModule Consented Scopes + foreach ($MgCommand in $MgCommandLookup.Values) { + if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { + Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" + } + } + } + } + else { + $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." + Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' + $result = $false + } + + return $result + } +} + diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index 030d5f4c70..3ef3f5fbe7 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -15,154 +15,10 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { ) begin { - function Test-MgCommandPrerequisites { - [CmdletBinding()] - [OutputType([bool])] - param ( - # The name of a command. - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] - [Alias('Command')] - [string[]] $Name, - # The service API version. - [Parameter(Mandatory = $false, Position = 2)] - [ValidateSet('v1.0')] - [string] $ApiVersion = 'v1.0', - # Specifies a minimum version. - [Parameter(Mandatory = $false)] - [version] $MinimumVersion, - # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. - [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] - [switch] $RequireListPermissions - ) - - begin { - [version] $MgAuthenticationModuleVersion = $null - $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" - if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { - $MgAuthenticationModuleVersion = $Matches[2] - } - else { - $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version - } - Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" - } - - process { - ## Initialize - $result = $true - - ## Get Graph Command Details - [hashtable] $MgCommandLookup = @{} - foreach ($CommandName in $Name) { - - [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion - - if ($MgCommands.Count -eq 1) { - $MgCommand = $MgCommands[0] - } - elseif ($MgCommands.Count -gt 1) { - $MgCommand = $MgCommands[0] - ## Resolve from multiple results - [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null - [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" - [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" - if ($MgCommandsWithListPermissions -and $RequireListPermissions) { - $MgCommand = $MgCommandsWithListPermissions[0] - } - elseif ($MgCommandsWithGetPermissions) { - $MgCommand = $MgCommandsWithGetPermissions[0] - } - else { - $MgCommand = $MgCommands[0] - } - } - - if ($MgCommand) { - $MgCommandLookup[$MgCommand.Command] = $MgCommand - } - else { - Write-Error "Unable to resolve a specific command for '$CommandName'." - } - } - - ## Import Required Modules - [string[]] $MgModules = @() - foreach ($MgCommand in $MgCommandLookup.Values) { - if (!$MgModules.Contains($MgCommand.Module)) { - $MgModules += $MgCommand.Module - [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" - try { - if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { - ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - } - else { - ## Load module to match currently loaded Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - } - } - catch { - $result = $false - Write-Error -ErrorRecord $_ - } - } - } - Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) - - ## Check MgModule Connection - $MgContext = Get-MgContext - if ($MgContext) { - if ($MgContext.AuthType -eq 'Delegated') { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' - $result = $false - } - } - } - else { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" - } - } - } - } - else { - $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' - $result = $false - } - - return $result - } - } + ## Initialize Critical Dependencies $CriticalError = $null - if (!(Test-MgCommandPrerequisites 'Get-MgBetaUser', 'Update-MgBetaUser' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } + if (!(Test-EntraBetaCommandPrerequisites 'Get-MgBetaUser', 'Update-MgBetaUser' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } $guestFilter = "(CreationType eq 'Invitation')" } diff --git a/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 b/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 new file mode 100644 index 0000000000..74ac054fdd --- /dev/null +++ b/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 @@ -0,0 +1,93 @@ +# ------------------------------------------------------------------------------ +# 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.Users) -eq $null) { + Import-Module Microsoft.Entra.Users + } + Import-Module (Join-Path $PSScriptRoot "..\..\Common-Functions.ps1") -Force + + Mock -CommandName Test-EntraCommandPrerequisites -MockWith { return $true } -ModuleName Microsoft.Entra.Users + Mock -CommandName Get-MgContext -MockWith { @{Scopes = @("User.Read.All", "Directory.ReadWrite.All")} } + Mock -CommandName Get-MgEnvironment -MockWith { @{GraphEndpoint = "https://graph.microsoft.com"; AzureADEndpoint = "https://login.microsoftonline.com"} } + Mock -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Users -MockWith { @{value = @{id = "1111-2222-3333"}} } + Mock -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -MockWith { @{Id = "4444-5555-6666"; DisplayName = "Test Guest"; UserPrincipalName = "testguest@contoso.com"; Sponsors = $null} } + Mock -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -MockWith { return } +} + +Describe "Update-EntraInvitedUserSponsorsFromInvitedBy" { + Context "Valid Inputs" { + + It "Should update sponsor for a specified guest user" { + $result = Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false + + $result | Should -Contain "testguest@contoso.com - Sponsor updated successfully for this user." + + Should -Invoke -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -Times 1 + Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Users -Times 1 + Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 1 + } + + It "Should process all guest users when -All is specified" { + $result = Update-EntraInvitedUserSponsorsFromInvitedBy -All -Confirm:$false + + $result | Should -Contain "testguest@contoso.com - Sponsor updated successfully for this user." + + Should -Invoke -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -Times 1 + Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Users -Times 1 + Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 1 + } + + It "Should not update sponsor if sponsor already exists" { + Mock -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -MockWith { @{Id = "4444-5555-6666"; Sponsors = @{id = "1111-2222-3333"}} } + + $result = Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false + + $result | Should -Contain "testguest@contoso.com - Sponsor already exists for this user." + + Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 0 + } + + It "Should contain 'User-Agent' header" { + $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraInvitedUserSponsorsFromInvitedBy" + + Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false + + $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraInvitedUserSponsorsFromInvitedBy" + + Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 -ParameterFilter { + $Headers.'User-Agent' | Should -Be $userAgentHeaderValue + $true + } + } + } + + Context "Invalid Inputs" { + + It "Should throw an error when neither -UserId nor -All is specified" { + { Update-EntraInvitedUserSponsorsFromInvitedBy } | Should -Throw "Please specify either -UserId or -All" + } + + It "Should return an error when no invited users are found" { + Mock -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -MockWith { $null } + + $result = Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "nonexistent-user" -Confirm:$false + + $result | Should -Contain "No guest users to process" + + Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 0 + } + + It "Should return an error when invitedBy information is not available" { + Mock -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Users -MockWith { return @{value = $null} } + + $result = Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false + + $result | Should -Contain "testguest@contoso.com - Invited user information not available for this user." + + Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 0 + } + } +} diff --git a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 new file mode 100644 index 0000000000..bf955a24c7 --- /dev/null +++ b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -0,0 +1,90 @@ +# ------------------------------------------------------------------------------ +# 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 Test-EntraBetaCommandPrerequisites -MockWith { return $true } -ModuleName Microsoft.Entra.Beta.Users + Mock -CommandName Get-MgContext -MockWith { @{Scopes = @("User.Read.All", "Directory.ReadWrite.All")} } + Mock -CommandName Get-MgEnvironment -MockWith { @{GraphEndpoint = "https://graph.microsoft.com"; AzureADEndpoint = "https://login.microsoftonline.com"} } + Mock -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -MockWith { @{value = @{id = "1111-2222-3333"}} } + Mock -CommandName Get-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -MockWith { @{Id = "4444-5555-6666"; DisplayName = "Test Guest"; UserPrincipalName = "testguest@contoso.com"; Sponsors = $null} } + Mock -CommandName Update-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -MockWith { return } +} + +Describe "Update-EntraBetaInvitedUserSponsorsFromInvitedBy" { + Context "Valid Inputs" { + + It "Should update sponsor for a specified guest user" { + $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false + + $result | Should -Contain "testguest@contoso.com - Sponsor updated successfully for this user." + + Should -Invoke -CommandName Get-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -Times 1 + Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 + Should -Invoke -CommandName Update-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -Times 1 + } + + It "Should process all guest users when -All is specified" { + $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -All -Confirm:$false + + $result | Should -Contain "testguest@contoso.com - Sponsor updated successfully for this user." + + Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 + } + + It "Should not update sponsor if sponsor already exists" { + Mock -CommandName Get-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -MockWith { @{Id = "4444-5555-6666"; Sponsors = @{id = "1111-2222-3333"}} } + + $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false + + $result | Should -Contain "testguest@contoso.com - Sponsor already exists for this user." + + Should -Invoke -CommandName Update-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -Times 0 + } + + It "Should contain 'User-Agent' header" { + $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" + + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false + + Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 -ParameterFilter { + $Headers.'User-Agent' | Should -Be $userAgentHeaderValue + $true + } + } + } + + Context "Invalid Inputs" { + + It "Should throw an error when neither -UserId nor -All is specified" { + { Update-EntraBetaInvitedUserSponsorsFromInvitedBy } | Should -Throw "Please specify either -UserId or -All" + } + + It "Should return an error when no invited users are found" { + Mock -CommandName Get-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -MockWith { $null } + + $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "nonexistent-user" -Confirm:$false + + $result | Should -Contain "No guest users to process" + + Should -Invoke -CommandName Update-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -Times 0 + } + + It "Should return an error when invitedBy information is not available" { + Mock -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -MockWith { return @{value = $null} } + + $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false + + $result | Should -Contain "testguest@contoso.com - Invited user information not available for this user." + + Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Beta.Users -Times 0 + } + } +} + From d7585587f8401919c99af98f62c88a1074c040e4 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Thu, 13 Feb 2025 09:58:13 +0300 Subject: [PATCH 04/21] Tests refactors, docs creation and cmdlet update to remove prerequisites --- .../Test-EntraBetaCommandPrerequisites.ps1 | 145 ------------------ ...-EntraInvitedUserSponsorsFromInvitedBy.ps1 | 46 +++--- ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 52 +++---- ...traBetaInvitedUserSponsorsFromInvitedBy.md | 120 +++++++++++++++ ...e-EntraInvitedUserSponsorsFromInvitedBy.md | 120 +++++++++++++++ ...InvitedUserSponsorsFromInvitedBy.Tests.ps1 | 97 +++++------- ...nvitedUserSponsorsFromInvitedBy.Tests..ps1 | 66 ++++++++ ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 90 ----------- 8 files changed, 385 insertions(+), 351 deletions(-) delete mode 100644 module/Entra/Microsoft.Entra/Users/Test-EntraBetaCommandPrerequisites.ps1 create mode 100644 module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md create mode 100644 module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md create mode 100644 test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 delete mode 100644 test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 diff --git a/module/Entra/Microsoft.Entra/Users/Test-EntraBetaCommandPrerequisites.ps1 b/module/Entra/Microsoft.Entra/Users/Test-EntraBetaCommandPrerequisites.ps1 deleted file mode 100644 index 158185c10b..0000000000 --- a/module/Entra/Microsoft.Entra/Users/Test-EntraBetaCommandPrerequisites.ps1 +++ /dev/null @@ -1,145 +0,0 @@ - function Test-EntraCommandPrerequisites { - [CmdletBinding()] - [OutputType([bool])] - param ( - # The name of a command. - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] - [Alias('Command')] - [string[]] $Name, - # The service API version. - [Parameter(Mandatory = $false, Position = 2)] - [ValidateSet('v1.0')] - [string] $ApiVersion = 'v1.0', - # Specifies a minimum version. - [Parameter(Mandatory = $false)] - [version] $MinimumVersion, - # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. - [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] - [switch] $RequireListPermissions - ) - - begin { - [version] $MgAuthenticationModuleVersion = $null - $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" - if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { - $MgAuthenticationModuleVersion = $Matches[2] - } - else { - $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version - } - Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" - } - - process { - ## Initialize - $result = $true - - ## Get Graph Command Details - [hashtable] $MgCommandLookup = @{} - foreach ($CommandName in $Name) { - - [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion - - if ($MgCommands.Count -eq 1) { - $MgCommand = $MgCommands[0] - } - elseif ($MgCommands.Count -gt 1) { - $MgCommand = $MgCommands[0] - ## Resolve from multiple results - [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null - [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" - [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" - if ($MgCommandsWithListPermissions -and $RequireListPermissions) { - $MgCommand = $MgCommandsWithListPermissions[0] - } - elseif ($MgCommandsWithGetPermissions) { - $MgCommand = $MgCommandsWithGetPermissions[0] - } - else { - $MgCommand = $MgCommands[0] - } - } - - if ($MgCommand) { - $MgCommandLookup[$MgCommand.Command] = $MgCommand - } - else { - Write-Error "Unable to resolve a specific command for '$CommandName'." - } - } - - ## Import Required Modules - [string[]] $MgModules = @() - foreach ($MgCommand in $MgCommandLookup.Values) { - if (!$MgModules.Contains($MgCommand.Module)) { - $MgModules += $MgCommand.Module - [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" - try { - if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { - ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - } - else { - ## Load module to match currently loaded Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - } - } - catch { - $result = $false - Write-Error -ErrorRecord $_ - } - } - } - Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) - - ## Check MgModule Connection - $MgContext = Get-MgContext - if ($MgContext) { - if ($MgContext.AuthType -eq 'Delegated') { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' - $result = $false - } - } - } - else { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" - } - } - } - } - else { - $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' - $result = $false - } - - return $result - } -} \ No newline at end of file diff --git a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 index 056eb2d101..08b0c4073b 100644 --- a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 @@ -1,4 +1,3 @@ - # ------------------------------------------------------------------------------ # Copyright (c) Microsoft Corporation. All Rights Reserved. # Licensed under the MIT License. See License in the project root for license information. @@ -10,22 +9,17 @@ function Update-EntraInvitedUserSponsorsFromInvitedBy { DefaultParameterSetName = 'AllInvitedGuests')] param ( - # UserId of Guest User - [Parameter(ParameterSetName = 'ByUsers')] + # UserId of Guest User + [Parameter(ParameterSetName = 'ByUsers',HelpMessage ="The Unique ID of the User (User ID).")] [String[]] $UserId, # Enumerate and Update All Guest Users. - [Parameter(ParameterSetName = 'AllInvitedGuests')] + [Parameter(ParameterSetName = 'AllInvitedGuests',HelpMessage="A Flag indicating whether to include all invited guests.")] [switch] $All ) - begin { - - ## Initialize Critical Dependencies - $CriticalError = $null - if (!(Test-EntraCommandPrerequisites 'Get-Mguser', 'Update-Mguser' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } - + begin { $guestFilter = "(CreationType eq 'Invitation')" } @@ -33,32 +27,31 @@ function Update-EntraInvitedUserSponsorsFromInvitedBy { $customHeaders = New-EntraCustomHeaders -Command $MyInvocation.MyCommand - if ($CriticalError) { return } if ($null -eq $UserId -and !$All) { Write-Error "Please specify either -UserId or -All" return } if ($All) { - $InvitedUsers = Get-MgUser -Filter $guestFilter -All -ExpandProperty Sponsors + $invitedUsers = Get-EntraUser -Filter $guestFilter -All -ExpandProperty Sponsors } else { - foreach ($user in $userId) { - $InvitedUsers += Get-MgUser -UserId $user -ExpandProperty Sponsors + foreach ($user in $UserId) { + $invitedUsers += Get-EntraUser -UserId $user -ExpandProperty Sponsors } } - if ($null -eq $InvitedUsers) { + if ($null -eq $invitedUsers) { Write-Error "No guest users to process" } else { - foreach ($InvitedUser in $InvitedUsers) { + foreach ($invitedUser in $invitedUsers) { $invitedBy = $null $splatArgumentsGetInvitedBy = @{ Method = 'Get' - Uri = ((Get-MgEnvironment -Name (Get-MgContext).Environment).GraphEndpoint + - "/v1.0/users/" + $InvitedUser.Id + "/invitedBy") + Uri = ((Get-EntraEnvironment -Name (Get-EntraContext).Environment).GraphEndpoint + + "/v1.0/users/" + $invitedUser.id + "/invitedBy") } $invitedBy = Invoke-MgGraphRequest @splatArgumentsGetInvitedBy -Headers $customHeaders @@ -66,32 +59,32 @@ function Update-EntraInvitedUserSponsorsFromInvitedBy { Write-Verbose ($invitedBy | ConvertTo-Json -Depth 10) if ($null -ne $invitedBy -and $null -ne $invitedBy.value -and $null -ne (Get-ObjectPropertyValue $invitedBy.value -Property 'id')) { - Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $InvitedUser.DisplayName, $invitedBy.value.id) + Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $invitedUser.displayName, $invitedBy.value.id) - if (($null -like $InvitedUser.Sponsors) -or ($InvitedUser.Sponsors.id -notcontains $invitedBy.value.id)) { + if (($null -like $invitedUser.sponsors) -or ($invitedUser.sponsors.id -notcontains $invitedBy.value.id)) { Write-Verbose "Sponsors does not contain the user who invited them!" - if ($PSCmdlet.ShouldProcess(("$($InvitedUser.displayName) ($($InvitedUser.UserPrincipalName) - $($InvitedUser.id))"), "Update Sponsors")) { + if ($PSCmdlet.ShouldProcess(("$($invitedUser.displayName) ($($invitedUser.userPrincipalName) - $($invitedUser.id))"), "Update Sponsors")) { try { $sponsorUrl = ("https://graph.microsoft.com/v1.0/users/{0}" -f $invitedBy.value.id) $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } $sponsorsRequestBody = $dirObj | ConvertTo-Json - Update-MgUser -UserId $InvitedUser.Id -BodyParameter $sponsorsRequestBody -Header $customHeaders - Write-Output "$($InvitedUser.UserPrincipalName) - Sponsor updated succesfully for this user." + Update-EntraUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders + Write-Output "$($invitedUser.userPrincipalName) - Sponsor updated successfully for this user." } catch { - Write-Output "$($InvitedUser.UserPrincipalName) - Failed updating sponsor for this user." + Write-Output "$($invitedUser.userPrincipalName) - Failed updating sponsor for this user." Write-Error $_ } } } else { - Write-Output "$($InvitedUser.UserPrincipalName) - Sponsor already exists for this user." + Write-Output "$($invitedUser.userPrincipalName) - Sponsor already exists for this user." } } else { - Write-Output "$($InvitedUser.UserPrincipalName) - Invited user information not available for this user." + Write-Output "$($invitedUser.userPrincipalName) - Invited user information not available for this user." } } } @@ -101,4 +94,3 @@ function Update-EntraInvitedUserSponsorsFromInvitedBy { Write-Verbose "Complete!" } } - diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index 3ef3f5fbe7..c42cf05ac6 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -1,3 +1,7 @@ +# ------------------------------------------------------------------------------ +# 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', @@ -5,21 +9,16 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { param ( # UserId of Guest User - [Parameter(ParameterSetName = 'ByUsers')] + [Parameter(ParameterSetName = 'ByUsers',HelpMessage ="The Unique ID of the User (User ID).")] [String[]] - $UserId, + $userId, # Enumerate and Update All Guest Users. - [Parameter(ParameterSetName = 'AllInvitedGuests')] + [Parameter(ParameterSetName = 'AllInvitedGuests',HelpMessage="A Flag indicating whether to include all invited guests.")] [switch] - $All + $all ) - begin { - - ## Initialize Critical Dependencies - $CriticalError = $null - if (!(Test-EntraBetaCommandPrerequisites 'Get-MgBetaUser', 'Update-MgBetaUser' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } - + begin { $guestFilter = "(CreationType eq 'Invitation')" } @@ -27,32 +26,31 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { $customHeaders = New-EntraBetaCustomHeaders -Command $MyInvocation.MyCommand - if ($CriticalError) { return } - if ($null -eq $UserId -and !$All) { + if ($null -eq $userId -and !$all) { Write-Error "Please specify either -UserId or -All" return } - if ($All) { - $InvitedUsers = Get-MgBetaUser -Filter $guestFilter -All -ExpandProperty Sponsors + if ($all) { + $invitedUsers = Get-EntraBetaUser -Filter $guestFilter -All -ExpandProperty sponsors } else { foreach ($user in $userId) { - $InvitedUsers += Get-MgUser -UserId $user -ExpandProperty Sponsors + $invitedUsers += Get-EntraBetaUser -UserId $user -ExpandProperty sponsors } } - if ($null -eq $InvitedUsers) { + if ($null -eq $invitedUsers) { Write-Error "No guest users to process" } else { - foreach ($InvitedUser in $InvitedUsers) { + foreach ($invitedUser in $invitedUsers) { $invitedBy = $null $splatArgumentsGetInvitedBy = @{ Method = 'Get' - Uri = ((Get-MgEnvironment -Name (Get-MgContext).Environment).GraphEndpoint + - "/beta/users/" + $InvitedUser.Id + "/invitedBy") + Uri = ((Get-MgEnvironment -Name (Get-EntraBetaContext).Environment).GraphEndpoint + + "/beta/users/" + $invitedUser.id + "/invitedBy") } $invitedBy = Invoke-MgGraphRequest @splatArgumentsGetInvitedBy -Headers $customHeaders @@ -60,32 +58,32 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { Write-Verbose ($invitedBy | ConvertTo-Json -Depth 10) if ($null -ne $invitedBy -and $null -ne $invitedBy.value -and $null -ne (Get-ObjectPropertyValue $invitedBy.value -Property 'id')) { - Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $InvitedUser.DisplayName, $invitedBy.value.id) + Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $invitedUser.displayName, $invitedBy.value.id) - if (($null -like $InvitedUser.Sponsors) -or ($InvitedUser.Sponsors.id -notcontains $invitedBy.value.id)) { + if (($null -like $invitedUser.sponsors) -or ($invitedUser.sponsors.id -notcontains $invitedBy.value.id)) { Write-Verbose "Sponsors does not contain the user who invited them!" - if ($PSCmdlet.ShouldProcess(("$($InvitedUser.displayName) ($($InvitedUser.UserPrincipalName) - $($InvitedUser.id))"), "Update Sponsors")) { + if ($PSCmdlet.ShouldProcess(("$($invitedUser.displayName) ($($invitedUser.userPrincipalName) - $($invitedUser.id))"), "Update Sponsors")) { try { $sponsorUrl = ("https://graph.microsoft.com/beta/users/{0}" -f $invitedBy.value.id) $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } $sponsorsRequestBody = $dirObj | ConvertTo-Json - Update-MgUser -UserId $InvitedUser.Id -BodyParameter $sponsorsRequestBody - Write-Output "$($InvitedUser.UserPrincipalName) - Sponsor updated succesfully for this user." + Update-EntraUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders + Write-Output "$($invitedUser.userPrincipalName) - Sponsor updated successfully for this user." } catch { - Write-Output "$($InvitedUser.UserPrincipalName) - Failed updating sponsor for this user." + Write-Output "$($invitedUser.userPrincipalName) - Failed updating sponsor for this user." Write-Error $_ } } } else { - Write-Output "$($InvitedUser.UserPrincipalName) - Sponsor already exists for this user." + Write-Output "$($invitedUser.userPrincipalName) - Sponsor already exists for this user." } } else { - Write-Output "$($InvitedUser.UserPrincipalName) - Invited user information not available for this user." + Write-Output "$($invitedUser.userPrincipalName) - Invited user information not available for this user." } } } 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..82c27b4e43 --- /dev/null +++ b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md @@ -0,0 +1,120 @@ +--- +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.Users-Help.xml +Module Name: Microsoft.Entra +online version: https://learn.microsoft.com/powershell/module/Microsoft.Entra/Update-EntraBetaInvitedUserSponsorsFromInvitedBy + +schema: 2.0.0 +--- + +# Update-EntraBetaInvitedUserSponsorsFromInvitedBy + +## Synopsis + +Updates the sponsors of invited users based on the user who invited them. + +## Syntax + +```powershell +Update-EntraBetaInvitedUserSponsorsFromInvitedBy + [-UserId ] + [-All] + [] +``` + +## Description + +The `Update-EntraBetaInvitedUserSponsorsFromInvitedBy` cmdlet updates the sponsors for invited users based on the inviter's information in Microsoft Entra ID. + +The calling user must be assigned at least one of the following Microsoft Entra roles: + +- User Administrator +- Privileged Authentication Administrator + +## Examples + +### Example 1: Update sponsors for a specific guest user + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All' +Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId 'guestuser@contoso.com' +``` + +This command updates the sponsors for the specified guest user in Microsoft Entra ID. + +### Example 2: Update sponsors for all invited guest users + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All' +Update-EntraBetaInvitedUserSponsorsFromInvitedBy -All +``` + +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 + +None. + +## Outputs + +None. + +## 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) + +[Update-EntraUser](Update-EntraBetaUser.md) + diff --git a/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md b/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md new file mode 100644 index 0000000000..11db6e247e --- /dev/null +++ b/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md @@ -0,0 +1,120 @@ +--- +title: Update-EntraInvitedUserSponsorsFromInvitedBy +description: This article provides details on the Update-EntraInvitedUserSponsorsFromInvitedBy command. + +ms.topic: reference +ms.date: 06/26/2024 +ms.author: eunicewaweru +ms.reviewer: stevemutungi +manager: CelesteDG +author: msewaweru + +external help file: Microsoft.Entra.Users-Help.xml +Module Name: Microsoft.Entra +online version: https://learn.microsoft.com/powershell/module/Microsoft.Entra/Update-EntraInvitedUserSponsorsFromInvitedBy + +schema: 2.0.0 +--- + +# Update-EntraInvitedUserSponsorsFromInvitedBy + +## Synopsis + +Updates the sponsors of invited users based on the user who invited them. + +## Syntax + +```powershell +Update-EntraInvitedUserSponsorsFromInvitedBy + [-UserId ] + [-All] + [] +``` + +## Description + +The `Update-EntraInvitedUserSponsorsFromInvitedBy` cmdlet updates the sponsors for invited users based on the inviter's information in Microsoft Entra ID. + +The calling user must be assigned at least one of the following Microsoft Entra roles: + +- User Administrator +- Privileged Authentication Administrator + +## Examples + +### Example 1: Update sponsors for a specific guest user + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All' +Update-EntraInvitedUserSponsorsFromInvitedBy -UserId 'guestuser@contoso.com' +``` + +This command updates the sponsors for the specified guest user in Microsoft Entra ID. + +### Example 2: Update sponsors for all invited guest users + +```powershell +Connect-Entra -Scopes 'User.ReadWrite.All' +Update-EntraInvitedUserSponsorsFromInvitedBy -All +``` + +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 + +None. + +## Outputs + +None. + +## 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-EntraUser.md) + +[Update-EntraUser](Update-EntraUser.md) + diff --git a/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 b/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 index 74ac054fdd..589c129538 100644 --- a/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 +++ b/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 @@ -9,85 +9,58 @@ BeforeAll { } Import-Module (Join-Path $PSScriptRoot "..\..\Common-Functions.ps1") -Force - Mock -CommandName Test-EntraCommandPrerequisites -MockWith { return $true } -ModuleName Microsoft.Entra.Users - Mock -CommandName Get-MgContext -MockWith { @{Scopes = @("User.Read.All", "Directory.ReadWrite.All")} } - Mock -CommandName Get-MgEnvironment -MockWith { @{GraphEndpoint = "https://graph.microsoft.com"; AzureADEndpoint = "https://login.microsoftonline.com"} } - Mock -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Users -MockWith { @{value = @{id = "1111-2222-3333"}} } - Mock -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -MockWith { @{Id = "4444-5555-6666"; DisplayName = "Test Guest"; UserPrincipalName = "testguest@contoso.com"; Sponsors = $null} } - Mock -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -MockWith { return } + Mock -CommandName Get-EntraUser -MockWith { + @{ Id = "123"; DisplayName = "Test Guest"; UserPrincipalName = "test@contoso.com"; Sponsors = $null } + } -ModuleName Microsoft.Entra.Users + Mock -CommandName Invoke-MgGraphRequest -MockWith { + @{ value = @{ id = "456" } } + } -ModuleName Microsoft.Entra.Users + Mock -CommandName Update-EntraUser -MockWith { $true } -ModuleName Microsoft.Entra.Users } Describe "Update-EntraInvitedUserSponsorsFromInvitedBy" { Context "Valid Inputs" { - - It "Should update sponsor for a specified guest user" { - $result = Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false - - $result | Should -Contain "testguest@contoso.com - Sponsor updated successfully for this user." - - Should -Invoke -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -Times 1 - Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Users -Times 1 - Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 1 - } - - It "Should process all guest users when -All is specified" { - $result = Update-EntraInvitedUserSponsorsFromInvitedBy -All -Confirm:$false - - $result | Should -Contain "testguest@contoso.com - Sponsor updated successfully for this user." - - Should -Invoke -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -Times 1 - Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Users -Times 1 - Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 1 + It "Should update sponsor for a single user" { + Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor updated succesfully" } - It "Should not update sponsor if sponsor already exists" { - Mock -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -MockWith { @{Id = "4444-5555-6666"; Sponsors = @{id = "1111-2222-3333"}} } - - $result = Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false - - $result | Should -Contain "testguest@contoso.com - Sponsor already exists for this user." - - Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 0 - } - - It "Should contain 'User-Agent' header" { - $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraInvitedUserSponsorsFromInvitedBy" - - Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false - - $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraInvitedUserSponsorsFromInvitedBy" - - Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 -ParameterFilter { - $Headers.'User-Agent' | Should -Be $userAgentHeaderValue - $true - } + It "Should process all invited users when -All is specified" { + Update-EntraInvitedUserSponsorsFromInvitedBy -All -Confirm:$false | Should -Match "Sponsor updated succesfully" } } Context "Invalid Inputs" { - - It "Should throw an error when neither -UserId nor -All is specified" { + It "Should throw an error when neither -UserId nor -All is provided" { { Update-EntraInvitedUserSponsorsFromInvitedBy } | Should -Throw "Please specify either -UserId or -All" } + } - It "Should return an error when no invited users are found" { - Mock -CommandName Get-MgUser -ModuleName Microsoft.Entra.Users -MockWith { $null } - - $result = Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "nonexistent-user" -Confirm:$false - - $result | Should -Contain "No guest users to process" - - Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 0 + Context "Edge Cases" { + It "Should not update if sponsor already exists" { + Mock -CommandName Get-EntraUser -MockWith { + @{ Id = "123"; DisplayName = "Test Guest"; UserPrincipalName = "test@contoso.com"; Sponsors = @{ id = "456" } } + } -ModuleName Microsoft.Entra.Users + + Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor already exists" } - It "Should return an error when invitedBy information is not available" { - Mock -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Users -MockWith { return @{value = $null} } - - $result = Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false + It "Should handle missing invitedBy information" { + Mock -CommandName Invoke-MgGraphRequest -MockWith { @{ value = $null } } -ModuleName Microsoft.Entra.Users + + Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Invited user information not available" + } + } - $result | Should -Contain "testguest@contoso.com - Invited user information not available for this user." + Context "User-Agent Header" { + It "Should contain 'User-Agent' header" { + $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraInvitedUserSponsorsFromInvitedBy" - Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Users -Times 0 + Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "123" + + Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.DirectoryManagement -Times 1 -ParameterFilter { + $Headers.'User-Agent' | Should -Be $userAgentHeaderValue + $true + } } } } diff --git a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 new file mode 100644 index 0000000000..e28126b9e5 --- /dev/null +++ b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 @@ -0,0 +1,66 @@ +# ------------------------------------------------------------------------------ +# 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-EntraBetaUser -MockWith { + @{ Id = "123"; DisplayName = "Test Guest"; UserPrincipalName = "test@contoso.com"; Sponsors = $null } + } -ModuleName Microsoft.Entra.Beta.Users + Mock -CommandName Invoke-MgGraphRequest -MockWith { + @{ value = @{ id = "456" } } + } -ModuleName Microsoft.Graph.Beta.Users + Mock -CommandName Update-EntraBetaUser -MockWith { $true } -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 succesfully" + } + + It "Should process all invited users when -All is specified" { + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -All -Confirm:$false | Should -Match "Sponsor updated succesfully" + } + } + + Context "Invalid Inputs" { + It "Should throw an error when neither -UserId nor -All is provided" { + { Update-EntraBetaInvitedUserSponsorsFromInvitedBy } | Should -Throw "Please specify either -UserId or -All" + } + } + + Context "Edge Cases" { + It "Should not update if sponsor already exists" { + Mock -CommandName Get-EntraBetaUser -MockWith { + @{ Id = "123"; DisplayName = "Test Guest"; UserPrincipalName = "test@contoso.com"; Sponsors = @{ id = "456" } } + } + + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor already exists" + } + + It "Should handle missing invitedBy information" { + Mock -CommandName Invoke-MgGraphRequest -MockWith { @{ value = $null } } -ModuleName Microsoft.Entra.Beta.Users + + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Invited user information not available" + } + } + + Context "User-Agent Header" { + It "Should contain 'User-Agent' header" { + $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraInvitedUserSponsorsFromInvitedBy" + + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" + + Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 -ParameterFilter { + $Headers.'User-Agent' | Should -Be $userAgentHeaderValue + $true + } + } + } +} diff --git a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 deleted file mode 100644 index bf955a24c7..0000000000 --- a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ /dev/null @@ -1,90 +0,0 @@ -# ------------------------------------------------------------------------------ -# 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 Test-EntraBetaCommandPrerequisites -MockWith { return $true } -ModuleName Microsoft.Entra.Beta.Users - Mock -CommandName Get-MgContext -MockWith { @{Scopes = @("User.Read.All", "Directory.ReadWrite.All")} } - Mock -CommandName Get-MgEnvironment -MockWith { @{GraphEndpoint = "https://graph.microsoft.com"; AzureADEndpoint = "https://login.microsoftonline.com"} } - Mock -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -MockWith { @{value = @{id = "1111-2222-3333"}} } - Mock -CommandName Get-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -MockWith { @{Id = "4444-5555-6666"; DisplayName = "Test Guest"; UserPrincipalName = "testguest@contoso.com"; Sponsors = $null} } - Mock -CommandName Update-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -MockWith { return } -} - -Describe "Update-EntraBetaInvitedUserSponsorsFromInvitedBy" { - Context "Valid Inputs" { - - It "Should update sponsor for a specified guest user" { - $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false - - $result | Should -Contain "testguest@contoso.com - Sponsor updated successfully for this user." - - Should -Invoke -CommandName Get-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -Times 1 - Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 - Should -Invoke -CommandName Update-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -Times 1 - } - - It "Should process all guest users when -All is specified" { - $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -All -Confirm:$false - - $result | Should -Contain "testguest@contoso.com - Sponsor updated successfully for this user." - - Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 - } - - It "Should not update sponsor if sponsor already exists" { - Mock -CommandName Get-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -MockWith { @{Id = "4444-5555-6666"; Sponsors = @{id = "1111-2222-3333"}} } - - $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false - - $result | Should -Contain "testguest@contoso.com - Sponsor already exists for this user." - - Should -Invoke -CommandName Update-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -Times 0 - } - - It "Should contain 'User-Agent' header" { - $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" - - Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false - - Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 -ParameterFilter { - $Headers.'User-Agent' | Should -Be $userAgentHeaderValue - $true - } - } - } - - Context "Invalid Inputs" { - - It "Should throw an error when neither -UserId nor -All is specified" { - { Update-EntraBetaInvitedUserSponsorsFromInvitedBy } | Should -Throw "Please specify either -UserId or -All" - } - - It "Should return an error when no invited users are found" { - Mock -CommandName Get-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -MockWith { $null } - - $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "nonexistent-user" -Confirm:$false - - $result | Should -Contain "No guest users to process" - - Should -Invoke -CommandName Update-MgBetaUser -ModuleName Microsoft.Entra.Beta.Users -Times 0 - } - - It "Should return an error when invitedBy information is not available" { - Mock -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -MockWith { return @{value = $null} } - - $result = Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "4444-5555-6666" -Confirm:$false - - $result | Should -Contain "testguest@contoso.com - Invited user information not available for this user." - - Should -Invoke -CommandName Update-MgUser -ModuleName Microsoft.Entra.Beta.Users -Times 0 - } - } -} - From d1d5563d05c6d109caa30ac3c2bb88444c5d632d Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:08:13 +0300 Subject: [PATCH 05/21] Fixes --- .../Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 | 2 +- .../Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 | 2 +- .../Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md | 3 +-- .../Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md | 3 +-- .../Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 | 2 +- ...Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 index 08b0c4073b..0448f032cb 100644 --- a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 @@ -70,7 +70,7 @@ function Update-EntraInvitedUserSponsorsFromInvitedBy { $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } $sponsorsRequestBody = $dirObj | ConvertTo-Json - Update-EntraUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders + Set-EntraUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders Write-Output "$($invitedUser.userPrincipalName) - Sponsor updated successfully for this user." } catch { diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index c42cf05ac6..c0e088e876 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -69,7 +69,7 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } $sponsorsRequestBody = $dirObj | ConvertTo-Json - Update-EntraUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders + Set-EntraUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders Write-Output "$($invitedUser.userPrincipalName) - Sponsor updated successfully for this user." } catch { diff --git a/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md index 82c27b4e43..3e8b835b4e 100644 --- a/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md +++ b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md @@ -116,5 +116,4 @@ None. [Get-EntraUser](Get-EntraBetaUser.md) -[Update-EntraUser](Update-EntraBetaUser.md) - +[Set-EntraUser](Set-EntraBetaUser.md) diff --git a/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md b/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md index 11db6e247e..de9a6d28ae 100644 --- a/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md +++ b/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md @@ -116,5 +116,4 @@ None. [Get-EntraUser](Get-EntraUser.md) -[Update-EntraUser](Update-EntraUser.md) - +[Set-EntraUser](Set-EntraUser.md) diff --git a/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 b/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 index 589c129538..497919d8ad 100644 --- a/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 +++ b/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 @@ -15,7 +15,7 @@ BeforeAll { Mock -CommandName Invoke-MgGraphRequest -MockWith { @{ value = @{ id = "456" } } } -ModuleName Microsoft.Entra.Users - Mock -CommandName Update-EntraUser -MockWith { $true } -ModuleName Microsoft.Entra.Users + Mock -CommandName Set-EntraUser -MockWith { $true } -ModuleName Microsoft.Entra.Users } Describe "Update-EntraInvitedUserSponsorsFromInvitedBy" { diff --git a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 index e28126b9e5..f83d909822 100644 --- a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 +++ b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 @@ -15,7 +15,7 @@ BeforeAll { Mock -CommandName Invoke-MgGraphRequest -MockWith { @{ value = @{ id = "456" } } } -ModuleName Microsoft.Graph.Beta.Users - Mock -CommandName Update-EntraBetaUser -MockWith { $true } -ModuleName Microsoft.Entra.Beta.Users + Mock -CommandName Set-EntraBetaUser -MockWith { $true } -ModuleName Microsoft.Entra.Beta.Users } Describe "Update-EntraBetaInvitedUserSponsorsFromInvitedBy" { From 3b242b9b3d5ff75fb939001c18ae5dac089de30b Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:44:53 +0300 Subject: [PATCH 06/21] Changed to use -Select --- ...-EntraInvitedUserSponsorsFromInvitedBy.ps1 | 4 +-- ...traBetaInvitedUserSponsorsFromInvitedBy.md | 29 +++++++++++-------- ...e-EntraInvitedUserSponsorsFromInvitedBy.md | 25 +++++++++------- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 index 0448f032cb..b1551d2434 100644 --- a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 @@ -33,11 +33,11 @@ function Update-EntraInvitedUserSponsorsFromInvitedBy { } if ($All) { - $invitedUsers = Get-EntraUser -Filter $guestFilter -All -ExpandProperty Sponsors + $invitedUsers = Get-EntraUser -Filter $guestFilter -All -Select Sponsors } else { foreach ($user in $UserId) { - $invitedUsers += Get-EntraUser -UserId $user -ExpandProperty Sponsors + $invitedUsers += Get-EntraUser -UserId $user -Select Sponsors } } diff --git a/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md index 3e8b835b4e..828c2a2a5e 100644 --- a/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md +++ b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md @@ -9,9 +9,9 @@ ms.reviewer: stevemutungi manager: CelesteDG author: msewaweru -external help file: Microsoft.Entra.Users-Help.xml -Module Name: Microsoft.Entra -online version: https://learn.microsoft.com/powershell/module/Microsoft.Entra/Update-EntraBetaInvitedUserSponsorsFromInvitedBy +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 --- @@ -20,7 +20,7 @@ schema: 2.0.0 ## Synopsis -Updates the sponsors of invited users based on the user who invited them. +Update the Sponsors attribute to include the user who initially invited them to the tenant using the InvitedBy property. ## Syntax @@ -33,7 +33,7 @@ Update-EntraBetaInvitedUserSponsorsFromInvitedBy ## Description -The `Update-EntraBetaInvitedUserSponsorsFromInvitedBy` cmdlet updates the sponsors for invited users based on the inviter's information in Microsoft Entra ID. +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: @@ -42,16 +42,25 @@ The calling user must be assigned at least one of the following Microsoft Entra ## Examples -### Example 1: Update sponsors for a specific guest user +### Example 1: Enumerate all invited users in the Tenant and update Sponsors using InvitedBy value + +```powershell + Connect-Entra -Scopes 'User.ReadWrite.All' + Update-EntraBetaInvitedUserSponsorsFromInvitedBy +``` + +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 'guestuser@contoso.com' +Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId 'guestuser@contoso.com','guestuser1@contoso.com' ``` This command updates the sponsors for the specified guest user in Microsoft Entra ID. -### Example 2: Update sponsors for all invited guest users +### Example 3: Update sponsors for all invited guest users ```powershell Connect-Entra -Scopes 'User.ReadWrite.All' @@ -100,12 +109,8 @@ This cmdlet supports the common parameters: `-Debug`, `-ErrorAction`, `-ErrorVar ## Inputs -None. - ## Outputs -None. - ## Notes - If neither `-UserId` nor `-All` is specified, the cmdlet returns an error. diff --git a/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md b/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md index de9a6d28ae..d0c55549e5 100644 --- a/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md +++ b/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md @@ -3,7 +3,7 @@ title: Update-EntraInvitedUserSponsorsFromInvitedBy description: This article provides details on the Update-EntraInvitedUserSponsorsFromInvitedBy command. ms.topic: reference -ms.date: 06/26/2024 +ms.date: 02/11/2025 ms.author: eunicewaweru ms.reviewer: stevemutungi manager: CelesteDG @@ -20,7 +20,7 @@ schema: 2.0.0 ## Synopsis -Updates the sponsors of invited users based on the user who invited them. +Update the Sponsors attribute to include the user who initially invited them to the tenant using the InvitedBy property. ## Syntax @@ -33,7 +33,7 @@ Update-EntraInvitedUserSponsorsFromInvitedBy ## Description -The `Update-EntraInvitedUserSponsorsFromInvitedBy` cmdlet updates the sponsors for invited users based on the inviter's information in Microsoft Entra ID. +The `Update-EntraInvitedUserSponsorsFromInvitedBy` 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: @@ -42,16 +42,25 @@ The calling user must be assigned at least one of the following Microsoft Entra ## Examples -### Example 1: Update sponsors for a specific guest user +### Example 1: Enumerate all invited users in the Tenant and update Sponsors using InvitedBy value + +```powershell + Connect-Entra -Scopes 'User.ReadWrite.All' + Update-EntraInvitedUserSponsorsFromInvitedBy +``` + +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-EntraInvitedUserSponsorsFromInvitedBy -UserId 'guestuser@contoso.com' +Update-EntraInvitedUserSponsorsFromInvitedBy -UserId 'guestuser@contoso.com','guestuser1@contoso.com' ``` This command updates the sponsors for the specified guest user in Microsoft Entra ID. -### Example 2: Update sponsors for all invited guest users +### Example 3: Update sponsors for all invited guest users ```powershell Connect-Entra -Scopes 'User.ReadWrite.All' @@ -100,12 +109,8 @@ This cmdlet supports the common parameters: `-Debug`, `-ErrorAction`, `-ErrorVar ## Inputs -None. - ## Outputs -None. - ## Notes - If neither `-UserId` nor `-All` is specified, the cmdlet returns an error. From 1be86fb2884d0e8f1e6c193972d938099566a893 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:58:48 +0300 Subject: [PATCH 07/21] Implement Get-ObjectPropertyValue function --- ...-EntraInvitedUserSponsorsFromInvitedBy.ps1 | 40 +++++++++++++++++++ ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 39 ++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 index b1551d2434..90b611937c 100644 --- a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 @@ -94,3 +94,43 @@ function Update-EntraInvitedUserSponsorsFromInvitedBy { Write-Verbose "Complete!" } } + +function Get-ObjectPropertyValue { + [CmdletBinding()] + [OutputType([psobject])] + param ( + # Object containing property values + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [AllowNull()] + [psobject] $InputObjects, + # Name of property. Specify an array of property names to tranverse nested objects. + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] + [string[]] $Property + ) + + process { + foreach ($InputObject in $InputObjects) { + for ($iProperty = 0; $iProperty -lt $Property.Count; $iProperty++) { + ## Get property value + if ($InputObject -is [hashtable]) { + if ($InputObject.ContainsKey($Property[$iProperty])) { + $PropertyValue = $InputObject[$Property[$iProperty]] + } + else { $PropertyValue = $null } + } + else { + $PropertyValue = Select-Object -InputObject $InputObject -ExpandProperty $Property[$iProperty] -ErrorAction Ignore + if ($null -eq $PropertyValue) { break } + } + ## Check for more nested properties + if ($iProperty -lt $Property.Count - 1) { + $InputObject = $PropertyValue + if ($null -eq $InputObject) { break } + } + else { + Write-Output $PropertyValue + } + } + } + } +} \ No newline at end of file diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index c0e088e876..458dfad251 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -94,3 +94,42 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { } } +function Get-ObjectPropertyValue { + [CmdletBinding()] + [OutputType([psobject])] + param ( + # Object containing property values + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [AllowNull()] + [psobject] $InputObjects, + # Name of property. Specify an array of property names to tranverse nested objects. + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] + [string[]] $Property + ) + + process { + foreach ($InputObject in $InputObjects) { + for ($iProperty = 0; $iProperty -lt $Property.Count; $iProperty++) { + ## Get property value + if ($InputObject -is [hashtable]) { + if ($InputObject.ContainsKey($Property[$iProperty])) { + $PropertyValue = $InputObject[$Property[$iProperty]] + } + else { $PropertyValue = $null } + } + else { + $PropertyValue = Select-Object -InputObject $InputObject -ExpandProperty $Property[$iProperty] -ErrorAction Ignore + if ($null -eq $PropertyValue) { break } + } + ## Check for more nested properties + if ($iProperty -lt $Property.Count - 1) { + $InputObject = $PropertyValue + if ($null -eq $InputObject) { break } + } + else { + Write-Output $PropertyValue + } + } + } + } +} From 954cf7220abd862d971f78b81fe57fbac676fce0 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Thu, 13 Feb 2025 11:11:49 +0300 Subject: [PATCH 08/21] Updated to -Select for Beta --- .../Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index 458dfad251..c2fabb44e4 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -32,11 +32,11 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { } if ($all) { - $invitedUsers = Get-EntraBetaUser -Filter $guestFilter -All -ExpandProperty sponsors + $invitedUsers = Get-EntraBetaUser -Filter $guestFilter -All -Select Sponsors } else { foreach ($user in $userId) { - $invitedUsers += Get-EntraBetaUser -UserId $user -ExpandProperty sponsors + $invitedUsers += Get-EntraBetaUser -UserId $user -Select Sponsors } } From e49b3e33f342b85005ab22fc95560d3201ee2c1d Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Fri, 14 Feb 2025 07:38:35 +0300 Subject: [PATCH 09/21] Only better version is supported --- ...-EntraInvitedUserSponsorsFromInvitedBy.ps1 | 136 ------------------ ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 9 +- ...e-EntraInvitedUserSponsorsFromInvitedBy.md | 124 ---------------- ...InvitedUserSponsorsFromInvitedBy.Tests.ps1 | 66 --------- 4 files changed, 6 insertions(+), 329 deletions(-) delete mode 100644 module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 delete mode 100644 module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md delete mode 100644 test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 diff --git a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 b/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 deleted file mode 100644 index 90b611937c..0000000000 --- a/module/Entra/Microsoft.Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.ps1 +++ /dev/null @@ -1,136 +0,0 @@ -# ------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All Rights Reserved. -# Licensed under the MIT License. See License in the project root for license information. -# ------------------------------------------------------------------------------ - -function Update-EntraInvitedUserSponsorsFromInvitedBy { - [CmdletBinding(SupportsShouldProcess, - ConfirmImpact = 'High', - DefaultParameterSetName = 'AllInvitedGuests')] - param ( - - # UserId of Guest User - [Parameter(ParameterSetName = 'ByUsers',HelpMessage ="The Unique ID of the User (User ID).")] - [String[]] - $UserId, - # Enumerate and Update All Guest Users. - [Parameter(ParameterSetName = 'AllInvitedGuests',HelpMessage="A Flag indicating whether to include all invited guests.")] - [switch] - $All - ) - - begin { - $guestFilter = "(CreationType eq 'Invitation')" - } - - process { - - $customHeaders = New-EntraCustomHeaders -Command $MyInvocation.MyCommand - - if ($null -eq $UserId -and !$All) { - Write-Error "Please specify either -UserId or -All" - return - } - - if ($All) { - $invitedUsers = Get-EntraUser -Filter $guestFilter -All -Select Sponsors - } - else { - foreach ($user in $UserId) { - $invitedUsers += Get-EntraUser -UserId $user -Select Sponsors - } - } - - if ($null -eq $invitedUsers) { - Write-Error "No guest users to process" - } - else { - foreach ($invitedUser in $invitedUsers) { - $invitedBy = $null - - $splatArgumentsGetInvitedBy = @{ - Method = 'Get' - Uri = ((Get-EntraEnvironment -Name (Get-EntraContext).Environment).GraphEndpoint + - "/v1.0/users/" + $invitedUser.id + "/invitedBy") - } - - $invitedBy = Invoke-MgGraphRequest @splatArgumentsGetInvitedBy -Headers $customHeaders - - Write-Verbose ($invitedBy | ConvertTo-Json -Depth 10) - - if ($null -ne $invitedBy -and $null -ne $invitedBy.value -and $null -ne (Get-ObjectPropertyValue $invitedBy.value -Property 'id')) { - Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $invitedUser.displayName, $invitedBy.value.id) - - if (($null -like $invitedUser.sponsors) -or ($invitedUser.sponsors.id -notcontains $invitedBy.value.id)) { - 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/v1.0/users/{0}" -f $invitedBy.value.id) - $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } - $sponsorsRequestBody = $dirObj | ConvertTo-Json - - Set-EntraUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders - Write-Output "$($invitedUser.userPrincipalName) - Sponsor updated successfully for this user." - } - catch { - Write-Output "$($invitedUser.userPrincipalName) - Failed updating sponsor for this user." - Write-Error $_ - } - } - } - else { - Write-Output "$($invitedUser.userPrincipalName) - Sponsor already exists for this user." - } - } - else { - Write-Output "$($invitedUser.userPrincipalName) - Invited user information not available for this user." - } - } - } - } - - end { - Write-Verbose "Complete!" - } -} - -function Get-ObjectPropertyValue { - [CmdletBinding()] - [OutputType([psobject])] - param ( - # Object containing property values - [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] - [AllowNull()] - [psobject] $InputObjects, - # Name of property. Specify an array of property names to tranverse nested objects. - [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] - [string[]] $Property - ) - - process { - foreach ($InputObject in $InputObjects) { - for ($iProperty = 0; $iProperty -lt $Property.Count; $iProperty++) { - ## Get property value - if ($InputObject -is [hashtable]) { - if ($InputObject.ContainsKey($Property[$iProperty])) { - $PropertyValue = $InputObject[$Property[$iProperty]] - } - else { $PropertyValue = $null } - } - else { - $PropertyValue = Select-Object -InputObject $InputObject -ExpandProperty $Property[$iProperty] -ErrorAction Ignore - if ($null -eq $PropertyValue) { break } - } - ## Check for more nested properties - if ($iProperty -lt $Property.Count - 1) { - $InputObject = $PropertyValue - if ($null -eq $InputObject) { break } - } - else { - Write-Output $PropertyValue - } - } - } - } -} \ No newline at end of file diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index c2fabb44e4..6fb8ec0879 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -32,11 +32,13 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { } if ($all) { - $invitedUsers = Get-EntraBetaUser -Filter $guestFilter -All -Select Sponsors + #TODO: Change to Get-EntraBetaUser when -ExpandProperty is implemented + $invitedUsers = Get-MgUser -Filter $guestFilter -All -ExpandProperty Sponsors } else { foreach ($user in $userId) { - $invitedUsers += Get-EntraBetaUser -UserId $user -Select Sponsors + #TODO: Change to Get-EntraBetaUser when -ExpandProperty is implemented + $invitedUsers += Get-MgUser -UserId $user -ExpandProperty Sponsors } } @@ -49,7 +51,7 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { $splatArgumentsGetInvitedBy = @{ Method = 'Get' - Uri = ((Get-MgEnvironment -Name (Get-EntraBetaContext).Environment).GraphEndpoint + + Uri = ((Get-EntraEnvironment -Name (Get-EntraContext).Environment).GraphEndpoint + "/beta/users/" + $invitedUser.id + "/invitedBy") } @@ -133,3 +135,4 @@ function Get-ObjectPropertyValue { } } } + diff --git a/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md b/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md deleted file mode 100644 index d0c55549e5..0000000000 --- a/module/docs/entra-powershell-v1.0/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.md +++ /dev/null @@ -1,124 +0,0 @@ ---- -title: Update-EntraInvitedUserSponsorsFromInvitedBy -description: This article provides details on the Update-EntraInvitedUserSponsorsFromInvitedBy command. - -ms.topic: reference -ms.date: 02/11/2025 -ms.author: eunicewaweru -ms.reviewer: stevemutungi -manager: CelesteDG -author: msewaweru - -external help file: Microsoft.Entra.Users-Help.xml -Module Name: Microsoft.Entra -online version: https://learn.microsoft.com/powershell/module/Microsoft.Entra/Update-EntraInvitedUserSponsorsFromInvitedBy - -schema: 2.0.0 ---- - -# Update-EntraInvitedUserSponsorsFromInvitedBy - -## Synopsis - -Update the Sponsors attribute to include the user who initially invited them to the tenant using the InvitedBy property. - -## Syntax - -```powershell -Update-EntraInvitedUserSponsorsFromInvitedBy - [-UserId ] - [-All] - [] -``` - -## Description - -The `Update-EntraInvitedUserSponsorsFromInvitedBy` 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-EntraInvitedUserSponsorsFromInvitedBy -``` - -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-EntraInvitedUserSponsorsFromInvitedBy -UserId 'guestuser@contoso.com','guestuser1@contoso.com' -``` - -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-EntraInvitedUserSponsorsFromInvitedBy -All -``` - -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-EntraUser.md) - -[Set-EntraUser](Set-EntraUser.md) diff --git a/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 b/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 deleted file mode 100644 index 497919d8ad..0000000000 --- a/test/Entra/Users/Update-EntraInvitedUserSponsorsFromInvitedBy.Tests.ps1 +++ /dev/null @@ -1,66 +0,0 @@ -# ------------------------------------------------------------------------------ -# 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.Users) -eq $null) { - Import-Module Microsoft.Entra.Users - } - Import-Module (Join-Path $PSScriptRoot "..\..\Common-Functions.ps1") -Force - - Mock -CommandName Get-EntraUser -MockWith { - @{ Id = "123"; DisplayName = "Test Guest"; UserPrincipalName = "test@contoso.com"; Sponsors = $null } - } -ModuleName Microsoft.Entra.Users - Mock -CommandName Invoke-MgGraphRequest -MockWith { - @{ value = @{ id = "456" } } - } -ModuleName Microsoft.Entra.Users - Mock -CommandName Set-EntraUser -MockWith { $true } -ModuleName Microsoft.Entra.Users -} - -Describe "Update-EntraInvitedUserSponsorsFromInvitedBy" { - Context "Valid Inputs" { - It "Should update sponsor for a single user" { - Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor updated succesfully" - } - - It "Should process all invited users when -All is specified" { - Update-EntraInvitedUserSponsorsFromInvitedBy -All -Confirm:$false | Should -Match "Sponsor updated succesfully" - } - } - - Context "Invalid Inputs" { - It "Should throw an error when neither -UserId nor -All is provided" { - { Update-EntraInvitedUserSponsorsFromInvitedBy } | Should -Throw "Please specify either -UserId or -All" - } - } - - Context "Edge Cases" { - It "Should not update if sponsor already exists" { - Mock -CommandName Get-EntraUser -MockWith { - @{ Id = "123"; DisplayName = "Test Guest"; UserPrincipalName = "test@contoso.com"; Sponsors = @{ id = "456" } } - } -ModuleName Microsoft.Entra.Users - - Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor already exists" - } - - It "Should handle missing invitedBy information" { - Mock -CommandName Invoke-MgGraphRequest -MockWith { @{ value = $null } } -ModuleName Microsoft.Entra.Users - - Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Invited user information not available" - } - } - - Context "User-Agent Header" { - It "Should contain 'User-Agent' header" { - $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraInvitedUserSponsorsFromInvitedBy" - - Update-EntraInvitedUserSponsorsFromInvitedBy -UserId "123" - - Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.DirectoryManagement -Times 1 -ParameterFilter { - $Headers.'User-Agent' | Should -Be $userAgentHeaderValue - $true - } - } - } -} From a6a65877fbbff3c4cf753fc0962b3919665254e8 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Thu, 20 Feb 2025 07:57:20 +0300 Subject: [PATCH 10/21] Updates --- ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index 6fb8ec0879..11c4fb2bd6 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -9,13 +9,15 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { param ( # UserId of Guest User - [Parameter(ParameterSetName = 'ByUsers',HelpMessage ="The Unique ID of the User (User ID).")] + [Parameter(ParameterSetName = 'ByUsers', HelpMessage = "The Unique ID of the User (User ID).")] + [ValidateScript({ ($_ -ne $null -and $_.Count -gt 0) -or $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('All') })] [String[]] - $userId, + $UserId, + # Enumerate and Update All Guest Users. - [Parameter(ParameterSetName = 'AllInvitedGuests',HelpMessage="A Flag indicating whether to include all invited guests.")] + [Parameter(ParameterSetName = 'AllInvitedGuests', HelpMessage = "A Flag indicating whether to include all invited guests.")] [switch] - $all + $All ) begin { @@ -26,17 +28,12 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { $customHeaders = New-EntraBetaCustomHeaders -Command $MyInvocation.MyCommand - if ($null -eq $userId -and !$all) { - Write-Error "Please specify either -UserId or -All" - return - } - - if ($all) { + if ($All) { #TODO: Change to Get-EntraBetaUser when -ExpandProperty is implemented $invitedUsers = Get-MgUser -Filter $guestFilter -All -ExpandProperty Sponsors } else { - foreach ($user in $userId) { + foreach ($user in $UserId) { #TODO: Change to Get-EntraBetaUser when -ExpandProperty is implemented $invitedUsers += Get-MgUser -UserId $user -ExpandProperty Sponsors } From b8db6c5fbbe5b15d27fea0c86d61b3ef7ad23a58 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Tue, 18 Mar 2025 11:02:41 +0300 Subject: [PATCH 11/21] Inlined functioned --- ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 83 ++++++++++--------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index 11c4fb2bd6..ff2a651573 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -28,14 +28,55 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { $customHeaders = New-EntraBetaCustomHeaders -Command $MyInvocation.MyCommand + # Inline Helper function to extract deeply nested json values + function Get-ObjectPropertyValue { + [CmdletBinding()] + [OutputType([psobject])] + param ( + # Object containing property values + [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] + [AllowNull()] + [psobject] $InputObjects, + # Name of property. Specify an array of property names to tranverse nested objects. + [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] + [string[]] $Property + ) + + process { + foreach ($InputObject in $InputObjects) { + for ($iProperty = 0; $iProperty -lt $Property.Count; $iProperty++) { + ## Get property value + if ($InputObject -is [hashtable]) { + if ($InputObject.ContainsKey($Property[$iProperty])) { + $PropertyValue = $InputObject[$Property[$iProperty]] + } + else { $PropertyValue = $null } + } + else { + $PropertyValue = Select-Object -InputObject $InputObject -ExpandProperty $Property[$iProperty] -ErrorAction Ignore + if ($null -eq $PropertyValue) { break } + } + ## Check for more nested properties + if ($iProperty -lt $Property.Count - 1) { + $InputObject = $PropertyValue + if ($null -eq $InputObject) { break } + } + else { + Write-Output $PropertyValue + } + } + } + } + } + if ($All) { #TODO: Change to Get-EntraBetaUser when -ExpandProperty is implemented - $invitedUsers = Get-MgUser -Filter $guestFilter -All -ExpandProperty Sponsors + $invitedUsers = Get-MgBetaUser -Filter $guestFilter -All -ExpandProperty Sponsors } else { foreach ($user in $UserId) { #TODO: Change to Get-EntraBetaUser when -ExpandProperty is implemented - $invitedUsers += Get-MgUser -UserId $user -ExpandProperty Sponsors + $invitedUsers += Get-MgBetaUser -UserId $user -ExpandProperty Sponsors } } @@ -93,43 +134,5 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { } } -function Get-ObjectPropertyValue { - [CmdletBinding()] - [OutputType([psobject])] - param ( - # Object containing property values - [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] - [AllowNull()] - [psobject] $InputObjects, - # Name of property. Specify an array of property names to tranverse nested objects. - [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] - [string[]] $Property - ) - process { - foreach ($InputObject in $InputObjects) { - for ($iProperty = 0; $iProperty -lt $Property.Count; $iProperty++) { - ## Get property value - if ($InputObject -is [hashtable]) { - if ($InputObject.ContainsKey($Property[$iProperty])) { - $PropertyValue = $InputObject[$Property[$iProperty]] - } - else { $PropertyValue = $null } - } - else { - $PropertyValue = Select-Object -InputObject $InputObject -ExpandProperty $Property[$iProperty] -ErrorAction Ignore - if ($null -eq $PropertyValue) { break } - } - ## Check for more nested properties - if ($iProperty -lt $Property.Count - 1) { - $InputObject = $PropertyValue - if ($null -eq $InputObject) { break } - } - else { - Write-Output $PropertyValue - } - } - } - } -} From 54b8374cc111bf0c9899cb39324f44524f1165c0 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Tue, 18 Mar 2025 11:07:26 +0300 Subject: [PATCH 12/21] Beta updates --- .../Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index ff2a651573..d5f60531be 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -109,7 +109,7 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } $sponsorsRequestBody = $dirObj | ConvertTo-Json - Set-EntraUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders + Set-EntraBetaUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders Write-Output "$($invitedUser.userPrincipalName) - Sponsor updated successfully for this user." } catch { From 61194f2ea7c8da01a109afdfc6926e1ae9bca446 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:52:35 +0300 Subject: [PATCH 13/21] added examples to ocs --- ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 2 +- ...traBetaInvitedUserSponsorsFromInvitedBy.md | 36 ++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index d5f60531be..f34f31e808 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -109,7 +109,7 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } $sponsorsRequestBody = $dirObj | ConvertTo-Json - Set-EntraBetaUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders + Update-MgBetaUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders Write-Output "$($invitedUser.userPrincipalName) - Sponsor updated successfully for this user." } catch { diff --git a/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md index 828c2a2a5e..38b1fdc85d 100644 --- a/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md +++ b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md @@ -20,7 +20,7 @@ schema: 2.0.0 ## Synopsis -Update the Sponsors attribute to include the user who initially invited them to the tenant using the InvitedBy property. +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 @@ -49,6 +49,18 @@ The calling user must be assigned at least one of the following Microsoft Entra 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 @@ -58,6 +70,16 @@ Connect-Entra -Scopes 'User.ReadWrite.All' Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId 'guestuser@contoso.com','guestuser1@contoso.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 @@ -67,6 +89,18 @@ 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 From d043f9e0281fecb597af61d7ac06345bf7710403 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:59:04 +0300 Subject: [PATCH 14/21] added examples to ocs --- .../Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md index 38b1fdc85d..884a96f25b 100644 --- a/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md +++ b/module/docs/entra-powershell-beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.md @@ -67,7 +67,7 @@ Enumerate all invited users in the Tenant and update Sponsors using InvitedBy va ```powershell Connect-Entra -Scopes 'User.ReadWrite.All' -Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId 'guestuser@contoso.com','guestuser1@contoso.com' +Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId 'externaluser1_externaldomain.com','externaluser1_externaldomain.com' ``` ```Output From 564adae6e90668b113408415f9c4a34b9477af14 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Fri, 21 Mar 2025 11:48:37 +0300 Subject: [PATCH 15/21] Removed Get-ObjectPropertyValue since we are only checking for the 'id' value and hence we don't the inline function. Updated tests --- ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 54 +++---------------- ...nvitedUserSponsorsFromInvitedBy.Tests.ps1} | 47 ++++++++-------- 2 files changed, 34 insertions(+), 67 deletions(-) rename test/EntraBeta/Users/{Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 => Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1} (58%) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index f34f31e808..f559304017 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -10,7 +10,6 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { # UserId of Guest User [Parameter(ParameterSetName = 'ByUsers', HelpMessage = "The Unique ID of the User (User ID).")] - [ValidateScript({ ($_ -ne $null -and $_.Count -gt 0) -or $PSCmdlet.MyInvocation.BoundParameters.ContainsKey('All') })] [String[]] $UserId, @@ -28,47 +27,10 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { $customHeaders = New-EntraBetaCustomHeaders -Command $MyInvocation.MyCommand - # Inline Helper function to extract deeply nested json values - function Get-ObjectPropertyValue { - [CmdletBinding()] - [OutputType([psobject])] - param ( - # Object containing property values - [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] - [AllowNull()] - [psobject] $InputObjects, - # Name of property. Specify an array of property names to tranverse nested objects. - [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] - [string[]] $Property - ) - - process { - foreach ($InputObject in $InputObjects) { - for ($iProperty = 0; $iProperty -lt $Property.Count; $iProperty++) { - ## Get property value - if ($InputObject -is [hashtable]) { - if ($InputObject.ContainsKey($Property[$iProperty])) { - $PropertyValue = $InputObject[$Property[$iProperty]] - } - else { $PropertyValue = $null } - } - else { - $PropertyValue = Select-Object -InputObject $InputObject -ExpandProperty $Property[$iProperty] -ErrorAction Ignore - if ($null -eq $PropertyValue) { break } - } - ## Check for more nested properties - if ($iProperty -lt $Property.Count - 1) { - $InputObject = $PropertyValue - if ($null -eq $InputObject) { break } - } - else { - Write-Output $PropertyValue - } - } - } - } + if ((-not $UserId -or $UserId.Count -eq 0) -and -not $All) { + throw "Please specify either -UserId or -All" } - + $invitedUsers=@() if ($All) { #TODO: Change to Get-EntraBetaUser when -ExpandProperty is implemented $invitedUsers = Get-MgBetaUser -Filter $guestFilter -All -ExpandProperty Sponsors @@ -86,18 +48,18 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { else { foreach ($invitedUser in $invitedUsers) { $invitedBy = $null - + $environment=(Get-EntraContext).Environment + $baseUri=(Get-EntraEnvironment -Name $environment).GraphEndpoint $splatArgumentsGetInvitedBy = @{ Method = 'Get' - Uri = ((Get-EntraEnvironment -Name (Get-EntraContext).Environment).GraphEndpoint + - "/beta/users/" + $invitedUser.id + "/invitedBy") + Uri = $baseUri +"/beta/users/" + $invitedUser.id + "/invitedBy" } - $invitedBy = Invoke-MgGraphRequest @splatArgumentsGetInvitedBy -Headers $customHeaders + $invitedBy = Invoke-GraphRequest @splatArgumentsGetInvitedBy -Headers $customHeaders Write-Verbose ($invitedBy | ConvertTo-Json -Depth 10) - if ($null -ne $invitedBy -and $null -ne $invitedBy.value -and $null -ne (Get-ObjectPropertyValue $invitedBy.value -Property 'id')) { + if ($null -ne $invitedBy -and $null -ne $invitedBy.value -and $null -ne $invitedBy.value.id) { Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $invitedUser.displayName, $invitedBy.value.id) if (($null -like $invitedUser.sponsors) -or ($invitedUser.sponsors.id -notcontains $invitedBy.value.id)) { diff --git a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 similarity index 58% rename from test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 rename to test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 index f83d909822..4d80c0910b 100644 --- a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests..ps1 +++ b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 @@ -9,43 +9,48 @@ BeforeAll { } Import-Module (Join-Path $PSScriptRoot "..\..\Common-Functions.ps1") -Force - Mock -CommandName Get-EntraBetaUser -MockWith { - @{ Id = "123"; DisplayName = "Test Guest"; UserPrincipalName = "test@contoso.com"; Sponsors = $null } + 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 + + Mock -CommandName Get-MgBetaUser -MockWith { + [PSCustomObject]@{ + Id = "123" + DisplayName = "Test Guest" + UserPrincipalName = "test@contoso.com" + Sponsors = $null + } } -ModuleName Microsoft.Entra.Beta.Users - Mock -CommandName Invoke-MgGraphRequest -MockWith { - @{ value = @{ id = "456" } } - } -ModuleName Microsoft.Graph.Beta.Users + + Mock -CommandName Invoke-GraphRequest -MockWith { + return @{ value = @{ id = "456" } } + } -ModuleName Microsoft.Entra.Beta.Users + Mock -CommandName Set-EntraBetaUser -MockWith { $true } -ModuleName Microsoft.Entra.Beta.Users + + Mock -CommandName Update-MgBetaUser -MockWith {"Sponsor updated successfully for user 123"} -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 succesfully" + 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 succesfully" + 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 } | Should -Throw "Please specify either -UserId or -All" + { Update-EntraBetaInvitedUserSponsorsFromInvitedBy -Confirm:$false} | Should -Throw "Please specify either -UserId or -All" } } Context "Edge Cases" { - It "Should not update if sponsor already exists" { - Mock -CommandName Get-EntraBetaUser -MockWith { - @{ Id = "123"; DisplayName = "Test Guest"; UserPrincipalName = "test@contoso.com"; Sponsors = @{ id = "456" } } - } - - Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor already exists" - } - It "Should handle missing invitedBy information" { - Mock -CommandName Invoke-MgGraphRequest -MockWith { @{ value = $null } } -ModuleName Microsoft.Entra.Beta.Users + Mock -CommandName Invoke-GraphRequest -MockWith { @{ value = $null } } -ModuleName Microsoft.Entra.Beta.Users Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Invited user information not available" } @@ -53,14 +58,14 @@ Describe "Update-EntraBetaInvitedUserSponsorsFromInvitedBy" { Context "User-Agent Header" { It "Should contain 'User-Agent' header" { - $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraInvitedUserSponsorsFromInvitedBy" + $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" - Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false - Should -Invoke -CommandName Invoke-MgGraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 -ParameterFilter { + Should -Invoke -CommandName Invoke-GraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 -ParameterFilter { $Headers.'User-Agent' | Should -Be $userAgentHeaderValue $true } } } -} +} \ No newline at end of file From 79e31571d66607e9c7ece447d105cc24e1d88989 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Fri, 21 Mar 2025 11:51:01 +0300 Subject: [PATCH 16/21] Delete module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAccessActivity.ps1 --- .../Get-EntraCrossTenantAccessActivity.ps1 | 500 ------------------ 1 file changed, 500 deletions(-) delete mode 100644 module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAccessActivity.ps1 diff --git a/module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAccessActivity.ps1 b/module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAccessActivity.ps1 deleted file mode 100644 index 01f726d073..0000000000 --- a/module/Entra/Microsoft.Entra/Reports/Get-EntraCrossTenantAccessActivity.ps1 +++ /dev/null @@ -1,500 +0,0 @@ -function Get-EntraCrossTenantAccessActivity { - [CmdletBinding()] - param( - - #Return events based on external tenant access direction, either 'Inbound', 'Outbound', or 'Both' - [Parameter(Position = 0)] - [ValidateSet('Inbound', 'Outbound')] - [string]$AccessDirection, - - #Return events for the supplied external tenant ID - [Parameter(Position = 1)] - [guid]$ExternalTenantId, - - #Show summary statistics by tenant - [switch]$SummaryStats, - - #Atemmpt to resolve the external tenant ID - [switch]$ResolveTenantId - - ) - - begin { - ## Initialize Critical Dependencies - function Test-MgCommandPrerequisites { - [CmdletBinding()] - [OutputType([bool])] - param ( - # The name of a command. - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] - [Alias('Command')] - [string[]] $Name, - # The service API version. - [Parameter(Mandatory = $false, Position = 2)] - [ValidateSet('v1.0')] - [string] $ApiVersion = 'v1.0', - # Specifies a minimum version. - [Parameter(Mandatory = $false)] - [version] $MinimumVersion, - # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. - [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] - [switch] $RequireListPermissions - ) - - begin { - [version] $MgAuthenticationModuleVersion = $null - $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" - if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { - $MgAuthenticationModuleVersion = $Matches[2] - } - else { - $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version - } - Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" - } - - process { - ## Initialize - $result = $true - - ## Get Graph Command Details - [hashtable] $MgCommandLookup = @{} - foreach ($CommandName in $Name) { - - [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion -ErrorAction Break - - if ($MgCommands.Count -eq 1) { - $MgCommand = $MgCommands[0] - } - elseif ($MgCommands.Count -gt 1) { - $MgCommand = $MgCommands[0] - ## Resolve from multiple results - [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null - [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" - [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" - if ($MgCommandsWithListPermissions -and $RequireListPermissions) { - $MgCommand = $MgCommandsWithListPermissions[0] - } - elseif ($MgCommandsWithGetPermissions) { - $MgCommand = $MgCommandsWithGetPermissions[0] - } - else { - $MgCommand = $MgCommands[0] - } - } - - if ($MgCommand) { - $MgCommandLookup[$MgCommand.Command] = $MgCommand - } - else { - Write-Error "Unable to resolve a specific command for '$CommandName'." - } - } - - ## Import Required Modules - [string[]] $MgModules = @() - foreach ($MgCommand in $MgCommandLookup.Values) { - if (!$MgModules.Contains($MgCommand.Module)) { - $MgModules += $MgCommand.Module - [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" - try { - if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { - ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - } - else { - ## Load module to match currently loaded Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - } - } - catch { - $result = $false - Write-Error -ErrorRecord $_ - } - } - } - Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) - - ## Check MgModule Connection - $MgContext = Get-MgContext - if ($MgContext) { - if ($MgContext.AuthType -eq 'Delegated') { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' - $result = $false - } - } - } - else { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" - } - } - } - } - else { - $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' - $result = $false - } - - return $result - } - } - $CriticalError = $null - if (!(Test-MgCommandPrerequisites 'Get-MgAuditLogSignIn' -MinimumVersion 2.8.0 -ErrorVariable CriticalError)) { return } - - #External Tenant ID check - - if ($ExternalTenantId) { - - Write-Verbose -Message "$(Get-Date -f T) - Checking supplied external tenant ID - $ExternalTenantId..." - - if ($ExternalTenantId -eq (Get-MgContext).TenantId) { - - Write-Error "$(Get-Date -f T) - Supplied external tenant ID ($ExternalTenantId) cannot match connected tenant ID ($((Get-MgContext).TenantId)))" -ErrorAction Stop - - } - else { - - Write-Verbose -Message "$(Get-Date -f T) - Supplied external tenant ID OK" - } - - } - - } - - process { - ## Return Immediately On Critical Error - if ($CriticalError) { return } - - #Get filtered sign-in logs and handle parameters - - if ($AccessDirection -eq "Outbound") { - - if ($ExternalTenantId) { - - Write-Verbose -Message "$(Get-Date -f T) - Access direction 'Outbound' selected" - Write-Verbose -Message "$(Get-Date -f T) - Outbound: getting sign-ins for local users accessing external tenant ID - $ExternalTenantId" - - $SignIns = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and ResourceTenantId eq '{0}'" -f $ExternalTenantId) -All | Group-Object ResourceTenantID - - } - else { - - Write-Verbose -Message "$(Get-Date -f T) - Access direction 'Outbound' selected" - Write-Verbose -Message "$(Get-Date -f T) - Outbound: getting external tenant IDs accessed by local users" - - $SignIns = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and ResourceTenantId ne '{0}'" -f (Get-MgContext).TenantId) -All | Group-Object ResourceTenantID - - } - - } - elseif ($AccessDirection -eq 'Inbound') { - - if ($ExternalTenantId) { - - Write-Verbose -Message "$(Get-Date -f T) - Access direction 'Inbound' selected" - Write-Verbose -Message "$(Get-Date -f T) - Inbound: getting sign-ins for users accessing local tenant from external tenant ID - $ExternalTenantId" - - $SignIns = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and HomeTenantId eq '{0}' and TokenIssuerType eq 'AzureAD'" -f $ExternalTenantId) -All | Group-Object HomeTenantID - - } - else { - - Write-Verbose -Message "$(Get-Date -f T) - Access direction 'Inbound' selected" - Write-Verbose -Message "$(Get-Date -f T) - Inbound: getting external tenant IDs for external users accessing local tenant" - - $SignIns = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and HomeTenantId ne '{0}' and TokenIssuerType eq 'AzureAD'" -f (Get-MgContext).TenantId) -All | Group-Object HomeTenantID - - } - } - else { - - if ($ExternalTenantId) { - - Write-Verbose -Message "$(Get-Date -f T) - Default access direction 'Both'" - Write-Verbose -Message "$(Get-Date -f T) - Outbound: getting sign-ins for local users accessing external tenant ID - $ExternalTenantId" - - $Outbound = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and ResourceTenantId eq '{0}'" -f $ExternalTenantId) -All | Group-Object ResourceTenantID - - - Write-Verbose -Message "$(Get-Date -f T) - Inbound: getting sign-ins for users accessing local tenant from external tenant ID - $ExternalTenantId" - - $Inbound = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and HomeTenantId eq '{0}' and TokenIssuerType eq 'AzureAD'" -f $ExternalTenantId) -All | Group-Object HomeTenantID - } - else { - - Write-Verbose -Message "$(Get-Date -f T) - Default access direction 'Both'" - Write-Verbose -Message "$(Get-Date -f T) - Outbound: getting external tenant IDs accessed by local users" - - $Outbound = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and ResourceTenantId ne '{0}'" -f (Get-MgContext).TenantId) -All | Group-Object ResourceTenantID - - - Write-Verbose -Message "$(Get-Date -f T) - Inbound: getting external tenant IDs for external users accessing local tenant" - - $Inbound = Get-MgAuditLogSignIn -Filter ("CrossTenantAccessType ne 'none' and HomeTenantId ne '{0}' and TokenIssuerType eq 'AzureAD'" -f (Get-MgContext).TenantId) -All | Group-Object HomeTenantID - } - - #Combine outbound and inbound results - - [array]$SignIns = $Outbound - $SignIns += $Inbound - } - #Analyse sign-in logs - - Write-Verbose -Message "$(Get-Date -f T) - Checking for sign-ins..." - - if ($SignIns) { - - Write-Verbose -Message "$(Get-Date -f T) - Sign-ins obtained" - Write-Verbose -Message "$(Get-Date -f T) - Iterating Sign-ins..." - - foreach ($TenantID in $SignIns) { - - #Handle resolving tenant ID - - if ($ResolveTenantId) { - - Write-Verbose -Message "$(Get-Date -f T) - Attempting to resolve external tenant - $($TenantId.Name)" - - #Nullify $ResolvedTenant value - - $ResolvedTenant = $null - - #Attempt to resolve tenant ID - - try { $ResolvedTenant = Resolve-MsIdTenant -TenantId $TenantId.Name -ErrorAction Stop } - catch { Write-Warning $_.Exception.Message; Write-Verbose -Message "$(Get-Date -f T) - Issue resolving external tenant - $($TenantId.Name)" } - - if ($ResolvedTenant) { - - if ($ResolvedTenant.Result -eq 'Resolved') { - - $ExternalTenantName = $ResolvedTenant.DisplayName - $DefaultDomainName = $ResolvedTenant.DefaultDomainName - } - else { - $ExternalTenantName = $ResolvedTenant.Result - $DefaultDomainName = $ResolvedTenant.Result - } - - if ($ResolvedTenant.oidcMetadataResult -eq 'Resolved') { - - $oidcMetadataTenantRegionScope = $ResolvedTenant.oidcMetadataTenantRegionScope - } - else { - - $oidcMetadataTenantRegionScope = 'NotFound' - - } - - } - else { - - $ExternalTenantName = "NotFound" - $DefaultDomainName = "NotFound" - $oidcMetadataTenantRegionScope = 'NotFound' - } - - } - #Handle access direction - - if (($AccessDirection -eq 'Inbound') -or ($AccessDirection -eq 'Outbound')) { - - $Direction = $AccessDirection - - } - else { - - if ($TenantID.Name -eq $TenantID.Group[0].HomeTenantId) { - - $Direction = "Inbound" - - } - elseif ($TenantID.Name -eq $TenantID.Group[0].ResourceTenantId) { - - $Direction = "Outbound" - - } - - } - - #Provide summary - - if ($SummaryStats) { - - Write-Verbose -Message "$(Get-Date -f T) - Creating summary stats for external tenant - $($TenantId.Name)" - - #Handle resolving tenant ID - - if ($ResolveTenantId) { - - $Analysis = [pscustomobject]@{ - - ExternalTenantId = $TenantId.Name - ExternalTenantName = $ExternalTenantName - ExternalTenantRegionScope = $oidcMetadataTenantRegionScope - AccessDirection = $Direction - SignIns = ($TenantId).count - SuccessSignIns = ($TenantID.Group.Status | Where-Object { $_.ErrorCode -eq 0 } | Measure-Object).count - FailedSignIns = ($TenantID.Group.Status | Where-Object { $_.ErrorCode -ne 0 } | Measure-Object).count - UniqueUsers = ($TenantID.Group | Select-Object UserId -Unique | Measure-Object).count - UniqueResources = ($TenantID.Group | Select-Object ResourceId -Unique | Measure-Object).count - - } - - } - else { - - #Build custom output object - - $Analysis = [pscustomobject]@{ - - ExternalTenantId = $TenantId.Name - AccessDirection = $Direction - SignIns = ($TenantId).count - SuccessSignIns = ($TenantID.Group.Status | Where-Object { $_.ErrorCode -eq 0 } | Measure-Object).count - FailedSignIns = ($TenantID.Group.Status | Where-Object { $_.ErrorCode -ne 0 } | Measure-Object).count - UniqueUsers = ($TenantID.Group | Select-Object UserId -Unique | Measure-Object).count - UniqueResources = ($TenantID.Group | Select-Object ResourceId -Unique | Measure-Object).count - - } - - - } - - Write-Verbose -Message "$(Get-Date -f T) - Adding stats for $($TenantId.Name) to total analysis object" - - [array]$TotalAnalysis += $Analysis - - } - else { - - #Get individual events by external tenant - - Write-Verbose -Message "$(Get-Date -f T) - Getting individual sign-in events for external tenant - $($TenantId.Name)" - - - foreach ($Event in $TenantID.group) { - - - if ($ResolveTenantId) { - - $CustomEvent = [pscustomobject]@{ - - ExternalTenantId = $TenantId.Name - ExternalTenantName = $ExternalTenantName - ExternalDefaultDomain = $DefaultDomainName - ExternalTenantRegionScope = $oidcMetadataTenantRegionScope - AccessDirection = $Direction - UserDisplayName = $Event.UserDisplayName - UserPrincipalName = $Event.UserPrincipalName - UserId = $Event.UserId - UserType = $Event.UserType - CrossTenantAccessType = $Event.CrossTenantAccessType - AppDisplayName = $Event.AppDisplayName - AppId = $Event.AppId - ResourceDisplayName = $Event.ResourceDisplayName - ResourceId = $Event.ResourceId - SignInId = $Event.Id - CreatedDateTime = $Event.CreatedDateTime - StatusCode = $Event.Status.Errorcode - StatusReason = $Event.Status.FailureReason - - - } - - $CustomEvent - - } - else { - - $CustomEvent = [pscustomobject]@{ - - ExternalTenantId = $TenantId.Name - AccessDirection = $Direction - UserDisplayName = $Event.UserDisplayName - UserPrincipalName = $Event.UserPrincipalName - UserId = $Event.UserId - UserType = $Event.UserType - CrossTenantAccessType = $Event.CrossTenantAccessType - AppDisplayName = $Event.AppDisplayName - AppId = $Event.AppId - ResourceDisplayName = $Event.ResourceDisplayName - ResourceId = $Event.ResourceId - SignInId = $Event.Id - CreatedDateTime = $Event.CreatedDateTime - StatusCode = $Event.Status.Errorcode - StatusReason = $Event.Status.FailureReason - } - - $CustomEvent - - } - } - - } - - } - - } - else { - - Write-Warning "$(Get-Date -f T) - No sign-ins matching the selected criteria found." - - } - - #Display summary table - - if ($SummaryStats) { - - #Show array of summary objects for each external tenant - - Write-Verbose -Message "$(Get-Date -f T) - Displaying total analysis object" - - if (!$AccessDirection) { - - $TotalAnalysis | Sort-Object ExternalTenantId - - } - else { - - $TotalAnalysis | Sort-Object SignIns -Descending - - } - - } - - } - - end { - if ($CriticalError) { return } - } -} - From c624de3313d628c0b3a71390551e2fafab90cdd3 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Fri, 21 Mar 2025 11:51:20 +0300 Subject: [PATCH 17/21] Delete module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaCommandPrerequisite.ps1 --- .../Test-EntraBetaCommandPrerequisite.ps1 | 150 ------------------ 1 file changed, 150 deletions(-) delete mode 100644 module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaCommandPrerequisite.ps1 diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaCommandPrerequisite.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaCommandPrerequisite.ps1 deleted file mode 100644 index fbe520199c..0000000000 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Test-EntraBetaCommandPrerequisite.ps1 +++ /dev/null @@ -1,150 +0,0 @@ - # ------------------------------------------------------------------------------ -# Copyright (c) Microsoft Corporation. All Rights Reserved. -# Licensed under the MIT License. See License in the project root for license information. -# ------------------------------------------------------------------------------ - function Test-EntraBetaCommandPrerequisites { - [CmdletBinding()] - [OutputType([bool])] - param ( - # The name of a command. - [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 1)] - [Alias('Command')] - [string[]] $Name, - # The service API version. - [Parameter(Mandatory = $false, Position = 2)] - [ValidateSet('v1.0')] - [string] $ApiVersion = 'v1.0', - # Specifies a minimum version. - [Parameter(Mandatory = $false)] - [version] $MinimumVersion, - # Require "list" permissions rather than "get" permissions when Get-Mg* commands are specified. - [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] - [switch] $RequireListPermissions - ) - - begin { - [version] $MgAuthenticationModuleVersion = $null - $Assembly = [System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object FullName -Like "Microsoft.Graph.Authentication,*" - if ($Assembly.FullName -match "Version=(([0-9]+.[0-9]+.[0-9]+).[0-9]+),") { - $MgAuthenticationModuleVersion = $Matches[2] - } - else { - $MgAuthenticationModuleVersion = Get-Command 'Connect-MgGraph' -Module 'Microsoft.Graph.Authentication' | Select-Object -ExpandProperty Version - } - Write-Debug "Microsoft.Graph.Authentication module version loaded: $MgAuthenticationModuleVersion" - } - - process { - ## Initialize - $result = $true - - ## Get Graph Command Details - [hashtable] $MgCommandLookup = @{} - foreach ($CommandName in $Name) { - - [array] $MgCommands = Find-MgGraphCommand -Command $CommandName -ApiVersion $ApiVersion - - if ($MgCommands.Count -eq 1) { - $MgCommand = $MgCommands[0] - } - elseif ($MgCommands.Count -gt 1) { - $MgCommand = $MgCommands[0] - ## Resolve from multiple results - [array] $MgCommandsWithPermissions = $MgCommands | Where-Object Permissions -NE $null - [array] $MgCommandsWithListPermissions = $MgCommandsWithPermissions | Where-Object URI -NotLike "*}" - [array] $MgCommandsWithGetPermissions = $MgCommandsWithPermissions | Where-Object URI -Like "*}" - if ($MgCommandsWithListPermissions -and $RequireListPermissions) { - $MgCommand = $MgCommandsWithListPermissions[0] - } - elseif ($MgCommandsWithGetPermissions) { - $MgCommand = $MgCommandsWithGetPermissions[0] - } - else { - $MgCommand = $MgCommands[0] - } - } - - if ($MgCommand) { - $MgCommandLookup[$MgCommand.Command] = $MgCommand - } - else { - Write-Error "Unable to resolve a specific command for '$CommandName'." - } - } - - ## Import Required Modules - [string[]] $MgModules = @() - foreach ($MgCommand in $MgCommandLookup.Values) { - if (!$MgModules.Contains($MgCommand.Module)) { - $MgModules += $MgCommand.Module - [string] $ModuleName = "Microsoft.Graph.$($MgCommand.Module)" - try { - if ($MgAuthenticationModuleVersion -lt $MinimumVersion) { - ## Check for newer module but load will likely fail due to old Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -MinimumVersion $MinimumVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' with minimum version '{1}' was found but currently loaded 'Microsoft.Graph.Authentication' module is version '{2}'. To resolve, try opening a new PowerShell session and running the command again." -f $ModuleName, $MinimumVersion, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Import-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with minimum version '{1}' not found. To resolve, try installing module '{0}' with the latest version. For example: Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -MinimumVersion '{1}'" -f $ModuleName, $MinimumVersion) - } - } - else { - ## Load module to match currently loaded Microsoft.Graph.Authentication module - try { - Import-Module $ModuleName -RequiredVersion $MgAuthenticationModuleVersion -Scope Global -ErrorAction Stop -Verbose:$false - } - catch [System.IO.FileLoadException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleOutOfDate' -Message ("The module '{0}' was found but is not a compatible version. To resolve, try updating module '{0}' to version '{1}' to match currently loaded modules. For example: Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Update-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - catch [System.IO.FileNotFoundException] { - $result = $false - Write-Error -Exception $_.Exception -Category ResourceUnavailable -ErrorId 'MgModuleWithVersionNotFound' -Message ("The module '{0}' with version '{1}' not found. To resolve, try installing module '{0}' with version '{1}' to match currently loaded modules. For example: Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) -TargetObject $ModuleName -RecommendedAction ("Install-Module {0} -RequiredVersion '{1}'" -f $ModuleName, $MgAuthenticationModuleVersion) - } - } - } - catch { - $result = $false - Write-Error -ErrorRecord $_ - } - } - } - Write-Verbose ('Required Microsoft Graph Modules: {0}' -f (($MgModules | ForEach-Object { "Microsoft.Graph.$_" }) -join ', ')) - - ## Check MgModule Connection - $MgContext = Get-MgContext - if ($MgContext) { - if ($MgContext.AuthType -eq 'Delegated') { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - $Exception = New-Object System.Security.SecurityException -ArgumentList "Additional scope required for command '$($MgCommand.Command)', call Connect-MgGraph with one of the following scopes: $($MgCommand.Permissions.Name -join ', ')" - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::PermissionDenied) -ErrorId 'MgScopePermissionRequired' - $result = $false - } - } - } - else { - ## Check MgModule Consented Scopes - foreach ($MgCommand in $MgCommandLookup.Values) { - if ($MgCommand.Permissions -and (!$MgContext.Scopes -or !(Compare-Object $MgCommand.Permissions.Name -DifferenceObject $MgContext.Scopes -ExcludeDifferent -IncludeEqual))) { - Write-Warning "Additional scope may be required for command '$($MgCommand.Command), add and consent ClientId '$($MgContext.ClientId)' to one of the following app scopes: $($MgCommand.Permissions.Name -join ', ')" - } - } - } - } - else { - $Exception = New-Object System.Security.Authentication.AuthenticationException -ArgumentList "Authentication needed, call Connect-MgGraph." - Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::AuthenticationError) -CategoryReason 'AuthenticationException' -ErrorId 'MgAuthenticationRequired' - $result = $false - } - - return $result - } -} - From 2fff099a18897266b334aeb85fd377504ed61d2d Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Wed, 26 Mar 2025 07:23:32 +0300 Subject: [PATCH 18/21] compute once --- .../Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index f559304017..3663ccdaa6 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -26,6 +26,8 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { process { $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" @@ -48,8 +50,6 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { else { foreach ($invitedUser in $invitedUsers) { $invitedBy = $null - $environment=(Get-EntraContext).Environment - $baseUri=(Get-EntraEnvironment -Name $environment).GraphEndpoint $splatArgumentsGetInvitedBy = @{ Method = 'Get' Uri = $baseUri +"/beta/users/" + $invitedUser.id + "/invitedBy" From 7ae84d452ffa620be9bf96a0329fb8e0b5a232c2 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:39:34 +0300 Subject: [PATCH 19/21] Migrate to use Graph API --- ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 106 ++++++++++-------- 1 file changed, 61 insertions(+), 45 deletions(-) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index 3663ccdaa6..19b16b8ce2 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -7,87 +7,103 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { ConfirmImpact = 'High', DefaultParameterSetName = 'AllInvitedGuests')] param ( - - # UserId of Guest User [Parameter(ParameterSetName = 'ByUsers', HelpMessage = "The Unique ID of the User (User ID).")] - [String[]] - $UserId, + [String[]] $UserId, - # Enumerate and Update All Guest Users. [Parameter(ParameterSetName = 'AllInvitedGuests', HelpMessage = "A Flag indicating whether to include all invited guests.")] - [switch] - $All + [switch] $All ) - begin { + begin { $guestFilter = "(CreationType eq 'Invitation')" } process { - $customHeaders = New-EntraBetaCustomHeaders -Command $MyInvocation.MyCommand - $environment=(Get-EntraContext).Environment - $baseUri=(Get-EntraEnvironment -Name $environment).GraphEndpoint + $environment = (Get-EntraContext).Environment + $baseUri = (Get-EntraEnvironment -Name $environment).GraphEndpoint - if ((-not $UserId -or $UserId.Count -eq 0) -and -not $All) { + if ((-not $UserId -or $UserId.Count -eq 0) -and -not $All) { throw "Please specify either -UserId or -All" } - $invitedUsers=@() + + $invitedUsers = @() + $uri = $baseUri+"/beta/users?$filter=$($guestFilter)&$expand=sponsors" + if ($All) { - #TODO: Change to Get-EntraBetaUser when -ExpandProperty is implemented - $invitedUsers = Get-MgBetaUser -Filter $guestFilter -All -ExpandProperty Sponsors + $invitedUsers = (Invoke-GraphRequest -Method GET -Uri $uri).value } else { foreach ($user in $UserId) { - #TODO: Change to Get-EntraBetaUser when -ExpandProperty is implemented - $invitedUsers += Get-MgBetaUser -UserId $user -ExpandProperty Sponsors + $userUri = $baseUri+"/beta/users/$user`?\$expand=sponsors" + $invitedUsers += (Invoke-GraphRequest -Method GET -Uri $userUri).value } } - if ($null -eq $invitedUsers) { + if (-not $invitedUsers) { Write-Error "No guest users to process" + return } - else { - foreach ($invitedUser in $invitedUsers) { - $invitedBy = $null - $splatArgumentsGetInvitedBy = @{ - Method = 'Get' - Uri = $baseUri +"/beta/users/" + $invitedUser.id + "/invitedBy" - } - $invitedBy = Invoke-GraphRequest @splatArgumentsGetInvitedBy -Headers $customHeaders + 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) + Write-Verbose ($invitedBy | ConvertTo-Json -Depth 10) - if ($null -ne $invitedBy -and $null -ne $invitedBy.value -and $null -ne $invitedBy.value.id) { - Write-Verbose ("InvitedBy for Guest User {0}: {1}" -f $invitedUser.displayName, $invitedBy.value.id) + 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) - if (($null -like $invitedUser.sponsors) -or ($invitedUser.sponsors.id -notcontains $invitedBy.value.id)) { - Write-Verbose "Sponsors does not contain the user who invited them!" + # Get current sponsors + $currentSponsorIds = @() + if ($invitedUser.sponsors) { + foreach ($s in $invitedUser.sponsors) { + if ($s.id) { + $currentSponsorIds += $s.id + } + } + } - if ($PSCmdlet.ShouldProcess(("$($invitedUser.displayName) ($($invitedUser.userPrincipalName) - $($invitedUser.id))"), "Update Sponsors")) { - try { - $sponsorUrl = ("https://graph.microsoft.com/beta/users/{0}" -f $invitedBy.value.id) - $dirObj = @{"sponsors@odata.bind" = @($sponsorUrl) } - $sponsorsRequestBody = $dirObj | ConvertTo-Json + if (-not ($currentSponsorIds -contains $inviterId)) { + Write-Verbose "Sponsors does not contain the user who invited them!" - Update-MgBetaUser -UserId $invitedUser.id -BodyParameter $sponsorsRequestBody -Header $customHeaders - Write-Output "$($invitedUser.userPrincipalName) - Sponsor updated successfully for this user." + 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) } - catch { - Write-Output "$($invitedUser.userPrincipalName) - Failed updating sponsor for this user." - Write-Error $_ + $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 for this user." - } } else { - Write-Output "$($invitedUser.userPrincipalName) - Invited user information not available for this user." + Write-Output "$($invitedUser.userPrincipalName) - Sponsor already exists." } } + else { + Write-Output "$($invitedUser.userPrincipalName) - InvitedBy info not found." + } } } From dd7c32fa628a248bb844a721907dace8877efdd4 Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Tue, 1 Apr 2025 09:33:45 +0300 Subject: [PATCH 20/21] update to use Graph API and update to tests --- ...raBetaInvitedUserSponsorsFromInvitedBy.ps1 | 8 +- ...InvitedUserSponsorsFromInvitedBy.Tests.ps1 | 103 ++++++++++++++---- 2 files changed, 82 insertions(+), 29 deletions(-) diff --git a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 index 19b16b8ce2..f7c529af72 100644 --- a/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 +++ b/module/EntraBeta/Microsoft.Entra.Beta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.ps1 @@ -14,11 +14,9 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { [switch] $All ) - begin { - $guestFilter = "(CreationType eq 'Invitation')" - } - process { + $guestFilter = "(CreationType eq 'Invitation')" + $expand = "sponsors" $customHeaders = New-EntraBetaCustomHeaders -Command $MyInvocation.MyCommand $environment = (Get-EntraContext).Environment $baseUri = (Get-EntraEnvironment -Name $environment).GraphEndpoint @@ -28,7 +26,7 @@ function Update-EntraBetaInvitedUserSponsorsFromInvitedBy { } $invitedUsers = @() - $uri = $baseUri+"/beta/users?$filter=$($guestFilter)&$expand=sponsors" + $uri = "$baseUri/beta/users?`$filter=$guestFilter&`$expand=sponsors" if ($All) { $invitedUsers = (Invoke-GraphRequest -Method GET -Uri $uri).value diff --git a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 index 4d80c0910b..c259046dd3 100644 --- a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 +++ b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 @@ -13,18 +13,75 @@ BeforeAll { Mock -CommandName Get-EntraEnvironment -MockWith { return @{ GraphEndpoint = "https://graph.microsoft.com" } } -ModuleName Microsoft.Entra.Beta.Users - Mock -CommandName Get-MgBetaUser -MockWith { - [PSCustomObject]@{ - Id = "123" - DisplayName = "Test Guest" - UserPrincipalName = "test@contoso.com" - Sponsors = $null + $guestFilter = "(CreationType eq 'Invitation')" + $expand = "sponsors" + + # Mock Invoke-GraphRequest for GET with the user filter and expand parameters + Mock -CommandName Invoke-GraphRequest -MockWith { + Write-Output "Mock called" + @{ + 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 + } -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 { - return @{ value = @{ id = "456" } } - } -ModuleName Microsoft.Entra.Beta.Users + @{ + 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 + + Mock -CommandName Set-EntraBetaUser -MockWith { $true } -ModuleName Microsoft.Entra.Beta.Users @@ -50,22 +107,20 @@ Describe "Update-EntraBetaInvitedUserSponsorsFromInvitedBy" { Context "Edge Cases" { It "Should handle missing invitedBy information" { - Mock -CommandName Invoke-GraphRequest -MockWith { @{ value = $null } } -ModuleName Microsoft.Entra.Beta.Users - - Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Invited user information not available" + Mock -CommandName Invoke-GraphRequest -MockWith { @{ value =@() } } -ModuleName Microsoft.Entra.Beta.Users + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor updated successfully" } } + It "Should contain 'User-Agent' header" { + $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" + + # Call the function + Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false - Context "User-Agent Header" { - It "Should contain 'User-Agent' header" { - $userAgentHeaderValue = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" - - Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false - - Should -Invoke -CommandName Invoke-GraphRequest -ModuleName Microsoft.Entra.Beta.Users -Times 1 -ParameterFilter { - $Headers.'User-Agent' | Should -Be $userAgentHeaderValue - $true - } - } + # 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 } -} \ No newline at end of file +} + +} From d757cd7b01a5e62c873063012338580bf73bd08b Mon Sep 17 00:00:00 2001 From: Emmanuel Ng'ang'a <60355631+emmanuel-karanja@users.noreply.github.com> Date: Tue, 1 Apr 2025 09:37:23 +0300 Subject: [PATCH 21/21] cleanup tests --- ...InvitedUserSponsorsFromInvitedBy.Tests.ps1 | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 index c259046dd3..f85e805da8 100644 --- a/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 +++ b/test/EntraBeta/Users/Update-EntraBetaInvitedUserSponsorsFromInvitedBy.Tests.ps1 @@ -18,7 +18,6 @@ BeforeAll { # Mock Invoke-GraphRequest for GET with the user filter and expand parameters Mock -CommandName Invoke-GraphRequest -MockWith { - Write-Output "Mock called" @{ value = @( @{ @@ -80,12 +79,6 @@ BeforeAll { 'User-Agent' = "PowerShell/$psVersion EntraPowershell/$entraVersion Update-EntraBetaInvitedUserSponsorsFromInvitedBy" } } -ModuleName Microsoft.Entra.Beta.Users - - - - Mock -CommandName Set-EntraBetaUser -MockWith { $true } -ModuleName Microsoft.Entra.Beta.Users - - Mock -CommandName Update-MgBetaUser -MockWith {"Sponsor updated successfully for user 123"} -ModuleName Microsoft.Entra.Beta.Users } Describe "Update-EntraBetaInvitedUserSponsorsFromInvitedBy" { @@ -111,16 +104,18 @@ Describe "Update-EntraBetaInvitedUserSponsorsFromInvitedBy" { Update-EntraBetaInvitedUserSponsorsFromInvitedBy -UserId "123" -Confirm:$false | Should -Match "Sponsor updated successfully" } } - 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 - } -} + 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 + } + } + } }