ScriptBlocks/Get-SPOwnerEligibility.Graph.ps1
|
param( [Parameter(Mandatory = $true)] [string]$ClientId, [Parameter(Mandatory = $true)] [string]$TenantId, [Parameter(Mandatory = $true)] [string]$Thumbprint, [Parameter(Mandatory = $true)] [string]$MembersJsonPath, [Parameter(Mandatory = $false)] [string]$MinimumGraphModuleVersion = "2.33.0" ) $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $VerbosePreference = "SilentlyContinue" $WarningPreference = "SilentlyContinue" $InformationPreference = "SilentlyContinue" function Test-IsBlank { param( [Parameter(Mandatory = $false)] [AllowNull()] [object]$Value ) if ($null -eq $Value) { return $true } return ([string]$Value).Trim().Length -eq 0 } function Convert-ToObjectArray { param( [Parameter(Mandatory = $false)] [AllowNull()] [object]$InputObject ) if ($null -eq $InputObject) { return ,([object[]]@()) } if ($InputObject -is [string]) { return ,([object[]]@($InputObject)) } if ($InputObject -is [System.Array]) { return ,([object[]]$InputObject) } if ($InputObject -is [System.Collections.IEnumerable]) { $items = New-Object System.Collections.Generic.List[object] foreach ($item in $InputObject) { $items.Add($item) } return ,([object[]]$items.ToArray()) } return ,([object[]]@($InputObject)) } function New-SuccessResult { param( [Parameter(Mandatory = $true)] [object]$Data ) return @{ Success = $true Data = $Data } } function New-FailureResult { param( [Parameter(Mandatory = $true)] [string]$Error, [Parameter(Mandatory = $false)] [AllowNull()] [object]$Data ) $result = @{ Success = $false Error = $Error } if ($null -ne $Data) { $result.Data = $Data } return $result } function Assert-RequiredModuleAvailable { param( [Parameter(Mandatory = $true)] [string]$ModuleName, [Parameter(Mandatory = $false)] [AllowNull()] [string]$MinimumVersion ) $availableModules = @(Get-Module -ListAvailable -Name $ModuleName) if ($availableModules.Count -eq 0) { throw "Required module '$ModuleName' is not installed. Install it with: Install-Module $ModuleName -Scope CurrentUser -Force" } if (-not (Test-IsBlank $MinimumVersion)) { $minimumVersionObject = [version]$MinimumVersion $matchingModules = @( $availableModules | Where-Object { $_.Version -ge $minimumVersionObject } | Sort-Object Version -Descending ) if ($matchingModules.Count -eq 0) { $installedVersions = ( $availableModules | Sort-Object Version -Descending | ForEach-Object { $_.Version.ToString() } ) -join ", " throw "Required module '$ModuleName' is installed, but no version >= $MinimumVersion was found. Installed versions: $installedVersions. Update with: Update-Module $ModuleName -Force" } } } function Import-RequiredGraphModules { param( [Parameter(Mandatory = $false)] [AllowNull()] [string]$MinimumVersion ) Assert-RequiredModuleAvailable ` -ModuleName "Microsoft.Graph.Authentication" ` -MinimumVersion $MinimumVersion Assert-RequiredModuleAvailable ` -ModuleName "Microsoft.Graph.Users" ` -MinimumVersion $MinimumVersion if (Test-IsBlank $MinimumVersion) { Import-Module Microsoft.Graph.Authentication -ErrorAction Stop | Out-Null Import-Module Microsoft.Graph.Users -ErrorAction Stop | Out-Null } else { Import-Module Microsoft.Graph.Authentication -MinimumVersion $MinimumVersion -ErrorAction Stop | Out-Null Import-Module Microsoft.Graph.Users -MinimumVersion $MinimumVersion -ErrorAction Stop | Out-Null } } function Get-MemberPropertyValue { param( [Parameter(Mandatory = $true)] [AllowNull()] [object]$Member, [Parameter(Mandatory = $true)] [string]$Name ) if ($null -eq $Member) { return $null } $property = $Member.PSObject.Properties[$Name] if ($property -and $null -ne $property.Value) { return $property.Value } return $null } function Get-IdentityHintFromMember { param( [Parameter(Mandatory = $true)] [object]$Member ) $email = Get-MemberPropertyValue -Member $Member -Name "Email" $loginName = Get-MemberPropertyValue -Member $Member -Name "LoginName" if (-not (Test-IsBlank $email)) { return ([string]$email).Trim() } if (Test-IsBlank $loginName) { return $null } $candidate = ([string]$loginName).Trim() if ($candidate -match '\|membership\|(.+)$') { return $matches[1].Trim() } if ($candidate -match '^[^|]+\|[^|]+\|(.+)$') { return $matches[1].Trim() } return $candidate } function Test-IsDirectUserPrincipal { param( [Parameter(Mandatory = $true)] [AllowNull()] [object]$Member ) if ($null -eq $Member) { return $false } $principalType = Get-MemberPropertyValue -Member $Member -Name "PrincipalType" $loginName = Get-MemberPropertyValue -Member $Member -Name "LoginName" if (-not (Test-IsBlank $principalType)) { $principalTypeText = ([string]$principalType).Trim() if ($principalTypeText -eq "1") { return $true } if ($principalTypeText -ieq "User") { return $true } if ($principalTypeText -match '(^|[,\s])User([,\s]|$)') { return $true } return $false } if (Test-IsBlank $loginName) { return $false } $loginNameText = ([string]$loginName).Trim() if ($loginNameText -match '\|membership\|') { return $true } if ($loginNameText -match '^i:0#\.f\|membership\|') { return $true } if ($loginNameText -match '^c:0') { return $false } if ($loginNameText -match '^spo-grid-all-users/') { return $false } if ($loginNameText -match '^SHAREPOINT\\system$') { return $false } if ($loginNameText -match '^[^@\s]+@[^@\s]+\.[^@\s]+$') { return $true } return $false } function New-BasicMemberReportObject { param( [Parameter(Mandatory = $true)] [object]$Member, [Parameter(Mandatory = $false)] [AllowNull()] [string]$Reason ) return [PSCustomObject]@{ DisplayName = Get-MemberPropertyValue -Member $Member -Name "DisplayName" Email = Get-MemberPropertyValue -Member $Member -Name "Email" LoginName = Get-MemberPropertyValue -Member $Member -Name "LoginName" PrincipalType = Get-MemberPropertyValue -Member $Member -Name "PrincipalType" IdentityHint = Get-IdentityHintFromMember -Member $Member Reason = $Reason } } function New-MissingOrBlockedReportObject { param( [Parameter(Mandatory = $true)] [object]$BasicMember, [Parameter(Mandatory = $false)] [AllowNull()] [object]$GraphUser, [Parameter(Mandatory = $true)] [string]$Reason ) $entraUserId = $null $userPrincipalName = $null $mail = $null $accountEnabled = $null if ($null -ne $GraphUser) { $entraUserId = $GraphUser.Id $userPrincipalName = $GraphUser.UserPrincipalName $mail = $GraphUser.Mail $accountEnabled = $GraphUser.AccountEnabled } return [PSCustomObject]@{ DisplayName = $BasicMember.DisplayName Email = $BasicMember.Email LoginName = $BasicMember.LoginName PrincipalType = $BasicMember.PrincipalType IdentityHint = $BasicMember.IdentityHint EntraUserId = $entraUserId UserPrincipalName = $userPrincipalName Mail = $mail AccountEnabled = $accountEnabled Reason = $Reason } } function New-NoTeamsReportObject { param( [Parameter(Mandatory = $true)] [object]$BasicMember, [Parameter(Mandatory = $true)] [object]$GraphUser ) return [PSCustomObject]@{ DisplayName = $BasicMember.DisplayName Email = $BasicMember.Email LoginName = $BasicMember.LoginName PrincipalType = $BasicMember.PrincipalType IdentityHint = $BasicMember.IdentityHint EntraUserId = $GraphUser.Id UserPrincipalName = $GraphUser.UserPrincipalName Mail = $GraphUser.Mail AccountEnabled = $GraphUser.AccountEnabled Reason = "MSTeamsServicePlanNotEnabled" } } function New-EligibleUserReportObject { param( [Parameter(Mandatory = $true)] [object]$BasicMember, [Parameter(Mandatory = $true)] [object]$GraphUser ) return [PSCustomObject]@{ DisplayName = $BasicMember.DisplayName Email = $BasicMember.Email LoginName = $BasicMember.LoginName PrincipalType = $BasicMember.PrincipalType IdentityHint = $BasicMember.IdentityHint EntraUserId = $GraphUser.Id UserPrincipalName = $GraphUser.UserPrincipalName Mail = $GraphUser.Mail AccountEnabled = $GraphUser.AccountEnabled HasMSTeams = $true } } function Get-GraphUserForOwnerMember { param( [Parameter(Mandatory = $true)] [object]$Member ) $identityHint = Get-IdentityHintFromMember -Member $Member if (Test-IsBlank $identityHint) { return [PSCustomObject]@{ Found = $false User = $null Reason = "NoIdentityHint" } } $identity = ([string]$identityHint).Trim() $escapedIdentity = $identity.Replace("'", "''") $properties = @( "Id", "DisplayName", "UserPrincipalName", "Mail", "AccountEnabled", "AssignedPlans" ) try { $user = Get-MgUser ` -UserId $identity ` -Property $properties ` -ErrorAction Stop if ($null -ne $user) { return [PSCustomObject]@{ Found = $true User = $user Reason = $null } } } catch { } try { $filter = "mail eq '$escapedIdentity' or userPrincipalName eq '$escapedIdentity'" $users = @(Get-MgUser ` -Filter $filter ` -Property $properties ` -ConsistencyLevel eventual ` -ErrorAction Stop) if ($users.Count -gt 0) { return [PSCustomObject]@{ Found = $true User = $users[0] Reason = $null } } return [PSCustomObject]@{ Found = $false User = $null Reason = "UserNotFound" } } catch { return [PSCustomObject]@{ Found = $false User = $null Reason = "GraphLookupFailed: $($_.Exception.Message)" } } } function Test-UserHasEnabledMSTeamsPlan { param( [Parameter(Mandatory = $true)] [AllowNull()] [object]$GraphUser ) $teamsServicePlanId = "57ff2da0-773e-42df-b2af-ffb7a2317929" if ($null -eq $GraphUser) { return $false } if ($null -eq $GraphUser.AssignedPlans) { return $false } foreach ($plan in @($GraphUser.AssignedPlans)) { $servicePlanId = $null $capabilityStatus = $null if ($null -ne $plan.ServicePlanId) { $servicePlanId = ([string]$plan.ServicePlanId).Trim().ToLowerInvariant() } if ($null -ne $plan.CapabilityStatus) { $capabilityStatus = ([string]$plan.CapabilityStatus).Trim() } if ($servicePlanId -eq $teamsServicePlanId -and $capabilityStatus -eq "Enabled") { return $true } } return $false } $graphConnected = $false try { Import-RequiredGraphModules -MinimumVersion $MinimumGraphModuleVersion if (Test-IsBlank $MembersJsonPath) { throw "MembersJsonPath is empty." } if (-not (Test-Path -LiteralPath $MembersJsonPath)) { throw "MembersJsonPath does not exist: '$MembersJsonPath'." } Connect-MgGraph ` -ClientId $ClientId ` -TenantId $TenantId ` -CertificateThumbprint $Thumbprint ` -NoWelcome ` -ErrorAction Stop | Out-Null $graphConnected = $true $membersJson = Get-Content -LiteralPath $MembersJsonPath -Raw -Encoding UTF8 if (Test-IsBlank $membersJson) { $members = @() } else { $members = Convert-ToObjectArray -InputObject ($membersJson | ConvertFrom-Json -ErrorAction Stop) } $eligibleUsers = New-Object System.Collections.Generic.List[object] $excludedNonUsers = New-Object System.Collections.Generic.List[object] $excludedMissingOrBlocked = New-Object System.Collections.Generic.List[object] $excludedWithoutTeams = New-Object System.Collections.Generic.List[object] foreach ($member in $members) { $basic = New-BasicMemberReportObject -Member $member -Reason $null if (-not (Test-IsDirectUserPrincipal -Member $member)) { $excludedNonUsers.Add( (New-BasicMemberReportObject ` -Member $member ` -Reason "NotDirectUserAccount") ) continue } $resolved = Get-GraphUserForOwnerMember -Member $member if (-not $resolved.Found -or $null -eq $resolved.User) { $excludedMissingOrBlocked.Add( (New-MissingOrBlockedReportObject ` -BasicMember $basic ` -GraphUser $null ` -Reason $resolved.Reason) ) continue } $graphUser = $resolved.User if ($graphUser.AccountEnabled -ne $true) { $excludedMissingOrBlocked.Add( (New-MissingOrBlockedReportObject ` -BasicMember $basic ` -GraphUser $graphUser ` -Reason "AccountDisabledOrBlocked") ) continue } $hasTeams = Test-UserHasEnabledMSTeamsPlan -GraphUser $graphUser if (-not $hasTeams) { $excludedWithoutTeams.Add( (New-NoTeamsReportObject ` -BasicMember $basic ` -GraphUser $graphUser) ) continue } $eligibleUsers.Add( (New-EligibleUserReportObject ` -BasicMember $basic ` -GraphUser $graphUser) ) } $data = [PSCustomObject]@{ EligibleOwnerUsers = [object[]]$eligibleUsers.ToArray() EligibleOwnerUserCount = [int]$eligibleUsers.Count ExcludedNonUserMembers = [object[]]$excludedNonUsers.ToArray() ExcludedNonUserMemberCount = [int]$excludedNonUsers.Count ExcludedMissingOrBlockedUsers = [object[]]$excludedMissingOrBlocked.ToArray() ExcludedMissingOrBlockedUserCount = [int]$excludedMissingOrBlocked.Count ExcludedWithoutMSTeamsUsers = [object[]]$excludedWithoutTeams.ToArray() ExcludedWithoutMSTeamsCount = [int]$excludedWithoutTeams.Count } $result = New-SuccessResult -Data $data } catch { $result = New-FailureResult ` -Error $_.Exception.ToString() ` -Data ([PSCustomObject]@{ MembersJsonPath = $MembersJsonPath MinimumGraphModuleVersion = $MinimumGraphModuleVersion RequiredModules = @( "Microsoft.Graph.Authentication", "Microsoft.Graph.Users" ) }) } finally { if ($graphConnected) { try { Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null } catch { } } } Write-Output ("__JSON__" + ($result | ConvertTo-Json -Compress -Depth 25)) |