Skip to content
Closed
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
107 changes: 107 additions & 0 deletions backend/Config/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -45943,6 +45943,113 @@
"x-cipp-any-tenant": true
}
},
"/api/ListGuestUsers": {
"get": {
"summary": "List guest users with lifecycle status",
"operationId": "ListGuestUsers",
"tags": [
"Identity > Administration > Users"
],
"description": "Lists all guest accounts in a tenant with a computed lifecycle status (Active, Pending Acceptance, Stale, Never Signed In or Disabled) based on the invitation state and sign-in activity from the Graph beta API.",
"parameters": [
{
"name": "staleDays",
"in": "query",
"description": "Days without any sign-in before an enabled guest is considered stale. Defaults to 90.",
"required": false,
"schema": {
"type": "integer"
}
},
{
"$ref": "#/components/parameters/tenantFilter"
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"type": "object",
"description": "Derived from the Microsoft Graph entity it queries, and the fields the endpoint selects onto each record, and the columns the CIPP UI renders. The fields taken from Graph are the ones this endpoint selects, so they are what the response actually carries.",
"properties": {
"accountEnabled": {
"type": "boolean",
"x-cipp-field-source": "graph,backend,frontend"
},
"createdDateTime": {
"type": "string",
"x-cipp-field-source": "graph,backend,frontend"
},
"daysSinceSignIn": {
"x-cipp-field-source": "backend,frontend"
},
"displayName": {
"type": "string",
"x-cipp-field-source": "graph,backend,frontend"
},
"externalUserState": {
"type": "string",
"x-cipp-field-source": "graph,backend"
},
"externalUserStateChangeDateTime": {
"type": "string",
"x-cipp-field-source": "graph,backend"
},
"id": {
"type": "string",
"x-cipp-field-source": "graph,backend"
},
"lastInteractiveSignInDateTime": {
"x-cipp-field-source": "backend"
},
"lastNonInteractiveSignInDateTime": {
"x-cipp-field-source": "backend"
},
"lastSignInDateTime": {
"x-cipp-field-source": "backend,frontend"
},
"lastSuccessfulSignInDateTime": {
"x-cipp-field-source": "backend"
},
"mail": {
"type": "string",
"x-cipp-field-source": "graph,backend,frontend"
},
"sourceDomain": {
"x-cipp-field-source": "backend,frontend"
},
"status": {
"x-cipp-field-source": "backend,frontend"
},
"userPrincipalName": {
"type": "string",
"x-cipp-field-source": "graph,backend"
}
}
}
}
}
}
},
"401": {
"description": "Unauthorized - invalid or missing bearer token"
},
"403": {
"description": "Forbidden - caller lacks the required RBAC role"
}
},
"security": [
{
"bearerAuth": []
}
],
"x-cipp-role": "Identity.User.Read"
}
},
"/api/ListHaloClients": {
"get": {
"summary": "ListHaloClients",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
function Invoke-ListGuestUsers {
<#
.FUNCTIONALITY
Entrypoint
.ROLE
Identity.User.Read
.SYNOPSIS
List guest users with lifecycle status
.DESCRIPTION
Lists all guest accounts in a tenant with a computed lifecycle status (Active, Pending Acceptance, Stale, Never Signed In or Disabled) based on the invitation state and sign-in activity from the Graph beta API.
#>
[CmdletBinding()]
param($Request, $TriggerMetadata)

$APIName = $Request.Params.CIPPEndpoint
$Headers = $Request.Headers

# The tenant to list guest users for
$TenantFilter = $Request.Query.tenantFilter
# Days without any sign-in before an enabled guest is considered stale. Defaults to 90.
$StaleDays = $Request.Query.staleDays ? [int]$Request.Query.staleDays : 90

try {
# signInActivity can only be requested on tenants with an Entra ID P1 license - Graph
# rejects the whole query on unlicensed tenants, so fall back to listing without
# sign-in data there and compute status from the invitation state alone.
$SignInLogsCapable = Test-CIPPStandardLicense -StandardName 'GuestLifecycle' -TenantFilter $TenantFilter -Preset Entra -SkipLog

$SelectFields = @(
'id', 'displayName', 'mail', 'userPrincipalName', 'createdDateTime',
'accountEnabled', 'externalUserState', 'externalUserStateChangeDateTime'
)
if ($SignInLogsCapable) { $SelectFields += 'signInActivity' }
# Graph caps the page size lower when signInActivity is selected
$Top = $SignInLogsCapable ? 500 : 999
$Uri = "https://graph.microsoft.com/beta/users?`$filter=userType eq 'Guest'&`$select=$($SelectFields -join ',')&`$count=true&`$top=$Top"
$GuestUsers = New-GraphGetRequest -uri $Uri -tenantid $TenantFilter -ComplexFilter

$Now = Get-Date
$GraphRequest = foreach ($Guest in $GuestUsers) {
# Last sign-in is the most recent of the three signInActivity fields.
# lastSuccessfulSignInDateTime can run ahead of the other two, so leaving it
# out would report recently-active guests as stale.
$LastSignIn = $null
$Candidates = @(
$Guest.signInActivity.lastSignInDateTime
$Guest.signInActivity.lastNonInteractiveSignInDateTime
$Guest.signInActivity.lastSuccessfulSignInDateTime
)
foreach ($Candidate in $Candidates) {
if ($Candidate -and (-not $LastSignIn -or [datetime]$Candidate -gt [datetime]$LastSignIn)) {
$LastSignIn = $Candidate
}
}
$DaysSinceSignIn = $LastSignIn ? [math]::Round(($Now - [datetime]$LastSignIn).TotalDays) : $null

$Status = if ($Guest.accountEnabled -eq $false) {
'Disabled'
} elseif ($Guest.externalUserState -eq 'PendingAcceptance') {
'Pending Acceptance'
} elseif (-not $SignInLogsCapable) {
'Unknown'
} elseif (-not $LastSignIn) {
'Never Signed In'
} elseif ($DaysSinceSignIn -ge $StaleDays) {
'Stale'
} else {
'Active'
}

[PSCustomObject]@{
id = $Guest.id
displayName = $Guest.displayName
mail = $Guest.mail
userPrincipalName = $Guest.userPrincipalName
sourceDomain = $Guest.mail ? ($Guest.mail -split '@')[1] : $null
status = $Status
accountEnabled = $Guest.accountEnabled
externalUserState = $Guest.externalUserState
externalUserStateChangeDateTime = $Guest.externalUserStateChangeDateTime
createdDateTime = $Guest.createdDateTime
lastSignInDateTime = $LastSignIn
lastInteractiveSignInDateTime = $Guest.signInActivity.lastSignInDateTime
lastNonInteractiveSignInDateTime = $Guest.signInActivity.lastNonInteractiveSignInDateTime
lastSuccessfulSignInDateTime = $Guest.signInActivity.lastSuccessfulSignInDateTime
daysSinceSignIn = $DaysSinceSignIn
}
}
$StatusCode = [System.Net.HttpStatusCode]::OK
} catch {
$ErrorMessage = Get-CippException -Exception $_
Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to list guest users: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage
$StatusCode = [System.Net.HttpStatusCode]::InternalServerError
$GraphRequest = @{ Error = $ErrorMessage.NormalizedError }
}

return ([HttpResponseContext]@{
StatusCode = $StatusCode
Body = @($GraphRequest)
})
}
Loading
Loading