Summary
To suspend users by their last known successful authentication we first need to know some data about the users. Directory Insights data contains information about a users last known authentication time. The Get-JcLoginEvent
function is a streamlined function to help query users who have completed a successful authentication within a defined time period. This function returns a user’s last known successful “radius_auth”, “sso_auth”, “login_attempt”, “ldap_bind” and/or “user_login_attempt”.
The Suspend-InactiveUsers
function will suspend users who’s last timestamp is greater (older) than the daysSinceLogin
parameter (or if they have no known last authentication time)
Data is first gathered about users through the Get-JcLoginEvent
function and then passed to Suspend-InactiveUsers
if we want to suspend users from an organization.
Running the function on a cadence
The function can be run on a cadence through a cron job or scheduled task to meet compliance. To invoke the function, first connect to a JumpCloud org with Connect-JCOnline
ex:
function Suspend-InactiveUsers{
... # function contents
}
Connect-JCOnline -JumpCloudApiKey:("jsdaf892370asdfkljfdaslkjjsdaf892370asdf")
Get-JcLoginEvent -CsvPath:("~/suspension/authnticationTimestamps.csv") -StartDate((Get-Date).AddDays(-30))
Suspend-InactiveUsers -CsvPath:("~/suspension/authnticationTimestamps.csv") -DaysSinceLogin:(30) -ExcludeAttribute:('serviceAccount')
To enable all users who’ve been disabled:
$users = Get-JCsdkUser
foreach ($user in $users) {
$output = Set-JcSdkUser -id $user.Id -State "ACTIVATED"
}
Suspend-InactiveUsers Function
The Suspend-InactiveUsers
function will suspend users who have not successfully authenticated to a JumpCloud service between the current date and the date set when calling the function.
This function takes the CSV generated from Get-JCLoginEvent
and suspends users who’s last known login date is DaysSinceLogin
(days) older than the date generated and saved in the jc-login-suspend-last-run.txt
file.’
The following parameters can be used with this function: ReadOnly
, CsvPath
, ExcludeAttribute
and DaysSinceLogin
ReadOnly
can be set to $true
. Setting the parameter to true will will query users and print the users that would be suspended if the parameter was not set - this can be helpful for testing.
CsvPath
should be set to the path of the .csv file generated from the Get-JCLoginEvent
function
ExcludeAttribute
can be set to a string. When this parameter is set the function will search users with custom attributes. If the name of a user’s custom attribute matches the parameter and the value of that parameter is true
then the user will be excluded from suspension. Ex. Setting ExcludeAttribute
to serviceAccount
will query users with the custom attribute name serviceAccount
and value true
if any users are found to match that custom attribute, they will be excluded from the list of suspended users even if they have not authenticated to a service in the filterDate
window.
DaysSinceLogin
is the integer value to query user known login times against. The number of days set in this parameter are used to generate the date which determines suspension. The suspension date threshold is the date generated in the jc-login-suspend-last-run.txt
file subtracted by the number of days set in this parameter. For example. If the Get-JCLoginEvent
function was ran at Friday, August 6th, 2021 at 12:30:00 pm and the DaysSinceLogin
parameter was set to 30, users who have not authenticated after Wednesday, July 7, 2021 at 12:30:00 pm would be suspended.
Script
function Get-JcLoginEvent {
[CmdletBinding()]
param (
[Parameter(HelpMessage = "Set to true to print the number of users who would have been suspend, this will not suspend users")]
[switch]
$ReadOnly,
[Parameter(Mandatory = $true, HelpMessage = "Path to the CSV file. If no such CSV file exists, it will be created. If the CSV file does exist, it will be updated.")]
[string]
$CsvPath,
[Parameter(Mandatory = $true, HelpMessage = "The DateTime value to begin filtering between. ex. (Get-Date.AddDays(-30)) ")]
[datetime]
$StartDate,
[Parameter(Mandatory = $false, HelpMessage = "The DateTime value to end filtering between. Default: Get-Date (current date). ex. (Get-Date.AddDays(-30)) ")]
[datetime]
$EndDate
)
begin {
# Write date of last execution to a file
# $CsvDirectory = Split-Path $CsvPath
if ( [System.String]::IsNullOrEmpty($EndDate) ) {
$EndDate = Get-Date
}
# New-Item -Path $CsvDirectory -Name "jc-login-suspend-last-run.txt" -Value $EndDate -Force
# Check if CSV exists. If it does not, create an empty one.
If (Test-Path -Path $CsvPath -PathType Leaf) {
Write-Debug "File found at location: $($CsvPath)."
} Else {
Write-Debug "No file found at location: $($CsvPath). Creating file."
[System.Collections.ArrayList]$CsvData = @()
}
$users = Get-JCUser -returnProperties username
# $DaysPerSpan = .5
# $Span = New-TimeSpan -Start $StartDate -End $EndDate
# $Spans = @()
# for ($i = 0; $i -lt $Span.Days; $i += ($DaysPerSpan)) {
# $LoopStart = ($StartDate).AddDays($i)
# $LoopEnd = ($StartDate).AddDays($i + $DaysPerSpan)
# If ($LoopEnd -gt $EndDate) {
# $LoopEnd = $EndDate
# }
# $Spans += New-Object psobject -Property @{StartDate = $LoopStart; EndDate = $LoopEnd }
# }
# Write-Debug "$($Spans.Count) day interval(s) will be queried"
}
process {
# Gather Directory Insights Data
$EventTypes = (
"radius_auth_attempt",
"sso_auth",
"login_attempt",
"user_login_attempt"
)
$FilterFields = @(
"success",
"initiated_by",
"event_type",
"timestamp"
"sso_token_success"
)
Write-Debug "Collecting Directory Insights data for the following event types: $($EventTypes). This may take a moment."
#$DirectoryInsightsData = Get-JcSdkEvent -Service all -StartTime $StartDate -SearchTermOr @{ "event_type" = $EventTypes }
$resultsArrayList = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
$stopwatch = [system.diagnostics.stopwatch]::StartNew()
### Systems START
$systemsJob = $users | Foreach-Object -ThrottleLimit 5 -Parallel {
$resultsArrayList = $using:resultsArrayList
$response = Get-JcSdkEvent -Service systems -StartTime $using:StartDate -EndTime $using:EndDate -SearchTermAnd @{"event_type" = $using:EventTypes; "username" = $_.username } -Fields $using:FilterFields -ErrorAction Ignore
# Write-Host "Searching $($_.username) | start: ($($using:StartDate)) end: ($($using:EndDate))"
if ($response) {
# normalize the date time formats
$response | ForEach-Object { $_.timestamp = Get-Date "$($_.timestamp)" }
$sortedResponse = $response | Sort-Object -property timestamp -Descending
foreach ($res in $sortedResponse) {
If ($res.success -eq "True") {
$resultsArrayList.Add($res)
break
}
}
}
} -AsJob
### SYSTEMS END
### SSO START
# $resultsArrayList = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
$ssoJob = $users | Foreach-Object -ThrottleLimit 5 -Parallel {
$resultsArrayList = $using:resultsArrayList
$response = Get-JcSdkEvent -Service sso -StartTime $using:StartDate -EndTime $using:EndDate -SearchTermAnd @{"event_type" = $using:EventTypes; "initiated_by.username" = $_.username } -Fields $using:FilterFields -ErrorAction Ignore
# Write-Host "Searching $($_.username) | start: ($($using:StartDate)) end: ($($using:EndDate))"
if ($response) {
# normalize the date time formats
$response | ForEach-Object { $_.timestamp = Get-Date "$($_.timestamp)" }
$sortedResponse = $response | Sort-Object -property timestamp -Descending
foreach ($res in $sortedResponse) {
If ($res.sso_token_success -eq 'True') {
$resultsArrayList.Add($res)
break
}
}
}
} -AsJob
### SSO END
### Directory
# $resultsArrayList = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
$directoryJob = $users | Foreach-Object -ThrottleLimit 5 -Parallel {
$resultsArrayList = $using:resultsArrayList
$response = Get-JcSdkEvent -Service directory -StartTime $using:StartDate -EndTime $using:EndDate -SearchTermAnd @{"event_type" = $using:EventTypes; "initiated_by.username" = $_.username } -Fields $using:FilterFields -ErrorAction Ignore
# Write-Host "Searching $($_.username) | start: ($($using:StartDate)) end: ($($using:EndDate))"
if ($response) {
# normalize the date time formats
$response | ForEach-Object { $_.timestamp = Get-Date "$($_.timestamp)" }
$sortedResponse = $response | Sort-Object -property timestamp -Descending
foreach ($res in $sortedResponse) {
If ($res.success -eq "True") {
$resultsArrayList.Add($res)
break
}
}
}
} -AsJob
### DIRECTORY END
### RADIUS
# $resultsArrayList = [System.Collections.Concurrent.ConcurrentBag[object]]::new()
$RadiusJob = $users | Foreach-Object -ThrottleLimit 5 -Parallel {
$resultsArrayList = $using:resultsArrayList
$response = Get-JcSdkEvent -Service radius -StartTime $using:StartDate -EndTime $using:EndDate -SearchTermAnd @{"event_type" = $using:EventTypes; "initiated_by.username" = $_.username } -Fields $using:FilterFields -ErrorAction Ignore
# Write-Host "Searching $($_.username) | start: ($($using:StartDate)) end: ($($using:EndDate))"
if ($response) {
# normalize the date time formats
$response | ForEach-Object { $_.timestamp = Get-Date "$($_.timestamp)" }
$sortedResponse = $response | Sort-Object -property timestamp -Descending
foreach ($res in $sortedResponse) {
If ($res.success -eq "True") {
$resultsArrayList.Add($res)
break
}
}
}
} -AsJob
### RADIUS END
$systemsJob | Receive-Job -Wait
$ssoJob | Receive-Job -Wait
$directoryJob | Receive-Job -Wait
$RadiusJob | Receive-Job -Wait
$stopwatch.Stop()
$totalSecs = [math]::Round($stopwatch.Elapsed.TotalSeconds, 0)
Write-host "Events Data query took $totalSecs seconds to run"
foreach ($user in $users) {
$latestEventByUser = $resultsArrayList | Where-Object { $_.initiated_by.username -eq $user.Username } | Sort-Object -property timestamp -Descending | Select-Object -First 1
if ($latestEventByUser) {
# if new user to the csv
$CsvData.Add(
[PSCustomObject]@{
id = $user._id;
username = $latestEventByUser.initiated_by.username;
timestamp = $latestEventByUser.timestamp
}
) | Out-Null
} else {
$CsvData.Add(
[PSCustomObject]@{
id = $user._id;
username = $user.Username;
timestamp = "NA"
}
) | Out-Null
}
}
}
end {
if ( $ReadOnly ) {
$CsvData
} else {
$CsvData | ConvertTo-CSV | Out-File -FilePath $CsvPath -Force
}
}
}
function Suspend-InactiveUsers
{
[CmdletBinding()]
param (
[Parameter(HelpMessage = "Set to true to print the number of users who would have been suspend, this will not suspend users")]
[switch]
$ReadOnly = $false,
[Parameter(Mandatory = $true, HelpMessage = "Path to the CSV file. If no such CSV file exists, it will be created. If the CSV file does exist, it will be updated.")]
[string]
$CsvPath,
[Parameter(HelpMessage = "Custom Attribute to Filter against, if the value of this custom attribute is true, this user will be excluded from suspension")]
[string]
$ExcludeAttribute,
[Parameter(Mandatory = $true, HelpMessage = "The integer value to filter between the current date ex. (30) ")]
[int]
$DaysSinceLogin
)
begin
{
$SuspendedList = [System.Collections.ArrayList]@()
$CsvDirectory = Split-Path $CsvPath
$LastRunTimePath = $CsvDirectory + "/jc-login-suspend-last-run.txt"
If ( Test-Path -Path $LastRunTimePath -PathType Leaf )
{
$LastRunTime = ( Get-Date ( Get-Content $LastRunTimePath ) )
$SuspendDate = $LastRunTime.AddDays(-$DaysSinceLogin)
Write-Debug "Searching for user's who's last known login time occured before: $SuspendDate"
}
else
{
Write-Error "ERROR: Unable to locate file at location: $($LastRunTimePath)."
}
# Check if CSV exists. If it does not, error out.
If (Test-Path -Path $CsvPath -PathType Leaf)
{
Write-Debug "File found at location: $($CsvPath)."
[System.Collections.ArrayList]$CsvData = Import-Csv -Path $CsvPath
}
Else
{
Write-Error "ERROR: Unable to locate CSV at location: $($CsvPath)."
}
$UserList = Get-JcSdkUser
if ($ExcludeAttribute)
{
$exclusionUsers = @()
foreach ($user in $UserList)
{
if ($user.Attributes | Where-Object { $_.Name -eq $ExcludeAttribute -and $_.Value -eq 'true' })
{
# if user has custom attribute name w/ true value add it to the exclusion list.
$exclusionUsers += $user
}
}
Write-Debug "Users found: $($UserList.count)"
Write-Debug "Exclusion Users found: $($exclusionUsers.count)"
$difference = $UserList | where-object { $_.username -notcontains $exclusionUsers.username }
Write-Debug "Users w/o $ExcludeAttribute attribute found: $($difference.count)"
# just set the users object to difference...
$UserList = $difference
}
}
process
{
ForEach ( $User in $CsvData )
{
# If the user has been deleted since the last run, remove them from the CSV
If ( ($User.id -notin $UserList.id) -And ($User.id -notin $exclusionUsers.id ) )
{
Write-Debug "$($User.username) has been deleted. Removing from list."
If (-not $ReadOnly)
{
$CsvData.RemoveAt($CsvData.id.IndexOf($User.id))
}
}
else
{
If ( ([datetime]$User.timestamp).ticks -lt $SuspendDate.ticks )
{
Write-Debug "Suspending user: $($User.username). Last login: $($User.timestamp)."
$SuspendedUser = Get-JcSdkUser -Id $User.Id
$SuspendedList.Add([PSCustomObject]@{
Username = $SuspendedUser.Username;
Firstname = $SuspendedUser.Firstname;
Lastname = $SuspendedUser.Lastname;
Email = $SuspendedUser.Email;
LastLoginDate = $User.timestamp;
Id = $SuspendedUser.Id;
Manager = ( $SuspendedUser.Attributes | ? { $_.Name -eq "Manager" } ).Value;
}) | Out-Null
If ( -not $ReadOnly )
{
Set-JcSdkUser -Id $User.id -State "SUSPENDED" | Out-Null
$CsvData.RemoveAt($CsvData.id.IndexOf($User.id))
}
}
}
}
# For each user not found in the csv, they don't have a recorded last login
ForEach ($User in $UserList)
{
if ($User.id -notin $CsvData.id)
{
# Check if user is still inactive - don't disable if they are not
if ( $User.Activated )
{
# suspend
Write-Debug "Suspending user: $($User.username). Last login: N/A."
$SuspendedList.Add([PSCustomObject]@{
Username = $User.Username;
Firstname = $User.Firstname;
Lastname = $User.Lastname;
Email = $User.Email;
LastLoginDate = "N/A";
Id = $User.Id;
Manager = ( $User.Attributes | ? { $_.Name -eq "Manager" } ).Value;
}) | Out-Null
If ( -not $ReadOnly )
{
Set-JcSdkUser -Id $User.id -State "SUSPENDED" | Out-Null
}
}
}
}
}
end
{
if ( -not $ReadOnly )
{
$CsvData | Export-Csv $CsvPath
}
$SuspendedList
}
}
Tags:
[powershell
automation
users
authentication
]