You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Microsoft Graph脚本问题:过滤授权用户后无法获取正确登录时间

问题与解决方案

问题

需要生成非活跃用户报表,统计最后登录时间超过90天的授权用户(授权信息通过extensionAttribute15字段标识,值以E、f、k开头)。但使用Microsoft Graph的PowerShell脚本添加extensionAttribute15的过滤条件后,所有用户的最后登录时间都显示为"Never";移除过滤条件后能获取真实登录时间,但结果会包含未授权用户。

原因

Microsoft Graph Beta端点存在限制:当在$filter中使用自定义扩展属性(如extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute15)时,即使在$select中指定了signInActivity,该字段也不会被返回,导致脚本中$User.SignInActivity始终为null,最终显示"Never"。

解决方案

方法一:分两步查询(适合用户量较大的场景)

先获取所有符合extensionAttribute15条件的用户ID,再通过这些ID批量查询包含signInActivity在内的完整用户信息,避开过滤与投影的冲突。

修改后的脚本:

# Azure AD Enterprise App for authentication to the 84 tenant
$AppId = "X"
$TenantId = "X"
$AppSecret = 'X'

# Construct URI and body needed for authentication
$uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
$body = @{
    client_id     = $AppId
    scope         = "https://graph.microsoft.com/.default"
    client_secret = $AppSecret
    grant_type    = "client_credentials" 
}

# Get OAuth 2.0 Token
$tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType "application/x-www-form-urlencoded" -Body $body -UseBasicParsing
$token = ($tokenRequest.Content | ConvertFrom-Json).access_token
$headers = @{Authorization = "Bearer $token"}

# Step 1: 获取符合extensionAttribute15条件的用户ID列表
Write-Host "Fetching authorized user IDs..."
$userIds = @()
$nextLink = "https://graph.microsoft.com/beta/users?`$filter=startsWith(extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute15,'E') or startswith(extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute15,'f') or startswith(extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute15,'k')&`$select=id&`$top=999"

while ($nextLink -ne $null) {
    $response = Invoke-RestMethod -Uri $nextLink -Headers $headers -Method Get
    $userIds += $response.Value.id
    $nextLink = $response.'@Odata.NextLink'
}

# 添加Split-Chunk函数用于拆分ID列表
function Split-Chunk {
    param(
        [Parameter(ValueFromPipeline=$true)]
        [array]$InputObject,
        [int]$Size
    )
    begin {
        $chunk = @()
    }
    process {
        foreach ($item in $InputObject) {
            $chunk += $item
            if ($chunk.Count -eq $Size) {
                ,$chunk
                $chunk = @()
            }
        }
    }
    end {
        if ($chunk.Count -gt 0) {
            ,$chunk
        }
    }
}

# Step 2: 批量查询用户的完整信息(包含signInActivity)
Write-Host "Fetching full user data with sign-in activity..."
$Report = [System.Collections.Generic.List[Object]]::new()
# 按每20个ID一组拆分(Graph批量查询限制)
$idChunks = $userIds | Split-Chunk -Size 20

foreach ($chunk in $idChunks) {
    $idFilter = ($chunk | ForEach-Object { "id eq '$_'" }) -join " or "
    $uri = "https://graph.microsoft.com/beta/users?`$filter=$idFilter&`$select=displayName,userPrincipalName, mail, department, jobTitle, extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute14, extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute15, accountEnabled, signInActivity, UserType, id&`$expand=manager(`$select=displayName,userPrincipalName)"
    
    $userData = Invoke-RestMethod -Uri $uri -Headers $headers -Method Get
    foreach ($User in $userData.Value) {
        if ($Null -ne $User.SignInActivity) {
            $LastSignIn = Get-Date($User.SignInActivity.LastSignInDateTime) -format g
            $DaysSinceSignIn = (New-TimeSpan $LastSignIn).Days
        }
        else {
            $LastSignIn = "Never" 
            $DaysSinceSignIn = "N/A"
        }

        $ReportLine = [PSCustomObject] @{
            UPN                = $User.UserPrincipalName
            DisplayName        = $User.DisplayName
            Email              = $User.Mail
            SignInStatus       = $User.accountEnabled
            Department         = $User.department
            AADLastSignIn      = $LastSignIn
            DaysInactive       = $DaysSinceSignIn  # 新增字段:直观显示未活跃天数
            JobTitle           = $User.jobTitle
            JobCode            = $User.extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute14
            License            = $User.extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute15
            Manager            = if($User.manager) { $User.manager.userPrincipalName } Else {$null}
        }
        $Report.Add($ReportLine)
    }
}

