Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 19 additions & 12 deletions src/functions/public/Device/Get-IntuneDeviceLogin.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -316,21 +316,18 @@

Write-Verbose -Message "Searching for devices where user '$targetUserId' has logged in"

# Get all managed devices with usersLoggedOn property
# Note: Graph API may not support filtering on usersLoggedOn collection, so we retrieve all and filter client-side
# Invoke-GraphGet automatically handles pagination
# Get all managed devices with usersLoggedOn property.
# Invoke-GraphGet already handles pagination, so we query once and filter client-side.
$uri = "$baseUri`?`$select=id,deviceName,usersLoggedOn"
$allDevices = [System.Collections.Generic.List[object]]::new()
$nextUri = $uri

while ($null -ne $nextUri) {
$resp = Invoke-GraphGet -Uri $nextUri
$resp = Invoke-GraphGet -Uri $uri

$allDevices = @()
if ($null -ne $resp) {
if ($null -ne $resp.value) {
$allDevices.AddRange($resp.value)
$allDevices = @($resp.value)
} else {
$allDevices = @($resp)
}

$nextUri = $resp.'@odata.nextLink'
}

if ($allDevices.Count -eq 0) {
Expand All @@ -341,18 +338,28 @@
Write-Verbose -Message "Checking $($allDevices.Count) managed devices for user logons"

$matchCount = 0
$seenResults = [System.Collections.Generic.HashSet[string]]::new()
foreach ($device in $allDevices) {
# Check if target user is in the usersLoggedOn collection
$userLogon = $device.usersLoggedOn | Where-Object -FilterScript { $_.userId -eq $targetUserId }
if ($userLogon) {
$latestUserLogon = $userLogon |
Sort-Object -Property lastLogOnDateTime -Descending |
Select-Object -First 1
$logonTimestamp = [datetime]$latestUserLogon.lastLogOnDateTime
$resultKey = "{0}|{1}|{2}" -f $device.id, $targetUserId, $logonTimestamp.ToString('o')
if (-not $seenResults.Add($resultKey)) {
continue
}

$matchCount++
$user = Resolve-EntraUserById -UserId $targetUserId
[PSCustomObject]@{
DeviceName = $device.deviceName
UserPrincipalName = $user.userPrincipalName
DeviceId = $device.id
UserId = $targetUserId
LastLogonDateTime = [datetime]$userLogon.lastLogOnDateTime
LastLogonDateTime = $logonTimestamp
}
}
}
Expand Down
142 changes: 89 additions & 53 deletions tests/public/Device/Get-IntuneDeviceLogin.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -812,16 +812,16 @@ Describe 'Get-IntuneDeviceLogin' {
$results[0].DeviceId | Should -Be $testDeviceId2
}

It 'Should find device in second page during pagination' {
It 'Should find device when match appears later in aggregated paginated data' {
# Arrange
$mockUser = [PSCustomObject]@{
id = $testUserId
userPrincipalName = $testUserPrincipalName
}

# Simulate paginated response: first page has different user, second page has target user
$page1 = [PSCustomObject]@{
value = @(
# Simulate aggregated output from Invoke-GraphGet after auto-pagination
$mockDevicesResponse = [PSCustomObject]@{
value = @(
[PSCustomObject]@{
id = 'c0000000-0000-0000-0000-000000000001'
deviceName = 'OTHER-DEVICE-1'
Expand All @@ -831,13 +831,7 @@ Describe 'Get-IntuneDeviceLogin' {
lastLogOnDateTime = '2024-03-05T10:30:00Z'
}
)
}
)
'@odata.nextLink' = 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$select=id,deviceName,usersLoggedOn&$skiptoken=xyz'
}

$page2 = [PSCustomObject]@{
value = @(
},
[PSCustomObject]@{
id = $testDeviceId1
deviceName = 'DEVICE-001'
Expand All @@ -851,19 +845,12 @@ Describe 'Get-IntuneDeviceLogin' {
)
}

$callCount = 0
Mock -CommandName 'Invoke-GraphGet' -MockWith {
param([string]$Uri)
$callCount++
if ($Uri -match 'users/') {
return $mockUser
} else {
# Simulate pagination by returning page1 first, then page2
if ($Uri -match 'skiptoken') {
return $page2
} else {
return $page1
}
return $mockDevicesResponse
}
}
Mock -CommandName 'Resolve-EntraUserById' -MockWith { return $mockUser }
Expand All @@ -877,16 +864,16 @@ Describe 'Get-IntuneDeviceLogin' {
$results[0].DeviceName | Should -Be 'DEVICE-001'
}

It 'Should find multiple devices across multiple pages' {
It 'Should find multiple devices across paginated results' {
# Arrange
$mockUser = [PSCustomObject]@{
id = $testUserId
userPrincipalName = $testUserPrincipalName
}

# Simulate multiple pages with target user on both pages
$page1 = [PSCustomObject]@{
value = @(
# Simulate aggregated output from Invoke-GraphGet after auto-pagination
$aggregatedResponse = [PSCustomObject]@{
value = @(
[PSCustomObject]@{
id = $testDeviceId1
deviceName = 'DEVICE-001'
Expand All @@ -896,13 +883,7 @@ Describe 'Get-IntuneDeviceLogin' {
lastLogOnDateTime = '2024-03-05T10:30:00Z'
}
)
}
)
'@odata.nextLink' = 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$select=id,deviceName,usersLoggedOn&$skiptoken=abc'
}

$page2 = [PSCustomObject]@{
value = @(
},
[PSCustomObject]@{
id = $testDeviceId2
deviceName = 'DEVICE-002'
Expand All @@ -921,11 +902,7 @@ Describe 'Get-IntuneDeviceLogin' {
if ($Uri -match 'users/') {
return $mockUser
} else {
if ($Uri -match 'skiptoken') {
return $page2
} else {
return $page1
}
return $aggregatedResponse
}
}
Mock -CommandName 'Resolve-EntraUserById' -MockWith { return $mockUser }
Expand All @@ -939,16 +916,16 @@ Describe 'Get-IntuneDeviceLogin' {
$results[1].DeviceId | Should -Be $testDeviceId2
}

It 'Should handle pagination with no matches on first page but match on second' {
It 'Should handle paginated results where match appears later in the dataset' {
# Arrange
$mockUser = [PSCustomObject]@{
id = $testUserId
userPrincipalName = $testUserPrincipalName
}

# First page: no target user
$page1 = [PSCustomObject]@{
value = @(
# Simulate aggregated output from Invoke-GraphGet after auto-pagination
$aggregatedResponse = [PSCustomObject]@{
value = @(
[PSCustomObject]@{
id = 'c0000000-0000-0000-0000-000000000001'
deviceName = 'OTHER-DEVICE'
Expand All @@ -963,14 +940,7 @@ Describe 'Get-IntuneDeviceLogin' {
id = 'c0000000-0000-0000-0000-000000000002'
deviceName = 'OTHER-DEVICE-2'
usersLoggedOn = @()
}
)
'@odata.nextLink' = 'https://graph.microsoft.com/beta/deviceManagement/managedDevices?$select=id,deviceName,usersLoggedOn&$skiptoken=def'
}

# Second page: match found
$page2 = [PSCustomObject]@{
value = @(
},
[PSCustomObject]@{
id = $testDeviceId1
deviceName = 'DEVICE-ON-PAGE2'
Expand All @@ -988,13 +958,9 @@ Describe 'Get-IntuneDeviceLogin' {
param([string]$Uri)
if ($Uri -match 'users/') {
return $mockUser
} else {
if ($Uri -match 'skiptoken') {
return $page2
} else {
return $page1
}
}

return $aggregatedResponse
}
Mock -CommandName 'Resolve-EntraUserById' -MockWith { return $mockUser }

Expand All @@ -1006,6 +972,76 @@ Describe 'Get-IntuneDeviceLogin' {
$results[0].DeviceId | Should -Be $testDeviceId1
$results[0].DeviceName | Should -Be 'DEVICE-ON-PAGE2'
}

It 'Should suppress duplicate rows when Graph returns duplicate device entries' {
# Arrange
$mockUser = [PSCustomObject]@{
id = $testUserId
userPrincipalName = $testUserPrincipalName
}

$duplicateResponse = [PSCustomObject]@{
value = @(
[PSCustomObject]@{
id = $testDeviceId1
deviceName = 'DEVICE-001'
usersLoggedOn = @(
[PSCustomObject]@{
userId = $testUserId
lastLogOnDateTime = '2024-03-05T10:30:00Z'
}
)
},
[PSCustomObject]@{
id = $testDeviceId2
deviceName = 'DEVICE-002'
usersLoggedOn = @(
[PSCustomObject]@{
userId = $testUserId
lastLogOnDateTime = '2024-03-04T09:15:00Z'
}
)
},
[PSCustomObject]@{
id = $testDeviceId1
deviceName = 'DEVICE-001'
usersLoggedOn = @(
[PSCustomObject]@{
userId = $testUserId
lastLogOnDateTime = '2024-03-05T10:30:00Z'
}
)
},
[PSCustomObject]@{
id = $testDeviceId2
deviceName = 'DEVICE-002'
usersLoggedOn = @(
[PSCustomObject]@{
userId = $testUserId
lastLogOnDateTime = '2024-03-04T09:15:00Z'
}
)
}
)
}

Mock -CommandName 'Invoke-GraphGet' -MockWith {
param([string]$Uri)
if ($Uri -match 'users/') {
return $mockUser
}

return $duplicateResponse
}
Mock -CommandName 'Resolve-EntraUserById' -MockWith { return $mockUser }

# Act
$results = @(Get-IntuneDeviceLogin -UserPrincipalName $testUserPrincipalName)

# Assert
$results.Count | Should -Be 2
($results | Select-Object -ExpandProperty DeviceId | Sort-Object) | Should -Be @($testDeviceId1, $testDeviceId2)
}
}

Context 'When called with ByUserId parameter set' {
Expand Down
Loading