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))