# 筛选最后登录超过90天或从未登录的用户
$InactiveReport = $Report | Where-Object { $_.DaysInactive -ge 90 -or $_.AADLastSignIn -eq "Never" }

# 输出结果
$InactiveReport | Sort-Object DisplayName | Out-GridView

方法二:客户端侧过滤(适合用户量较小的场景)

先获取所有用户的完整信息(包含signInActivity),然后在PowerShell中筛选extensionAttribute15符合条件的用户,避免在Graph查询中使用扩展属性过滤。

修改后的核心逻辑:

# 获取所有用户的完整信息
$URI = "https://graph.microsoft.com/beta/users?`$select=displayName,userPrincipalName, mail, department, jobTitle, extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute14, extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute15, accountEnabled, signInActivity, UserType, id&`$expand=manager(`$select=displayName,userPrincipalName)&`$top=999"
$SignInData = (Invoke-RestMethod -Uri $URI -Headers $Headers -Method Get -ContentType "application/json") 
$Report = [System.Collections.Generic.List[Object]]::new() 

# 处理初始数据
Foreach ($User in $SignInData.Value) {  
    if ($Null -ne $User.SignInActivity) {
        $LastSignIn = Get-Date($User.SignInActivity.LastSignInDateTime) -format g
        $DaysSinceSignIn = (New-TimeSpan $LastSignIn).Days
    }
    else {
        $LastSignIn = "Never" 
        $DaysSinceSignIn = "N/A"
    }

    $ReportLine = [PSCustomObject] @{
        UPN                = $User.UserPrincipalName
        DisplayName        = $User.DisplayName
        Email              = $User.Mail
        SignInStatus       = $User.accountEnabled
        Department         = $User.department
        AADLastSignIn      = $LastSignIn
        DaysInactive       = $DaysSinceSignIn
        JobTitle           = $User.jobTitle
        JobCode            = $User.extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute14
        License            = $User.extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute15
        Manager            = if($User.manager) { $User.manager.userPrincipalName } Else {$null}
    }
    $Report.Add($ReportLine) 
}

# 处理分页数据(原脚本逻辑不变)
While ($NextLink -ne $Null) {
    Write-Host "Still processing..."
    $SignInData = Invoke-WebRequest -Method GET -Uri $NextLink -ContentType "application/json" -Headers $Headers
    $SignInData = $SignInData | ConvertFrom-JSon
    ForEach ($User in $SignInData.Value) {  
        if ($Null -ne $User.SignInActivity) {
            $LastSignIn = Get-Date($User.SignInActivity.LastSignInDateTime) -format g
            $DaysSinceSignIn = (New-TimeSpan $LastSignIn).Days
        }
        else {
            $LastSignIn = "Never" 
            $DaysSinceSignIn = "N/A"
        }
        
        $ReportLine = [PSCustomObject] @{  
            UPN                = $User.UserPrincipalName
            DisplayName        = $User.DisplayName
            Email              = $User.Mail
            SignInStatus       = $User.accountEnabled  
            Department         = $User.department
            AADLastSignIn      = $LastSignIn
            DaysInactive       = $DaysSinceSignIn
            JobTitle           = $User.jobTitle
            JobCode            = $User.extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute14
            License            = $User.extension_1fe7973b28e74213b897d62528e614c7_extensionAttribute15
            Manager            = if($User.manager) { $User.manager.userPrincipalName } Else {$null}
        }
        $Report.Add($ReportLine) 
    } 
    $NextLink = $SignInData.'@Odata.NextLink'
}

# 客户端侧筛选授权用户+非活跃用户
$InactiveAuthorizedUsers = $Report | Where-Object {
    ($_.License -match "^E|^f|^k") -and 
    ($_.DaysInactive -ge 90 -or $_.AADLastSignIn -eq "Never")
}

# 输出结果
$InactiveAuthorizedUsers | Sort DisplayName | Out-GridView

补充说明

  • 方法一的Split-Chunk函数用于拆分用户ID列表,适配Graph批量查询的数量限制(最多20个ID)。
  • 用户量较大时优先选择方法一,避免客户端侧过滤占用过多本地资源。

内容的提问来源于stack exchange,提问作者Alex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 05:10:56