From 3bbb96a8e0b41aa5431ce86c3a90b5cb1bf174c7 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Wed, 13 Dec 2023 14:26:58 -0700 Subject: [PATCH 001/346] todos for individual certificate generation --- scripts/automation/Radius/Config.ps1 | 2 +- .../Radius/Functions/Private/Test-User.ps1 | 41 ++++ .../Functions/Public/Generate-UserCerts.ps1 | 215 ++++++++++++++---- 3 files changed, 209 insertions(+), 49 deletions(-) create mode 100644 scripts/automation/Radius/Functions/Private/Test-User.ps1 diff --git a/scripts/automation/Radius/Config.ps1 b/scripts/automation/Radius/Config.ps1 index a99a2aaaa..d965e7c67 100644 --- a/scripts/automation/Radius/Config.ps1 +++ b/scripts/automation/Radius/Config.ps1 @@ -37,7 +37,7 @@ $CertType = "UsernameCn" # Do not modify below ################################################################################ -$UserAgent_ModuleVersion = '1.0.7' +$UserAgent_ModuleVersion = '1.1.0' $UserAgent_ModuleName = 'PasswordlessRadiusConfig' #Build the UserAgent string $UserAgent_ModuleName = "JumpCloud_$($UserAgent_ModuleName).PowerShellModule" diff --git a/scripts/automation/Radius/Functions/Private/Test-User.ps1 b/scripts/automation/Radius/Functions/Private/Test-User.ps1 new file mode 100644 index 000000000..5bc57524d --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Test-User.ps1 @@ -0,0 +1,41 @@ +function Test-User { + [CmdletBinding()] + param ( + # Parameter help description + [Parameter(ParameterSetName = 'username')] + [System.String] + $username, + # Parameter help description + [Parameter(ParameterSetName = 'userid')] + [System.String] + $userID + ) + begin { + # Get User Group membership + # TODO: update if data is older than 30 mins + if ( -not $GLOBAL:RadiusUserMembership ) { + $GLOBAL:RadiusUserMembership = Get-JCUserGroupMember -ByID $JCUSERGROUP + } + } + process { + switch ($PSCmdlet.ParameterSetName) { + 'userid' { + $matchedUser = $Global:RadiusUserMembership | Where-Object { $userID -in $_.UserId } + $inputText = $userID + } + 'username' { + $matchedUser = $Global:RadiusUserMembership | Where-Object { $username -in $_.Username } + $inputText = $username + } + } + if ($matchedUser) { + Write-Debug "Matched Username Found: $($matchedUser.username)" + } else { + Write-Warning "User specified $inputText was not found within the Radius Server Membership Lists" + return $null + } + } + end { + return $matchedUser + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 index e83a06068..be6252fd9 100644 --- a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 @@ -5,7 +5,6 @@ Connect-JCOnline $JCAPIKEY -force ################################################################################ # Do not modify below ################################################################################ - # Check if CA-Key is saved in env if ($env:certKeyPassword) { Write-Host "Found CA-Key password in env" @@ -27,8 +26,7 @@ if ($env:certKeyPassword) { # Import the functions Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force -$SystemHash = Get-JCSystem -returnProperties displayName, os - +# Import User.Json/ create list if it does not exist if (Test-Path -Path "$JCScriptRoot/users.json" -PathType Leaf) { Write-Host "[status] Found user.json file" $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 @@ -45,11 +43,22 @@ if (Test-Path -Path "$JCScriptRoot/users.json" -PathType Leaf) { } # Get user membership of group -$groupMembers = Get-GroupMembership -groupID $JCUSERGROUP +# TODO: update group membership if last date is -gt 30 mins +if ( -not $GLOBAL:RadiusUserMembership ) { + $GLOBAL:RadiusUserMembership = Get-JCUserGroupMember -ByID $JCUSERGROUP + $groupMembers = $GLOBAL:RadiusUserMembership +} else { + $groupMembers = $GLOBAL:RadiusUserMembership +} if ($groupMembers) { Write-Host "[status] Found $($groupmembers.count) users in Radius User Group" } +# Get SystemHash +# TODO: update group membership if last date is -gt 30 mins +# TODO: global variable and track time last updated +$SystemHash = Get-JCSystem -returnProperties displayName, os + # Create UserCerts dir if (Test-Path "$JCScriptRoot/UserCerts") { Write-Host "[status] User Cert Directory Exists" @@ -58,56 +67,166 @@ if (Test-Path "$JCScriptRoot/UserCerts") { New-Item -ItemType Directory -Path "$JCScriptRoot/UserCerts" } -# if user from group is on the system, continue with script: -foreach ($user in $groupMembers) { - # Create the User Certs - $MatchedUser = get-webjcuser -userID $user.id - Write-Host "Generating Cert for user: $($MatchedUser.username)" - - if ($MatchedUser.id -in $userArray.userId) { - if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { - Write-Host "[status] $($MatchedUser.username) already has certs generated... skipping" - } else { - Generate-UserCert -CertType $CertType -user $MatchedUser.username -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" - } - } else { - Write-Host "[status] $($MatchedUser.username) not found in users.json" - - # Find user system associations - $SystemUserAssociations = @() - $SystemUserAssociations += (Get-JCAssociation -Type user -Id $MatchedUser.id -TargetType system | Select-Object @{N = 'SystemID'; E = { $_.targetId } }) - - $systemAssociations = @() - foreach ($system in $SystemUserAssociations) { - $systemInfo = $SystemHash | Where-Object _id -EQ $system.SystemID - $systemTable = @{ - systemId = $systemInfo._id - displayName = $systemInfo.displayName - osFamily = $systemInfo.os +function Show-GenerationMenu { + $title = 'JumpCloud Radius Cert Deployment' + Clear-Host + Write-Host "================ $Title ================" + Write-Host "1: Press '1' to generate new certificates for NEW RADIUS users. $([char]0x1b)[96mNOTE: This will only generate certificates for users who have not yet had a certificate generated." + Write-Host "2: Press '2' to generate new certificates for ONE Specific RADIUS user. $([char]0x1b)[96mNOTE: you will be prompted to overwrite any previously generated certificates" + Write-Host "3: Press '3' to re-generate new certificates for ALL users. $([char]0x1b)[96mNOTE: This will overwrite any previously generated certificates" + Write-Host "4: Press '4' to re-generate new certificates for users who's cert is set to expire shortly. $([char]0x1b)[96mNOTE: This will overwrite any previously generated certificates" + Write-Host "E: Press 'E' to exit." +} +Do { + Show-GenerationMenu + $confirmation = Read-Host "Please make a selection" + switch ($confirmation) { + '1' { + # process all users, generate certificates for uses who do not yet have a certificate + foreach ($user in $groupMembers) { + # Get the user details: + $MatchedUser = get-webjcuser -userID $user.id + Write-Host "Generating Cert for user: $($MatchedUser.username)" + if ($matchedUser.id -in $userArray.userid) { + if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { + Write-Host "[status] $($MatchedUser.username) already has certs generated... skipping" + } else { + # Generate a new cert for this user: + Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + } + } else { + Write-Host "[status] $($MatchedUser.username) not found in users.json" + + # Find user system associations + $SystemUserAssociations = @() + $SystemUserAssociations += (Get-JCAssociation -Type user -Id $MatchedUser.id -TargetType system | Select-Object @{N = 'SystemID'; E = { $_.targetId } }) + + $systemAssociations = @() + foreach ($system in $SystemUserAssociations) { + $systemInfo = $SystemHash | Where-Object _id -EQ $system.SystemID + $systemTable = @{ + systemId = $systemInfo._id + displayName = $systemInfo.displayName + osFamily = $systemInfo.os + } + $systemAssociations += $systemTable + } + + $userTable = @{ + userId = $MatchedUser.id + userName = $MatchedUser.username + localUsername = $(If ($MatchedUser.hasLocalUsername) { + $matchedUser.localUsername + } else { + $matchedUser.username + }) + systemAssociations = $systemAssociations + commandAssociations = @() + } + + if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { + Write-Host "[status] $($MatchedUser.username) has certs generated... adding to users.json" + } else { + Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + } + $userArray += $userTable + } } - $systemAssociations += $systemTable + # Update UserArray + $userArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + Break } - - $userTable = @{ - userId = $MatchedUser.id - userName = $MatchedUser.username - localUsername = $(If ($MatchedUser.hasLocalUsername) { - $matchedUser.localUsername + '2' { + # process individual users, generate certificates for users who have not been added to users.json + # if users have been added to users.json, prompt to re-generate + Clear-Variable "ConfirmUser" + while (-not $confirmUser) { + $confirmationUser = Read-Host "Enter the Username or UserID of the user" + $confirmUser = Test-User -username $confirmationUser -debug + } + # get data about the user + $MatchedUser = get-webjcuser -userID $confirmUser.UserID + # Generate a new cert for this user: + if ($matchedUser.id -in $userArray.userid) { + if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { + Do { + $overwrite = Read-Host "do you want to overwrite yn?" + switch ($overwrite) { + 'y' { + Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + Break + + } + 'n' { + Write-Host "[status] $($MatchedUser.username) already has certs generated... skipping" + Break + + } + } + } until (($overwrite -eq "y") -or ($overwrite -eq "n")) } else { - $matchedUser.username - }) - systemAssociations = $systemAssociations - commandAssociations = @() + # Generate a new cert for this user: + Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + } + } else { + Write-Host "[status] $($MatchedUser.username) not found in users.json" + + # Find user system associations + $SystemUserAssociations = @() + $SystemUserAssociations += (Get-JCAssociation -Type user -Id $MatchedUser.id -TargetType system | Select-Object @{N = 'SystemID'; E = { $_.targetId } }) + + $systemAssociations = @() + foreach ($system in $SystemUserAssociations) { + $systemInfo = $SystemHash | Where-Object _id -EQ $system.SystemID + $systemTable = @{ + systemId = $systemInfo._id + displayName = $systemInfo.displayName + osFamily = $systemInfo.os + } + $systemAssociations += $systemTable + } + + $userTable = @{ + userId = $MatchedUser.id + userName = $MatchedUser.username + localUsername = $(If ($MatchedUser.hasLocalUsername) { + $matchedUser.localUsername + } else { + $matchedUser.username + }) + systemAssociations = $systemAssociations + commandAssociations = @() + } + + if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { + Write-Host "[status] $($MatchedUser.username) has certs generated... adding to users.json" + } else { + Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + } + $userArray += $userTable + } + # Update UserArray + $userArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + Break } + '3' { + # process new users, re-generate certificates for users who have not been added to users.json + # TODO: implement + Break + } + '4' { + #TODO: overwrite soon to expire certificates - if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { - Write-Host "[status] $($MatchedUser.username) has certs generated... adding to users.json" - } else { - Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" } - $userArray += $userTable + 'E' { + Write-Host "Returning to main menu" + } + default { + Write-Host "Invalid Choice. Please try again" + Break + } } -} +} while ($confirmation -ne 'E') + -$userArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" From bf1dc0965c03d90cb6f12793412418a083787a4e Mon Sep 17 00:00:00 2001 From: Geoffrey Wein Date: Wed, 13 Dec 2023 14:59:29 -0700 Subject: [PATCH 002/346] update cert filter --- .../automation/Radius/Functions/Public/Distribute-UserCerts.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 index 1cd42ee19..d1ea163b0 100644 --- a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 @@ -52,7 +52,7 @@ if ($RadiusCertCommands.Count -ge 1) { # Create commands for each user foreach ($user in $userArray) { # Get certificate and zip to upload to Commands - $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -Filter "$($user.userName)*" + $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -Filter "$($user.userName)-*" # set crt and pfx filepaths $userCrt = ($userCertFiles | Where-Object { $_.Name -match "crt" }).FullName $userPfx = ($userCertFiles | Where-Object { $_.Name -match "pfx" }).FullName From 85e9fed2829dbac1a302f837a97ab63486f9839c Mon Sep 17 00:00:00 2001 From: Geoffrey Wein Date: Wed, 13 Dec 2023 15:01:41 -0700 Subject: [PATCH 003/346] update changelog and config --- scripts/automation/Radius/Changelog.md | 14 ++++++++++++++ scripts/automation/Radius/Config.ps1 | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/automation/Radius/Changelog.md b/scripts/automation/Radius/Changelog.md index 2560f009e..99182cdb4 100644 --- a/scripts/automation/Radius/Changelog.md +++ b/scripts/automation/Radius/Changelog.md @@ -1,3 +1,17 @@ +## 1.1.0 + +Release Date: December 13, 2023 + +#### RELEASE NOTES + +``` +Fixed an issue where similar usernames were having incorrect certificates deployed +``` + +#### Bug Fixes: + +- Addressed a bug where similar usernames were having incorrect certificates deployed. Ex: john.smith and john + ## 1.0.7 Release Date: December 1, 2023 diff --git a/scripts/automation/Radius/Config.ps1 b/scripts/automation/Radius/Config.ps1 index a99a2aaaa..d965e7c67 100644 --- a/scripts/automation/Radius/Config.ps1 +++ b/scripts/automation/Radius/Config.ps1 @@ -37,7 +37,7 @@ $CertType = "UsernameCn" # Do not modify below ################################################################################ -$UserAgent_ModuleVersion = '1.0.7' +$UserAgent_ModuleVersion = '1.1.0' $UserAgent_ModuleName = 'PasswordlessRadiusConfig' #Build the UserAgent string $UserAgent_ModuleName = "JumpCloud_$($UserAgent_ModuleName).PowerShellModule" From 86b4e4dc794783d0b3c064b701dbb653f9edb346 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Thu, 14 Dec 2023 13:08:04 -0700 Subject: [PATCH 004/346] gather sha1 data for local certs --- scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 b/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 index d1275d71a..16bbba7fd 100644 --- a/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 +++ b/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 @@ -64,7 +64,7 @@ function Get-CertInfo { $certHash = @{} # Use openssl to gather serial, subject, issuer and enddate information $certInfo = Invoke-Expression "$opensslBinary x509 -in $($cert.Path) -enddate -serial -subject -issuer -noout" - + $certSHA1 = Get-FileHash -Path $($cert.Path) -Algorithm SHA1 # Convert string data into a key/value pair $certInfo | ForEach-Object { $property = $_ | ConvertFrom-StringData @@ -79,6 +79,7 @@ function Get-CertInfo { $certHash += $property } + $certHash.Add('sha1', $certSHA1.Hash) # Add hash to certObj array $certObj += $certHash From 3dc3a56aa4fc948fe18537dc540f203273e018cb Mon Sep 17 00:00:00 2001 From: Geoffrey Wein Date: Fri, 15 Dec 2023 13:20:15 -0700 Subject: [PATCH 005/346] formatting function for gui --- .../automation/Radius/Functions/Private/PadCenter.ps1 | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 scripts/automation/Radius/Functions/Private/PadCenter.ps1 diff --git a/scripts/automation/Radius/Functions/Private/PadCenter.ps1 b/scripts/automation/Radius/Functions/Private/PadCenter.ps1 new file mode 100644 index 000000000..2f7ae3ddb --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/PadCenter.ps1 @@ -0,0 +1,10 @@ +function PadCenter { + param ( + [string]$string, + [char]$char + ) + $length = $host.ui.rawui.windowsize.width + $spaces = $length - $string.Length + $padLeft = $spaces / 2 + $string.Length + return $string.PadLeft($padLeft, $char).PadRight($length, $char) +} \ No newline at end of file From 3c86a21b0a224ce09446a9dbbfb6153bba7a3a76 Mon Sep 17 00:00:00 2001 From: Geoffrey Wein Date: Fri, 15 Dec 2023 13:20:22 -0700 Subject: [PATCH 006/346] main menu updates --- .../Functions/Private/Show-RadiusMainMenu.ps1 | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 b/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 index fba4f9122..1c41b3896 100644 --- a/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 +++ b/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 @@ -1,6 +1,6 @@ function Show-RadiusMainMenu { param ( - [string]$Title = 'JumpCloud Radius Cert Deployment' + [string]$Title = ' JumpCloud Radius Cert Deployment ' ) # Get cert information $rootCAInfo = Get-CertInfo -rootCa @@ -12,20 +12,49 @@ function Show-RadiusMainMenu { # Find all certs that will expire between current date and cut off date $expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $cutoffDate + # Get UserGroup information from Config.ps1 + $radiusUserGroup = Get-JcSdkUserGroup -Id $JCUSERGROUP | Select-Object Name + $radiusUserGroupMemberCount = (Get-JcSdkUserGroupMember -GroupId $JCUSERGROUP).Count + + # Get SSID information from Config.ps1 + $radiusSSID = $NETWORKSSID.Split(';').Trim() + # Output for Users Clear-Host - Write-Host "================ $Title ================" - Write-Host "$([char]0x1b)[33mEdit the variables in Config.ps1 before continuing this script `n" + + # ==== TITLE ==== + Write-Host $(PadCenter -string $Title -char '=') + Write-Host $(PadCenter -string "Edit the variables in Config.ps1 before continuing this script `n" -char ' ') -ForegroundColor Yellow + # /==== TITLE ==== + + # ==== ROOT CA ==== + Write-Host $(PadCenter -string ' Root CA ' -char '-') if ($rootCAInfo -eq $null) { - Write-Host "$([char]0x1b)[33mNo Root CA detected`n" + Write-Host $(PadCenter -string "No Root CA detected`n" -char ' ') -ForegroundColor Yellow } else { - Write-Host "$([char]0x1b)[32mRoot CA Serial Number: $($rootCAInfo.serial)" - Write-Host "$([char]0x1b)[32mRoot CA Expiration: $($rootCAInfo.notAfter)`n" + Write-Host $(PadCenter -string "Root CA Serial Number: $($rootCAInfo.serial)" -char ' ') -ForegroundColor Green + Write-Host $(PadCenter -string "Root CA Expiration: $($rootCAInfo.notAfter)`n" -char ' ') -ForegroundColor Green } if ($expiringCerts) { - Write-Host "$([char]0x1b)[91m$($($expiringCerts.subject).Count) user certs will expire in 15 days `n" + Write-Host $(PadCenter -string "$($($expiringCerts.subject).Count) user certs will expire in 15 days `n" -char ' ') -ForegroundColor Red } + Write-Host $(PadCenter -string "-" -char '-') + # /==== ROOT CA ==== + # ==== GROUP/SSID ==== + Write-Host $(PadCenter -string "Radius User Group: $($radiusUserGroup.Name)" -char " ") -ForegroundColor Green + Write-Host $(PadCenter -string "Radius Users: $($radiusUserGroupMemberCount)" -char " ") -ForegroundColor Green + if ($radiusSSID.count -gt 1) { + Write-Host $(PadCenter -string "Radius SSIDs:" -char " ") -ForegroundColor Green + $radiusSSID | ForEach-Object { + Write-Host $(PadCenter -string $_ -char " ") -ForegroundColor Green + } + } else { + Write-Host $(PadCenter -string "Radius SSID: $radiusSSID" -char " ") -ForegroundColor Green + Write-Host "`n" + } + # /==== GROUP/SSID ==== + Write-Host $(PadCenter -string "-" -char '-') Write-Host "1: Press '1' to generate your Root Certificate." Write-Host "2: Press '2' to generate/update your User Certificate(s)." Write-Host "3: Press '3' to distribute your User Certificate(s)." From 2dd51d7429e76a0afafe88b19bdfae7c33200396 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Fri, 29 Dec 2023 16:26:19 -0700 Subject: [PATCH 007/346] generate cert by user with cache data --- .gitignore | 2 + scripts/automation/Radius/Config.ps1 | 10 +- .../Functions/JCRadiusCertDeployment.psm1 | 12 +- .../Radius/Functions/Private/Get-CertInfo.ps1 | 59 ++-- .../HashFunctions/Convert-FromHashtable.ps1 | 21 ++ .../Private/HashFunctions/Get-DynamicHash.ps1 | 66 +++++ .../HashFunctions/Test-UserFromHash.ps1 | 56 ++++ .../Radius/Functions/Private/PadCenter.ps1 | 4 + .../Functions/Private/Show-RadiusMainMenu.ps1 | 39 +-- .../Private/SystemTable/New-SystemTable.ps1 | 30 ++ .../Private/UserTable/Get-UserFromTable.ps1 | 53 ++++ .../Private/UserTable/New-UserTable.ps1 | 40 +++ .../Private/UserTable/Set-UserTable.ps1 | 79 ++++++ .../Private/settings/Get-JCRSettingsFile.ps1 | 45 +++ .../Private/settings/New-JCRSettingsFile.ps1 | 41 +++ .../Private/settings/Set-JCRSettingsFile.ps1 | 100 +++++++ .../Private/settings/Update-JCRGlobalVars.ps1 | 163 +++++++++++ .../Functions/Public/Generate-UserCerts.ps1 | 265 +++++++++--------- .../Radius/Start-RadiusDeployment.ps1 | 2 + 19 files changed, 909 insertions(+), 178 deletions(-) create mode 100644 scripts/automation/Radius/Functions/Private/HashFunctions/Convert-FromHashtable.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/HashFunctions/Get-DynamicHash.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/UserTable/New-UserTable.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/settings/Get-JCRSettingsFile.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/settings/New-JCRSettingsFile.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/settings/Set-JCRSettingsFile.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/settings/Update-JCRGlobalVars.ps1 diff --git a/.gitignore b/.gitignore index 2835b809e..01806df1c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ /scripts/automation/Radius/*/*.srl /scripts/automation/Radius/Config.ps1 /scripts/automation/Radius/users.json +/scripts/automation/Radius/settings.json +/scripts/automation/Radius/data/* __pycache__/ *.pyc # Ignore PWSH CSV Import/Update Files: diff --git a/scripts/automation/Radius/Config.ps1 b/scripts/automation/Radius/Config.ps1 index d965e7c67..4697de9cc 100644 --- a/scripts/automation/Radius/Config.ps1 +++ b/scripts/automation/Radius/Config.ps1 @@ -1,9 +1,5 @@ -# READ/ WRITE API KEY -$JCAPIKEY = 'YOURAPIKEY' -# JUMPCLOUD ORGID -$JCORGID = 'YOURORGID' # JUMPCLOUD USER GROUP ID -$JCUSERGROUP = 'YOURJCUSERGROUP' +$Global:JCUSERGROUP = 'YOURJCUSERGROUP' # USER CERT PASSWORD (this password is sent to the devices via JumpCloud Commands) $JCUSERCERTPASS = 'secret1234!' # USER CERT Validity Length (days) @@ -11,7 +7,7 @@ $JCUSERCERTVALIDITY = 90 # List Of Radius Network SSID(s) # For Multiple SSIDs enter as a single string seperated by a semicolon ex: # "CorpNetwork_Denver;CorpNetwork_Boulder;CorpNetwork_Boulder 5G;Guest Network" -$NETWORKSSID = "YOUR_SSID" +$Global:NETWORKSSID = "YOUR_SSID" # OpenSSLBinary by default this is (openssl) # NOTE: If openssl does not work, try using the full path to the openssl file # MacOS HomeBrew Example: '/usr/local/Cellar/openssl@3/3.1.1/bin/openssl' @@ -108,6 +104,6 @@ if (($JCAPIKEY).Length -ne 40) { if (($JCORGID).Length -ne 24) { throw "The entered JumpCloud Organization ID is not the expected length" } -if (($JCUSERGROUP).Length -ne 24) { +if (($Global:JCUSERGROUP).Length -ne 24) { throw "The entered JumpCloud UserGroup ID is not the expected length" } diff --git a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 b/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 index 797c55c9c..a3d816d39 100644 --- a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 +++ b/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 @@ -1,4 +1,6 @@ # Load all functions from public and private folders +#TODO: why define both? +$Private = @( Get-ChildItem -Path "$JCScriptRoot/Functions/Private/*.ps1" -Recurse) $Private = @( Get-ChildItem -Path "$PSScriptRoot/Private/*.ps1" -Recurse) Foreach ($Import in $Private) { Try { @@ -6,4 +8,12 @@ Foreach ($Import in $Private) { } Catch { Write-Error -Message "Failed to import function $($Import.FullName): $_" } -} \ No newline at end of file +} + +$global:JCRConfig = Get-JCRSettingsFile + +Update-JCRGlobalVars + +# if ($global:JCConfig.globalVars.lastupdate - ) { + +# } \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 b/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 index 16bbba7fd..774729630 100644 --- a/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 +++ b/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 @@ -1,7 +1,15 @@ function Get-CertInfo { + [CmdletBinding()] param ( - [switch]$RootCA, - [switch]$UserCerts + [Parameter(ParameterSetName = 'CA', Mandatory = $true)] + [switch] + $RootCA, + [Parameter(ParameterSetName = 'User', Mandatory = $true)] + [switch] + $UserCerts, + [Parameter(ParameterSetName = 'User', Mandatory = $false)] + [system.string] + $username ) begin { # Import the Config.ps1 variables @@ -14,10 +22,15 @@ function Get-CertInfo { if ($UserCerts) { # Find all userCert paths - $foundCerts = Resolve-Path -Path "$JCScriptRoot/UserCerts/*.crt" -ErrorAction SilentlyContinue + if ($username) { + $foundCerts = Resolve-Path -Path "$JCScriptRoot/UserCerts/$username-*.crt" -ErrorAction SilentlyContinue + + } else { + $foundCerts = Resolve-Path -Path "$JCScriptRoot/UserCerts/*.crt" -ErrorAction SilentlyContinue + } } - $certObj = @() + $certObj = New-Object System.Collections.ArrayList } process { # If no cert is found, return null @@ -37,6 +50,7 @@ function Get-CertInfo { } # Create hashtable to contain cert info + # TODO: pscustomobject insted of hash $certHash = @{} # Use openssl to gather serial, subject, issuer, and enddate information $certInfo = Invoke-Expression "$opensslBinary x509 -in $($foundCerts.Path) -enddate -serial -subject -issuer -noout" @@ -61,28 +75,35 @@ function Get-CertInfo { } elseif ($UserCerts) { foreach ($cert in $foundCerts) { # Create hashtable to contain cert info - $certHash = @{} + $certHash = [PSCustomObject]@{} # Use openssl to gather serial, subject, issuer and enddate information - $certInfo = Invoke-Expression "$opensslBinary x509 -in $($cert.Path) -enddate -serial -subject -issuer -noout" - $certSHA1 = Get-FileHash -Path $($cert.Path) -Algorithm SHA1 + $certInfo = Invoke-Expression "$opensslBinary x509 -in $($cert.Path) -enddate -serial -subject -issuer -fingerprint -sha1 -noout" # Convert string data into a key/value pair $certInfo | ForEach-Object { $property = $_ | ConvertFrom-StringData - - # Convert notAfter property into datetime format - if ($property.notAfter) { - $date = $property.notAfter - $date = $date.replace('GMT', '').Trim() - $date = $date -replace '\s+', ' ' - $property.notAfter = [datetime]::ParseExact($date , "MMM d HH:mm:ss yyyy", $null) + switch ($($property.keys)) { + 'notAfter' { + $date = $property.notAfter + $date = $date.replace('GMT', '').Trim() + $date = $date -replace '\s+', ' ' + $property.notAfter = [datetime]::ParseExact($date , "MMM d HH:mm:ss yyyy", $null) + } + 'sha1 Fingerprint' { + $property.Values = ($($property.Values)).ToLower().Replace(":", "") + $property.keys = 'sha1' + } + Default { + } } - - $certHash += $property + $certHash | Add-Member -Name $property.keys -Type NoteProperty -Value "$($property.Values)" } - $certHash.Add('sha1', $certSHA1.Hash) - + # lastly add the username of the certificate to the hash: + $certFile = Get-Item $($cert.Path) + $username = $certFile.name.split('-')[0] + $certHash | Add-Member -Name 'username' -Type NoteProperty -Value $username + $certHash | Add-Member -Name 'generated' -Type NoteProperty -Value ($certFile.LastWriteTime.ToString('MM/dd/yyyy HH:mm:ss')) # Add hash to certObj array - $certObj += $certHash + $certObj.add( $certHash) | Out-Null } } } diff --git a/scripts/automation/Radius/Functions/Private/HashFunctions/Convert-FromHashtable.ps1 b/scripts/automation/Radius/Functions/Private/HashFunctions/Convert-FromHashtable.ps1 new file mode 100644 index 000000000..cb146c64e --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/HashFunctions/Convert-FromHashtable.ps1 @@ -0,0 +1,21 @@ +function ConvertFrom-HashTable { + # attribute function from https://stackoverflow.com/questions/73894087/how-do-i-convert-a-powershell-hashtable-to-an-object + # inspired from mklement0's answer + param( + [Parameter(Mandatory, ValueFromPipeline)] + [System.Collections.IDictionary] $HashTable + ) + process { + $dict = New-Object System.Collections.Specialized.OrderedDictionary + foreach ($item in $HashTable.GetEnumerator()) { + if ($item.Value -is [System.Collections.IDictionary]) { + # Nested dictionary? Recurse. + $dict[[object] $item.Key] = ConvertFrom-HashTable -HashTable $item.Value # NOTE: Casting to [object] prevents problems with *numeric* hashtable keys. + } else { + # Copy value as-is. + $dict[[object] $item.Key] = $item.Value + } + } + [pscustomobject] $dict # Convert to [pscustomobject] and output. + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/HashFunctions/Get-DynamicHash.ps1 b/scripts/automation/Radius/Functions/Private/HashFunctions/Get-DynamicHash.ps1 new file mode 100644 index 000000000..58261db9c --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/HashFunctions/Get-DynamicHash.ps1 @@ -0,0 +1,66 @@ +Function Get-DynamicHash () { + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $true)][ValidateSet('System', 'User', 'Command', 'Group')][string]$Object, + [Parameter(Position = 1, Mandatory = $true)][ValidateNotNullOrEmpty()][string[]]$returnProperties + ) + DynamicParam { + if ($Object -eq 'Group') { + $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary + $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] + + $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute + $paramAttributes.Mandatory = $true + $paramAttributesCollect.Add($paramAttributes) + $paramAttributesCollect.Add((New-Object -Type System.Management.Automation.ValidateSetAttribute('System', 'User'))) + + $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("GroupType", [string], $paramAttributesCollect) + + $paramDictionary.Add("GroupType", $dynParam1) + return $paramDictionary + } + } + begin { + $GroupType = $PSBoundParameters['GroupType'] + $DynamicHash = New-Object System.Collections.Hashtable + } + process { + switch ($Object) { + System { + Write-Debug "Generating ResultsHash" + $ResultsHash = Get-JCSystem -returnProperties $returnProperties + } + User { + Write-Debug "Generating ResultsHash" + $ResultsHash = Get-JCUser -returnProperties $returnProperties + } + Command { + Write-Debug "Generating ResultsHash" + $ResultsHash = Get-JCCommand -returnProperties $returnProperties + } + Group { + Write-Debug "Generating ResultsHash" + $returnProperties += "id" + switch ($GroupType) { + System { + $ResultsHash = Get-JCGroup -Type System | Select-Object -Property $returnProperties + } + User { + $ResultsHash = Get-JCGroup -Type User | Select-Object -Property $returnProperties + } + } + } + } + Write-Debug "Adding results to hashtable" + foreach ($Result in $ResultsHash) { + if ($Result.id) { + $DynamicHash.Add($Result.id, @($Result))# | Select-Object -ExcludeProperty 'id')) + } else { + $DynamicHash.Add($Result._id, @($Result | Select-Object -Property *, @{Name = 'id'; Expression = { $_._id } } -ExcludeProperty '_id') ) + } + } + } + end { + return $DynamicHash + } +} diff --git a/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 b/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 new file mode 100644 index 000000000..417183eef --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 @@ -0,0 +1,56 @@ +function Test-UserFromHash { + [CmdletBinding()] + param ( + # Parameter help description + [Parameter(ParameterSetName = 'username')] + [System.String] + $username, + # Parameter help description + [Parameter(ParameterSetName = 'userid')] + [System.String] + $userID + ) + begin { + # Get User Group membership + if ( -not $Global:JCRUsers ) { + $Global:JCRUsers = Get-Content -path "$JCScriptRoot/data/userHash.json" | ConvertFrom-Json -AsHashtable + } + } + process { + switch ($PSCmdlet.ParameterSetName) { + 'userid' { + # validate that the userID is in the radiusMembership hash: + if ($Global:JCRRadiusMembers[$userID]) { + # finally return the $matchedUser object + $matchedUser = $Global:JCRUsers[$userID] + } else { + $matchedUser = $null + } + $inputText = $userID + } + 'username' { + # Get the index of the user within the hashtable + $matchedIndex = $Global:JCRUsers.values.username.ToLower().IndexOf($username.ToLower()) + # Get the UserID from the keys + $matchedUserID = $Global:JCRUsers.keys[$matchedIndex] + # validate that the userID is in the radiusMembership hash: + if ($Global:JCRRadiusMembers[$matchedUserID]) { + # finally return the $matchedUser object + $matchedUser = $Global:JCRUsers[$matchedUserID] + } else { + $matchedUser = $null + } + $inputText = $username + } + } + if ($matchedUser) { + Write-Debug "Matched Username Found: $($matchedUser.username)" + } else { + Write-Warning "User specified $inputText was not found within the Radius Server Membership Lists" + return $null + } + } + end { + return $matchedUser + } +} diff --git a/scripts/automation/Radius/Functions/Private/PadCenter.ps1 b/scripts/automation/Radius/Functions/Private/PadCenter.ps1 index 2f7ae3ddb..256dbb0d5 100644 --- a/scripts/automation/Radius/Functions/Private/PadCenter.ps1 +++ b/scripts/automation/Radius/Functions/Private/PadCenter.ps1 @@ -3,7 +3,11 @@ function PadCenter { [string]$string, [char]$char ) + $maxlength = 120 $length = $host.ui.rawui.windowsize.width + if ($length -gt $maxlength) { + $length = $maxlength + } $spaces = $length - $string.Length $padLeft = $spaces / 2 + $string.Length return $string.PadLeft($padLeft, $char).PadRight($length, $char) diff --git a/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 b/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 index 1c41b3896..ee573c568 100644 --- a/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 +++ b/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 @@ -10,14 +10,14 @@ function Show-RadiusMainMenu { $cutoffDate = (Get-Date).AddDays(15).Date # Find all certs that will expire between current date and cut off date - $expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $cutoffDate + $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $cutoffDate # Get UserGroup information from Config.ps1 - $radiusUserGroup = Get-JcSdkUserGroup -Id $JCUSERGROUP | Select-Object Name - $radiusUserGroupMemberCount = (Get-JcSdkUserGroupMember -GroupId $JCUSERGROUP).Count + $radiusUserGroup = Get-JcSdkUserGroup -Id $global:JCUSERGROUP | Select-Object Name + $radiusUserGroupMemberCount = (Get-JcSdkUserGroupMember -GroupId $global:JCUSERGROUP).Count # Get SSID information from Config.ps1 - $radiusSSID = $NETWORKSSID.Split(';').Trim() + $radiusSSID = $Global:NETWORKSSID.replace(';', ' ') # Output for Users Clear-Host @@ -35,29 +35,34 @@ function Show-RadiusMainMenu { Write-Host $(PadCenter -string "Root CA Serial Number: $($rootCAInfo.serial)" -char ' ') -ForegroundColor Green Write-Host $(PadCenter -string "Root CA Expiration: $($rootCAInfo.notAfter)`n" -char ' ') -ForegroundColor Green } - if ($expiringCerts) { - Write-Host $(PadCenter -string "$($($expiringCerts.subject).Count) user certs will expire in 15 days `n" -char ' ') -ForegroundColor Red + if ($Global:expiringCerts) { + Write-Host $(PadCenter -string "$($($Global:expiringCerts.subject).Count) user certs will expire in 15 days `n" -char ' ') -ForegroundColor Red } - Write-Host $(PadCenter -string "-" -char '-') + Write-Host $(PadCenter -string " Details " -char '-') # /==== ROOT CA ==== - # ==== GROUP/SSID ==== + # ==== GROUP/SSID/Global Variables ==== Write-Host $(PadCenter -string "Radius User Group: $($radiusUserGroup.Name)" -char " ") -ForegroundColor Green Write-Host $(PadCenter -string "Radius Users: $($radiusUserGroupMemberCount)" -char " ") -ForegroundColor Green - if ($radiusSSID.count -gt 1) { - Write-Host $(PadCenter -string "Radius SSIDs:" -char " ") -ForegroundColor Green - $radiusSSID | ForEach-Object { - Write-Host $(PadCenter -string $_ -char " ") -ForegroundColor Green - } - } else { - Write-Host $(PadCenter -string "Radius SSID: $radiusSSID" -char " ") -ForegroundColor Green - Write-Host "`n" - } + Write-Host $(PadCenter -string "Radius SSID(s): $radiusSSID" -char " ") -ForegroundColor Green + Write-Host $(PadCenter -string "Last Updated User/System Data: $($Global:JCRConfig.globalVars.lastupdate)" -char " ") -ForegroundColor Green + # Write-Host "`n" + # if ($radiusSSID.count -gt 1) { + # Write-Host $(PadCenter -string "Radius SSIDs:" -char " ") -ForegroundColor Green + # $radiusSSID | ForEach-Object { + # Write-Host $(PadCenter -string $_ -char " ") -ForegroundColor Green + # } + # } else { + # Write-Host $(PadCenter -string "Radius SSID: $radiusSSID" -char " ") -ForegroundColor Green + # Write-Host "`n" + # } # /==== GROUP/SSID ==== + Write-Host $(PadCenter -string "-" -char '-') Write-Host "1: Press '1' to generate your Root Certificate." Write-Host "2: Press '2' to generate/update your User Certificate(s)." Write-Host "3: Press '3' to distribute your User Certificate(s)." Write-Host "4: Press '4' to monitor your User Certification Distribution." + Write-Host "4: Press '5' to update User/System Data" Write-Host "Q: Press 'Q' to quit." } \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 b/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 new file mode 100644 index 000000000..27a4363c5 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 @@ -0,0 +1,30 @@ +function New-SystemTable { + [CmdletBinding()] + param ( + [Parameter()] + [System.String] + $userID + ) + begin { + # Create new lists: + $systemAssociations = @() + } + process { + # Get User to System Associations: + $AssociationTable = $Global:JCRAssociations[$userID] + # $SystemUserAssociations += (Get-JCAssociation -Type user -Id $userID -TargetType system | Select-Object @{N = 'SystemID'; E = { $_.targetId } }) + foreach ($system in $AssociationTable.systemAssociations) { + # $systemInfo = $GLOBAL:SystemHash[$system.resource_object_id] + $systemTable = @{ + systemId = $system.resource_object_id + displayName = $system.hostname + osFamily = $system.device_os + } + $systemAssociations += $systemTable + } + + } + end { + return $systemAssociations + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 new file mode 100644 index 000000000..da890f362 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 @@ -0,0 +1,53 @@ +function Get-UserFromTable { + [CmdletBinding()] + param ( + [Parameter()] + [System.String] + $jsonFilePath, + [Parameter()] + [System.String] + $userid + ) + begin { + # Import User.Json/ create list if it does not exist + if (Test-Path -Path $jsonFilePath -PathType Leaf) { + $userArray = Get-Content -Raw -Path $jsonFilePath | ConvertFrom-Json -Depth 6 + # If the json is a single item, explicitly make it a list so we can add to it + If ($userArray.count -eq 1) { + $array = New-Object System.Collections.ArrayList + $array.add($userArray) + $userArray = $array + } + + } else { + New-Item -Path $jsonFilePath + $userArray = @() + } + # Get the user from the jsonData + $userObject = $Global:JCRUsers[$userid] + + } + process { + try { + $userIndex = $userArray.userid.IndexOf($userid) + if ($userIndex -ge 0) { + $userArrayObject = $userArray[$userIndex] + Write-Host "[status] $($userObject.username) found in users.json at index: $userIndex " + } else { + throw + } + } catch { + # otherwise plan to append + $userIndex = $null + Write-Host "[status] $($userObject.username) not found in users.json" + } + } + end { + if ($userArrayObject) { + return $userArrayObject, $userIndex + } else { + return $null, $null + } + } +} +Get-userFromTable -jsonFilePath "$JCScriptRoot/users.json" -userId "5ef5fee5c421153422df086c" \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/UserTable/New-UserTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/New-UserTable.ps1 new file mode 100644 index 000000000..26d763c89 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/UserTable/New-UserTable.ps1 @@ -0,0 +1,40 @@ +function New-UserTable { + [CmdletBinding()] + param ( + [Parameter()] + [System.String] + $id, + [Parameter()] + [System.String] + $username, + [Parameter()] + [System.String] + $localUsername + ) + begin { + $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 + $systemAssociations = New-SystemTable -userID $id + # for new users, just set the commandAssociation to $null as they have + # not yet been issued a command + $commandAssociations = $null + if (-not $localUsername) { + $localUsername = $username + } + $certInfo = Get-CertInfo -UserCerts -username $username + } + process { + $userTable = [PSCustomObject]@{ + userId = $id + userName = $username + localUsername = $localUsername + systemAssociations = $systemAssociations + commandAssociations = $commandAssociations + certInfo = $certInfo + } + $userArray += $userTable + + } + end { + $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" + } +} diff --git a/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 new file mode 100644 index 000000000..7d56ef56a --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 @@ -0,0 +1,79 @@ +function Set-UserTable { + [CmdletBinding(DefaultParameterSetName = 'lookup')] + param ( + [Parameter(mandatory, ParameterSetName = 'index')] + [System.String] + $index, + [Parameter(mandatory, ParameterSetName = 'lookup')] + [System.String] + $id, + [Parameter()] + [System.String] + $username, + [Parameter()] + [System.String] + $localUsername, + [Parameter()] + [System.Object] + $systemAssociationsObject, + [Parameter()] + [System.Object] + $commandAssociationsObject, + [Parameter()] + [System.Object] + $certInfoObject + ) + begin { + # Get User Array: + $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 + if ($PSBoundParameters.ContainsKey('index')) { + $userIndex = $index + $userObject = $userArray[$index] + Write-Warning "this is the old object" + $userObject + } + if (($PSBoundParameters.ContainsKey('lookup'))) { + # Get User From Table + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $id + } + + # determine if there's data to update from parameter input, else just + # use the existing data + if ($systemAssociationsObject) { + $systemAssociationsInfo = $systemAssociationsObject + } else { + $systemAssociationsInfo = $userObject.systemAssociations + } + if ($commandAssociationsObject) { + commandAssociationsInfo = $commandAssociationsObject + } else { + $commandAssociationsInfo = $userObject.commandAssociations + } + if ($certInfoObject) { + $certInfo = $certInfoObject + } else { + $certInfo = $userObject.certInfo + } + } + process { + # build the userTable object + $userTable = [PSCustomObject]@{ + userId = $userObject.userId + userName = $userObject.username + localUsername = $userObject.localUsername + systemAssociations = $systemAssociationsInfo + commandAssociations = $commandAssociationsInfo + certInfo = $certInfo + } + Write-Warning "this is the new object" + $userTable + + # set the user table to new object + $userArray[$userIndex] = $userTable + + } + end { + # update the userTable + $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" + } +} diff --git a/scripts/automation/Radius/Functions/Private/settings/Get-JCRSettingsFile.ps1 b/scripts/automation/Radius/Functions/Private/settings/Get-JCRSettingsFile.ps1 new file mode 100644 index 000000000..4dc3c78e0 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/settings/Get-JCRSettingsFile.ps1 @@ -0,0 +1,45 @@ +function Get-JCRSettingsFile { + [CmdletBinding()] + param ( + [Parameter( + DontShow, + HelpMessage = 'Returns Config.json with value, copy, write properties' + )] + [switch] + $raw + ) + + begin { + # Config should be in /PowerShell/JumpCloudModule/Config.json + $ModuleRoot = (Get-Item -Path:($global:JCScriptRoot)) + $configFilePath = join-path -path $ModuleRoot -childpath 'settings.json' + + if (-Not (test-path -path $configFilePath)) { + write-host "write new settings file $configFilePath" + # Create new file with default settings + New-JCRSettingsFile + } + } + + process { + if (-Not $raw) { + $rawConfig = Get-Content -Path $configFilePath | ConvertFrom-Json + $config = @{} + foreach ($item in $rawConfig.psobject.Properties) { + # $config.$item + $config.Add($item.Name, @{}) + foreach ($setting in $item.value.psobject.Properties) { + # $setting + $config.$($Item.Name).Add($setting.Name, $setting.value.value) + } + } + } else { + # Get Contents + $config = Get-Content -Path $configFilePath | ConvertFrom-Json + } + } + + end { + return $config + } +} diff --git a/scripts/automation/Radius/Functions/Private/settings/New-JCRSettingsFile.ps1 b/scripts/automation/Radius/Functions/Private/settings/New-JCRSettingsFile.ps1 new file mode 100644 index 000000000..246970b8b --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/settings/New-JCRSettingsFile.ps1 @@ -0,0 +1,41 @@ +function New-JCRSettingsFile { + [CmdletBinding()] + param ( + [Parameter( + HelpMessage = 'To Force Re-Creation of the Config file, set the $force parameter to $true' + )] + [switch] + $force + ) + + begin { + # Config should be in /PowerShell/JumpCloudModule/Config.json + $ModuleRoot = (Get-Item -Path:($global:JCScriptRoot)) + $configFilePath = join-path -path $ModuleRoot -childpath 'settings.json' + + # Define Default Settings for the Config file + $date = Get-Date + $config = @{ + 'globalVars' = @{ + 'lastUpdate' = @{value = $date; write = $true; copy = $true ; + } + } + } + } + + process { + # if creating the settings file for the first time, update global vars; lastupdate date + write-host "update vars" + Update-JCRGlobalVars -force + } + + end { + write-host "endblock" + if ((test-path -Path $configFilePath) -And ($force)) { + $config | ConvertTo-Json | Out-File -FilePath $configFilePath + } else { + write-host "create new settings file:" + $config | ConvertTo-Json | Out-File -FilePath $configFilePath + } + } +} diff --git a/scripts/automation/Radius/Functions/Private/settings/Set-JCRSettingsFile.ps1 b/scripts/automation/Radius/Functions/Private/settings/Set-JCRSettingsFile.ps1 new file mode 100644 index 000000000..2a28a3889 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/settings/Set-JCRSettingsFile.ps1 @@ -0,0 +1,100 @@ +function Set-JCRSettingsFile { + [CmdletBinding()] + param ( + ) + DynamicParam { + $ModuleRoot = (Get-Item -Path:($JCScriptRoot)) + $configFilePath = join-path -path $ModuleRoot -childpath 'settings.json' + + + if (test-path -path $configFilePath) { + $config = Get-Content -Path $configFilePath | ConvertFrom-Json + # Create the dictionary + $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + # Foreach key in the supplied config file: + foreach ($key in $config.PSObject.Properties) { + foreach ($item in $config.($key.Name).PSObject.Properties) { + # Skip create dynamic params for these not-writable properties: + if (($config.($key.Name).($item.Name).Write -eq $false)) { + continue + } + # Set the dynamic parameters' name + # write-host "adding dynamic param: $key$($item) $($config[$key][$item]['value'].getType().Name)" + $ParamName_Filter = "$($key.Name)$($item.Name)" + # Create the collection of attributes + $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + # If ValidateSet is specified in the config file, set the value here: + if ($config.($key.Name).($item.Name).validateSet) { + $arrSet = @($($config.($key.Name).($item.Name).'validateSet').split()) + $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) + $AttributeCollection.Add($ValidateSetAttribute) + } + # If the type of value is a bool, create a custom validateSet attribute here: + $paramType = $($config.($key.Name).($item.Name)).getType().Name + if ($paramType -eq 'boolean') { + $arrSet = @("true", "false") + $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) + $AttributeCollection.Add($ValidateSetAttribute) + } + # Create and set the parameters' attributes + $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute + $ParameterAttribute.Mandatory = $false + $ParameterAttribute.HelpMessage = "sets the $($item) settings for the $($key) feature" + # Add the attributes to the attributes collection + $AttributeCollection.Add($ParameterAttribute) + # Add the param + $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_Filter, $paramType, $AttributeCollection) + $RuntimeParameterDictionary.Add($ParamName_Filter, $RuntimeParameter) + } + } + # Returns the dictionary + return $RuntimeParameterDictionary + } + } + begin { + if ($JCAPIKEY.length -ne 40) { + Connect-JCOnline -Force | Out-Null + } + + # Config should be in /PowerShell/JumpCloudModule/Config.json + $ModuleRoot = (Get-Item -Path:($JCScriptRoot)) + $configFilePath = join-path -path $ModuleRoot -childpath 'settings.json' + + if (test-path -path $configFilePath) { + $config = Get-Content -Path $configFilePath | ConvertFrom-Json + } else { + New-JCSettingsFile + $config = Get-Content -Path $configFilePath | ConvertFrom-Json + } + } + + process { + $params = $PSBoundParameters + # update config settings + foreach ($param in $params.Keys) { + foreach ($key in $config.PSObject.Properties) { + if ($param -match $key.Name) { + # Split the name + $ParamKey = $param -split $key.Name + # assign the first group + $config.($($key.Name)).($paramKey[1]).value = $params[$param] + } + } + } + # # Re-Calculate Parallel Settings: + # if (($config.'parallel'.'Override' -eq $true) -And (($config.'parallel'.'Eligible' -eq $true))) { + # $config.'parallel'.'Calculated' = $false + # } elseif (($config.'parallel'.'Override'.'value' -eq $false) -And (($config.'parallel'.'Eligible'.'value' -eq $true))) { + # $config.'parallel'.'Calculated'.'value' = $true + # } else { + # $config.'parallel'.'Calculated'.'value' = $false + # } + } + + end { + # Write out the new settings + $config | ConvertTo-Json | Out-File -FilePath $configFilePath + # Update Global Variable + $global:JCRConfig = Get-JCRSettingsFile + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/settings/Update-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Private/settings/Update-JCRGlobalVars.ps1 new file mode 100644 index 000000000..b46abcd1b --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/settings/Update-JCRGlobalVars.ps1 @@ -0,0 +1,163 @@ +function Update-JCRGlobalVars { + [CmdletBinding()] + param ( + [Parameter()] + [switch] + $force + ) + begin { + # ensure the data directory exists to cache the json files: + if (Test-Path "$JCScriptRoot/data") { + Write-Host "[status] Data Directory Exists" + } else { + Write-Host "[status] Creating Data Directory" + New-Item -ItemType Directory -Path "$JCScriptRoot/data" + } + + # get settings file + $lastUpdateTimespan = New-TimeSpan -Start $global:JCRConfig.globalvars.lastupdate -end (Get-Date) + if ($lastUpdateTimespan.TotalHours -gt 24) { + $update = $true + } else { + $update = $false + } + if ($force) { + $update = $true + } + } + process { + switch ($update) { + $true { + # update the global variables + $systems = Get-DynamicHash -Object System -returnProperties hostname, os, osFamily, version, fde, lastContact + $users = Get-DynamicHash -Object User -returnProperties email, employeeIdentifier, department, suspended, location, Addresses, manager, sudo, Displayname, username, systemUsername + $users | ForEach-Object { $_ | Add-Member -name "userId" -value $_ -Type NoteProperty -force } + # Get Radius membership list: + $radiusMembers = Get-JcSdkUserGroupMember -GroupId $Global:JCUSERGROUP + # Get Report Hash: + $headers = @{ + "accept" = "application/json"; + "x-api-key" = $Env:JCApiKey; + "x-org-id" = $Env:JCOrgId + } + # request new user to device report: + $reportRequest = Invoke-RestMethod -Uri 'https://api.jumpcloud.com/insights/directory/v1/reports/users-to-devices' -Method POST -Headers $headers + # now fetch available reports: + do { + $reportList = Invoke-RestMethod -Uri 'https://api.jumpcloud.com/insights/directory/v1/reports?sort=CREATED_AT' -Method GET -Headers $headers + $lastReport = $reportList | Where-Object { $_.id -eq $reportRequest.id } + if ($lastReport.status -eq 'PENDING') { + Write-Warning "[status] waiting 20s for jumpcloud report to complete" + start-sleep -Seconds 20 + } + } until ($lastReport.status -eq 'COMPLETED') + # download json + $artifactID = ($lastReport.artifacts | Where-Object { $_.format -eq 'json' }).id + $reportID = $lastReport.id + $reportContent = Invoke-RestMethod -Uri "https://api.jumpcloud.com/insights/directory/v1/reports/$reportID/artifacts/$artifactID/content" -Method GET -Headers $headers + # create the hashtable: + $userAssociationList = New-Object System.Collections.Hashtable + foreach ($item in $reportContent) { + if ($item.user_object_id -And $item.resource_object_id) { + if (-not $userAssociationList[$item.user_object_id]) { + $userAssociationList.add( + $item.user_object_id, @{ + 'systemAssociations' = @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, device_os); + 'userData' = @($item | Select-Object -Property email, username) + }) + } else { + $userAssociationList[$item.user_object_id].systemAssociations += @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, device_os) + } + } + } + } + $false { + Write-Warning "It's been $($lastUpdateTimespan.hours) hours since we last pulled user, system and association data, no need to update" + $userAssociationList = Get-Content -Raw -Path "$JCScriptRoot/data/associationHash.json" | ConvertFrom-Json -Depth 6 -AsHashtable + } + } + + # # validate that the system association data is correct in users.json: + $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 + foreach ($userid in $Global:JCRRadiusMembers.keys) { + $MatchedUser = $GLOBAL:JCRUsers[$userid] + Write-Host "checking out $($MatchedUser.username) userid: $userid" + $userArrayObject, $userIndex = Get-UserFromTable -userID $userid -jsonFilePath "$JCScriptRoot/users.json" + + if ($userIndex -ge 0) { + # $userArrayObject + $currentSystemObject = $userArrayObject.systemAssociations + $incomingSystemObject = $userAssociationList[$userid].systemAssociations + # determine if there's some difference that needs to be recorded: + try { + $difference = Compare-Object -ReferenceObject $currentSystemObject.systemId -DifferenceObject $incomingSystemObject.systemId + if ($difference) { + + Set-UserTable -index $userIndex -username $MatchedUser.username -localUsername $MatchedUser.systemUsername -systemAssociationsObject ($incomingSystemObject | ConvertFrom-HashTable) + } + } catch { + <#Do this if a terminating exception happens#> + $difference = $null + } + # if ($difference) { + # # if there's a difference in systemIDS, update table with the incomingSystemObject + # # $userTable = New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername + # # Update-JsonData -jsonFilePath "$JCScriptRoot/users.json" -userID $userID -updatedUserTable $userTable + # } + } else { + # case for new user + New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername + } + + } + # lastly validate users that should no longer be recorded: + $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 + foreach ($user in $userArray) { + # If userID from users.json is no longer in RadiusMembers.keys, then: + If ( -Not ($user.userId -in $Global:JCRRadiusMembers.keys) ) { + # Get User From Table + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $user.userId + $userArray = $userArray | Where-Object { $_.userID -ne $user.userId } + # "removing $($user.userid)" + } + # Remove the User From Table + } + $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" + } + end { + switch ($update) { + $true { + # write hash cache + $users | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/userHash.json" + $systems | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/systemHash.json" + $userAssociationList | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/associationHash.json" + # add the username to the membership hash + $radiusMemberList = New-Object System.Collections.Hashtable + foreach ($member in $radiusMembers) { + $radiusMemberList.Add( + $member.toID, @{ + 'userID' = $member.toID + 'username' = $users[$member.toID].username + }) + } + # $radiusMemberList = ($radiusMembers | select @{name = 'userID'; expression = { $_.toID } }, @{name = 'username'; expression = { $users[$_.toID].username } }) + $radiusMemberList | ConvertTo-Json | Out-File "$JCScriptRoot/data/radiusMembers.json" + # set global vars + $Global:JCRUsers = $users + $Global:JCRSystems = $systems + $Global:JCRAssociations = $userAssociationList + $Global:JCRRadiusMembers = $radiusMemberList + # update the settings date + Set-JCRSettingsFile -globalVarslastUpdate (Get-Date) + } + $false { + Write-Warning "pulling saved data from data file:" + # set global vars from local cache + $Global:JCRUsers = Get-Content -path "$JCScriptRoot/data/userHash.json" | ConvertFrom-Json -AsHashtable + $Global:JCRSystems = Get-Content -path "$JCScriptRoot/data/systemHash.json" | ConvertFrom-Json -AsHashtable + $Global:JCRAssociations = Get-Content -path "$JCScriptRoot/data/associationHash.json" | ConvertFrom-Json -AsHashtable + $Global:JCRRadiusMembers = Get-Content -path "$JCScriptRoot/data/radiusMembers.json" | ConvertFrom-Json -AsHashtable + } + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 index be6252fd9..59367737f 100644 --- a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 @@ -1,3 +1,5 @@ +# TODO: param for testing + # Import Global Config: . "$JCScriptRoot/config.ps1" Connect-JCOnline $JCAPIKEY -force @@ -24,7 +26,7 @@ if ($env:certKeyPassword) { } # Import the functions -Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force +# Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force # Import User.Json/ create list if it does not exist if (Test-Path -Path "$JCScriptRoot/users.json" -PathType Leaf) { @@ -42,23 +44,6 @@ if (Test-Path -Path "$JCScriptRoot/users.json" -PathType Leaf) { $userArray = @() } -# Get user membership of group -# TODO: update group membership if last date is -gt 30 mins -if ( -not $GLOBAL:RadiusUserMembership ) { - $GLOBAL:RadiusUserMembership = Get-JCUserGroupMember -ByID $JCUSERGROUP - $groupMembers = $GLOBAL:RadiusUserMembership -} else { - $groupMembers = $GLOBAL:RadiusUserMembership -} -if ($groupMembers) { - Write-Host "[status] Found $($groupmembers.count) users in Radius User Group" -} - -# Get SystemHash -# TODO: update group membership if last date is -gt 30 mins -# TODO: global variable and track time last updated -$SystemHash = Get-JCSystem -returnProperties displayName, os - # Create UserCerts dir if (Test-Path "$JCScriptRoot/UserCerts") { Write-Host "[status] User Cert Directory Exists" @@ -68,155 +53,167 @@ if (Test-Path "$JCScriptRoot/UserCerts") { } function Show-GenerationMenu { - $title = 'JumpCloud Radius Cert Deployment' + $title = ' JumpCloud Radius Cert Deployment ' Clear-Host - Write-Host "================ $Title ================" - Write-Host "1: Press '1' to generate new certificates for NEW RADIUS users. $([char]0x1b)[96mNOTE: This will only generate certificates for users who have not yet had a certificate generated." - Write-Host "2: Press '2' to generate new certificates for ONE Specific RADIUS user. $([char]0x1b)[96mNOTE: you will be prompted to overwrite any previously generated certificates" - Write-Host "3: Press '3' to re-generate new certificates for ALL users. $([char]0x1b)[96mNOTE: This will overwrite any previously generated certificates" - Write-Host "4: Press '4' to re-generate new certificates for users who's cert is set to expire shortly. $([char]0x1b)[96mNOTE: This will overwrite any previously generated certificates" + Write-Host $(PadCenter -string $Title -char '=') + Write-Host $(PadCenter -string "Select an option below to generate/regenerate user certificates`n" -char ' ') -ForegroundColor Yellow + # ==== instructions ==== + # TODO: move notes from below into a more legible location + # Write-Host $(PadCenter -string ' User Certificate Options ' -char '-') + # Write-Host $(PadCenter -string "$([char]0x1b)[96m[]: This will only generate certificates for users who do not have a certificate file yet.`n" -char ' ') + + if ($Global:expiringCerts) { + Write-Host $(PadCenter -string ' Certs Expiring Soon ' -char '-') + $Global:expiringCerts | Format-Table -Property username, @{name = 'Remaining Days'; expression = { (New-TimeSpan -Start (Get-Date) -End ("$($_.notAfter)")).Days } }, @{name = "Expires On"; expression = { $_.notAfter } } + } + + Write-Host $(PadCenter -string "-" -char '-') + Write-Host "1: Press '1' to generate new certificates for NEW RADIUS users. `n`t$([char]0x1b)[96mNOTE: This will only generate certificates for users who do not have a certificate file yet." + Write-Host "2: Press '2' to generate new certificates for ONE RADIUS user. `n`t$([char]0x1b)[96mNOTE: you will be prompted to overwrite any previously generated certificates" + Write-Host "3: Press '3' to re-generate new certificates for ALL users. `n`t$([char]0x1b)[96mNOTE: This will overwrite any local generated certificates" + Write-Host "4: Press '4' to re-generate new certificates for users who's cert is set to expire shortly. `n`t$([char]0x1b)[96mNOTE: This will overwrite any local generated certificates" Write-Host "E: Press 'E' to exit." } Do { Show-GenerationMenu $confirmation = Read-Host "Please make a selection" + switch ($confirmation) { '1' { # process all users, generate certificates for uses who do not yet have a certificate - foreach ($user in $groupMembers) { + # Get List of files, figure out userList of users who've not had a cert generated: + $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts/" -Filter "*-client-signed.pfx" + $certFileList = New-Object System.Collections.ArrayList + foreach ($file in $userCertFiles) { + $userFromFile = $file.BaseName.Replace("-client-signed", "") + $certFileList.add($userFromFile) | Out-Null + } + # Get each RadiusMember User: + foreach ($user in $Global:JCRRadiusMembers.keys) { # Get the user details: - $MatchedUser = get-webjcuser -userID $user.id - Write-Host "Generating Cert for user: $($MatchedUser.username)" - if ($matchedUser.id -in $userArray.userid) { - if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { - Write-Host "[status] $($MatchedUser.username) already has certs generated... skipping" - } else { - # Generate a new cert for this user: - Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null - } + $MatchedUser = $GLOBAL:JCRUsers[$user] + # If the user has a certificate continue + if ($MatchedUser.username -in $certFileList) { + #if the cert already exists, break + Write-Host "[status] $($MatchedUser.username) has a certificate already. skipping..." } else { - Write-Host "[status] $($MatchedUser.username) not found in users.json" - - # Find user system associations - $SystemUserAssociations = @() - $SystemUserAssociations += (Get-JCAssociation -Type user -Id $MatchedUser.id -TargetType system | Select-Object @{N = 'SystemID'; E = { $_.targetId } }) - - $systemAssociations = @() - foreach ($system in $SystemUserAssociations) { - $systemInfo = $SystemHash | Where-Object _id -EQ $system.SystemID - $systemTable = @{ - systemId = $systemInfo._id - displayName = $systemInfo.displayName - osFamily = $systemInfo.os - } - $systemAssociations += $systemTable - } - - $userTable = @{ - userId = $MatchedUser.id - userName = $MatchedUser.username - localUsername = $(If ($MatchedUser.hasLocalUsername) { - $matchedUser.localUsername - } else { - $matchedUser.username - }) - systemAssociations = $systemAssociations - commandAssociations = @() - } - - if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { - Write-Host "[status] $($MatchedUser.username) has certs generated... adding to users.json" + # if the user does not have a certificate, generate + Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + # Get the user from the users.json file + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $MatchedUser.id + # set user table: + if ($userIndex -ge 0) { + # update the new certificate info & set commandAssociation to $null + # TODO: commandAssociation not being set to null + $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username + Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null } else { - Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + # Create a new table entry + New-UserTable -id $MatchedUser.id -username $MatchedUser.username -localUsername $MatchedUser.systemUsername } - $userArray += $userTable } } - # Update UserArray - $userArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" Break } '2' { - # process individual users, generate certificates for users who have not been added to users.json - # if users have been added to users.json, prompt to re-generate - Clear-Variable "ConfirmUser" - while (-not $confirmUser) { + while (-not $confirmUser.id) { $confirmationUser = Read-Host "Enter the Username or UserID of the user" - $confirmUser = Test-User -username $confirmationUser -debug + $confirmUser = Test-UserFromHash -username $confirmationUser -debug } - # get data about the user - $MatchedUser = get-webjcuser -userID $confirmUser.UserID # Generate a new cert for this user: - if ($matchedUser.id -in $userArray.userid) { - if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { - Do { - $overwrite = Read-Host "do you want to overwrite yn?" - switch ($overwrite) { - 'y' { - Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null - Break + if (Test-Path -Path "$JCScriptRoot/UserCerts/$($confirmUser.username)-client-signed.pfx") { + Do { + $overwrite = Read-Host "do you want to overwrite y/n?" + switch ($overwrite) { + 'y' { + Generate-UserCert -CertType $CertType -user $confirmUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + Break - } - 'n' { - Write-Host "[status] $($MatchedUser.username) already has certs generated... skipping" - Break + } + 'n' { + Write-Host "[status] $($confirmUser.username) already has certs generated... skipping" + Break - } } - } until (($overwrite -eq "y") -or ($overwrite -eq "n")) - } else { - # Generate a new cert for this user: - Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null - } - } else { - Write-Host "[status] $($MatchedUser.username) not found in users.json" - - # Find user system associations - $SystemUserAssociations = @() - $SystemUserAssociations += (Get-JCAssociation -Type user -Id $MatchedUser.id -TargetType system | Select-Object @{N = 'SystemID'; E = { $_.targetId } }) - - $systemAssociations = @() - foreach ($system in $SystemUserAssociations) { - $systemInfo = $SystemHash | Where-Object _id -EQ $system.SystemID - $systemTable = @{ - systemId = $systemInfo._id - displayName = $systemInfo.displayName - osFamily = $systemInfo.os } - $systemAssociations += $systemTable - } - - $userTable = @{ - userId = $MatchedUser.id - userName = $MatchedUser.username - localUsername = $(If ($MatchedUser.hasLocalUsername) { - $matchedUser.localUsername - } else { - $matchedUser.username - }) - systemAssociations = $systemAssociations - commandAssociations = @() - } - - if (Test-Path -Path "$JCScriptRoot/UserCerts/$($MatchedUser.username)-client-signed.pfx") { - Write-Host "[status] $($MatchedUser.username) has certs generated... adding to users.json" - } else { - Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null - } - $userArray += $userTable + } until (($overwrite -eq "y") -or ($overwrite -eq "n")) + } else { + # Generate a new cert for this user: + Generate-UserCert -CertType $CertType -user $confirmUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null } - # Update UserArray - $userArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + # Get the user from the users.json file + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id + # set user table: + if ($userIndex -ge 0) { + Write-Warning "setting existing table for user $($confirmUser.username)" + # update the new certificate info & set commandAssociation to $null + # TODO: commandAssociation not being set to null + $certInfo = Get-CertInfo -UserCerts -username $confirmUser.username + Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null + } else { + Write-Warning "setting new table for user $($confirmUser.username)" + # Create a new table entry + New-UserTable -id $confirmUser.id -username $confirmUser.username -localUsername $confirmUser.systemUsername + } + # clear the user variable: + Clear-Variable "ConfirmUser" Break } '3' { - # process new users, re-generate certificates for users who have not been added to users.json - # TODO: implement + # re-generate new certificates for ALL users + foreach ($user in $Global:JCRRadiusMembers.keys) { + # Get the user details: + $MatchedUser = $GLOBAL:JCRUsers[$user] + # Regenerate + Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + # Get the user from the users.json file + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $MatchedUser.id + # set user table: + if ($userIndex -ge 0) { + # update the new certificate info & set commandAssociation to $null + # TODO: commandAssociation not being set to null + $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username + Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null + } else { + # Create a new table entry + New-UserTable -id $MatchedUser.id -username $MatchedUser.username -localUsername $MatchedUser.systemUsername + } + } Break } '4' { - #TODO: overwrite soon to expire certificates + do { + $overwrite = Read-Host "do you want to overwrite yn?" + switch ($overwrite) { + 'y' { + foreach ($userCert in $Global:expiringCerts) { + $userArrayIndex = $userArray.username.IndexOf($userCert.username) + $IdentifiedUser = $userArray[$userArrayIndex] + $MatchedUser = $GLOBAL:JCRUsers[$IdentifiedUser.userid] + Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null + # Get the user from the users.json file + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $MatchedUser.id + # set user table: + if ($userIndex -ge 0) { + # update the new certificate info & set commandAssociation to $null + # TODO: commandAssociation not being set to null + $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username + Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null + } else { + # Create a new table entry + New-UserTable -id $MatchedUser.id -username $MatchedUser.username -localUsername $MatchedUser.systemUsername + } + } + Break + } + 'n' { + Write-Host "[status] $($MatchedUser.username) already has certs generated... skipping" + Break + + } + } + } until (($overwrite -eq "y") -or ($overwrite -eq "n")) } 'E' { Write-Host "Returning to main menu" diff --git a/scripts/automation/Radius/Start-RadiusDeployment.ps1 b/scripts/automation/Radius/Start-RadiusDeployment.ps1 index 28aa5d585..21dcc1764 100644 --- a/scripts/automation/Radius/Start-RadiusDeployment.ps1 +++ b/scripts/automation/Radius/Start-RadiusDeployment.ps1 @@ -25,6 +25,8 @@ do { . "$JCScriptRoot/Functions/Public/Distribute-UserCerts.ps1" } '4' { . "$JCScriptRoot/Functions/Public/Monitor-CertDeployment.ps1" + } '5' { + . "$JCScriptRoot/Functions/Public/Update-JCRData.ps1" } } Pause From 7b737ee48cc27e6b8697a669abf1698fc375ca27 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Tue, 2 Jan 2024 10:57:37 -0700 Subject: [PATCH 008/346] fix for global variables setup --- .../Functions/JCRadiusCertDeployment.psm1 | 9 +- .../Private/UserTable/Get-UserFromTable.ps1 | 1 - ...CRGlobalVars.ps1 => Get-JCRGlobalVars.ps1} | 94 ++++++------------- .../Private/settings/New-JCRSettingsFile.ps1 | 2 +- .../Private/settings/Update-JCRUsersJson.ps1 | 61 ++++++++++++ .../Radius/Start-RadiusDeployment.ps1 | 5 +- 6 files changed, 100 insertions(+), 72 deletions(-) rename scripts/automation/Radius/Functions/Private/settings/{Update-JCRGlobalVars.ps1 => Get-JCRGlobalVars.ps1} (65%) create mode 100644 scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 diff --git a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 b/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 index a3d816d39..33583af7c 100644 --- a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 +++ b/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 @@ -12,8 +12,7 @@ Foreach ($Import in $Private) { $global:JCRConfig = Get-JCRSettingsFile -Update-JCRGlobalVars - -# if ($global:JCConfig.globalVars.lastupdate - ) { - -# } \ No newline at end of file +# Get global variables or update if necessary +Get-JCRGlobalVars +# Update Users Json if there's a change +Update-JCRUsersJson diff --git a/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 index da890f362..0b6f5f433 100644 --- a/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 @@ -50,4 +50,3 @@ function Get-UserFromTable { } } } -Get-userFromTable -jsonFilePath "$JCScriptRoot/users.json" -userId "5ef5fee5c421153422df086c" \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/settings/Update-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 similarity index 65% rename from scripts/automation/Radius/Functions/Private/settings/Update-JCRGlobalVars.ps1 rename to scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 index b46abcd1b..354de3dda 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Update-JCRGlobalVars.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 @@ -1,4 +1,4 @@ -function Update-JCRGlobalVars { +function Get-JCRGlobalVars { [CmdletBinding()] param ( [Parameter()] @@ -24,6 +24,17 @@ function Update-JCRGlobalVars { if ($force) { $update = $true } + + # also validate that the data files are non-null, if they are, force update] + $requiredHashFiles = @('associationHash.json', 'radiusMembers.json', 'systemHash.json', 'userHash.json') + foreach ($file in $requiredHashFiles) { + $fileContents = Get-Content "$JCScriptRoot/data/$file" + if ([string]::IsNullOrEmpty($file)) { + Write-Host "[status] $JCScriptRoot/data/$file file is null, updating global variables" + $update = $true + break + } + } } process { switch ($update) { @@ -31,9 +42,18 @@ function Update-JCRGlobalVars { # update the global variables $systems = Get-DynamicHash -Object System -returnProperties hostname, os, osFamily, version, fde, lastContact $users = Get-DynamicHash -Object User -returnProperties email, employeeIdentifier, department, suspended, location, Addresses, manager, sudo, Displayname, username, systemUsername - $users | ForEach-Object { $_ | Add-Member -name "userId" -value $_ -Type NoteProperty -force } + # $users | ForEach-Object { $_ | Add-Member -name "userId" -value $_ -Type NoteProperty -force } # Get Radius membership list: $radiusMembers = Get-JcSdkUserGroupMember -GroupId $Global:JCUSERGROUP + # add the username to the membership hash + $radiusMemberList = New-Object System.Collections.Hashtable + foreach ($member in $radiusMembers) { + $radiusMemberList.Add( + $member.toID, @{ + 'userID' = $member.toID + 'username' = $users[$member.toID].username + }) + } # Get Report Hash: $headers = @{ "accept" = "application/json"; @@ -70,78 +90,24 @@ function Update-JCRGlobalVars { } } } + + # finally write out the data to file: + Write-host "writing files" + $users | ConvertTo-Json -Depth 100 -Compress | Out-File "$JCScriptRoot/data/userHash.json" + $systems | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/systemHash.json" + $userAssociationList | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/associationHash.json" + $radiusMemberList | ConvertTo-Json | Out-File "$JCScriptRoot/data/radiusMembers.json" } $false { Write-Warning "It's been $($lastUpdateTimespan.hours) hours since we last pulled user, system and association data, no need to update" $userAssociationList = Get-Content -Raw -Path "$JCScriptRoot/data/associationHash.json" | ConvertFrom-Json -Depth 6 -AsHashtable } } - - # # validate that the system association data is correct in users.json: - $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 - foreach ($userid in $Global:JCRRadiusMembers.keys) { - $MatchedUser = $GLOBAL:JCRUsers[$userid] - Write-Host "checking out $($MatchedUser.username) userid: $userid" - $userArrayObject, $userIndex = Get-UserFromTable -userID $userid -jsonFilePath "$JCScriptRoot/users.json" - - if ($userIndex -ge 0) { - # $userArrayObject - $currentSystemObject = $userArrayObject.systemAssociations - $incomingSystemObject = $userAssociationList[$userid].systemAssociations - # determine if there's some difference that needs to be recorded: - try { - $difference = Compare-Object -ReferenceObject $currentSystemObject.systemId -DifferenceObject $incomingSystemObject.systemId - if ($difference) { - - Set-UserTable -index $userIndex -username $MatchedUser.username -localUsername $MatchedUser.systemUsername -systemAssociationsObject ($incomingSystemObject | ConvertFrom-HashTable) - } - } catch { - <#Do this if a terminating exception happens#> - $difference = $null - } - # if ($difference) { - # # if there's a difference in systemIDS, update table with the incomingSystemObject - # # $userTable = New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername - # # Update-JsonData -jsonFilePath "$JCScriptRoot/users.json" -userID $userID -updatedUserTable $userTable - # } - } else { - # case for new user - New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername - } - - } - # lastly validate users that should no longer be recorded: - $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 - foreach ($user in $userArray) { - # If userID from users.json is no longer in RadiusMembers.keys, then: - If ( -Not ($user.userId -in $Global:JCRRadiusMembers.keys) ) { - # Get User From Table - $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $user.userId - $userArray = $userArray | Where-Object { $_.userID -ne $user.userId } - # "removing $($user.userid)" - } - # Remove the User From Table - } - $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" } end { switch ($update) { $true { - # write hash cache - $users | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/userHash.json" - $systems | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/systemHash.json" - $userAssociationList | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/associationHash.json" - # add the username to the membership hash - $radiusMemberList = New-Object System.Collections.Hashtable - foreach ($member in $radiusMembers) { - $radiusMemberList.Add( - $member.toID, @{ - 'userID' = $member.toID - 'username' = $users[$member.toID].username - }) - } - # $radiusMemberList = ($radiusMembers | select @{name = 'userID'; expression = { $_.toID } }, @{name = 'username'; expression = { $users[$_.toID].username } }) - $radiusMemberList | ConvertTo-Json | Out-File "$JCScriptRoot/data/radiusMembers.json" + # set global vars $Global:JCRUsers = $users $Global:JCRSystems = $systems diff --git a/scripts/automation/Radius/Functions/Private/settings/New-JCRSettingsFile.ps1 b/scripts/automation/Radius/Functions/Private/settings/New-JCRSettingsFile.ps1 index 246970b8b..6c105376b 100644 --- a/scripts/automation/Radius/Functions/Private/settings/New-JCRSettingsFile.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/New-JCRSettingsFile.ps1 @@ -26,7 +26,7 @@ function New-JCRSettingsFile { process { # if creating the settings file for the first time, update global vars; lastupdate date write-host "update vars" - Update-JCRGlobalVars -force + Get-JCRGlobalVars -force } end { diff --git a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 new file mode 100644 index 000000000..ccb79c2b8 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 @@ -0,0 +1,61 @@ +function Update-JCRUsersJson { + [CmdletBinding()] + param ( + [Parameter()] + [switch] + $force + ) + begin { + + } + process { + # validate that the system association data is correct in users.json: + $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 + foreach ($userid in $Global:JCRRadiusMembers.keys) { + $MatchedUser = $GLOBAL:JCRUsers[$userid] + Write-Host "checking out $($MatchedUser.username) userid: $userid" + $userArrayObject, $userIndex = Get-UserFromTable -userID $userid -jsonFilePath "$JCScriptRoot/users.json" + + if ($userIndex -ge 0) { + # $userArrayObject + $currentSystemObject = $userArrayObject.systemAssociations + $incomingSystemObject = $Global:JCRAssociations[$userid].systemAssociations + # determine if there's some difference that needs to be recorded: + try { + $difference = Compare-Object -ReferenceObject $currentSystemObject.systemId -DifferenceObject $incomingSystemObject.systemId + if ($difference) { + + Set-UserTable -index $userIndex -username $MatchedUser.username -localUsername $MatchedUser.systemUsername -systemAssociationsObject ($incomingSystemObject | ConvertFrom-HashTable) + } + } catch { + <#Do this if a terminating exception happens#> + $difference = $null + } + # if ($difference) { + # # if there's a difference in systemIDS, update table with the incomingSystemObject + # # $userTable = New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername + # # Update-JsonData -jsonFilePath "$JCScriptRoot/users.json" -userID $userID -updatedUserTable $userTable + # } + } else { + # case for new user + New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername + } + + } + # lastly validate users that should no longer be recorded: + $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 + foreach ($user in $userArray) { + # If userID from users.json is no longer in RadiusMembers.keys, then: + If ( -Not ($user.userId -in $Global:JCRRadiusMembers.keys) ) { + # Get User From Table + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $user.userId + $userArray = $userArray | Where-Object { $_.userID -ne $user.userId } + # "removing $($user.userid)" + } + # Remove the User From Table + } + } + end { + $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Start-RadiusDeployment.ps1 b/scripts/automation/Radius/Start-RadiusDeployment.ps1 index 21dcc1764..5d3425b7e 100644 --- a/scripts/automation/Radius/Start-RadiusDeployment.ps1 +++ b/scripts/automation/Radius/Start-RadiusDeployment.ps1 @@ -1,6 +1,9 @@ # Import Global Config: +Write-Verbose 'Verifying JCAPI Key' +if ($JCAPIKEY.length -ne 40) { + Connect-JCOnline -force +} . "$psscriptroot/config.ps1" -Connect-JCOnline $JCAPIKEY -force ################################################################################ # Do not modify below From bba10cbf973028b7ac150cb00e710d002db9db5d Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Tue, 2 Jan 2024 14:56:45 -0700 Subject: [PATCH 009/346] distribute by username, more TODOs --- .../Private/Deploy-UserCertificate.ps1 | 446 +++++++++ .../Private/UserTable/Set-UserTable.ps1 | 2 +- .../commands/get-commandByUsername.ps1 | 27 + .../commands/get-queuedCommandByUser.ps1 | 34 + .../commands/invoke-commandByUserId.ps1 | 51 + .../Functions/Public/Distribute-UserCerts.ps1 | 913 ++++++++++-------- 6 files changed, 1065 insertions(+), 408 deletions(-) create mode 100644 scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/commands/get-commandByUsername.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/commands/get-queuedCommandByUser.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 b/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 new file mode 100644 index 000000000..4207fc17a --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 @@ -0,0 +1,446 @@ +function Deploy-UserCertificate { + [CmdletBinding()] + param ( + # Parameter help description + [Parameter(Mandatory)] + [System.Object[]] + $userObject, + # Parameter help description + [Parameter()] + [switch] + $force + ) + + begin { + # $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 10 + + # take stock of the user object + + } + + process { + foreach ($user in $userObject) { + + #### Begin removal of queued commands + existing command: + # TODO: get existing command for the user + remove + Write-Warning "clearing existing commands for $($user.username)" + $radiusCommandsByUser = Get-CommandByUsername -username $user.username + foreach ($command in $radiusCommandsByUser) { + Remove-JcSdkCommand -Id $command.id | Out-Null + } + Write-Warning "clearing queued commands for $($user.username)" + # TODO: get queued command for the user + remove + $queuedRadiusCommandsByUser = Get-queuedCommandByUser -username $user.username + foreach ($queuedCommand in $queuedRadiusCommandsByUser) { + Clear-JCQueuedCommand -workflowId $queuedCommand.id | Out-Null + } + # now clear out the user command associations from users.json + $User.commandAssociations = @() + #### End removal of queued commands + existing command + Write-Warning "PROCESSING:" + $user + Write-Warning "..." + # Get certificate and zip to upload to Commands + $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -Filter "$($user.userName)-*" + # set crt and pfx filepaths + $userCrt = ($userCertFiles | Where-Object { $_.Name -match "crt" }).FullName + $userPfx = ($userCertFiles | Where-Object { $_.Name -match "pfx" }).FullName + # define .zip name + $userPfxZip = "$JCScriptRoot/UserCerts/$($user.userName)-client-signed.zip" + # get certInfo for commands: + $certInfo = Get-CertInfo -UserCerts -username $user.username + + # $certInfo = Invoke-Expression "$opensslBinary x509 -in $($userCrt) -enddate -serial -subject -issuer -noout" + # $certHash = @{} + # $certInfo | ForEach-Object { + # $property = $_ | ConvertFrom-StringData + # $certHash += $property + # } + switch ($certType) { + 'EmailSAN' { + # set cert identifier to SAN email of cert + $sanID = Invoke-Expression "$opensslBinary x509 -in $($userCrt) -ext subjectAltName -noout" + $regex = 'email:(.*?)$' + $subjMatch = Select-String -InputObject "$($sanID)" -Pattern $regex + $certIdentifier = $subjMatch.matches.Groups[1].value + # in macOS search user certs by email + $macCertSearch = 'e' + } + 'EmailDN' { + # Else set cert identifier to email of cert subject + $regex = 'emailAddress = (.*?)$' + $subjMatch = Select-String -InputObject "$($certHash.Subject)" -Pattern $regex + $certIdentifier = $subjMatch.matches.Groups[1].value + # in macOS search user certs by email + $macCertSearch = 'e' + } + 'UsernameCn' { + # if username just set cert identifier to username + $certIdentifier = $($user.userName) + # in macOS search user certs by common name (username) + $macCertSearch = 'c' + } + } + # Create the zip + Compress-Archive -Path $userPfx -DestinationPath $userPfxZip -CompressionLevel NoCompression -Force + # Find OS of System + switch ($user.systemAssociations.device_os) { + 'macOS' { + # Get the macOS system ids + $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'macOS' } | Select-Object systemId + + # Check to see if previous commands exist + $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" + + if ($Command.Count -ge 1) { + $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):MacOSX command already exists, skipping..." + continue + } + + # Create new Command and upload the signed pfx + try { + $CommandBody = @{ + Name = "RadiusCert-Install:$($user.userName):MacOSX" + Command = @" +unzip -o /tmp/$($user.userName)-client-signed.zip -d /tmp +chmod 755 /tmp/$($user.userName)-client-signed.pfx +currentUser=`$(/usr/bin/stat -f%Su /dev/console) +currentUserUID=`$(id -u "`$currentUser") +currentCertSN="$($certHash.serial)" +networkSsid="$($NETWORKSSID)" +# store orig case match value +caseMatchOrigValue=`$(shopt -p nocasematch; true) +# set to case-insensitive +shopt -s nocasematch +userCompare="$($user.localUsername)" +if [[ "`$currentUser" == "`$userCompare" ]]; then + # restore case match type + `$caseMatchOrigValue + certs=`$(security find-certificate -a -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain) + regexSHA='SHA-1 hash: ([0-9A-F]{5,40})' + regexSN='"snbr"=0x([0-9A-F]{5,40})' + global_rematch() { + # Set local variables + local s=`$1 regex=`$2 + # While string matches regex expression + while [[ `$s =~ `$regex ]]; do + # Echo out the match + echo "`${BASH_REMATCH[1]}" + # Remove the string + s=`${s#*"`${BASH_REMATCH[1]}"} + done + } + # Save results + # Get Text Results + textSHA=`$(global_rematch "`$certs" "`$regexSHA") + # Set as array for SHA results + arraySHA=(`$textSHA) + # Get Text Results + textSN=`$(global_rematch "`$certs" "`$regexSN") + # Set as array for SN results + arraySN=(`$textSN) + # set import var + import=true + if [[ `${#arraySN[@]} == `${#arraySHA[@]} ]]; then + len=`${#arraySN[@]} + for (( i=0; i<`$len; i++ )); do + if [[ `$currentCertSN == `${arraySN[`$i]} ]]; then + echo "Found Cert: SN: `${arraySN[`$i]} SHA: `${arraySHA[`$i]}" + installedCertSN=`${arraySN[`$i]} + installedCertSHA=`${arraySHA[`$i]} + # if cert is installed, no need to update + import=false + else + echo "Removing previously installed radius cert:" + echo "SN: `${arraySN[`$i]} SHA: `${arraySHA[`$i]}" + security delete-certificate -Z "`${arraySHA[`$i]}" /Users/$($user.localUsername)/Library/Keychains/login.keychain + fi + done + + else + echo "array length mismatch, will not delete old certs" + fi + + if [[ `$import == true ]]; then + /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security import /tmp/$($user.userName)-client-signed.pfx -x -k /Users/$($user.localUsername)/Library/Keychains/login.keychain -P $JCUSERCERTPASS -T "/System/Library/SystemConfiguration/EAPOLController.bundle/Contents/Resources/eapolclient" + if [[ `$? -eq 0 ]]; then + echo "Import Success" + # get the SHA hash of the newly imported cert + installedCertSN=`$(/bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security find-certificate -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain | grep snbr | awk '{print `$1}' | sed 's/"snbr"=0x//g') + if [[ `$installedCertSN == `$currentCertSN ]]; then + installedCertSHA=`$(/bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security find-certificate -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain | grep SHA-1 | awk '{print `$3}') + fi + + else + echo "import failed" + exit 4 + fi + else + echo "cert already imported" + fi + + # check if the cert secruity preference is set: + IFS=';' read -ra network <<< "`$networkSsid" + for i in "`${network[@]}"; do + echo "begin setting network SSID: `$i" + if /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security get-identity-preference -s "com.apple.network.eap.user.identity.wlan.ssid.`$i" -Z "`$installedCertSHA"; then + echo "it was already set" + else + echo "certificate not linked from SSID: `$i to certSN: `$currentCertSN, setting now" + /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security set-identity-preference -s "com.apple.network.eap.user.identity.wlan.ssid.`$i" -Z "`$installedCertSHA" + if [[ `$? -eq 0 ]]; then + echo "SSID: `$i and certificate linked" + else + echo "Could not associate SSID: `$i and certifiacte" + fi + fi + done + + # print results + echo "################## Cert Install Results ##################" + echo "Installed Cert SN: `$installedCertSN" + echo "Installed Cert SHA1: `$installedCertSHA" + echo "##########################################################" + + # Finally clean up files + if [[ -f "/tmp/$($user.userName)-client-signed.zip" ]]; then + echo "Removing Temp Zip" + rm "/tmp/$($user.userName)-client-signed.zip" + fi + if [[ -f "/tmp/$($user.userName)-client-signed.pfx" ]]; then + echo "Removing Temp Pfx" + rm "/tmp/$($user.userName)-client-signed.pfx" + fi +else + # restore case match type + `$caseMatchOrigValue + echo "Current logged in user, `$currentUser, does not match expected certificate user. Please ensure $($user.localUsername) is signed in and retry" + # Finally clean up files + if [[ -f "/tmp/$($user.userName)-client-signed.zip" ]]; then + echo "Removing Temp Zip" + rm "/tmp/$($user.userName)-client-signed.zip" + fi + if [[ -f "/tmp/$($user.userName)-client-signed.pfx" ]]; then + echo "Removing Temp Pfx" + rm "/tmp/$($user.userName)-client-signed.pfx" + fi + exit 4 +fi + +"@ + launchType = "trigger" + User = "000000000000000000000000" + trigger = "RadiusCertInstall" + commandType = "mac" + timeout = 600 + TimeToLiveSeconds = 864000 + files = (New-JCCommandFile -certFilePath $userPfxZip -FileName "$($user.userName)-client-signed.zip" -FileDestination "/tmp/$($user.userName)-client-signed.zip") + } + $NewCommand = New-JcSdkCommand @CommandBody + + # Find newly created command and add system as target + # TODO: Condition for duplicate commands + $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" + $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } + } catch { + throw $_ + } + + $CommandTable = [PSCustomObject]@{ + commandId = $command._id + commandName = $command.name + commandPreviouslyRun = $false + commandQueued = $false + systems = $systemIds + } + + $user.commandAssociations += $CommandTable + + Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Mac OS X" + + } + 'Windows' { + # Get the Windows system ids + $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'Windows' } | Select-Object systemId + + # Check to see if previous commands exist + $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" + + if ($Command.Count -ge 1) { + $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):Windows command already exists, skipping..." + continue + } + + # Create new Command and upload the signed pfx + try { + $CommandBody = @{ + Name = "RadiusCert-Install:$($user.userName):Windows" + Command = @" +`$ErrorActionPreference = "Stop" +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +`$PkgProvider = Get-PackageProvider +If ("Nuget" -notin `$PkgProvider.Name){ + Install-PackageProvider -Name NuGet -Force +} +`$CurrentUser = (Get-WMIObject -ClassName Win32_ComputerSystem).Username +if ( -Not [string]::isNullOrEmpty(`$CurrentUser) ){ + `$CurrentUser = `$CurrentUser.Split('\')[1] +} else { + `$CurrentUser = `$null +} +if (`$CurrentUser -eq "$($user.localUsername)") { + if (-not(Get-InstalledModule -Name RunAsUser -errorAction "SilentlyContinue")) { + Write-Host "RunAsUser Module not installed, Installing..." + Install-Module RunAsUser -Force + Import-Module RunAsUser -Force + } else { + Write-Host "RunAsUser Module installed, importing into session..." + Import-Module RunAsUser -Force + } + # create temp new radius directory + If (Test-Path "C:\RadiusCert"){ + Write-Host "Radius Temp Cert Directory Exists" + } else { + New-Item "C:\RadiusCert" -itemType Directory + } + # expand archive as root and copy to temp location + Expand-Archive -LiteralPath C:\Windows\Temp\$($user.userName)-client-signed.zip -DestinationPath C:\RadiusCert -Force + `$password = ConvertTo-SecureString -String $JCUSERCERTPASS -AsPlainText -Force + `$ScriptBlockInstall = { `$password = ConvertTo-SecureString -String $JCUSERCERTPASS -AsPlainText -Force + Import-PfxCertificate -Password `$password -FilePath "C:\RadiusCert\$($user.userName)-client-signed.pfx" -CertStoreLocation Cert:\CurrentUser\My + } + `$imported = Get-PfxData -Password `$password -FilePath "C:\RadiusCert\$($user.userName)-client-signed.pfx" + # Get Current Certs As User + `$ScriptBlockCleanup = { + `$certs = Get-ChildItem Cert:\CurrentUser\My\ + + foreach (`$cert in `$certs){ + if (`$cert.subject -match "$($certIdentifier)") { + if (`$(`$cert.serialNumber) -eq "$($certHash.serial)"){ + write-host "Found Cert:``nCert SN: `$(`$cert.serialNumber)" + } else { + write-host "Removing Cert:``nCert SN: `$(`$cert.serialNumber)" + Get-ChildItem "Cert:\CurrentUser\My\`$(`$cert.thumbprint)" | remove-item + } + } + } + } + `$scriptBlockValidate = { + if (Get-ChildItem Cert:\CurrentUser\My\`$(`$imported.thumbrprint)){ + return `$true + } else { + return `$false + } + } + Write-Host "Importing Pfx Certificate for $($user.userName)" + `$certInstall = Invoke-AsCurrentUser -ScriptBlock `$ScriptBlockInstall -CaptureOutput + `$certInstall + Write-Host "Cleaning Up Previously Installed Certs for $($user.userName)" + `$certCleanup = Invoke-AsCurrentUser -ScriptBlock `$ScriptBlockCleanup -CaptureOutput + `$certCleanup + Write-Host "Validating Installed Certs for $($user.userName)" + `$certValidate = Invoke-AsCurrentUser -ScriptBlock `$scriptBlockValidate -CaptureOutput + write-host `$certValidate + + # finally clean up temp files: + If (Test-Path "C:\Windows\Temp\$($user.userName)-client-signed.zip"){ + Remove-Item "C:\Windows\Temp\$($user.userName)-client-signed.zip" + } + If (Test-Path "C:\RadiusCert\$($user.userName)-client-signed.pfx"){ + Remove-Item "C:\RadiusCert\$($user.userName)-client-signed.pfx" + } + + # Lastly validate if the cert was installed + if (`$certValidate.Trim() -eq "True"){ + Write-Host "Cert was installed" + } else { + Throw "Cert was not installed" + } +} else { + if (`$CurrentUser -eq `$null){ + Write-Host "No users are signed into the system. Please ensure $($user.userName) is signed in and retry." + } else { + Write-Host "Current logged in user, `$CurrentUser, does not match expected certificate user. Please ensure $($user.localUsername) is signed in and retry." + } + # finally clean up temp files: + If (Test-Path "C:\Windows\Temp\$($user.userName)-client-signed.zip"){ + Remove-Item "C:\Windows\Temp\$($user.userName)-client-signed.zip" + } + If (Test-Path "C:\RadiusCert\$($user.userName)-client-signed.pfx"){ + Remove-Item "C:\RadiusCert\$($user.userName)-client-signed.pfx" + } + exit 4 +} +"@ + launchType = "trigger" + trigger = "RadiusCertInstall" + commandType = "windows" + shell = "powershell" + timeout = 600 + TimeToLiveSeconds = 864000 + files = (New-JCCommandFile -certFilePath $userPfxZip -FileName "$($user.userName)-client-signed.zip" -FileDestination "C:\Windows\Temp\$($user.userName)-client-signed.zip") + } + $NewCommand = New-JcSdkCommand @CommandBody + + # Find newly created command and add system as target + $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" + $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } + } catch { + throw $_ + } + + $CommandTable = [PSCustomObject]@{ + commandId = $command._id + commandName = $command.name + commandPreviouslyRun = $false + commandQueued = $false + systems = $systemIds + } + + $user.commandAssociations += $CommandTable + Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" + + } + $null { + Write-Warning "$($user.username) is not associated with any systems, skipping command generation" + + + } + } + # Update the user table with the information from the generated commands: + $userObjectFromTable, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot\users.json" -userid $user.userid + Set-UserTable -index $userIndex -commandAssociationsObject $user.commandAssociations + # Invoke Commands + #TODO:: skip if this is not per user basis + $confirmation = Read-Host "Would you like to invoke commands? [y/n]" + # $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + + while ($confirmation -ne 'y') { + if ($confirmation -eq 'n') { + Write-Host "[status] To invoke the commands at a later time, select option '4' to monitor your User Certification Distribution" + Write-Host "[status] Returning to main menu" + exit + } + $confirmation = Read-Host "Would you like to invoke commands? [y/n]" + } + + # TODO: for individual users, invoke command retry by username + # else invoke for all? + + $invokeCommands = invoke-commandByUserId -userID $user.userid + # $invokeCommands = Invoke-CommandsRetry -jsonFile "$JCScriptRoot\users.json" + Write-Host "[status] Commands Invoked" + + # TODO: Set-JCUserTable -Commands to update the user array. + # Set commandPreviouslyRun property to true + $user.commandAssociations | ForEach-Object { $_.commandPreviouslyRun = $true } + + # $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + + } + } + + end { + + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 index 7d56ef56a..ef87799a3 100644 --- a/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 @@ -45,7 +45,7 @@ function Set-UserTable { $systemAssociationsInfo = $userObject.systemAssociations } if ($commandAssociationsObject) { - commandAssociationsInfo = $commandAssociationsObject + $commandAssociationsInfo = $commandAssociationsObject } else { $commandAssociationsInfo = $userObject.commandAssociations } diff --git a/scripts/automation/Radius/Functions/Private/commands/get-commandByUsername.ps1 b/scripts/automation/Radius/Functions/Private/commands/get-commandByUsername.ps1 new file mode 100644 index 000000000..fba5ba520 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/commands/get-commandByUsername.ps1 @@ -0,0 +1,27 @@ +function get-CommandByUsername { + [CmdletBinding()] + param ( + # Parameter help description + [Parameter()] + [system.string] + $username + ) + + begin { + # define searchFilter + $SearchFilter = @{ + searchTerm = "RadiusCert-Install:${username}:" + fields = @('name') + } + + } + + process { + # Get command Results + $commandResults = Search-JcSdkCommand -SearchFilter $SearchFilter -Fields name + } + + end { + return $commandResults + } +} diff --git a/scripts/automation/Radius/Functions/Private/commands/get-queuedCommandByUser.ps1 b/scripts/automation/Radius/Functions/Private/commands/get-queuedCommandByUser.ps1 new file mode 100644 index 000000000..eaa0576f3 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/commands/get-queuedCommandByUser.ps1 @@ -0,0 +1,34 @@ +function get-queuedCommandByUser { + [CmdletBinding()] + param ( + # Parameter help description + [Parameter()] + [system.string] + $username + ) + + begin { + $headers = @{ + "x-api-key" = $Env:JCApiKey + "x-org-id" = $Env:JCOrgId + + } + $limit = [int]100 + $skip = [int]0 + $resultsArray = @() + $SearchFilter = @{ + searchTerm = "RadiusCert-Install:${username}:" + fields = @('name') + } + + } + + process { + $response = Invoke-RestMethod -Uri "https://console.jumpcloud.com/api/v2/queuedcommand/workflows?&skip=$skip&limit=$limit&search[fields][0]=name&search[searchTerm]=RadiusCert-Install:${username}:" -Method GET -Headers $headers + } + + end { + return $response.results + } +} +get-queuedCommandByUser -username "Farmer_100" \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 new file mode 100644 index 000000000..f89e9051f --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 @@ -0,0 +1,51 @@ +function invoke-commandByUserid { + [CmdletBinding(DefaultParameterSetName = 'all')] + param ( + # The userID to of which to invoke the command + [Parameter(ParameterSetName = 'user')] + [system.string] + $userID + + ) + + begin { + # get the command id + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot\users.json" -userid $userID + + # get macOS Command + $macOS_commandId = ($userObject.commandAssociations | Where-Object { $_.commandName -match "MacOSX" }).commandId + # get Windows Command + $windows_commandId = ($userObject.commandAssociations | Where-Object { $_.commandName -match "Windows" }).commandId + # get list of macOS systems + $macOS_systemIds = $userObject.systemAssociations | Where-Object { $_.device_os -eq "macOS" } + # get list of Windows systems + $windows_systemIds = $userObject.systemAssociations | Where-Object { $_.device_os -eq "Windows" } + } + + process { + # explicitly create arrays for windows/ mac system IDs + $windowsArray = New-Object System.Collections.ArrayList + foreach ($system in $windows_systemIds) { + $windowsArray.add($system.systemId) | Out-Null + } + $macOSArray = New-Object System.Collections.ArrayList + foreach ($system in $macOS_systemIds) { + $macOSArray.add($system.systemId) | Out-Null + } + # invoke commands + If ($macOS_commandId) { + Write-Warning "running command: Start-JCSDKCommand -id $($macOS_commandId) -SystemIDs $macOSArray" + $macInvokedCommands = Start-JcSdkCommand -Id $macOS_commandId -SystemIds $macOSArray + } + + if ($windows_commandId) { + Write-Warning "running command: Start-JCSDKCommand -id $($windows_commandId) -SystemIDs $windowsArray" + $windowsInvokedCommands = Start-JcSdkCommand -Id $windows_commandId -SystemIds $windowsArray + } + } + + end { + return $true + } + +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 index 1cd42ee19..082066452 100644 --- a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 @@ -7,428 +7,527 @@ Connect-JCOnline $JCAPIKEY -force ################################################################################ # Import the functions -Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force +# Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force # Import the users.json file and convert to PSObject -$userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 +$userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 10 + +# TODO: surface this information # Check to see if previous commands exist -$RadiusCertCommands = Get-JCCommand | Where-Object { $_.Name -like 'RadiusCert-Install*' } +$SearchFilter = @{ + searchTerm = "RadiusCert-Install:" + fields = @('name') +} +$RadiusCertCommands = Search-JcSdkCommand -SearchFilter $SearchFilter -Fields name +# $RadiusCertCommands = Get-JCCommand -returnProperties name | Where-Object { $_.Name -like 'RadiusCert-Install*' } +# Split out the username from the commands so we can tell which users do not have a command already +$RadiusCertCommandList = New-Object System.Collections.ArrayList +foreach ($command in $RadiusCertCommands) { + $commandSplit = $command.name.split(':') + $RadiusCertCommandList.Add([PSCustomObject]@{ + CommandName = $command.name + Username = $commandSplit[1] + CommandID = $command._id + }) | Out-Null +} +$existingCommandUsers = $RadiusCertCommandList.Username | Get-Unique +$newRadiusUsers = (Compare-Object $userarray.username $existingCommandUsers).InputObject +# TODO: revamp with menu screen +# TODO: generate new commands for a single user +# TODO: why this if statement here: if ($RadiusCertCommands.Count -ge 1) { Write-Host "[status] $([char]0x1b)[96mRadiusCert commands detected, please make a selection." Write-Host "1: Press '1' to generate new commands for ALL users. $([char]0x1b)[96mNOTE: This will remove any previously generated Radius User Certificate Commands titled 'RadiusCert-Install:*'" Write-Host "2: Press '2' to generate new commands for NEW RADIUS users. $([char]0x1b)[96mNOTE: This will only generate commands for users who did not have a cert previously" + Write-Host "3: Press '3' to generate new commands for ONE Specific RADIUS user." Write-Host "E: Press 'E' to exit." - $confirmation = Read-Host "Please make a selection" - while ($confirmation -ne 'E') { - if ($confirmation -eq '1') { - # Get queued commands - $queuedCommands = Get-JCQueuedCommands - # Clear any queued commands for old RadiusCert commands - foreach ($command in $RadiusCertCommands) { - if ($command._id -in $queuedCommands.command) { - $queuedCommandInfo = $queuedCommands | Where-Object command -EQ $command._id - Clear-JCQueuedCommand -workflowId $queuedCommandInfo.id + do { + $confirmation = Read-Host "Please make a selection" + + switch ($confirmation) { + '1' { + # Get queued commands + $queuedCommands = Get-JCQueuedCommands + # Clear any queued commands for old RadiusCert commands + foreach ($command in $RadiusCertCommands) { + if ($command._id -in $queuedCommands.command) { + $queuedCommandInfo = $queuedCommands | Where-Object command -EQ $command._id + Clear-JCQueuedCommand -workflowId $queuedCommandInfo.id + } } + Write-Host "[status] Removing $($RadiusCertCommands.Count) commands" + # Delete previous commands + $RadiusCertCommands | Remove-JCCommand -force | Out-Null + # Clean up users.json array + $userArray | ForEach-Object { $_.commandAssociations = @() } + $confirmation = 'E' } - Write-Host "[status] Removing $($RadiusCertCommands.Count) commands" - # Delete previous commands - $RadiusCertCommands | Remove-JCCommand -force | Out-Null - # Clean up users.json array - $userArray | ForEach-Object { $_.commandAssociations = @() } - break - } elseif ($confirmation -eq '2') { - Write-Host "[status] Proceeding with execution" - break - } - } - - # Break out of the scriptblock and return to main menu - if ($confirmation -eq 'E') { - break - } -} - -# Create commands for each user -foreach ($user in $userArray) { - # Get certificate and zip to upload to Commands - $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -Filter "$($user.userName)*" - # set crt and pfx filepaths - $userCrt = ($userCertFiles | Where-Object { $_.Name -match "crt" }).FullName - $userPfx = ($userCertFiles | Where-Object { $_.Name -match "pfx" }).FullName - # define .zip name - $userPfxZip = "$JCScriptRoot/UserCerts/$($user.userName)-client-signed.zip" - # get certInfo for commands: - $certInfo = Invoke-Expression "$opensslBinary x509 -in $($userCrt) -enddate -serial -subject -issuer -noout" - $certHash = @{} - $certInfo | ForEach-Object { - $property = $_ | ConvertFrom-StringData - $certHash += $property - } - switch ($certType) { - 'EmailSAN' { - # set cert identifier to SAN email of cert - $sanID = Invoke-Expression "$opensslBinary x509 -in $($userCrt) -ext subjectAltName -noout" - $regex = 'email:(.*?)$' - $subjMatch = Select-String -InputObject "$($sanID)" -Pattern $regex - $certIdentifier = $subjMatch.matches.Groups[1].value - # in macOS search user certs by email - $macCertSearch = 'e' - } - 'EmailDN' { - # Else set cert identifier to email of cert subject - $regex = 'emailAddress = (.*?)$' - $subjMatch = Select-String -InputObject "$($certHash.Subject)" -Pattern $regex - $certIdentifier = $subjMatch.matches.Groups[1].value - # in macOS search user certs by email - $macCertSearch = 'e' - } - 'UsernameCn' { - # if username just set cert identifier to username - $certIdentifier = $($user.userName) - # in macOS search user certs by common name (username) - $macCertSearch = 'c' - } - } - # Create the zip - Compress-Archive -Path $userPfx -DestinationPath $userPfxZip -CompressionLevel NoCompression -Force - # Find OS of System - if ($user.systemAssociations.osFamily -contains 'Mac OS X') { - # Get the macOS system ids - $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'Mac OS X' } | Select-Object systemId - - # Check to see if previous commands exist - $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" - - if ($Command.Count -ge 1) { - $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):MacOSX command already exists, skipping..." - continue - } - - # Create new Command and upload the signed pfx - try { - $CommandBody = @{ - Name = "RadiusCert-Install:$($user.userName):MacOSX" - Command = @" -unzip -o /tmp/$($user.userName)-client-signed.zip -d /tmp -chmod 755 /tmp/$($user.userName)-client-signed.pfx -currentUser=`$(/usr/bin/stat -f%Su /dev/console) -currentUserUID=`$(id -u "`$currentUser") -currentCertSN="$($certHash.serial)" -networkSsid="$($NETWORKSSID)" -# store orig case match value -caseMatchOrigValue=`$(shopt -p nocasematch; true) -# set to case-insensitive -shopt -s nocasematch -userCompare="$($user.localUsername)" -if [[ "`$currentUser" == "`$userCompare" ]]; then - # restore case match type - `$caseMatchOrigValue - certs=`$(security find-certificate -a -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain) - regexSHA='SHA-1 hash: ([0-9A-F]{5,40})' - regexSN='"snbr"=0x([0-9A-F]{5,40})' - global_rematch() { - # Set local variables - local s=`$1 regex=`$2 - # While string matches regex expression - while [[ `$s =~ `$regex ]]; do - # Echo out the match - echo "`${BASH_REMATCH[1]}" - # Remove the string - s=`${s#*"`${BASH_REMATCH[1]}"} - done - } - # Save results - # Get Text Results - textSHA=`$(global_rematch "`$certs" "`$regexSHA") - # Set as array for SHA results - arraySHA=(`$textSHA) - # Get Text Results - textSN=`$(global_rematch "`$certs" "`$regexSN") - # Set as array for SN results - arraySN=(`$textSN) - # set import var - import=true - if [[ `${#arraySN[@]} == `${#arraySHA[@]} ]]; then - len=`${#arraySN[@]} - for (( i=0; i<`$len; i++ )); do - if [[ `$currentCertSN == `${arraySN[`$i]} ]]; then - echo "Found Cert: SN: `${arraySN[`$i]} SHA: `${arraySHA[`$i]}" - installedCertSN=`${arraySN[`$i]} - installedCertSHA=`${arraySHA[`$i]} - # if cert is installed, no need to update - import=false - else - echo "Removing previously installed radius cert:" - echo "SN: `${arraySN[`$i]} SHA: `${arraySHA[`$i]}" - security delete-certificate -Z "`${arraySHA[`$i]}" /Users/$($user.localUsername)/Library/Keychains/login.keychain - fi - done - - else - echo "array length mismatch, will not delete old certs" - fi - - if [[ `$import == true ]]; then - /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security import /tmp/$($user.userName)-client-signed.pfx -x -k /Users/$($user.localUsername)/Library/Keychains/login.keychain -P $JCUSERCERTPASS -T "/System/Library/SystemConfiguration/EAPOLController.bundle/Contents/Resources/eapolclient" - if [[ `$? -eq 0 ]]; then - echo "Import Success" - # get the SHA hash of the newly imported cert - installedCertSN=`$(/bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security find-certificate -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain | grep snbr | awk '{print `$1}' | sed 's/"snbr"=0x//g') - if [[ `$installedCertSN == `$currentCertSN ]]; then - installedCertSHA=`$(/bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security find-certificate -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain | grep SHA-1 | awk '{print `$3}') - fi - - else - echo "import failed" - exit 4 - fi - else - echo "cert already imported" - fi - - # check if the cert secruity preference is set: - IFS=';' read -ra network <<< "`$networkSsid" - for i in "`${network[@]}"; do - echo "begin setting network SSID: `$i" - if /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security get-identity-preference -s "com.apple.network.eap.user.identity.wlan.ssid.`$i" -Z "`$installedCertSHA"; then - echo "it was already set" - else - echo "certificate not linked from SSID: `$i to certSN: `$currentCertSN, setting now" - /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security set-identity-preference -s "com.apple.network.eap.user.identity.wlan.ssid.`$i" -Z "`$installedCertSHA" - if [[ `$? -eq 0 ]]; then - echo "SSID: `$i and certificate linked" - else - echo "Could not associate SSID: `$i and certifiacte" - fi - fi - done - - # print results - echo "################## Cert Install Results ##################" - echo "Installed Cert SN: `$installedCertSN" - echo "Installed Cert SHA1: `$installedCertSHA" - echo "##########################################################" - - # Finally clean up files - if [[ -f "/tmp/$($user.userName)-client-signed.zip" ]]; then - echo "Removing Temp Zip" - rm "/tmp/$($user.userName)-client-signed.zip" - fi - if [[ -f "/tmp/$($user.userName)-client-signed.pfx" ]]; then - echo "Removing Temp Pfx" - rm "/tmp/$($user.userName)-client-signed.pfx" - fi -else - # restore case match type - `$caseMatchOrigValue - echo "Current logged in user, `$currentUser, does not match expected certificate user. Please ensure $($user.localUsername) is signed in and retry" - # Finally clean up files - if [[ -f "/tmp/$($user.userName)-client-signed.zip" ]]; then - echo "Removing Temp Zip" - rm "/tmp/$($user.userName)-client-signed.zip" - fi - if [[ -f "/tmp/$($user.userName)-client-signed.pfx" ]]; then - echo "Removing Temp Pfx" - rm "/tmp/$($user.userName)-client-signed.pfx" - fi - exit 4 -fi - -"@ - launchType = "trigger" - User = "000000000000000000000000" - trigger = "RadiusCertInstall" - commandType = "mac" - timeout = 600 - TimeToLiveSeconds = 864000 - files = (New-JCCommandFile -certFilePath $userPfxZip -FileName "$($user.userName)-client-signed.zip" -FileDestination "/tmp/$($user.userName)-client-signed.zip") - } - $NewCommand = New-JcSdkCommand @CommandBody - - # Find newly created command and add system as target - # TODO: Condition for duplicate commands - $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" - $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } - } catch { - throw $_ - } - - $CommandTable = [PSCustomObject]@{ - commandId = $command._id - commandName = $command.name - commandPreviouslyRun = $false - commandQueued = $false - systems = $systemIds - } - - $user.commandAssociations += $CommandTable - - Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Mac OS X" - } - if ($user.systemAssociations.osFamily -contains 'Windows') { - # Get the Windows system ids - $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'Windows' } | Select-Object systemId - - # Check to see if previous commands exist - $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" - - if ($Command.Count -ge 1) { - $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):Windows command already exists, skipping..." - continue - } - - # Create new Command and upload the signed pfx - try { - $CommandBody = @{ - Name = "RadiusCert-Install:$($user.userName):Windows" - Command = @" -`$ErrorActionPreference = "Stop" -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -`$PkgProvider = Get-PackageProvider -If ("Nuget" -notin `$PkgProvider.Name){ - Install-PackageProvider -Name NuGet -Force -} -`$CurrentUser = (Get-WMIObject -ClassName Win32_ComputerSystem).Username -if ( -Not [string]::isNullOrEmpty(`$CurrentUser) ){ - `$CurrentUser = `$CurrentUser.Split('\')[1] -} else { - `$CurrentUser = `$null -} -if (`$CurrentUser -eq "$($user.localUsername)") { - if (-not(Get-InstalledModule -Name RunAsUser -errorAction "SilentlyContinue")) { - Write-Host "RunAsUser Module not installed, Installing..." - Install-Module RunAsUser -Force - Import-Module RunAsUser -Force - } else { - Write-Host "RunAsUser Module installed, importing into session..." - Import-Module RunAsUser -Force - } - # create temp new radius directory - If (Test-Path "C:\RadiusCert"){ - Write-Host "Radius Temp Cert Directory Exists" - } else { - New-Item "C:\RadiusCert" -itemType Directory - } - # expand archive as root and copy to temp location - Expand-Archive -LiteralPath C:\Windows\Temp\$($user.userName)-client-signed.zip -DestinationPath C:\RadiusCert -Force - `$password = ConvertTo-SecureString -String $JCUSERCERTPASS -AsPlainText -Force - `$ScriptBlockInstall = { `$password = ConvertTo-SecureString -String $JCUSERCERTPASS -AsPlainText -Force - Import-PfxCertificate -Password `$password -FilePath "C:\RadiusCert\$($user.userName)-client-signed.pfx" -CertStoreLocation Cert:\CurrentUser\My - } - `$imported = Get-PfxData -Password `$password -FilePath "C:\RadiusCert\$($user.userName)-client-signed.pfx" - # Get Current Certs As User - `$ScriptBlockCleanup = { - `$certs = Get-ChildItem Cert:\CurrentUser\My\ - - foreach (`$cert in `$certs){ - if (`$cert.subject -match "$($certIdentifier)") { - if (`$(`$cert.serialNumber) -eq "$($certHash.serial)"){ - write-host "Found Cert:``nCert SN: `$(`$cert.serialNumber)" - } else { - write-host "Removing Cert:``nCert SN: `$(`$cert.serialNumber)" - Get-ChildItem "Cert:\CurrentUser\My\`$(`$cert.thumbprint)" | remove-item + '2' { + If ($newRadiusUsers) { + $UserSelectionArray = foreach ($user in $newRadiusUsers) { + $userArray | where-Object { $_.username -eq $user } + } } + # $userArray + Write-Host "[status] Proceeding with execution" + $confirmation = 'E' } - } - } - `$scriptBlockValidate = { - if (Get-ChildItem Cert:\CurrentUser\My\`$(`$imported.thumbrprint)){ - return `$true - } else { - return `$false - } - } - Write-Host "Importing Pfx Certificate for $($user.userName)" - `$certInstall = Invoke-AsCurrentUser -ScriptBlock `$ScriptBlockInstall -CaptureOutput - `$certInstall - Write-Host "Cleaning Up Previously Installed Certs for $($user.userName)" - `$certCleanup = Invoke-AsCurrentUser -ScriptBlock `$ScriptBlockCleanup -CaptureOutput - `$certCleanup - Write-Host "Validating Installed Certs for $($user.userName)" - `$certValidate = Invoke-AsCurrentUser -ScriptBlock `$scriptBlockValidate -CaptureOutput - write-host `$certValidate - - # finally clean up temp files: - If (Test-Path "C:\Windows\Temp\$($user.userName)-client-signed.zip"){ - Remove-Item "C:\Windows\Temp\$($user.userName)-client-signed.zip" - } - If (Test-Path "C:\RadiusCert\$($user.userName)-client-signed.pfx"){ - Remove-Item "C:\RadiusCert\$($user.userName)-client-signed.pfx" - } - - # Lastly validate if the cert was installed - if (`$certValidate.Trim() -eq "True"){ - Write-Host "Cert was installed" - } else { - Throw "Cert was not installed" - } -} else { - if (`$CurrentUser -eq `$null){ - Write-Host "No users are signed into the system. Please ensure $($user.userName) is signed in and retry." - } else { - Write-Host "Current logged in user, `$CurrentUser, does not match expected certificate user. Please ensure $($user.localUsername) is signed in and retry." - } - # finally clean up temp files: - If (Test-Path "C:\Windows\Temp\$($user.userName)-client-signed.zip"){ - Remove-Item "C:\Windows\Temp\$($user.userName)-client-signed.zip" - } - If (Test-Path "C:\RadiusCert\$($user.userName)-client-signed.pfx"){ - Remove-Item "C:\RadiusCert\$($user.userName)-client-signed.pfx" - } - exit 4 -} -"@ - launchType = "trigger" - trigger = "RadiusCertInstall" - commandType = "windows" - shell = "powershell" - timeout = 600 - TimeToLiveSeconds = 864000 - files = (New-JCCommandFile -certFilePath $userPfxZip -FileName "$($user.userName)-client-signed.zip" -FileDestination "C:\Windows\Temp\$($user.userName)-client-signed.zip") - } - $NewCommand = New-JcSdkCommand @CommandBody - - # Find newly created command and add system as target - $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" - $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } - } catch { - throw $_ - } + '3' { + try { + Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore + } catch { + New-Variable -Name "ConfirmUser" -Value $null + } + while (-not $confirmUser) { + # TODO: Offer option to go back a step and exit the while loop + $confirmationUser = Read-Host "Enter the Username or UserID of the user" + $confirmUser = Test-UserFromHash -username $confirmationUser -debug + } - $CommandTable = [PSCustomObject]@{ - commandId = $command._id - commandName = $command.name - commandPreviouslyRun = $false - commandQueued = $false - systems = $systemIds + # Get the userobject + index from users.json + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id + # $userArrayIndex = $userArray.username.IndexOf($confirmUser.username) + $UserSelectionArray = $userArray[$userIndex] + # # $UserSelectionArray = $userArray | where-Object { $_.username -eq $confirmUser.username } + # # Get queued commands + # $queuedCommands = Get-JCQueuedCommands | Where-Object { $_.name -match $confirmUser.username } + # # Clear any queued commands for old RadiusCert commands + # foreach ($command in $RadiusCertCommands) { + # if ($command._id -in $queuedCommands.command) { + # $queuedCommandInfo = $queuedCommands | Where-Object command -EQ $command._id + # Clear-JCQueuedCommand -workflowId $queuedCommandInfo.id + # } + # } + # $RadiusCertCommands = $RadiusCertCommands | Where-Object { $_.name -match $confirmUser.username } + # Write-Host "[status] Removing $($RadiusCertCommands.Count) commands" + # # Delete previous commands + # $RadiusCertCommands | Remove-JCCommand -force | Out-Null + # # Clean up users.json array + # $UserSelectionArray | ForEach-Object { $_.commandAssociations = @() } + Deploy-UserCertificate -userObject $UserSelectionArray + } } - - $user.commandAssociations += $CommandTable - Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" - } -} - -# Invoke Commands -$confirmation = Read-Host "Would you like to invoke commands? [y/n]" -$UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" - -while ($confirmation -ne 'y') { - if ($confirmation -eq 'n') { - Write-Host "[status] To invoke the commands at a later time, select option '4' to monitor your User Certification Distribution" - Write-Host "[status] Returning to main menu" - exit - } - $confirmation = Read-Host "Would you like to invoke commands? [y/n]" + } while ($confirmation -ne 'E') } -$invokeCommands = Invoke-CommandsRetry -jsonFile "$JCScriptRoot\users.json" -Write-Host "[status] Commands Invoked" - -# Set commandPreviouslyRun property to true -$userArray.commandAssociations | ForEach-Object { $_.commandPreviouslyRun = $true } - -$UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" - +# Create commands for each user +# foreach ($user in $UserSelectionArray) { + +# #### Begin removal of queued commands + existing command: +# # TODO: get existing command for the user + remove +# Write-Warning "clearing existing commands for $($user.username)" +# $radiusCommandsByUser = Get-CommandByUsername -username $user.username +# foreach ($command in $radiusCommandsByUser) { +# Remove-JcSdkCommand -Id $command.id | Out-Null +# } +# Write-Warning "clearing queued commands for $($user.username)" +# # TODO: get queued command for the user + remove +# $queuedRadiusCommandsByUser = Get-queuedCommandByUser -username $user.username +# foreach ($queuedCommand in $queuedRadiusCommandsByUser) { +# Clear-JCQueuedCommand -workflowId $queuedCommand.id | Out-Null +# } +# # now clear out the user command associations from users.json +# $User.commandAssociations = @() +# #### End removal of queued commands + existing command +# Write-Warning "PROCESSING:" +# $user +# Write-Warning "..." +# # Get certificate and zip to upload to Commands +# $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -Filter "$($user.userName)-*" +# # set crt and pfx filepaths +# $userCrt = ($userCertFiles | Where-Object { $_.Name -match "crt" }).FullName +# $userPfx = ($userCertFiles | Where-Object { $_.Name -match "pfx" }).FullName +# # define .zip name +# $userPfxZip = "$JCScriptRoot/UserCerts/$($user.userName)-client-signed.zip" +# # get certInfo for commands: +# $certInfo = Get-CertInfo -UserCerts -username $user.username + +# # $certInfo = Invoke-Expression "$opensslBinary x509 -in $($userCrt) -enddate -serial -subject -issuer -noout" +# # $certHash = @{} +# # $certInfo | ForEach-Object { +# # $property = $_ | ConvertFrom-StringData +# # $certHash += $property +# # } +# switch ($certType) { +# 'EmailSAN' { +# # set cert identifier to SAN email of cert +# $sanID = Invoke-Expression "$opensslBinary x509 -in $($userCrt) -ext subjectAltName -noout" +# $regex = 'email:(.*?)$' +# $subjMatch = Select-String -InputObject "$($sanID)" -Pattern $regex +# $certIdentifier = $subjMatch.matches.Groups[1].value +# # in macOS search user certs by email +# $macCertSearch = 'e' +# } +# 'EmailDN' { +# # Else set cert identifier to email of cert subject +# $regex = 'emailAddress = (.*?)$' +# $subjMatch = Select-String -InputObject "$($certHash.Subject)" -Pattern $regex +# $certIdentifier = $subjMatch.matches.Groups[1].value +# # in macOS search user certs by email +# $macCertSearch = 'e' +# } +# 'UsernameCn' { +# # if username just set cert identifier to username +# $certIdentifier = $($user.userName) +# # in macOS search user certs by common name (username) +# $macCertSearch = 'c' +# } +# } +# # Create the zip +# Compress-Archive -Path $userPfx -DestinationPath $userPfxZip -CompressionLevel NoCompression -Force +# # Find OS of System +# switch ($user.systemAssociations.device_os) { +# 'macOS' { +# # Get the macOS system ids +# $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'macOS' } | Select-Object systemId + +# # Check to see if previous commands exist +# $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" + +# if ($Command.Count -ge 1) { +# $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):MacOSX command already exists, skipping..." +# continue +# } + +# # Create new Command and upload the signed pfx +# try { +# $CommandBody = @{ +# Name = "RadiusCert-Install:$($user.userName):MacOSX" +# Command = @" +# unzip -o /tmp/$($user.userName)-client-signed.zip -d /tmp +# chmod 755 /tmp/$($user.userName)-client-signed.pfx +# currentUser=`$(/usr/bin/stat -f%Su /dev/console) +# currentUserUID=`$(id -u "`$currentUser") +# currentCertSN="$($certHash.serial)" +# networkSsid="$($NETWORKSSID)" +# # store orig case match value +# caseMatchOrigValue=`$(shopt -p nocasematch; true) +# # set to case-insensitive +# shopt -s nocasematch +# userCompare="$($user.localUsername)" +# if [[ "`$currentUser" == "`$userCompare" ]]; then +# # restore case match type +# `$caseMatchOrigValue +# certs=`$(security find-certificate -a -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain) +# regexSHA='SHA-1 hash: ([0-9A-F]{5,40})' +# regexSN='"snbr"=0x([0-9A-F]{5,40})' +# global_rematch() { +# # Set local variables +# local s=`$1 regex=`$2 +# # While string matches regex expression +# while [[ `$s =~ `$regex ]]; do +# # Echo out the match +# echo "`${BASH_REMATCH[1]}" +# # Remove the string +# s=`${s#*"`${BASH_REMATCH[1]}"} +# done +# } +# # Save results +# # Get Text Results +# textSHA=`$(global_rematch "`$certs" "`$regexSHA") +# # Set as array for SHA results +# arraySHA=(`$textSHA) +# # Get Text Results +# textSN=`$(global_rematch "`$certs" "`$regexSN") +# # Set as array for SN results +# arraySN=(`$textSN) +# # set import var +# import=true +# if [[ `${#arraySN[@]} == `${#arraySHA[@]} ]]; then +# len=`${#arraySN[@]} +# for (( i=0; i<`$len; i++ )); do +# if [[ `$currentCertSN == `${arraySN[`$i]} ]]; then +# echo "Found Cert: SN: `${arraySN[`$i]} SHA: `${arraySHA[`$i]}" +# installedCertSN=`${arraySN[`$i]} +# installedCertSHA=`${arraySHA[`$i]} +# # if cert is installed, no need to update +# import=false +# else +# echo "Removing previously installed radius cert:" +# echo "SN: `${arraySN[`$i]} SHA: `${arraySHA[`$i]}" +# security delete-certificate -Z "`${arraySHA[`$i]}" /Users/$($user.localUsername)/Library/Keychains/login.keychain +# fi +# done + +# else +# echo "array length mismatch, will not delete old certs" +# fi + +# if [[ `$import == true ]]; then +# /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security import /tmp/$($user.userName)-client-signed.pfx -x -k /Users/$($user.localUsername)/Library/Keychains/login.keychain -P $JCUSERCERTPASS -T "/System/Library/SystemConfiguration/EAPOLController.bundle/Contents/Resources/eapolclient" +# if [[ `$? -eq 0 ]]; then +# echo "Import Success" +# # get the SHA hash of the newly imported cert +# installedCertSN=`$(/bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security find-certificate -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain | grep snbr | awk '{print `$1}' | sed 's/"snbr"=0x//g') +# if [[ `$installedCertSN == `$currentCertSN ]]; then +# installedCertSHA=`$(/bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security find-certificate -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain | grep SHA-1 | awk '{print `$3}') +# fi + +# else +# echo "import failed" +# exit 4 +# fi +# else +# echo "cert already imported" +# fi + +# # check if the cert secruity preference is set: +# IFS=';' read -ra network <<< "`$networkSsid" +# for i in "`${network[@]}"; do +# echo "begin setting network SSID: `$i" +# if /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security get-identity-preference -s "com.apple.network.eap.user.identity.wlan.ssid.`$i" -Z "`$installedCertSHA"; then +# echo "it was already set" +# else +# echo "certificate not linked from SSID: `$i to certSN: `$currentCertSN, setting now" +# /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security set-identity-preference -s "com.apple.network.eap.user.identity.wlan.ssid.`$i" -Z "`$installedCertSHA" +# if [[ `$? -eq 0 ]]; then +# echo "SSID: `$i and certificate linked" +# else +# echo "Could not associate SSID: `$i and certifiacte" +# fi +# fi +# done + +# # print results +# echo "################## Cert Install Results ##################" +# echo "Installed Cert SN: `$installedCertSN" +# echo "Installed Cert SHA1: `$installedCertSHA" +# echo "##########################################################" + +# # Finally clean up files +# if [[ -f "/tmp/$($user.userName)-client-signed.zip" ]]; then +# echo "Removing Temp Zip" +# rm "/tmp/$($user.userName)-client-signed.zip" +# fi +# if [[ -f "/tmp/$($user.userName)-client-signed.pfx" ]]; then +# echo "Removing Temp Pfx" +# rm "/tmp/$($user.userName)-client-signed.pfx" +# fi +# else +# # restore case match type +# `$caseMatchOrigValue +# echo "Current logged in user, `$currentUser, does not match expected certificate user. Please ensure $($user.localUsername) is signed in and retry" +# # Finally clean up files +# if [[ -f "/tmp/$($user.userName)-client-signed.zip" ]]; then +# echo "Removing Temp Zip" +# rm "/tmp/$($user.userName)-client-signed.zip" +# fi +# if [[ -f "/tmp/$($user.userName)-client-signed.pfx" ]]; then +# echo "Removing Temp Pfx" +# rm "/tmp/$($user.userName)-client-signed.pfx" +# fi +# exit 4 +# fi + +# "@ +# launchType = "trigger" +# User = "000000000000000000000000" +# trigger = "RadiusCertInstall" +# commandType = "mac" +# timeout = 600 +# TimeToLiveSeconds = 864000 +# files = (New-JCCommandFile -certFilePath $userPfxZip -FileName "$($user.userName)-client-signed.zip" -FileDestination "/tmp/$($user.userName)-client-signed.zip") +# } +# $NewCommand = New-JcSdkCommand @CommandBody + +# # Find newly created command and add system as target +# # TODO: Condition for duplicate commands +# $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" +# $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } +# } catch { +# throw $_ +# } + +# $CommandTable = [PSCustomObject]@{ +# commandId = $command._id +# commandName = $command.name +# commandPreviouslyRun = $false +# commandQueued = $false +# systems = $systemIds +# } + +# $user.commandAssociations += $CommandTable + +# Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Mac OS X" + +# } +# 'Windows' { +# # Get the Windows system ids +# $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'Windows' } | Select-Object systemId + +# # Check to see if previous commands exist +# $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" + +# if ($Command.Count -ge 1) { +# $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):Windows command already exists, skipping..." +# continue +# } + +# # Create new Command and upload the signed pfx +# try { +# $CommandBody = @{ +# Name = "RadiusCert-Install:$($user.userName):Windows" +# Command = @" +# `$ErrorActionPreference = "Stop" +# [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +# `$PkgProvider = Get-PackageProvider +# If ("Nuget" -notin `$PkgProvider.Name){ +# Install-PackageProvider -Name NuGet -Force +# } +# `$CurrentUser = (Get-WMIObject -ClassName Win32_ComputerSystem).Username +# if ( -Not [string]::isNullOrEmpty(`$CurrentUser) ){ +# `$CurrentUser = `$CurrentUser.Split('\')[1] +# } else { +# `$CurrentUser = `$null +# } +# if (`$CurrentUser -eq "$($user.localUsername)") { +# if (-not(Get-InstalledModule -Name RunAsUser -errorAction "SilentlyContinue")) { +# Write-Host "RunAsUser Module not installed, Installing..." +# Install-Module RunAsUser -Force +# Import-Module RunAsUser -Force +# } else { +# Write-Host "RunAsUser Module installed, importing into session..." +# Import-Module RunAsUser -Force +# } +# # create temp new radius directory +# If (Test-Path "C:\RadiusCert"){ +# Write-Host "Radius Temp Cert Directory Exists" +# } else { +# New-Item "C:\RadiusCert" -itemType Directory +# } +# # expand archive as root and copy to temp location +# Expand-Archive -LiteralPath C:\Windows\Temp\$($user.userName)-client-signed.zip -DestinationPath C:\RadiusCert -Force +# `$password = ConvertTo-SecureString -String $JCUSERCERTPASS -AsPlainText -Force +# `$ScriptBlockInstall = { `$password = ConvertTo-SecureString -String $JCUSERCERTPASS -AsPlainText -Force +# Import-PfxCertificate -Password `$password -FilePath "C:\RadiusCert\$($user.userName)-client-signed.pfx" -CertStoreLocation Cert:\CurrentUser\My +# } +# `$imported = Get-PfxData -Password `$password -FilePath "C:\RadiusCert\$($user.userName)-client-signed.pfx" +# # Get Current Certs As User +# `$ScriptBlockCleanup = { +# `$certs = Get-ChildItem Cert:\CurrentUser\My\ + +# foreach (`$cert in `$certs){ +# if (`$cert.subject -match "$($certIdentifier)") { +# if (`$(`$cert.serialNumber) -eq "$($certHash.serial)"){ +# write-host "Found Cert:``nCert SN: `$(`$cert.serialNumber)" +# } else { +# write-host "Removing Cert:``nCert SN: `$(`$cert.serialNumber)" +# Get-ChildItem "Cert:\CurrentUser\My\`$(`$cert.thumbprint)" | remove-item +# } +# } +# } +# } +# `$scriptBlockValidate = { +# if (Get-ChildItem Cert:\CurrentUser\My\`$(`$imported.thumbrprint)){ +# return `$true +# } else { +# return `$false +# } +# } +# Write-Host "Importing Pfx Certificate for $($user.userName)" +# `$certInstall = Invoke-AsCurrentUser -ScriptBlock `$ScriptBlockInstall -CaptureOutput +# `$certInstall +# Write-Host "Cleaning Up Previously Installed Certs for $($user.userName)" +# `$certCleanup = Invoke-AsCurrentUser -ScriptBlock `$ScriptBlockCleanup -CaptureOutput +# `$certCleanup +# Write-Host "Validating Installed Certs for $($user.userName)" +# `$certValidate = Invoke-AsCurrentUser -ScriptBlock `$scriptBlockValidate -CaptureOutput +# write-host `$certValidate + +# # finally clean up temp files: +# If (Test-Path "C:\Windows\Temp\$($user.userName)-client-signed.zip"){ +# Remove-Item "C:\Windows\Temp\$($user.userName)-client-signed.zip" +# } +# If (Test-Path "C:\RadiusCert\$($user.userName)-client-signed.pfx"){ +# Remove-Item "C:\RadiusCert\$($user.userName)-client-signed.pfx" +# } + +# # Lastly validate if the cert was installed +# if (`$certValidate.Trim() -eq "True"){ +# Write-Host "Cert was installed" +# } else { +# Throw "Cert was not installed" +# } +# } else { +# if (`$CurrentUser -eq `$null){ +# Write-Host "No users are signed into the system. Please ensure $($user.userName) is signed in and retry." +# } else { +# Write-Host "Current logged in user, `$CurrentUser, does not match expected certificate user. Please ensure $($user.localUsername) is signed in and retry." +# } +# # finally clean up temp files: +# If (Test-Path "C:\Windows\Temp\$($user.userName)-client-signed.zip"){ +# Remove-Item "C:\Windows\Temp\$($user.userName)-client-signed.zip" +# } +# If (Test-Path "C:\RadiusCert\$($user.userName)-client-signed.pfx"){ +# Remove-Item "C:\RadiusCert\$($user.userName)-client-signed.pfx" +# } +# exit 4 +# } +# "@ +# launchType = "trigger" +# trigger = "RadiusCertInstall" +# commandType = "windows" +# shell = "powershell" +# timeout = 600 +# TimeToLiveSeconds = 864000 +# files = (New-JCCommandFile -certFilePath $userPfxZip -FileName "$($user.userName)-client-signed.zip" -FileDestination "C:\Windows\Temp\$($user.userName)-client-signed.zip") +# } +# $NewCommand = New-JcSdkCommand @CommandBody + +# # Find newly created command and add system as target +# $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" +# $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } +# } catch { +# throw $_ +# } + +# $CommandTable = [PSCustomObject]@{ +# commandId = $command._id +# commandName = $command.name +# commandPreviouslyRun = $false +# commandQueued = $false +# systems = $systemIds +# } + +# $user.commandAssociations += $CommandTable +# Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" + +# } +# $null { +# Write-Warning "$($user.username) is not associated with any systems, skipping command generation" + + +# } +# } + +# # Invoke Commands +# #TODO:: skip if this is not per user basis +# $confirmation = Read-Host "Would you like to invoke commands? [y/n]" +# # TODO: replace this with set-JCUserTable earlier after we create a command(s) for the user +# $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + +# while ($confirmation -ne 'y') { +# if ($confirmation -eq 'n') { +# Write-Host "[status] To invoke the commands at a later time, select option '4' to monitor your User Certification Distribution" +# Write-Host "[status] Returning to main menu" +# exit +# } +# $confirmation = Read-Host "Would you like to invoke commands? [y/n]" +# } + +# # TODO: for individual users, invoke command retry by username +# # else invoke for all? +# $invokeCommands = invoke-commandByUsername -userID $user.userid +# # $invokeCommands = Invoke-CommandsRetry -jsonFile "$JCScriptRoot\users.json" +# Write-Host "[status] Commands Invoked" + +# # TODO: Set-JCUserTable -Commands to update the user array. +# # Set commandPreviouslyRun property to true +# $user.commandAssociations | ForEach-Object { $_.commandPreviouslyRun = $true } + +# $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + +# } Write-Host "[status] Select option '4' to monitor your User Certification Distribution" -Write-Host "[status] Returning to main menu" \ No newline at end of file +Write-Host "[status] Returning to main menu" From 44d4553790970f21f67d411b1b1ed4c05bc5da57 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Tue, 2 Jan 2024 17:20:10 -0700 Subject: [PATCH 010/346] TODOs and distribute changes --- .../Private/Deploy-UserCertificate.ps1 | 88 +-- .../Private/UserTable/Get-UserFromTable.ps1 | 4 +- .../Private/UserTable/Set-UserTable.ps1 | 5 - .../commands/invoke-commandByUserId.ps1 | 2 - .../Private/menus/Show-DistributionMenu.ps1 | 41 ++ .../Private/menus/Show-GenerationMenu.ps1 | 24 + .../Private/menus/Show-ProgressBarText.ps1 | 13 + .../Private/menus/Show-StatusMessage.ps1 | 12 + .../Private/settings/Update-JCRUsersJson.ps1 | 6 +- .../Functions/Public/Distribute-UserCerts.ps1 | 569 ++---------------- .../Functions/Public/Generate-UserCerts.ps1 | 36 +- .../Radius/Start-RadiusDeployment.ps1 | 1 + 12 files changed, 222 insertions(+), 579 deletions(-) create mode 100644 scripts/automation/Radius/Functions/Private/menus/Show-DistributionMenu.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/menus/Show-GenerationMenu.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/menus/Show-ProgressBarText.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/menus/Show-StatusMessage.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 b/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 index 4207fc17a..596b2ce15 100644 --- a/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 +++ b/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 @@ -22,14 +22,12 @@ function Deploy-UserCertificate { foreach ($user in $userObject) { #### Begin removal of queued commands + existing command: - # TODO: get existing command for the user + remove - Write-Warning "clearing existing commands for $($user.username)" + # remove commands $radiusCommandsByUser = Get-CommandByUsername -username $user.username foreach ($command in $radiusCommandsByUser) { Remove-JcSdkCommand -Id $command.id | Out-Null } - Write-Warning "clearing queued commands for $($user.username)" - # TODO: get queued command for the user + remove + # remove queued commands $queuedRadiusCommandsByUser = Get-queuedCommandByUser -username $user.username foreach ($queuedCommand in $queuedRadiusCommandsByUser) { Clear-JCQueuedCommand -workflowId $queuedCommand.id | Out-Null @@ -37,9 +35,6 @@ function Deploy-UserCertificate { # now clear out the user command associations from users.json $User.commandAssociations = @() #### End removal of queued commands + existing command - Write-Warning "PROCESSING:" - $user - Write-Warning "..." # Get certificate and zip to upload to Commands $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -Filter "$($user.userName)-*" # set crt and pfx filepaths @@ -49,13 +44,7 @@ function Deploy-UserCertificate { $userPfxZip = "$JCScriptRoot/UserCerts/$($user.userName)-client-signed.zip" # get certInfo for commands: $certInfo = Get-CertInfo -UserCerts -username $user.username - - # $certInfo = Invoke-Expression "$opensslBinary x509 -in $($userCrt) -enddate -serial -subject -issuer -noout" - # $certHash = @{} - # $certInfo | ForEach-Object { - # $property = $_ | ConvertFrom-StringData - # $certHash += $property - # } + # determine certType switch ($certType) { 'EmailSAN' { # set cert identifier to SAN email of cert @@ -93,7 +82,7 @@ function Deploy-UserCertificate { $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" if ($Command.Count -ge 1) { - $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):MacOSX command already exists, skipping..." + # $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):MacOSX command already exists, skipping..." continue } @@ -256,7 +245,7 @@ fi $user.commandAssociations += $CommandTable - Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Mac OS X" + # Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Mac OS X" } 'Windows' { @@ -267,7 +256,7 @@ fi $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" if ($Command.Count -ge 1) { - $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):Windows command already exists, skipping..." + # $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):Windows command already exists, skipping..." continue } @@ -398,45 +387,72 @@ if (`$CurrentUser -eq "$($user.localUsername)") { } $user.commandAssociations += $CommandTable - Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" + # Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" } $null { - Write-Warning "$($user.username) is not associated with any systems, skipping command generation" + # Write-host "$($user.username) is not associated with any systems, skipping command generation" } } # Update the user table with the information from the generated commands: $userObjectFromTable, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot\users.json" -userid $user.userid + Set-UserTable -index $userIndex -commandAssociationsObject $user.commandAssociations # Invoke Commands #TODO:: skip if this is not per user basis - $confirmation = Read-Host "Would you like to invoke commands? [y/n]" - # $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" - - while ($confirmation -ne 'y') { - if ($confirmation -eq 'n') { - Write-Host "[status] To invoke the commands at a later time, select option '4' to monitor your User Certification Distribution" - Write-Host "[status] Returning to main menu" - exit + switch ($force) { + $true { + # nothing do to + } + $false { + $confirmation = Read-Host "Would you like to invoke commands? [y/n]" + # $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + + while ($confirmation -ne 'y') { + if ($confirmation -eq 'n') { + Write-Host "[status] To invoke the commands at a later time, select option '4' to monitor your User Certification Distribution" + Write-Host "[status] Returning to main menu" + exit + } + $confirmation = Read-Host "Would you like to invoke commands? [y/n]" + } } - $confirmation = Read-Host "Would you like to invoke commands? [y/n]" } # TODO: for individual users, invoke command retry by username # else invoke for all? + # finally update the user table to note that the command has been run, the cert has been deployed + # get the object once more: + $userObjectFromTable, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot\users.json" -userid $user.userid + # Set commandPreviouslyRun property to true if there are command associations to set + if ($userObjectFromTable.commandAssociations) { + $userObjectFromTable.commandAssociations | ForEach-Object { $_.commandPreviouslyRun = $true } + # set the deployed status to true, set the date + if (Get-Member -inputObject $userObjectFromTable.certInfo -name "deployed" -MemberType Properties) { + # if ($userObjectFromTable.certInfo.deployed) { + $userObjectFromTable.certInfo.deployed = $true + } else { + $userObjectFromTable.certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false - $invokeCommands = invoke-commandByUserId -userID $user.userid - # $invokeCommands = Invoke-CommandsRetry -jsonFile "$JCScriptRoot\users.json" - Write-Host "[status] Commands Invoked" - - # TODO: Set-JCUserTable -Commands to update the user array. - # Set commandPreviouslyRun property to true - $user.commandAssociations | ForEach-Object { $_.commandPreviouslyRun = $true } + } + if (Get-Member -inputObject $userObjectFromTable.certInfo -name "deploymentDate" -MemberType Properties) { + # if ($userObjectFromTable.certInfo.deploymentDate) { + $userObjectFromTable.certInfo.deploymentDate = (Get-Date) - # $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + } else { + $userObjectFromTable.certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value (Get-Date) + } + Set-UserTable -index $userIndex -commandAssociationsObject $userObjectFromTable.commandAssociations -certInfoObject $userObjectFromTable.certInfo + $invokeCommands = invoke-commandByUserId -userID $user.userid + # Write-Host "[status] Commands Invoked" + } + # if ($userObjectFromTable.certInfo.deployed) + # $userObjectFromTable.certInfo.deployed = $true + # $userObjectFromTable.certInfo.deploymentDate = (Get-Date) + # invoke the command } } diff --git a/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 index 0b6f5f433..a2c43bb67 100644 --- a/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 @@ -32,14 +32,14 @@ function Get-UserFromTable { $userIndex = $userArray.userid.IndexOf($userid) if ($userIndex -ge 0) { $userArrayObject = $userArray[$userIndex] - Write-Host "[status] $($userObject.username) found in users.json at index: $userIndex " + # Write-Host "[status] $($userObject.username) found in users.json at index: $userIndex " } else { throw } } catch { # otherwise plan to append $userIndex = $null - Write-Host "[status] $($userObject.username) not found in users.json" + # Write-Host "[status] $($userObject.username) not found in users.json" } } end { diff --git a/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 index ef87799a3..973164c73 100644 --- a/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 @@ -29,8 +29,6 @@ function Set-UserTable { if ($PSBoundParameters.ContainsKey('index')) { $userIndex = $index $userObject = $userArray[$index] - Write-Warning "this is the old object" - $userObject } if (($PSBoundParameters.ContainsKey('lookup'))) { # Get User From Table @@ -65,9 +63,6 @@ function Set-UserTable { commandAssociations = $commandAssociationsInfo certInfo = $certInfo } - Write-Warning "this is the new object" - $userTable - # set the user table to new object $userArray[$userIndex] = $userTable diff --git a/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 index f89e9051f..cde84b1e3 100644 --- a/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 +++ b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 @@ -34,12 +34,10 @@ function invoke-commandByUserid { } # invoke commands If ($macOS_commandId) { - Write-Warning "running command: Start-JCSDKCommand -id $($macOS_commandId) -SystemIDs $macOSArray" $macInvokedCommands = Start-JcSdkCommand -Id $macOS_commandId -SystemIds $macOSArray } if ($windows_commandId) { - Write-Warning "running command: Start-JCSDKCommand -id $($windows_commandId) -SystemIDs $windowsArray" $windowsInvokedCommands = Start-JcSdkCommand -Id $windows_commandId -SystemIds $windowsArray } } diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-DistributionMenu.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-DistributionMenu.ps1 new file mode 100644 index 000000000..00d94352b --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/menus/Show-DistributionMenu.ps1 @@ -0,0 +1,41 @@ +function Show-DistributionMenu { + [CmdletBinding()] + param ( + [Parameter()] + [System.Object] + $certObjectArray + ) + begin { + # $userCertInfo = (Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 10).certInfo + } + process { + + + $title = ' JumpCloud Radius Cert Deployment ' + Clear-Host + Write-Host $(PadCenter -string $Title -char '=') + Write-Host $(PadCenter -string "Select an option below to deploy user certificates to systems`n" -char ' ') -ForegroundColor Yellow + # deployment progress of newly generated certs + if ($userCertInfo) { + + Write-Host $(PadCenter -string ' Certificate Information ' -char '-') + Write-Host "Total # of local user certificates:" $userCertInfo.count + Write-Host "Total # of already distributed certificates:" ($userCertInfo | Where-Object { $_.deployed -eq $true }).count + Write-Host "Total # of un-deployed certificates:" ($userCertInfo | Where-Object { ( $_.deployed -eq $false) -or (-not $_.deployed) }).count + + } + # ==== instructions ==== + # TODO: move notes from below into a more legible location + Write-Host $(PadCenter -string ' User Certificate Deployment Options ' -char '-') + # List options: + Write-Host "1: Press '1' to generate new commands for ALL users. `n`t$([char]0x1b)[96mNOTE: This will remove any previously generated Radius User Certificate Commands titled 'RadiusCert-Install:*'`n`tand re-deploy their certificate file" + Write-Host "2: Press '2' to generate new commands for NEW RADIUS users. `n`t$([char]0x1b)[96mNOTE: This will only generate commands for users whos certificate has not been deployed." + Write-Host "3: Press '3' to generate new commands for ONE Specific RADIUS user." + Write-Host "E: Press 'E' to exit." + + Write-Host $(PadCenter -string "-" -char '-') + } + end { + + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-GenerationMenu.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-GenerationMenu.ps1 new file mode 100644 index 000000000..7df846486 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/menus/Show-GenerationMenu.ps1 @@ -0,0 +1,24 @@ +function Show-GenerationMenu { + $title = ' JumpCloud Radius Cert Deployment ' + Clear-Host + Write-Host $(PadCenter -string $Title -char '=') + Write-Host $(PadCenter -string "Select an option below to generate/regenerate user certificates`n" -char ' ') -ForegroundColor Yellow + # ==== instructions ==== + # TODO: move notes from below into a more legible location + # Write-Host $(PadCenter -string "$([char]0x1b)[96m[]: This will only generate certificates for users who do not have a certificate file yet.`n" -char ' ') + + if ($Global:expiringCerts) { + Write-Host $(PadCenter -string ' Certs Expiring Soon ' -char '-') + $Global:expiringCerts | Format-Table -Property username, @{name = 'Remaining Days'; expression = { (New-TimeSpan -Start (Get-Date) -End ("$($_.notAfter)")).Days } }, @{name = "Expires On"; expression = { $_.notAfter } } + } + + Write-Host $(PadCenter -string ' User Certificate Generation Options ' -char '-') + # List Options + Write-Host "1: Press '1' to generate new certificates for NEW RADIUS users. `n`t$([char]0x1b)[96mNOTE: This will only generate certificates for users who do not have a certificate file yet." + Write-Host "2: Press '2' to generate new certificates for ONE RADIUS user. `n`t$([char]0x1b)[96mNOTE: you will be prompted to overwrite any previously generated certificates" + Write-Host "3: Press '3' to re-generate new certificates for ALL users. `n`t$([char]0x1b)[96mNOTE: This will overwrite any local generated certificates" + Write-Host "4: Press '4' to re-generate new certificates for users who's cert is set to expire shortly. `n`t$([char]0x1b)[96mNOTE: This will overwrite any local generated certificates" + Write-Host "E: Press 'E' to exit." + + Write-Host $(PadCenter -string "-" -char '-') +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-ProgressBarText.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-ProgressBarText.ps1 new file mode 100644 index 000000000..fe4708e4b --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/menus/Show-ProgressBarText.ps1 @@ -0,0 +1,13 @@ +Function Show-ProgressBarText { + param( + [int]$completedItems, + [int]$totalItems, + [string]$actionText + ) + $pComplete = ($completedItems / $totalItems) * 100 + $barLength = [math]::Ceiling((($pComplete / 100)) * 30) + + $pbar = "[" + ('▯' * $barLength) + (' ' * (30 - $barLength)) + "]" + $pmessage = "${actionText}: $completedItems out of $TotalItems ($pComplete%) $pbar" + Write-Host $pmessage -NoNewline +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-StatusMessage.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-StatusMessage.ps1 new file mode 100644 index 000000000..797d7b7cc --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/menus/Show-StatusMessage.ps1 @@ -0,0 +1,12 @@ +function Show-StatusMessage { + [CmdletBinding()] + param ( + # Parameter help description + [Parameter()] + [System.String] + $message + ) + Write-Host "`r" + write-host "[status] - $message" + start-sleep 1 +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 index ccb79c2b8..c81477311 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 @@ -12,8 +12,9 @@ function Update-JCRUsersJson { # validate that the system association data is correct in users.json: $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 foreach ($userid in $Global:JCRRadiusMembers.keys) { + $MatchedUser = $GLOBAL:JCRUsers[$userid] - Write-Host "checking out $($MatchedUser.username) userid: $userid" + Show-ProgressBarText -completedItems $Global:JCRRadiusMembers.keys.IndexOf($userid) -totalItems ($Global:JCRRadiusMembers.keys).count -ActionText "Updating latest Radius group membership" $userArrayObject, $userIndex = Get-UserFromTable -userID $userid -jsonFilePath "$JCScriptRoot/users.json" if ($userIndex -ge 0) { @@ -36,6 +37,8 @@ function Update-JCRUsersJson { # # $userTable = New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername # # Update-JsonData -jsonFilePath "$JCScriptRoot/users.json" -userID $userID -updatedUserTable $userTable # } + Write-Host "`r" -NoNewline + } else { # case for new user New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername @@ -54,6 +57,7 @@ function Update-JCRUsersJson { } # Remove the User From Table } + Show-StatusMessage -message "Finished pulling radius group membership updates" } end { $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" diff --git a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 index 082066452..58c02817d 100644 --- a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 @@ -1,533 +1,84 @@ # Import Global Config: -. "$JCScriptRoot/config.ps1" -Connect-JCOnline $JCAPIKEY -force +# . "$JCScriptRoot/config.ps1" +# Connect-JCOnline $JCAPIKEY -force ################################################################################ # Do not modify below ################################################################################ -# Import the functions -# Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force - # Import the users.json file and convert to PSObject $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 10 # TODO: surface this information # Check to see if previous commands exist -$SearchFilter = @{ - searchTerm = "RadiusCert-Install:" - fields = @('name') -} -$RadiusCertCommands = Search-JcSdkCommand -SearchFilter $SearchFilter -Fields name -# $RadiusCertCommands = Get-JCCommand -returnProperties name | Where-Object { $_.Name -like 'RadiusCert-Install*' } -# Split out the username from the commands so we can tell which users do not have a command already -$RadiusCertCommandList = New-Object System.Collections.ArrayList -foreach ($command in $RadiusCertCommands) { - $commandSplit = $command.name.split(':') - $RadiusCertCommandList.Add([PSCustomObject]@{ - CommandName = $command.name - Username = $commandSplit[1] - CommandID = $command._id - }) | Out-Null -} -$existingCommandUsers = $RadiusCertCommandList.Username | Get-Unique -$newRadiusUsers = (Compare-Object $userarray.username $existingCommandUsers).InputObject +# $SearchFilter = @{ +# searchTerm = "RadiusCert-Install:" +# fields = @('name') +# } +# $RadiusCertCommands = Search-JcSdkCommand -SearchFilter $SearchFilter -Fields name + +# $RadiusCertCommandList = New-Object System.Collections.ArrayList +# foreach ($command in $RadiusCertCommands) { +# $commandSplit = $command.name.split(':') +# $RadiusCertCommandList.Add([PSCustomObject]@{ +# CommandName = $command.name +# Username = $commandSplit[1] +# CommandID = $command._id +# }) | Out-Null +# } +# $existingCommandUsers = $RadiusCertCommandList.Username | Get-Unique +# $newRadiusUsers = (Compare-Object $userarray.username $existingCommandUsers).InputObject # TODO: revamp with menu screen # TODO: generate new commands for a single user # TODO: why this if statement here: -if ($RadiusCertCommands.Count -ge 1) { - Write-Host "[status] $([char]0x1b)[96mRadiusCert commands detected, please make a selection." - Write-Host "1: Press '1' to generate new commands for ALL users. $([char]0x1b)[96mNOTE: This will remove any previously generated Radius User Certificate Commands titled 'RadiusCert-Install:*'" - Write-Host "2: Press '2' to generate new commands for NEW RADIUS users. $([char]0x1b)[96mNOTE: This will only generate commands for users who did not have a cert previously" - Write-Host "3: Press '3' to generate new commands for ONE Specific RADIUS user." - Write-Host "E: Press 'E' to exit." - do { - $confirmation = Read-Host "Please make a selection" - - switch ($confirmation) { - '1' { - # Get queued commands - $queuedCommands = Get-JCQueuedCommands - # Clear any queued commands for old RadiusCert commands - foreach ($command in $RadiusCertCommands) { - if ($command._id -in $queuedCommands.command) { - $queuedCommandInfo = $queuedCommands | Where-Object command -EQ $command._id - Clear-JCQueuedCommand -workflowId $queuedCommandInfo.id - } - } - Write-Host "[status] Removing $($RadiusCertCommands.Count) commands" - # Delete previous commands - $RadiusCertCommands | Remove-JCCommand -force | Out-Null - # Clean up users.json array - $userArray | ForEach-Object { $_.commandAssociations = @() } - $confirmation = 'E' +do { + Show-DistributionMenu -CertObjectArray $userArray.certInfo + $confirmation = Read-Host "Please make a selection" + + switch ($confirmation) { + '1' { + for ($i = 0; $i -lt $userArray.Count; $i++) { + <# Action that will repeat until the condition is met #> + Show-ProgressBarText -completedItems $i -totalItems $userArray.Count -ActionText "Distributing Radius Certificates" + $var = Deploy-UserCertificate -userObject $userArray[$i] -force | Out-Null + Write-Host "`r" -NoNewline } - '2' { - If ($newRadiusUsers) { - $UserSelectionArray = foreach ($user in $newRadiusUsers) { - $userArray | where-Object { $_.username -eq $user } - } - } - # $userArray - Write-Host "[status] Proceeding with execution" - $confirmation = 'E' + Show-StatusMessage -Message "Finished Distributing Certificates" + } + '2' { + $usersWithoutLatestCert = $userArray | Where-Object { ( $_.deployed -eq $false) -or (-not $_.deployed) } + + for ($i = 0; $i -lt $usersWithoutLatestCert.Count; $i++) { + <# Action that will repeat until the condition is met #> + Show-ProgressBarText -completedItems $i -totalItems $usersWithoutLatestCert.Count -ActionText "Distributing Radius Certificates" + $var = Deploy-UserCertificate -userObject $usersWithoutLatestCert[$i] -force | Out-Null + Write-Host "`r" -NoNewline } - '3' { - try { - Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore - } catch { - New-Variable -Name "ConfirmUser" -Value $null - } - while (-not $confirmUser) { - # TODO: Offer option to go back a step and exit the while loop - $confirmationUser = Read-Host "Enter the Username or UserID of the user" - $confirmUser = Test-UserFromHash -username $confirmationUser -debug - } + Show-StatusMessage -Message "Finished Distributing Certificates" - # Get the userobject + index from users.json - $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id - # $userArrayIndex = $userArray.username.IndexOf($confirmUser.username) - $UserSelectionArray = $userArray[$userIndex] - # # $UserSelectionArray = $userArray | where-Object { $_.username -eq $confirmUser.username } - # # Get queued commands - # $queuedCommands = Get-JCQueuedCommands | Where-Object { $_.name -match $confirmUser.username } - # # Clear any queued commands for old RadiusCert commands - # foreach ($command in $RadiusCertCommands) { - # if ($command._id -in $queuedCommands.command) { - # $queuedCommandInfo = $queuedCommands | Where-Object command -EQ $command._id - # Clear-JCQueuedCommand -workflowId $queuedCommandInfo.id - # } - # } - # $RadiusCertCommands = $RadiusCertCommands | Where-Object { $_.name -match $confirmUser.username } - # Write-Host "[status] Removing $($RadiusCertCommands.Count) commands" - # # Delete previous commands - # $RadiusCertCommands | Remove-JCCommand -force | Out-Null - # # Clean up users.json array - # $UserSelectionArray | ForEach-Object { $_.commandAssociations = @() } - Deploy-UserCertificate -userObject $UserSelectionArray + } + '3' { + try { + Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore + } catch { + New-Variable -Name "ConfirmUser" -Value $null } + while (-not $confirmUser) { + # TODO: Offer option to go back a step and exit the while loop + $confirmationUser = Read-Host "Enter the Username or UserID of the user" + $confirmUser = Test-UserFromHash -username $confirmationUser -debug + } + # Get the userobject + index from users.json + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id + # Add user to a list for processing + $UserSelectionArray = $userArray[$userIndex] + # Process existing commands/ Generate new commands/ Deploy new Certificate + Deploy-UserCertificate -userObject $UserSelectionArray } - } while ($confirmation -ne 'E') -} - -# Create commands for each user -# foreach ($user in $UserSelectionArray) { - -# #### Begin removal of queued commands + existing command: -# # TODO: get existing command for the user + remove -# Write-Warning "clearing existing commands for $($user.username)" -# $radiusCommandsByUser = Get-CommandByUsername -username $user.username -# foreach ($command in $radiusCommandsByUser) { -# Remove-JcSdkCommand -Id $command.id | Out-Null -# } -# Write-Warning "clearing queued commands for $($user.username)" -# # TODO: get queued command for the user + remove -# $queuedRadiusCommandsByUser = Get-queuedCommandByUser -username $user.username -# foreach ($queuedCommand in $queuedRadiusCommandsByUser) { -# Clear-JCQueuedCommand -workflowId $queuedCommand.id | Out-Null -# } -# # now clear out the user command associations from users.json -# $User.commandAssociations = @() -# #### End removal of queued commands + existing command -# Write-Warning "PROCESSING:" -# $user -# Write-Warning "..." -# # Get certificate and zip to upload to Commands -# $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -Filter "$($user.userName)-*" -# # set crt and pfx filepaths -# $userCrt = ($userCertFiles | Where-Object { $_.Name -match "crt" }).FullName -# $userPfx = ($userCertFiles | Where-Object { $_.Name -match "pfx" }).FullName -# # define .zip name -# $userPfxZip = "$JCScriptRoot/UserCerts/$($user.userName)-client-signed.zip" -# # get certInfo for commands: -# $certInfo = Get-CertInfo -UserCerts -username $user.username - -# # $certInfo = Invoke-Expression "$opensslBinary x509 -in $($userCrt) -enddate -serial -subject -issuer -noout" -# # $certHash = @{} -# # $certInfo | ForEach-Object { -# # $property = $_ | ConvertFrom-StringData -# # $certHash += $property -# # } -# switch ($certType) { -# 'EmailSAN' { -# # set cert identifier to SAN email of cert -# $sanID = Invoke-Expression "$opensslBinary x509 -in $($userCrt) -ext subjectAltName -noout" -# $regex = 'email:(.*?)$' -# $subjMatch = Select-String -InputObject "$($sanID)" -Pattern $regex -# $certIdentifier = $subjMatch.matches.Groups[1].value -# # in macOS search user certs by email -# $macCertSearch = 'e' -# } -# 'EmailDN' { -# # Else set cert identifier to email of cert subject -# $regex = 'emailAddress = (.*?)$' -# $subjMatch = Select-String -InputObject "$($certHash.Subject)" -Pattern $regex -# $certIdentifier = $subjMatch.matches.Groups[1].value -# # in macOS search user certs by email -# $macCertSearch = 'e' -# } -# 'UsernameCn' { -# # if username just set cert identifier to username -# $certIdentifier = $($user.userName) -# # in macOS search user certs by common name (username) -# $macCertSearch = 'c' -# } -# } -# # Create the zip -# Compress-Archive -Path $userPfx -DestinationPath $userPfxZip -CompressionLevel NoCompression -Force -# # Find OS of System -# switch ($user.systemAssociations.device_os) { -# 'macOS' { -# # Get the macOS system ids -# $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'macOS' } | Select-Object systemId - -# # Check to see if previous commands exist -# $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" - -# if ($Command.Count -ge 1) { -# $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):MacOSX command already exists, skipping..." -# continue -# } - -# # Create new Command and upload the signed pfx -# try { -# $CommandBody = @{ -# Name = "RadiusCert-Install:$($user.userName):MacOSX" -# Command = @" -# unzip -o /tmp/$($user.userName)-client-signed.zip -d /tmp -# chmod 755 /tmp/$($user.userName)-client-signed.pfx -# currentUser=`$(/usr/bin/stat -f%Su /dev/console) -# currentUserUID=`$(id -u "`$currentUser") -# currentCertSN="$($certHash.serial)" -# networkSsid="$($NETWORKSSID)" -# # store orig case match value -# caseMatchOrigValue=`$(shopt -p nocasematch; true) -# # set to case-insensitive -# shopt -s nocasematch -# userCompare="$($user.localUsername)" -# if [[ "`$currentUser" == "`$userCompare" ]]; then -# # restore case match type -# `$caseMatchOrigValue -# certs=`$(security find-certificate -a -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain) -# regexSHA='SHA-1 hash: ([0-9A-F]{5,40})' -# regexSN='"snbr"=0x([0-9A-F]{5,40})' -# global_rematch() { -# # Set local variables -# local s=`$1 regex=`$2 -# # While string matches regex expression -# while [[ `$s =~ `$regex ]]; do -# # Echo out the match -# echo "`${BASH_REMATCH[1]}" -# # Remove the string -# s=`${s#*"`${BASH_REMATCH[1]}"} -# done -# } -# # Save results -# # Get Text Results -# textSHA=`$(global_rematch "`$certs" "`$regexSHA") -# # Set as array for SHA results -# arraySHA=(`$textSHA) -# # Get Text Results -# textSN=`$(global_rematch "`$certs" "`$regexSN") -# # Set as array for SN results -# arraySN=(`$textSN) -# # set import var -# import=true -# if [[ `${#arraySN[@]} == `${#arraySHA[@]} ]]; then -# len=`${#arraySN[@]} -# for (( i=0; i<`$len; i++ )); do -# if [[ `$currentCertSN == `${arraySN[`$i]} ]]; then -# echo "Found Cert: SN: `${arraySN[`$i]} SHA: `${arraySHA[`$i]}" -# installedCertSN=`${arraySN[`$i]} -# installedCertSHA=`${arraySHA[`$i]} -# # if cert is installed, no need to update -# import=false -# else -# echo "Removing previously installed radius cert:" -# echo "SN: `${arraySN[`$i]} SHA: `${arraySHA[`$i]}" -# security delete-certificate -Z "`${arraySHA[`$i]}" /Users/$($user.localUsername)/Library/Keychains/login.keychain -# fi -# done - -# else -# echo "array length mismatch, will not delete old certs" -# fi - -# if [[ `$import == true ]]; then -# /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security import /tmp/$($user.userName)-client-signed.pfx -x -k /Users/$($user.localUsername)/Library/Keychains/login.keychain -P $JCUSERCERTPASS -T "/System/Library/SystemConfiguration/EAPOLController.bundle/Contents/Resources/eapolclient" -# if [[ `$? -eq 0 ]]; then -# echo "Import Success" -# # get the SHA hash of the newly imported cert -# installedCertSN=`$(/bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security find-certificate -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain | grep snbr | awk '{print `$1}' | sed 's/"snbr"=0x//g') -# if [[ `$installedCertSN == `$currentCertSN ]]; then -# installedCertSHA=`$(/bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security find-certificate -$($macCertSearch) "$($certIdentifier)" -Z /Users/$($user.localUsername)/Library/Keychains/login.keychain | grep SHA-1 | awk '{print `$3}') -# fi - -# else -# echo "import failed" -# exit 4 -# fi -# else -# echo "cert already imported" -# fi - -# # check if the cert secruity preference is set: -# IFS=';' read -ra network <<< "`$networkSsid" -# for i in "`${network[@]}"; do -# echo "begin setting network SSID: `$i" -# if /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security get-identity-preference -s "com.apple.network.eap.user.identity.wlan.ssid.`$i" -Z "`$installedCertSHA"; then -# echo "it was already set" -# else -# echo "certificate not linked from SSID: `$i to certSN: `$currentCertSN, setting now" -# /bin/launchctl asuser "`$currentUserUID" sudo -iu "`$currentUser" /usr/bin/security set-identity-preference -s "com.apple.network.eap.user.identity.wlan.ssid.`$i" -Z "`$installedCertSHA" -# if [[ `$? -eq 0 ]]; then -# echo "SSID: `$i and certificate linked" -# else -# echo "Could not associate SSID: `$i and certifiacte" -# fi -# fi -# done - -# # print results -# echo "################## Cert Install Results ##################" -# echo "Installed Cert SN: `$installedCertSN" -# echo "Installed Cert SHA1: `$installedCertSHA" -# echo "##########################################################" - -# # Finally clean up files -# if [[ -f "/tmp/$($user.userName)-client-signed.zip" ]]; then -# echo "Removing Temp Zip" -# rm "/tmp/$($user.userName)-client-signed.zip" -# fi -# if [[ -f "/tmp/$($user.userName)-client-signed.pfx" ]]; then -# echo "Removing Temp Pfx" -# rm "/tmp/$($user.userName)-client-signed.pfx" -# fi -# else -# # restore case match type -# `$caseMatchOrigValue -# echo "Current logged in user, `$currentUser, does not match expected certificate user. Please ensure $($user.localUsername) is signed in and retry" -# # Finally clean up files -# if [[ -f "/tmp/$($user.userName)-client-signed.zip" ]]; then -# echo "Removing Temp Zip" -# rm "/tmp/$($user.userName)-client-signed.zip" -# fi -# if [[ -f "/tmp/$($user.userName)-client-signed.pfx" ]]; then -# echo "Removing Temp Pfx" -# rm "/tmp/$($user.userName)-client-signed.pfx" -# fi -# exit 4 -# fi - -# "@ -# launchType = "trigger" -# User = "000000000000000000000000" -# trigger = "RadiusCertInstall" -# commandType = "mac" -# timeout = 600 -# TimeToLiveSeconds = 864000 -# files = (New-JCCommandFile -certFilePath $userPfxZip -FileName "$($user.userName)-client-signed.zip" -FileDestination "/tmp/$($user.userName)-client-signed.zip") -# } -# $NewCommand = New-JcSdkCommand @CommandBody - -# # Find newly created command and add system as target -# # TODO: Condition for duplicate commands -# $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" -# $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } -# } catch { -# throw $_ -# } - -# $CommandTable = [PSCustomObject]@{ -# commandId = $command._id -# commandName = $command.name -# commandPreviouslyRun = $false -# commandQueued = $false -# systems = $systemIds -# } - -# $user.commandAssociations += $CommandTable + } +} while ($confirmation -ne 'E') -# Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Mac OS X" -# } -# 'Windows' { -# # Get the Windows system ids -# $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'Windows' } | Select-Object systemId - -# # Check to see if previous commands exist -# $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" - -# if ($Command.Count -ge 1) { -# $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):Windows command already exists, skipping..." -# continue -# } - -# # Create new Command and upload the signed pfx -# try { -# $CommandBody = @{ -# Name = "RadiusCert-Install:$($user.userName):Windows" -# Command = @" -# `$ErrorActionPreference = "Stop" -# [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -# `$PkgProvider = Get-PackageProvider -# If ("Nuget" -notin `$PkgProvider.Name){ -# Install-PackageProvider -Name NuGet -Force -# } -# `$CurrentUser = (Get-WMIObject -ClassName Win32_ComputerSystem).Username -# if ( -Not [string]::isNullOrEmpty(`$CurrentUser) ){ -# `$CurrentUser = `$CurrentUser.Split('\')[1] -# } else { -# `$CurrentUser = `$null -# } -# if (`$CurrentUser -eq "$($user.localUsername)") { -# if (-not(Get-InstalledModule -Name RunAsUser -errorAction "SilentlyContinue")) { -# Write-Host "RunAsUser Module not installed, Installing..." -# Install-Module RunAsUser -Force -# Import-Module RunAsUser -Force -# } else { -# Write-Host "RunAsUser Module installed, importing into session..." -# Import-Module RunAsUser -Force -# } -# # create temp new radius directory -# If (Test-Path "C:\RadiusCert"){ -# Write-Host "Radius Temp Cert Directory Exists" -# } else { -# New-Item "C:\RadiusCert" -itemType Directory -# } -# # expand archive as root and copy to temp location -# Expand-Archive -LiteralPath C:\Windows\Temp\$($user.userName)-client-signed.zip -DestinationPath C:\RadiusCert -Force -# `$password = ConvertTo-SecureString -String $JCUSERCERTPASS -AsPlainText -Force -# `$ScriptBlockInstall = { `$password = ConvertTo-SecureString -String $JCUSERCERTPASS -AsPlainText -Force -# Import-PfxCertificate -Password `$password -FilePath "C:\RadiusCert\$($user.userName)-client-signed.pfx" -CertStoreLocation Cert:\CurrentUser\My -# } -# `$imported = Get-PfxData -Password `$password -FilePath "C:\RadiusCert\$($user.userName)-client-signed.pfx" -# # Get Current Certs As User -# `$ScriptBlockCleanup = { -# `$certs = Get-ChildItem Cert:\CurrentUser\My\ - -# foreach (`$cert in `$certs){ -# if (`$cert.subject -match "$($certIdentifier)") { -# if (`$(`$cert.serialNumber) -eq "$($certHash.serial)"){ -# write-host "Found Cert:``nCert SN: `$(`$cert.serialNumber)" -# } else { -# write-host "Removing Cert:``nCert SN: `$(`$cert.serialNumber)" -# Get-ChildItem "Cert:\CurrentUser\My\`$(`$cert.thumbprint)" | remove-item -# } -# } -# } -# } -# `$scriptBlockValidate = { -# if (Get-ChildItem Cert:\CurrentUser\My\`$(`$imported.thumbrprint)){ -# return `$true -# } else { -# return `$false -# } -# } -# Write-Host "Importing Pfx Certificate for $($user.userName)" -# `$certInstall = Invoke-AsCurrentUser -ScriptBlock `$ScriptBlockInstall -CaptureOutput -# `$certInstall -# Write-Host "Cleaning Up Previously Installed Certs for $($user.userName)" -# `$certCleanup = Invoke-AsCurrentUser -ScriptBlock `$ScriptBlockCleanup -CaptureOutput -# `$certCleanup -# Write-Host "Validating Installed Certs for $($user.userName)" -# `$certValidate = Invoke-AsCurrentUser -ScriptBlock `$scriptBlockValidate -CaptureOutput -# write-host `$certValidate - -# # finally clean up temp files: -# If (Test-Path "C:\Windows\Temp\$($user.userName)-client-signed.zip"){ -# Remove-Item "C:\Windows\Temp\$($user.userName)-client-signed.zip" -# } -# If (Test-Path "C:\RadiusCert\$($user.userName)-client-signed.pfx"){ -# Remove-Item "C:\RadiusCert\$($user.userName)-client-signed.pfx" -# } - -# # Lastly validate if the cert was installed -# if (`$certValidate.Trim() -eq "True"){ -# Write-Host "Cert was installed" -# } else { -# Throw "Cert was not installed" -# } -# } else { -# if (`$CurrentUser -eq `$null){ -# Write-Host "No users are signed into the system. Please ensure $($user.userName) is signed in and retry." -# } else { -# Write-Host "Current logged in user, `$CurrentUser, does not match expected certificate user. Please ensure $($user.localUsername) is signed in and retry." -# } -# # finally clean up temp files: -# If (Test-Path "C:\Windows\Temp\$($user.userName)-client-signed.zip"){ -# Remove-Item "C:\Windows\Temp\$($user.userName)-client-signed.zip" -# } -# If (Test-Path "C:\RadiusCert\$($user.userName)-client-signed.pfx"){ -# Remove-Item "C:\RadiusCert\$($user.userName)-client-signed.pfx" -# } -# exit 4 -# } -# "@ -# launchType = "trigger" -# trigger = "RadiusCertInstall" -# commandType = "windows" -# shell = "powershell" -# timeout = 600 -# TimeToLiveSeconds = 864000 -# files = (New-JCCommandFile -certFilePath $userPfxZip -FileName "$($user.userName)-client-signed.zip" -FileDestination "C:\Windows\Temp\$($user.userName)-client-signed.zip") -# } -# $NewCommand = New-JcSdkCommand @CommandBody - -# # Find newly created command and add system as target -# $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" -# $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } -# } catch { -# throw $_ -# } - -# $CommandTable = [PSCustomObject]@{ -# commandId = $command._id -# commandName = $command.name -# commandPreviouslyRun = $false -# commandQueued = $false -# systems = $systemIds -# } - -# $user.commandAssociations += $CommandTable -# Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" - -# } -# $null { -# Write-Warning "$($user.username) is not associated with any systems, skipping command generation" - - -# } -# } - -# # Invoke Commands -# #TODO:: skip if this is not per user basis -# $confirmation = Read-Host "Would you like to invoke commands? [y/n]" -# # TODO: replace this with set-JCUserTable earlier after we create a command(s) for the user -# $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" - -# while ($confirmation -ne 'y') { -# if ($confirmation -eq 'n') { -# Write-Host "[status] To invoke the commands at a later time, select option '4' to monitor your User Certification Distribution" -# Write-Host "[status] Returning to main menu" -# exit -# } -# $confirmation = Read-Host "Would you like to invoke commands? [y/n]" -# } - -# # TODO: for individual users, invoke command retry by username -# # else invoke for all? -# $invokeCommands = invoke-commandByUsername -userID $user.userid -# # $invokeCommands = Invoke-CommandsRetry -jsonFile "$JCScriptRoot\users.json" -# Write-Host "[status] Commands Invoked" - -# # TODO: Set-JCUserTable -Commands to update the user array. -# # Set commandPreviouslyRun property to true -# $user.commandAssociations | ForEach-Object { $_.commandPreviouslyRun = $true } - -# $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" - -# } Write-Host "[status] Select option '4' to monitor your User Certification Distribution" Write-Host "[status] Returning to main menu" diff --git a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 index 59367737f..9981471fe 100644 --- a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 @@ -1,8 +1,8 @@ # TODO: param for testing # Import Global Config: -. "$JCScriptRoot/config.ps1" -Connect-JCOnline $JCAPIKEY -force +# . "$JCScriptRoot/config.ps1" +# Connect-JCOnline $JCAPIKEY -force ################################################################################ # Do not modify below @@ -52,28 +52,6 @@ if (Test-Path "$JCScriptRoot/UserCerts") { New-Item -ItemType Directory -Path "$JCScriptRoot/UserCerts" } -function Show-GenerationMenu { - $title = ' JumpCloud Radius Cert Deployment ' - Clear-Host - Write-Host $(PadCenter -string $Title -char '=') - Write-Host $(PadCenter -string "Select an option below to generate/regenerate user certificates`n" -char ' ') -ForegroundColor Yellow - # ==== instructions ==== - # TODO: move notes from below into a more legible location - # Write-Host $(PadCenter -string ' User Certificate Options ' -char '-') - # Write-Host $(PadCenter -string "$([char]0x1b)[96m[]: This will only generate certificates for users who do not have a certificate file yet.`n" -char ' ') - - if ($Global:expiringCerts) { - Write-Host $(PadCenter -string ' Certs Expiring Soon ' -char '-') - $Global:expiringCerts | Format-Table -Property username, @{name = 'Remaining Days'; expression = { (New-TimeSpan -Start (Get-Date) -End ("$($_.notAfter)")).Days } }, @{name = "Expires On"; expression = { $_.notAfter } } - } - - Write-Host $(PadCenter -string "-" -char '-') - Write-Host "1: Press '1' to generate new certificates for NEW RADIUS users. `n`t$([char]0x1b)[96mNOTE: This will only generate certificates for users who do not have a certificate file yet." - Write-Host "2: Press '2' to generate new certificates for ONE RADIUS user. `n`t$([char]0x1b)[96mNOTE: you will be prompted to overwrite any previously generated certificates" - Write-Host "3: Press '3' to re-generate new certificates for ALL users. `n`t$([char]0x1b)[96mNOTE: This will overwrite any local generated certificates" - Write-Host "4: Press '4' to re-generate new certificates for users who's cert is set to expire shortly. `n`t$([char]0x1b)[96mNOTE: This will overwrite any local generated certificates" - Write-Host "E: Press 'E' to exit." -} Do { Show-GenerationMenu $confirmation = Read-Host "Please make a selection" @@ -106,6 +84,9 @@ Do { # update the new certificate info & set commandAssociation to $null # TODO: commandAssociation not being set to null $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username + # Add the cert info tracking to the object + $certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false + $certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value $null Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null } else { # Create a new table entry @@ -149,6 +130,9 @@ Do { # update the new certificate info & set commandAssociation to $null # TODO: commandAssociation not being set to null $certInfo = Get-CertInfo -UserCerts -username $confirmUser.username + # Add the cert info tracking to the object + $certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false + $certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value $null Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null } else { Write-Warning "setting new table for user $($confirmUser.username)" @@ -173,6 +157,8 @@ Do { # update the new certificate info & set commandAssociation to $null # TODO: commandAssociation not being set to null $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username + $certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false + $certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value $null Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null } else { # Create a new table entry @@ -199,6 +185,8 @@ Do { # update the new certificate info & set commandAssociation to $null # TODO: commandAssociation not being set to null $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username + $certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false + $certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value $null Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null } else { # Create a new table entry diff --git a/scripts/automation/Radius/Start-RadiusDeployment.ps1 b/scripts/automation/Radius/Start-RadiusDeployment.ps1 index 5d3425b7e..9c1883dd4 100644 --- a/scripts/automation/Radius/Start-RadiusDeployment.ps1 +++ b/scripts/automation/Radius/Start-RadiusDeployment.ps1 @@ -9,6 +9,7 @@ if ($JCAPIKEY.length -ne 40) { # Do not modify below ################################################################################ # set script root +#TODO: move to global functions $global:JCScriptRoot = $PSScriptRoot # Import the functions From b97ceb2c6d22d662e09b0a8b763920ea642384d1 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Wed, 3 Jan 2024 15:50:47 -0700 Subject: [PATCH 011/346] show progress of user cert distribution --- .../Private/Deploy-UserCertificate.ps1 | 116 ++++++++------- .../Private/menus/Show-DistributionMenu.ps1 | 11 +- .../Private/menus/Show-GenerationMenu.ps1 | 2 +- .../Private/menus/Show-ProgressBarText.ps1 | 13 -- .../Private/menus/Show-RadiusProgress.ps1 | 30 ++++ .../Private/menus/Show-StatusMessage.ps1 | 2 +- .../Private/settings/Update-JCRUsersJson.ps1 | 8 - .../Functions/Public/Distribute-UserCerts.ps1 | 140 +++++++++++------- .../Functions/Public/Generate-UserCerts.ps1 | 18 ++- 9 files changed, 204 insertions(+), 136 deletions(-) delete mode 100644 scripts/automation/Radius/Functions/Private/menus/Show-ProgressBarText.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/menus/Show-RadiusProgress.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 b/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 index 596b2ce15..12eba5c3c 100644 --- a/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 +++ b/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 @@ -7,15 +7,13 @@ function Deploy-UserCertificate { $userObject, # Parameter help description [Parameter()] - [switch] - $force + [bool] + $invokeCommands ) begin { # $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 10 - - # take stock of the user object - + #TODO: validate user object, if missing commandAssociations, or certInfo, return false } process { @@ -83,6 +81,7 @@ function Deploy-UserCertificate { if ($Command.Count -ge 1) { # $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):MacOSX command already exists, skipping..." + $status_commandGenerated = $false continue } @@ -246,6 +245,7 @@ fi $user.commandAssociations += $CommandTable # Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Mac OS X" + $status_commandGenerated = $true } 'Windows' { @@ -257,6 +257,7 @@ fi if ($Command.Count -ge 1) { # $confirmation = Write-Host "[status] RadiusCert-Install:$($user.userName):Windows command already exists, skipping..." + $status_commandGenerated = $false continue } @@ -388,12 +389,13 @@ if (`$CurrentUser -eq "$($user.localUsername)") { $user.commandAssociations += $CommandTable # Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" + $status_commandGenerated = $true } $null { # Write-host "$($user.username) is not associated with any systems, skipping command generation" - - + $status_commandGenerated = $false + $result_deployed = $false } } # Update the user table with the information from the generated commands: @@ -402,61 +404,73 @@ if (`$CurrentUser -eq "$($user.localUsername)") { Set-UserTable -index $userIndex -commandAssociationsObject $user.commandAssociations # Invoke Commands #TODO:: skip if this is not per user basis - switch ($force) { + # switch ($force) { + # $true { + # # nothing do to + # $action_invoke = $true + # } + # $false { + # $confirmation = Read-Host "Would you like to invoke commands? [y/n]" + # # $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" + + # while ($confirmation -ne 'y') { + # if ($confirmation -eq 'n') { + # $action_invoke = $false + # break + # } + # $confirmation = Read-Host "Would you like to invoke commands? [y/n]" + # } + # } + # } + switch ($invokeCommands) { $true { - # nothing do to - } - $false { - $confirmation = Read-Host "Would you like to invoke commands? [y/n]" - # $UserArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" - - while ($confirmation -ne 'y') { - if ($confirmation -eq 'n') { - Write-Host "[status] To invoke the commands at a later time, select option '4' to monitor your User Certification Distribution" - Write-Host "[status] Returning to main menu" - exit + # finally update the user table to note that the command has been run, the cert has been deployed + # get the object once more: + $userObjectFromTable, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot\users.json" -userid $user.userid + # Set commandPreviouslyRun property to true if there are command associations to set + if ($userObjectFromTable.commandAssociations) { + $userObjectFromTable.commandAssociations | ForEach-Object { $_.commandPreviouslyRun = $true } + # set the deployed status to true, set the date + if (Get-Member -inputObject $userObjectFromTable.certInfo -name "deployed" -MemberType Properties) { + # if ($userObjectFromTable.certInfo.deployed) { + $userObjectFromTable.certInfo.deployed = $true + } else { + $userObjectFromTable.certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false + } - $confirmation = Read-Host "Would you like to invoke commands? [y/n]" - } - } - } + if (Get-Member -inputObject $userObjectFromTable.certInfo -name "deploymentDate" -MemberType Properties) { + # if ($userObjectFromTable.certInfo.deploymentDate) { + $userObjectFromTable.certInfo.deploymentDate = (Get-Date) - # TODO: for individual users, invoke command retry by username - # else invoke for all? - # finally update the user table to note that the command has been run, the cert has been deployed - # get the object once more: - $userObjectFromTable, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot\users.json" -userid $user.userid - # Set commandPreviouslyRun property to true if there are command associations to set - if ($userObjectFromTable.commandAssociations) { - $userObjectFromTable.commandAssociations | ForEach-Object { $_.commandPreviouslyRun = $true } - # set the deployed status to true, set the date - if (Get-Member -inputObject $userObjectFromTable.certInfo -name "deployed" -MemberType Properties) { - # if ($userObjectFromTable.certInfo.deployed) { - $userObjectFromTable.certInfo.deployed = $true - } else { - $userObjectFromTable.certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false + } else { + $userObjectFromTable.certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value (Get-Date) + } + Set-UserTable -index $userIndex -commandAssociationsObject $userObjectFromTable.commandAssociations -certInfoObject $userObjectFromTable.certInfo + $invokeCommands = invoke-commandByUserId -userID $user.userid + $result_deployed = $true + } } - if (Get-Member -inputObject $userObjectFromTable.certInfo -name "deploymentDate" -MemberType Properties) { - # if ($userObjectFromTable.certInfo.deploymentDate) { - $userObjectFromTable.certInfo.deploymentDate = (Get-Date) - - } else { - - $userObjectFromTable.certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value (Get-Date) + $false { + $result_deployed = $false + } + Default { } - Set-UserTable -index $userIndex -commandAssociationsObject $userObjectFromTable.commandAssociations -certInfoObject $userObjectFromTable.certInfo - $invokeCommands = invoke-commandByUserId -userID $user.userid - # Write-Host "[status] Commands Invoked" } - # if ($userObjectFromTable.certInfo.deployed) - # $userObjectFromTable.certInfo.deployed = $true - # $userObjectFromTable.certInfo.deploymentDate = (Get-Date) - # invoke the command + + } } + end { + #TODO: eventually add message if we fail to generate a command + $resultTable = [ordered]@{ + 'Username' = $user.username; + 'Command Generated' = $status_commandGenerated; + 'Command Deployed' = $result_deployed + } + return $resultTable } } \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-DistributionMenu.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-DistributionMenu.ps1 index 00d94352b..75463005e 100644 --- a/scripts/automation/Radius/Functions/Private/menus/Show-DistributionMenu.ps1 +++ b/scripts/automation/Radius/Functions/Private/menus/Show-DistributionMenu.ps1 @@ -16,22 +16,21 @@ function Show-DistributionMenu { Write-Host $(PadCenter -string $Title -char '=') Write-Host $(PadCenter -string "Select an option below to deploy user certificates to systems`n" -char ' ') -ForegroundColor Yellow # deployment progress of newly generated certs - if ($userCertInfo) { + if ($certObjectArray) { Write-Host $(PadCenter -string ' Certificate Information ' -char '-') - Write-Host "Total # of local user certificates:" $userCertInfo.count - Write-Host "Total # of already distributed certificates:" ($userCertInfo | Where-Object { $_.deployed -eq $true }).count - Write-Host "Total # of un-deployed certificates:" ($userCertInfo | Where-Object { ( $_.deployed -eq $false) -or (-not $_.deployed) }).count + Write-Host "Total # of local user certificates:" $certObjectArray.count + Write-Host "Total # of already distributed certificates:" ($certObjectArray | Where-Object { $_.deployed -eq $true }).count + Write-Host "Total # of un-deployed certificates:" ($certObjectArray | Where-Object { ( $_.deployed -eq $false) -or (-not $_.deployed) }).count } # ==== instructions ==== - # TODO: move notes from below into a more legible location Write-Host $(PadCenter -string ' User Certificate Deployment Options ' -char '-') # List options: Write-Host "1: Press '1' to generate new commands for ALL users. `n`t$([char]0x1b)[96mNOTE: This will remove any previously generated Radius User Certificate Commands titled 'RadiusCert-Install:*'`n`tand re-deploy their certificate file" Write-Host "2: Press '2' to generate new commands for NEW RADIUS users. `n`t$([char]0x1b)[96mNOTE: This will only generate commands for users whos certificate has not been deployed." Write-Host "3: Press '3' to generate new commands for ONE Specific RADIUS user." - Write-Host "E: Press 'E' to exit." + Write-Host "E: Press 'E' to return to main menu." Write-Host $(PadCenter -string "-" -char '-') } diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-GenerationMenu.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-GenerationMenu.ps1 index 7df846486..817d6bd27 100644 --- a/scripts/automation/Radius/Functions/Private/menus/Show-GenerationMenu.ps1 +++ b/scripts/automation/Radius/Functions/Private/menus/Show-GenerationMenu.ps1 @@ -18,7 +18,7 @@ function Show-GenerationMenu { Write-Host "2: Press '2' to generate new certificates for ONE RADIUS user. `n`t$([char]0x1b)[96mNOTE: you will be prompted to overwrite any previously generated certificates" Write-Host "3: Press '3' to re-generate new certificates for ALL users. `n`t$([char]0x1b)[96mNOTE: This will overwrite any local generated certificates" Write-Host "4: Press '4' to re-generate new certificates for users who's cert is set to expire shortly. `n`t$([char]0x1b)[96mNOTE: This will overwrite any local generated certificates" - Write-Host "E: Press 'E' to exit." + Write-Host "E: Press 'E' to return to main menu." Write-Host $(PadCenter -string "-" -char '-') } \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-ProgressBarText.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-ProgressBarText.ps1 deleted file mode 100644 index fe4708e4b..000000000 --- a/scripts/automation/Radius/Functions/Private/menus/Show-ProgressBarText.ps1 +++ /dev/null @@ -1,13 +0,0 @@ -Function Show-ProgressBarText { - param( - [int]$completedItems, - [int]$totalItems, - [string]$actionText - ) - $pComplete = ($completedItems / $totalItems) * 100 - $barLength = [math]::Ceiling((($pComplete / 100)) * 30) - - $pbar = "[" + ('▯' * $barLength) + (' ' * (30 - $barLength)) + "]" - $pmessage = "${actionText}: $completedItems out of $TotalItems ($pComplete%) $pbar" - Write-Host $pmessage -NoNewline -} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-RadiusProgress.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-RadiusProgress.ps1 new file mode 100644 index 000000000..a9de709d6 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/menus/Show-RadiusProgress.ps1 @@ -0,0 +1,30 @@ +Function Show-RadiusProgress { + param( + [int]$completedItems, + [int]$totalItems, + [string]$actionText, + [System.Object]$previousOperationResult + ) + + $propertyNames = @($previousOperationResult.keys) + $propertyNames += "Items Processed" + $headerString = "{0,-$($($propertyNames[0]).length)}" + + for ($i = 1; $i -lt $propertyNames.Count; $i++) { + <# Action that will repeat until the condition is met #> + $headerString += " | {$i,-$($($propertyNames[$i]).length)}" + } + if ($completedItems -eq 1) { + write-host ($headerString -f $propertyNames) + $propertyvalues = @($previousOperationResult.Values) + $propertyvalues += "$($completedItems) / $($TotalItems)" + + } else { + $propertyvalues = @($previousOperationResult.Values) + $propertyvalues += "$($completedItems) / $($TotalItems)" + + } + + write-host ($headerString -f $propertyValues) + +} diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-StatusMessage.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-StatusMessage.ps1 index 797d7b7cc..25f008834 100644 --- a/scripts/automation/Radius/Functions/Private/menus/Show-StatusMessage.ps1 +++ b/scripts/automation/Radius/Functions/Private/menus/Show-StatusMessage.ps1 @@ -8,5 +8,5 @@ function Show-StatusMessage { ) Write-Host "`r" write-host "[status] - $message" - start-sleep 1 + start-sleep 3 } \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 index c81477311..e49ca46c7 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 @@ -14,7 +14,6 @@ function Update-JCRUsersJson { foreach ($userid in $Global:JCRRadiusMembers.keys) { $MatchedUser = $GLOBAL:JCRUsers[$userid] - Show-ProgressBarText -completedItems $Global:JCRRadiusMembers.keys.IndexOf($userid) -totalItems ($Global:JCRRadiusMembers.keys).count -ActionText "Updating latest Radius group membership" $userArrayObject, $userIndex = Get-UserFromTable -userID $userid -jsonFilePath "$JCScriptRoot/users.json" if ($userIndex -ge 0) { @@ -32,12 +31,6 @@ function Update-JCRUsersJson { <#Do this if a terminating exception happens#> $difference = $null } - # if ($difference) { - # # if there's a difference in systemIDS, update table with the incomingSystemObject - # # $userTable = New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername - # # Update-JsonData -jsonFilePath "$JCScriptRoot/users.json" -userID $userID -updatedUserTable $userTable - # } - Write-Host "`r" -NoNewline } else { # case for new user @@ -57,7 +50,6 @@ function Update-JCRUsersJson { } # Remove the User From Table } - Show-StatusMessage -message "Finished pulling radius group membership updates" } end { $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" diff --git a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 index 58c02817d..9adb74732 100644 --- a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 @@ -1,84 +1,118 @@ # Import Global Config: # . "$JCScriptRoot/config.ps1" # Connect-JCOnline $JCAPIKEY -force +[CmdletBinding(DefaultParameterSetName = 'gui')] +param ( + [Parameter(ParameterSetName = 'cli')] + [ValidateSet("All", "New", "ByUsername")] + [system.String] + $generateType, + # Parameter help description + [Parameter(ParameterSetName = 'cli')] + [System.String] + $username +) ################################################################################ # Do not modify below ################################################################################ +# TODO: move into function file & rename +function pfi { + $promptForInvokeInput = $true + while ($promptForInvokeInput) { + $invokeCommands = Read-Host "Would you like to invoke commands after generating them y/n? (or 'E' to return to menu)" + switch ($invokeCommands) { + 'e' { + $promptForInvokeInput = $false + break + } + 'n' { + return $false + } + 'y' { + return $true + } + default { + write-host "invalid input please type 'y' or 'n' (or 'E' to return to menu)" + } + } + } +} # Import the users.json file and convert to PSObject $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 10 -# TODO: surface this information -# Check to see if previous commands exist -# $SearchFilter = @{ -# searchTerm = "RadiusCert-Install:" -# fields = @('name') -# } -# $RadiusCertCommands = Search-JcSdkCommand -SearchFilter $SearchFilter -Fields name - -# $RadiusCertCommandList = New-Object System.Collections.ArrayList -# foreach ($command in $RadiusCertCommands) { -# $commandSplit = $command.name.split(':') -# $RadiusCertCommandList.Add([PSCustomObject]@{ -# CommandName = $command.name -# Username = $commandSplit[1] -# CommandID = $command._id -# }) | Out-Null -# } -# $existingCommandUsers = $RadiusCertCommandList.Username | Get-Unique -# $newRadiusUsers = (Compare-Object $userarray.username $existingCommandUsers).InputObject - -# TODO: revamp with menu screen -# TODO: generate new commands for a single user -# TODO: why this if statement here: do { - Show-DistributionMenu -CertObjectArray $userArray.certInfo - $confirmation = Read-Host "Please make a selection" + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-DistributionMenu -CertObjectArray $userArray.certInfo + $confirmation = Read-Host "Please make a selection" + $invokeCommands = pfi + } + 'cli' { + $confirmationMap = @{ + 'All' = '1'; + 'New' = '2'; + "ByUsername" = '3'; + } + $confirmation = $confirmationMap[$generateType] + } + } switch ($confirmation) { '1' { for ($i = 0; $i -lt $userArray.Count; $i++) { - <# Action that will repeat until the condition is met #> - Show-ProgressBarText -completedItems $i -totalItems $userArray.Count -ActionText "Distributing Radius Certificates" - $var = Deploy-UserCertificate -userObject $userArray[$i] -force | Out-Null - Write-Host "`r" -NoNewline + $result = Deploy-UserCertificate -userObject $userArray[$i] -invokeCommands $invokeCommands + Show-RadiusProgress -completedItems ($i + 1) -totalItems $userArray.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $result + # Write-Host "`r" -NoNewline } Show-StatusMessage -Message "Finished Distributing Certificates" } '2' { - $usersWithoutLatestCert = $userArray | Where-Object { ( $_.deployed -eq $false) -or (-not $_.deployed) } - + # TODO: prompt to invoke after creating commands + $usersWithoutLatestCert = $userArray | Where-Object { ( $_.certinfo.deployed -eq $false) -or (-not $_.certinfo.deployed) } for ($i = 0; $i -lt $usersWithoutLatestCert.Count; $i++) { - <# Action that will repeat until the condition is met #> - Show-ProgressBarText -completedItems $i -totalItems $usersWithoutLatestCert.Count -ActionText "Distributing Radius Certificates" - $var = Deploy-UserCertificate -userObject $usersWithoutLatestCert[$i] -force | Out-Null - Write-Host "`r" -NoNewline + $result = Deploy-UserCertificate -userObject $usersWithoutLatestCert[$i] -invokeCommands $invokeCommands + Show-RadiusProgress -completedItems ($i + 1) -totalItems $usersWithoutLatestCert.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $result + # Write-Host "`r" -NoNewline } Show-StatusMessage -Message "Finished Distributing Certificates" } '3' { - try { - Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore - } catch { - New-Variable -Name "ConfirmUser" -Value $null + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + try { + Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore + } catch { + New-Variable -Name "ConfirmUser" -Value $null + } + while (-not $confirmUser) { + $confirmationUser = Read-Host "Enter the Username of the user (or '@exit' to return to menu)" + if ($confirmationUser -eq '@exit') { + break + } + try { + $confirmUser = Test-UserFromHash -username $confirmationUser -debug + } catch { + Write-Warning "User specified $confirmationUser was not found within the Radius Server Membership Lists" + } + } + } + 'cli' { + $confirmUser = Test-UserFromHash -username $username -debug + } } - while (-not $confirmUser) { - # TODO: Offer option to go back a step and exit the while loop - $confirmationUser = Read-Host "Enter the Username or UserID of the user" - $confirmUser = Test-UserFromHash -username $confirmationUser -debug + if ($confirmUser) { + # Get the userobject + index from users.json + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id + # Add user to a list for processing + $UserSelectionArray = $userArray[$userIndex] + # Process existing commands/ Generate new commands/ Deploy new Certificate + $result = Deploy-UserCertificate -userObject $UserSelectionArray -invokeCommands $invokeCommands + Show-RadiusProgress -completedItems $UserSelectionArray.count -totalItems $UserSelectionArray.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $result + Show-StatusMessage -Message "Finished Distributing Certificates" } - # Get the userobject + index from users.json - $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id - # Add user to a list for processing - $UserSelectionArray = $userArray[$userIndex] - # Process existing commands/ Generate new commands/ Deploy new Certificate - Deploy-UserCertificate -userObject $UserSelectionArray } } } while ($confirmation -ne 'E') - - -Write-Host "[status] Select option '4' to monitor your User Certification Distribution" -Write-Host "[status] Returning to main menu" diff --git a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 index 9981471fe..517fc3b91 100644 --- a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 @@ -97,9 +97,21 @@ Do { Break } '2' { - while (-not $confirmUser.id) { - $confirmationUser = Read-Host "Enter the Username or UserID of the user" - $confirmUser = Test-UserFromHash -username $confirmationUser -debug + try { + Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore + } catch { + New-Variable -Name "ConfirmUser" -Value $null + } + while (-not $confirmUser) { + $confirmationUser = Read-Host "Enter the Username of the user (or '@exit' to return to menu)" + if ($confirmationUser -eq '@exit') { + break + } + try { + $confirmUser = Test-UserFromHash -username $confirmationUser -debug + } catch { + Write-Warning "User specified $confirmationUser was not found within the Radius Server Membership Lists" + } } # Generate a new cert for this user: if (Test-Path -Path "$JCScriptRoot/UserCerts/$($confirmUser.username)-client-signed.pfx") { From 28ce92c0283bf4e2cd63a1e08d36654ef31bb511 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Fri, 5 Jan 2024 11:59:12 -0700 Subject: [PATCH 012/346] every file as functions, cleanup --- .../Functions/JCRadiusCertDeployment.psm1 | 22 +- .../Private/Deploy-UserCertificate.ps1 | 35 +- .../Functions/Private/Get-CertKeyPass.ps1 | 1 - .../Private/Get-CommandObjectTable.ps1 | 4 +- .../HashFunctions/Test-UserFromHash.ps1 | 4 +- .../Private/Invoke-CommandsRetry.ps1 | 48 ++- .../Private/Invoke-UserCertProcess.ps1 | 114 ++++++ .../Private/Show-CertDeploymentMenu.ps1 | 12 - .../Private/SystemTable/New-SystemTable.ps1 | 4 +- .../Radius/Functions/Private/Test-User.ps1 | 41 -- .../Private/UserJson/Get-UserJsonData.ps1 | 31 ++ .../Private/UserJson/Set-UserJsonData.ps1 | 16 + .../Private/UserTable/Get-UserFromTable.ps1 | 19 +- .../Private/UserTable/New-UserTable.ps1 | 12 +- .../Private/UserTable/Set-UserTable.ps1 | 9 +- .../commands/get-queuedCommandByUser.ps1 | 1 - .../commands/invoke-commandByUserId.ps1 | 4 +- .../Private/menus/Get-ResponsePrompt.ps1 | 29 ++ .../Private/menus/Show-CertDeploymentMenu.ps1 | 17 + .../{ => menus}/Show-RadiusMainMenu.ps1 | 4 +- .../Private/menus/Show-RadiusProgress.ps1 | 5 +- .../Private/settings/Get-JCRGlobalVars.ps1 | 11 +- .../Private/settings/Update-JCRUsersJson.ps1 | 17 +- .../Public/Cert-GenerateOrImport.ps1 | 12 - .../Functions/Public/Distribute-UserCerts.ps1 | 222 ++++++----- .../Functions/Public/Generate-RootCert.ps1 | 130 ++++--- .../Functions/Public/Generate-UserCerts.ps1 | 363 ++++++++---------- .../Public/Monitor-CertDeployment.ps1 | 59 ++- .../Radius/Start-RadiusDeployment.ps1 | 10 +- .../automation/Radius/Tests/Invoke-Pester.ps1 | 124 ++++++ .../Public/Distribute-UserCerts.Tests.ps1 | 9 + scripts/automation/Radius/readme.md | 4 +- 32 files changed, 857 insertions(+), 536 deletions(-) create mode 100644 scripts/automation/Radius/Functions/Private/Invoke-UserCertProcess.ps1 delete mode 100644 scripts/automation/Radius/Functions/Private/Show-CertDeploymentMenu.ps1 delete mode 100644 scripts/automation/Radius/Functions/Private/Test-User.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/UserJson/Get-UserJsonData.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/UserJson/Set-UserJsonData.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/menus/Get-ResponsePrompt.ps1 create mode 100644 scripts/automation/Radius/Functions/Private/menus/Show-CertDeploymentMenu.ps1 rename scripts/automation/Radius/Functions/Private/{ => menus}/Show-RadiusMainMenu.ps1 (94%) delete mode 100644 scripts/automation/Radius/Functions/Public/Cert-GenerateOrImport.ps1 create mode 100644 scripts/automation/Radius/Tests/Invoke-Pester.ps1 create mode 100644 scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 diff --git a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 b/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 index 33583af7c..8432ab694 100644 --- a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 +++ b/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 @@ -1,6 +1,4 @@ -# Load all functions from public and private folders -#TODO: why define both? -$Private = @( Get-ChildItem -Path "$JCScriptRoot/Functions/Private/*.ps1" -Recurse) +# Load all functions from private folders $Private = @( Get-ChildItem -Path "$PSScriptRoot/Private/*.ps1" -Recurse) Foreach ($Import in $Private) { Try { @@ -10,6 +8,24 @@ Foreach ($Import in $Private) { } } +# Load all public functions: +$Private = @( Get-ChildItem -Path "$PSScriptRoot/Public/*.ps1" -Recurse) +Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } +} + +# setup: +# build required users.json file: +# set script root: +$global:JCScriptRoot = "$PSScriptRoot/../" + +# import config: +. "$JCScriptRoot/config.ps1" +# try to get the settings file, create new one if it does not exist: $global:JCRConfig = Get-JCRSettingsFile # Get global variables or update if necessary diff --git a/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 b/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 index 12eba5c3c..14ebb7924 100644 --- a/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 +++ b/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 @@ -8,12 +8,37 @@ function Deploy-UserCertificate { # Parameter help description [Parameter()] [bool] - $invokeCommands + $forceInvokeCommands, + # prompt replace existing certificate + [Parameter()] + [switch] + $prompt ) begin { - # $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 10 #TODO: validate user object, if missing commandAssociations, or certInfo, return false + + switch ($forceInvokeCommands) { + $true { + $invokeCommands = $true + } + $false { + $invokeCommands = $false + } + } + switch ($prompt) { + $true { + $invokeCommandsChoice = Get-ResponsePrompt -message "Would you like to invoke commands after they've been generated?" + switch ($invokeCommandsChoice) { + $true { + $invokeCommands = $true + } + $false { + $invokeCommands = $false + } + } + } + } } process { @@ -71,7 +96,7 @@ function Deploy-UserCertificate { # Create the zip Compress-Archive -Path $userPfx -DestinationPath $userPfxZip -CompressionLevel NoCompression -Force # Find OS of System - switch ($user.systemAssociations.device_os) { + switch ($user.systemAssociations.osFamily) { 'macOS' { # Get the macOS system ids $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'macOS' } | Select-Object systemId @@ -388,12 +413,12 @@ if (`$CurrentUser -eq "$($user.localUsername)") { } $user.commandAssociations += $CommandTable - # Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" + Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" $status_commandGenerated = $true } $null { - # Write-host "$($user.username) is not associated with any systems, skipping command generation" + Write-host "$($user.username) is not associated with any systems, skipping command generation" $status_commandGenerated = $false $result_deployed = $false } diff --git a/scripts/automation/Radius/Functions/Private/Get-CertKeyPass.ps1 b/scripts/automation/Radius/Functions/Private/Get-CertKeyPass.ps1 index e4f295c55..164584ffe 100644 --- a/scripts/automation/Radius/Functions/Private/Get-CertKeyPass.ps1 +++ b/scripts/automation/Radius/Functions/Private/Get-CertKeyPass.ps1 @@ -16,7 +16,6 @@ function Get-CertKeyPass { $secureCertKeyPass = Read-Host -Prompt "Enter a password for the certificate key" -AsSecureString $certKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText $checkKey = openssl rsa -in $foundKeyPem -check -passin pass:$($certKeyPass) 2>&1 - $checkKey if ($checkKey -match "RSA key ok") { # Save password to ENV variable Write-Host "Saving password as Environment Variable" diff --git a/scripts/automation/Radius/Functions/Private/Get-CommandObjectTable.ps1 b/scripts/automation/Radius/Functions/Private/Get-CommandObjectTable.ps1 index 2ab94b4c7..28dff5b77 100644 --- a/scripts/automation/Radius/Functions/Private/Get-CommandObjectTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/Get-CommandObjectTable.ps1 @@ -97,7 +97,7 @@ Function Get-CommandObjectTable { # Iterate through all the associated commands foreach ($command in $commandsObject.commandAssociations) { # Check to see if the current command is pending/queued - if ($command.commandId -in $QueuedCommands.command) { + if (($command.commandId -in $QueuedCommands.command) -And ($command -ne $null)) { # Get the queued command info for all workflows $queuedCommandInfo = $QueuedCommands | Where-Object command -EQ $command.commandId # Get the individual workflow information @@ -137,7 +137,7 @@ Function Get-CommandObjectTable { } } - if ($CommandObjectTable.commandName -notcontains $command.commandName) { + if (($CommandObjectTable.commandName -notcontains $command.commandName) -And ($command -ne $null)) { if (!(Get-JcSdkCommandAssociation -CommandId $command.commandId -Targets system)) { $CommandTable = @{ commandName = $command.commandName diff --git a/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 b/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 index 417183eef..17e56c615 100644 --- a/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 +++ b/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 @@ -20,7 +20,7 @@ function Test-UserFromHash { switch ($PSCmdlet.ParameterSetName) { 'userid' { # validate that the userID is in the radiusMembership hash: - if ($Global:JCRRadiusMembers[$userID]) { + if ($Global:JCRRadiusMembers.userID.IndexOf($userID)) { # finally return the $matchedUser object $matchedUser = $Global:JCRUsers[$userID] } else { @@ -34,7 +34,7 @@ function Test-UserFromHash { # Get the UserID from the keys $matchedUserID = $Global:JCRUsers.keys[$matchedIndex] # validate that the userID is in the radiusMembership hash: - if ($Global:JCRRadiusMembers[$matchedUserID]) { + if ($Global:JCRRadiusMembers.username.IndexOf($username)) { # finally return the $matchedUser object $matchedUser = $Global:JCRUsers[$matchedUserID] } else { diff --git a/scripts/automation/Radius/Functions/Private/Invoke-CommandsRetry.ps1 b/scripts/automation/Radius/Functions/Private/Invoke-CommandsRetry.ps1 index c2a96a504..940f4ebfe 100644 --- a/scripts/automation/Radius/Functions/Private/Invoke-CommandsRetry.ps1 +++ b/scripts/automation/Radius/Functions/Private/Invoke-CommandsRetry.ps1 @@ -7,7 +7,8 @@ Function Invoke-CommandsRetry { ) begin { $RetryCommands = @() - $commandsObject = Get-Content -Raw -Path $jsonFile | ConvertFrom-Json -Depth 6 + $userArray = Get-UserJsonData + # $commandsObject = Get-Content -Raw -Path $jsonFile | ConvertFrom-Json -Depth 6 $queuedCommands = Get-JCQueuedCommands $SearchFilter = @{ searchTerm = 'RadiusCert-Install' @@ -19,34 +20,41 @@ Function Invoke-CommandsRetry { } process { # Prompt to rerun commands that have failed or expired - Foreach ($command in $commandsObject.commandAssociations) { + Foreach ($user in ($userArray) | Where-Object { $_.commandAssociations -ne $null }) { + $userIndex = $userArray.userId.IndexOf($user.userID) $failedCommands = $mostRecentCommandResults | Where-Object DataExitCode -NE 0 - - if ($queuedCommands.command -contains $command.commandId) { - Write-Host "[status] $($command.commandName) is currently $([char]0x1b)[93mPENDING" - continue - } else { - if (($failedCommands.workflowId -contains $command.commandId) -or ($command.commandPreviouslyRun -eq $false) -or ($QueuedCommands.command -notcontains $command.commandId -and $finishedCommands.workflowId -notcontains $command.commandId)) { - try { - if (!(Get-JcSdkCommandAssociation -CommandId $command.commandId -Targets system)) { - continue - } else { - Invoke-CommandRun -commandID $command.commandId - Write-Host "[status] $([char]0x1b)[92mInvoking $($command.commandName)" - # set command to queued - $command.commandQueued = $true + $commands = $user.commandAssociations + # foreach command in the command object + foreach ($command in $commands) { + if ($queuedCommands.command -contains $command.commandId) { + Write-Host "[status] $($command.commandName) is currently $([char]0x1b)[93mPENDING" + continue + } else { + if (($failedCommands.workflowId -contains $command.commandId) -or ($command.commandPreviouslyRun -eq $false) -or ($QueuedCommands.command -notcontains $command.commandId -and $finishedCommands.workflowId -notcontains $command.commandId)) { + try { + # invoke each user's command on their set systems: + invoke-commandByUserid -userID $user.userId + # update user table info per command + (($user.commandAssociations) | Where-Object { $_.commandId -eq $command.commandId }).commandPreviouslyRun = $true + (($user.commandAssociations) | Where-Object { $_.commandId -eq $command.commandId }).commandQueued = $true + $user.certInfo.deployed = $true + $user.certInfo.deploymentDate = (get-date) + Set-UserTable -index $userIndex -certInfoObject $user.certInfo -commandAssociationsObject $user.commandAssociations + # track retried commands $RetryCommands += $command.commandId + } catch { + Write-Error "$($command.commandName) could not be invoked" } - } catch { - Write-Error "$($command.commandId) could not be invoked" } } } } } end { - # write out/ update jsonFile - $commandsObject | ConvertTo-Json -Depth 6 | Out-File $jsonFile + # TODO: validate no need to write this out since it's done in Set-UserTable + # # write out/ update jsonFile + # Set-UserJsonData -userArray $userArray + # $commandsObject | ConvertTo-Json -Depth 6 | Out-File $jsonFile return $RetryCommands } } \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Invoke-UserCertProcess.ps1 b/scripts/automation/Radius/Functions/Private/Invoke-UserCertProcess.ps1 new file mode 100644 index 000000000..f5c3c3a81 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Invoke-UserCertProcess.ps1 @@ -0,0 +1,114 @@ +Function Invoke-UserCertProcess { + [CmdletBinding()] + param ( + [Parameter(ParameterSetName = 'radiusMember')] + [System.object] + $radiusMember, + [Parameter(ParameterSetName = 'selectedUserObject')] + [System.String] + $selectedUserObject, + [Parameter(Mandatory)] + [ValidateSet('EmailSAN', 'EmailDN', 'UsernameCN')] + [System.String] + $certType, + # force replace existing certificate + [Parameter()] + [switch] + $forceReplaceCert, + # prompt replace existing certificate + [Parameter()] + [switch] + $prompt + ) + begin { + + switch ($PSCmdlet.ParameterSetName) { + 'radiusMember' { + try { + + $MatchedUser = $GLOBAL:JCRUsers[$radiusMember.userID] + } catch { + exit + } + } + 'userObject' { + $MatchedUser = $GLOBAL:JCRUsers[$selectedUserObject.userid] + } + } + + # get the user from user.json + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $MatchedUser.id + # Test if the file exists: + switch (Test-Path "$JCScriptRoot/UserCerts/$($matchedUser.username)-client-signed.pfx") { + $true { + switch ($forceReplaceCert) { + $true { + $writeCert = $true + $cert_action = "Overwritten" + } + $false { + $writeCert = $false + $cert_action = "Skip Generation" + + } + } + if ($prompt) { + + $writeCert = Get-ResponsePrompt -message "A certifcate already exists for user: $($matchedUser.username) do you want to re-generate this certificate?" + switch ($writeCert) { + $true { + $cert_action = "Overwritten" + + } + $false { + $cert_action = "Skip Generation" + } + } + + } + } + $false { + $writeCert = $true + $cert_action = "New Cert Generated" + } + Default { + $writeCert = $false + $cert_action = "Unknown Action" + } + } + + } + process { + # if writeCert, generate the cert + if ($writeCert) { + Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" *> /dev/null + # validate that the cert was written correctly: + #TODO: validate and return as variable + } + + # generate the cert depending if -force or if new + if ($userIndex -ge 0) { + # update the new certificate info & set commandAssociation to $null + # TODO: commandAssociation not being set to null + $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username + # Add the cert info tracking to the object + $certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false + $certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value $null + Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null + } else { + # Create a new table entry + New-UserTable -id $MatchedUser.id -username $MatchedUser.username -localUsername $MatchedUser.systemUsername + } + + } + end { + #TODO: eventually add message if we fail to generate a command + $resultTable = [ordered]@{ + 'Username' = $MatchedUser.username; + 'Cert Action' = $cert_action; + 'Generated Date' = $certInfo.generated; + } + + return $resultTable + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Show-CertDeploymentMenu.ps1 b/scripts/automation/Radius/Functions/Private/Show-CertDeploymentMenu.ps1 deleted file mode 100644 index ddde39384..000000000 --- a/scripts/automation/Radius/Functions/Private/Show-CertDeploymentMenu.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -function Show-CertDeploymentMenu { - param ( - [string]$Title = 'Radius Cert Deployment Status' - ) - Clear-Host - Write-Host "================ $Title ================" - - Write-Host "1: Press '1' to view results." - Write-Host "2: Press '2' to view failed command runs" - Write-Host "3: Press '3' to invoke/retry commands" - Write-Host "E: Press 'E' to exit." -} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 b/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 index 27a4363c5..f23a24181 100644 --- a/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 @@ -16,9 +16,9 @@ function New-SystemTable { foreach ($system in $AssociationTable.systemAssociations) { # $systemInfo = $GLOBAL:SystemHash[$system.resource_object_id] $systemTable = @{ - systemId = $system.resource_object_id + systemId = $system.systemId displayName = $system.hostname - osFamily = $system.device_os + osFamily = $system.osFamily } $systemAssociations += $systemTable } diff --git a/scripts/automation/Radius/Functions/Private/Test-User.ps1 b/scripts/automation/Radius/Functions/Private/Test-User.ps1 deleted file mode 100644 index 5bc57524d..000000000 --- a/scripts/automation/Radius/Functions/Private/Test-User.ps1 +++ /dev/null @@ -1,41 +0,0 @@ -function Test-User { - [CmdletBinding()] - param ( - # Parameter help description - [Parameter(ParameterSetName = 'username')] - [System.String] - $username, - # Parameter help description - [Parameter(ParameterSetName = 'userid')] - [System.String] - $userID - ) - begin { - # Get User Group membership - # TODO: update if data is older than 30 mins - if ( -not $GLOBAL:RadiusUserMembership ) { - $GLOBAL:RadiusUserMembership = Get-JCUserGroupMember -ByID $JCUSERGROUP - } - } - process { - switch ($PSCmdlet.ParameterSetName) { - 'userid' { - $matchedUser = $Global:RadiusUserMembership | Where-Object { $userID -in $_.UserId } - $inputText = $userID - } - 'username' { - $matchedUser = $Global:RadiusUserMembership | Where-Object { $username -in $_.Username } - $inputText = $username - } - } - if ($matchedUser) { - Write-Debug "Matched Username Found: $($matchedUser.username)" - } else { - Write-Warning "User specified $inputText was not found within the Radius Server Membership Lists" - return $null - } - } - end { - return $matchedUser - } -} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/UserJson/Get-UserJsonData.ps1 b/scripts/automation/Radius/Functions/Private/UserJson/Get-UserJsonData.ps1 new file mode 100644 index 000000000..cf4b60152 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/UserJson/Get-UserJsonData.ps1 @@ -0,0 +1,31 @@ +function Get-UserJsonData { + [OutputType([System.Collections.ArrayList])] + [CmdletBinding()] + param ( + + ) + begin { + if (Test-Path -Path "$JCScriptRoot/users.json" -PathType Leaf) { + $content = (Get-Content -Raw -Path "$JCScriptRoot/users.json") + if ([string]::isNullOrEmpty($content)) { + $userArray = New-Object System.Collections.ArrayList + } else { + $userArray = $content | ConvertFrom-Json -Depth 6 + + } + } else { + $userArray = New-Object System.Collections.ArrayList + } + } + process { + # If the json is a single item, explicitly make it a list so we can add to it + If ($userArray.count -eq 1) { + $array = New-Object System.Collections.ArrayList + $array.add($userArray) | Out-Null + $userArray = $array + } + } + end { + return [System.Collections.ArrayList]$userArray + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/UserJson/Set-UserJsonData.ps1 b/scripts/automation/Radius/Functions/Private/UserJson/Set-UserJsonData.ps1 new file mode 100644 index 000000000..4d3aba071 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/UserJson/Set-UserJsonData.ps1 @@ -0,0 +1,16 @@ +function Set-UserJsonData { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [System.Object] + $userArray + ) + # If the json is a single item, explicitly make it a list so we can add to it + If ($userArray.count -eq 1) { + $array = New-Object System.Collections.ArrayList + $array.add($userArray) + $userArray = $array + } + $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" + +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 index a2c43bb67..c2b5f9ba5 100644 --- a/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 @@ -4,25 +4,12 @@ function Get-UserFromTable { [Parameter()] [System.String] $jsonFilePath, - [Parameter()] + [Parameter(Mandatory)] [System.String] - $userid + $userId ) begin { - # Import User.Json/ create list if it does not exist - if (Test-Path -Path $jsonFilePath -PathType Leaf) { - $userArray = Get-Content -Raw -Path $jsonFilePath | ConvertFrom-Json -Depth 6 - # If the json is a single item, explicitly make it a list so we can add to it - If ($userArray.count -eq 1) { - $array = New-Object System.Collections.ArrayList - $array.add($userArray) - $userArray = $array - } - - } else { - New-Item -Path $jsonFilePath - $userArray = @() - } + $userArray = Get-UserJsonData # Get the user from the jsonData $userObject = $Global:JCRUsers[$userid] diff --git a/scripts/automation/Radius/Functions/Private/UserTable/New-UserTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/New-UserTable.ps1 index 26d763c89..dfac6949f 100644 --- a/scripts/automation/Radius/Functions/Private/UserTable/New-UserTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/UserTable/New-UserTable.ps1 @@ -12,7 +12,13 @@ function New-UserTable { $localUsername ) begin { - $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 + $userArray = Get-UserJsonData + If ($userArray.count -eq 1) { + $array = New-Object System.Collections.ArrayList + $array.add($userArray) | Out-Null + $userArray = $array + } + $systemAssociations = New-SystemTable -userID $id # for new users, just set the commandAssociation to $null as they have # not yet been issued a command @@ -31,10 +37,10 @@ function New-UserTable { commandAssociations = $commandAssociations certInfo = $certInfo } - $userArray += $userTable + $userArray += ($userTable) } end { - $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" + Set-UserJsonData -userArray $userArray } } diff --git a/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 b/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 index 973164c73..ddcf49b07 100644 --- a/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 @@ -25,7 +25,7 @@ function Set-UserTable { ) begin { # Get User Array: - $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 + $userArray = Get-UserJsonData if ($PSBoundParameters.ContainsKey('index')) { $userIndex = $index $userObject = $userArray[$index] @@ -35,6 +35,11 @@ function Set-UserTable { $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $id } + # TODO: if index is not correct make a stink about it + if ($userIndex -lt 0) { + throw "user not in user table exiting" + } + # determine if there's data to update from parameter input, else just # use the existing data if ($systemAssociationsObject) { @@ -69,6 +74,6 @@ function Set-UserTable { } end { # update the userTable - $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" + Set-UserJsonData -userArray $userArray } } diff --git a/scripts/automation/Radius/Functions/Private/commands/get-queuedCommandByUser.ps1 b/scripts/automation/Radius/Functions/Private/commands/get-queuedCommandByUser.ps1 index eaa0576f3..a4c73cd8a 100644 --- a/scripts/automation/Radius/Functions/Private/commands/get-queuedCommandByUser.ps1 +++ b/scripts/automation/Radius/Functions/Private/commands/get-queuedCommandByUser.ps1 @@ -31,4 +31,3 @@ function get-queuedCommandByUser { return $response.results } } -get-queuedCommandByUser -username "Farmer_100" \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 index cde84b1e3..ba892ef00 100644 --- a/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 +++ b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 @@ -17,9 +17,9 @@ function invoke-commandByUserid { # get Windows Command $windows_commandId = ($userObject.commandAssociations | Where-Object { $_.commandName -match "Windows" }).commandId # get list of macOS systems - $macOS_systemIds = $userObject.systemAssociations | Where-Object { $_.device_os -eq "macOS" } + $macOS_systemIds = $userObject.systemAssociations | Where-Object { $_.osFamily -eq "macOS" } # get list of Windows systems - $windows_systemIds = $userObject.systemAssociations | Where-Object { $_.device_os -eq "Windows" } + $windows_systemIds = $userObject.systemAssociations | Where-Object { $_.osFamily -eq "Windows" } } process { diff --git a/scripts/automation/Radius/Functions/Private/menus/Get-ResponsePrompt.ps1 b/scripts/automation/Radius/Functions/Private/menus/Get-ResponsePrompt.ps1 new file mode 100644 index 000000000..49f2302ce --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/menus/Get-ResponsePrompt.ps1 @@ -0,0 +1,29 @@ +function Get-ResponsePrompt { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [System.String] + $message + ) + + + $promptForInvokeInput = $true + while ($promptForInvokeInput) { + $invokeCommands = Read-Host "$message`nPlease type: 'y'/'n' (or 'E' to return to menu)" + switch ($invokeCommands) { + 'e' { + $promptForInvokeInput = $false + break + } + 'n' { + return $false + } + 'y' { + return $true + } + default { + write-host "Invalid input`nPlease type 'y'/ 'n' (or 'E' to return to menu)" + } + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-CertDeploymentMenu.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-CertDeploymentMenu.ps1 new file mode 100644 index 000000000..cbbf8c914 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/menus/Show-CertDeploymentMenu.ps1 @@ -0,0 +1,17 @@ +function Show-CertDeploymentMenu { + $title = ' JumpCloud Radius Cert Deployment ' + Clear-Host + Write-Host $(PadCenter -string $Title -char '=') + Write-Host $(PadCenter -string "Select an option below to view certificate deployment status`n" -char ' ') -ForegroundColor Yellow + + # ==== instructions ==== + Write-Host $(PadCenter -string ' Certificate Deployment Result Options ' -char '-') + # List options: + Write-Host "1: Press '1' to view results." + Write-Host "2: Press '2' to view failed command runs" + Write-Host "3: Press '3' to invoke/retry commands" + Write-Host "E: Press 'E' to exit." + + Write-Host $(PadCenter -string "-" -char '-') + +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-RadiusMainMenu.ps1 similarity index 94% rename from scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 rename to scripts/automation/Radius/Functions/Private/menus/Show-RadiusMainMenu.ps1 index ee573c568..c7baca4a8 100644 --- a/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 +++ b/scripts/automation/Radius/Functions/Private/menus/Show-RadiusMainMenu.ps1 @@ -7,7 +7,7 @@ function Show-RadiusMainMenu { $userCertInfo = Get-CertInfo -UserCerts # Determine cut off date for expiring certs - $cutoffDate = (Get-Date).AddDays(15).Date + $global:cutoffDate = (Get-Date).AddDays(15).Date # Find all certs that will expire between current date and cut off date $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $cutoffDate @@ -43,7 +43,7 @@ function Show-RadiusMainMenu { # ==== GROUP/SSID/Global Variables ==== Write-Host $(PadCenter -string "Radius User Group: $($radiusUserGroup.Name)" -char " ") -ForegroundColor Green - Write-Host $(PadCenter -string "Radius Users: $($radiusUserGroupMemberCount)" -char " ") -ForegroundColor Green + Write-Host $(PadCenter -string "Total Radius Users: $($radiusUserGroupMemberCount)" -char " ") -ForegroundColor Green Write-Host $(PadCenter -string "Radius SSID(s): $radiusSSID" -char " ") -ForegroundColor Green Write-Host $(PadCenter -string "Last Updated User/System Data: $($Global:JCRConfig.globalVars.lastupdate)" -char " ") -ForegroundColor Green # Write-Host "`n" diff --git a/scripts/automation/Radius/Functions/Private/menus/Show-RadiusProgress.ps1 b/scripts/automation/Radius/Functions/Private/menus/Show-RadiusProgress.ps1 index a9de709d6..e0abe6287 100644 --- a/scripts/automation/Radius/Functions/Private/menus/Show-RadiusProgress.ps1 +++ b/scripts/automation/Radius/Functions/Private/menus/Show-RadiusProgress.ps1 @@ -11,10 +11,10 @@ Function Show-RadiusProgress { $headerString = "{0,-$($($propertyNames[0]).length)}" for ($i = 1; $i -lt $propertyNames.Count; $i++) { - <# Action that will repeat until the condition is met #> $headerString += " | {$i,-$($($propertyNames[$i]).length)}" } if ($completedItems -eq 1) { + Write-Host $(PadCenter -string " results " -char '-') write-host ($headerString -f $propertyNames) $propertyvalues = @($previousOperationResult.Values) $propertyvalues += "$($completedItems) / $($TotalItems)" @@ -26,5 +26,8 @@ Function Show-RadiusProgress { } write-host ($headerString -f $propertyValues) + if ($completedItems -eq $totalItems) { + Write-Host $(PadCenter -string "" -char '-') + } } diff --git a/scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 index 354de3dda..384606b74 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 @@ -46,13 +46,14 @@ function Get-JCRGlobalVars { # Get Radius membership list: $radiusMembers = Get-JcSdkUserGroupMember -GroupId $Global:JCUSERGROUP # add the username to the membership hash - $radiusMemberList = New-Object System.Collections.Hashtable + $radiusMemberList = New-Object System.Collections.ArrayList foreach ($member in $radiusMembers) { $radiusMemberList.Add( - $member.toID, @{ + [PSCustomObject]@{ 'userID' = $member.toID 'username' = $users[$member.toID].username - }) + } + ) } # Get Report Hash: $headers = @{ @@ -82,11 +83,11 @@ function Get-JCRGlobalVars { if (-not $userAssociationList[$item.user_object_id]) { $userAssociationList.add( $item.user_object_id, @{ - 'systemAssociations' = @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, device_os); + 'systemAssociations' = @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, @{Name = 'osFamily'; Expression = { $_.device_os } }); 'userData' = @($item | Select-Object -Property email, username) }) } else { - $userAssociationList[$item.user_object_id].systemAssociations += @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, device_os) + $userAssociationList[$item.user_object_id].systemAssociations += @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, @{Name = 'osFamily'; Expression = { $_.device_os } }) } } } diff --git a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 index e49ca46c7..9123a6b74 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 @@ -10,16 +10,17 @@ function Update-JCRUsersJson { } process { # validate that the system association data is correct in users.json: - $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 - foreach ($userid in $Global:JCRRadiusMembers.keys) { + # $userArray = Get-UserJsonData + foreach ($user in $Global:JCRRadiusMembers) { - $MatchedUser = $GLOBAL:JCRUsers[$userid] - $userArrayObject, $userIndex = Get-UserFromTable -userID $userid -jsonFilePath "$JCScriptRoot/users.json" + $MatchedUser = $GLOBAL:JCRUsers[$user.userID] + $userArrayObject, $userIndex = Get-UserFromTable -userID $user.userID -jsonFilePath "$JCScriptRoot/users.json" if ($userIndex -ge 0) { # $userArrayObject $currentSystemObject = $userArrayObject.systemAssociations - $incomingSystemObject = $Global:JCRAssociations[$userid].systemAssociations + $incomingSystemObject = $Global:JCRAssociations[$user.userID].systemAssociations + # determine if there's some difference that needs to be recorded: try { $difference = Compare-Object -ReferenceObject $currentSystemObject.systemId -DifferenceObject $incomingSystemObject.systemId @@ -34,15 +35,15 @@ function Update-JCRUsersJson { } else { # case for new user - New-UserTable -id $userid -username $MatchedUser.username -localUsername $matchedUser.systemUsername + New-UserTable -id $user.userID -username $MatchedUser.username -localUsername $matchedUser.systemUsername } } # lastly validate users that should no longer be recorded: - $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 + $userArray = Get-UserJsonData foreach ($user in $userArray) { # If userID from users.json is no longer in RadiusMembers.keys, then: - If ( -Not ($user.userId -in $Global:JCRRadiusMembers.keys) ) { + If ( -Not ($user.userid -in $Global:JCRRadiusMembers.userid) ) { # Get User From Table $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $user.userId $userArray = $userArray | Where-Object { $_.userID -ne $user.userId } diff --git a/scripts/automation/Radius/Functions/Public/Cert-GenerateOrImport.ps1 b/scripts/automation/Radius/Functions/Public/Cert-GenerateOrImport.ps1 deleted file mode 100644 index 7f31e54c4..000000000 --- a/scripts/automation/Radius/Functions/Public/Cert-GenerateOrImport.ps1 +++ /dev/null @@ -1,12 +0,0 @@ - -do { - Show-GenerateOrImportCertMenu - $option = Read-Host "Please make a selection" - switch ($option) { - '1' { - . "$JCScriptRoot/Functions/Public/Generate-RootCert.ps1" - } '2' { - Get-CertKeyPass - } - } -} until ($option.ToUpper() -eq 'E') \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 index 9adb74732..c8db039bd 100644 --- a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 @@ -1,118 +1,136 @@ -# Import Global Config: -# . "$JCScriptRoot/config.ps1" -# Connect-JCOnline $JCAPIKEY -force -[CmdletBinding(DefaultParameterSetName = 'gui')] -param ( - [Parameter(ParameterSetName = 'cli')] - [ValidateSet("All", "New", "ByUsername")] - [system.String] - $generateType, - # Parameter help description - [Parameter(ParameterSetName = 'cli')] - [System.String] - $username -) -################################################################################ -# Do not modify below -################################################################################ -# TODO: move into function file & rename -function pfi { - $promptForInvokeInput = $true - while ($promptForInvokeInput) { - $invokeCommands = Read-Host "Would you like to invoke commands after generating them y/n? (or 'E' to return to menu)" - switch ($invokeCommands) { - 'e' { - $promptForInvokeInput = $false - break - } - 'n' { - return $false - } - 'y' { - return $true - } - default { - write-host "invalid input please type 'y' or 'n' (or 'E' to return to menu)" - } - } - } -} +function Distribute-UserCerts { + [CmdletBinding(DefaultParameterSetName = 'gui')] + param ( + # Type of certs to distribute, All, New or byUsername + [Parameter(ParameterSetName = 'cli', Mandatory)] + [ValidateSet("All", "New", "ByUsername")] + [system.String] + $type, + # username + [Parameter(ParameterSetName = 'cli')] + [System.String] + $username, + # Force invoke commands after generation + [Parameter(ParameterSetName = 'cli')] + [switch] + $forceInvokeCommands + ) -# Import the users.json file and convert to PSObject -$userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 10 + # Import the users.json file and convert to PSObject + $userArray = Get-UserJsonData -do { - switch ($PSCmdlet.ParameterSetName) { - 'gui' { - Show-DistributionMenu -CertObjectArray $userArray.certInfo - $confirmation = Read-Host "Please make a selection" - $invokeCommands = pfi - } - 'cli' { - $confirmationMap = @{ - 'All' = '1'; - 'New' = '2'; - "ByUsername" = '3'; - } - $confirmation = $confirmationMap[$generateType] - } - } + do { + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-DistributionMenu -CertObjectArray $userArray.certInfo + $confirmation = Read-Host "Please make a selection" - switch ($confirmation) { - '1' { - for ($i = 0; $i -lt $userArray.Count; $i++) { - $result = Deploy-UserCertificate -userObject $userArray[$i] -invokeCommands $invokeCommands - Show-RadiusProgress -completedItems ($i + 1) -totalItems $userArray.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $result - # Write-Host "`r" -NoNewline } - Show-StatusMessage -Message "Finished Distributing Certificates" - } - '2' { - # TODO: prompt to invoke after creating commands - $usersWithoutLatestCert = $userArray | Where-Object { ( $_.certinfo.deployed -eq $false) -or (-not $_.certinfo.deployed) } - for ($i = 0; $i -lt $usersWithoutLatestCert.Count; $i++) { - $result = Deploy-UserCertificate -userObject $usersWithoutLatestCert[$i] -invokeCommands $invokeCommands - Show-RadiusProgress -completedItems ($i + 1) -totalItems $usersWithoutLatestCert.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $result - # Write-Host "`r" -NoNewline + 'cli' { + $confirmationMap = @{ + 'All' = '1'; + 'New' = '2'; + "ByUsername" = '3'; + } + $confirmation = $confirmationMap[$type] + # if force invoke is set, invoke the commands after generation: + switch ($forceInvokeCommands) { + $true { + $invokeCommands = $true + } + $false { + $invokeCommands = $false + } + } } - Show-StatusMessage -Message "Finished Distributing Certificates" - } - '3' { - switch ($PSCmdlet.ParameterSetName) { - 'gui' { - try { - Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore - } catch { - New-Variable -Name "ConfirmUser" -Value $null + + switch ($confirmation) { + '1' { + # case for all users + for ($i = 0; $i -lt $userArray.Count; $i++) { + $result = Deploy-UserCertificate -userObject $userArray[$i] -forceInvokeCommands $invokeCommands + Show-RadiusProgress -completedItems ($i + 1) -totalItems $userArray.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $result + } + # return after an action if cli, else stay in function + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-StatusMessage -Message "Finished Distributing Certificates" } - while (-not $confirmUser) { - $confirmationUser = Read-Host "Enter the Username of the user (or '@exit' to return to menu)" - if ($confirmationUser -eq '@exit') { - break - } + 'cli' { + return + } + } + } + '2' { + # case for new users; users that do not have certinfo marked as already deployed (i.e. users with new certs or un-deployed certs) + $usersWithoutLatestCert = $userArray | Where-Object { ( $_.certinfo.deployed -eq $false) -or (-not $_.certinfo.deployed) } + for ($i = 0; $i -lt $usersWithoutLatestCert.Count; $i++) { + $result = Deploy-UserCertificate -userObject $usersWithoutLatestCert[$i] -forceInvokeCommands $invokeCommands + Show-RadiusProgress -completedItems ($i + 1) -totalItems $usersWithoutLatestCert.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $result + } + # return after an action if cli, else stay in function + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-StatusMessage -Message "Finished Distributing Certificates" + } + 'cli' { + return + } + } + } + '3' { + # case for users by username + switch ($PSCmdlet.ParameterSetName) { + 'gui' { try { - $confirmUser = Test-UserFromHash -username $confirmationUser -debug + Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore } catch { - Write-Warning "User specified $confirmationUser was not found within the Radius Server Membership Lists" + New-Variable -Name "ConfirmUser" -Value $null + } + while (-not $confirmUser) { + $confirmationUser = Read-Host "Enter the Username of the user (or '@exit' to return to menu)" + if ($confirmationUser -eq '@exit') { + break + } + try { + $confirmUser = Test-UserFromHash -username $confirmationUser -debug + } catch { + Write-Warning "User specified $confirmationUser was not found within the Radius Server Membership Lists" + } } } + 'cli' { + $confirmUser = Test-UserFromHash -username $username -debug + } + } + if ($confirmUser) { + # Get the userobject + index from users.json + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id + # Add user to a list for processing + $UserSelectionArray = $userArray[$userIndex] + # Process existing commands/ Generate new commands/ Deploy new Certificate + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $result = Deploy-UserCertificate -userObject $UserSelectionArray -prompt + } + 'cli' { + $result = Deploy-UserCertificate -userObject $UserSelectionArray -forceInvokeCommands $invokeCommands + } + } + Show-RadiusProgress -completedItems $UserSelectionArray.count -totalItems $UserSelectionArray.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $result } - 'cli' { - $confirmUser = Test-UserFromHash -username $username -debug + # return after an action if cli, else stay in function + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-StatusMessage -Message "Finished Distributing Certificates" + } + 'cli' { + return + } } } - if ($confirmUser) { - # Get the userobject + index from users.json - $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id - # Add user to a list for processing - $UserSelectionArray = $userArray[$userIndex] - # Process existing commands/ Generate new commands/ Deploy new Certificate - $result = Deploy-UserCertificate -userObject $UserSelectionArray -invokeCommands $invokeCommands - Show-RadiusProgress -completedItems $UserSelectionArray.count -totalItems $UserSelectionArray.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $result - Show-StatusMessage -Message "Finished Distributing Certificates" - } } - } -} while ($confirmation -ne 'E') + } while ($confirmation -ne 'E') +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 b/scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 index ae41962b1..fd3570667 100644 --- a/scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 +++ b/scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 @@ -1,64 +1,82 @@ -# this script will generate a Self Signed CA (root cert) to be imported on the -# Radius CBA-BYO Authentication UI +Function Generate-RootCert { + [CmdletBinding(DefaultParameterSetName = 'gui')] + param ( + # Force invoke commands after generation + [Parameter(ParameterSetName = 'cli')] + [switch] + $forceReplcaeCert + ) + # this script will generate a Self Signed CA (root cert) to be imported on the + # Radius CBA-BYO Authentication UI -# Edit the variables in Config.ps1 before running this script -. "$JCScriptRoot/Config.ps1" + # Edit the variables in Config.ps1 before running this script + . "$JCScriptRoot/Config.ps1" -if ( ([System.String]::IsNullOrEmpty($JCORGID)) -Or ($JCORGID.Length -ne 24) ) { - throw "OrganizationID not specified, please update config.ps1" -} + if ( ([System.String]::IsNullOrEmpty($JCORGID)) -Or ($JCORGID.Length -ne 24) ) { + throw "OrganizationID not specified, please update config.ps1" + } -################################################################################ -# Do Not Edit Below: -################################################################################ -Set-Location $JCScriptRoot + ################################################################################ + # Do Not Edit Below: + ################################################################################ + Set-Location $JCScriptRoot -# REM Generate Root Server Private Key and server certificate (self signed as CA) -Write-Host "Generating Self Signed Root CA Certificate" -if (Test-Path -Path "$JCScriptRoot/Cert") { - Write-Host "Cert Path Exists" -} else { - Write-Host "Creating Cert Path" - New-Item -ItemType Directory -Path "$JCScriptRoot/Cert" -} -# Check if cert exists, prompt user to overwrite with a while loop -if (Test-Path -Path "$JCScriptRoot/Cert/radius_ca_cert.pem") { - Write-Host "CA Cert already exists" - $overwrite = Read-Host "Do you want to overwrite the existing CA Cert? (y/n)" - if ($overwrite -eq "y") { - Write-Host "Overwriting CA Cert..." + # REM Generate Root Server Private Key and server certificate (self signed as CA) + Write-Host "Generating Self Signed Root CA Certificate" + if (Test-Path -Path "$JCScriptRoot/Cert") { + Write-Host "Cert Path Exists" } else { - Write-Host "Exiting " - break + Write-Host "Creating Cert Path" + New-Item -ItemType Directory -Path "$JCScriptRoot/Cert" } -} -$CertPath = Resolve-Path "$JCScriptRoot/Cert" -$outKey = "$CertPath/radius_ca_key.pem" -$outCA = "$CertPath/radius_ca_cert.pem" -# Ask the user to enter a pass phrase for the CA key: -# Clear the pass phrase from the env: -$env:certKeyPassword = "" -$secureCertKeyPass = Read-Host -Prompt "Enter a password for the certificate key" -AsSecureString -$certKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText -# Save the pass phrase in the env: -$env:certKeyPassword = $certKeyPass -Invoke-Expression "$opensslBinary req -x509 -newkey rsa:2048 -days 365 -keyout $outKey -out $outCA -passout pass:$($env:certKeyPassword) -subj /C=$($Subj.countryCode)/ST=$($Subj.stateCode)/L=$($Subj.Locality)/O=$($Subj.Organization)/OU=$($Subj.OrganizationUnit)/CN=$($Subj.CommonName)" -# REM PEM pass phrase: myorgpass -Invoke-Expression "$opensslBinary x509 -in $outCA -noout -text" -# openssl x509 -in ca-cert.pem -noout -text -# Update Extensions Distinguished Names: -$exts = Get-ChildItem -Path "$JCScriptRoot/Extensions" -foreach ($ext in $exts) { - Write-Host "Updating Subject Headers for $($ext.Name)" - $extContent = Get-Content -Path $ext.FullName -Raw - $reqDistinguishedName = "[req_distinguished_name] -C = $($subj.countryCode) -ST = $($subj.stateCode) -L = $($subj.Locality) -O = $($subj.Organization) -OU = $($subj.OrganizationUnit) -CN = $($subj.CommonName) + # Check if cert exists, prompt user to overwrite with a while loop + if (Test-Path -Path "$JCScriptRoot/Cert/radius_ca_cert.pem") { + Write-Host "CA Cert already exists" + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $overwrite = Get-ResponsePrompt -message "Do you want to overwrite the existing CA Cert?" + switch ($overwrite) { + $true { + continue + } + $false { + return + } + } + } + 'cli' { -" - $extContent -Replace ("\[req_distinguished_name\][\s\S]*(?=\[v3_req\])", $reqDistinguishedName) | Set-Content -Path $ext.FullName -NoNewline -Force + } + } + } + $CertPath = Resolve-Path "$JCScriptRoot/Cert" + $outKey = "$CertPath/radius_ca_key.pem" + $outCA = "$CertPath/radius_ca_cert.pem" + # Ask the user to enter a pass phrase for the CA key: + # Clear the pass phrase from the env: + $env:certKeyPassword = "" + $secureCertKeyPass = Read-Host -Prompt "Enter a password for the certificate key" -AsSecureString + $certKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText + # Save the pass phrase in the env: + $env:certKeyPassword = $certKeyPass + Invoke-Expression "$opensslBinary req -x509 -newkey rsa:2048 -days 365 -keyout $outKey -out $outCA -passout pass:$($env:certKeyPassword) -subj /C=$($Subj.countryCode)/ST=$($Subj.stateCode)/L=$($Subj.Locality)/O=$($Subj.Organization)/OU=$($Subj.OrganizationUnit)/CN=$($Subj.CommonName)" + # REM PEM pass phrase: myorgpass + Invoke-Expression "$opensslBinary x509 -in $outCA -noout -text" + # openssl x509 -in ca-cert.pem -noout -text + # Update Extensions Distinguished Names: + $exts = Get-ChildItem -Path "$JCScriptRoot/Extensions" + foreach ($ext in $exts) { + Write-Host "Updating Subject Headers for $($ext.Name)" + $extContent = Get-Content -Path $ext.FullName -Raw + $reqDistinguishedName = "[req_distinguished_name] + C = $($subj.countryCode) + ST = $($subj.stateCode) + L = $($subj.Locality) + O = $($subj.Organization) + OU = $($subj.OrganizationUnit) + CN = $($subj.CommonName) + + " + $extContent -Replace ("\[req_distinguished_name\][\s\S]*(?=\[v3_req\])", $reqDistinguishedName) | Set-Content -Path $ext.FullName -NoNewline -Force + } } diff --git a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 index 517fc3b91..642c3625d 100644 --- a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 @@ -1,229 +1,196 @@ -# TODO: param for testing - -# Import Global Config: -# . "$JCScriptRoot/config.ps1" -# Connect-JCOnline $JCAPIKEY -force - -################################################################################ -# Do not modify below -################################################################################ -# Check if CA-Key is saved in env -if ($env:certKeyPassword) { - Write-Host "Found CA-Key password in env" - # Check if the key.pem works with the password - $foundKeyPem = Resolve-Path -Path "$JCScriptRoot/Cert/*key.pem" - $checkKey = openssl rsa -in $foundKeyPem -check -passin pass:$($env:certKeyPassword) 2>&1 - if ($checkKey -match "RSA key ok") { - Write-Debug "ENV CA-Key password is works with the current key" +# todo: rename to be PS-ApprovedVerb "New-UserCert" +function Generate-UserCerts { + [CmdletBinding(DefaultParameterSetName = 'gui')] + param ( + # Type of certs to distribute, All, New or byUsername + [Parameter(ParameterSetName = 'cli', Mandatory)] + [ValidateSet("All", "New", "ByUsername", "ExpiringSoon")] + [system.String] + $type, + # username + [Parameter(ParameterSetName = 'cli')] + [System.String] + $username, + # Force invoke commands after generation + [Parameter(ParameterSetName = 'cli')] + [switch] + $forceReplaceCerts + ) + #### begin function setup: + # Check if CA-Key is saved in env + if ($env:certKeyPassword) { + Write-Host "Found CA-Key password in env" + # Check if the key.pem works with the password + $foundKeyPem = Resolve-Path -Path "$JCScriptRoot/Cert/*key.pem" + $checkKey = openssl rsa -in $foundKeyPem -check -passin pass:$($env:certKeyPassword) 2>&1 + if ($checkKey -match "RSA key ok") { + Write-Debug "ENV CA-Key password is works with the current key" + } else { + Write-Host "CA-Key password is incorrect" + Get-CertKeyPass + } } else { - Write-Host "CA-Key password is incorrect" + # Get CA-Key password + Write-Host "CA-Key password not found in the ENV" Get-CertKeyPass } -} else { - # Get CA-Key password - Write-Host "CA-Key password not found in the ENV" - Get-CertKeyPass -} -# Import the functions -# Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force + # get userArray or initialize + $userArray = Get-UserJsonData -# Import User.Json/ create list if it does not exist -if (Test-Path -Path "$JCScriptRoot/users.json" -PathType Leaf) { - Write-Host "[status] Found user.json file" - $userArray = Get-Content -Raw -Path "$JCScriptRoot/users.json" | ConvertFrom-Json -Depth 6 - # If the json is a single item, explicitly make it a list so we can add to it - If ($userArray.count -eq 1) { - $array = New-Object System.Collections.ArrayList - $array.add($userArray) - $userArray = $array + # Create UserCerts dir + if (Test-Path "$JCScriptRoot/UserCerts") { + Write-Host "[status] User Cert Directory Exists" + } else { + Write-Host "[status] Creating User Cert Directory" + New-Item -ItemType Directory -Path "$JCScriptRoot/UserCerts" } + #### end function setup -} else { - Write-Host "[status] users.json file not found" - $userArray = @() -} - -# Create UserCerts dir -if (Test-Path "$JCScriptRoot/UserCerts") { - Write-Host "[status] User Cert Directory Exists" -} else { - Write-Host "[status] Creating User Cert Directory" - New-Item -ItemType Directory -Path "$JCScriptRoot/UserCerts" -} - -Do { - Show-GenerationMenu - $confirmation = Read-Host "Please make a selection" - - switch ($confirmation) { - '1' { - # process all users, generate certificates for uses who do not yet have a certificate - # Get List of files, figure out userList of users who've not had a cert generated: - $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts/" -Filter "*-client-signed.pfx" - $certFileList = New-Object System.Collections.ArrayList - foreach ($file in $userCertFiles) { - $userFromFile = $file.BaseName.Replace("-client-signed", "") - $certFileList.add($userFromFile) | Out-Null + Do { + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-GenerationMenu + $confirmation = Read-Host "Please make a selection" } - # Get each RadiusMember User: - foreach ($user in $Global:JCRRadiusMembers.keys) { - # Get the user details: - $MatchedUser = $GLOBAL:JCRUsers[$user] - # If the user has a certificate continue - if ($MatchedUser.username -in $certFileList) { - #if the cert already exists, break - Write-Host "[status] $($MatchedUser.username) has a certificate already. skipping..." - } else { - # if the user does not have a certificate, generate - Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null - # Get the user from the users.json file - $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $MatchedUser.id - # set user table: - if ($userIndex -ge 0) { - # update the new certificate info & set commandAssociation to $null - # TODO: commandAssociation not being set to null - $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username - # Add the cert info tracking to the object - $certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false - $certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value $null - Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null - } else { - # Create a new table entry - New-UserTable -id $MatchedUser.id -username $MatchedUser.username -localUsername $MatchedUser.systemUsername + 'cli' { + $confirmationMap = @{ + 'New' = '1'; + "ByUsername" = '2'; + 'All' = '3'; + "ExpiringSoon" = '4'; + } + $confirmation = $confirmationMap[$type] + # if force invoke is set, invoke the commands after generation: + switch ($forceReplaceCerts) { + $true { + $replcaeCerts = $true + } + $false { + $replcaeCerts = $false } } } - Break } - '2' { - try { - Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore - } catch { - New-Variable -Name "ConfirmUser" -Value $null - } - while (-not $confirmUser) { - $confirmationUser = Read-Host "Enter the Username of the user (or '@exit' to return to menu)" - if ($confirmationUser -eq '@exit') { - break + + switch ($confirmation) { + '1' { + # process all users, generate certificates for uses who do not yet have a certificate + # Get each RadiusMember User: + for ($i = 0; $i -lt $JCRRadiusMembers.count; $i++) { + $result = Invoke-UserCertProcess -radiusMember $JCRRadiusMembers[$i] -certType $CertType + Show-RadiusProgress -completedItems ($i + 1) -totalItems $JCRRadiusMembers.count -ActionText "Generating Radius Certificates" -previousOperationResult $result } - try { - $confirmUser = Test-UserFromHash -username $confirmationUser -debug - } catch { - Write-Warning "User specified $confirmationUser was not found within the Radius Server Membership Lists" + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-StatusMessage -Message "Finished Generating Certificates" + } + 'cli' { + return + } } } - # Generate a new cert for this user: - if (Test-Path -Path "$JCScriptRoot/UserCerts/$($confirmUser.username)-client-signed.pfx") { - Do { - $overwrite = Read-Host "do you want to overwrite y/n?" - switch ($overwrite) { - 'y' { - Generate-UserCert -CertType $CertType -user $confirmUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null - Break - + '2' { + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + try { + Clear-Variable -Name "ConfirmUser" -ErrorAction Ignore + } catch { + New-Variable -Name "ConfirmUser" -Value $null } - 'n' { - Write-Host "[status] $($confirmUser.username) already has certs generated... skipping" - Break - + while (-not $confirmUser) { + $confirmationUser = Read-Host "Enter the Username of the user (or '@exit' to return to menu)" + if ($confirmationUser -eq '@exit') { + break + } + try { + $confirmUser = Test-UserFromHash -username $confirmationUser -debug + } catch { + Write-Warning "User specified $confirmationUser was not found within the Radius Server Membership Lists" + } } } - } until (($overwrite -eq "y") -or ($overwrite -eq "n")) - } else { - # Generate a new cert for this user: - Generate-UserCert -CertType $CertType -user $confirmUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null - } - # Get the user from the users.json file - $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id - # set user table: - if ($userIndex -ge 0) { - Write-Warning "setting existing table for user $($confirmUser.username)" - # update the new certificate info & set commandAssociation to $null - # TODO: commandAssociation not being set to null - $certInfo = Get-CertInfo -UserCerts -username $confirmUser.username - # Add the cert info tracking to the object - $certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false - $certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value $null - Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null - } else { - Write-Warning "setting new table for user $($confirmUser.username)" - # Create a new table entry - New-UserTable -id $confirmUser.id -username $confirmUser.username -localUsername $confirmUser.systemUsername - } - # clear the user variable: - Clear-Variable "ConfirmUser" - Break - } - '3' { - # re-generate new certificates for ALL users - foreach ($user in $Global:JCRRadiusMembers.keys) { - # Get the user details: - $MatchedUser = $GLOBAL:JCRUsers[$user] - # Regenerate - Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null - # Get the user from the users.json file - $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $MatchedUser.id - # set user table: - if ($userIndex -ge 0) { - # update the new certificate info & set commandAssociation to $null - # TODO: commandAssociation not being set to null - $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username - $certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false - $certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value $null - Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null - } else { - # Create a new table entry - New-UserTable -id $MatchedUser.id -username $MatchedUser.username -localUsername $MatchedUser.systemUsername + 'cli' { + $confirmUser = Test-UserFromHash -username $username -debug + } + } + if ($confirmUser) { + # Get the userobject + index from users.json + $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType -prompt + Show-RadiusProgress -completedItems $userObject.count -totalItems $userObject.count -ActionText "Generating Radius Certificates" -previousOperationResult $result + } + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-StatusMessage -Message "Finished Generating Certificates" + } + 'cli' { + return + } } } - Break - } - '4' { - do { - $overwrite = Read-Host "do you want to overwrite yn?" - switch ($overwrite) { - 'y' { - foreach ($userCert in $Global:expiringCerts) { - $userArrayIndex = $userArray.username.IndexOf($userCert.username) - $IdentifiedUser = $userArray[$userArrayIndex] - $MatchedUser = $GLOBAL:JCRUsers[$IdentifiedUser.userid] - Generate-UserCert -CertType $CertType -user $MatchedUser -rootCAKey "$JCScriptRoot/Cert/radius_ca_key.pem" -rootCA "$JCScriptRoot/Cert/radius_ca_cert.pem" | Out-Null - # Get the user from the users.json file - $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $MatchedUser.id - # set user table: - if ($userIndex -ge 0) { - # update the new certificate info & set commandAssociation to $null - # TODO: commandAssociation not being set to null - $certInfo = Get-CertInfo -UserCerts -username $MatchedUser.username - $certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false - $certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value $null - Set-UserTable -index $userIndex -certInfoObject $certInfo -commandAssociationsObject $null - } else { - # Create a new table entry - New-UserTable -id $MatchedUser.id -username $MatchedUser.username -localUsername $MatchedUser.systemUsername + '3' { + # re-generate new certificates for ALL users; will force rewrite all certs + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $overwriteExistingCerts = Get-ResponsePrompt -message "Are you confident you want to replace all locally generated user certificates?" + switch ($overwriteExistingCerts) { + $true { + continue + } + $false { + return } } - Break } - 'n' { - Write-Host "[status] $($MatchedUser.username) already has certs generated... skipping" - Break + } + # Get each RadiusMember User: + for ($i = 0; $i -lt $JCRRadiusMembers.count; $i++) { + $result = Invoke-UserCertProcess -radiusMember $JCRRadiusMembers[$i] -certType $CertType -forceReplaceCert + Show-RadiusProgress -completedItems ($i + 1) -totalItems $JCRRadiusMembers.count -ActionText "Generating Radius Certificates" -previousOperationResult $result + } + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-StatusMessage -Message "Finished Generating Certificates" + } + 'cli' { + return + } + } + } + '4' { + # TODO: if there are no certs set to expire in 'x' days inform when pressing this option + for ($i = 0; $i -lt $ExpiringCerts.Count; $i++) { + $userCert = $ExpiringCerts[$i] + <# Action that will repeat until the condition is met #> + $userArrayIndex = $userArray.username.IndexOf($userCert.username) + $IdentifiedUser = $userArray[$userArrayIndex] + $result = Invoke-UserCertProcess -radiusMember $IdentifiedUser -certType $CertType -forceReplaceCert + Show-RadiusProgress -completedItems ($i + 1) -totalItems $ExpiringCerts.count -ActionText "Generating Radius Certificates" -previousOperationResult $result + # recalculate expiring certs: + $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $cutoffDate + } + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-StatusMessage -Message "Finished Generating Certificates" + } + 'cli' { + return } } - } until (($overwrite -eq "y") -or ($overwrite -eq "n")) - } - 'E' { - Write-Host "Returning to main menu" - } - default { - Write-Host "Invalid Choice. Please try again" - Break + } + 'E' { + Write-Host "Returning to main menu" + } + default { + Write-Host "Invalid Choice. Please try again" + } } - } -} while ($confirmation -ne 'E') + + } while ($confirmation -ne 'E') +} diff --git a/scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 b/scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 index 4405a427f..8ab95cec8 100644 --- a/scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 +++ b/scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 @@ -1,36 +1,29 @@ -# Import Global Config: -. "$JCScriptRoot/config.ps1" -Connect-JCOnline $JCAPIKEY -force +Function Monitor-CertDeployment { -################################################################################ -# Do not modify below -################################################################################ -# Import the functions -Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force -# Define jsonData file -$jsonFile = "$JCScriptRoot/users.json" -# Show user selection -do { - Show-CertDeploymentMenu - $option = Read-Host "Please make a selection" - switch ($option) { - '1' { - Get-CommandObjectTable -Detailed -jsonFile $jsonFile - Pause - } '2' { - Get-CommandObjectTable -Failed -jsonFile $jsonFile - Pause - } '3' { - $retryCommands = Invoke-CommandsRetry -jsonFile $jsonFile - Pause - } - } -} until ($option.ToUpper() -eq 'E') + ################################################################################ + # Do not modify below + ################################################################################ + # Import the functions + Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force + # Define jsonData file + $jsonFile = "$JCScriptRoot/users.json" -################################################################################ -# If needed you can clear out your command queue with the following commands. -# Copy and Paste these into a powershell terminal window to clear all queued -# commands in your org. -# . "scripts/automation/Radius/RadiusCertFunctions.ps1" -# Get-JCQueuedCommands | Foreach-Object { Clear-JCQueuedCommand -workflowId $_.id } + # Show user selection + do { + Show-CertDeploymentMenu + $option = Read-Host "Please make a selection" + switch ($option) { + '1' { + Get-CommandObjectTable -Detailed -jsonFile $jsonFile + Pause + } '2' { + Get-CommandObjectTable -Failed -jsonFile $jsonFile + Pause + } '3' { + $retryCommands = Invoke-CommandsRetry -jsonFile $jsonFile + Pause + } + } + } until ($option.ToUpper() -eq 'E') +} diff --git a/scripts/automation/Radius/Start-RadiusDeployment.ps1 b/scripts/automation/Radius/Start-RadiusDeployment.ps1 index 9c1883dd4..a27ee8821 100644 --- a/scripts/automation/Radius/Start-RadiusDeployment.ps1 +++ b/scripts/automation/Radius/Start-RadiusDeployment.ps1 @@ -22,15 +22,15 @@ do { $selection = Read-Host "Please make a selection" switch ($selection) { '1' { - . "$JCScriptRoot/Functions/Public/Generate-RootCert.ps1" + Generate-RootCert } '2' { - . "$JCScriptRoot/Functions/Public/Generate-UserCerts.ps1" + Generate-UserCerts } '3' { - . "$JCScriptRoot/Functions/Public/Distribute-UserCerts.ps1" + Distribute-UserCerts } '4' { - . "$JCScriptRoot/Functions/Public/Monitor-CertDeployment.ps1" + Monitor-CertDeployment } '5' { - . "$JCScriptRoot/Functions/Public/Update-JCRData.ps1" + Get-JCRGlobalVars -force } } Pause diff --git a/scripts/automation/Radius/Tests/Invoke-Pester.ps1 b/scripts/automation/Radius/Tests/Invoke-Pester.ps1 new file mode 100644 index 000000000..eefc23f2a --- /dev/null +++ b/scripts/automation/Radius/Tests/Invoke-Pester.ps1 @@ -0,0 +1,124 @@ +# InvokePester.ps1 is intended to be called directly as a file-function +# There are two parameter sets + +Param( + [Parameter(ParameterSetName = 'SingleOrgTests', Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0)][ValidateNotNullOrEmpty()][System.String]$JumpCloudApiKey + , [Parameter(ParameterSetName = 'SingleOrgTests', Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 2)][System.String[]]$ExcludeTagList + , [Parameter(ParameterSetName = 'SingleOrgTests', Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 3)][System.String[]]$IncludeTagList +) + +# Get list of tags and validate that tags have been applied +$PesterTests = Get-ChildItem -Path:($PSScriptRoot + '/*.Tests.ps1') -Recurse +$Tags = ForEach ($PesterTest In $PesterTests) { + $PesterTestFullName = $PesterTest.FullName + $FileContent = Get-Content -Path:($PesterTestFullName) + $DescribeLines = $FileContent | Select-String -Pattern:([RegEx]'(Describe)')#.Matches.Value + ForEach ($DescribeLine In $DescribeLines) { + If ($DescribeLine.Line -match 'Tag') { + $TagParameterValue = ($DescribeLine.Line | Select-String -Pattern:([RegEx]'(?<=-Tag)(.*?)(?=\s)')).Matches.Value + @(":", "(", ")", "'") | ForEach-Object { If ($TagParameterValue -like ('*' + $_ + '*')) { + $TagParameterValue = $TagParameterValue.Replace($_, '') + } } + $TagParameterValue + } Else { + Write-Error ('Tag missing in "' + $PesterTestFullName + '" on line number "' + $DescribeLine.LineNumber + '" value "' + ($DescribeLine.Line).Trim() + '"') + } + } +} +# Filters on tags +$IncludeTags = If ($IncludeTagList) { + $IncludeTagList +} Else { + $Tags | Where-Object { $_ -notin $ExcludeTags } | Select-Object -Unique +} +# locally, clear pester run paths if it exists before run: +If ($PesterRunPaths) { + Clear-Variable -Name PesterRunPaths +} +# Determine the parameter set path + +if ($env:CI) { + If ($env:job_group) { + # split tests by job group: + $PesterTestsPaths = Get-ChildItem -Path $PSScriptRoot -Filter *.Tests.ps1 -Recurse | Where-Object size -GT 0 | Sort-Object -Property Name + Write-Host "[Status] $($PesterTestsPaths.count) tests found" + $CIindex = @() + $numItems = $($PesterTestsPaths.count) + $numBuckets = 3 + $itemsPerBucket = [math]::Floor(($numItems / $numBuckets)) + $remainder = ($numItems % $numBuckets) + $extra = 0 + for ($i = 0; $i -lt $numBuckets; $i++) { + <# Action that will repeat until the condition is met #> + if ($i -eq ($numBuckets - 1)) { + $extra = $remainder + } + $indexList = ($itemsPerBucket + $extra) + # Write-Host "Container $i contains $indexList items:" + $CIIndexList = @() + $CIIndexList += for ($k = 0; $k -lt $indexList; $k++) { + <# Action that will repeat until the condition is met #> + $bucketIndex = $i * $itemsPerBucket + # write-host "`$tags[$($bucketIndex + $k)] ="$tags[($bucketIndex + $k)] + $PesterTestsPaths[$bucketIndex + $k] + } + # add to ciIndex Array + $CIindex += , ($CIIndexList) + } + $PesterRunPaths = $CIindex[[int]$($env:job_group)] + Write-Host "[status] The following $($($CIindex[[int]$($env:job_group)]).count) tests will be run:" + $($CIindex[[int]$($env:job_group)]) | ForEach-Object { Write-Host "$_" } + } +} else { + # run setup org locally and set variables +} +$env:JCAPIKEY = $JumpCloudApiKey +Connect-JCOnline -JumpCloudApiKey:($env:JCAPIKEY) -force + +if (-Not $PesterRunPaths) { + $PesterRunPaths = @( + "$PSScriptRoot" + ) +} +# Load private functions +Write-Host ('[status]Load private functions: ' + "$PSScriptRoot/../Functions/Private/*.ps1") +Write-Host ('[status]Load public functions: ' + "$PSScriptRoot/../Functions/Public/*.ps1") +Get-ChildItem -Path:("$PSScriptRoot/../Functions/Private/*.ps1") -Recurse | ForEach-Object { . $_.FullName } + + +# Set the test result directory: +$PesterResultsFileXmldir = "$PSScriptRoot/test_results/" +# create the directory if it does not exist: +if (-not (Test-Path $PesterResultsFileXmldir)) { + New-Item -Path $PesterResultsFileXmldir -ItemType Directory +} + +# define pester configuration +$configuration = New-PesterConfiguration +$configuration.Run.Path = $PesterRunPaths +$configuration.Should.ErrorAction = 'Continue' +$configuration.CodeCoverage.Enabled = $true +$configuration.testresult.Enabled = $true +$configuration.testresult.OutputFormat = 'JUnitXml' +$configuration.Filter.Tag = $IncludeTags +$configuration.Filter.ExcludeTag = $ExcludeTagList +$configuration.CodeCoverage.OutputPath = ($PesterResultsFileXmldir + 'coverage.xml') +$configuration.testresult.OutputPath = ($PesterResultsFileXmldir + 'results.xml') + +Write-Host ("[RUN COMMAND] Invoke-Pester -Path:('$PesterRunPaths') -TagFilter:('$($IncludeTags -join "','")') -ExcludeTagFilter:('$($ExcludeTagList -join "','")') -PassThru") -BackgroundColor:('Black') -ForegroundColor:('Magenta') +# Run Pester tests +Invoke-Pester -Configuration $configuration + +$PesterTestResultPath = (Get-ChildItem -Path:("$($PesterResultsFileXmldir)")).FullName | Where-Object { $_ -match "results.xml" } +If (Test-Path -Path:($PesterTestResultPath)) { + [xml]$PesterResults = Get-Content -Path:($PesterTestResultPath) + If ($PesterResults.ChildNodes.failures -gt 0) { + Write-Error ("Test Failures: $($PesterResults.ChildNodes.failures)") + } + If ($PesterResults.ChildNodes.errors -gt 0) { + Write-Error ("Test Errors: $($PesterResults.ChildNodes.errors)") + } +} Else { + Write-Error ("Unable to find file path: $PesterTestResultPath") +} +Write-Host -ForegroundColor Green '-------------Done-------------' \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 new file mode 100644 index 000000000..4b87b2b6e --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 @@ -0,0 +1,9 @@ +Describe 'distribute tests' { + context 'certs for all users are generated' { + it 'generates certs for all users' { + . "/Users/jworkman/Documents/GitHub/support/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1" + Distribute-UserCerts -generateType all + # validate that the commands were created for valid users + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/readme.md b/scripts/automation/Radius/readme.md index daa41fd0c..9ae179a61 100644 --- a/scripts/automation/Radius/readme.md +++ b/scripts/automation/Radius/readme.md @@ -77,7 +77,7 @@ Change the variable `$JCORGID` to the [organization ID value](https://support.ju #### Set Your User Group ID -Change the variable `$JCUSERGROUP` to the ID of the JumpCloud user group with access to the Radius server. To get the ID of a user group, navigate to the user group within the JumpCloud Administrator Console. +Change the variable `$global:JCUSERGROUP` to the ID of the JumpCloud user group with access to the Radius server. To get the ID of a user group, navigate to the user group within the JumpCloud Administrator Console. After selecting the User Group, view the url for the user group it should look similar to this url: `https://console.jumpcloud.com/#/groups/user/5f808a1bb544064831f7c9fb/details` @@ -182,7 +182,7 @@ After successful import or generation of a self signed CA, the CA's serial numbe ### User Cert Generation -With the certificate authority generated/ imported, individual user certs can then be generated. The ID of the user group stored as the variable: `$JCUSERGROUP` is used to store JumpCloud users destined for passwordless Radius access. For each user in the group, a `.pfx` certificate will be generated in the `/projectDirectory/Radius/UserCerts/` directory. The user certificates are stored locally and monitored for expiration. +With the certificate authority generated/ imported, individual user certs can then be generated. The ID of the user group stored as the variable: `$global:JCUSERGROUP` is used to store JumpCloud users destined for passwordless Radius access. For each user in the group, a `.pfx` certificate will be generated in the `/projectDirectory/Radius/UserCerts/` directory. The user certificates are stored locally and monitored for expiration. If local user certificates are set to expire within 15 days, a notification is displayed on the main menu: From afa5673a787aca94a5de21c0b514810179915a49 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Fri, 5 Jan 2024 14:27:35 -0700 Subject: [PATCH 013/346] move private functions/ create module --- .../Deploy-UserCertificate.ps1 | 0 .../{ => CertDeployment}/Get-CertInfo.ps1 | 0 .../{ => CertDeployment}/Get-CertKeyPass.ps1 | 0 .../Get-ExpiringCertInfo.ps1 | 0 .../Generate-UserCert.ps1 | 0 .../Invoke-UserCertProcess.ps1 | 0 .../Get-CommandObjectTable.ps1 | 0 .../Functions/Private/Get-GroupMembership.ps1 | 31 ---- .../Functions/Private/Get-WebJCUser.ps1 | 33 ----- .../Functions/Private/Invoke-CommandRun.ps1 | 31 ---- .../{ => commands}/Clear-JCQueuedCommand.ps1 | 0 .../{ => commands}/Get-JCQueuedCommands.ps1 | 0 .../{ => commands}/Invoke-CommandsRetry.ps1 | 0 .../{ => commands}/New-JCCommandFile.ps1 | 0 .../Private/{ => menus}/PadCenter.ps1 | 0 ...serCerts.ps1 => Start-DeployUserCerts.ps1} | 2 +- ...ootCert.ps1 => Start-GenerateRootCert.ps1} | 2 +- ...rCerts.ps1 => Start-GenerateUserCerts.ps1} | 2 +- ...nt.ps1 => Start-MonitorCertDeployment.ps1} | 4 +- .../automation/Radius/JumpCloud-Radius.psd1 | 133 ++++++++++++++++++ ...tDeployment.psm1 => JumpCloud-Radius.psm1} | 6 +- .../Radius/Start-RadiusDeployment.ps1 | 10 +- .../automation/Radius/Tests/Invoke-Pester.ps1 | 1 - .../Public/Distribute-UserCerts.Tests.ps1 | 25 +++- .../Tests/Public/Generate-UserCerts.Tests.ps1 | 32 +++++ .../Radius/Tests/Setup-JCRadiusOrg.ps1 | 0 26 files changed, 196 insertions(+), 116 deletions(-) rename scripts/automation/Radius/Functions/Private/{ => CertDeployment}/Deploy-UserCertificate.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => CertDeployment}/Get-CertInfo.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => CertDeployment}/Get-CertKeyPass.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => CertDeployment}/Get-ExpiringCertInfo.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => CertGeneration}/Generate-UserCert.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => CertGeneration}/Invoke-UserCertProcess.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => CertResults}/Get-CommandObjectTable.ps1 (100%) delete mode 100644 scripts/automation/Radius/Functions/Private/Get-GroupMembership.ps1 delete mode 100644 scripts/automation/Radius/Functions/Private/Get-WebJCUser.ps1 delete mode 100644 scripts/automation/Radius/Functions/Private/Invoke-CommandRun.ps1 rename scripts/automation/Radius/Functions/Private/{ => commands}/Clear-JCQueuedCommand.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => commands}/Get-JCQueuedCommands.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => commands}/Invoke-CommandsRetry.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => commands}/New-JCCommandFile.ps1 (100%) rename scripts/automation/Radius/Functions/Private/{ => menus}/PadCenter.ps1 (100%) rename scripts/automation/Radius/Functions/Public/{Distribute-UserCerts.ps1 => Start-DeployUserCerts.ps1} (99%) rename scripts/automation/Radius/Functions/Public/{Generate-RootCert.ps1 => Start-GenerateRootCert.ps1} (99%) rename scripts/automation/Radius/Functions/Public/{Generate-UserCerts.ps1 => Start-GenerateUserCerts.ps1} (99%) rename scripts/automation/Radius/Functions/Public/{Monitor-CertDeployment.ps1 => Start-MonitorCertDeployment.ps1} (85%) create mode 100644 scripts/automation/Radius/JumpCloud-Radius.psd1 rename scripts/automation/Radius/{Functions/JCRadiusCertDeployment.psm1 => JumpCloud-Radius.psm1} (78%) create mode 100644 scripts/automation/Radius/Tests/Public/Generate-UserCerts.Tests.ps1 create mode 100644 scripts/automation/Radius/Tests/Setup-JCRadiusOrg.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Deploy-UserCertificate.ps1 rename to scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertInfo.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 rename to scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertInfo.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Get-CertKeyPass.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertKeyPass.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Get-CertKeyPass.ps1 rename to scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertKeyPass.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Get-ExpiringCertInfo.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-ExpiringCertInfo.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Get-ExpiringCertInfo.ps1 rename to scripts/automation/Radius/Functions/Private/CertDeployment/Get-ExpiringCertInfo.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Generate-UserCert.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Generate-UserCert.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Generate-UserCert.ps1 rename to scripts/automation/Radius/Functions/Private/CertGeneration/Generate-UserCert.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Invoke-UserCertProcess.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Invoke-UserCertProcess.ps1 rename to scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Get-CommandObjectTable.ps1 b/scripts/automation/Radius/Functions/Private/CertResults/Get-CommandObjectTable.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Get-CommandObjectTable.ps1 rename to scripts/automation/Radius/Functions/Private/CertResults/Get-CommandObjectTable.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Get-GroupMembership.ps1 b/scripts/automation/Radius/Functions/Private/Get-GroupMembership.ps1 deleted file mode 100644 index 64ef1cdb0..000000000 --- a/scripts/automation/Radius/Functions/Private/Get-GroupMembership.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -function get-GroupMembership { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [system.string] - $groupID - ) - begin { - $skip = 0 - $limit = 100 - $headers = @{ - "x-api-key" = $JCAPIKEY - "x-org-id" = $JCORGID - } - $paginate = $true - $list = @() - } - process { - while ($paginate) { - $response = Invoke-RestMethod -Uri "https://console.jumpcloud.com/api/v2/usergroups/$groupID/membership?limit=$limit&skip=$skip" -Method GET -Headers $headers - $list += $response - $skip += $limit - if ($response.count -lt $limit) { - $paginate = $false - } - } - } - end { - return $list - } -} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Get-WebJCUser.ps1 b/scripts/automation/Radius/Functions/Private/Get-WebJCUser.ps1 deleted file mode 100644 index 06928e950..000000000 --- a/scripts/automation/Radius/Functions/Private/Get-WebJCUser.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -function get-webjcuser { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [system.string] - $userID - ) - begin { - $headers = @{ - "x-api-key" = $JCAPIKEY - "x-org-id" = $JCORGID - } - } - process { - $response = Invoke-RestMethod -Uri "https://console.jumpcloud.com/api/systemusers/$userID" -Method GET -Headers $headers - $userObj = [PSCustomObject]@{ - # If the localUserAccount field is set, use that for username, otherwise use JC username - hasLocalUsername = $(if ([string]::IsNullOrEmpty($response.systemUsername)) { - $false - } else { - $true - }) - username = $response.username - localUsername = $response.systemUsername - - id = $response._id - email = $response.email - } - } - end { - return $userObj - } -} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Invoke-CommandRun.ps1 b/scripts/automation/Radius/Functions/Private/Invoke-CommandRun.ps1 deleted file mode 100644 index 4bfe0ae96..000000000 --- a/scripts/automation/Radius/Functions/Private/Invoke-CommandRun.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -function Invoke-CommandRun { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [System.String] - $commandID - ) - begin { - if ($commandID.length -ne 24) { - throw "Supplied CommandID is not of the correct length" - } - } - process { - $headers = @{ - 'x-api-key' = $Env:JCApiKey - 'x-org-id' = $Env:JCOrgId - "content-type" = "application/json" - } - $body = @{ - _id = $commandID - } | ConvertTo-Json - $response = Invoke-RestMethod -Uri 'https://console.jumpcloud.com/api/runCommand' -Method POST -Headers $headers -ContentType 'application/json' -Body $body -UserAgent $UserAgent - } - end { - if (!$response.queueIds) { - Throw "Command with ID: $commandID could not be triggered" - } - - } - -} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Clear-JCQueuedCommand.ps1 b/scripts/automation/Radius/Functions/Private/commands/Clear-JCQueuedCommand.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Clear-JCQueuedCommand.ps1 rename to scripts/automation/Radius/Functions/Private/commands/Clear-JCQueuedCommand.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Get-JCQueuedCommands.ps1 b/scripts/automation/Radius/Functions/Private/commands/Get-JCQueuedCommands.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Get-JCQueuedCommands.ps1 rename to scripts/automation/Radius/Functions/Private/commands/Get-JCQueuedCommands.ps1 diff --git a/scripts/automation/Radius/Functions/Private/Invoke-CommandsRetry.ps1 b/scripts/automation/Radius/Functions/Private/commands/Invoke-CommandsRetry.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/Invoke-CommandsRetry.ps1 rename to scripts/automation/Radius/Functions/Private/commands/Invoke-CommandsRetry.ps1 diff --git a/scripts/automation/Radius/Functions/Private/New-JCCommandFile.ps1 b/scripts/automation/Radius/Functions/Private/commands/New-JCCommandFile.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/New-JCCommandFile.ps1 rename to scripts/automation/Radius/Functions/Private/commands/New-JCCommandFile.ps1 diff --git a/scripts/automation/Radius/Functions/Private/PadCenter.ps1 b/scripts/automation/Radius/Functions/Private/menus/PadCenter.ps1 similarity index 100% rename from scripts/automation/Radius/Functions/Private/PadCenter.ps1 rename to scripts/automation/Radius/Functions/Private/menus/PadCenter.ps1 diff --git a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 similarity index 99% rename from scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 rename to scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 index c8db039bd..27da419a7 100644 --- a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 @@ -1,5 +1,5 @@ -function Distribute-UserCerts { +function Start-DeployUserCerts { [CmdletBinding(DefaultParameterSetName = 'gui')] param ( # Type of certs to distribute, All, New or byUsername diff --git a/scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 b/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 similarity index 99% rename from scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 rename to scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 index fd3570667..49e6e4bc4 100644 --- a/scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 @@ -1,4 +1,4 @@ -Function Generate-RootCert { +Function Start-GenerateRootCert { [CmdletBinding(DefaultParameterSetName = 'gui')] param ( # Force invoke commands after generation diff --git a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 similarity index 99% rename from scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 rename to scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 index 642c3625d..59d1ee490 100644 --- a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 @@ -1,5 +1,5 @@ # todo: rename to be PS-ApprovedVerb "New-UserCert" -function Generate-UserCerts { +function Start-GenerateUserCerts { [CmdletBinding(DefaultParameterSetName = 'gui')] param ( # Type of certs to distribute, All, New or byUsername diff --git a/scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 b/scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 similarity index 85% rename from scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 rename to scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 index 8ab95cec8..5a666d413 100644 --- a/scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 @@ -1,11 +1,11 @@ -Function Monitor-CertDeployment { +Function Start-CertDeploymentMonitoring { ################################################################################ # Do not modify below ################################################################################ # Import the functions - Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force + # Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force # Define jsonData file $jsonFile = "$JCScriptRoot/users.json" diff --git a/scripts/automation/Radius/JumpCloud-Radius.psd1 b/scripts/automation/Radius/JumpCloud-Radius.psd1 new file mode 100644 index 000000000..70274b1bd --- /dev/null +++ b/scripts/automation/Radius/JumpCloud-Radius.psd1 @@ -0,0 +1,133 @@ +# +# Module manifest for module 'JumpCloud-Radius' +# +# Generated by: jworkman +# +# Generated on: 1/5/2024 +# + +@{ + +# Script module or binary module file associated with this manifest. +# RootModule = '' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '71bfaf58-3326-4512-9a7f-a2d9dc19d6b5' + +# Author of this module +Author = 'jworkman' + +# Company or vendor of this module +CompanyName = 'Unknown' + +# Copyright statement for this module +Copyright = '(c) jworkman. All rights reserved.' + +# Description of the functionality provided by this module +# Description = '' + +# Minimum version of the PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = 'Start-DeployUserCerts', 'Start-GenerateRootCert', + 'Start-GenerateUserCerts', 'Start-MonitorCertDeployment' + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 b/scripts/automation/Radius/JumpCloud-Radius.psm1 similarity index 78% rename from scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 rename to scripts/automation/Radius/JumpCloud-Radius.psm1 index 8432ab694..c5207aa47 100644 --- a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 +++ b/scripts/automation/Radius/JumpCloud-Radius.psm1 @@ -1,5 +1,5 @@ # Load all functions from private folders -$Private = @( Get-ChildItem -Path "$PSScriptRoot/Private/*.ps1" -Recurse) +$Private = @( Get-ChildItem -Path "$PSScriptRoot/Functions/Private/*.ps1" -Recurse) Foreach ($Import in $Private) { Try { . $Import.FullName @@ -9,7 +9,7 @@ Foreach ($Import in $Private) { } # Load all public functions: -$Private = @( Get-ChildItem -Path "$PSScriptRoot/Public/*.ps1" -Recurse) +$Private = @( Get-ChildItem -Path "$PSScriptRoot/Functions/Public/*.ps1" -Recurse) Foreach ($Import in $Private) { Try { . $Import.FullName @@ -21,7 +21,7 @@ Foreach ($Import in $Private) { # setup: # build required users.json file: # set script root: -$global:JCScriptRoot = "$PSScriptRoot/../" +$global:JCScriptRoot = "$PSScriptRoot" # import config: . "$JCScriptRoot/config.ps1" diff --git a/scripts/automation/Radius/Start-RadiusDeployment.ps1 b/scripts/automation/Radius/Start-RadiusDeployment.ps1 index a27ee8821..55b2626d1 100644 --- a/scripts/automation/Radius/Start-RadiusDeployment.ps1 +++ b/scripts/automation/Radius/Start-RadiusDeployment.ps1 @@ -13,7 +13,7 @@ if ($JCAPIKEY.length -ne 40) { $global:JCScriptRoot = $PSScriptRoot # Import the functions -Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force +Import-Module "$JCScriptRoot/JumpCloud-Radius.psm1" -DisableNameChecking -Force # Show user selection do { @@ -22,13 +22,13 @@ do { $selection = Read-Host "Please make a selection" switch ($selection) { '1' { - Generate-RootCert + Start-GenerateRootCert } '2' { - Generate-UserCerts + Start-GenerateUserCerts } '3' { - Distribute-UserCerts + Start-DeployUserCerts } '4' { - Monitor-CertDeployment + Start-MonitorCertDeployment } '5' { Get-JCRGlobalVars -force } diff --git a/scripts/automation/Radius/Tests/Invoke-Pester.ps1 b/scripts/automation/Radius/Tests/Invoke-Pester.ps1 index eefc23f2a..41b6036f5 100644 --- a/scripts/automation/Radius/Tests/Invoke-Pester.ps1 +++ b/scripts/automation/Radius/Tests/Invoke-Pester.ps1 @@ -1,6 +1,5 @@ # InvokePester.ps1 is intended to be called directly as a file-function # There are two parameter sets - Param( [Parameter(ParameterSetName = 'SingleOrgTests', Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 0)][ValidateNotNullOrEmpty()][System.String]$JumpCloudApiKey , [Parameter(ParameterSetName = 'SingleOrgTests', Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 2)][System.String[]]$ExcludeTagList diff --git a/scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 index 4b87b2b6e..ad6c1ba55 100644 --- a/scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 @@ -1,9 +1,20 @@ -Describe 'distribute tests' { - context 'certs for all users are generated' { - it 'generates certs for all users' { - . "/Users/jworkman/Documents/GitHub/support/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1" - Distribute-UserCerts -generateType all - # validate that the commands were created for valid users - } +Describe 'Distribute User Cert Tests' -Tag 'Distribute' { + Context 'Distribute all certificates for all users forcibly' { + + } + Context 'Distribute all certificates for all users without invoking' { + + } + Context 'Distribute new certificates for new users forcibly' { + + } + Context 'Distribute new certificates for new users without invoking' { + + } + Context 'Distribute new certificates for a single user forcibly' { + + } + Context 'Distribute new certificates for a single user without invoking' { + } } \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/Public/Generate-UserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Generate-UserCerts.Tests.ps1 new file mode 100644 index 000000000..8a3d6231b --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Generate-UserCerts.Tests.ps1 @@ -0,0 +1,32 @@ +Describe 'Generate User Cert Tests' -Tag "Generate" { + Context 'Certs forcibly re-generated for all users' { + It 'Certs re-generated have actually been re-written for all users' { + . "/Users/jworkman/Documents/GitHub/support/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1" + $timeBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') + $certsBefore = Get-CertInfo -UserCerts + # wait one second. + Start-Sleep 1 + Generate-UserCerts -type All -forceReplaceCerts + # validate that the commands were created for valid users + # Get Certs + $certs = Get-CertInfo -UserCerts + foreach ($cert in $certs) { + $matchingBeforeCert = $certsBefore | Where-Object { $username -eq $cert.username } + # Each cert should have a generated date -gt the $timeBefore + $cert.generated | should -BeGreaterThan $timeBefore + $cert.generated | should -BeGreaterThan $matchingBeforeCert.generated + $cert.serial | should -Not -Be $matchingBeforeCert.serial + $cert.sha1 | should -Not -Be $matchingBeforeCert.sha1 + } + } + } + Context 'Certs generated for newly added users' { + + } + Context 'Certs generated for users whos cert is set to exipre soon' { + + } + Context 'Certs generated by username' { + + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/Setup-JCRadiusOrg.ps1 b/scripts/automation/Radius/Tests/Setup-JCRadiusOrg.ps1 new file mode 100644 index 000000000..e69de29bb From ecf489ddb545d76e8b71d0432116d428d4dfb6d3 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Mon, 8 Jan 2024 11:11:30 -0700 Subject: [PATCH 014/346] cleanup module --- scripts/automation/Radius/Changelog.md | 17 ++ .../CertGeneration/Invoke-UserCertProcess.ps1 | 1 - .../Public/Start-GenerateUserCerts.ps1 | 9 +- .../Public/Start-MonitorCertDeployment.ps1 | 2 +- .../automation/Radius/JumpCloud-Radius.psd1 | 160 +++++++++--------- .../automation/Radius/JumpCloud-Radius.psm1 | 5 +- .../Radius/Start-RadiusDeployment.ps1 | 3 +- ...ts.ps1 => Start-DeployUserCerts.Tests.ps1} | 0 ....ps1 => Start-GenerateUserCerts.Tests.ps1} | 1 + 9 files changed, 112 insertions(+), 86 deletions(-) rename scripts/automation/Radius/Tests/Public/{Distribute-UserCerts.Tests.ps1 => Start-DeployUserCerts.Tests.ps1} (100%) rename scripts/automation/Radius/Tests/Public/{Generate-UserCerts.Tests.ps1 => Start-GenerateUserCerts.Tests.ps1} (98%) diff --git a/scripts/automation/Radius/Changelog.md b/scripts/automation/Radius/Changelog.md index 99182cdb4..1447b7e98 100644 --- a/scripts/automation/Radius/Changelog.md +++ b/scripts/automation/Radius/Changelog.md @@ -1,3 +1,20 @@ +## 2.0.0 + +Release Date: January 5, 2024 + +#### RELEASE NOTES + +``` +This release offers a significant overhaul for the Radius Cert Deployment tool, many new underlying functions have been introducted to reduce the number of required API calls. Most notably, the tool will cache data from an organization on load. +``` + +#### Features: + +- Added an option to both generate and deploy radius certificates by username +- Association data is cached up front rather than gathered throughout the script, offering performance improvements for organizations with a large number of radius users +- Added ability to run each public function headless in order to automate cert generation and distribution +- Added an table to keep track of generated/ deployed certificates when using the tool + ## 1.1.0 Release Date: December 13, 2023 diff --git a/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 index f5c3c3a81..f40465976 100644 --- a/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 @@ -53,7 +53,6 @@ Function Invoke-UserCertProcess { } } if ($prompt) { - $writeCert = Get-ResponsePrompt -message "A certifcate already exists for user: $($matchedUser.username) do you want to re-generate this certificate?" switch ($writeCert) { $true { diff --git a/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 index 59d1ee490..3b7eeb6ac 100644 --- a/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 @@ -117,7 +117,14 @@ function Start-GenerateUserCerts { if ($confirmUser) { # Get the userobject + index from users.json $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id - $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType -prompt + switch ($forceReplaceCerts) { + $true { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType -forceReplaceCert + } + $false { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType + } + } Show-RadiusProgress -completedItems $userObject.count -totalItems $userObject.count -ActionText "Generating Radius Certificates" -previousOperationResult $result } switch ($PSCmdlet.ParameterSetName) { diff --git a/scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 b/scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 index 5a666d413..bdd0cc1f8 100644 --- a/scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 @@ -1,4 +1,4 @@ -Function Start-CertDeploymentMonitoring { +Function Start-MonitorCertDeployment { ################################################################################ diff --git a/scripts/automation/Radius/JumpCloud-Radius.psd1 b/scripts/automation/Radius/JumpCloud-Radius.psd1 index 70274b1bd..991a773ee 100644 --- a/scripts/automation/Radius/JumpCloud-Radius.psd1 +++ b/scripts/automation/Radius/JumpCloud-Radius.psd1 @@ -8,126 +8,126 @@ @{ -# Script module or binary module file associated with this manifest. -# RootModule = '' + # Script module or binary module file associated with this manifest. + RootModule = 'JumpCloud-Radius.psm1' -# Version number of this module. -ModuleVersion = '0.0.1' + # Version number of this module. + ModuleVersion = '2.0.0' -# Supported PSEditions -# CompatiblePSEditions = @() + # Supported PSEditions + # CompatiblePSEditions = @() -# ID used to uniquely identify this module -GUID = '71bfaf58-3326-4512-9a7f-a2d9dc19d6b5' + # ID used to uniquely identify this module + GUID = '71bfaf58-3326-4512-9a7f-a2d9dc19d6b5' -# Author of this module -Author = 'jworkman' + # Author of this module + Author = 'jworkman' -# Company or vendor of this module -CompanyName = 'Unknown' + # Company or vendor of this module + CompanyName = 'Unknown' -# Copyright statement for this module -Copyright = '(c) jworkman. All rights reserved.' + # Copyright statement for this module + Copyright = '(c) jworkman. All rights reserved.' -# Description of the functionality provided by this module -# Description = '' + # Description of the functionality provided by this module + # Description = '' -# Minimum version of the PowerShell engine required by this module -# PowerShellVersion = '' + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '7.0.0' -# Name of the PowerShell host required by this module -# PowerShellHostName = '' + # Name of the PowerShell host required by this module + # PowerShellHostName = '' -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# ClrVersion = '' + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' -# Modules that must be imported into the global environment prior to importing this module -# RequiredModules = @() + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @('JumpCloud') -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Start-DeployUserCerts', 'Start-GenerateRootCert', - 'Start-GenerateUserCerts', 'Start-MonitorCertDeployment' + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = 'Start-DeployUserCerts', 'Start-GenerateRootCert', + 'Start-GenerateUserCerts', 'Start-MonitorCertDeployment' -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = @() + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() -# Variables to export from this module -VariablesToExport = '*' + # Variables to export from this module + VariablesToExport = '*' -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = @() + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() -# DSC resources to export from this module -# DscResourcesToExport = @() + # DSC resources to export from this module + # DscResourcesToExport = @() -# List of all modules packaged with this module -# ModuleList = @() + # List of all modules packaged with this module + # ModuleList = @() -# List of all files packaged with this module -# FileList = @() + # List of all files packaged with this module + # FileList = @() -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ - PSData = @{ + PSData = @{ - # Tags applied to this module. These help with module discovery in online galleries. - # Tags = @() + # Tags applied to this module. These help with module discovery in online galleries. + # Tags = @() - # A URL to the license for this module. - # LicenseUri = '' + # A URL to the license for this module. + # LicenseUri = '' - # A URL to the main website for this project. - # ProjectUri = '' + # A URL to the main website for this project. + # ProjectUri = '' - # A URL to an icon representing this module. - # IconUri = '' + # A URL to an icon representing this module. + # IconUri = '' - # ReleaseNotes of this module - # ReleaseNotes = '' + # ReleaseNotes of this module + # ReleaseNotes = '' - # Prerelease string of this module - # Prerelease = '' + # Prerelease string of this module + # Prerelease = '' - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false - # External dependent modules of this module - # ExternalModuleDependencies = @() + # External dependent modules of this module + # ExternalModuleDependencies = @() - } # End of PSData hashtable + } # End of PSData hashtable -} # End of PrivateData hashtable + } # End of PrivateData hashtable -# HelpInfo URI of this module -# HelpInfoURI = '' + # HelpInfo URI of this module + # HelpInfoURI = '' -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' } diff --git a/scripts/automation/Radius/JumpCloud-Radius.psm1 b/scripts/automation/Radius/JumpCloud-Radius.psm1 index c5207aa47..d85a128fe 100644 --- a/scripts/automation/Radius/JumpCloud-Radius.psm1 +++ b/scripts/automation/Radius/JumpCloud-Radius.psm1 @@ -9,9 +9,10 @@ Foreach ($Import in $Private) { } # Load all public functions: -$Private = @( Get-ChildItem -Path "$PSScriptRoot/Functions/Public/*.ps1" -Recurse) -Foreach ($Import in $Private) { +$Public = @( Get-ChildItem -Path "$PSScriptRoot/Functions/Public/*.ps1" -Recurse) +Foreach ($Import in $Public) { Try { + write-host "import function $($Import.FullName): $_" . $Import.FullName } Catch { Write-Error -Message "Failed to import function $($Import.FullName): $_" diff --git a/scripts/automation/Radius/Start-RadiusDeployment.ps1 b/scripts/automation/Radius/Start-RadiusDeployment.ps1 index 55b2626d1..861d3e1d3 100644 --- a/scripts/automation/Radius/Start-RadiusDeployment.ps1 +++ b/scripts/automation/Radius/Start-RadiusDeployment.ps1 @@ -13,7 +13,7 @@ if ($JCAPIKEY.length -ne 40) { $global:JCScriptRoot = $PSScriptRoot # Import the functions -Import-Module "$JCScriptRoot/JumpCloud-Radius.psm1" -DisableNameChecking -Force +Import-Module "$JCScriptRoot/JumpCloud-Radius.psd1" -DisableNameChecking -Force # Show user selection do { @@ -30,6 +30,7 @@ do { } '4' { Start-MonitorCertDeployment } '5' { + #TODO: move as a public function Get-JCRGlobalVars -force } } diff --git a/scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 similarity index 100% rename from scripts/automation/Radius/Tests/Public/Distribute-UserCerts.Tests.ps1 rename to scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 diff --git a/scripts/automation/Radius/Tests/Public/Generate-UserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 similarity index 98% rename from scripts/automation/Radius/Tests/Public/Generate-UserCerts.Tests.ps1 rename to scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 index 8a3d6231b..e82d301b8 100644 --- a/scripts/automation/Radius/Tests/Public/Generate-UserCerts.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 @@ -24,6 +24,7 @@ Describe 'Generate User Cert Tests' -Tag "Generate" { } Context 'Certs generated for users whos cert is set to exipre soon' { + # Set config } Context 'Certs generated by username' { From 26e1111c20ac469e9ff49ccad044caa374b792f6 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Tue, 9 Jan 2024 14:06:29 -0700 Subject: [PATCH 015/346] global var + root ca tests --- scripts/automation/Radius/Config.ps1 | 10 +- .../settings => Public}/Get-JCRGlobalVars.ps1 | 9 +- .../Public/Start-GenerateRootCert.ps1 | 35 +++-- .../automation/Radius/JumpCloud-Radius.psd1 | 2 +- .../Radius/Start-RadiusDeployment.ps1 | 3 +- .../Tests/Public/Get-JCRGlobalVars.Tests.ps1 | 129 ++++++++++++++++++ .../Public/Start-GenerateRootCert.Tests.ps1 | 60 ++++++++ 7 files changed, 228 insertions(+), 20 deletions(-) rename scripts/automation/Radius/Functions/{Private/settings => Public}/Get-JCRGlobalVars.ps1 (94%) create mode 100644 scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 create mode 100644 scripts/automation/Radius/Tests/Public/Start-GenerateRootCert.Tests.ps1 diff --git a/scripts/automation/Radius/Config.ps1 b/scripts/automation/Radius/Config.ps1 index 4697de9cc..c5b7994d7 100644 --- a/scripts/automation/Radius/Config.ps1 +++ b/scripts/automation/Radius/Config.ps1 @@ -1,9 +1,9 @@ # JUMPCLOUD USER GROUP ID $Global:JCUSERGROUP = 'YOURJCUSERGROUP' # USER CERT PASSWORD (this password is sent to the devices via JumpCloud Commands) -$JCUSERCERTPASS = 'secret1234!' +$Global:JCUSERCERTPASS = 'secret1234!' # USER CERT Validity Length (days) -$JCUSERCERTVALIDITY = 90 +$Global:JCUSERCERTVALIDITY = 90 # List Of Radius Network SSID(s) # For Multiple SSIDs enter as a single string seperated by a semicolon ex: # "CorpNetwork_Denver;CorpNetwork_Boulder;CorpNetwork_Boulder 5G;Guest Network" @@ -11,9 +11,9 @@ $Global:NETWORKSSID = "YOUR_SSID" # OpenSSLBinary by default this is (openssl) # NOTE: If openssl does not work, try using the full path to the openssl file # MacOS HomeBrew Example: '/usr/local/Cellar/openssl@3/3.1.1/bin/openssl' -$opensslBinary = 'openssl' +$Global:opensslBinary = 'openssl' # Enter Cert Subject Headers (do not enter strings with spaces) -$Subj = [PSCustomObject]@{ +$Global:Subj = [PSCustomObject]@{ countryCode = "US" stateCode = "CO" Locality = "Boulder" @@ -27,7 +27,7 @@ $Subj = [PSCustomObject]@{ # EmailSAN # EmailDN # UsernameCn (Default) -$CertType = "UsernameCn" +$Global:CertType = "UsernameCn" ################################################################################ # Do not modify below diff --git a/scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 similarity index 94% rename from scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 rename to scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 index 384606b74..0142c2da0 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Get-JCRGlobalVars.ps1 +++ b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 @@ -28,7 +28,14 @@ function Get-JCRGlobalVars { # also validate that the data files are non-null, if they are, force update] $requiredHashFiles = @('associationHash.json', 'radiusMembers.json', 'systemHash.json', 'userHash.json') foreach ($file in $requiredHashFiles) { - $fileContents = Get-Content "$JCScriptRoot/data/$file" + if (Test-Path -Path "$JCScriptRoot/data/$file") { + $fileContents = Get-Content "$JCScriptRoot/data/$file" + } else { + Write-Host "[status] $JCScriptRoot/data/$file file does not exist, updating global variables" + $update = $true + break + } + # if the file is null force update if ([string]::IsNullOrEmpty($file)) { Write-Host "[status] $JCScriptRoot/data/$file file is null, updating global variables" $update = $true diff --git a/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 b/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 index 49e6e4bc4..e6b3ad580 100644 --- a/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 @@ -1,6 +1,11 @@ Function Start-GenerateRootCert { [CmdletBinding(DefaultParameterSetName = 'gui')] param ( + # Cert Key Password + # Parameter help description + [Parameter(ParameterSetName = 'cli')] + [string] + $certKeyPassword, # Force invoke commands after generation [Parameter(ParameterSetName = 'cli')] [switch] @@ -54,9 +59,16 @@ Function Start-GenerateRootCert { $outCA = "$CertPath/radius_ca_cert.pem" # Ask the user to enter a pass phrase for the CA key: # Clear the pass phrase from the env: - $env:certKeyPassword = "" - $secureCertKeyPass = Read-Host -Prompt "Enter a password for the certificate key" -AsSecureString - $certKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $env:certKeyPassword = "" + $secureCertKeyPass = Read-Host -Prompt "Enter a password for the certificate key" -AsSecureString + $certKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText + } + 'cli' { + $certKeyPass = $certKeyPassword + } + } # Save the pass phrase in the env: $env:certKeyPassword = $certKeyPass Invoke-Expression "$opensslBinary req -x509 -newkey rsa:2048 -days 365 -keyout $outKey -out $outCA -passout pass:$($env:certKeyPassword) -subj /C=$($Subj.countryCode)/ST=$($Subj.stateCode)/L=$($Subj.Locality)/O=$($Subj.Organization)/OU=$($Subj.OrganizationUnit)/CN=$($Subj.CommonName)" @@ -68,15 +80,16 @@ Function Start-GenerateRootCert { foreach ($ext in $exts) { Write-Host "Updating Subject Headers for $($ext.Name)" $extContent = Get-Content -Path $ext.FullName -Raw - $reqDistinguishedName = "[req_distinguished_name] - C = $($subj.countryCode) - ST = $($subj.stateCode) - L = $($subj.Locality) - O = $($subj.Organization) - OU = $($subj.OrganizationUnit) - CN = $($subj.CommonName) + $reqDistinguishedName = @" +[req_distinguished_name] +C = $($subj.countryCode) +ST = $($subj.stateCode) +L = $($subj.Locality) +O = $($subj.Organization) +OU = $($subj.OrganizationUnit) +CN = $($subj.CommonName) - " +"@ $extContent -Replace ("\[req_distinguished_name\][\s\S]*(?=\[v3_req\])", $reqDistinguishedName) | Set-Content -Path $ext.FullName -NoNewline -Force } } diff --git a/scripts/automation/Radius/JumpCloud-Radius.psd1 b/scripts/automation/Radius/JumpCloud-Radius.psd1 index 991a773ee..04f150a17 100644 --- a/scripts/automation/Radius/JumpCloud-Radius.psd1 +++ b/scripts/automation/Radius/JumpCloud-Radius.psd1 @@ -70,7 +70,7 @@ # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. FunctionsToExport = 'Start-DeployUserCerts', 'Start-GenerateRootCert', - 'Start-GenerateUserCerts', 'Start-MonitorCertDeployment' + 'Start-GenerateUserCerts', 'Start-MonitorCertDeployment', 'Get-JCRGlobalVars' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() diff --git a/scripts/automation/Radius/Start-RadiusDeployment.ps1 b/scripts/automation/Radius/Start-RadiusDeployment.ps1 index 861d3e1d3..55b2626d1 100644 --- a/scripts/automation/Radius/Start-RadiusDeployment.ps1 +++ b/scripts/automation/Radius/Start-RadiusDeployment.ps1 @@ -13,7 +13,7 @@ if ($JCAPIKEY.length -ne 40) { $global:JCScriptRoot = $PSScriptRoot # Import the functions -Import-Module "$JCScriptRoot/JumpCloud-Radius.psd1" -DisableNameChecking -Force +Import-Module "$JCScriptRoot/JumpCloud-Radius.psm1" -DisableNameChecking -Force # Show user selection do { @@ -30,7 +30,6 @@ do { } '4' { Start-MonitorCertDeployment } '5' { - #TODO: move as a public function Get-JCRGlobalVars -force } } diff --git a/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 new file mode 100644 index 000000000..58c0cf6b4 --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 @@ -0,0 +1,129 @@ +Describe "Get Global Variable Data Tests" { + BeforeAll { + $dataPath = "$JCScriptRoot/data/" + $requiredFiles = @( + 'associationHash.json', + 'radiusMembers.json', + 'systemHash.json', + 'userHash.json' + ) + + } + Context "When no 'data' directory exists" { + BeforeAll { + if (Test-Path -Path ($dataPath)) { + Remove-Item -Path $dataPath -Recurse + } + } + It "The tool is able to generate a 'data' directory and populate the global variables" { + # Get the latest data for the org: + Get-JCRGlobalVars + foreach ($file in $requiredFiles) { + # each json hash should have been generated and exist + "$dataPath/$file" | Should -Exist + # each file should be non-null + $fileContent = Get-Content -Path "$dataPath/$file" + $fileContent | Should -Not -BeNullOrEmpty + } + } + } + Context "When the 'data' directory already exists" { + BeforeAll { + # if the data directory does not exist, generate the global vars + directory + if (-not (Test-Path -Path ($dataPath))) { + Get-JCRGlobalVars + } + # explicitly import the settings file functions for these tests: + $Private = @( Get-ChildItem -Path "$JCScriptRoot/Functions/Private/settings/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + } + It "Data should not be refreshed when running Get-JCRGlobalVars if it's been less than 24 hours since last update" { + # Get the settings data + $settingsData = Get-JCRSettingsFile + $timespan = New-TimeSpan -Start (Get-Date).AddHours(-24) -End $settingsData.globalVars.lastupdate + # Write-Host "Time between 24 hrs and settings file: $($timespan.TotalHours)" + # get files before + $filesBefore = Get-ChildItem -Path $dataPath + # check the settings time the files were last written + if ($timespan.TotalHours -lt 24) { + # continue with test + } else { + # set the settings file to a mocked value of now + Set-JCRSettingsFile -globalVarslastUpdate (Get-Date) + + } + # run Get-JCRGlobalVars + Get-JCRGlobalVars + + # check the files: + $filesAfter = Get-ChildItem -Path $dataPath + + # test each file write date + foreach ($file in $requiredFiles) { + # write-host "validating write times for $file" + $beforeWriteTime = (($filesBefore | Where-Object { $_.Name -eq $file })).LastWriteTime + $afterWriteTime = (($filesAfter | Where-Object { $_.Name -eq $file })).LastWriteTime + # Write-Host "before: $beforeWriteTime should be after: $afterWriteTime" + # The file write time before running Get-JCRGlobalVars should be the same after running the function + $beforeWriteTime | should -be $afterWriteTime + } + } + It "Data should refresh when running Get-JCRGlobalVars if it's been more than 24 hours since last update" { + # Get the settings data + $settingsData = Get-JCRSettingsFile + $timespan = New-TimeSpan -Start (Get-Date).AddHours(-24) -End $settingsData.globalVars.lastupdate + # Write-Host "Time between 24 hrs and settings file: $($timespan.TotalHours)" + # get files before + $filesBefore = Get-ChildItem -Path $dataPath + # check the settings time the files were last written + if ($timespan.TotalHours -gt 24) { + # continue with test + } else { + # set the settings file to a mocked value of now + Set-JCRSettingsFile -globalVarslastUpdate (Get-Date).AddHours(-25) + Start-Sleep 1 + $settingsData = Get-JCRSettingsFile + $timespan = New-TimeSpan -Start (Get-Date).AddHours(-24) -End $settingsData.globalVars.lastupdate + # Write-Host "Time between 24 hrs and settings file: $($timespan.TotalHours)" + } + # run Get-JCRGlobalVars + Get-JCRGlobalVars + + # check the files: + $filesAfter = Get-ChildItem -Path $dataPath + # test each file write date + foreach ($file in $requiredFiles) { + # write-host "validating write times for $file" + $beforeWriteTime = (($filesBefore | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks + $afterWriteTime = (($filesAfter | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks + # Write-Host "before: $beforeWriteTime should not be after: $afterWriteTime" + # The file write time before running Get-JCRGlobalVars should not be the same after running the function + $beforeWriteTime | should -Not -Be $afterWriteTime + } + } + It "Data should be re-written when the -Force parameter is used; regardless of setings write date" { + # check the files before + $filesBefore = Get-ChildItem -Path $dataPath + # run Get-JCRGlobalVars with force param + Get-JCRGlobalVars -Force + + # check the files after: + $filesAfter = Get-ChildItem -Path $dataPath + # test each file write date + foreach ($file in $requiredFiles) { + # write-host "validating write times for $file" + $beforeWriteTime = (($filesBefore | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks + $afterWriteTime = (($filesAfter | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks + # Write-Host "before: $beforeWriteTime should not be after: $afterWriteTime" + # The file write time before running Get-JCRGlobalVars should not be the same after running the function + $beforeWriteTime | should -Not -Be $afterWriteTime + } + } + } +} diff --git a/scripts/automation/Radius/Tests/Public/Start-GenerateRootCert.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-GenerateRootCert.Tests.ps1 new file mode 100644 index 000000000..e758514bd --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Start-GenerateRootCert.Tests.ps1 @@ -0,0 +1,60 @@ +Describe "Generate Root Certifcate Tests" { + Context "A new certificate can be generated" -skip { + # If the /Cert/ folder is not empty, clear the directory + $items = Get-ChildItem $JCScriptRoot/Cert + if ($items) { + foreach ($item in $items) { + write-host "removing $($item.FullName)" + Remove-Item -Path $item.FullName + } + } + Start-GenerateRootCert -certKeyPassword "Test123" + + # both the key and the cert should be generated + $itemsAfter = Get-ChildItem $JCScriptRoot/Cert + $itemsAfter.BaseName | Should -Contain "radius_ca_cert" + $itemsAfter.BaseName | Should -Contain "radius_ca_key" + + #validate the subject matches what's defined in config: + $CA_subject = Invoke-Expression "$opensslBinary x509 -noout -in $JCScriptRoot/Cert/radius_ca_cert.pem -subject" + $CA_subject = $CA_subject.split("subject=").split(",") + ($CA_subject | Where-Object { $_ -match "C = " }) | Should -Match $Global:Subj.countryCode + ($CA_subject | Where-Object { $_ -match "ST = " }) | Should -Match $Global:Subj.stateCode + ($CA_subject | Where-Object { $_ -match "L = " }) | Should -Match $Global:Subj.Locality + ($CA_subject | Where-Object { $_ -match "O = " }) | Should -Match $Global:Subj.Organization + ($CA_subject | Where-Object { $_ -match "OU = " }) | Should -Match $Global:Subj.OrganizationUnit + ($CA_subject | Where-Object { $_ -match "CN = " }) | Should -Match $Global:Subj.CommonName + } + Context "An existing certificate can be replaced" { + # get existing cert serial: + $origSN = Invoke-Expression "$opensslBinary x509 -noout -in $JCScriptRoot/Cert/radius_ca_cert.pem -serial" + # force generate new CA + Start-GenerateRootCert -forceReplcaeCert -certKeyPassword "newTest1234" + # get new SN + $newSN = Invoke-Expression "$opensslBinary x509 -noout -in $JCScriptRoot/Cert/radius_ca_cert.pem -serial" + # the serial numbers of the cert should not be the same, i.e. a new cert has replaced the existing one + $origSN | Should -Not -Be $newSN + # both the key and the cert should be generated + $itemsAfter = Get-ChildItem $JCScriptRoot/Cert + $itemsAfter.BaseName | Should -Contain "radius_ca_cert" + $itemsAfter.BaseName | Should -Contain "radius_ca_key" + + #validate the subject matches what's defined in config: + $CA_subject = Invoke-Expression "$opensslBinary x509 -noout -in $JCScriptRoot/Cert/radius_ca_cert.pem -subject" + $CA_subject = $CA_subject.split("subject=").split(",") + ($CA_subject | Where-Object { $_ -match "C = " }) | Should -Match $Global:Subj.countryCode + ($CA_subject | Where-Object { $_ -match "ST = " }) | Should -Match $Global:Subj.stateCode + ($CA_subject | Where-Object { $_ -match "L = " }) | Should -Match $Global:Subj.Locality + ($CA_subject | Where-Object { $_ -match "O = " }) | Should -Match $Global:Subj.Organization + ($CA_subject | Where-Object { $_ -match "OU = " }) | Should -Match $Global:Subj.OrganizationUnit + ($CA_subject | Where-Object { $_ -match "CN = " }) | Should -Match $Global:Subj.CommonName + + } + Context "An existing certificate can be renewed" { + # TODO: implement + # openssl req -new -key radius_ca_key.key -out newcsr.csr + # openssl x509 -req -days 365 -in newcsr.csr -signkey radius_ca_key.key -out radius_ca_cert.pem + # rm newcsr.csr + + } +} \ No newline at end of file From 1b20110b33d9860bc8893588fb204a51ee530cfb Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Tue, 9 Jan 2024 14:56:29 -0700 Subject: [PATCH 016/346] add ability to skip associationData in globalVars --- .../Functions/Public/Get-JCRGlobalVars.ps1 | 121 ++++++++++++------ .../Tests/Public/Get-JCRGlobalVars.Tests.ps1 | 28 ++++ 2 files changed, 109 insertions(+), 40 deletions(-) diff --git a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 index 0142c2da0..11e07f95c 100644 --- a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 +++ b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 @@ -3,7 +3,10 @@ function Get-JCRGlobalVars { param ( [Parameter()] [switch] - $force + $force, + [Parameter()] + [switch] + $skipAssociation ) begin { # ensure the data directory exists to cache the json files: @@ -23,23 +26,59 @@ function Get-JCRGlobalVars { } if ($force) { $update = $true + switch ($skipAssociation) { + $true { + $updateAssociation = $false + } + $false { + $updateAssociation = $true + } + } } # also validate that the data files are non-null, if they are, force update] - $requiredHashFiles = @('associationHash.json', 'radiusMembers.json', 'systemHash.json', 'userHash.json') + $requiredHashFiles = @('radiusMembers.json', 'systemHash.json', 'userHash.json') foreach ($file in $requiredHashFiles) { if (Test-Path -Path "$JCScriptRoot/data/$file") { $fileContents = Get-Content "$JCScriptRoot/data/$file" } else { Write-Host "[status] $JCScriptRoot/data/$file file does not exist, updating global variables" $update = $true - break } # if the file is null force update - if ([string]::IsNullOrEmpty($file)) { + if ([string]::IsNullOrEmpty($fileContents)) { + Write-Host "[status] $JCScriptRoot/data/$file file is null, updating global variables" + $update = $true + } + } + + $requiredAssociationHashFiles = @('associationHash.json') + foreach ($file in $requiredAssociationHashFiles) { + if (Test-Path -Path "$JCScriptRoot/data/$file") { + $fileContents = Get-Content "$JCScriptRoot/data/$file" + switch ($skipAssociation) { + $true { + Write-Host "[status] $JCScriptRoot/data/$file will be skipped" + $updateAssociation = $false + } + } + } else { + Write-Host "[status] $JCScriptRoot/data/$file file does not exist, updating global variables" + $update = $true + $updateAssociation = $true + } + # if the file is null force update + if ([string]::IsNullOrEmpty($fileContents)) { Write-Host "[status] $JCScriptRoot/data/$file file is null, updating global variables" $update = $true - break + $updateAssociation = $true + } else { + switch ($skipAssociation) { + $true { + Write-Host "[status] $JCScriptRoot/data/$file will be skipped" + $updateAssociation = $false + } + } } } } @@ -60,50 +99,52 @@ function Get-JCRGlobalVars { 'userID' = $member.toID 'username' = $users[$member.toID].username } - ) - } - # Get Report Hash: - $headers = @{ - "accept" = "application/json"; - "x-api-key" = $Env:JCApiKey; - "x-org-id" = $Env:JCOrgId + ) | Out-Null } - # request new user to device report: - $reportRequest = Invoke-RestMethod -Uri 'https://api.jumpcloud.com/insights/directory/v1/reports/users-to-devices' -Method POST -Headers $headers - # now fetch available reports: - do { - $reportList = Invoke-RestMethod -Uri 'https://api.jumpcloud.com/insights/directory/v1/reports?sort=CREATED_AT' -Method GET -Headers $headers - $lastReport = $reportList | Where-Object { $_.id -eq $reportRequest.id } - if ($lastReport.status -eq 'PENDING') { - Write-Warning "[status] waiting 20s for jumpcloud report to complete" - start-sleep -Seconds 20 + if ($updateAssociation) { + # Get Report Hash: + $headers = @{ + "accept" = "application/json"; + "x-api-key" = $Env:JCApiKey; + "x-org-id" = $Env:JCOrgId } - } until ($lastReport.status -eq 'COMPLETED') - # download json - $artifactID = ($lastReport.artifacts | Where-Object { $_.format -eq 'json' }).id - $reportID = $lastReport.id - $reportContent = Invoke-RestMethod -Uri "https://api.jumpcloud.com/insights/directory/v1/reports/$reportID/artifacts/$artifactID/content" -Method GET -Headers $headers - # create the hashtable: - $userAssociationList = New-Object System.Collections.Hashtable - foreach ($item in $reportContent) { - if ($item.user_object_id -And $item.resource_object_id) { - if (-not $userAssociationList[$item.user_object_id]) { - $userAssociationList.add( - $item.user_object_id, @{ - 'systemAssociations' = @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, @{Name = 'osFamily'; Expression = { $_.device_os } }); - 'userData' = @($item | Select-Object -Property email, username) - }) - } else { - $userAssociationList[$item.user_object_id].systemAssociations += @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, @{Name = 'osFamily'; Expression = { $_.device_os } }) + # request new user to device report: + $reportRequest = Invoke-RestMethod -Uri 'https://api.jumpcloud.com/insights/directory/v1/reports/users-to-devices' -Method POST -Headers $headers + # now fetch available reports: + do { + $reportList = Invoke-RestMethod -Uri 'https://api.jumpcloud.com/insights/directory/v1/reports?sort=CREATED_AT' -Method GET -Headers $headers + $lastReport = $reportList | Where-Object { $_.id -eq $reportRequest.id } + if ($lastReport.status -eq 'PENDING') { + Write-Warning "[status] waiting 20s for jumpcloud report to complete" + start-sleep -Seconds 20 + } + } until ($lastReport.status -eq 'COMPLETED') + # download json + $artifactID = ($lastReport.artifacts | Where-Object { $_.format -eq 'json' }).id + $reportID = $lastReport.id + $reportContent = Invoke-RestMethod -Uri "https://api.jumpcloud.com/insights/directory/v1/reports/$reportID/artifacts/$artifactID/content" -Method GET -Headers $headers + # create the hashtable: + $userAssociationList = New-Object System.Collections.Hashtable + foreach ($item in $reportContent) { + if ($item.user_object_id -And $item.resource_object_id) { + if (-not $userAssociationList[$item.user_object_id]) { + $userAssociationList.add( + $item.user_object_id, @{ + 'systemAssociations' = @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, @{Name = 'osFamily'; Expression = { $_.device_os } }); + 'userData' = @($item | Select-Object -Property email, username) + }) | Out-Null + } else { + $userAssociationList[$item.user_object_id].systemAssociations += @($item | Select-Object -Property @{Name = 'systemId'; Expression = { $_.resource_object_id } }, hostname, @{Name = 'osFamily'; Expression = { $_.device_os } }) + } } } + # write out the association hash + $userAssociationList | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/associationHash.json" } # finally write out the data to file: - Write-host "writing files" $users | ConvertTo-Json -Depth 100 -Compress | Out-File "$JCScriptRoot/data/userHash.json" $systems | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/systemHash.json" - $userAssociationList | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/associationHash.json" $radiusMemberList | ConvertTo-Json | Out-File "$JCScriptRoot/data/radiusMembers.json" } $false { diff --git a/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 index 58c0cf6b4..f4e7ebfb1 100644 --- a/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 @@ -125,5 +125,33 @@ Describe "Get Global Variable Data Tests" { $beforeWriteTime | should -Not -Be $afterWriteTime } } + It "Data should be re-written when the -Force parameter is used and Association data should be skipped with -SkipAssociation; regardless of setings write date" { + # check the files before + $filesBefore = Get-ChildItem -Path $dataPath + # run Get-JCRGlobalVars with force param + Get-JCRGlobalVars -Force -SkipAssociation + + # check the files after: + $filesAfter = Get-ChildItem -Path $dataPath + # test each file write date + foreach ($file in $requiredFiles) { + # write-host "validating write times for $file" + if ($file -ne "associationHash.json") { + $beforeWriteTime = (($filesBefore | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks + $afterWriteTime = (($filesAfter | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks + # Write-Host "before: $beforeWriteTime should not be after: $afterWriteTime" + # The file write time before running Get-JCRGlobalVars should not be the same after running the function + $beforeWriteTime | should -Not -Be $afterWriteTime + + } else { + $beforeWriteTime = (($filesBefore | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks + $afterWriteTime = (($filesAfter | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks + # Write-Host "before: $beforeWriteTime should not be after: $afterWriteTime" + # The file write time before running Get-JCRGlobalVars should not be the same after running the function + $beforeWriteTime | should -Be $afterWriteTime + + } + } + } } } From f79ccc4f08de1a57c0688c84bd8aa379b2263e6a Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Wed, 10 Jan 2024 08:26:57 -0700 Subject: [PATCH 017/346] individual user tests --- .../HashFunctions/Test-UserFromHash.ps1 | 2 +- .../Functions/Public/Get-JCRGlobalVars.ps1 | 7 ++- .../Public/Start-GenerateUserCerts.Tests.ps1 | 45 ++++++++++++++++++- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 b/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 index 17e56c615..c8e28d5a3 100644 --- a/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 +++ b/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 @@ -32,7 +32,7 @@ function Test-UserFromHash { # Get the index of the user within the hashtable $matchedIndex = $Global:JCRUsers.values.username.ToLower().IndexOf($username.ToLower()) # Get the UserID from the keys - $matchedUserID = $Global:JCRUsers.keys[$matchedIndex] + $matchedUserID = $Global:JCRUsers.keys | Select-Object -Index $matchedIndex # validate that the userID is in the radiusMembership hash: if ($Global:JCRRadiusMembers.username.IndexOf($username)) { # finally return the $matchedUser object diff --git a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 index 11e07f95c..23bdaa731 100644 --- a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 +++ b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 @@ -140,6 +140,9 @@ function Get-JCRGlobalVars { } # write out the association hash $userAssociationList | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/associationHash.json" + } else { + $userAssociationList = Get-Content -Raw -Path "$JCScriptRoot/data/associationHash.json" | ConvertFrom-Json -Depth 6 -AsHashtable + } # finally write out the data to file: @@ -148,7 +151,7 @@ function Get-JCRGlobalVars { $radiusMemberList | ConvertTo-Json | Out-File "$JCScriptRoot/data/radiusMembers.json" } $false { - Write-Warning "It's been $($lastUpdateTimespan.hours) hours since we last pulled user, system and association data, no need to update" + # write-host "It's been $($lastUpdateTimespan.hours) hours since we last pulled user, system and association data, no need to update" $userAssociationList = Get-Content -Raw -Path "$JCScriptRoot/data/associationHash.json" | ConvertFrom-Json -Depth 6 -AsHashtable } } @@ -166,7 +169,7 @@ function Get-JCRGlobalVars { Set-JCRSettingsFile -globalVarslastUpdate (Get-Date) } $false { - Write-Warning "pulling saved data from data file:" + # write-host "pulling saved data from data file:" # set global vars from local cache $Global:JCRUsers = Get-Content -path "$JCScriptRoot/data/userHash.json" | ConvertFrom-Json -AsHashtable $Global:JCRSystems = Get-Content -path "$JCScriptRoot/data/systemHash.json" | ConvertFrom-Json -AsHashtable diff --git a/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 index e82d301b8..0c8b6d304 100644 --- a/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 @@ -1,12 +1,12 @@ Describe 'Generate User Cert Tests' -Tag "Generate" { - Context 'Certs forcibly re-generated for all users' { + Context 'Certs forcibly re-generated for all users' -skip { It 'Certs re-generated have actually been re-written for all users' { . "/Users/jworkman/Documents/GitHub/support/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1" $timeBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') $certsBefore = Get-CertInfo -UserCerts # wait one second. Start-Sleep 1 - Generate-UserCerts -type All -forceReplaceCerts + Start-GenerateUserCerts -type All -forceReplaceCerts # validate that the commands were created for valid users # Get Certs $certs = Get-CertInfo -UserCerts @@ -21,6 +21,47 @@ Describe 'Generate User Cert Tests' -Tag "Generate" { } } Context 'Certs generated for newly added users' { + beforeall { + # Select a user TODO: create a pester user + $user = Get-JCuser -username "chet.atkins" + $certs = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -filter "$($user.username)*" + foreach ($cert in $certs) { + remove-item $cert.fullname + } + + } + It 'When a new user is added to the radius group, the tool will generate a new cert' { + # Get the certs before + $certsBefore = Get-ChildItem -Path "$JCScriptRoot/UserCerts" + # add the new user to the radius group + Add-JCUserGroupMember -GroupID $Global:JCUSERGROUP -UserID $user.id + # update the cache + Get-JCRGlobalVars -force -skipAssociation + # wait just one moment before testing membership since we are writing a file + Start-Sleep 1 + # the new user should be in the membership list: + $global:JCRRadiusMembers.username | Should -Contain $user.username + # Generate the user cert: + Start-GenerateUserCerts -type ByUsername -username $($user.username) -forceReplaceCerts + # Get the certs after + $certsAfter = Get-ChildItem -Path "$JCScriptRoot/UserCerts" + # filter by username + $UserCerts = $certsAfter | Where-Object { $_.Name -match "$($user.username)" } + # the files and each type of expected cert file should exist + $UserCerts.Name | Should -Match $user.username + $UserCerts.fullname | where-object { $_ -match ".csr" } | Should -exist + $UserCerts.fullname | where-object { $_ -match ".pfx" } | Should -exist + $UserCerts.fullname | where-object { $_ -match ".crt" } | Should -exist + $UserCerts.fullname | where-object { $_ -match ".key" } | Should -exist + # cleanup + Remove-JCUserGroupMember -GroupID $Global:JCUSERGROUP -UserID $user.id + # update cache + Get-JCRGlobalVars -force -skipAssociation + # wait just one moment before testing membership since we are writing a file + Start-Sleep 1 + # the global variables should be cleaned up + $global:JCRRadiusMembers.username | Should -Not -Contain $user.username + } } Context 'Certs generated for users whos cert is set to exipre soon' { From 8c9ab9703cabba1d70d079cf3b9cb81b3e716a2f Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Wed, 10 Jan 2024 11:11:14 -0700 Subject: [PATCH 018/346] test for users who's cert is about to expire --- .../Public/Start-GenerateUserCerts.ps1 | 6 ++ .../Public/Start-GenerateUserCerts.Tests.ps1 | 68 ++++++++++++++++++- 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 index 3b7eeb6ac..40b72d173 100644 --- a/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 @@ -168,6 +168,10 @@ function Start-GenerateUserCerts { } '4' { # TODO: if there are no certs set to expire in 'x' days inform when pressing this option + # recalculate expiring certs: + $userCertInfo = Get-CertInfo -UserCerts + $global:cutoffDate = (Get-Date).AddDays(15).Date + $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $cutoffDate for ($i = 0; $i -lt $ExpiringCerts.Count; $i++) { $userCert = $ExpiringCerts[$i] <# Action that will repeat until the condition is met #> @@ -177,6 +181,8 @@ function Start-GenerateUserCerts { Show-RadiusProgress -completedItems ($i + 1) -totalItems $ExpiringCerts.count -ActionText "Generating Radius Certificates" -previousOperationResult $result # recalculate expiring certs: + $userCertInfo = Get-CertInfo -UserCerts + $global:cutoffDate = (Get-Date).AddDays(15).Date $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $cutoffDate } switch ($PSCmdlet.ParameterSetName) { diff --git a/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 index 0c8b6d304..fd1bbb7a3 100644 --- a/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 @@ -1,5 +1,5 @@ Describe 'Generate User Cert Tests' -Tag "Generate" { - Context 'Certs forcibly re-generated for all users' -skip { + Context 'Certs forcibly re-generated for all users' { It 'Certs re-generated have actually been re-written for all users' { . "/Users/jworkman/Documents/GitHub/support/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1" $timeBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') @@ -65,7 +65,71 @@ Describe 'Generate User Cert Tests' -Tag "Generate" { } Context 'Certs generated for users whos cert is set to exipre soon' { - # Set config + BeforeAll { + # import necessary functions: + . "$JCScriptRoot/Functions/Private/CertDeployment/Get-CertInfo.ps1" + . "$JCScriptRoot/Functions/Private/CertDeployment/Get-ExpiringCertInfo.ps1" + # Set config + $configPath = "$JCScriptRoot/config.ps1" + $content = Get-Content -path $configPath + # set the user cert validity to just 10 days + $content -replace ('\$Global:JCUSERCERTVALIDITY = *.+', '$Global:JCUSERCERTVALIDITY = 10') | Set-Content -Path $configPath + + # get user from membership list + $RandomUsername = $global:JCRRadiusMembers.username | Get-Random -count 1 + + # regenerate user cert + Start-GenerateUserCerts -type ByUsername -username $($RandomUsername) -forceReplaceCerts + + # Update Global Expiring list: + $userCertInfo = Get-CertInfo -UserCerts + # Determine cut off date for expiring certs + $global:cutoffDate = (Get-Date).AddDays(15).Date + # Find all certs that will expire between current date and cut off date + $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $cutoffDate + + } + It 'Certs that are set to expire soon can be updated' { + # at this point expiring certs should be populated from beforeAll block + $Global:expiringCerts | Should -not -BeNullOrEmpty + # reset the validity counter + $content = Get-Content -path $configPath + # set the user cert validity to 90 days + $content -replace ('\$Global:JCUSERCERTVALIDITY = *.+', '$Global:JCUSERCERTVALIDITY = 90') | Set-Content -Path $configPath + # Get the certs before generation minus the .zip if it exists + $certsBefore = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -Filter "$($RandomUsername)*" -Exclude "*.zip" + # get the date before + $dateBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') + Start-Sleep 1 + # Generate certs for expired users, this should replace any expiring certs + Start-GenerateUserCerts -type ExpiringSoon -forceReplaceCerts + # Update Global Expiring list: + $userCertInfo = Get-CertInfo -UserCerts + $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $cutoffDate + # there should be no more certs left in the expiring cert var + $Global:expiringCerts | Should -BeNullOrEmpty + # Get the certs after generation minus the .zip if it exists + $certsAfter = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -Filter "$($RandomUsername)*" -Exclude "*.zip" + # test each file, it should have been written + foreach ($cert in $certsAfter) { + Write-Host "$($cert.Name)" + $beforeWriteTime = (($certsBefore | Where-Object { $_.Name -eq $cert.Name })).LastWriteTime.Ticks + $afterWriteTime = (($certsAfter | Where-Object { $_.Name -eq $cert.Name })).LastWriteTime.Ticks + # the time written on the cert should be updated + $beforeWriteTime | should -Not -Be $afterWriteTime + } + # the user cert table should have been updated too + $certInfo = Get-CertInfo -UserCerts -username $RandomUsername + $certInfo.count | should -be 1 + $certInfo.generated | Should -BeGreaterThan $dateBefore + } + AfterAll { + # reset the validity counter + $content = Get-Content -path $configPath + # set the user cert validity to 90 days + $content -replace ('\$Global:JCUSERCERTVALIDITY = *.+', '$Global:JCUSERCERTVALIDITY = 90') | Set-Content -Path $configPath + } + } Context 'Certs generated by username' { From 3cb728590c8401fccd29a34c1dc476608e3469c1 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Wed, 10 Jan 2024 15:25:58 -0700 Subject: [PATCH 019/346] first pass at deploy tests --- .../CertDeployment/Deploy-UserCertificate.ps1 | 10 ++- .../CertGeneration/Invoke-UserCertProcess.ps1 | 2 +- .../HashFunctions/Test-UserFromHash.ps1 | 3 + .../commands/invoke-commandByUserId.ps1 | 2 +- .../Private/settings/Update-JCRUsersJson.ps1 | 9 +- .../Functions/Public/Get-JCRGlobalVars.ps1 | 3 +- .../automation/Radius/JumpCloud-Radius.psm1 | 2 - .../Public/Start-DeployUserCerts.Tests.ps1 | 86 +++++++++++++++++++ .../Public/Start-GenerateUserCerts.Tests.ps1 | 1 - 9 files changed, 102 insertions(+), 16 deletions(-) diff --git a/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 index 14ebb7924..530d076f8 100644 --- a/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 @@ -97,7 +97,7 @@ function Deploy-UserCertificate { Compress-Archive -Path $userPfx -DestinationPath $userPfxZip -CompressionLevel NoCompression -Force # Find OS of System switch ($user.systemAssociations.osFamily) { - 'macOS' { + 'Mac OS X' { # Get the macOS system ids $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'macOS' } | Select-Object systemId @@ -256,7 +256,8 @@ fi $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):MacOSX" $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } } catch { - throw $_ + status_commandGenerated = $false + # throw $_ } $CommandTable = [PSCustomObject]@{ @@ -401,7 +402,8 @@ if (`$CurrentUser -eq "$($user.localUsername)") { $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" $systemIds | ForEach-Object { Set-JcSdkCommandAssociation -CommandId:("$($Command._id)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null } } catch { - throw $_ + status_commandGenerated = $false + # throw $_ } $CommandTable = [PSCustomObject]@{ @@ -472,7 +474,7 @@ if (`$CurrentUser -eq "$($user.localUsername)") { $userObjectFromTable.certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value (Get-Date) } Set-UserTable -index $userIndex -commandAssociationsObject $userObjectFromTable.commandAssociations -certInfoObject $userObjectFromTable.certInfo - $invokeCommands = invoke-commandByUserId -userID $user.userid + $invokeCommands = invoke-commandByUserId -userID $user.userId $result_deployed = $true } } diff --git a/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 index f40465976..7f2970c7a 100644 --- a/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 @@ -28,7 +28,7 @@ Function Invoke-UserCertProcess { $MatchedUser = $GLOBAL:JCRUsers[$radiusMember.userID] } catch { - exit + Write-Warning "could not identify user by userobject: $radiusMember" } } 'userObject' { diff --git a/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 b/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 index c8e28d5a3..3241f003e 100644 --- a/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 +++ b/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 @@ -31,6 +31,9 @@ function Test-UserFromHash { 'username' { # Get the index of the user within the hashtable $matchedIndex = $Global:JCRUsers.values.username.ToLower().IndexOf($username.ToLower()) + if ($matchedIndex -lt 0) { + throw "could not find user in cached data: $username" + } # Get the UserID from the keys $matchedUserID = $Global:JCRUsers.keys | Select-Object -Index $matchedIndex # validate that the userID is in the radiusMembership hash: diff --git a/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 index ba892ef00..8565a8e72 100644 --- a/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 +++ b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 @@ -17,7 +17,7 @@ function invoke-commandByUserid { # get Windows Command $windows_commandId = ($userObject.commandAssociations | Where-Object { $_.commandName -match "Windows" }).commandId # get list of macOS systems - $macOS_systemIds = $userObject.systemAssociations | Where-Object { $_.osFamily -eq "macOS" } + $macOS_systemIds = $userObject.systemAssociations | Where-Object { $_.osFamily -eq "Mac OS X" } # get list of Windows systems $windows_systemIds = $userObject.systemAssociations | Where-Object { $_.osFamily -eq "Windows" } } diff --git a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 index 9123a6b74..129ab4e8b 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 @@ -6,13 +6,11 @@ function Update-JCRUsersJson { $force ) begin { - } process { # validate that the system association data is correct in users.json: # $userArray = Get-UserJsonData foreach ($user in $Global:JCRRadiusMembers) { - $MatchedUser = $GLOBAL:JCRUsers[$user.userID] $userArrayObject, $userIndex = Get-UserFromTable -userID $user.userID -jsonFilePath "$JCScriptRoot/users.json" @@ -43,16 +41,15 @@ function Update-JCRUsersJson { $userArray = Get-UserJsonData foreach ($user in $userArray) { # If userID from users.json is no longer in RadiusMembers.keys, then: - If ( -Not ($user.userid -in $Global:JCRRadiusMembers.userid) ) { + If (($user.userId -notin $Global:JCRRadiusMembers.userID) ) { # Get User From Table $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $user.userId + # Remove the User From Table $userArray = $userArray | Where-Object { $_.userID -ne $user.userId } - # "removing $($user.userid)" } - # Remove the User From Table } } end { - $userArray | ConvertTo-Json -Depth 6 | Set-Content -Path "$JCScriptRoot/users.json" + Set-UserJsonData -userArray $userArray } } \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 index 23bdaa731..b4026882b 100644 --- a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 +++ b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 @@ -167,14 +167,15 @@ function Get-JCRGlobalVars { $Global:JCRRadiusMembers = $radiusMemberList # update the settings date Set-JCRSettingsFile -globalVarslastUpdate (Get-Date) + Update-JCRUsersJson } $false { - # write-host "pulling saved data from data file:" # set global vars from local cache $Global:JCRUsers = Get-Content -path "$JCScriptRoot/data/userHash.json" | ConvertFrom-Json -AsHashtable $Global:JCRSystems = Get-Content -path "$JCScriptRoot/data/systemHash.json" | ConvertFrom-Json -AsHashtable $Global:JCRAssociations = Get-Content -path "$JCScriptRoot/data/associationHash.json" | ConvertFrom-Json -AsHashtable $Global:JCRRadiusMembers = Get-Content -path "$JCScriptRoot/data/radiusMembers.json" | ConvertFrom-Json -AsHashtable + Update-JCRUsersJson } } } diff --git a/scripts/automation/Radius/JumpCloud-Radius.psm1 b/scripts/automation/Radius/JumpCloud-Radius.psm1 index d85a128fe..1266ae69e 100644 --- a/scripts/automation/Radius/JumpCloud-Radius.psm1 +++ b/scripts/automation/Radius/JumpCloud-Radius.psm1 @@ -31,5 +31,3 @@ $global:JCRConfig = Get-JCRSettingsFile # Get global variables or update if necessary Get-JCRGlobalVars -# Update Users Json if there's a change -Update-JCRUsersJson diff --git a/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 index ad6c1ba55..a4421a62e 100644 --- a/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 @@ -1,20 +1,106 @@ Describe 'Distribute User Cert Tests' -Tag 'Distribute' { Context 'Distribute all certificates for all users forcibly' { + BeforeAll { + # get required functions + . "$JCScriptRoot/Functions/Private/UserJson/Get-UserJsonData.ps1" + . "$JCScriptRoot/Functions/Private/CertDeployment/Get-CertInfo.ps1" + . "$JCScriptRoot/Functions/Private/UserTable/Get-UserFromTable.ps1" + + # clear certs: + $certs = Get-ChildItem -Path "$JCScriptRoot/UserCerts" + foreach ($cert in $certs) { + Remove-Item -Path $cert.FullName + } + #remove existing users + $usersToRemove = Get-JCuser -email "*pesterRadius*" | Remove-JCUser -force + Get-JCRGlobalVars -force -skipAssociation + } + it 'users with system associations will have deployed certs' { + + # generate all new certs + Start-GenerateUserCerts -type All -forceReplaceCerts + $userArray = Get-UserJsonData + foreach ($user in $userArray) { + # cert should not be deployed + $user.certinfo.deployed | Should -Be $false + } + start-deployUserCerts -type All -forceInvokeCommands + Start-Sleep 1 + $userArray = Get-UserJsonData + foreach ($user in $userArray) { + # cert should be deployed + if ($user.systemAssociations) { + $user.certinfo.deployed | Should -Be $true + } + $user.commandAssociations | ForEach-Object { + $command = Get-JcSdkCommand -Id $_.commandId -Fields name + $command | should -Not -BeNullOrEmpty + } + } + } + it 'users without system associations should not have a deployed cert, even if the force option is specified' { + $user = New-RandomUser -Domain "pesterRadius" | New-JCUser + $dateBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') + + Add-JCUserGroupMember -GroupID $Global:JCUSERGROUP -UserID $user.id + + # update membership + Get-JCRGlobalVars -skipAssociation -force + Start-GenerateUserCerts -type ByUsername -username $user.username -forceReplaceCerts + + start-deployUserCerts -type ByUsername -username $user.username -forceInvokeCommands + $obj, $index = Get-UserFromTable -userid $user.id + $obj.certInfo.generated | Should -BeGreaterThan $dateBefore + $obj.certInfo.deployed | Should -Be $false + $obj.commandAssociations | should -be $null + $obj.systemAssociations | should -be $null + # user should not have a command nor should their certinfo show that the cert was deployed + } } Context 'Distribute all certificates for all users without invoking' { + It 'users with system associations will have new commands generated; command will not be invoked' { + + } + It 'users with out system associations will not have new commands generated' { + + } } Context 'Distribute new certificates for new users forcibly' { + it 'a new user with a system association will get a new command and it will be invoked' { + + } + it 'a new user without a system association will not get a new command and it will not be invoked' { + + } } Context 'Distribute new certificates for new users without invoking' { + it 'a new user with a system association will get a new command and it will not be invoked' { + + } + it 'a new user without a system association will not get a new command and it will not be invoked' { + + } } Context 'Distribute new certificates for a single user forcibly' { + it 'a user with system associations receeives a new command is created and deployed' { + + } + it 'a user without system associations receeives a new command is not created and not deployed' { + + } } Context 'Distribute new certificates for a single user without invoking' { + it 'a user with system associations receeives a new command is created and is not deployed' { + + } + it 'a user without system associations receeives a new command is not created and not deployed' { + + } } } \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 index fd1bbb7a3..d3163d74d 100644 --- a/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 @@ -1,7 +1,6 @@ Describe 'Generate User Cert Tests' -Tag "Generate" { Context 'Certs forcibly re-generated for all users' { It 'Certs re-generated have actually been re-written for all users' { - . "/Users/jworkman/Documents/GitHub/support/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1" $timeBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') $certsBefore = Get-CertInfo -UserCerts # wait one second. From eae8dc1878e4fdecc0b9ab8b5b79849bdbd3bcbc Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Wed, 10 Jan 2024 15:40:43 -0700 Subject: [PATCH 020/346] remove verbose messaging in Get-JCRGlobalVars --- .../Radius/Functions/Public/Get-JCRGlobalVars.ps1 | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 index b4026882b..39ad35b1c 100644 --- a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 +++ b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 @@ -10,9 +10,7 @@ function Get-JCRGlobalVars { ) begin { # ensure the data directory exists to cache the json files: - if (Test-Path "$JCScriptRoot/data") { - Write-Host "[status] Data Directory Exists" - } else { + if (-not (Test-Path "$JCScriptRoot/data")) { Write-Host "[status] Creating Data Directory" New-Item -ItemType Directory -Path "$JCScriptRoot/data" } @@ -58,7 +56,7 @@ function Get-JCRGlobalVars { $fileContents = Get-Content "$JCScriptRoot/data/$file" switch ($skipAssociation) { $true { - Write-Host "[status] $JCScriptRoot/data/$file will be skipped" + # Write-Host "[status] $JCScriptRoot/data/$file will be skipped" $updateAssociation = $false } } @@ -75,7 +73,7 @@ function Get-JCRGlobalVars { } else { switch ($skipAssociation) { $true { - Write-Host "[status] $JCScriptRoot/data/$file will be skipped" + # Write-Host "[status] $JCScriptRoot/data/$file will be skipped" $updateAssociation = $false } } @@ -144,7 +142,6 @@ function Get-JCRGlobalVars { $userAssociationList = Get-Content -Raw -Path "$JCScriptRoot/data/associationHash.json" | ConvertFrom-Json -Depth 6 -AsHashtable } - # finally write out the data to file: $users | ConvertTo-Json -Depth 100 -Compress | Out-File "$JCScriptRoot/data/userHash.json" $systems | ConvertTo-Json -Depth 10 | Out-File "$JCScriptRoot/data/systemHash.json" @@ -159,7 +156,6 @@ function Get-JCRGlobalVars { end { switch ($update) { $true { - # set global vars $Global:JCRUsers = $users $Global:JCRSystems = $systems @@ -167,6 +163,7 @@ function Get-JCRGlobalVars { $Global:JCRRadiusMembers = $radiusMemberList # update the settings date Set-JCRSettingsFile -globalVarslastUpdate (Get-Date) + # update users.json Update-JCRUsersJson } $false { @@ -175,6 +172,7 @@ function Get-JCRGlobalVars { $Global:JCRSystems = Get-Content -path "$JCScriptRoot/data/systemHash.json" | ConvertFrom-Json -AsHashtable $Global:JCRAssociations = Get-Content -path "$JCScriptRoot/data/associationHash.json" | ConvertFrom-Json -AsHashtable $Global:JCRRadiusMembers = Get-Content -path "$JCScriptRoot/data/radiusMembers.json" | ConvertFrom-Json -AsHashtable + # update users.json Update-JCRUsersJson } } From 9e54d062c97b13d2974793dc8b348cee03df69b4 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Wed, 10 Jan 2024 16:26:52 -0700 Subject: [PATCH 021/346] test that new user /system associations can be written and distributed --- .../settings/Set-JCRAssociationHash.ps1 | 45 +++++++++++ .../Private/settings/Update-JCRUsersJson.ps1 | 7 +- .../Public/Start-DeployUserCerts.Tests.ps1 | 80 +++++++++++++++++-- 3 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 scripts/automation/Radius/Functions/Private/settings/Set-JCRAssociationHash.ps1 diff --git a/scripts/automation/Radius/Functions/Private/settings/Set-JCRAssociationHash.ps1 b/scripts/automation/Radius/Functions/Private/settings/Set-JCRAssociationHash.ps1 new file mode 100644 index 000000000..728df5017 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/settings/Set-JCRAssociationHash.ps1 @@ -0,0 +1,45 @@ +function Set-JCRAssociationHash { + [CmdletBinding()] + param ( + [Parameter()] + [system.string] + $userId + ) + begin { + # get the data + $associationFile = "$JCScriptRoot/data/associationHash.json" + $associationContent = Get-Content -Path $associationFile | ConvertFrom-Json -depth 6 -AsHashtable + } + process { + $systemMembership = Get-JcSdkUserTraverseSystem -UserId $userId + $systemList = New-Object System.Collections.ArrayList + foreach ($systemMember in $systemMembership) { + $systemDetails = Get-JCsdksystem -id $systemMember.id -fields 'osFamily hostname' + $systemList.Add( + [PSCustomObject]@{ + systemId = $systemDetails.id + osFamily = $systemDetails.osFamily + hostname = $systemDetails.hostname + } + ) | Out-Null + } + $matchedUser = $JCRUsers[$userid] + # if the userID is not there add it + if ($userID -notin $associationContent.keys) { + # add the content) + $associationContent.add( + $userId, @{ + 'systemAssociations' = $systemList + 'userData' = @($matchedUser | Select-Object -Property email, username) + }) | Out-Null + } else { + $associationContent[$userid].systemAssociations = $systemList + } + } + end { + # write out the file + $associationContent | ConvertTo-Json -Depth 6 | Set-Content -Path "$associationFile" + $Global:JCRAssociations = Get-Content -path "$associationFile" | ConvertFrom-Json -AsHashtable + + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 index 129ab4e8b..b693b09d1 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 @@ -21,9 +21,12 @@ function Update-JCRUsersJson { # determine if there's some difference that needs to be recorded: try { - $difference = Compare-Object -ReferenceObject $currentSystemObject.systemId -DifferenceObject $incomingSystemObject.systemId + if ($currentSystemObject -eq $null) { + $difference = $incomingSystemObject + } else { + $difference = Compare-Object -ReferenceObject $currentSystemObject.systemId -DifferenceObject $incomingSystemObject.systemId + } if ($difference) { - Set-UserTable -index $userIndex -username $MatchedUser.username -localUsername $MatchedUser.systemUsername -systemAssociationsObject ($incomingSystemObject | ConvertFrom-HashTable) } } catch { diff --git a/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 index a4421a62e..a2512104f 100644 --- a/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 @@ -1,11 +1,17 @@ Describe 'Distribute User Cert Tests' -Tag 'Distribute' { + BeforeAll { + # Load all functions from private folders + $Private = @( Get-ChildItem -Path "$JCScriptRoot/Functions/Private/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + } Context 'Distribute all certificates for all users forcibly' { BeforeAll { - # get required functions - . "$JCScriptRoot/Functions/Private/UserJson/Get-UserJsonData.ps1" - . "$JCScriptRoot/Functions/Private/CertDeployment/Get-CertInfo.ps1" - . "$JCScriptRoot/Functions/Private/UserTable/Get-UserFromTable.ps1" - # clear certs: $certs = Get-ChildItem -Path "$JCScriptRoot/UserCerts" foreach ($cert in $certs) { @@ -59,8 +65,37 @@ Describe 'Distribute User Cert Tests' -Tag 'Distribute' { } Context 'Distribute all certificates for all users without invoking' { + BeforeAll { + # clear certs: + $certs = Get-ChildItem -Path "$JCScriptRoot/UserCerts" + foreach ($cert in $certs) { + Remove-Item -Path $cert.FullName + } + #remove existing users + $usersToRemove = Get-JCuser -email "*pesterRadius*" | Remove-JCUser -force + Get-JCRGlobalVars -force -skipAssociation + } It 'users with system associations will have new commands generated; command will not be invoked' { - + # generate all new certs + Start-GenerateUserCerts -type All -forceReplaceCerts + $userArray = Get-UserJsonData + foreach ($user in $userArray) { + # cert should not be deployed + $user.certinfo.deployed | Should -Be $false + } + start-deployUserCerts -type All -forceInvokeCommands + Start-Sleep 1 + $userArray = Get-UserJsonData + foreach ($user in $userArray) { + # cert should be deployed + if ($user.systemAssociations) { + $user.certinfo.deployed | Should -Be $false + } + $user.commandAssociations | ForEach-Object { + $command = Get-JcSdkCommand -Id $_.commandId -Fields name + $command | should -Not -BeNullOrEmpty + } + } } It 'users with out system associations will not have new commands generated' { @@ -68,7 +103,40 @@ Describe 'Distribute User Cert Tests' -Tag 'Distribute' { } Context 'Distribute new certificates for new users forcibly' { + BeforeAll { + # clear certs: + $certs = Get-ChildItem -Path "$JCScriptRoot/UserCerts" + foreach ($cert in $certs) { + Remove-Item -Path $cert.FullName + } + #remove existing users + $usersToRemove = Get-JCuser -email "*pesterRadius*" | Remove-JCUser -force + Get-JCRGlobalVars -force -skipAssociation + } it 'a new user with a system association will get a new command and it will be invoked' { + $user = New-RandomUser -Domain "pesterRadius" | New-JCUser + $dateBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') + # add user to membership group + Add-JCUserGroupMember -GroupID $Global:JCUSERGROUP -UserID $user.id + # get random system + $system = Get-JCSystem -os windows | Get-Random -Count 1 + Add-JCSystemUser -UserID $user.id -SystemID $system.id + + # update membership + Get-JCRGlobalVars -skipAssociation -force + # todo: manually update association table to account for new membership + Set-JCRAssociationHash -userId $user.id + Update-JCRUsersJson + # now generate the user certs + Start-GenerateUserCerts -type ByUsername -username $user.username -forceReplaceCerts + start-deployUserCerts -type ByUsername -username $user.username -forceInvokeCommands + + $obj, $index = Get-UserFromTable -userid $user.id + $obj.certInfo.generated | Should -BeGreaterThan $dateBefore + $obj.certInfo.deployed | Should -Be $true + $obj.commandAssociations | should -Not -BeNullOrEmpty + $obj.systemAssociations | should -Not -BeNullOrEmpty + } it 'a new user without a system association will not get a new command and it will not be invoked' { From 0fe11a64dfdcc0f7a7837700beda3cd60445347b Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Fri, 12 Jan 2024 16:03:59 -0700 Subject: [PATCH 022/346] passing tests --- .../CertDeployment/Deploy-UserCertificate.ps1 | 8 +-- .../Private/SystemTable/New-SystemTable.ps1 | 6 +- .../commands/invoke-commandByUserId.ps1 | 4 +- .../settings/Set-JCRAssociationHash.ps1 | 6 +- .../Private/settings/Update-JCRUsersJson.ps1 | 28 ++++++-- .../Functions/Public/Get-JCRGlobalVars.ps1 | 2 + .../automation/Radius/Tests/Invoke-Pester.ps1 | 3 + .../Tests/Public/Get-JCRGlobalVars.Tests.ps1 | 33 +++++----- .../Public/Start-DeployUserCerts.Tests.ps1 | 38 ++++++++--- .../Public/Start-GenerateRootCert.Tests.ps1 | 4 +- .../Public/Start-GenerateUserCerts.Tests.ps1 | 18 ++++- .../Radius/Tests/Setup-JCRadiusOrg.ps1 | 0 .../Radius/Tests/SetupRadiusOrg.ps1 | 66 +++++++++++++++++++ 13 files changed, 171 insertions(+), 45 deletions(-) delete mode 100644 scripts/automation/Radius/Tests/Setup-JCRadiusOrg.ps1 create mode 100644 scripts/automation/Radius/Tests/SetupRadiusOrg.ps1 diff --git a/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 index 530d076f8..a89cd0e4b 100644 --- a/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 @@ -97,7 +97,7 @@ function Deploy-UserCertificate { Compress-Archive -Path $userPfx -DestinationPath $userPfxZip -CompressionLevel NoCompression -Force # Find OS of System switch ($user.systemAssociations.osFamily) { - 'Mac OS X' { + 'macOS' { # Get the macOS system ids $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'macOS' } | Select-Object systemId @@ -274,9 +274,9 @@ fi $status_commandGenerated = $true } - 'Windows' { + 'windows' { # Get the Windows system ids - $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'Windows' } | Select-Object systemId + $systemIds = $user.systemAssociations | Where-Object { $_.osFamily -eq 'windows' } | Select-Object systemId # Check to see if previous commands exist $Command = Get-JCCommand -name "RadiusCert-Install:$($user.userName):Windows" @@ -415,7 +415,7 @@ if (`$CurrentUser -eq "$($user.localUsername)") { } $user.commandAssociations += $CommandTable - Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" + # Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" $status_commandGenerated = $true } diff --git a/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 b/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 index f23a24181..3c1695032 100644 --- a/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 @@ -18,7 +18,11 @@ function New-SystemTable { $systemTable = @{ systemId = $system.systemId displayName = $system.hostname - osFamily = $system.osFamily + osFamily = if ($system.osFamily -eq "darwin") { + "macOS" + } elseif ($system.osFamily -eq "windows") { + "windows" + } } $systemAssociations += $systemTable } diff --git a/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 index 8565a8e72..6ccc79d01 100644 --- a/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 +++ b/scripts/automation/Radius/Functions/Private/commands/invoke-commandByUserId.ps1 @@ -17,9 +17,9 @@ function invoke-commandByUserid { # get Windows Command $windows_commandId = ($userObject.commandAssociations | Where-Object { $_.commandName -match "Windows" }).commandId # get list of macOS systems - $macOS_systemIds = $userObject.systemAssociations | Where-Object { $_.osFamily -eq "Mac OS X" } + $macOS_systemIds = $userObject.systemAssociations | Where-Object { $_.osFamily -eq "macOS" } # get list of Windows systems - $windows_systemIds = $userObject.systemAssociations | Where-Object { $_.osFamily -eq "Windows" } + $windows_systemIds = $userObject.systemAssociations | Where-Object { $_.osFamily -eq "windows" } } process { diff --git a/scripts/automation/Radius/Functions/Private/settings/Set-JCRAssociationHash.ps1 b/scripts/automation/Radius/Functions/Private/settings/Set-JCRAssociationHash.ps1 index 728df5017..e91d4dc2e 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Set-JCRAssociationHash.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/Set-JCRAssociationHash.ps1 @@ -18,7 +18,11 @@ function Set-JCRAssociationHash { $systemList.Add( [PSCustomObject]@{ systemId = $systemDetails.id - osFamily = $systemDetails.osFamily + osFamily = if ($systemDetails.osFamily -eq "darwin") { + "macOS" + } elseif ($systemDetails.osFamily -eq "windows") { + "windows" + } hostname = $systemDetails.hostname } ) | Out-Null diff --git a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 index b693b09d1..424ff6e90 100644 --- a/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 +++ b/scripts/automation/Radius/Functions/Private/settings/Update-JCRUsersJson.ps1 @@ -16,18 +16,32 @@ function Update-JCRUsersJson { if ($userIndex -ge 0) { # $userArrayObject - $currentSystemObject = $userArrayObject.systemAssociations - $incomingSystemObject = $Global:JCRAssociations[$user.userID].systemAssociations + # write-host "$($userArrayObject.username) | $($userArrayObject.userId)" + $currentSystemObject = $userArrayObject.systemAssociations | Select-Object systemId, hostname, osFamily + $incomingSystemObject = $Global:JCRAssociations[$userArrayObject.userId].systemAssociations + $incomingList = New-Object System.Collections.ArrayList + foreach ($system in $Global:JCRAssociations[$userArrayObject.userId].systemAssociations) { + $incomingList.Add( + [pscustomobject]@{ + systemId = $system.systemId + hostname = $system.hostname + osFamily = $system.osFamily + }) | Out-Null + } # determine if there's some difference that needs to be recorded: try { if ($currentSystemObject -eq $null) { - $difference = $incomingSystemObject + $difference = $incomingList + Set-UserTable -index $userIndex -username $MatchedUser.username -localUsername $MatchedUser.systemUsername -systemAssociationsObject ($incomingList) } else { - $difference = Compare-Object -ReferenceObject $currentSystemObject.systemId -DifferenceObject $incomingSystemObject.systemId - } - if ($difference) { - Set-UserTable -index $userIndex -username $MatchedUser.username -localUsername $MatchedUser.systemUsername -systemAssociationsObject ($incomingSystemObject | ConvertFrom-HashTable) + # write-host "test for differences" + if ($currentSystemObject -eq $incomingList) { + # write-host "nothing to do" + } else { + # write-host "writing user to do" + Set-UserTable -index $userIndex -username $MatchedUser.username -localUsername $MatchedUser.systemUsername -systemAssociationsObject ($incomingList) + } } } catch { <#Do this if a terminating exception happens#> diff --git a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 index 39ad35b1c..468162095 100644 --- a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 +++ b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 @@ -19,8 +19,10 @@ function Get-JCRGlobalVars { $lastUpdateTimespan = New-TimeSpan -Start $global:JCRConfig.globalvars.lastupdate -end (Get-Date) if ($lastUpdateTimespan.TotalHours -gt 24) { $update = $true + $updateAssociation = $true } else { $update = $false + $updateAssociation = $false } if ($force) { $update = $true diff --git a/scripts/automation/Radius/Tests/Invoke-Pester.ps1 b/scripts/automation/Radius/Tests/Invoke-Pester.ps1 index 41b6036f5..9bf0bc4c3 100644 --- a/scripts/automation/Radius/Tests/Invoke-Pester.ps1 +++ b/scripts/automation/Radius/Tests/Invoke-Pester.ps1 @@ -104,6 +104,9 @@ $configuration.Filter.ExcludeTag = $ExcludeTagList $configuration.CodeCoverage.OutputPath = ($PesterResultsFileXmldir + 'coverage.xml') $configuration.testresult.OutputPath = ($PesterResultsFileXmldir + 'results.xml') +Write-Host "Begin Org Setup Before Tests:" +. "$PSScriptRoot/SetupRadiusOrg.ps1" + Write-Host ("[RUN COMMAND] Invoke-Pester -Path:('$PesterRunPaths') -TagFilter:('$($IncludeTags -join "','")') -ExcludeTagFilter:('$($ExcludeTagList -join "','")') -PassThru") -BackgroundColor:('Black') -ForegroundColor:('Magenta') # Run Pester tests Invoke-Pester -Configuration $configuration diff --git a/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 index f4e7ebfb1..6d4768edc 100644 --- a/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 @@ -1,4 +1,4 @@ -Describe "Get Global Variable Data Tests" { +Describe "Get Global Variable Data Tests" -Tag "Cache" { BeforeAll { $dataPath = "$JCScriptRoot/data/" $requiredFiles = @( @@ -7,7 +7,15 @@ Describe "Get Global Variable Data Tests" { 'systemHash.json', 'userHash.json' ) - + # explicitly import the settings file functions for these tests: + $Private = @( Get-ChildItem -Path "$JCScriptRoot/Functions/Private/settings/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } } Context "When no 'data' directory exists" { BeforeAll { @@ -31,17 +39,10 @@ Describe "Get Global Variable Data Tests" { BeforeAll { # if the data directory does not exist, generate the global vars + directory if (-not (Test-Path -Path ($dataPath))) { + Write-Host "Generating data:" Get-JCRGlobalVars } - # explicitly import the settings file functions for these tests: - $Private = @( Get-ChildItem -Path "$JCScriptRoot/Functions/Private/settings/*.ps1" -Recurse) - Foreach ($Import in $Private) { - Try { - . $Import.FullName - } Catch { - Write-Error -Message "Failed to import function $($Import.FullName): $_" - } - } + } It "Data should not be refreshed when running Get-JCRGlobalVars if it's been less than 24 hours since last update" { # Get the settings data @@ -87,10 +88,10 @@ Describe "Get Global Variable Data Tests" { } else { # set the settings file to a mocked value of now Set-JCRSettingsFile -globalVarslastUpdate (Get-Date).AddHours(-25) - Start-Sleep 1 + Start-Sleep 2 $settingsData = Get-JCRSettingsFile - $timespan = New-TimeSpan -Start (Get-Date).AddHours(-24) -End $settingsData.globalVars.lastupdate - # Write-Host "Time between 24 hrs and settings file: $($timespan.TotalHours)" + $timespan = New-TimeSpan -Start $settingsData.globalVars.lastupdate -End (Get-Date).AddHours(-24) + Write-Host "Time between 24 hrs and settings file: $($timespan.TotalHours)" } # run Get-JCRGlobalVars Get-JCRGlobalVars @@ -99,10 +100,10 @@ Describe "Get Global Variable Data Tests" { $filesAfter = Get-ChildItem -Path $dataPath # test each file write date foreach ($file in $requiredFiles) { - # write-host "validating write times for $file" + write-host "validating write times for $file" $beforeWriteTime = (($filesBefore | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks $afterWriteTime = (($filesAfter | Where-Object { $_.Name -eq $file })).LastWriteTime.Ticks - # Write-Host "before: $beforeWriteTime should not be after: $afterWriteTime" + Write-Host "before: $beforeWriteTime should not be after: $afterWriteTime" # The file write time before running Get-JCRGlobalVars should not be the same after running the function $beforeWriteTime | should -Not -Be $afterWriteTime } diff --git a/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 index a2512104f..5b9c6c724 100644 --- a/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 @@ -9,6 +9,13 @@ Describe 'Distribute User Cert Tests' -Tag 'Distribute' { Write-Error -Message "Failed to import function $($Import.FullName): $_" } } + # import helper functions: + . "$PSScriptRoot/../../../../../PowerShell/JumpCloud Module/Tests/HelperFunctions.ps1" + # Manually update user associations for radius members, cache won't pick them up before: + foreach ($user in $global:JCRRadiusMembers) { + Set-JCRAssociationHash -UserID $user.userID + } + } Context 'Distribute all certificates for all users forcibly' { BeforeAll { @@ -83,7 +90,7 @@ Describe 'Distribute User Cert Tests' -Tag 'Distribute' { # cert should not be deployed $user.certinfo.deployed | Should -Be $false } - start-deployUserCerts -type All -forceInvokeCommands + start-deployUserCerts -type All Start-Sleep 1 $userArray = Get-UserJsonData foreach ($user in $userArray) { @@ -136,19 +143,32 @@ Describe 'Distribute User Cert Tests' -Tag 'Distribute' { $obj.certInfo.deployed | Should -Be $true $obj.commandAssociations | should -Not -BeNullOrEmpty $obj.systemAssociations | should -Not -BeNullOrEmpty - - - } - it 'a new user without a system association will not get a new command and it will not be invoked' { - } - } Context 'Distribute new certificates for new users without invoking' { it 'a new user with a system association will get a new command and it will not be invoked' { + $user = New-RandomUser -Domain "pesterRadius" | New-JCUser + $dateBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') + # add user to membership group + Add-JCUserGroupMember -GroupID $Global:JCUSERGROUP -UserID $user.id + # get random system + $system = Get-JCSystem -os windows | Get-Random -Count 1 + Add-JCSystemUser -UserID $user.id -SystemID $system.id - } - it 'a new user without a system association will not get a new command and it will not be invoked' { + # update membership + Get-JCRGlobalVars -skipAssociation -force + # todo: manually update association table to account for new membership + Set-JCRAssociationHash -userId $user.id + Update-JCRUsersJson + # now generate the user certs + Start-GenerateUserCerts -type ByUsername -username $user.username -forceReplaceCerts + start-deployUserCerts -type ByUsername -username $user.username + + $obj, $index = Get-UserFromTable -userid $user.id + $obj.certInfo.generated | Should -BeGreaterThan $dateBefore + $obj.certInfo.deployed | Should -Be $false + $obj.commandAssociations | should -Not -BeNullOrEmpty + $obj.systemAssociations | should -Not -BeNullOrEmpty } diff --git a/scripts/automation/Radius/Tests/Public/Start-GenerateRootCert.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-GenerateRootCert.Tests.ps1 index e758514bd..7b1612fd3 100644 --- a/scripts/automation/Radius/Tests/Public/Start-GenerateRootCert.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Start-GenerateRootCert.Tests.ps1 @@ -1,5 +1,5 @@ -Describe "Generate Root Certifcate Tests" { - Context "A new certificate can be generated" -skip { +Describe "Generate Root Certifcate Tests" -Tag "GenerateRootCert" { + Context "A new certificate can be generated" { # If the /Cert/ folder is not empty, clear the directory $items = Get-ChildItem $JCScriptRoot/Cert if ($items) { diff --git a/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 index d3163d74d..c3960d7bc 100644 --- a/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 +++ b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 @@ -1,4 +1,15 @@ -Describe 'Generate User Cert Tests' -Tag "Generate" { +Describe 'Generate User Cert Tests' -Tag "GenerateUserCerts" { + BeforeAll { + # Load all functions from private folders + $Private = @( Get-ChildItem -Path "$JCScriptRoot/Functions/Private/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + } Context 'Certs forcibly re-generated for all users' { It 'Certs re-generated have actually been re-written for all users' { $timeBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') @@ -21,9 +32,10 @@ Describe 'Generate User Cert Tests' -Tag "Generate" { } Context 'Certs generated for newly added users' { beforeall { - # Select a user TODO: create a pester user - $user = Get-JCuser -username "chet.atkins" + $user = New-RandomUser -Domain "pesterRadius" | New-JCUser + Add-JCUserGroupMember -GroupID $Global:JCUSERGROUP -UserID $user.id $certs = Get-ChildItem -Path "$JCScriptRoot/UserCerts" -filter "$($user.username)*" + # if user cert exists, for random user, remove: foreach ($cert in $certs) { remove-item $cert.fullname } diff --git a/scripts/automation/Radius/Tests/Setup-JCRadiusOrg.ps1 b/scripts/automation/Radius/Tests/Setup-JCRadiusOrg.ps1 deleted file mode 100644 index e69de29bb..000000000 diff --git a/scripts/automation/Radius/Tests/SetupRadiusOrg.ps1 b/scripts/automation/Radius/Tests/SetupRadiusOrg.ps1 new file mode 100644 index 000000000..5429018b6 --- /dev/null +++ b/scripts/automation/Radius/Tests/SetupRadiusOrg.ps1 @@ -0,0 +1,66 @@ +# import helper functions: +. "$PSScriptRoot/../../../../PowerShell/JumpCloud Module/Tests/HelperFunctions.ps1" +# beforeAll, remove PesterRadiusGroup + users +Write-Warning "Removing Pester Radius User Groups with name: PesterRadiusGroup*" +$pesterRadiusGroups = Get-JcSdkUserGroup -filter "name:search:PesterRadiusGroup" +foreach ($pesterRadiusGroup in $pesterRadiusGroups) { + Remove-JcSdkUserGroup -Id $pesterRadiusGroup.Id | Out-Null +} + +Write-Warning "Removing Pester Radius Users with emailDomain: *pesterradtest.com" +$pesterRadiusUsers = Get-JCUser -email "*pesterradtest.com" +foreach ($user in $pesterRadiusUsers) { + Remove-JcSdkUser -Id $user.id | Out-Null +} +# remove existing users created in test: +Write-Warning "Removing Pester Radius Users with emailDomain: *pesterRadius*" +$usersToRemove = Get-JCuser -email "*pesterRadius*" | Remove-JCUser -force + +# Create users + +Write-Warning "Creating New Pester Radius Users" +# user bound to mac +$macUser = New-RandomUser -Domain "PesterRadTest" | New-JCUser +$macOSSystem = Get-JCSystem -os "Mac OS X" | Get-Random -Count 1 +Set-JcSdkSystemAssociation -SystemId $macOSSystem.id -id $macUser.id -Type 'user' -Op "add" +# user bound to windows + +$windowsUser = New-RandomUser -Domain "PesterRadTest" | New-JCUser +$windowsSystem = Get-JCSystem -os "Windows" | Get-Random -Count 1 +Set-JcSdkSystemAssociation -SystemId $macOSSystem.id -id $windowsUser.id -Type 'user' -Op "add" +# user bound to both macOS and windows +$bothUser = New-RandomUser -Domain "PesterRadTest" | New-JCUser +Set-JcSdkSystemAssociation -SystemId $macOSSystem.id -id $bothUser.id -Type 'user' -Op "add" +Set-JcSdkSystemAssociation -SystemId $windowsSystem.id -id $bothUser.id -Type 'user' -Op "add" + +# create user group + Add membership +Write-Warning "Creating New Pester Radius Group" +$randomNum = (Get-Random -Minimum 900 -Maximum 999) +$radiusUserGroup = New-JCUserGroup -GroupName "PesterRadiusGroup-$randomNum" +Set-JcSdkUserGroupMember -GroupId $radiusUserGroup.Id -Id $macUser.id -Op "add" +Set-JcSdkUserGroupMember -GroupId $radiusUserGroup.Id -Id $windowsUser.id -Op "add" +Set-JcSdkUserGroupMember -GroupId $radiusUserGroup.Id -Id $bothUser.id -Op "add" + +# update config: +Write-Warning "Updating Config File" + +$configPath = "$PSScriptRoot/../config.ps1" +$configContent = Get-Content -path $configPath +# Update the userGroupID: +$configContent -replace ('\$Global:JCUSERGROUP = *.+', "`$Global:JCUSERGROUP = `"$($radiusUserGroup.id)`"") | Set-Content -Path $configPath +# update the openSSL path: +if ($IsMacOS) { + $brewList = brew list openssl@3 + if (-Not ($brewList)) { + throw "OpenSSL v3 is not installed on this system" + } + $brewListBinary = $brewList | Where-Object { $_ -match "/bin/openssl" } + $regmatch = $brewListBinary | Select-String -pattern "\/([0-9].[0-9].[0-9])\/" + $opensslVersion = $regmatch.matches.groups[1].value + + Write-Warning "OpenSSL Version: $opensslVersion is installed via homebrew on this system; updating config:" + $configContent = Get-Content -path $configPath + $configContent -replace ('\$Global:opensslBinary = *.+', "`$Global:opensslBinary = `"$($brewListBinary)`"") | Set-Content -Path $configPath +} + +Import-Module "$psscriptRoot/../JumpCloud-Radius.psd1" -Force \ No newline at end of file From a407c8c808bbfab40ae115232d8d77108f97527d Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Fri, 12 Jan 2024 16:04:46 -0700 Subject: [PATCH 023/346] config update --- scripts/automation/Radius/Config.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/automation/Radius/Config.ps1 b/scripts/automation/Radius/Config.ps1 index c5b7994d7..27d63c5a5 100644 --- a/scripts/automation/Radius/Config.ps1 +++ b/scripts/automation/Radius/Config.ps1 @@ -5,7 +5,7 @@ $Global:JCUSERCERTPASS = 'secret1234!' # USER CERT Validity Length (days) $Global:JCUSERCERTVALIDITY = 90 # List Of Radius Network SSID(s) -# For Multiple SSIDs enter as a single string seperated by a semicolon ex: +# For Multiple SSIDs enter as a single string separated by a semicolon ex: # "CorpNetwork_Denver;CorpNetwork_Boulder;CorpNetwork_Boulder 5G;Guest Network" $Global:NETWORKSSID = "YOUR_SSID" # OpenSSLBinary by default this is (openssl) From 62e511e1113f2acb13e3c38692b2f3927b1ad622 Mon Sep 17 00:00:00 2001 From: Joe Workman Date: Tue, 16 Jan 2024 20:39:03 -0700 Subject: [PATCH 024/346] documentation + param descriptions --- .../CertDeployment/Deploy-UserCertificate.ps1 | 10 ++-- .../Private/CertDeployment/Get-CertInfo.ps1 | 6 +-- .../CertDeployment/Get-CertKeyPass.ps1 | 1 + .../CertGeneration/Generate-UserCert.ps1 | 2 +- .../CertGeneration/Invoke-UserCertProcess.ps1 | 10 ++-- .../Functions/Public/Get-JCRGlobalVars.ps1 | 4 +- .../Public/Start-DeployUserCerts.ps1 | 6 +-- .../Public/Start-GenerateRootCert.ps1 | 9 +--- .../Public/Start-GenerateUserCerts.ps1 | 24 ++++++++-- .../automation/Radius/images/deployMenu.png | Bin 0 -> 74289 bytes .../automation/Radius/images/expireCert.png | Bin 51048 -> 80177 bytes .../Radius/images/expireCertFromWindow.png | Bin 0 -> 81227 bytes scripts/automation/Radius/images/mainMenu.png | Bin 47336 -> 75877 bytes .../automation/Radius/images/mainMenuNoCA.png | Bin 39367 -> 66356 bytes scripts/automation/Radius/readme.md | 44 +++++++++--------- 15 files changed, 63 insertions(+), 53 deletions(-) create mode 100644 scripts/automation/Radius/images/deployMenu.png create mode 100644 scripts/automation/Radius/images/expireCertFromWindow.png diff --git a/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 index a89cd0e4b..cff78ccf5 100644 --- a/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 @@ -1,16 +1,16 @@ function Deploy-UserCertificate { [CmdletBinding()] param ( - # Parameter help description - [Parameter(Mandatory)] + # Input from users.json + [Parameter(HelpMessage = 'An individual or array of user objects from users.json', Mandatory)] [System.Object[]] $userObject, - # Parameter help description - [Parameter()] + # when specified will force newly generated commands to be invoked on systems + [Parameter(HelpMessage = 'When specified, this parameter will invoke commands on systems associated to the user from the "userObject" parameter')] [bool] $forceInvokeCommands, # prompt replace existing certificate - [Parameter()] + [Parameter(HelpMessage = 'When specified, this parameter will prompt for user imput and ask if generated commands should be invoked on associated systems')] [switch] $prompt ) diff --git a/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertInfo.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertInfo.ps1 index 774729630..4b4884b9f 100644 --- a/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertInfo.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertInfo.ps1 @@ -1,13 +1,13 @@ function Get-CertInfo { [CmdletBinding()] param ( - [Parameter(ParameterSetName = 'CA', Mandatory = $true)] + [Parameter(HelpMessage = 'When specified this function will return certificate information for the root CA located in /Cert', ParameterSetName = 'CA', Mandatory = $true)] [switch] $RootCA, - [Parameter(ParameterSetName = 'User', Mandatory = $true)] + [Parameter(HelpMessage = 'When specified this function will return all user certificate information for user certs located in /UserCerts', ParameterSetName = 'User', Mandatory = $true)] [switch] $UserCerts, - [Parameter(ParameterSetName = 'User', Mandatory = $false)] + [Parameter(HelpMessage = 'When specified this function will return a single users certificate infomration for a cert located in /UserCerts', ParameterSetName = 'User', Mandatory = $false)] [system.string] $username ) diff --git a/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertKeyPass.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertKeyPass.ps1 index 164584ffe..252708842 100644 --- a/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertKeyPass.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertKeyPass.ps1 @@ -1,4 +1,5 @@ function Get-CertKeyPass { + #TODO: params required to test if a CA password is correct $foundKeyPem = Resolve-Path -Path "$JCScriptRoot/Cert/*key.pem" Write-Host "Found key: $($foundKeyPem)" diff --git a/scripts/automation/Radius/Functions/Private/CertGeneration/Generate-UserCert.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Generate-UserCert.ps1 index 66c796bb4..8669f5aaf 100644 --- a/scripts/automation/Radius/Functions/Private/CertGeneration/Generate-UserCert.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertGeneration/Generate-UserCert.ps1 @@ -1,7 +1,7 @@ function Generate-UserCert { [CmdletBinding()] param ( - [Parameter(Mandatory = $true)] + [Parameter(HelpMessage = 'The type of certificate to generate, either: "EmailSAN", "EmailDN" or "UsernameCN"', Mandatory = $true)] [ValidateSet("EmailSAN", "EmailDn", "UsernameCN")] [system.String] $CertType, diff --git a/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 index 7f2970c7a..56d88b1a3 100644 --- a/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 @@ -1,31 +1,29 @@ Function Invoke-UserCertProcess { [CmdletBinding()] param ( - [Parameter(ParameterSetName = 'radiusMember')] + [Parameter(HelpMessage = 'The user object from users.json', ParameterSetName = 'radiusMember')] [System.object] $radiusMember, [Parameter(ParameterSetName = 'selectedUserObject')] [System.String] $selectedUserObject, - [Parameter(Mandatory)] + [Parameter(HelpMessage = 'The type of certificate to generate, either: "EmailSAN", "EmailDN" or "UsernameCN"', Mandatory)] [ValidateSet('EmailSAN', 'EmailDN', 'UsernameCN')] [System.String] $certType, # force replace existing certificate - [Parameter()] + [Parameter(HelpMessage = 'When specified, existing certificates will be replaced')] [switch] $forceReplaceCert, # prompt replace existing certificate - [Parameter()] + [Parameter(HelpMessage = 'When specified, this parameter will prompt for user imput and ask if existing certificates should be replaced' )] [switch] $prompt ) begin { - switch ($PSCmdlet.ParameterSetName) { 'radiusMember' { try { - $MatchedUser = $GLOBAL:JCRUsers[$radiusMember.userID] } catch { Write-Warning "could not identify user by userobject: $radiusMember" diff --git a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 index 468162095..2606662e6 100644 --- a/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 +++ b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 @@ -1,10 +1,10 @@ function Get-JCRGlobalVars { [CmdletBinding()] param ( - [Parameter()] + [Parameter(HelpMessage = "Force update all cached users, systems, associations, radius group members")] [switch] $force, - [Parameter()] + [Parameter(HelpMessage = "Skips the user to system association cache, which may take a long time on larger organizations")] [switch] $skipAssociation ) diff --git a/scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 index 27da419a7..26cd22615 100644 --- a/scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 @@ -3,16 +3,16 @@ function Start-DeployUserCerts { [CmdletBinding(DefaultParameterSetName = 'gui')] param ( # Type of certs to distribute, All, New or byUsername - [Parameter(ParameterSetName = 'cli', Mandatory)] + [Parameter(HelpMessage = 'Type of cert deployment to initiate', ParameterSetName = 'cli', Mandatory)] [ValidateSet("All", "New", "ByUsername")] [system.String] $type, # username - [Parameter(ParameterSetName = 'cli')] + [Parameter(HelpMessage = 'The JumpCloud username of a user to deploy a certificate', ParameterSetName = 'cli')] [System.String] $username, # Force invoke commands after generation - [Parameter(ParameterSetName = 'cli')] + [Parameter(HelpMessage = 'Switch to force invoke generated commands on systems', ParameterSetName = 'cli')] [switch] $forceInvokeCommands ) diff --git a/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 b/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 index e6b3ad580..a531da5f4 100644 --- a/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 @@ -2,14 +2,9 @@ Function Start-GenerateRootCert { [CmdletBinding(DefaultParameterSetName = 'gui')] param ( # Cert Key Password - # Parameter help description - [Parameter(ParameterSetName = 'cli')] + [Parameter(HelpMessage = 'The root certificate key password', ParameterSetName = 'cli')] [string] - $certKeyPassword, - # Force invoke commands after generation - [Parameter(ParameterSetName = 'cli')] - [switch] - $forceReplcaeCert + $certKeyPassword ) # this script will generate a Self Signed CA (root cert) to be imported on the # Radius CBA-BYO Authentication UI diff --git a/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 index 40b72d173..db836ca60 100644 --- a/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 +++ b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 @@ -3,16 +3,16 @@ function Start-GenerateUserCerts { [CmdletBinding(DefaultParameterSetName = 'gui')] param ( # Type of certs to distribute, All, New or byUsername - [Parameter(ParameterSetName = 'cli', Mandatory)] + [Parameter(HelpMessage = 'Type of certificate to initialize. To generate all new certificates for existing users, specify "all", To generate certificates for users who have not yet had certificates generated, specify "new". To generate certificates by an individual, speficy "ByUsername" and populate the "username" parameter. To generate certificates for users who have certificates expiring in 15 days or less, specify "ExpiringSoon".', ParameterSetName = 'cli', Mandatory)] [ValidateSet("All", "New", "ByUsername", "ExpiringSoon")] [system.String] $type, # username - [Parameter(ParameterSetName = 'cli')] + [Parameter(HelpMessage = 'The JumpCloud username of an individual user', ParameterSetName = 'cli')] [System.String] $username, # Force invoke commands after generation - [Parameter(ParameterSetName = 'cli')] + [Parameter(HelpMessage = 'When specified, this parameter will replace certificates if they already exist on the current filesystem', ParameterSetName = 'cli')] [switch] $forceReplaceCerts ) @@ -119,10 +119,24 @@ function Start-GenerateUserCerts { $userObject, $userIndex = Get-UserFromTable -jsonFilePath "$JCScriptRoot/users.json" -userID $confirmUser.id switch ($forceReplaceCerts) { $true { - $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType -forceReplaceCert + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType -forceReplaceCert + } + 'cli' { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType -forceReplaceCert + } + } } $false { - $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType -Prompt + } + 'cli' { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $CertType + } + } } } Show-RadiusProgress -completedItems $userObject.count -totalItems $userObject.count -ActionText "Generating Radius Certificates" -previousOperationResult $result diff --git a/scripts/automation/Radius/images/deployMenu.png b/scripts/automation/Radius/images/deployMenu.png new file mode 100644 index 0000000000000000000000000000000000000000..f09ad2f8b81fc6acf397ad52daea81d94b9882ee GIT binary patch literal 74289 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Ⓢ;A&FTBVo(f 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

s0#%Cf+zS7mMMiiX+o@ z!|cLY3ZA{7EUtJW8+MYc&R=keXgfV%{q`*7EFp$4PhCPD({nk$ajd!>@MnTX*Bdm! zuruPw`;%1w5B&u25DM##`J+I`9Rtvzh@tJ3vfM69O2u9ioggjM%%-D)WopnxxFOCl z6##_5s#Q^@l|%fC8vyRvMSz;vEwtzpce!O}faDrhfypG8M%}~Pk3NY5^3?AV5%b_j z=Cb!4d&Z!<4z&8bR`}>DD01iz?Sa5vl%zp6HANm_&jlP3>5@>pM7+tb*?Rp0qmNwa zE!GJb&wz@pRK-LWL+kyAt~IK#2F?o~R?(Ah{`BTDx7aG<)ih7+D5meaF>?bQFPwd# z`548wtds|_4x8n!bj##Yww_&jIbh6>7h(_evY@eIe1C^c*{{CS2(QxP!u=z!M}H0R zVHk_csI?x=d#w1x+Va0J+PWWkQ+cLz9A^B}5o`as6VAonVkEM-3bLJCbqFfRWL!FD zPHT(7UDpe?YY5{rdcPvkPa;4UJ+}%sZkm()40thCtp%p=ddlJVmj?@s?o3A*q%udB z`*a`!VS)(sjneGa$?g~&Vs5+$O6O;^a_#MY4`CQE@yqN`^2v6O+i<+Aa=t3vOE05N z_2(T5Mn+yiR2Y7b`LO-7?h2slLnSzH3i^O*R2jcGYE0r5F41@k+G;bYzQ+aGMmW|4 zGtxgPgD~-FLgK0DBOEXGXu`e@pyQFSQE4k$E!{RQoZ(~n%SClU|0|o;#Ncc7{T}fx zW#no0ZFm|4QDz*3Ut8F#9ymd@6g2@vEJYrwYIA2b7H=yvU)+nxgN7w!kesKAnF)$Q2-)&3CtLG zQ7l7&t0Y8kCAlb6Z-cVJ!P1LH6JNZYbQiXCH&byh_eCYyBNR%`Hqze>zB96$TyIE= zQ!%I@|NNC3;6Hq)L6nNSKRZA@X{3~X>CYZLA*X*lA>Fd~;fEw36)kLI`hDD~o0b!@ zq)l_L#SxHL8n*Q4ZOmq`^;Um{&n&Y^ScP5q$ZmIY9~-g_x1EJYP}=KtNh@G&-NNn- z=N92NK~Zg{ZG!!)(Pv0z1@} *R+X13ikrerMU#H~I|XxnB6X-uFaW+i#>+{cr** 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 diff --git a/scripts/automation/Radius/images/expireCertFromWindow.png b/scripts/automation/Radius/images/expireCertFromWindow.png new file mode 100644 index 0000000000000000000000000000000000000000..e4fcca051cd39424579804042c351bb57c4c86e8 GIT binary patch literal 81227 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%0Af*JKi_ zPHRQzkln ztpRbPQFM#qwF84--wtLq_z=Q4`H*3bh>LIAoSP-v-E5@`|77$PB%80m{R-K zb_qgOSL3UO@V(i$lUxq7FQDJd%apv51>KlMQi)8?48%Q!A;&>{IqlJd?=c^WoV~yq zB<(55e+GGGZX&}XF#V>iYDQa!j=+TfGCRDgA6a(jUUG)rv2KE5^|ta!e=)q;#rXiz z6yUS=3qQ@R(7*RGzYcf0q*Yz0z}9dzhUWgTVFr~PR>Ap3e0I{z(;~B1qoDb|Ayl*2b;o|?T5A1 zMEwJl6^F>CT}P=Z4B%o`SM$2y>cM)ge&wkKRc0kidcp!SmZSXFYzm`Y$EmcybELWM#8PIq*GP1C zFm(1K_G(KW{vNRWQCU?cgGk@GQ+1}=RZR!X3Xit%JM?CXqJ`|!PiQ>ED?=xFm1V)H zUTt~0zcF~D$zvW+7TU8Ao;PVg$`GmIj|u_eO^KMY))#9rIHW^!lV0M;iW((CF3{Os zB!`vhQi2>of(Km&e&r9zQRWNt?!ZBp3t1kjgoDM zqKA~E!EsbG*8+APvBJB`;1ADprgUPg#x8k2G!Vn`jK3Zh4S{e~@;<)9tWFqOsrD=z zIPq;xu-Y5(h%DZicG_rX%At(utuSW!wKa-Pq(o5aHrFxoZt92V8zccg_34qw(~n>z zNVB);7uv0}>sGB#l*^YD(@yJrBsr}^Ro9s^0Maw)|6CpV0YRI znnY4u2lgkXen6PgnIMOUMht@rIlCiUCWr(_+(+Mju%bj$kC&}gr80U`dv~EqO-1!Yf*{I{ymD_`aE7T`Pk)kfpZZs zRgpYKiS=;%cUU5OzZjK3#n0lh86ryC{er-MW9g3XO9nsEkG@sL?l)Ijwvl^I+%U`Z zvy1(O;Z%iu3TveL8lLI{S$SP9Ytc`#@hD#{R}YO|k;p7#6f)t&a1!AfgQt{4B7}&T z3N6p(XKDd%F!OrxBa&6j+iG$Gp&_g7u&Wn1-tXH)Fml#;Xs>>KqWU?*-K$&eTwq$k zqllBHwS){Py$_Jh-zLbq!alUS9&$6b&zv4QKSgX2^4jd=g(?kv;A`e5SX!~Y+Fz>& z&GbOEQ2>|*13aqo6)*}?^5Amw0s z{yh64IPW7W+4IA=;#{gGjs81p(jV*i2IF;h3nEfet}kGu-}6a99x6n0=Yn$P*Qc8b zQLYw>-yC^fOfIbVXL4Fkgb0*>8?ExvG`b+YOg%I15-s9ZFbhw4?pi-%GvE=&A)tnTiAa|ffHbA1n_XG&P#mmv zR(NY@{x;1OV*=~u4fC?^Jn>>!P2?-^+rY4tXO(Kn62yDWa>lNKy=7`QoT2zba!US@ zsPxMiL{|@+xt!_PC0$ZR24vaTs0E9ckt5G-b@@Shf6-nl@09n&^I_5kw?Iz_+m%q6 zGLBlK@<`jANw_}SWBPec#zVJTEaiPUC}mmlw-*4LPq);k@1O8tV(KTc%7(ul`htUn z$OJX=H$HC|ryVYN9Iw(FDbfwSuKh`Nhu3u$`y|*d2G`2*A*nZZhSS1_wo@9)AkP^q3KvatjbK=t)bxsPN(Hw0qkr>>!??oCvk&!6oE+ zN8Nn895xmd#n%R5Wg9ggz#87I9J|53hXk(g zndrzV-n9`0(lTbDv)89#59^*1@PF1Yz3f^Iw*XwyX zILE|mREC0HnAjY@7YVv!TNk)~OcawBQ{z@`IeXx+{D3a)#sEuT5dO1&r|K@<1ca&H zo-$YK(&)`pt)_A4;0u8EyxXgv67EmP_HfczSV&BhZx=3+D-5+r2xC;AbBW2~xbeJe zcPd&Ru+K!ey+CPsm@&+;oLVR0i`?a)!ZYxI2%=qShDr3XM*AiU9enFw9we81#kk}y z#Lod)5_S0bXz(rKdifc1N3yu1PZc3(eo(RKb{J6`f7a{zg1{2zf6!&IHBvZ<(v*?6 zMSzr*da;8iu35*>T!vw{xSC<6{6{a~SvS|@kjkv*>@G6SHMHH#wIARa6*VjOY7W#^ z>!qP0XiJo9<-&I!8ji7gp`|iX>_7#a7WcrvH;0=2)|9xahO3%;X!zY zikekr1Kw?sG+90*_I`!^#f~S$Ii272@V#n<>?_-@^*H`%0?>TFBL7w6q>SKir1&kF zA5y$X?SeS&DE1TkTMZ`-e!XvI5j-0WW;KP*apQP5r#mb!%oLAR9R9~TeuKZ?13n0& z;_e_792b_hNG* z$izF`VTzR9o{A9AvpSV$^9XgFZhK5Rm6pukWT!y|4qUFF4f!`-VejWJUCNZNzlXcgOW9b{r$Q;BhKJ;d_hCiN4@dC!d2{%JEE^=}-)5SBMeW=TuHw}AXuMR9p z-H2Mfpi^FY^{HnAHa_LCFjDT}N_WMXycU*M6zZ5bkA%d8*}8j_-c{JX&H?2vfi7n@ zm~-;=Z#t6uc}4U0*_-LCx(uMONlHwO0^_F)^Z9rPfhH^NHx_3X?3>U)*|E()S~{me z;-J(jv+Xjqucl(XNwk`wJi(JTS^6glxyTpP-xaRrq%iV`5=wuqwus49p`-SQvaqDe z?D$@%;zeE$a+HD$2tOObr{~pnj-*m0A}xo5OU2XOgmjzTBu_^Nnwb|A2aYXkFOdgy z3ty!2dxzU~>YZ86yHT<8pK%kmR{CaQ1{Mr2(iT{h^v1okeIVg0^l1Vnpxd%u(*Zdd zQn*XSJsOQrzqwP}6-(cHpSWUMa?tXKGRs$f`+A%;>1x>?+UlKJR(Zxi2JXC=>l`Wy zn$HW_TLjm%XqRAUY&H9JD%>^qkXfqEv>+|rEZZ$^Fu@p_MZcpz3Kdb1M2T(CiE>@0 z*Zd^4x$&?pcK>y^`rO8Dfn*V{(wzQPR;VC0Ha zVVq6#W|qS5^aN-+X~m>NQb>q4x0gV=vW;2K5favFW?ut=049H;Ojf%d z*6A!1PaqYY&c8LzgUzo-RTtPU1RVM}a37xFK|UK$yhQ@`jdn8B@^w5=S#DnRdR?NQqMWn(Ui*!%X6dlh_)K3ueW_>-v+B!+jzSk0 zdG;|N!4SKQ62f{`sk@|cFqcInlvOr*2okh0p%lFJR&ROLwi${>Vl(okfD_S75wmgk7GVPjpfVjkQLsk8)0WVZ?M~K05c1>J{*!0(+g>~4L^GFU^7^IT0*5JUFwYZ2 z7$!1%!MvTXoU1ckgVa4&jVFX4D+hbGbylsZS4huPWxHho%8CkLxO!r;95FJLAH_NnbPb{!k0ug3X<<$9y>W;<^E(TLK33 zQ|g{^<`Y7Oi{?mV7p{F9W3@xE2T#GYIjU4Ztr|)*hZtF(&yhf=>`g9lVq+-94SE|5R-|_(%tIhmk<1WPHH3^f(jR zVmYz!b*+*5v`y@7{9CKD^%FIJ^-U4ByrGd+vWN|#1%|XnUtT%nl31)5=c9Yc*&B{x z8rMioZ~lmLW`nixz=1fMrA&^sqXdk51um z9lPn;arOmiR)J#kn>8p$u9e`7>Y)G92Ypz+Ad=;g=E7)2#Ei#i0dA*c%0xu17at#$}zI@ZF85+FgC^A%0i7}j1 zG6^^|Fd~022Fc|GMUu z*;&2I*Po{XST4FU4EyZBLyFJ;t&Z2X1sdj=az$)Zz)$zcHGh3Tl8$g{S;uM!op5_uZ0hC~g4x8db+V8l;_ce;2Wb~CYP0<;I3N}dF_4iHuy8C$AegZ z^*(m&YYv(UR2X{g0ESVBg=ai9K3FT3L}4y#Jl1@et9EVZNczQD;w2k<|J5|tYsv70 z*qL*3BD2D%kLKZRz#s@Hm)F8RB7!V7uvJ^0f5pgi-OsC0g=B|ozgBhs7E^n|ZDsff zvYJ&2C_9osUG7e39`8Se$qr%+vE^k+-2KU`?>k!Gex+3_(D{TU?*fj@C5%j|=;AiY zsck3&KhqDkP>G61tKaN~D^^`=1NeB&x2DmTGIjde4$1JMvSx^B;e|L*$Jtd#kt5@q zPv50kE_fxkj$fO^x2B#+=xSzr-S@7^z--!%Yl8w+M)QfQzJ#x$PJla2$dLgsCl19rd6T(6`L4ZmeLuj5&Zr zu2hx|Gz9$cV%)9T6@kcu4tnN&V^ILTs)!Bs*lY1O~SX7dg)f`I6a|(_O z%V@uJ|KYy)(DgfvT$*u+Oi<)3ZC?fr@hp?||6uK{!=mcfK3+v6q@iHj zMnDBY8l-E6Mx?tz3F+<_Qb1a|q`Mmim^g!bKhJ*NectPwbDeYk9OhbUSnJpK{kgy2 z#6#NohF5%+FMAM*9kVh8V>xb!Ujt(6riCg^Ad|=?MtEyuULY+jWkboDl-?L0{u zZbI~H*7y2eXyUpE&lHb%Ui*9-3%`zz(LS~}ZseeF}4_Y z2>(Qd#Rc`Y@`vLV0>@GIGq1kl32c6$7+`_PT8`&EQ9GNq2IQX0UnsnrS?5awYIPAc8nS;Rf{AplS`h@Y&+*a6$X=0mx#z9$@?1HSY z=h>}(1oEu|h^6&NGZr|N_Ek0~bv5^4G9bZrT#p13Gb=L^0P##s0I$p_L6~^nneT<Ar0l|VUXU?3f~}eY!qD?p8)&#cb}SLe$=lOv`%#W>AQ&_%XPZO-2enWBHdI1}7g|h8yHN3J|N9n?&gWg+ZTVR} zj^?$aZmqAHl;*i*gcLnqO0%aMgx!Go1{v75&MrqCJdBRYR3`i5`5)nY6zg9y4EUZ1 z1@*gqRMj0Z$_*$M?%&5Nzvhf5BU&*nImEm8@OUDaIUqd4=kb0VCT~mE;FA)z`96i! z;JyL`CEBcAavule^ADhS%1XjTv{Q~l?H4Y6P|ri&A4yb?_)8Zj?cf(<{d~#NzLz|$ zriZ>yhE~}VAMt_c?dMqe8$y7n=B4G??2InNAx=ph;RT-N;ao; zgc20I3A{dGByV={p#n?X)a>xKfGBp<`v)A%Nq?wEjLy^6O0L`i{sZ>{FumK zXg0`viFGC&(?b9R5A*FeVpujr9ob4kSi?4TRG}$N5k677`0j(0Yd3P3#K|d=hA(3^ zH>|gKe$o%@*$yv7ij{tlFAR4$=#;Zi)+bQBcks*A{A2maY}bc*!R+@J)>xZ$%Jss6 z-{tHF&SJkc;TO#`=Dm${G^tWDdiUDHVFP1JfxPoTlL}|2cGAoC*{3xb&RVs%eIjhW z%+awD9U~B!%pg`54+W9)Opp0{mzxi3V15@2mVpG_aDZg&MM(eIrpZ>LY$(LFH#AWL ze(+tP$KjXL<+4T+|5ulLx3UxVC)<6_sUjv{DlIOkAmUYBBzt&FYW;5PdS@sdn@lG@ zwbNTdI%qI_ z*CMfP(&Nl^TCMon7Sn{4J~gdo|5)|${G_t7N`q6s-w)Q>>j4S4HaMkiaTa@V|@x4HL>f2Y-s7{Vkv zw(M$+Zhcn5Zc==*pp4*UGz{ui^yi{HIl()Dwv=kSH9*GWFyzF`)8ad?ugF=Cvehz?frHErQwbYU>UmS(nybYa;^QU>cmByyW zFD--e`BXV=tu^X#ShHnG1)ksyBifNx4rISI*7AzvAm< z%SYX?`p)jSYl|tM`No@#h{1|6!uMSmRs%s+qJzZgmMU{waVMs+z?#ItpYs>v#NHRX zeixWhm@mSXuG6qe$&?m1dC#6U%?&M?oQ)5|n)=gvLuO{FBOL;&d4H+8I%A)j!9Jtg z0bTpuUdFDD+Q5%dZ z?q)vZJddRaPDyC-fFr65R2S}#W z@+irO9yD3=ct~)(m>9(`ToK|tBU0~$WaiLJJ7BIq%;PVm{?#g19CYREp7e@astQm3 zA|%Cc;e_+-Q=0)QkGymVB)_2ryT4I8fVtTh-&XT(WqB>0Qtpv$x^1Z$Ta>H@%1B+~ z>sDV%2mGABTW{;=Jg;1*)F96MgNEK3h{{*uGXyV~9Uq|~SY#*4d8*0xC~|+d>H#xS ztnBtthZ2xg)fsf^@TM+9L;@8X-@G8WoFk7D;ei+}?I+q#@l{_uxU5`^w2!w9Ms4p{ zkge3U9e4SI!=9UuTK3H=ItOu33Lg__HZr_bZ9mea8sXO4(bEd?BDyGf++dBxhVhKj z%KKaz&ckpR*+0|DT)f>G6Bl3Y0NU~>1LNPuHi0h6pvNH@ge{|plm3ulpnugCea5|o zse-R7`oX$K8={LC@4h}5m=>orvm6O^IFQUdh~;9HyZ$XDNh+$ZTDW;pcp2xmpVKk8 z3apr|ca9Kce2Aa5+($(MIpQltfH0IUoCrUj2CT(KFCp@*Jpxwl&GVR$T^3rzMO9UC z0ArX(=((mGerQEF+VB;K$W=NU4A2`wM2c2_yUCyQE*W-~;n%sSGv8XQPw$W3L@UOA z^vfEYF7dXhp;@Yuara+5EjE2E`@Onp^=D!GV9KnM3uRMBW)^RTDEGdPJ!{qn$*l;r z!b!uC>RnIUN4zF4%rL`ERmCDrEALLEX338My{6m45-~14DFb2hK58zRhaJO9o)vKw zvSR(X*n+t0&=+rh^WL0#6%9D%Vm~;37+{(2t)IDgA!=VU=m96T8eViAsJCZ9taX8W zs;VW$uc>R*5-oJV&1)i0c_L%=aJn-}#?nw}yyd7f1xO0A3L<|{p9)sAp9EInLiSW$ zado46O+$NXgM@K%>!R5PPAn&-?0ciMQNfC-VExi8owIrT*16N)N8~3lwhO(jyy-qG zvSSi8t@V7U`c)2sG#-r;}ELE_hGE0|oz#<7>i z!GGo+Y5G?&RVX_f-UN^6-I&lQlSk`orteiV$P}0*6U0WpEu@GWod<3N zE9ad^GQ8I*PJPLDY&_16L#VDsufyn~H0;N^VDt)p4pK5=28uge^SQ(wX{U0o;l#t? zsw2H`jHH~V8atT7PQ}0}qmZW5nvo^wQpEMq$aM%$fP#+A?z^)e8K`Va+KDME_)6^h zW|rF9vCdvm&$!9uAXPL|wcKuTy*w$)E4l5=jaTh<|L8i8`{mIh`9_zlLTS|1@ll*{ z?+xEz+C|^+RjA1IZAA(PXOOQ6p*n7r$%=LgC;A)0k9-BZ33fVaADNeFc1V{RJ=0tx zGENJph>e0Qv_8_ToA&8y=sb}c*|JylcK)gm*{Zo&^5P#cb7(o^^1)Rfm{er)FogJi z-g~&iWwF(y&zF1qrVQ`}J|CW}wHsiRHMaH+Zi+uN{IJX>fQHj%mE8NL^nIkHye9KM zqVCV{?iD(yU_^>W@BjsAKeF{vH)g4-=eDjCCz|(Mn6mf)W#p^?(^NMWdidtFT0#JeR%c@u9SOZ?vDj zthXAOzZ+$Zm6VifTyF6VAg;7Vt=_Y&*Gr`;#^8+)R-+}@k|OKOb1ff=*`|nL=VSAw zUT@Xu6T;b4L!#*NICaZrUR`E|=A#(t;{UI#de_f`xti=7mtD=eXqy_Z5#yVkB$I8F zgW&n^b_;hIUS9;lP3mB_S$944iI=PA=}Bvi^$6XK1p;lXY-H~JwC!)jn6Si;0h+(J z`r<+`&b@w9f;RW{4It*U2lMq&v35(Y!E2j&9lYv~?fHihYibroon4(fp;D4^5-`X! z*Huy5nTC|eEYTH}`Eq-}qk*L9Yc6|G@+on5clW3OJS!Vz7WXTg`DJ(UETWQh@HTi=ON64L%&5-$o6;i zU1C=ak4E<7`um0sv?BIqtJKpKbw5DBWazE=Dy<+P}bcQDXukys1EhnuKbt_0(hB z^KUYkmc3;QJPB3GM?|w-nBo^#d@rMN3~QIxaBW$(|Bc0ci9oT~)x}z6Dwait&1ClL zczIv0c>FlIa3OAJZ_DXeB6aV5T=M`ug*H!b(mn(-iF%!zT??Hf zsxCA|<(8V+YN1HLse*@6#>5Lb?e1NP^H3*o{)9C=iivKUz-}s1E3skUT?aNe-l#{{M}%qY}n< zNW1o;$}ux4GmJ>jYPAXLfBE;MixfhCLt)1E%^q@_NgR-jVt3`roO&U=H=pqxfoAUtVFNND>H$r)a?qXak2kR5 zr!^jDWG6NkasSV_c6K~_D<)=^%Jm`^<|^0r+`iY_TRTW%Im0{!rfP)!Yzw8!(OAl( z3Nkb%c@(vnU*2{dS!c5#?|NMMx|LEMl_o<~Nbd}&0lxvahMbhU%$}(`!FQ|GcFMuX zyp%QsO8{Q^Tf5*>8g`>9^t=xklum2ayb9y0)bIe6h+|sG6>2K}kI_3&G*AxkHW01h z%`aF`jG)&rV3iKo&dOyk)j_i2v6~y(@M~gcJP>`1Lvl5>wp^gN_of;NF#?p#*;|09 zE(oT8Zhbt*v08!CD&fop#HQT$hTcxT>!8;yWXuWOBlhtewYG<>5p51&4!*J}kmoT{ zl}xc58KGv0r4lerYfr^Qp3eCFXhgUEp_pJ#xkXm2CnNry?PVCis|KgmYSdr5JHKzb zuqk*0hj>TWZejJeXm=|7tY)X~>~yn&#t%DmhD*H9o#5#?R_t)H|%)D2qc zc@+N9(Fq2{p6eZ?oS=yKi3R*oAGavE^f$=vqIgO`C&83S$YL9_v>t&|gCw%Id;UK_ z_QtKKE!|+;@2Fy&3ZJv5VqK_s#!M2gp?tyHRR}4f`w3%U{P#!2JY{+idBv|;D|mRK zoK^iF>8OWISQ2WG>-qwo2#p+hW>7@5=mqp)mg(SDx7yc z%W-?2Nj*0xvmsR9l;2UF*7Fsi)k^^&oq5rVqgMXlH;n534k*eM{gTJk9y zYw_+G@C|LV| zD2~XIPC#j`^~Qg$*6zPpcKXqH6vo_B-Spx#0%>j*2pF4DI?8m@ny@s#VSUYP{+3$A z=K0y|PWTkYGx6dQ5wpH$xw{LVmt?y`V=R>}17wL8L314G)BENjk`FMXc!jxo!f(P+ zeydzX{)nnhlc`4JSMoq7TG2_5R7x{%S^4U5y5^mjC@-Xp*K$OC2zJ71d;9Z8!z8l0 zdXwbTEP2n6*Eo!?PEK+WRkZTDYV*@5bI-X}C)WAE#CIbjmO<+!?}JXy`z&%~Df(xJ z!^22X4vR9P5Go;0T)3E5*l~@IC-iU*((B;;jjv$PRJBtg+w``My@=CR zj=CzI&-S_RHi0eEza#vIVEbvz>Ps&%;KhwXSl+GAW%Of0=LGFsW~B?SL2@NMFTV2I z^WBcULlw&XPqa{HaMD)w7HZkcQ84NZr%OGVLuZXbHJ7OxQXl2&om2C!i-izER%{S3 zRWkp4zRsR}cUiBxqHJfh`(=_WpI~wev*HpfTr?qb;{t0=1lpFccQ4@(kDB)4tPZ#O zWG(D^#}=;T`43vCgpkiHnp(I(O=jkjbN8H*>iUG0XK?Q?q}`}QMp94gLE4Em6FhN0 zP%h0CWR^QFj5ny!p+GZML|+#Lx4xu}YEXR8W5;EZRGC4&KgSYDEhDZUi&k5YLUD;E z8l`oP)9%N_9_wRZi0tM*Wg@;C$t#}Z)fQ##jul{jLnvr>)Bwc1y%;M9&D8>7xMUl6n{&nA*xq&e; zvI*X_T3fZp8p`s6lBDUpwC{yfx%1WtI38=2-HB%0ewnnR9d1GxRYVV)G_))53FS5I z<6;mU{}=Brm_UH&87VM`p7C2U+bx z*^)|@Pk#GA6atUUCU;Tx?#(}@y*^tsn@3-o-J`L7IK~e>){#5_3;aNm4W*4a7{6YI z%;N{(+Rxlit!@jLeO;f3(gZ5sw4k?t+?7b{9D|f!J4$8z$0+j-dHx845~a)4AL;u3 zec7*e@{lU@XB#0(f8pF$AAvYO9a_aBN4 zrQXx23CruQ2zY4RPOO)}eV`bHf|EozyW-A0;U*=Dg#Mn0{bx=YkuG9&wn^EFPrN1A zfpXu4(MnE7exgz`%d2{{?v99v`TG`1^y%GJA(2(K?u2!;rG014@7CYPp!+=JL@Pe6 zz1~~lTtdaYmu}Dg*;3|k(?DR2@YjUb)doU|!x*21YCN?uWjy26mp*T;&IXgs+55)@#>isB086@|F= z&RAoPz?NKCA{K8W7S^T!8xE8BR^NVIz@f@=2(`CFz)2A8{15CkJ#D+E4=W~WK2)=i zG@7NnO_j$7^o&I@A->MNV9>ADczU`2h;(ARdNdmqXNMHl8NpiEA6Tw@#I%ZjTk}=b zBXgw0c(C{D@&g)tcY2}TXMG`&G=z(-G3KLVc@4=#!^A<#Vl)h|QlPX8U&2VY3i@ST zkCPlP$`-$ekluLr~$=0NZ{n z_G{yrWW40+|Mc zJE7xQV(EJy+NUVnB*0pbjBJebY1AyV@y8je;_J0x=5LCr;6kr|jO7-YyAVjU)PuaW z49CYb(kpFD-mosY{MVcpU!Yy`py%aywEf{4wE50w znku+2n{s4bMg4Pp=BI`Uk{)2sN+#H2!CCGhfB^&r5>O7A%XK#uX2peXfDIt2gNOZ@zLyYhukxdh-WkYFT^RSnzgg-2o~ zDPp@`v@DBB)cJwZt>XV%0G^!HUH-!p8r7iS2nr?MHYj85O3v^xU@U6deYp^UxSG6qJ8@U< zSp;TE^wfohL7msJDjfrdo+MYdJcIdU_Oi+Wln(CY8nwO;5FJb=rNOdwi&j<`$6J<{~ z_l;=d(y#uE_hYWa$L|&T3M7BSLo+(}|@?J*MuUi#s$JEeYU<&(i&SkX0BWYZ8q(`gkfPTF4%S{T&GnoUR-W8--Y2D0Iq~9Rx14GqyzsEP zC;dS)mJ-1qIXjtluAYEPfRn$y{W1RxP+fJt=<&MM7kgI@>dQVt~+DQmCrWldWWGNzqazd(k83_4o}mupwsDL zT)*25ye;Gw1r%xD3Tz;qU+@lgS;{}znGieH^WMgOWizSLnuT=rW$Dj#dif3M@ zj}>njja1fo9u59nW_jSX-8zI^weO+&q27^6|M+oqFuw!1+o&0lC2l`WoUTc%YY)n0 z0VJ@vVEx8{p;C~V(_=`xB1{i>m4MVZrg?4+<=AUjL|S}u^!C`O>sT|BI>Db^t=rt=o+)5W`g|vsJD7Pnk2GNFou7`S z)Ewq7S*{cur4N-LZS0FKpgF@^Pq}Y?C2XG!TNGanf5t$O+Xz~tXFiGF&%L_od%6JO zul!r`zKk|@^=_LFYL`>dXtc4oz^y2Tyd6akOj?I^j9a;ubo>b3GUZ(ReP@HgmcE%o zZ>yc{r$xY12cW+Bq&1+>UQ(N5#kf@Q2_=kW`&#a-cKzDC6vdGo`WkiU?#(p3QXP4DG39gmZI{+;d z3>`CQHJHowC-RDDAIJpf*v^MIM^izz$QnXF*zNO0| z%+3EafnC}M3=LNnbO^V!H^^jZuMGTIoYCT~-;Vz1)B%?IpG>5>U)oW=%p%fE6O7#fAw}YPSIhZwnGyy~@QrOPw8*h6(g-K7!7? zF#FHk<<}gE(mdLGZu~#dbhr1`=ZCWO`pnP}V;Oxkw|LJ;q;CpJeWXTJ`=)H3-U7yx z)vA=2I-OHKf0@H+G5oB{k=W}1!$+q+?B&b?=(%CCcJ2>Z-P+=2?Wliq>G^DjV}$~N z$nAhtJS$I_lpI=|Z+MD7+vO%Vqt3=u4k|IcO?IVirL;a$WNVHd5rpgKN3usYQUZ>~ zpdwHwR!b8ujeg8@Y#ty!%f2KTaXDK8SAX~_JYrn_Y6q2XKOkAR^^AWv1=2@(YQ!Ct zmdWo#Y=&rq=B`{(GJU_?R*%EKa zWw1ZVMZ~Jj+upgBDhpMxo->f2V^mXJe`y|zS=-IN1T42!{wM7imNiZ(jF4G$P-(cxF*9mXgMg9SR6kP-dTcA##y^%pl-`&wg0u@$%!^Akb*__Lf26n}etK z>DC>j*96+(=cW}1s)He-*o0~4t%cmnc(u2Gh4WWFf=^bHf9y-{Q@!vkZn}7j`}Qn> zM+W1r6vW7sdbiue=P0)IzhQH{A-Pd6g3*b5;|+qIdtC_jg%UcU+cpciMM{B(V;z&! zjp3Rk6f`Ontl|_et53TI64zV3{rEqOu%wQRpB!CDoc z^qU;79~nuw8syfWndZaVxjKRm>R$)zdoJtdBTY%JH<-k#T*H3i0}3Fm*Pnr(>_@{p zx+o3z5Yu~YXP8j}z!X_SbELHxVj9jLay~YI2$2sOg;fM(W!$8#=+zpE(-9xATXZ=1 z$Fp0FNIwNd+5YBh8HvFd*EcpV+fdZ*f0n_p~Y2Sm_M#$He(k^gZFyEcE zP?oQ}`@o1E5Z-t@60)|veko~O3tl0QE0`U4z%^}AeG-bm#>UB$=*9_a!cTsx`bHPY z_d40(8+h%;} z_sB8D#F2dm(2)mVmKb@=nu}jA89{LWaKJQg$iT`Bw3G6svx9 z@$#v&`4VBjcEi1w9SWL?t?C-8db3!Mxb=VNvn2nh;P0NT6Z=8=-OXtYRbiyf)>bj( z3I_B5ND6deL!yXn)ofxzx6$hWg~I3En&{>icCT4_MB*ut4+c=?IEjN3n+xRZ3VwVV z5yD*!rxACh~PTo`DN<~9}y&N;6_ z4sC|e!uBVYAfUOeRXpn|nJmK^&r`0so>q!a5=8PoETBPLClDbwZ#kYGHI7a(eUB=U zz-WZK4EC_^ehDh^cYZk3QyyL(()MGd1nzpXgbR+h7j8XXw1St67FX+m)od1C4fG(n z__*})xMB@rd5>+cH=WM0lH3lLpCa`R)hWx5Vyo!&W|ZR9U_5XLKGvPJ3Ry z`2=#0d`8N>Ymq<)-Wr_SIzph7r9NX60sd?%m!F`@BlR&A72kUO2YKhtT|c-(-W^Zs z_2iPU#j`RW(|B6$?53PKAO3PAjHdwbQm{(2t4{ND%VvC&DHg75aw9z<{Dk<4T{KhK zsMzY!lSMKZ`#bj49#-Kt)Kv{Tn9|lpq5+U#zZ9Pcz71Q**8Cxpp~koNu#kNM#OeI8 z1MvQX9+6~lr+z=SSfy<~n^WHmfL+uJOWX4tKPQ$^28gNhfz`;ueXC6?u4&@KgrQch z`G<~{BQ`bM2*Zh~{p3(JZR=S3G9T7aQO&dsWFqGk(`4Wu!xx1jlbxZe;lehgDSU zzDil?wAQbUtKK*b&Ln-*dhM zIWm4@y~lWD0fDuuQ+9hM#%TNAq4mPy6-urs7M=3d$DDMar~j_xLL3$DZddp_C9yik zw4IwZ6`Qr5k1(sarZvp15c4&1xWyFmCw1CqdFVW)$=uD(!j@^_6D$LERbo03AdctG zWAoZp>b$rW^)T*E)wK2*_>9}<fZQ1=Sa1r?zN zi*IrZ8v#ppQEEg%V$&W~X?&I`$Win;K2sily?|9##?@hwBQh(T6NkB#># zWMYwHwgJBb_ugJ<;vFSMa1E^n^8q0#fH~Kqe+3MdxluKdQ_{!BP2424xA(*+thgrL zejvHNPaR!Chr;Dg{>J5tssF;|6{2~(^-41ayqXEpk_j;a-c-KELHtU4u^}#)U%bUL z)}c)uG(mDr^M`!zhM$;C_U%7BT^Z#RYkHR!-Lvs8V!lUp@P%dXQdkhj*(;rSj4lxH zvFp?bOL1w|?^Z*tLN;+hzacWLOOF8ZCOa~c4?;JJCsQ|=%~uu#w4{XJTroKenynsT zc(8(xr6WiKD-*a)=p0d&LKc@aL`SA6mCY^}scV@7Um&1>l$7Tu;eh51abl*W=T#5* zt$<92J9?V#@6YfpAPkkhva{r7lb~Y${oJK~mV42^yMEj=URx17;=jf6)scC6_#|56 zcHwpjf|Q^|9`2ORSzIg@p{fnalT22uNGY={e)>rI2I`HGE)G6$LJv)Ag#D!jAl9|V zq-d3#Tnc2&jsP^TP16F!4@CCB#YMA}CORJbhBeCJlA^8>-GjeYgbY6lq-8wM>BaX5 zutz+~3z8pvDx4p)hd;&IM?c>|;3Bw-#PzeOAC8~94ybYM0j%!CXv2LfK^=pSYTQm} zoj9QWpY!!N+M)g}mTf3WE6#aP9jrY|Maj?v~=S1d^*te6I?nZnJ{>Pi>aX^!e8v9>I8_b zAQoFWt(-@!cZ&09nbzUg!H;>8==~Im5y}98mG@y^Q~!qRW0`9zX2lvl1)SyJuDD_w z=MVkdg&5B<_0Sc4qqn_fmQwdvgzZTrbI|JNP4iVKh8XV|DK_2^MuY?w&^1cwZfP` z;kRL%uZ>&5e-Bd!AHP(ud-a4vr!9C!B9P^0G8x2ZI3UTz(PYWnkM~A5%IvnJr%eMn z|0=5L;%4L!I$kW^S<-*ieZ$<_n0Y3KicVilS+`xnAx$E8>WP@Pm(HNyoatJPcdgcS zr&x@418pzDb0!mpq^>3Lyy%Vw^3Z<^y3r&OU{@b{*ugTc8(*%4?s5Yaidi@+_fg5e zqN~XelIXWv&55N#A@JV&*{$=jlOM}v#ak@9FkWdQKH*<%QRsjl8hwxlmN*AO>4WE- zzfs{|h*K`@d!~*=VpIp`RM9`}iD7nd#qigfAa3fzH+x>O7g(AujuIM*D)$M_$)K@0 z^E#0w+2GWE(;`-7E?~55q0VAXLf`GifFo~2Ey8AK{>=_EVa7Y0d7H1e)tWtq($KAH z9C&DXo>|(jW_DCdUuq|<~h4tb)^ z|G{bI>d0)%oWA(ddcJTPoEZ*=oX^3A4kM1wauJ@_Tkm=uXZYH0Dm_nC2=g>8Mh58z zrvMQCi_VfB-m&c!i+s2_yj+*uHo5fVLoMjk)7$;P`~=AD_584dWB6DLZ_1p!xYiu%&7*RF;}vUxem1{wWD1nPY9w~9HA+WxEp@Bk6fGNthK*o zAbDKc+riWo7Tc)4{ayYb?g;K7bk~*&H3OTlV^$0f`#+2Ry}Q^AA3pv4KyLp1*UU!m z`nZ_BMHe|=!)*O!cxU!~u@^e>9^mIQ zqr(wVaD)m=Fyg#n*zmvFL^1IFm+rkY@{K9(^%fnE93|`CHM=jR9GDa9^<#$^yJ8m& zRM-d5Zgy;-BxPeKRWYuAY-TW~c{7J$D=3X!>rrI&8OBVuGfevS=f*42zD-a2H+Wyn zqM$k0$F`EEKh=FF{pNgdJrq*HQg4v{IzlvL%I+6}4n9D4cK2SbtU zh43gds_@f^;H#UBt4+_A}!D7+k+?D)ZlyKxE zWELulJ@!bUleXStwupUN4Slp+qj0nNULmgEfbCHJh(YvKebR$NNgX3RU(n zN~l^4W{{oYt@U7F_#CE?cku~364Z1R;a#U}Kh=5w=BYvHtxPTrLo39AW?uhRD2L*p zPTH(b!lK9udYOMLXizTHn~@rcev17>Hhd&Oa6aEi;MfuwP7u`_E1g(3?4A!vax5)-yB=jf%OJsQe|-T++ICu^>>eC4 z?6f1P%Td3$`2)buy<2LEqZJ`F3zfeE#sw+rW}?~VOEDwE&;nY(I|hr-Md>3o{NdR( z*W8mWQj?(5NVpJ6hq7X+-WiX%YMn9=_}w8;Gs4m8U$8w@FNIQ-C+}LLNuH-Woq?{vfryT7Q;}Tr}fSG2C~|X?pVkOd=D8)`u+!9Whmq- z5~5&D_APJHZ)$t&bXuoYzEE3BBad6VG+Ulf$m)pul8oUq(~C>wl{oKJ?!_^EuVrV2Oo3Fry{)Km zYvi)pGPt(xD^JXXwZ=fnU1=W!u=w9KT2?T*d7i%3E4>-FMtYt^NB|6A;qfLs<*@Wi z*rJ#%pQKAe#=((3-l0RP9n?uJEL7B5v-!^uYABQIH0T!`{_J)X$ zk-$%$u=8CLUa5_5I$-%ZK)1*^V(8Lj`b6evcQo!Bp21o9TwX17G^FxDXph9uPqnI; z@mRHTA7v!CM@~UT!m4pr90D~sm5jQ1dgDDa&^@i?behYlLu1X`?#1hTK#V@ESF9fVrr|;&_3R4g@#^2ys?GnbHHL5Jy%WBB*y|1% zOpA$&e(NK99Z>a2?Rq`0wQ`H_Yfx3`vdz5E9CG`qlf~`i!2IXC{;~_)$ytN%)rXtK zF@@{5;u#+>aMj245X~yEMObxCj0C-DGLK72xs<_92}L=gD!ur)aTJTd5%Tz`6p}jL zZ^EtmrKy}Dt9h??@GK^q;?uii)bmWDyZfdW74urISo)9*;nTcnCA-pxaa-PNUDH8Z zk-l=!x%p;z_Dfl!@obud`Zmu8&HNT%*EVPSgUiH#BbMEo?4xJ}YngN;kuZm_S_-f0 zvtPq0KBzbK77f+6T13(Hx^KD*trO!>{fwm@9rcrczWDP&5R0jH^($dA@gZzd?t4}ie;@0^5CS>WIzNG*u?npx0udP%6bFVy+^SuR}o7GC=1u>|9D&65C1aowB z^z^N276F~$}APDxH8on-^+dWK4qG86ks$_Cg_3-qiZRMYzRib zj(C98fiV0e*$lxo5_buF5Q5KXN1MCFo7biAj~B;8?#DDPVBhu>i)x}gg!g;qFD%Ro z6PP`&H_(FYmrXe5o3upkudhQce>G^xY)`b(hTl31+UUVqYit$=N&A5Hf@2SZ*8X_7MDW_(1MT@8u%&P000vDyLy(LYCq`ia8W8%2!0NU7cf zFXpqr9ZAHj9fv+%w*EWa_1OE%K{2P|$+=w}7wc}K(wL0#9QtSIHtU6-Cj=yAj*BSrS0o%`*qC2Fj>zAmplgi!%=c{FYkv`4#aPl+$jPmVz zkpDqI@bZMF5kzxBG3Vw%+2Z8UWsN}mYE}@TIN!ZD(`4%vYo-{KG7)20H?ip9@gb-; z>dAq9m^A@SyLtC){^wqlu$ykU+4APuvc*@5(4xW}5(~%hA4#2}114X}U2_TeMhk=< z1G2u7q1WstF^(#v%`}K=Q(mmTrm{NKnA74HNF|m(&F3$8ru#ELu`cG8lHcs!jik?w z*3$;U>j@|a3tKZK9UVTXbgut!CX?*)M0zOVZL*|AS*gNmg0c6b@64znZc+}`BvE#K ztp*CSYC@bHslWwoezhAAwXsCJ+pQO%rvrY>WwHn?Pq{=vcJV&~20H&9M-;5lX1-hi z^l|#@-$6LmLN24P_`j5aCtK`E~_s%5XM7jGB+gDM=y z&0~b{zAJ#C_XV%4(Ng%$1AqQh)T;(P8N9KJWB@Dlmy=9k0rTAD54B6w@5{FVAGJW7 zzAVcufrwGQ(=5Yn$)bcj?$;>e!pJgjfqd^ zzw?@n zsn-R3)G=xiM}r64B+ZWWq4ku^=J+DkJUj0P)|fK z>eB_rco^q{4f zD3f^Y=?EohEIjj`eJ1J-lNq9#eD|Y*;W05e4zK z9Gv3h`6?NMAsg2_4W&GiLbylOeMLK2ZjVPW}<6^hIq`X|6 zyIebms=rlt@OfJhK;@?%;WG%7ME+$fOM`G`4JoMS{*{=GiCh?o-?Q<2X$_dKX<%Cn ztPs~~SpWLdMju{x7d(HY->weu))J3q;0_pCxtf&{}S$fs|FF z0NtZs%HvrUP}NIVnDDUfUL5}DYWBCBCG!=dR1)MxB_drZPN~{QlhY;zfZMBs2Qo`k zxU_S%+T7(r_nV0ou6g08DkI+LC7uL}B$4Bs7br*gmBHC+K4`|f)qtNRmP^kM420z9 ztq43XIP)%hqTN<*BB=Qso`cO$e$9*%7FY;!_$BA32%>DSZ zF7gQYNuR=Br9BY57S{wR;n(KmoYvYeP^qSiJw$mq5(Qs;G%xq8E#mt>*gNZ}sQT~U z69R$)f^-NH(jeU}-7Oic`{y=&dI z?tjl(vsf+;p81?}K4*XSXYbd3Z)lqJ+M`Y9wGF1)pQRVqre4PqUEUKG!*9iP%ZM(B z*FrvpU%~{Lv+$QEEQgEh!2RLcKXy_IHis}A5dD$*+cJIK;AkZPu>}hp+e}Seq?vm#FGgF`J7?T)J2bAKH z7_>b?zdP+&Z)|enzkc#Z+MDMiYbM~ZNKxs3yQeo(2C|OB#%{J)&!9x?qkxF(k+8!u zgvc2u4|L03-pdTu`~FtZy4Sxx}nSDfFM_D9IS3X{U^w#KeMn#$v=HAyMz& zKM2&R73oXHCp{E=SM5KZ+VfE01tWHJ#3V<)4>3k3kH8aRcX+zMj}B7pk#5qbW#^Ak z7~qvjQLg~L9(5`#B;OkpuFFBGH}XQA>bHC)XP!!449ZaHGVQG*t&J`$q zjbYD8&0jQcC+2d>b87M27iuIf?g32?-&A|tdnM{bHDC7%En+NLxt2L0$7`m1G~ZH< z=LZ3$NM-y*eMhaM7P9Q;fhH>@;wePCC$O1md z_uevdI%0BL=!0lZY1zeldTl=P$MGAv`U4zay+en%kbew{T|Dq-?e5FXapLyzDKxqqaprWmNd}<=upkbvxgVzkjX$s zo~%l60?yi_^wRBj+bCJt!2)S|og5xc+Fm zIQU>RRRZYzSG)~w=|e*kT_~B`EplAD?`J|q;jPE3Q*~eY`sD;G(Kx=86d@nh0>MCmXEFuCZ@7Uo|7ntmx~q9W>6&%uSl(W>6qdmAPU`V=vGm z9Pv!haBo>H&*^fOvuQG(qhf9TNRwVs!e|}7k+ldz+EK_f!R()fk?L->AOOjF7|utDs}D*Y-ZdG#`VHT<+X+wp!Z!!9fXaJ2cJ?I`=qrLm1R5cs%|CJ%THFwBBx`tIYNrNSLdn` zZF8{wlgc)}Sh%%;t^|5rpa0zO%ce3^GaF*rMa4zE$7wahA7j&(BA-fo=oF`*qhRl~)yh zZs+0$zVI|28@kVkMo-?KOHJVPiQYIsmLt@4bI$oW4@sZttnlkOyCFFxvmb+L9BU-U z_2@Oif=;K&+smyK{6yDLB*xL|kDi4cO51@g763C3agyH}4|%bZ8uiD%N1~s>h+87k zodQ|S;i&>D$G0)%N##Y`cvc)GRZZJ`TWI4(g)3bGSYcm}&EP(crU{|82otOU6Wq~D zE4L=JE;~Kf8QcLDgW-^(aeD9?%`PjS?SS;d2$%$`OXD1T&bH1x*P&G(5hcj>4_AT1^} zwOu}arW&Gwi-Ry;rF$6&wQUX!(d^F21L7Tn|$_jG;aisq-pK**c|zeC|)h-3}3Gx88wDcfVGHP5dMYPk-gX74|KafT3)&v@h{tfE*= zU2zWQjSF!!_Vbk?52P`0hz zts)^a50rsiMia-3AQpHy`9;Qg8*ycnZC^sSWp`EnKI_qpz!kx{r0cO!w*G6&^f}1h z=FY)sQ6>cFHOX2)^ngHu@yz!Y6RQ0yD<2p$ zDDOc02z!s!$tLc@z`!3j?TPXBS#LlGwo^#QE&K0s#;8%V^9{?g(jME>9HIR& zk`?^h%j%ZrpkWM%_#FKSKB*>6ypYi-d5AzLc=KOo<`96To?=`JW6jp-2I zxy3{JLX1DVx8EJgt9G{xxu{3rT>HWA)hS;~a+eB=OhH~>)rs@C*M{&8=-XdQKX-V4 z1{zr=dm52dcuZTlL#xn%Wxn!#)AoptM?@v&w@cQNGXGNL1LZ5j6}1cxTwcDW7(QIg z!tdrjg;|OE(LF<6jve*Bq-I>oev+qjh>Wa7+};Y-)!&>5dyT!_{efk#_rp&?tpD7L z00K3CjJ^*#&p5DMuUv@qJd_9n!RUUh!!3{>!SgM`l17;ZCjN^KMjTe0-qhW)G97Vp zc=ruuL0>(Hfl}SEVERq?QaOBo(?m{4YBH5*DRF~Q(L}*Jd-qbC8rrbr>)my+4t}r%&!%< zjP=pzC||0JOem1VsO>>>QFwfMsrq@!*~BEii~5ni%Dh{{lw|0g){bLH!qgV1;NXJDoZ4BgI$WyzKR*bwod4KFzo$j4uVFD)JX& zi?2}YJ1rG>V-e2t1RM%szxynFk+-l$LWqaC%yaO{d4!Ny_9O}NQUBx)MSE}0T=97f zV&e1NL%-AHrn&Z2%zE5%9LGdo9F_~cXa zZysgNy;sXydbyx9{=9Ev2UDZaWG7rJtm5lPj0ljJVH34^;m>?jc^M-!WL+y>E8uw% zG2Iq(rIN*o>TV`q(Lf=oB9;r?`zA^CbDP#c#J5n>+4C&7R}aXoluWlyZs@U;jQf_)BGZgS*TJZamgcr)n*zismv_4ch!t@G)cRx1e7*yOvogeY;` zy#9UB&BOp=%_ZuuPGw(T`gFUKFT)Cnwi0^D%O45gxN#Wb`C=rM0nszAh`r7^|CfBL z87++Jbp$SO@4WZ$3(IAxBQzz$;ZK%S9Ja$0qo%vhDU7DOh>rwjIS=(_uO}4oSW(ME z_Jj{@)6G>m%GF-aqKEh*7EN9nJ(qnjQcQ@$K)=bSFPF_LaP`| zLF(0e9Sq2H&E9#;doFg;ieJ1RqE|jMxcuZJIlD-;m*llth@_x)4g)cn3o$%JfOro^ za+~KMYdOZcqYQpuJu4R^-Xd{v?Ms96F|#vtH4F9lgj@)z*wr3eUjt1XcHlG1XIrTw znnhC!J+fz1FQ-diuEX8M-bNS1+6amcn{gB&#g)^$$^B-Ds3;*^$G|Xrn_uC_XaSqr z9Vu|~*gk4=du&^t{_!W9EY4dYpQg|gUeiyZeD--Tnz#K~^DRQAsV}M39I3MPKQCBv zbhdF5b<|2TJPXg9=>4F8xSYMD+pjRFMmMq8Z-Wj{A1yCUQ*4`Rsb-;k-k8!*n6?I? zl8THKgPAa|1g6RM3LWUdoDxs!$lEdltGz8H)x?;vEu#SI6_E5KGT4NdrDJ{cX~Ub9 zJ7&G-m4r!xi{0jrfZ5WM)SpM3)yY_^R^+3zGpktZ`*N&xTYvlNOisPo!u-8A0Xl9u znnnzT6X?uR7S6WJ{%-mZciP8yGE*G??QU9OjZRNlsgs+3LP z!bfuK!mYDpt|^Wdm1BD?_P&tjs*OP$G?qlXHrDTg;ZJM@(%oX-syD0h3bdKFCFj^g zq6%>lNwYr|d-RGzI0ChtmA>t5E+XB^K? zqd_V9`U8#0eqD>7F-0^1qaGGU%Gej^La*zYYr%*t7W7eW|8<759yK6;T}b~Fx%nQU z|Fz!hP^W44#unZM8XXp?4_~HMILjp&W*iqCDrJS3MetQ<`ppmci@~#gG7>g(6?S z`Hc?6yB5sj!cJfOnL103WMZfF?AYnL1z|xdh*s!f1h*!s;}cK`Ts|Ue7xv@0nkH~22%Hj}vl?RdIc9J2&J%V=@Nim{)wJNzCeT=YpDqT6%0pY~HeOV^tW6i4e15%?6)ot+cT}Wl zm{-D(W$^=DWWh&>AawP_<#3HR-59vg1wOoy96gieTs?7+HNRMI_3$N54OE@Oc;OPi5oWdVQ0*ByUghE zM?-H>=;BN9;d2B4JN)5S$YTPTNc|ilEA*1X3ovpS|M&XSlvM)jAN$sq+_h`T1!^)_ zr{+s|a_F@i>@d~4i%l*s7em87;Gr^n$#!i&aNr>)b~;1n@u=pw#NNRTa(r_GKdkK)L|fKZ=}S zoM53-Y5U07vyyv$a=UUnP#oz5L+<^!`J_acnQ4de^}LuDXM8dEq4>(3&e(w5cQ9bp zCPKBy6{+1F!SjvJ8b6V#zO)&^?a7G}uo*EFSjB5E~1S6m(!V3Ntb^+7$}D>dsjB@}iKc?P`Lj zA&4U^>EclkZ6T2gce&@mZpXSw9WT(vV${lkqwhm?6~heTo{X(_XMpydQGZ6km%~y} zne!Q(3<8cXE{>uayvg@VVuXX4*LY5pm}kvg)5FU}J9&Tb>=}MCw#N&ej>1`nln5lj zUDH{h|sC`%JI zej$$3%BUN=9Q8c`U-1j)Yq8%gdm$0KQN=QBO*Y0{<(m_1pdgmahXU)Kg#|D=+CLMs zeTJbHs$Ig!gHxU()Hca3U-p8Gn!JpPw>3q?@uh%J?peuX>tJ7n9{ip_Tq-iJ{woSW z>x3)|18dp2!inOxk9|q$q|k`Ou9vGd8@VD&`r?y*DLaUzo_7=G|9KoMHh>GQOB1Ta z-}K`KrNzPzIRc~21pD|h!2CfnHrop&)l{9x1~>4Rd@tqp)0a8y#c45${_n4GOT{z= ztfUDv!QINgm%zouc8UeRa72V^1UpBgkr)2ni$cO`X#Ih z2Luk?Y1K$lwLAxnfHR{`SEa>lslvFIH$?C)AB@<|1iE$ghBnN8cy*h<1|LGQso=tS&AMl?wl{_9dN)YR*ic<&GKd4Wc^IE}19%QeUgSS<7UF*Bz{s1X-(`kD%3wxj7ZphvhJ#onOXW_M)T{R9r z4>3i^z)jqqffA6%!2M?T38BVrk!*imB`fnColyoeC&2h-b$ z+A3hCa+j#mU@e$M7yA1SKGD~`8MC2^MQ|Vn&gPu$nwd{N`Bn>IS5I(nFY>EhWM4si z8(r@F@sDHfPA6B)^dREILG?G*``kU8FT^9e^=!bhn{IVJLEo|Pm6WjJeWtg@OTW;n zmcR!Az(!g{?AvmN9NX<8BgOvkrcJofVg7d))<6N+3NiC2^u$clxMlHuC8N1-S!eYr z&Z6f_E}<3Iwv27(Ha(n<%#3T->VagU$!26YgyP0Fw?PKZb3~g&TE;A{rdQ}qYRss> zg`-PyF<2I9Q_fQ&!<@w7bHnLN@;=ZOLro&lK=2*!^lX8K)X?*abhG&2X}xiwg)rfZ zu10&WQq&D{In@u_o=_$3@5G-Ufg(Log(7`$BP7uzwucrRxE!+;F?XOW3B0TV&q%Cj z6As~FpH@8AZ5z<*YVUf!Iq{ojWN>JgMcwdjrSJCqhVcP)J$hg!$kq1BpUYwAe)gQj zd|Rzk$PWkfb)`UATd6k;vX`?m3QpGVsC*nA^CCbME=1hxc(di-P@3i-7&*s6^QrIe zN64&Ugv0ves?am@vdQ>WOPB8Z9-b0kyH1?Qo~;#XmH4IR&lE2QS{Y|dTe>|G$5Q0& zKKtB@lB0L%41 zJcE(q}Ea_=A~!%Q-_iEI;EdA)g?JHEjMA5=-d5c@w~;v&x}hzCb9GzT2|A2FQPIeg&Tv# z$xG=4dJWI5`ok{V`@|0W5L#o&>sM>Kx}MP9)j@7S&lFl|Mc3%uwVfH(5v(#N>EL!S%j1E3^Hf?;6?(P)n`_>67dw2{oec1Q^&z8 zTD9p%G^En6#TLq+@I4{hThSMe>z!GbHm+SGUzKxhalN9kf>}`Q;iJSx86HK%gAEO# zg8R^B*LfPNcRPF?U=cFbFQHVL@?8=(V7hwU>)g|A>B+NPhM@ezTtFR^?Ar;aQ@Jj} z^URA&1Hp+UERu8}B0qOp z`O+;_`Na~03g6Sq-JSxcqFRnzNSZbHg`?8vUudF-HqkDad)@iTnWc-`fb|qr4>xUF zd_|(SY^Jy(1G^B}n#R3v-*WA7$0*$~;!VT-%tA>{mo9uxE#8ud zHdlvz2PrZf1|68Y#QO{BKG_TDgF!sz}^)Zuv#9ZOLpSvTR>ES^Q?EqSx*o zAqn>AU=qK_Y;L#%3%>6kS_UYsW6xV&w^7-Z1r)e)VH@r-vF66fD&&N=s*_DMOFJO; z(L}#H&a3N#EWbkj60U1c0HC$`1Rg;)AP%C8mwnd8f9=Z)%>uZ$ z#lV)|xHnvYdm9N{pM0KTVDWq8M44I#e}!~ITi@Qm>W|<3jvygDbl?rh)0HyFKrQ$) z#QT>RBQ@cJ6=WyoLOOPu-`BpsoTMQOh{`ODBs>2}(*4fNJq^&70N(MU?AV6)zgzG> zjv+r;@df;oH?frk1q>U5DGx;)#1(;n?&a{tH^*cN8=7cAttawPUd**6-Tv+(zNbw;wP4R#ub z;uxYflZ5N5zd48igC0jh9;zQXw_j*ajuL;T%a+=tJlPS59?HYCq$cJ~#fVF0^@)%F zD)3@8tJY8QY`Ate z#5vPX)O9gR>xrr$`D(}9;L#!<;h5gwDL<)!A43!>%=%pgC!Yr? zl;o~Ew~yo26y&ZiK{J53TA)X@rNqS7h?=*RS!s8l`Z*g#94X1m`)?+pZ@`tgySqzxGSt4^xSQ|$QmFq%^Qw_$zFs&>yc#yxZ^_p>2Wg`5RV<^@ z6ZHdy*X$lv@Y)(*Vo-JXHNANe>mUiv)*(6v_&DlK{Jw1M8kbonZy5Y8Jx9`_*Z%>5 zJ5>M>I76B&5rws_%>=?n;*rWuS+Cqzkoas9OCKCxR%*dRWA1-<;TNav`CRzvQ(!}b z0Bwg(7XZQWc09wq3MHo`L_0plES9VV63{WB&sqBmp5z*OD+11lVwxfmC8bJ7x)VTE zDb5l?tn(|jk{(N=XD?3_s1XK|0iaZc$=j;5)z#INnc}jak^9!hR8)7rJiK4MHCTHl z-#i|s)={(J>~^zvrWPvo?bYtL*QZdoI$tvx4G~_Ntdw(8I-j1mf(}}HQ0{&`HetPZ zaLJ8ktHot;!os#s%hpIKOm>YtX1P!#GMIZoj8VLL6HuNBTl0FDU#2Hc5S*@yF#6;z zyE{TSlBEk}!=Hz@0A$LxUWu%$Rp-M8bq<%In9WAFnf@{mUx-kxZ3iU5Pbw>ODUO;+ z>l05nbX(dHQSwb|Wi5X|p_8N$@9HT5(M`>>`_W;mAzOhbTO94eU2;-(cC7b(eX^5%&Sj^5{*|Po|38p)w42lSa(mS{T2uXcun3J# zuXEcM7Cy#`o$mAJ=zX-=A%4ivNnaZBbFQl6npVyqydF%GlD%S-EM!VVA?T#dHb-|c zbfgxWycFRLg>+t4nr4Hhm>-Z4p@&3K1j3%TRsR?a1h^H6KVZ55F$o}RA-x`a_LyDl z5fQr~N*gOeWdn$qp;&*LDOa)GUTPsHU|T~yO%OeTd0ZY`Q>|=Qt{dGlQ~S}g20|>$ zfbOi(Rs6T3i(u@}PE%k$>mS1j_bj}*(WY-saAEM?EIwaFV#5BUkeJK+x~yQF@pYt> z%$C=mM&4%_G|7+L&UUh{LE15#;6JbnIo-6uR$Tj2X-cEFFQ@xT{Ujld?lO z&ogN!8&N;cLofk&U|T=3HfAY#xRg7i9Kjv7*QQ6xO(Dv>8B~_?!4W! z7T7Wss0OST02Llv_&ly7E#?p>Fs@*v9iX8Ig9ZYO`;%B$d}|VW!UvN0quxJ?#4gPhauZg+Q2Cprb}ih%vTz6f_$FI6}SLeDcSW&I3chcYsl@-Fqw2mBrx zp%$-m|GF|z`-eCNd&y71ZhHb#AnjP9#RmVTeED!f_uXZ9}XFmzWOGH7e(Tj4?U zeLFn`PeG6|zqfg1N8C6KGsf3I67ud~2k!@&W7No7*7KImwcH#?VBjaedvh#Ne)nJW`~^zYb$iW8E1$+h1^dCeU)}3FH-S5wAaIpr~A6b=75n zw$t}ngx2*(WJWqs%`V6;+N_&Dd- z7G|VV`W)~1$%Rl1T}#pwKv^j!F=IBJA4p%b&-Gkuz+up!Hp3fr>N2=wi8l>uhPdaM z+}MzqpUo!!LaQ~(TR7GJL{pNEqUR%P4{D#E4EB6)0QV%1@|G_QX%-~e&bRCip^`@N zEz<*VrOF57ruyV0l=gzmL0U~)2}Ac_*UczF_lVyT`TEk#RuQuFglC8hkF>Fe96G*9 zwn$)D3&$Vgc_Wc)cxnQh{(RwZ{uGCAh1y~`{B@3V2+mWl1Cc_1gk*G@u;1f!?E3^; zJnO(s^J7Z?9%IqEV|f^Ulq+C!ygv~Kj@uo1OGPY=&2xYHV+xfW)-2W@pUCz5n0q?Y zX{&>RL&X=ndmKV`h*lq??H7SL(oJ=tn+XN4{SFur`EBfhFf>*K-a>)Q%zq%m0C+AH zZ20N1q}g=M>+CmWsWbw$7%f)`<36T+O%69-u}K87#hX6>5U>JntJjQ!w_>lM*p(3c z7h{;*>f&WSgmP-4LS@Ema+Su1VFhu^gT+$V$jNQmE`Q-o7jVTYUI~}-tl3tsLoL=T z!HR23d2*m3#-N(1IWX+mCSFI38!m4DgZ0vM%Er;}v9DGC`;n|%2-C5s&yP}@>-@oO zdO*ew6+~~xt)f;5{6Of`P^GTs_Wy8Repue zcM=$)AFy{j&?e9Q)K$1nnV(x7EY=RRKSSt!mZ??*4E5J)3ObJHt2jdxq$-d5xFHa> zd9d}i)VtzrmxhCM7^YwqGX3?|bfl!Cz>kY#*Lc?FF?@iE5*)+TW~v=CO6AKFo^3)P@f7M2i`Nkb+Rl`RAuO3HbZj%N z1;==W0WVZj961e{t7vajX(zR9=3Z^TCcP4)qB}mgJI$&TS3BIyEp9T<8p)S~?v5@D z56N%7C`j{21sU1IK^X`q65b+?AIp&X#j*{eUvVFrksvEVM9gZutwzt-RAT0 zVZg?sq9`4pOn5Ou%GPEmG~7@8*`9o|T>qis>d%f6_>H=4Hj>?9GwIYEOBk=^%n=%R z1e#bii1TXUIE$CDO!AXXUjoYuiX3?PU`%;L*mSe_3&VZLq7W-NAOY)iVx z8?>{RaC0d=x9JZob#BMg+S`G;mKGY4N z58NIZ8a^S@)WCbf9J-jFYtC1^Y#V4mgW|eUChUrwDix2ec#aw8NOxWFk#zh;s1ox` z$qYz4G|xRojn#GwKG%#Ub+8-)^jnZzN5{?vCG?NB*y?SMS2K_~hhhL0Lew&HBrFRw zXC)h9pghQ3BcObRgd`}a@Jv#>OLMF;k<>R;$E91aD+|4^!12xV5&Fo8xI5gMZ3H{; z8;t3O%1WZzPvN9oYlaP1`_iF>bW}qiU(O#|c8Qozfd(7`UhEIZc&!+xS0_B*h;GTl zLDqsSRBS%fGqt92oC2Rth*WO6=NnrJUlp8}Y6yJGAF;A#7^~u=g9_4Q;e(knHbiW` zU5M;1XWT>q#DDc!T=at|GV~GMAMPF=bJ_)o-sM)}pdBql`6kexgQ@xK_92eowc2bx z)$&!;D!KXcwgS}n(sj8M20EsUS=NE{O>i!Vg(qfNIMK^3Jc#LFX{=P@0x_MPaeg54 z)gLuVqE#*OdkPXO^9BO8p1|4$V@d#*7})r7ZA#;rQ4Lea1sf7C25fc)g%x&YqWV6s znf$e_A13UYA*MA;u4W~{wM09)PY;=2GesFp5<<_9y^D4}D5tOoD+7WR{bQ+8(F zT$jB~xaBqx{IUj7l;V1+D{Tjw)Q^L+%q!DMF z4=9y~fzc@lhW{tHDK0k>0deMed3_WM@(Sp7m5D_xd0lif?Z>b-GF#1t8K_5_M|^St zA8!B7ZCY0;D(o3qCr0jub?TK{F;U5IX|@%A5GDaDv~uMuA{QYlKrKYR`gI0oXPO1W z3RT+cuj^n6TvbguNxl~8k_EgLOW*_M|2?;P^ToB4`=Pep>BsD(HW3;6*4Q>O@!#UG znR2&or-hl@26U#Fs*G##UL!|hrCB$wY+juK1~1Wus*;!<;HI7y`OP?4jx7(KtlkCJ zIwdoS=NC=+zU)YXIBcC*>-v^m%5L(rWP!Y^_q*hw3fR{Jm8Op=W|}jb+%--oD$WjZ z#o6^0br=lZ7|_VA%F{O+cnri=-(XOrt!e}PjN{dq1LrkL;HQ~jxl85*FmoK}Mgi_} z-^Z+SM~Y8=X?y<^Mt*i^_yvV8e6N~Cx;Wm5;yy7v76k2jkY&47DHRZ={P{u7v6L$(=dRz?-=!yu2v*S z64;iAI(J!Pk%4}sGKS*5oh#QlidnR`Aj}K5IH@1$ zh0n`h6*%f6>vPbfS_nIG$XWXzQxBCiU1#8BWlB9QFO!MVWPrMu6R-{1UltKF!9wJ$a2AdbBnQbG-U0(<;fkRGKHN0=wYgDH$m;Hkd;dlnycf-tWjD&1WCZxED^UR zeH641_E72&+k;#TdgPOn>brMtUzv*njX9p@$7p%-UG!q5i8DcINZ}#B4ct2m^Jzf5 zQoDt}9e9w$-~ymfClX-vwYpytvy|Wc@Ew>rcGVsG=x7?1hu- zTT)R42Y2pgFVK+Azs*xBz?mklzemj8F>XHK4?m$R*y2EG+>Rm>ul_dS`E#zpd^j5! zbyPl~P5wZn_*xwUu{jgx&-xPhd7HtNO>Ht1~KL~+FPZ^~i zCqi;AL9mDoHS0pbYRK5tD>CXsse$1w2OmCB^2`*`9wH+fkN!g{VMi$v9}h&!MK*15 z%ve_nlPeiR)fsF{IVm;R)Z5w!jCUE3-OTbUlMhcaGQ+8>w5TXmc&N0@S z{~j5x7E{d8sMhO)qeL{^TlTP5s^FDqx9C+Iw-?U#LQ_HVssUtxt05Sj>)XzZnt;_A zE@<;1={9VvL|jkA542gZW<8aq?R`Ze5zOqhniv)6_|%(7Tx23Ka6?Kx?8of!!;e%3 z)#tC7LeCh_f?T@BE=+A{yn&)biK^d<)5B`>%s4_#(WqIv&>Yx?P165xkl!%jUzr`C^9M)D>2yRdB!|N-~|HPKAxf-wL?}LAZm10XRF2AHE z_;^}u^5cs)Zt_ov4Nfix^OiU~^`;Dta0lB(D9oGJ*yogFkhaS1s#P>wKzH*iHV=WY z8XQz<(5&aW^IhqFLv3d@6%Y(Aijyd&q%KfhG_aZfUZOIP*dVuVSQ+)kr$)?|kWKS~ z>N41sD23_IaCRhKGm`0)tU)=@p{l&IvwA)T`FFwq3dY(?`l#P)giLEfs7h$={L-)b#k&ftrsWkG%1a z|6r~>WN!1ll*w!`USk&J`PD#0kv;*tFfdx7TSf)rffx7xtl(}BlU$h#PUHw~W~Kj! zAoo*bncG}?cPIbv*^Ix$$5&)%0g#rs?5n=vU*h7w|A9JPJF1xst9RT#l$;y#pxE_X zp_LpfTS*EbBYH$Wj3e!D9a4=cYb*%WRWS?Vqf*#!?%n&S$0i@4vd4U{gQ-x%mNTUvm#o@SkwkFUvfY$Fj zoiY2N==}ozeR-$l7;;(2q3DV8dv6l-Fbu21xa1yT;pVm*w^_sq__O&uZX`h$z42dI z+o*q8+kaWxe_7jqS=)bE+uN#?e_7jqS=)bE+kaWxe_7jqS=)bE+kaWxe_7jqS=)bE z+kaWxe_7jqS=;}FEB~KaTTSAC!4xh@&qb|d%Kr|1j)X|W+GnEUlHG-#Y^uaw!=*#0 zjrQt*;`x@xYT{yIk0LV!Ful(AL*$|!38rdS1ykL0T#wedDv=185Kstu4uXgpy{FUM zO-9t%T16t~8!d^{7!O|%v4dN%jE#+LI%4<5l1408A2%7@x2jQNH6N`>%Vc+ zgY$l)U5iR%y**)h?&BJvuQ#6O7zic{I}3U9Ldl=k8|TI_j_}?G#&GcNccsV5zvwgm z5?NS*K9J{<_+&>VClahnDcn?cC*Jp(og>hV{k8khGL`4WHhQ2TC7>JegwR4$E)o#O zRPbnSuIgOxtwZ%-ipIb(s z$B&eTE6Q+nMfImMIa|12oiY{67ZA4r{q5513V&j{_rhBV7)3-VV4&IrcEYG(puoaA zK4<)rVflMIyC*#qPQJlk=SXOfU@L*KDn5y}aMqMQb!ZY5h^8)!b(_PJ?#! zT1LDRKr*{U%V51eLS*BsNIYWfkI48-aUBbNK06%aUz$T-e}RxPWs&-_rCxXR8B6!H zb1EgWes)737i#8nnGK{8K3TCtd}2c$1q2I6o4oiLLqTLbN)+yJ5$}r|T!+(HM-8~L zfI}v9$n2=k$b*Mz7+^I^t5i~-ez%53#79A9JX=qJ5w__XU%vZ$fIsgkN+^+kbCpMj zzpszIFTGOyXChI#^PlRi2KvXjwXqfkt<4_KP94&B@&Q#W`cUGKRNmsQ_r}uHPuPk0 zR#s*T9E|(cQb@(%Mg*5_<1Yq(mm|ExW5ulTvEltflO?XzLNQvpfCoMi?D8S3-iu?h zR9EJ5j7rqcRw5&AZ{~HoAjRUlpdbd!qTVPPs~)vjqqT@pxtycrOi>5OskP(QT8|P? zp{!sVBmO_JMMCE1>bIb6#wIYmZn;(f^OFHEKVN2TMqCf4t;AQ>(|-D3u+@A6-ej=` zw)t|{E)?^K$oJ`SH|Yye%RFz3%guo_T*!~1dwQ=I-}T0*Wn(6u?AF=>IP4e{Icd-88QGLXzw-!L&TdCcc%Z{||7ZcAs4w6nZL z$Y{{;pyuf0p*8!hFks<_ux@d2#21Rz-Z|q`oy_;#Hc-bWjnWHzM;qU?+-+yQ1iVNv z$Q`0NEx*=}HcDe8i@H71ZVwP{Y&;?EoNJeGRQ;Pipp9x6pbr>U5r441AStj7zRz2% z7YIn(8fmrb0;Rx>>gFw^!|4;Ho)xWdshioh*Hk1|(?;8)`V9P1kHh2PkTGz)J~ zT0O6bWE^HN?Zuq39d|kEPetogPe>KJ)b55Xw+HYRg=?i1Y+~H%_i?;EnpRlMxR;wI&lbY1A&GY>qo$f>jz6s00=j8OQS7# zKt$345<3MwT?pIm8W2-G!h2lh2cyHcjH5y=GKeeDsYVa?b|bNQMu0yvo;8SW(VqX= zVpER=z@o$YjrjZ&aKyO&mJAGtmj#Svy1z}yIMM(mTI4g=-RX=kNITjSKR?IPuFE8; z4{0B8<<%1;X_xC&ykxkLs?`arEcfGk9T0te;dt+p?r4J~YGJUgM3dV-)=SUxXQryh z*;}vU3gd_meiZ1Io<1UqfF>tfqUU@xW#fjKvG(I_vHG=$;N!1#NY~X@Bqb%85ah%1 z{Vxv1qg9W)K5R-$e7#Rvc9AaXNGN`FB%L_-jov6NZrED)GK@3Jq*~2i@PxR1&N*PT zP*Qq(qPpoh$LpHKKQ6K`0ni!Aw{N#91MsBm|OfR1QF@Z@3%L?c$Hf6h-WIiAh zog~ya0&3PItvMj|Xsua`%Zj<8A2_SHx*$}*mhAG1O$;=g|2gt^8Kp%P;GrsW9GfXT zKPWe7M6sQ*e^B2%%KUol*}CMMwgu9r<4afHvt3@!BYX7@ptdDh5#!6GiM-oqXIb+Eq@oJA~$ETX>2XH67${_W-#YKoFXNFOv!^>}4Ce!KX z3a#&4Wv5%e*)O%eq!@y6c4m$1u}~E?QVDu0#~%(jnyxaHgBb!%Miz<^MwVb!D&E@K z1Z1SPcWZFncQomHoRAt}=3~Wfb6n=smLHFsLN^B_3vl+2yHrK&<|(LgcOlL|(Re~- z?iq{E?6_jw&0k8bfE+^m3C*pNa?wWXcy5yl>6`r}OG;`bn=%3k-&YSMH{XsYLDVr( zI0fFnPbk71=!!@?BJ>t+U~# zQZ6}tlC-rs*hrT~ByKsL(3^8MTfOCj8;E{if>sCKxd#ghGs!qh0;|2PaAcw9+K>^~ z)X5atf6?F%m(3uB#{*=?tqK`y{57vV4J%(3wg?8H!vm7ILM+weT1degJT~rW8T9pM z6#Cwd((6&W(7~fzQb$ejr?V@UMM>Y7Qe%L5vk%CQ-lezA9pj${uE8mVu2 z4@@kl0Z%73xU54=&qfF?6;{}WY^V6)(}RtqM>9s(1m^)pi@8Er;zoJTlBW0g>AAMJ z{^q*Ti3ey4OQ`y~7@++9jr}FlC9iac)go#l_8IHF|90Dd727cYRfuz#Run;h_e*}I z{l^~xQa}YQLgpX-K1qHpr0tlub$KXca{qNRemRphazI6&b%m-v%|8su;Vn=LnBo6b z;~&mSy^jY}5zd!rCBFZM0i{#7qna^fI}-fES>?TMy8@i=#?zbpA^QC17ySRaOzJ>` zymUclDL4vrv)*o4`d}%r848(Oj01lbKUqER7zt@si_UUJwBMj!;4X^yUEhHU-u}Y@ z_)XyNhXRGKLzIX)q`bY~Hh&8kLTY?5!+!K*57i!}k3$#Uh~@d_@JXAw^DulcWC25* ziZA2s&$rhY2UHDyD{#qa`q^}+-_Jkmf>m|z_JD&WVzm0TUxUY9VrdU@9*a(6uxpbE>zqM_U0G>Y#MTak}|L~f9JnGuO6cq=4X^dMd5n;_>?VS-HUm1Zf7mCIC|*i<<(W7 zNpEJ@;2=GcE3jEna8{;WNdtkLL}!83)4!B5{C4s`f7atQbV@uDf5mDu1DQc3ARsUr zX}y_+XJz#D^^LklMfecpJ|cakWi%SSD&X@Yyv)1W@{@Av`&DUlQ=?1uyicE&TqatG z_V%>qFjNA`*4;JnhqJ^FC_T zei%~zQi!%@IWYO;C#&82>pLx;E;H-p`b{`~z7T7>rSmeKC@jLoq7he)fvzqgFfQ)M zp5aUtqdVI)e}lrq()XaZ%dY#HrJ+In!X2?`VcuuPG=E;>Z}-zyas>-jAARUcbdawx zuBrb#{ms$(bD(AwFvO-cVg;)7>rID#?H{syxu!;D>J=*w-(TCwoH4O~cnq3y*ku1f zuLlX+`B)+vufb8?9V;R#nz3vADeJ%vqN98B%+8~igGJn}tYWZZU|`^@et*6~N=nMz zoh9Odyi_mboLQiON!Ym1*@qq6#N0Q``7zy&(uGAaMhxiYVugG5; zUu3p?d3k?E|9^FL<RAWf^1ZQ!{1FHeqI>tQkvoWBpD4^||N$80VZDu>Z!i5-{)Rs~|Z#jWA-@`antmohg&KY3|Q zmO9%56r$VjC>A?ZJ8E;qqOJ0M7!td<%lZp7-;mnME^hQI!X`elfKO28nlKDV ztvtdI6qi82AsxNVjypM4^OH~u)TuP-ne5PA%OTq_qIIQm1-JQYpZlPHVMRz} zn5S1~FIlOzVO&TYJfZUH2yeol7Ca#FDBxP{uKOGu0TFh{AXA-g;4!!GcY8WI$c6Zi zrNI!P+6+_}*ZMjiBN^&}vOKY$&7ZnnqVv{C$FQ}I2hedOO%*eHYHfibxU;cj+(lc0 zGwZc~-)%9jzz{1QK&{V%D;1nUK|wOOT~YfH%~OtvgO*$QcZ6d^)D1VU5A-_CkLy27 z-%OjaHtl})5vqW+L9Yn{@C5&q-6G-M}0PW z6=BkALSa7%uR0jlFJJ7-rTEy-#ngP_WU+<=#xKT6>V-he$P560Jh--M?>gMEG~wXS z6(`%ik+r>EWiC4COD)c&iE(qfeO*9#JWv?Xwzjr@5jA&GjJFJt_rwg(*8OM3XCe`Y z0RoU3_u(F?F9F_;^*iRvKwkBa2!DL9RM(;07vuOaX>(^YkX>(bPWjoKPi;V2yg&y} zoKv+3$Ihnl;EE7i*H+x2$?q=d+cu;)X0;;)5{gyh^L&}q@s9u`B06u%i|`>k{1`4x zLf`cAs|uJZ%YS)_Q$+Xz0TtbTeTRikS7%wx(yPK2T?_D1{{J@UIR|Uy2F_X^y@b%s z4NE5)v?j)R&3@l%db4Y$(%L6&+-5*j13qZvm!y#0)*&T&T1`W7m4^Fq=pBi7#CkP< z)Jgr_X<#L`o!nWFP{=>2kxU7J3Fv{&3j5nb1;Ae&(ZVn6{;hnKYa7nQ#N3;(c>ULf zJ<0jfW{H>+L75P>Cb<9J!{0Kr3S3ac%I zOi}j0%L^_13amkj-D*S{nXjpcTaybjed8SKPyASPJj2>hiSC41lW1f$3wzCxCmNc3 zKfDz`o9h`Ah)@!LP%G++LzI4tdvJseE~7Il$LEosf8NLU^1uNcEcQ} zT}JvqO4e>+&*9Il_hgWshFalu%VS-NO-w*#Om}J%gW7V?2Jo?&$#{Jz-I%mxu{Wm) zCE9HA3WROhX5ciHM(IN;&7krh>P4&MXG|(BK9^onvrZVN&t!q&e5dyi{3j$VZ>Cw| zl;MvXbiJ-g=7RvN6M9Og#WSo;Ulm;rO6+W9Q6)p32So_rt< zR`4CM;fzj7cE3tX{F?!&J4sGS-Jmh(Xf(uNl-5b2PuTn!BiQ}6Zi}|!kZaExCg%%E zld~Q+w?cYO73O79E3#~F=j5HoViO#-c4R#JtF`49X;sgdzxGQ``m7pcb1c9HJn3*u z2jvcNP6@p(!~n`(=RvFh1#Kg4Ik!g?Y>UD#7uhi(ii4t)Bk7Ww=O(UN$kk)n=x6&# zE~JJz_s zRAHSYaHZmZ$vaO0sD%-M)+a-!hl^h6)?|8Xan2-X)GDwd`aFOYs%XvCrv>yz=g&D`9AIv2 zEb6^&KAs8;6rfmUe(CFXU4&uwYD3!AewOB>a-ZcqP$8Lh*^0n^Q>j0!!Hvh^;l=3A zlJ9kMPg1LGWJA&;iWM@*rNQ7cWoCrQ*Mh7|h}(v;?e%;QRBIL&ad)gJ#gjG(pQv{g zzO^;gQ{z;g(S(iBAjmCe;g!He80muPP~20uDqXImF)OA4P_^1 z$B8(z`GmM_yrR<5kN_PV76vOtVI#6wMug_*+#H{GQz%mq(*)7!ReWwKeI!9 zhQNdoxrRJ`amr+FD2DZQ%{cCORtjn()1^j2p@=9cW{*!m?D?V}%xG9rk_Ui9m%~>diank>NA0?AwyaxwlMkJ8_E19w z;C_Ex49zu&J4}(;%g}EIb5)C7ym#!s?BF-@W39#y_NzCF{P)J3kFcAmk)#Bk83BH*|TS6&ziN@thv{{iPY9qCcDXS6AurMOy!xPE*>61 zA0FPdMB*E`Gx)Wi^Kcmx2L%Of6$J%WZ8xy3gR>1D9_I&_xF>4u+IPaVs&zl;az}*f zZw|}lz00G?d}As{7(UEhWOmG;cV7brp!9U7?r5cWb;u!B9GV*{#(4*G-SZXoyHCQ4 zLXM_q-_{_EgN+BTg0mue8|bfT*D&fl&(MOy!Ff4aadEQ1w;%LV2TtBqsnZ4w?K@8P zXm|pD9P~ch1QascJr@fSdRn$CF~1@G^S)emL~Ni4 zzRZyc=|$O|2^VcdMI?1uPI4qmgrI4-tH-N}c}oyI`t-?yDc{I@Jq{gtS^?U}Q4Z=O zF4*(+*+qQE<>~M?+FSa|tYK1IjE_8ivKznnsQA{nz(v3f8x39JA9uX@_(Tpg5XPWJ zKu7lCF}HNO12+3+aI!YEF;=nF(7@xyrHS#bg*)I8;!@Xe9|qh9H!bmR@rZE0RJf1gC;Y!l z3Hm+}{GGm*`1?XRJp~mN+^?RMn~jaDyFJ+BextRv#B)aL#}8y~va+&Dxmnvv=qf(_Pjy^Q`hmTN$14c{z}wrK-&>F$>}CgeA}%ftcq{-A z5a7dI!RPMl>S5`_=j#6OuSWiEN72UJ%FW@GhXdG^^>@3LFTtK3(hnZ|?&zPtzwXn< z$Kk&{xw`*nShxWKewP5A@IMCp(>AWE)bFzr+73Q8&L)ZuE;#Yv`j8QNEF|^k`hS)D zx5xjfY5d=s;-XLfx90zp{J%8~+-=+xz%IBhJ!JknGykdlzlHy)Cg6^=AG@s*TpD+4TQ*04G z&`{E+@b^b`d_xQq>d5Ke`S3_dHjcj;S4^6nBD5&(E!by0!SQ~>?=Hc)_)(Lh-Hnt6!ET6{Oh2@m*HD%1dP-B;WH}b)%dvmtN>C4@dcBzIF?F!}v{hYbFgW;nD4*Qh? zK98TPDktxmJ7ji|8**dL9MJ&K91ZLJKVp~7p$z(*Vb7^U@jJ_QJ2;+QzSt7ux4N>T zux0Gu@Oo@L{1I;?y$-)7VLCTxUY|J3EH==IVN2!pnjF(h5Z6EZHqXQ}P}{UiG(<_;3=o+?5GosIk}J zo2!X63bf#Dd)H7x2nK`MUp4Z4ht+hIXPa$ks%l$QIg8i0p5O7@Ke|O0;7PnUi%@2l zc3bNpgc>a>gl5~Sfbnq>O*Qv0`}{>1zamnO`~IFJY|Dgv4wM1sXY7Z z!E+rVTG)#dHpk(ne5%^9SICN{3w)b_w6)-Q!#($@^6)2DyB0vhy%h0BQ!My@&E7FP z-hkUNlVmj`p)4y6)x~P=)$rMl=;jHaRkxX(i&h@oD)F&P`I|yEDmgXW*cGq0!;!IB6WNN7S3n5&O<{h~a2ZtEMo(_arWF#(Nnr zP;lRW_KQp7ySfq(;qC{}t%ra~lo~~WpxuCB_p#*!kn-D-A4b?wyCQ}m+^o)ZWY?F} zeq7Wl!{#ni#IlXB%xDWFFe`u3nT(=i{ge)1kpqU_t>`l1IZsNl$a|^uZn$^CsmQ-D zCXvryMB>#lYc$%6k&NMSP)G>A!6+Tt^itxkCUz>qVA!75G6|5X`%vWJ^Xyz zP70=CX_khtvD4mdvhUPC&A8e}QhBgT z&HEDF{8@v@&SvLSTkmJvu}9brs7iVtWTSmgxCOUdB)zns{dh2*bp0viuIe4Y)30)J zZ1S@uH9JL5lX_3TN`gn$;dA<_5BejQ?=nL=O~N^d4w{Z6ChX)6cN%wm2LM@{NCR3OXW@kD2K-|X8Vtv>qE4*rwVEIT(a?z6UEd`5W^5+^}1|J34FnV4E}zjUX&1k))m zpJ>zNX|aO=R8&@2)No680{{gGdQ?)W5mg7>kln>q=k67q6!Zn;1*8KcBkV8ZFKbLY zRDiobX71-#dhw!pmMQHNsL8dZCV%GLRT$B z;ikRc7c(QjqXx)$m9Jph;n+|?=ex;r$2HaJgzMEC4S_%7o%$mxk`A5IM%b%JNd0`> zH*(x;dbMA?cqHL=de>#X-X!1evYCAIHfh)cj~f@4Bf^swQSDXn=x;yJNYdmpHCR_= zfk?z9_qVFT`-vdKXLm0bd}yoBi>*c!XGvx*lp+Xok9r0IY2{2ZoO&*vb}NpdTj{vp zYE?U{z~Qjr*%tE(XLRl4us!7B<6BCl0y#0G-sjm?wJ1R&yY z3f(%8g9I9Nq+i2l=_l_ex5DSbpdRo0{_LEu_l+uFs?WaJr?EuE##}O+J`7iAv~XUCP(3bjg+_zvEJb zv~tdCzX^;#$N(M@@~1)@6A$*;PCnpNyExhaZMv4ZSE$nP3@@kLsCtpeqxoTLtgx`7 z1gQCyA83efe78MOtO0yauhjT!UvHNc^qM;h=O2&{J12xos zILY`IU-vH_TU2oKx?qWFsBX=~CaQ7{G7{v(+;-0N%9z=@Ctm!k&r!frpO%i3nk~=7 zQ5ef`YhDt|`$tm*T8evfwb>}==kOkRpJ{H^PXlq?|lTPGBPdqA#UY42b1_|I0f`iZX6QiJ8$S$ z+f$~Ax$-uPTR(PLTz@QhwYlJJY!w*W6>v_lx6m5vI8mfxE-8~gNzU;UV~a^>!8C8O z*kmo1saTbm=iYE?RU6ZDnbS>=Vd6V?s%2n-O(P%JP9WCN>=WYcd=Q5m@}KnRSG%Ot z7-WacAhi=DFm0fDuXe~D^2I5=@C3}{l-zloO;)ib<8v72mwk+R6WPCbWVX}_wtxSF`QGLnM+L_Pa{X89}N7tTR@JLTg*eyd#6 z(>Lho`1onSE&P5&wEl4tz$m8H#apa7 zzNU4{_=HyN-c<_aocjr4u+b1P z&M0u4aNQEZJ^vc!a#T>u+vrk==reV7Dq?-OA^~dDS?h#(gShA@S}KF;erURggSV{? zg)@0YUmQc(X7}TA?pMc7(WGBD5p@=k;n0%I$nHry&204Qwx|(VDQ>AO~Msl(*7D6(`K)^ihNO2 z3;-krOI-pTU6EhNLVRV*^y=G7zQEvqp~@_`!OO;L#&SR^za(&}_;W#pOe zQf&7P$>{7@a9lkx5N~&!EV>W3jGp&i;VAmDs!%=DdL{KCkMT*Ybm01XH&&7TbRAVQ z?)9ngSY`u@)XwTJc1u{*1iMMd6G5i42|E3-DL#N%r7lTwm6zeSdj~CASLLSgm}r@f zxx+WFt?ZA-w^R|lTh^D^CSwJ0EOpt+Fzo3V`O0+qg=bnjb5YbC{yg;@_Dr9vEE)G? z8%*c)rB7v!Qc`F|FhHts(r74E6gJ18KuX6~u-H_A7`b?M)p6o>hqT13)+PUwdN7-r zT)q}8X4ZSlwy!vC;=&&7bu_yIP3B#ro3|KmQh8|F<)sCAnZ}g`ZVyuHtS=Ku1nE@%3L;XLa z#j6!xPd-!T)cXXcuy@vTo%e$YX0JPx0{d9(jxUQ9lJv%plwqiVsK$e>oeG9qrjfRy@$@R6OIlxJP~*A&MoES;)|Y%pYQ6KM z(n;fATD2I*_BnS4fcR}@DfL$MEN3z#Mrbx{I6ai;L@mfvKW!>Vjo7a5jiB97 zkrgtc$v;K-BIaypFyRY*XiEx6|%#?&ydB*PJeX$;fA4>)3 zY(g8ZZp}-!%n@{=7|Bs4H2$2*%+7hPMZsJdvC*HT$NilV@dZ*g3wZv}W2d#omwl>` z_Gh0_Uo7~~PCqzY882-uyqiHd7zY>xv0npN>{IodO@GhoN%7swT`%er&i$wpjwrM| z3fNVl7+5%>=CEJ>bboi1yKa=B*HCpo#C|y-5VJYj8RZdvomWeKefb6ZYL?XiVtrQ) zwp51s#%fglyyW0$q3RhZWv5cK0!cg6z2Qr4KX8;Hqw#VaJP8DeXyLZAJPWqnPL?SL zV1fd0Xr>k7h|&dedl86y1g7*#^=^oW^lf_o{r&7l4_e5|TI32JAHAhDiPzWUe4@`0 zizc@BWT*i}b&LLGXnI+3JGkDv%BT3a9b6a%g~qG92U@&PTe-=59BkibH3qCIbi{Ll zamJM|wAv+fKFM+tV##W|ph9EaTyC;NoeM!=du9*x3N_j5{cTqs&RIp8U{)B)X6@`C zX=elW*s~oBukgz;?0b5_V-ZbYCvIx};IrUKk&fSj#O?W6@XyUl9>GL%^Z$A(j@VcS z>-o*U9Kc1hI$7@#vylYnWx>d{r}C?}Mn|jZp`(CSY%*J*Z1#U3l5o8K&w7K#IUIlG z|Mxpm5jSay_>nLF!esvUTle-g;yx}r-Vy45AvwW{5AZC-Kgj0X{dZBK0^T>bUV61t z!atAfAE@U)&9Z*LK|`8sahx{@nQ5+pD})lc6inm6Q_JatQBBP(QU_`LPoS5VehFZ| zgAU%~4@opE5JL=|$c$j25p*VG3|Q5o(s{JC^w8yVdT&@^Ka^!raz;4s4YuJ|k0Yd^ zpbq8IHN*{8jn&#lCXBCj+XYCaf?};MrMKMOz^xpCN~RQe!57qrw+FL!+Ar*sHF4RF zM~A&*om)L`eT!e8WxU1)jQNfQuS~HNmFv`GoKAiJQjHllknlnk2+D9;na;}8Tt{A_ zhFi@55<`8#ETq+IQAA^KNgWT42ahx}GFfbqbb)3t)A2ziJv!+Y|EAfkTzVh^t=0mT zjrQ3sxLRqsQt(Uu1YRmu4}|gNU<1b`>kHIFD8K52ek<|C) zgZAe)bsKN^a*?&Jj)Lk8l}rZ!rD)S88X%v!Dq5rw>VVPsBOWKrt8ig57{545cFwP%SauNaOHW9d%<~p1IVfBz7nmQ@ z{F278JZJ!h@!y<*;01xc3`KEN`P9P*%a`8e9cR*|{d(Td@$ezb zoi{b=Q?81-n+c@Vu-H|z{`m*F2RcGm4Wv1iYTWRW@xa*9W~Xn@X4yiT?Qgm+#$396 zdk4!rCO`)y{<4_O4%6EJe?U+0CMc2g&H8#T*#@kxMYO_1&0Fc~7WR8zdk>aC@i+$gQWo!`}|rNyhRD7IlWR&Z#@B| zKOwFyiAXM7_qjn2n)1!1u@REd8Xtfgmn|_polvzIiSOQ-TR@M}S&Kl+E@tRiiu*QE zAH)zS_C@Zzv)8;28{9=v!FgV@bTDM8fweG{Z6TKz_+-6e(kK^OTC%_ai7HZ5;Hj1w#c1PbTj1MTe!4tG9ypmzhu zoVFZM)fPfWD~?TX$FdhPo6)y0A_pIHw99=c$O0>QZAd)qYZiA6Lb7~wKE1nld}b)| zYFADWOhbQZDGGg6;32fWr#c=XT=?Mq`;P%ZkdLp8lOF8d>nK=fV%W&-&)7$=&7F1* zU2YBK7oHk4NYkF|U(1x=M6jF>r@5r)Gx**``PvJA@f3ZVb$M{ZW|2FPJ3}%k$3%P1 z^MX52<~57UJpDTF$pR$5h%NmHi@-{qIT=N`*`DP}m!0jRCK=wnfA?gS<9YUKGV`F#!!;{5=jF@I_ScPxpLn;Xx?++T?QR`r$69bIXgIFM z!93Q#F`>u{&AVs4NDR-^n(W~Uq_(hd5ChildP#4=j}#=z zq;Z96?WfcB*pa`(9QM+7zC>G;g<7Vhp!`X3;mgPuUwqd1$U)(bu3LLhB~aT;P09a}xPjfy-tgP%3=nzb|yP1(=H$=1-Cmk*q`E)@6g z!f~)0ba%eBY(3TlIf@+t8|bRxFWzfb@GP|cAn=_V$~QBFF0Cf&~a7p^+J=&fpTyox*ruThO^bH%FA-GoIMrf_D_iO z#PDC{Af+nK$FFDxU^-_f?Hg)(XQL+eS4a0e_;+s`EZ20SSPiGr&)<|@sIm(rNEtib zf3ML%hv?Z@Z#Eh)ZdR@+UUJ*#CcSO=)fI~tM;H6el$AJB8i#*=#Y-+n^|tEMRBIsR zB&YJ|E!w!T!jo(OQA%MBQqb5yOW8-Gmbf7^ea*XokA*>9xS?F_hxd5HqY1aqBBE(6 zRzCvP`em#|VUDlh@DPhZzLYpx zAHNhe#`ACy8aT*q-z`VvUaw3@i>+GiO!? ztp+J;GLQG7(zca4euWxsh6Bm75XCy~Ik!3o{z@*=8r#xQ%4c*x|sfT@~!@ii(2VLf7FabNS7Q{mx=ur<$RR zR*k)x;t#~)nM=*sYZak_nUHXN1)e6hZ5Qb+dIB<3nHn@aJ zahr~*1Fw1>Q6V%a2Rm=+PxiliWf4FF}`X#J+xjx%%F!w`xh=; zreA|3E3`_PHoost_}G+xAvN*Q6sbs6GocpRZq4WhzD0R+s)A+oLS{w#m2;uM<$hcJ ztGotfgS2&Fw|F|IDjS)dP?M{UvTDsR=jy!|VxEjY>md+qy~G4}iUH^id?h9T(-Yu4 zU_0>;RPju>*CsbLN{ic4 zu>?$0wudk66{tYPyZk!9L>DOacG@Q-FreW%5P{4EH;$mk3txL@#2tGd1_JM&h-ROI z8?f+M*zje70R(~k(L3h}nQdrwvbk%%WO$l zbh7=v$bQSN&};K<^2a$2kc@AsYIRAUcwJ^YExxcv*meTE793^$SWGY7sXZ+&CN<2+rQ~Fy9n^|nM@8#8lwY`&`#Q_7JUsFBEqQZLcHP&$Xogcg!@e)vpqWg zac&wbs5wpKvA~O~*WrStaDPDQgKYy_OJD4hsh>a5{NEVBB(?#xaB*b~lM~74fu_2u z&rOay><0Tb1RAaxH=VJM8Gxm`-Vd3a)W(jYh`jHMrfaDgH=b1aZ6>dfRjwah_eeXx zm;|h)h8;W80-Oq`D!oiC;+{?S<3|*JtFo^l@8UO!qcft~HuNCwBex28zn)^>%l>hI zC8=$>egDFK#=L?>^2Dgm>D2teWE12^+U4M_wY4cGuRM#l!qrsF6X=oI41L@~h3-ge zHI;t4M6v~v-)Q|K^;woF*Kw{)XrW0&pp0#DBoh8q+Lhz(=k3$ZMa1g8B=k?q{G>q zJ-X~Tv)RXtol@|bx14)#9t>xq_%x3@>mc5qUnK3;#?&%6Y;L|P+iC4^iZ{=UQ|h_s zx;f`Kv@^EXx97qBO^-h#id&eMmYXQX@xV288 zu0uSDR-nUAdI~a!6yX6Kn+<>@JwN7GmMrmt|0B_Ca7rVxI960rq_Jh-`2KC&j?#t- zhaL)a(=@AQ6V~pM85MlXZelhhrJ2$BP83S+#DUQ#lU7 zZ7m>fk#ggi(TQ=s#};GH)^ABx`G%^=Q2Wh>3U35-=r)Yad>`?3w_e%xnDmxWnH5s(3a1r;#qA=K z$4ikBwmYG`j(z-=Cc!C0^*3HW5Df~v@oW>dZ|WutB=3z&?y$D#H?@h*eFGJh{3QUr zyJ98Oy4%k(VH`@X)z!UlmN1OUHzCqm{6z6yrH zKGlmA4uM(}YTAuS0tHNhRA{S@I^#(BhsN$DN3}lg)-)aQA-&oLe zGysjr@!dKs@98f+kx-Jz1K>7M)!6kgIVmDttf8SpZ|*-c*7zFk||bIGIe7J~SrT ztguDxg%!5y;ga|oQbUX}40zgVqT|wClckXwDwuCmF-*T@sCdW0)Epl0+hgxRKeNAZ z8>xLYMuUSABvm--eorRO` zel3B1Gx}ARC0ND3u-5Q%n{}v$?A#NFRX(a5t5KiqQMmEv$iO_rzPYmC7$Lv(bY?ZUr~RVTVIakId0r0Z)oDOks^SOEiw{ zYJ6DR$*M((C+o2@7@SMdk$IgQ8*lSOpjG*E_nD-#C+pzF>HYg_YQ9;6pumOMN|UP^ zp{GlRdn{N}B;f+AtemA3X*uq?*JN;6dVXi zf+P($YGjK^(+)LOVd9+isE;ja9KXiIvwcUE+-i~0bYDvLbQ?L*60d$q1(=)r4)e*k zXxe8y0N%^39)n@)Q!Ms%6K_<}YJ`a8R`Kvgr55O5pNZ&1M|dW(uQ}S*ra5dZ1L}sm z33{~!y@vI(KPr^o?F!lJaerOc7T@&LlHNa+))E+WDZK~N8?>&nJF%Ou2;SA-T^qc= z-PvYpLCuq96qw%8At(^PBvg;n1a5CLYm*j3Emo(L%c_lS|9*4E0mh#xOeofnFtI1w z69uef`-bm3PAZ7|3NwoT{0-qUxQ^(JtfEBu% z1TsryT^C9Go%$Y%`a&3?sV6NJ(B|l_7IPCgBBkcnOMc}3bY^u*9Z~#?ItabO!6uQC zNhvl^%(c+%woxjdfFd;{W5fJfE&gyaso3l*D9M}ZZgUF2^z2294OV$NHIuOPi0k{M z!ub#1?CVE2_@M}6?b?UDW;jd&nLFXsG2|G)){65N>mYEZ@}=}?mUfzYj7z=`IOqcw34f=(H7fE9M*bI@FD;QVJcHHj;=NKHlpG z85NYb;`Z?qz|vo41JJ43dTebkSQwZmg+Mw?K;Y89H6s0J?0DrK&iJlTJkzW9xPGpM z&s7+^N%Ch?5zlG1_q>(9nE4{Xim21fbKe=_vEp?ntkR)o@eO}A=0i<1(u7mI?{=)> zy@ctnn#lM^AWi5OA0)q%QLdD02tA95Uiw2s#Y=CMI+^(u)p#)luU|2{Lo2+Cm7OXH zGS-&iVEP%ehL50~>jr^od1AB)7M0)Qgim!3Uj4GhC}z%&Zzm;H0}Nk?mU_2?HMSm; zc4@UD3R|R~oJQFrMZD4QH?>+ILtVzG zRP0)*_k@U7qzOcR1N^eKv#9LNAXE|wUk}%rD;u0^Xo2_2O5r$Dy}zM6n`jO{f|fo<>i#BZ>09gY|GTJ)9VgqF+e{3%|3#FG(i`hGY)Oi ztW^kq8~X42|Nn>f|IE(+M`jI{$9?y2eUx>35GU>LwSB%TIQR(G_$qj^%v|g8{Gjlo z|9Qp+wcqmpO&esUqq}?enYo15r%`r=o~fbO7#5(WUct~Oh49;weST+2Dc^^tQfvpa z)NlkH_pI(~e=A~8AWqr;X4&Bd-%)3EO6DE^Vnh0cj#{A(y!7ZQ2yc6ChhDD zf~TaY$fkOr5Jgv5`7nHpqrK%*>pI}DNS$)CJDORg!EOE1A&=1}s%B@rM9KY&4_7}I zFCD*roJ1{DH|ZWd&2`I}hu@yv`j0-JleFEslp$sOD~J((I6ceT9dKejhtW==*%R4SJRTBa&*7lsu!m9>1Z{E~*00p!FJsp=U zn4o`=%%*MS-kj9J&2-g0gaU=#632uBPHdGIOc&pJ?r7^jzer>e&M+F?zl>1c1AGe$ha&A@bV)adW|sESZ|0V;(|nx9rwZDj zD{DmyZO}NH(&mz|lRXFN4=U^EN^~+%(kTV;Ikg^8p|IHEP8Svq(y^(BJWCrMrmD7? z^D?bH2GN)|t_Nx$ocz!?{$}1jesmaQYr+_AJ@jGWad&p3Fy zpo8Nt>D_Vif$dKwSp<}`)HpA`&vY0NiH6}wOsDO!)Wz~hxAC|g<9eS+Wv66>%|J=P z{22Wo^obXlrYaLCRd(5*0NySj??g8andVSONNC!r&O4Cmo_&A^Y+EK;nz zb>O+TsB_;*`!f64_o?GYT;Vex8x&5=dI4^2qJ^*q82aR^=SH?foOCc3y{ncB#0k~z8} zSZ7&h$zoZ+EU|6Gqp&Z21;iD++PeAL0aRb0Oy!?dQlQqt59~+c^M#_BLQ^}hHd+-7m`8ZKyYG1wsHfWhm^X3kE8ZVE zHMH3RqJ7j#S~ms@aGcR?tx_wK6{*1WGNQp9t>r7|b6D&#s^g0MBlIKG2Qq#I%L8g| zUxv<{d?ol+)nY039~BB7mv1vLPKKOsE=J!tJt+P41RL0^pC*kVm+;&c^4wiiu%vX^ z-Kn^p1A}I&;~1bK(JbffK1qf8J@GiiDkX3dV3j`LNr#pcN<lHkv1qd4sN+Pr8aGLmBzF`TBHgI4cSTx1`OMJ$%?!>q|hpn#|7;EX!El(!uKu> zy3)9}BL2$n5!&{n6Z|~()R~MqX)SRiHk{6yI=m%kkyeHK##)i6B?AgccgNa!Evy=@ z+OB=^+@X~|vo@>2jOVMR*l+bFd33fpq$F##bYmU|x1e{aQGU~QLde)+kErud*Wc_8 zlh$AXyW55;cKvBePE+MZ-3~r;MXs5WC7z@7f;Re>0;R+O{}AJiss2P275em_{mqEy z%8;cXP1maR4e4NX8cXIYsr%quyVvOiEj27wpDG6xMgp~Rj&1t1w{+Qy+59tOA|rpm zbDxf;)$Hi|nY823o=CQ#nw_acf=W~2Qaw1sghS^$Xwql+%_m4P-{U)xaX9m7Q)1H0 zV6ft4V|}uFJ&IN*>X8O#+Y+n=?Z>9lSi?GJp<*WLJgaUum{wsndAy8sdjAT@1`*p% zf4u&mrF8jzaR1k@t1J|<8^#yo8G&0-pWjUtMjyPMEUQXNgdJBwHue`TXhQ@3S$c2a zoR@n+UmgCjzker!$JN?+&b*kUaZ$5>@&C_pwHh}<5dSpuPonrYi(g;rVpHPpwd)_x zC&(YSh%LB(HThS2!TRbrhmD&#z=P@UVf_U>XBIC z1Cj7PIgb-I4#!E2yZTaPsQfe9*iZC{uo?(N;F!*VG$7D)HFK$)^}&KV{wJsp`f3>*x?}A zBjEhWZ2gO79k02KrH^Q4b7>aM+^nlvudMLQ$3{Qq9p9hO4i$Et9!@F)_~BS#v!MHTeP!vuH*~Nfm1W;x#(~8m{O(&#&8{j)y!uS^+>$ErTWcKCsAaqZj#}Me|tdUP$E&6y6wyw#jhH^B|g$% zkoa;!%!hTz{&|ms;Hqdehk*uu=i7-Lz{R}DL@E7jNb@qEqVy6rb75SwRB~mzXT&rf zSijS%c&}z?@NDX{zZJK3!Q<;2#5sK%{9}VQa}GnZ<{p1#3xz&l{2po`2&tEG z-xMM{IJvH*q&UOh$7>lpj4$+}pGRmdVaXl?avF%2q2R#Bw1J8A)}NDM(ijH{=r!qd5-Jt0EN=_%uC$vY_(Ct}Ka*;N{epLbzTg8U%pva~BB zfJJswHnK>5<)))W^yUeBtkIp+H99!-Ohniqe3d;8`Uu)-SiK99IT62ZL#98K($f6| zjc+^oJ@xdUdTqPf1ZgURK$>UkC~b>Zi<0SbL-XdhUwqR9s%64yU#7xm_ukNh<$LFwni>_ z@2S$qq%TCreR4kv!&dxbcr1~rT9?|9j@dCzq7rF0QvmY!lcL6mTY#!pHLM|8b>x0U zu;Woh>f`~GoiH>HgA1ON9#uNF8e9#`R-Bo)M5DlyGLloEz75#2(=TT8^-6Ao()Vc< z8y7N|GekRQ{6_qLRVq5T5ldu8F4ByJ|IBw5#ComAPRaz}Y@(D5&PoGitb&t+f?V)< z3U~{}bM174N5eK#vxju|b9_0g{kUcu+!_(LM)1D_AG(wpG$B~Y$+x|o$_Uq7)M`fo zEa)Pk#mjp#IwoBeT8-z=)E@EW-HhheDXeSMZ8Vx4i&>kSe)r=Y z{CHs9JGAg#sYD+3;T-uG9nVwk`;#H-D|Gq-i(p~K9AUjc^3@c>2doa?GBTR_Y1vtT zNJxgqEP3fM`$A^A_vT(#g@eh@fT+(8+%Lb^wyO@mr>4JbFM^CDPHpKMHGfB4^pGbS zsHS*-;oD$#e-Y(1&kLQ-5FT6)7&#G~?1jC?CWn@lk7NsopVv< z_lk&=diGLAVkMd;!!!)~ioSiDS@l{^fqoX>x^_0l+ag;mKYlbY*xO`61w48w3Gsm! z9&2C^*z&&`0&mGEJ}zA>c?Q*Szxd91~ha^_Qci2Pyz+F@jF#L?W53zMYi;it1BqHi}OmzWGNg#6_oxamta0Z`vFtNoYrYeZTKx?4X037uUD+Q*h;uw+-{4$M`c%ch>q@$XcS;n%}hWqzql&#?xDQS`q;r;HGx<74RES?n0?o zsX(7F=aBp$7epIa(A(T&N?sZlIbhs5u1*-Y^J8k^QHl^!;iS|vCnujmF8@x&kLriK zQ4I|rEt1O&bm3%kwLVeX2`xFAU{9<2dEDj_fswz;W;$(Bbk>tO&YA2iw=-1JGN%IG zQ9OXAn2u$=M#HWP}E>0sofgGour708-Zt-F~D@UD^EhhI3eyUiSKDJ&My*_jO3IO|_;zw!QHx4yuAS1B%_= zOnUg0AA?yesTbyz-1VBv$uhrHm30rA^ODD&@!y2nd;XuvO%J@au|hY$ZKy0LQB zMU@xmrJ!9an}62W88Ud1dqbCb@+od8ACfV(its5{yQr5lyKsdtLcemOmx`r0N;h;R*bcbPke0HXEMbT!E zUR{CiR_>g$@O?o4I`llnG~`8Hz>`YXd4xlTfH!iJIJ*gy^!<6}BpBPpvktsVsqFppT=P6|lx; z{d4UW6&h)i)1)2mq!>U@Pn{f%D!jQ&p8Lye;i{|)$BwQM!MSctZ$7-3#Mwl%8m&^Z zORr{Z^~p~=xw|yheT7x%s|a4Z58@Tq5l9Y){-1mO064SvZ#!5MfC6LAD(I4Hgit7RY z%x)=6E_M}#Pfh}A^SR-rV`=m}8Y^@$u6N?MWq?1L|nhoU$E0tF0;>x1@LoV5%4{q^iJcbE-UUHI322#nHFn>`70-wW0 z7sa5>RJNtkItfgk1Ml@ZM^&P>DqbU!Zp_bhn>jSHe{BNW@yHqb4^P>pT}+bWEV0GS z?+TgrL><{i!SUCrm5h_WVArutZQowzGHci)SMyQZE=*T49#j?3@(Q7HV5?;CY}0s* zqI)s<(&vWNVgimCOT+8iojX#Umb1+tr6a#-F~TZ%49sVTfF})G}|MB~88>N?YjNzW*juSmkjq6kESy6DieS8;DfFap)DlQ*fAw z-?aA58oOPcA|od*P)bMTwpbM4fMX!tei80Ll?R63s5Y3|A0H|_Bz?zEZeMF7(~Gk3 z6|W8jaXZ|97Y2OtA(`&c&rMaKMI~hZ(C2bk9LHLnUv{)%9;(If$2+Rzl?;!Q_A*F= zj9qkw9M}LFyPB07rUb;o%gKD@SieTJcBHPQ`0zy#2n5!DxAKVx38`?JG~Tg63X}HC zk5!*vt4t<<&zL@~&R8O2-l+~4NwJy)@@48=hPvAts=VKa8m_n>jVmSL81Sa|=FdRu zl_r-q0sX%%Al!9t?hG|HXtXi6`j+(X{{=wD#)qn#gp3|In?JaHdivE7OW!${$@28K z8lLTq#hgvGdTCzmF@U>3eRen{^!elVS^ReUELMUigM#?*x2CsZeM%;nW#}+sAaO0q zZh5NPm8n(4z1tsany9MQd*o-^Gjy6%KL*8aMm$TJ3!my^xNefm`(yXV*y`R&eul>y zcf1+Nm)^(^I$hHzFWs8Mq^FX>oE9JYxGU-&7{}~h)d(mgTfaO;j?8QQOw@1M4rTTb z<(pp)crp2=Q7m;HOF-rz{=W82Hz0gIUtnL&BMg^sG~7R}pPp-aWPoLx zZ}3d-Kp+-x8CpEb5D!r8jH-J%gMbk`^$Idmf+F?2``J-i#^oacGY_xA_9 zYq4Ad@mI4xv9Vjp1U56W;oz~ zHN7hNELEC?} zP|tO$@j)lk>E8Zf;X?c%W1w;)-BRy3LV1QyEB{jDjdhFLVH|}{NH79K-hp@6GNm$e zi7gj)DfODc=bXD*87s|-%ZiXaBG8As+ex;nfsWeC+b_KQDO)w%d-zY@iL((gO< zc@?8V==o0gzL!<{lhhe(Mt~lt<~yQ=7@K}J{Rk>HK`!vr@JwC zySYFiBgpZGR(WLdOXi9%xoRsva`kOYe@PA4v<54Gd{C0QC&VeASn8!_c=p`Vj$*AHMpP4Mp}h1*)F(lcV*? zzvKntazfsI8J#m4m*BjID)Ra5+ijiZ?-%Z49wNj9MM+gZ*4-Y{>y{R91eR#UQ4sOM zUJTKJs8=F`njYR3pqnUB6tF#JmiO++jC^5*U#O@G(TjUgS*y)i_B1x6LU98I4t5=# z5RVYl>wKZyj2RDB98lIIz?%PJ^{{wS1RN1yJR2pGPa+@nssMi{PblcVs&|n6@XE$= zScKzYA39qLNN`y{U!`Kxn&f3Tn9M$f`l(Lq_WsO>C2QIKqd4~vc(_XSHe+i1BweT- ziw-rqih@7d)HB^+8b($DjcAIQ%8n)4v_j9z6D_sfoD*6G=CpHzef#%vesF4ufoBad z<9=vPd~$}i6`tl?Dh5{RBg6*5W6gk z*cfls;W{u|Ei5K&4tm#BjnUYA7uXA=YzZDmp-f8b`w8r_0R#I959yR@HBg>;reedh zO~uvs&95p|TH|PB)uppGViaTxaA< zKf8#LtU7E}8{HGw>g$NO>YZSeSwdARb#Od+SML?5O&%dW2v$m_#%zkopz_-~2Bq^SHC(hi< zQLgF*ctT+HeN!Cn&a*gF_j>c8mZ$jiDPCKtKly zjg(f*Q`0EiN5!=zd6j*a=>23lt8v?S%nw^OoeI{NJQySQD)b~}55d8Xho?gOOVQ1{ z!Wj4_;aRBrVI}WJoTU#d-}YN#?TLT_S-}CWS50ED+f|63%3J6XhdQ;!Gn~Hujq2b^ zcS}(OpJJJ#H&=cw>s*$af?EBcgI-B;)kK4=$g|Ev_v^(wZXjFb`Ml1tYQ^c7y|SJQ z=Yp2>A5stC?<3s;Chza_vaD8RwG{Ur>dB5#$CRi^*T5fvgG`!Os?Iy5MZYS7Vw^pp z_o3bOvNNZU#D-q2R2*Nn(c0(*t*B)G58Ikdp2%%WOb;bcI}6Rm(x~WZH&^aSKdU#s zfxECRHTPPwF!Pz^_>&s_CD^X-lJmT!SN}}exb2ZOm#}e}XBASW!E~pp5oxR?w|9NK zMwpo#dsu^#`+$MDTC=ibl&_E@*>~h#w%fEv|G*8C%-J)&ioY75eyn#b?hZ=s)Y;ca zJZrZ?np@m>y+2N|fl+!Ic2^q(B@UG{T5FijQZ%JD2(KV+u*URhHjy0il#rnOyy(8V zyE(qxT(yD9`Xg{AwPvRb5!+}Q5Xxyelg|e(Aq6=qkn>v0XcG7xCV9E+W`d9Anc;PCDksIo()y*n(P`K35UTZ!-SQSP6 zuFOqnvbw<(-lq<3)15H0{Nn60NXj9Dp2Z#*szIMwd;20d_3PZKcT0_8t&h8N{n=jl z4E-_lY*G)uNy~jR`v=ss?}90)z7%;`lLZCZdQZW0?1IagW_P;PowTDGXniZ+C%JXi z0b!9_3-%X8MO>@w>Pt?RU$73RWDp?0KA7DH`~zpqnXQ-eANM+O>C2^(Wjc*VxjaZGUWKs!0Ae-D_KANM$P5^t>%v0 zl=1pN#VKt8Rc31FwN-ESHFnjdYyoRF7*N^7j|xRq3Zy zOQK4;qf;csouabE_xGbL9?MKF@kWjGN=y4_lbS9Zym`|762eOzg0&p5$^%aBpsx0+ z(2Wutgi}Wdhz=C^BzgksuzQQOM`O=&8N}aNj(p)juh0I6Qa|`VK1L7l zr~H_YuY~_G2>-Z|xZ!}*^hzxF-M`;besGgyYeF@Q^{=nrpN*p}a#W@QIg*fvVn?-iuEgAQw<<7j=9@$<;iw?wRx@ha*bo#F{kr zoW3@YtuUp85;=ii0nVR_qt+7SK&NwSYbYLSJ7>c%nei;PR41Zxb>O9e8S5w_SI2yz zQPAlGp=ZXOjqC};`ab)0JUeCao8rse@IsNqwnr(HK`iM_vI6ne z`GR2A;hn8INQmoK;$sDw@96r>k;qvlX!9+=%HWKJ=%>9q^{KsBAgwm)KUNce^(~weKQ8&mQWP+_asWZ?u%7{{)(mbshF_Tdm61j+Ts-4` zyoBFNoD|8xS(>p54-(QpaFhI~b-lg9+VwJ{nQ8=ML9&j#f|_ZT9tuBHQ2Yz}xXFt< zOnRI_OnIRpdu@HoEKrH6s`dd;%jF^@-%D?+s+VdgURm@m&KA=W|4iqVBVwAV+=og&D$KD;PTIts9*NwyD8!4rK9#8WDNE zY@L*_)8C#8UtiFkU$fN^_|-A|o1xs?mb3nFx`SuRS4Pa-6S->rwE9@^`gK;(Ddri z>GGSJDYuYeuwuT79s$BAVeo#hWiB9q#Nxk1TcB)PEwR_28u%5amjGZHE0p_6z&24B z{}#>gzz%BN!(Fsca~rHvMgQ36$yGr+SZ`{qI_#X(M<~VA=j=Y|l+$PSKduZAoZv5g z(Gj|Hx>%dWpLV$*CG8LKdF%z0J|+OhT@{bhOlcfa2uKI!UFsx`P9V*1qPuq|9# zg@tB;7C{ynO(P`yAdW?rbbkF+U>!-8u%xVp_#@-!@rPxuhKoCBuuBhy<7+_K z(cH}VOe#U8`mOSg&_r=!!V--QxF7QUM^+^w*1*2MES5JD8J9WNdPGSzKliq?(z+=6 z?}pPo*TGH|vI}W-H$YXh1jTN;lqyUgn5eWJ6t>V>>`rMpnfHlSwoexM6z}f{mIx~) zaXLlne~Ky6OS%g;*XH#RC4uFk1*R_Sl%5}T4e4)i>d;J~1OAD!5COv=xv&Czo|?9* zqbUZQkm8vY#KdoH$QKc6Tsm=JieDj0Zt}Mtw!Sd?KyuTw0dEPx zTs6lZ2^CT_T6PZxMw`>BKe77>^?GkP~_o3^;{sZ5AS1A#If+eaR!&T1dF0&{xElhPkr3l>zH>$4^5jRPyAXFlf^UxIktKL7LTGr8Of6%c~!QD}&&X z!$M)-UFcjCy$nsPDravSWQTDlRqIX_-^9nV^rjDO1DeUBroAa2i)soBQEjyxi|-a+ zs_$#_Nx;GT2h8fX4P&1!qCjtzj|#`~BHFp21o3=k&oT9$hs~!VpE_SFC_ETQ)CxUV zpDpm(&{0bBb-;|VRs=BzlcZM;7^5TAsvW2a15HRjZoo?5xdUlxuO>wtqPF%DGhAUps=l?H{jTvG=`QyB`_^ zq};iqS0$Oa86uTet+jbfRqc@}m$IUv^;*bvg-H&%Zbo!4s#EfGuWhwljy`IZbHhJ< z3MW>~Ki7n4?i!q{1ewZVKR&f(eNd>I!riZvp8&gXsl~0Xx$n*#xpnf#MiANDS%y@@ z@fObgp6!+gGeJ4AGm#TDI?@-q8OEYBM<@4WTG>q{j&;Jz!qN#O5Es(uZ{m<)d=skbSxHwEuEdv%gLrPrk zvXC5CuFl7nI%+WvY)bNh=e+@5HW|6^{g5e5Cm1tROSwj zRiN|L%54>I2t;CGw>KVuxSy?tp*0+{dsd`&YhS(Z>q{&rYc89qwpNMK+^>=t$fzrV z=I=^rR|3Mn9HD~lkk8VuSi8@60LUo#_I=LH#Pb*7=hk|p(4I9HUB2FR*4q%tJMhWQ z{7{tosBjq)IMz_&UdhX)E3waDfFhJd`Ap3w-N^z|<|9`x?f{vP+`JITDJ>Nbkk*qc zgIo=XiKKcMiHcm4TUi1Ju<>?&++zWI(>9+8wg_!l_YUL@E%Ek_%hV<`rSUCcr$tuM zHwEA_b%d60OHEbV`Kt_#1`I|?cNWfl&woA9x9N{nwk&5p#2KT$Ov}pnN`+x@Wgc%+ zz=$AlRl|p5&OQNDd>F#N*Ar+yOx>My_^l*SU%-=VM<1$@isxwCB-%{fH=|}>S!1$q z+B9vIU!~G&&NYi^Sw>=Vh1{nyVc(Ow{sxb~7xCn|F5mZ0Mr?(?XBSVU{q-x=H}0=y z=CC~gX2Q9%)uxZtq!K|}XcfA>({!Q6+D z8v&iD?O};HzL_|vnMUy)O?{ye;s@iGzS%Bz5Vh=QNw*RSKFKdBHZmWbgFB1g7u6F= zwB*#90v6dzI$K&Jo*)`!&hm3%t#-)!jO{Nonc50WtDEN(X(?RRc{JZc(Q9Z$IV($N zET8!23wubkbew|wtcT4Usc1L=PPlR z``5lIFH{tel;8N`ldU-b~esp)4 zM~Vv<6MURGn}Okxk9m^DzBPk1Wy?CfywUfH8Rg44N1ePs?!&>Zmq*SG0tADDwhC8~ z=Y4$xqzf04@<3P5&#if9W13F>?2=wq zB?#5A0j;^ACG)J_Mjd12C{V6v1kGME59@s|--~zpE#`YB@6^tgUMY|RfuV)z$ZbVA z42flMzOVKZ(*+55GW=uxUlsD*%|@wfg+~?5><9c-f43vmrCRfv^D`je1xP(2Mk#hD zb!aCGkCj|7iH@gR)2@si{9NH5vs%SarWRM83eyvx!d=sm*w@Zi*b&YQ$eP*NigGJ& z@nA~;wh7in*q-7JD;-IfB?_Y5S}@TFZ8u zRBht^yO@Qq&#-a3L@U2eG6ab*kQE*;G9{~pu37HiW8PoDA}dY_!c6g!E1+)W>ft2R4Q{0l`azdg%7y1@FQE=`#x&t?OoWw+CtE)PDBtdsXM!Be z)Mep$dH;Js_>t*3qlS%URq z-6)!QgcvD?d7ykTL9=lktvg;;lQ^SDEqLUT3EEX4RtDgS(c2E*_0lcJf*B;#x@4vj+F-@)E)61Una&6=Z#fc(L`ctH_dg%KfFaizWfrw2VUQZL9f ztkuG4d@nI>0)0FvVgr@g2-CQ&9;6yFSEClYNm1riI7(be$z5=og*&V8b?Q*2)>1R= zfm)*L))GI@ayjSawImleFJYVi7X6Ojux>HdP^^wNsc);IB%p=dH+-+oJb$dS)nql8 zgS9lx9|f!uO%(|zyLxNb@;)A}C<2!r(XedKVP9?Na{n_>#+IvfLBRJ;=w)^@yNvN& zo9t$_#Bm0n6kd)yRL3K)V>tN~d{uLvCSV$e8z2Ub$yrj9ro-G<*dE*FcKk@8 zlhC7Xs*PQxa~4hbL?_-#nJmRbzBH}%r3~&GK&R1WP50H|`6eoEFcQ5|%$DPJK9B%w zTcy@Dil!$x?s|Qo-7?joh2`YJ7qGvR+8)-I>QzoK>D26@?q!pFY0I;6&b>Q4G-$dN z*0;9nNx?&`Uxk!PH1&E9MXfr394NS52^3A{KK0x#E79!bHpcKt=mJ#=R&6U{e)%~e z-`>il*KKUOx+`)E$o6xsbtkG$t^m;;`JrM3OhIw^rf?(rogZ&50MApNWf)5v$ z&(cTlBANvdojBV{FNpPw4mI5uqY}RK31DK7PD}NJqKg)=j4t~Vf>!kqUIh78Gv~Hd zN4Oe!Hd?u7@r1O#57SL!zb&7303nuF(f9hM?wexd9U(V1OiwY__{}2)j6R7@e3iNA z*lKiZolKccG^;?Mciu!Q-sZf-wVMg%WT zQux|=i6Cr*3B6kg$~P^0apV`>D?82ZABYYVxxz~JUjhE-%}>+{>YER#D?v&>v&C}A zP?joYORI9n#NdlZs?EH8`pWNfScD5_!X19?r6LrqK#ElaE8hwrZv0PV|LY3WrQMHF zhE2f{f3m&)Bkcaav(WzY!+xqaG8V%VbEiLN0{kP8eHH?QF@)Ar>i@XXubco8hNr+| z$Nt6Hi2gg%>h~i7PxS_vcE?*b_?idRDo19zSG&;w$@>v(fmt_F$N5pcR(L7%C&z)0nFAIT;= zJ3mtnjxuywnPO{n*-4Ugujxo+H_3YMXg2&ik!!|c^KhHv$SHncVBznBC6@pBw*8x7 z^&*ZA{nxiW!Rt;FWCRXjcqWha&*2He%PBk;INHs;;iP;i$iaE{&yq^c-lsXY(EX^Q zQ0SdrVySuTwY-)T5q*SjMYqQoHyF~QY)w~&X+=RQs)%}fgF-N1$ddxyFDh*>r76+b zwMu}4e(aJJsI>9TCtW~bJJE5sp$vM(!mL%R%Uax5%``!jpO>q2?|S-!J^_p2Z69LH z?a6rUI!mdQRnz1!$3G*?4~6P2SKnPUJhflb{TmXs^>UNgasd*E>oTYL*xT@u^3mhR zQATfsggPC8O%)q&E9-KNfr^Gkc~)M}V%02t)`ZXd{4eYI%Gd}J0aL;~x5eFE32IJ9 z>#kS2zX&fdBNCq7f>qMXcBB5B=ZXnvR8>!E&EFTR7RrjKQ^*54G zDchavUKosI(GALt6iyfK(u5UEOs*E6e3m06f%Nz)TgW3)NE zjxBMSubl6?<1b}E?J@Kv>Wg@V;{R@&(z-P2p-{c>kE?Qb zs;Shav$t#QFfSpJh={!Yex@eRk@Y2I-IQMCxNH5tFflX9^qQi0uRYd>yXYx7{NpM< z3oK`26}U!{aayl-F=Y(o-8YXbX)=Ns;X>cDA=|>|dQ&Dlb-t z5jib&NFjVuMbpdkW}q>`$6KR)1)h8(VD+-RB~u^%Py{(|#z1GxH(4M}^wej*e~%Uk zBu@rjU_r_CuJoN-(y=_CSPp1?R~LxP3M<1^hD%OcAxuL&c8%K`-~BJ{EMUDLPr>_q zxW6hyet(YRlYAOK+{;0?!^6X)0O&o{BE|VE__3+&q|EBIu|}4~a+7(IUf5x%1SauN zKo?o=bAo1Tu?oXZE7n6Vc7*js4*O31EQH{Wj#j9|Y+G`~DMC|wPaPvsXgFhE@Q%$iE@pMFPv zK50iJzVVD$yOV~+b9h$n-6bz=wl`7w4k$tuTElyai&=rnmh7{3Jh>j2z6Rt@&Tgdg z(2TN!(wpB5<2G4-Dy1&u>cK4>UtY)5sqPf^y}&fthnBS_%UocQWyHaBhRQ|ntY6Wq zY^1VcC+tm}iYvJ$6_N%~!NbYOu=~U3(n*aQsKS^7q z-k{y>-=pn)z+0LXOQ1wml(5$cX%V4TV4A_9#k8&au11x`avtnx%$Igr3FoOAiHlT$ zUNHge+>IZWbcOE?JV43WtDscG7tif-Fiy+Gq9g;d1~?oxw@1XGYmt!7KKKTs_cFj?;~khh5+2)`0HJHQdsnsRt`4rQ`9QwsNRcoy$9An>P4{7C zAb9_!;P>6ul`kk@D=Vg}Ru_o2SjwkQFRis}-k7~XSIu>xHet8MsM4-MD^jmvI6FHp zTG!?(DsD-ys${q5>{YXrtD>^{RxO3zH;7!b&~Eg)o67H<)ZvI&iPv9wky!1$_VD23 zIJ-!#sDNu^Z6LkM`rg{#`!@}mTg-!p?$fV?jDMY@bIVU4Nks7t_VMm?DvES`)=*TX z(|Nx|QkVAkI^okOQh$@83fpg$@7u&)7TRau8*A25v7E@MzzxzU(H57C&_)QwHOJ$z zR!Q)G(ylfCnl}M^-E0{ulI#Y5;F)hD!komD>tK*+*;g+XqNlSN3;*=!U7z27Ycq|mwb>|MshW3bBqg&4stchYvQtx*|DQW`SD+?1xxc$FTTLR5@ z8-7~$f4}iofQgo zp5p79JEX!3H~9CBB;4T27mO>vu4hN_9oQK=WjvxuNX0%Hig(XZnl&2<>H5EAM5%}B zeiQ|2xB!Usb2!C!B?jIVIPoy{%rAIe-+(F|OE2!K@c8vd>&+tQ4@)@Ru{MKVt5YH! zqpf%GXd{c<<+&Dr#WkuPul>AsaAZ1GOT(rh!|IOQeV(1Vulj>{3%7(DFADusbIfu0 zJy*gkivoPnH?NN5B%hV$u|sbNl} zb%tLBu8@uRiP;Lj`eriz?b&{i7)SzihY^*r_q;hw0J)tw{XsUcw z24XS6m0plba8l3NvF-SZl7@L>lS+?^H(y3oWMPB+~GAD7TGmI6!I*}uj%PiR_nJ(ep0X6l_ z5VBRZmE7Y<#jZZ1BD!vik+y1SjU z2V`@anb<~cIh?0gzg(Hf-1)s6)Nug7zwU55bMK~Qqa;zDp^?v#iV0}!<+!xZsJwBD zDk?O&<{mSBE%1PWFed;WZ3Sv^TSW<(_j#RraX&r_hjl?Rc1t5OCx9wM(EUs4{j?G` zZ{uDgCRA4vuI4Q)i;R=6fQ#CQ8MHth`q0mQ!8IFe6uTeJ;6W^pp9pC({H?i0pDI)KCe-dS zg+BzFhGqup+Z0mEH22C6?r;9>sEfY9t6X{y&_UEJ6k{i^(=tyn>`PKh3zllI*q{+- zv>zt_O3#yaW>m7fD|4mYh!*xN%p#Mw%$h~N>c!c;)1rb}F#*MPmhk$B_M@#5lj^{` z$`)lA@070mY3H(0zk^GZ<}0o6Fyh2F(g-D$r=?9GdT6GmYD5x>P}=*7_mY!rwOSKV z+E5A$T$R9NX0$UH!Fs;ic5znWqs6}Kj+Bqm8SORPQV%T{+P%on*OeIbxJ6=lmKZk- z5v0moF^JLyvC3NAue4j?HHzQrcw>t}@>e~VD_oDhUNDxGC;~B;07dyeL3Imu0O4a@ z$G=%xba_xP`MEOQ{tzkBt*gY18~B=%>#y^$0AZ?do}wJB68mAYuPgKu3oZR(#Y;%Y zqpmH>VxH0gq76#U<3+3(mGXU#v~oOCju8%vjV=r+5-``^Ec>JpUDvy+fLsp3;eBt( z@rDFeLe-&SVG7Q-M6R}3AOcy>Q?Z(PZB$cttsx*FO`F%bRgcH#+7zGp%s6x8p{JXN z;)gHiR@YX3oQqz~MUo&QRajFOp?7}M%i!frB)=-I7{*Y(8Me@ns zwWIzDSg%{JR{s*b*`WNqaOgJm!ro17YgdQhs{1tbZwB*aDLHTQ7~HyBnK#BAL|G)= z{8g(z!N{eP?zYkhR)xEZsHKl<*>vs;XW{*66(_4fCF+i{^t!mxdd*SrtR%<7mg7yV z(l^6oghoPTOg?k9mPPY9ALeRwt@J9UdqweB9E~f3hbDYO-!Nz5LQB?R?@9yKca-=7 zOuEixVwtUbns%aaW7HZ*hZ$I&_h)IT6W$}Qls|I{>*P2E;#=btv$dFu%e?|>&hd2C zyj#vvJ$#?M5oiCkd62#aV)sdT3m@a&{JxGeRefqZ@o7M1bqKHZ5)0GIl#X=96o~R# zioF=qhEsD8j0F346{?5wkGYt8f|fU>Y{&?n4yq3)XD+YwS&+LysuR{MzcdBZxfDg{ z_|EkwjiT1%DnsI&3zS=W$0~in9u0$zF^QS15A*Z2n%!qDvpPlN0%SqNOrm8zazXf2 zo8cG}#g{XZA^vw*hwE2e7>j`QqiB;e8Ua0}{`^6IV)$Y!-vXu?Ik4$ExN6IoU}hXS zJx6%~b-dF$B0?i@H@ap7xB85O{i#NxoX@wyllY!RO5)!S`wBRFa4(~$L-A&`Q}Ce5 zlk!XT3F}B3&d<85@;or9dc)Q--)hJRK_YlUW*nl71t^$F=J2xn9mR^@TPUf1tjTUb zM<>KBkj=%W_L6A`k{I6(2{kI9<3>+EoZ&fAE;Eb$T0>=vj!$5~Hdx!YZTB%QCC^a$ z-kFw)u>d`K&vvTLJJ3}HE%U{zQ1k|K4eM)?yUYDy-)w`Oy{_3h_eyQ#x*0F1{m}MD zXuH0b0A~)`Gw8rQ)F9I}T+2xmHXRxddc8t>8UlV0;g9WqUbq+^#??qmyV|EdCIrnT zG}UCZ_oTY`^8JMs|7+KM|MnJV-}^V8*k0XiYc=E~lKyDD zHLM0ICQ+t-mG}y5qEeQt%$auD}C+1bohGr2>OgSv?hvbD!3X7vy|nL7Rr$9@!pfhDH%WrIxZbV^!RI`q|= zf^J6l#K&zm546s-5EF=-KrD~tmZvIiwT0O@`mw>nU*$YgkY|pBWy3Z_Sr1LhwBj*reGG8>=ZD7K(OI4Qx4KfDeBrUT zdw&ZWt$=d202JX#yTK_UrD*0fO-T)vkK&Gt!K;_@f@GVywP5WR*0-G-paRZM=G4$3 zm2v9Lw4sw`=rV4}nNcU?nI1cv#bA2Yi(|_zdtxW6f&kmtZt_aOkC(yIdt~!n?!?Jj z8rImN&a)FHqwBK#_TPDf15IJor!%osmR|>|J1A>WhviF==RCNg6Ldjwh~(>J2#I>| z4_47l{IZ!!=2+D1bEvmHH>QW_W9ah&8GK6(Rg;dxqs~zRJj>vy3d<|CCl31+ZeLlc z9#+r%Wh3KF4%LZ$1o=@^=2JD1rE#4y*-A4_COr|{m!1H=K0=&0ed3dPzEsTh3{)y^ zP6$CTta|PybP_k_fE{<-N@Ja!n@9IpHI^lQBhFNr&k%>ydwe_4-EaGb#=l0|J=9mp zM32czn)UN55R+`({NLY3-6D8VHas3s9MO|k+WD7efZ}vsFI)GymN55qoO4?|E67Eb zyO}@^Vw;gZ(@URr^3;XIKk>ZtW1>}lOiIklU_!hSa2h5tPon7K_=o6J!xQ?e=Z?f3 z&CLmsJbL_ieeXO?BbG`}TWhzO`F$^6z(}rfJYQjD^#K)hSYOx}*U#;h5%)Z>{@t~@ zfXZsi;#>WvBp*-MTN);XXb2ShZ&}p|{qvW27BZ^e*!**<7)8XSU?|@6-@YC&iHuN& zn20%EKKvtz{jDkcy_5jzk-y|#eEjcvfN$9V$o4fX>SxgA-`{rgA|nbYBBC1dofPar{oKe ztSZTYM%hW+5D&s!hw(ngc@8J8#UcXh&8Z!~v@styN$|YJIg-5d(B-x1%R=_tf?0n- zh}QDJ*WuP<&-IQk%Q3NuJbtw<#jhK$cPco+OZnBVS2o}|As->mv&lLO*f(MG#WDAZ z(=GQU;_s4zWV_<`ujds6)NM87Vu7*V^7SNFOef2HwwHOb>WmoVy1Xk_Xqk=1PqD05 zv-6=Y^4>2jx9bIT>)s{bSG1eoGepqFER~W0q@=3X*&JGEWGX?Eo10g7XbrXDre@M8 z(DM5Vb9>hcBq7aI8yl6L*ZLr}Z%WkE5KVp9td5}K;W_N|{=b=tWO^1>SF_AU;l?Us z-@wMIjTUTW*I?meNx!VLLSIJzQJC&z;v`CIu8i_12u3eRJ;%UwQC`kuf584-9yjhz z`<)wb$DnG_V}Tua=rNk{>p-)&EI>Cjx<#S=lut5n++0_PXXxj6-2WQin|e!wt4hvtVO23F0ex%J6 zX31SF*EG3XslHa^&Fr7xpKk`G8d|c7lnS0)7q(6-5AOxgmn)%(fzVlRjczPQ^0e#~ zNNC;)RJzbgd{UL5!6q#)T??L(y>hwhV88g)_Vqoat0s#y1>^70xN;NR+-xsK|7+aR zqBhHTjD8LlE0j-u9n0|U=<>uTqX43$n(0DKbkvW{pi~qfy(QR6UPJaxJ$2Cim^gRQ zdK!vZ^JFCAUGH6bgR>PRUiB}Y(k%sckHi(dT;F{>)8Ii{Lp+g&((bQnBC+#-@fW-_ zalws`^pPL)?B{C@F@m{uF|y~~-hRT3Uhm>rD<}Z?ulaBGQo^>N@GJn!-3362`spu1 z-RLk+7ch8+-qGJR>a|vd9znaN=B4lKE$@k;SL|0its2f$dPQ@iHvYq1lxX&SM3GJm zn^bbXE0mzRREw$W@*q2+gG)QL1*~Xju1d-DAP`Seh@)n!n~JJqb{>52f!wM?7jj?nIUADx%<8*`XOzXLdev_86yC-V))`2dH zmxf3_S?GYagr_D&EaCyf<8fiP5kcu&tzU`Y%zfNSfHD!=;Q0SwEU?2nQWIIkfcLO;Ql>ChMRl+^Fvhl0pMh*rER8TRrk2M_|y*npJz;R}O$cb9BEUlir za+}V3ZTt-&0B#@&=B(>4k4g7-vRS=UC$R%(ayp^P$*%9VS|sSg-i(z#e06wE(tNX> z9J@lHe7_knqn;4U#(L%UG}s8T>aDZdXbac0iz|0pea7FqSHMqLwfydZ$MOAudy2r? zG}aelf#q8;p%eO%%b(eV&j8rh45Cpj8Wb(4+C2YAt`tWBSlGPy? z{Vspyu{a4mBT~!v!MmqO0cNoFr!6yJDNu|_jlNqkU4zzpLr}s-l$qpvk7mj`z$u+! ze5M*L5<7lKY434{{ltF2C1|7$PBe zZLJ5_w}T*nL;sZ9Z+CMd!Lp`AEOt{ZFYZcbw9h+@<69Y@U~&zFMw`S=s=I%lNT$qZ z_@NB;FW%v}L)rPoFWx+0kVFVVtrO{SyygnsuxyssdZ|*Y9L3ewIdwZJ04V7610xj8 z!}HYGIs8xd1|1y=i;wps)#HNuKe=>9Irb1jpj5Z4$hmtaX&xXk+RMuBU`ll4bdovq z)0p=fKvt=+E$I!_z@IBoU}T=&r2}VCqsC&;utg5Y@H($kCBJ9Q1udT|t|`sArLPD! z?vUSbkwAcpXeAeS=Y9LhMd(uGAmXQli`-dCTs}47GJk3TG%YQ;F@@Ch9IP#F$YUwE z#}sjweBDav8x6kGwSB-3>L!rJy&)#i|4mFxcMnEhRzqvQsNae|?f+{{$8cl3>{vKu z(mK-1xw;5>14gSmYONdi2?MQsuP)1~IQ3na)a$JE;NjlaUt3B;o|64+fLo{sH%0o= zhBGOCu6u8pZ{x&8mIqgwi;<_E<&3=GHek-U0qR;+A?Og^ealWF6|9K~n)2)tJ?P$d zuEs7pF6!84vlB*v`cjW>*a?*m#!L#ysVD2P_oWR{TUrj!ArQc{9qYm9nKodJjkorY zQb4KiHbg7=Jl^J_a8fx|%l-H(m-$#SRsmOEjn)O2xUD$!PTBL~;5yztnck5uQ}^N^O-YPPF(X@vYQw(*^=H%43~sVn#|fEy0wx{kP4Q`g*I468Os-u z#1Hzk)43c?3KbCdK^$wYoBN8aHG0uX7czOQ8Go3~?rV2e?)wu6f19C-Ox)I*R`sxw zR|oN(rbC`?ffLc;%O#>Tu{mU8sYTDma+;=~zUo7mA_C@K;e> z4}iG4q?wOp5z1}Qm3WWN;#w;i>MsRITm2N+tS9C&A%U}daal-FnEAemEtt7mvj%Bt zKQmn5(_w78c>C^SV+n zsp%G9qKMl@W;qL&W`>2yvPI@ESO^LfO9e#!wtx z$TWWH-bJ6ePuK{CC9=fM?pGL^VgV3~!ZJ1*`2`rdk7!U@{gBdVeZ9R|F<|-?wQr$~ z5gYRP*$TsfM)SExV@JBncK>6LX#HsVXs`l4Q8C_n-MfMqB;}J)BNkWOQFk_XRFvq1FS?dma3?UPaM0|b5E>2!qT2%&7^;0D+#yKww-t!lNV zlD4ofM6Wq96ee?>w|l9aJ-Tjl7svvhy>VdrDuS!GN5zX6Z$G{51Ry$THzd5>ea!xq z8TDmeUXFt<@+l%n>_mYWMg^#L?K-wy?F#449o2`Nc@ss~)hyH%NgI`4V z+u67mJUr&$as-Lo1)Bd%^WF3zQ6?6u#7Sv>@pmo)67My}cN>hdK3| zQeH`HC|{2S3M<0Ge85cO);jT^mJ9I-`L5#npbt2k^$v&!+E=g#mO~Xdm_9C#k3F-5 zuc}P*xzYpk1!_oV(r^uzwJ@3#V(l-7h|{adu>+}R46p|EGw?x1X9j=JMLCFXU zdF#NMY^VH(PfS z2<&VY@Q`ItO=u!dS4q_L-D(^-%}1VVBYF(^>)nNev4vKq7gg)Ly$_g{GC9BdvVQwK zqhq9EGyF-Kw|XZ^ty^ARI&XXdnYRSbeG6)o<~;l8!5quYJY4|dL@R4*o%^PbxdQ8WCMF75ozU8Bt9RN=%(Tj7e`Z9i%tASKc6mAiE z_AA6I!w%=Ie_0#X!eizeWTJ^4R2<;Kem^VMKcI5WW zlnHW6^NfBlA{(ZYK{J%s9@5m~^#O%elEESrMwjks{8bS*E+2A1kPiqlg~E&R$>r`n z_tKK9#vDW#YbL(IVBrK_M;s=m#!6D$RdIIjCh5V8PDfJCo5bt+;)z21P0*tU#IQ)E zM$la8ND3^yxH;(8z5>AV3@sKf#|WI8g~#DPd>{5;%F-$}vyJa@cS3r%rP8bTY>GyH zgt%}9!{TSL_Fm&#s;Po~I;l?9cPR-RH4y0BK40-%+N>lzXuzz53~P$8i`srJ3wus# zgN=T1C$9x1XBL#yL<;Sd*zKU=uJ)Ef9)Z+P&;&iU>ZMSVN1e+YKU*NJqy7~>TtBax zi@qQQ-uZI7M-!alV(8*UsGzSZULF5L+nujh+s+<^<|%XhR)1&D^t;H_;dYg;D%d)B z+gf$WsWDn9L9uiuIB}iEdKeC@ujy7zfV7RyEz;v%))oheL!+2djk;n^GoWS;>(Vc- z@_-!J8Q$nNTmyy@ zOeh7bf*07yX3681md+cbKkXj2|FL^I^oJQu{sr$q3Dl|OqHiW46fgUXP1;on(q|4lC_4x4 zul@W1>Fopq!bbY8x4?rx5b%==z0RxKOO)N4yNDZ6eCTCbFoN30EkgPK5%v~fO}*h8 zu&szl3Q9{iBHbV*jPBf|o6!wZDFF%T9^K_AX%y*_mR1@jwb8Kc8~*bD|NX!3yRKba zz}e0@JMVkW`#kr3KlcM{RKpnsSaIaX&_LxOftau_YX)EA=V7;HjO4(f(ed0x89x7; z@!3&jHh@zOGA%DqZ%SiOyJyBb(;UDmOiE2e;m4ZFJ>Z$)2F@Dutk+)go9L0v&{*}5J+|V3OArUl)_cFn1X(fL5LGf5*G;8*Z^1;Sm6z9Y zgF!ftlAU?p@XwNhx9ZaZ9r3r6(<^$67eJ+S#57mK>y(RQPl|1>r1BN#cLE}ngOyh_ zep|Y6?JAPi$H@?Jk4u8oCqyK3R376$T&w$P=Gz+3rMpJLZZ{wuw^LZ_?5_5nsM6A& zkyG13Yj^(E%*iJ#@#DE@IhrBQS8AnaOequIcZfyPbCP|<%Z^_r)zz93k|G_N2OpBb z0uooK`}Ar;q_H|YmW3RVPQVPX6eXUi-DZ=Hisd&IW|+%Kag?*_(W{49Rv=PSc{&KZ%D9>Xo%ex0& z{%pW|K)PXKM*}4NQeW|Xa1CFBQ6MS_<=W+LfK8r4FPLIXt+i6!ZTv{=`2#-T|BD2= zxSLo=w%+>ULf}_{Ov}Rt3tN!g#H41sH&Q?;S!oUrE>+--atWCweOf#HNunQqlGa#O z;W%Y$5*C6Ov%|NR3``}cK|ZTOp?cD|m{!6XTSZ#hYwpFTk?K89c>=YuPd~9QX$hs7 z@%V07<+(h>&G#1DZRyq?5zx@REe2*eiH^D`){UIy+m5=swfdQO!o|XxQ3`BO|6zFq zl#68MD&A?lqe_{3tm7nFn`w7NP8IFUErK9}216cSg@>3wTum!350bB!isFC4=(N~C zWkuo&xPSBF1ugyDdtKh5MAOLee15mUB5M32%{hL~toqkY^Glgc?{`asr))?Kue2yn zerYRxiQB?uSw$%n1Dnsc4f_Xx-Lrv%cp?JYUmBEF@eRn>A+T;A{Jj-ymRepW@J`+D zu{CVt7xCTbc<M?02YT7By;277^0wJ|O=iD3mS{U%k+Tb|h0SWD<2KfdPmc{zTDIkn zq|QWtseSHdkSDpG88!ikdkI8xA}=P9AOxu?skP_!ClU| z*NV;_5dN*11l?h@&YH`y3;PhX2K`rvvrqV2NQuJihs0d}W9rhrl93uTx!D<%cw#?v zkgW22FK2LlxTWZTf3ZQ2*R5Vf(2Z+-OmDfe*s+unNp{4gl>1S|RC)&?Hy~ozBj!6z z*OE+td_{U^#lWZ(2VwXm4_kpB7!w0Iubr(rmlI%~rp#5$lwYAhP3f0<>hb#)EMW{z zB8R~sSEI$ArLelGMfdPl*SSiCBwvdmm;)o@$~#FiYmbLT)1mXc1#dA&k9Rr1V{2tf zLV{^DdG6L8^nb)_SUBXWUi*GDri;%*~Yo~V;z-{`oXsFWhIgXJIasX+PM?< ztO-eRR!uEf2ayHTgcO@*?LBN{CZEXu6ZH06Qv6+nkKbr&XOYTufo1vs)W88Fr^MIm zFvDy^Di_n{m&4}?>7QVUGQ}sSf!jdBK)HxVIRZg*8q+-D#S7IBwVN#&&hDH)Uoyvv z*Aml9P1ZSIqPZws;s_p7zYL(+Q^Tt6H0$IiS!p?9c6#xtknVk6j6vAkRYn%}{)le+an8Rg8d!9&WMyAfyTU~!ksobLX_;@WAK6F_Z4OH> zK^V$KH47Hh%gh;N3GyUuL?sn^@sYRd3N&DyVwPA;?Dh7(ziF&rX@A%$s(EC^-`Ck0 zv^>(I;k(^_62{)-c1VVfwI zTCoz7UTQ~EIU2%7-TD8a!7@F|7c2YEu9z@`RD)W_HPUep+b0Y2oOi?!GUE=t_L$5` zX!>zg&uQintDpzd+gDbt$yG2;ZKtgKjZ>QhVD7s?LGLc$%OeTrfv8Ge0 zXKDlOS+m1e#TO+%G$(>XMCWL^6R<)wHEg4CbBYSTz?yUJOl;ovbV2ki@W_>(pHT+~ zO^8QtGfPiXn*7A^PD9bIxgV?pUeF!e&s~F6N$t3bMI*u5ZT=Mzd`{=)m#$@x8G%Zx z7YspclRL@-5+?L=J7 zF9<~e5KzqRx*S+CzdxKn=%0L8n$^J$b@BzTJRn^GE6vE29(j;MRG?JcV%9e2>uls9 zu`vnWbU$QWs2^g8M!llMd_P&Ok5upKRZ{Y+^FF_EML4qUj?8|>$~22^z4=R|Y1n#@ zy47y2p|;Ia-!LY1etMz)!agW2|+o@NGTXeZP|`)wB(WAwC(g_c2z3g1!GB0!Ojn_U-jksd*YT&u(a!HFPr zX)xeX9}QObDpMeH(`NhoNpI&kFX$v@@pRkE=H->HILjaM&dLqb9iYt$d+w9CpS#Bl z0T}6}G$<38z9^@AQ5IUV*J>)e7{C0C-3Tiip!u{^q#OuV zN@7!7(=QhD`b8<{DE!jwPI*WgryaYg&q^*H32IClaeS0hWR1}Sc>20$K>G6a7R!Pg zV9#TZ^&sYr%&Q@DsgE}Xm?`1Oep`@{A=vSn(&eMs_CWmf_YH-bmP(^t@!c!q><~Vq zH;dU?^!1e`s#jUe^Uit!n`K$>rJ)P9@4!&i=&9Ue0bU6`ta4SBcIxf?3UeN|Cx=0n zrPVG?1xNJnB`xn>$LHxNx+qWVI;2ckJkF$v)A28j==XAj%Y8PQ^8N;@**D~#wz51J z%0iGeXP!8RGgdOHQ-I}mmHjZY1LWCx+vnQYoSf4|k9)&up{TT@FYMk;9SYsN*wP#a ziKa0BOaBDng3FYoZ&<)#63QDkqJ9}7RH|N-8!;_$*lk^i`K=ip{7W;sE2nc|$)2dA z^0=<*h`vfe9IaWTfniW{?s~2rqT+TTF8uoW*mM1FOXX7=5rvAl{R}N?xQyOAbDtFC z1_mDT>{Lv`NIf2prYa@Z-5W7X9S>7w<;=b(ZAid%Br1?y#&K$qZw+&fYrjwc`v*z~ z+My0Z%(2ibRN3RZvGFZI1Hq-FX}t}`ck%`=2q5?jt0(AD_A2rpG0SaTCB{dt+f_nK zOIAVH9x457y4Mh`n*-!7ocAbj(n-PDSPg^SC|Atz`ciR=WW!U#x%|_z!~F>bTLhwE z3o8XH0bbk@o^{b_ca;A~5S#pnXc#G$_5=#_Q6#p8AYRCoGYW8{<^-Bc+~@LakhQMC;ear-c)-4}8xl|(D~B^gqmVi9E_J|zYQ zedO1(PtsvzlKibFohSd7o^%c}SIQ@^;6;;Df)A1UiZ1L%&f2P^B$pm_Hylk5HKFid z?b!V0jDxY9@ypa-?k}-|*VNyF*YiHQ&SRB_BbEZj?S4EtZQJIy(tA*GYWBqbIsP{};9b_)`LrQ> zjnoLsL{`UmAgA^W9&)-Q4~U7{rO7)8r)R-&V}{5q_p0*6fy6Plk9^8$!maqZpYm{N zJVJ778B!tLFfVWWuB}}#N(Yk-<>;hEtuQ7QBY(y_j=CgEmUBnH<7r8V-MQ*e9@mEIBC_?EY38CZWy!y_U{u8!TR(`TXfpgJ~5^ zDg0Ae8k>`+fAm^`Gk3iwA?G%BoiN<9uTZ<~!l%_fbLKGxnUiQz5-e>uSW0SfYE7;y zZFFd*9k9KRK!4PYCcau9dzrCM>a$z#ma#YMZ2;}t)@vu}&t8jvzcZ`ztIs}#@fWZL zAdKg`TvL?bxlW6#2;Zn+lJdH_0JU*43R?~?F&wPXH-f5k#g`|X6LK{?Z1`t?h#C9>8 zu^LvB=+?m(hrr1eAcpEQ1|)Sd4%U#-FktI?YxsKYxxsVyd8XmpEJp6{Wx^ zW8E8*necrLa9+96JP6pOu9t5dVB<(H0o9J8vVK)jY77PJP}DIn2(Gy4w$Y`p3R+8R zD3_>Fi>WTxHPZ&ooDU6F)u88&b+{Lb(d|B$3MZ7NJFXsqITI0}!UuUNnWO#vva`K= zDDyeBj4J|8DiE7{40B5-V39_aXE&DBt-rFLl+YAj!AiuJK`^YA*vuiC_k=uvFBDSlUNfHU^55Rt!22SN{1N)EFFN3E?rxZsc z^@^kVO|ubYPtrk=H3e%>j*oUijsp?XaLM&@O$aQ>>lARJDFeLA_66~#{iM`mUd2x^ zZ(M^p!PD(*1Hek}m;SsJO|c*;zY=L7*@5k!=E2FX6N~W`TkDZd~xRMR}fYUBL@!5vYuz9-nlbABkKVTuBt7>}g z33aUDHddr@&$8!*nJ?$8^pLGqjYP$-S)wp;Ggo#9b=L0TeuBaz`EvC;g!^&_PoiLA zLtxww(3DwIDVMT;FDVz1l2uhzf|~(|`Kj;$sA{|mK+4S1_vkuPi~Q}xz#n_4a?Ws# zFD5qp10o8F#rh2}rDS~Gm3aTka~i)fGbZ*IF*aa_pnIosEfJgb;Pj=L#?%A&f#5dO zF+G)wFKSt;?53Z#s*-ZQ=?m^TaH5%;#m~yyj`(IX%Ye7{y5JiqIjk;Mu5)HI?a&Ju z2bm7tK3dvD^N>Sz^ru9UNAmOuYTJwAOBsEQwA=P*YEGcufz)T)Hj}%87(pXj>7fU5 zK(lw9;#((|ClKANS$?G35K(6_b?ccR8wErnYn4%XhVAM03>JACNZ`(kV1zj{VTo#T z75mYYh8{Lvr!-Mukq`uqS#|{hVMgTQjja89# z>K;#~vG9f7S_do4QQWgPQTFL&o}L)x+?@jlj!T~t5C0n~$DQ+lFdZ2S?2IFVpouaiU8Eh=_?(vsBE%mJ818to>0y~r&&HlV#L~%= zD|=5#7rUD652J0S$S1Qkz!KS09Me{uiKNkcqE|$ISuqHrs z9i}neUF$aKdGuDgDxiX4L2g zpQ`FRcHC66t2V23Zq*rIX~ua9f8IeR}wu>R6upbGo=sI^J@x z0~|JFSK)~(P?bm~=NMM26|i`8;i(gqLM`|+V4zF1G1!pPo*jCV8J|+`A_YFwzADJp z9)!Bn3!KDC>q;`@`L{;3BTDLixlWWVaes$Wd+$9(t2VNS0W0p-C(^O~`mvYX@?e|- zs^K%5-w!3V;GpB6StWCf^j`Ae{cMA+1oPr=mq*2{Y~&HrJP9g0FPo@RI%(%k^4xcW z6SeD?s5j@r9>#5)Ejhrz>u8?Ndp^HeKH4in_+?r#E*AWVeOMXZelr>*-(6 zNa3<_j<38e6X{DA3c=!9C!ph5^R39^)_6Bw>9$U~djphx0Q!#vr!=BF%}I`_gpaAe zE~Ot&mG*11$RHY`8YZzdgk`&zfTyj9IbEk5kVIM!bstLqrlD4aH5sq>S>WzEnUVa7 z9wBf$Cv`Rwdff{7SWrfDazSFy?)Jv`cX+&fM{+jeot5NoFKH&rBs-Y8?_QuKzWq zjRgftRXKs+%ebBgkXE$qju3TM#E(FMHzpS=(8GZ+Nns|Vs(8@poA2Cy zpCnBwgj(XRs&3d<1`eS8AAX)!#?&<#Qz4&A=y|LONVK1*1rDTdRXI?t4b3rx_qEiw zEfr_XoC%gHjCHk4$6$-e@CuJnoW1ARPw=9+9{5;13Eu~(sOq3^XPn)?w)DlB`PF+` zXTK}upx+>|FSd-!c%8|%~-mKEc zd$EoM1Wx!jZu4M#>01|%;B`@ae`rnS;;#)>gGYR1s<5P&6VV$XASSJ2)*|>VbGA?4 zklGL(OQi6Y$dqc;`S$dsb_h%`MM`?G9hh9@h4OWz|ARXIaeP%Fw<8SR zc60>-`;N&i{IjdSj@rO@CGlCS%^Cmiw&{4R#0RcO=5ZCA{m<8Y($&(FG#;w@-)+Wf zSSYxP*<|AX_yYgmU(Q=ZcXwk$;j`sis=SKnk7Qixuhe6YW)E)tY1YuQ@zyiy$_K$) z;>TIGI6h^})i1BF{u4cNdxw1Ft{6?_2J)w~JD;VMm2E@%0#8`=d^?ziT-2)8glR4k zx;|7See--yekPNi%HJ9CoCReNv$XEv$7yQ(3VjgQLt2P^+;BvY^|tr<$@)&FY3SY)xIOP1T$t^TJn z_qW3O4?=hlTZG~eP3-z#3IKbC1WCA~7UsqOol5K2(-7#vY5Zd!|NeQO@QO*df$?bn zzW4q|Ws4j3G}2dsjsLs9kayUr>91U$oJ@9hcJ`q;-6+eh4}2!ZAMpbFX5AZI5u}Xx z_^6(2y^m(vFbhS*;eK_}xawfSG-CPR4282F;cwmmR>O61N>X$kb*|CCmDxA6kg1v> zw8deRwM^Xt;q%z=ovXFOS{U%@Ny>76?bcLMQp(zHHV6Tx96o5h{8EE;WZ)8UJ1#d7 z9NZsg2kWKA<8p(!OSQ^K8BLl!=Um*LXWWQkEBAjJ6tvZ+&{MLP*9ADyj1AF7233@8CwP>BpJ65oo~S5fU;!nr`&tXwLSw$ zlsmc=nyFdPLy#}Jop@`J@I>8gS!cEe4CTiL7WUm@87W&3>QXtve_ho##svHBNnZOM z*ZDjTSrDv0^Wx&Qv}{nJQNyrx)=EInLlSCg`2!rBRioTnM_ZHe&v|*by7ZXGw1+mp8I3HVz_4NC=#14-$un#BkbU5Pi>uTPXI zc=a#ujq?Y7(46lHqkil`LnT5fgD)l)t*NDjyDHI(4C{`PCF7-d zvsWJOM>1BC^L;!xNLQD%;J4S1Fz%NMxtc=0jUMRM{!g3P))SDl0fHjYOSfp|)8#(@ z+?~H}#XAJ#dCWyd0F>*&5-m2tstz;0pI%{!y`k|WGsQ@|z2Qmbk@ zG4ICm;-c!fY!IC7Q_VA@w;{LnKC>+72$#rkKyZu7BmpBWFePnaZN5X6JE(CKsJ0LXrN^0dy~x~K6nb9AI4PH@sQ;NseS@?0OMA7NIm*M6PPVm07!Q%h~m*0FATa8M0+`Vti$ z9zId8rI&U#vtj7w+4HUXS^Z;Sg>kFFe9QLAK~ocRZ6F)+@Q}g4=_kkh_DNNZ#NP}e z_71yGe7z`btqYJmHW2T{hTOyi9EWRB>y+u5BEMpaYp0h2!*UJW5|asNu=LR79y|H- zYDpayvdUX|%2TIe2j9I*bqnrSn6$qM#iy2){*3jH@_0w>V|JsUsA&AkONo0*{;WoL zBLw&3;lW0Y%~?cZDK28g{r|=PNNHq1$YCJmr**od;6@$RUqn$&QVxv{Ed72O)-(14 zjhZ}T2e{aL7E^RIs#8C(9?r}=kw{aTYuI}>sjaR~+j{;5sa@s7&;@E! zo~z%>f3+^@(cM#7;*Zj{e}MjPDP^>!$*_m?k{ z8*nr9%FyY%h;*&rUxrQ(0~^glV^WVz-q;Nl4&g_W%Ygz>{1pbx!)cC)0d7s385p1E zrTOX*x98z$JGvjhQEZaau;>R$jCrNCWerV}g8CmYrr!kXxcIIlU(6^I2>2cE7Uo5WU~UZ*|8yJDoFegU^q*KcC3oX_d5O>+>@CQtyaaOt)I^c?4ypfxJ+Q zrYO2V#h(9mm#U#a)Hl!kEUxlz8p@wZG0=8gcM(Zs#+3C z4D#-%{IJ%qAfN0Dn)_fq-AtpZ!F}uNuS=_i_A$YKiW0HU1Giflc1dJ4uYArApDd93 z&-_%0+Mb+%mM;vb-Mjxly^FXCSXWtAuAg&*-$5Nz(XKr3vdEI-URX$IT!S8uuhp^^ zpV=lslP`aG61wSai#{%T*)6`kEhINa0X_xJ4?Uu*FyT{b zu;LRE4!pcYWg)%uJpIG$9o|cZ_A*4hhj3O8^VjhvJc{oO?o~=eH9Cz+?E@U&mAs=t zX1%8?%ftz1j*M2qi6KD*r0avh;c61nNAT&ql)Roz4*Mw)4;uv!_(b7Y0ia}A{<2I? zwwS*rihN=v(9weUg2VS6$mv}k1CIO}W6a`ZWCxplUAyT|Kb#TBUv^p1p!$@b=7j-U zK{d48^mKHk-=0lH+ez;>NAJv)kl}7|+#)|9lP)E1cNlvs34z#6l}leQ0N86Ztu$CY zccDwVp2(Jfj zUdYH4%goiYIS%7XvZ~}bc7w6*PtfWqeLi(CEL5H^L%hjTL(+`K_t0g^(5gCo^?usH z=-r8C1^hGtq60UQA%;I>N!0e`2nrN#IvBqMX&W~SRP+TU zhxK|p25FpqxD0T!#%r<}hGex%J<9Cj-iffV$L8xOd(nr#!Op*;uubCLn(7w65o)_3 zI0`}hHCUgLFr4`pO(0MX34`R%PBofMvXzw--cx$iZ&WL_*plVhcL{Z`TW2I7O6Iv4&*&=~NmUm^5#L0{3Wi8iy6^(z-tD zf1n$t5pT?dwjf5g%-Z7V*o?~WpikX0>$rRx<1+$0YW6_(MSqIm|43r80Uc-Dx(jr- z<5`%ZGyRTClVKK*`YG*6M_R*Xw8sRzef66-tY&aYIX|kHjT;Xb4uka2@*i7tRHuhC zi*=Cec1CH}11>b7(m^ai)1%}Cb2`51ChwS(Ul`V_(`}S&kq$weG6Fu*E!jTn1l!cBMO8_ z$B9frl~QVjgWt?R2lK77vZUrbclmySq!%PB&%Jl&eG(Z8`-fjRtussdXVjUfyHLxC zY3s~JUZ3*?PoUU`&n$Ktg8|80B^z0elFok>vtLEiuas#=zPNe_cm(cXvh+AxI*cU3 zZ>G=RBJ8%{ECUjJp5n<6@=%gXM4T{>I?de~nlPlKaGYsULHWRhR~7bJ^J+g3>dfrM20t;`~9A7n}$=h zknZ8h?x=v*yDQ>R8-bWSw&iW~vvEjC$0JZ4y0__k6Wa^NryNv8y)$q5(KohkQf?;q zsUR}1hq(&HyWO68nAbAZ?%EYGQLboy?uZ4=-}xt7F!n|@K@_4Me?|tx*!fY4%1x95 z1`la|gK&T4NgTH_?hhog>*Qy(patKbNGHArd zmD${`2lnD?5W06gI7Y@;e*>36^yXAO;PW4JQ3pjt*h}#uhJ$V{u&~;SqB~Ht;ttWQA( zJ96>IPWV@x6l~?)_S_yMTRC*7_F^%?wm-Or3rv;Rk=7BMLlSGx4J6 zw#yynr<`+?^TjRwa5Hq*%_j1bUOyku?uUfWJMT`+MuY8lUiyejSj{Gj9KPVN5A)hR zSMYHFeX{vzqDzlv54=!Gv>$kw|Fs5zzIoTDEN5F+LNj#{1)fgn4DpUmj_r14@- zOZcMT6O~j^loErp;s=JDkW`1K)yGwMb{I>=guSS+=C1tNv{beC{d<=iG``m||mXP35Bl;fF!)mku(4 z>pk^Z=^k85^ap6p^|j@F)nG8^{I3f=_}$+skiYP?N}TzrFEF_2Q%1>K7Jw=)D$y|z zg{fk@Rb{lcOgg3@!yVr=fZ%P9e)8L&Z|95=%$1k61?g{aC zw$+5b8(mH6U=^@VuE^YNwPvY0QhjO=fAKS6iy)y!x*j|o8bQMO0w!<-#|A5$z}5d% z5cmo2o>YxLg)&jozmbqXPT1KT=hM>n^>5_=jLYB1-~9O%=6CKEO#NRhY5vuf{PUSJ z-~S=;gkQ-&;Uk2;|Ht8it{C6D&NFwXMn+=heIzHbW0EuiNM~__6FPf&<;5`q;wTBF zvT1Qh*bNgb)T~>7bX`adpfOFE{hkN`^?qpamRJV;04PW0T$0;%BfJEIw zvwNXHg`p4D0i&+E3pf)4b0m zW*(db#jd!`mKgCp&vv&>+y9=YoeVMn4D)2qg3kQ=txB=bFNu3$RqeZHUmx5ba} zCF=&0JH~&H{p)MQy*aKdeUjBczb#BLFYe$K!jt1@?~9yF-Ws{mN-XTufCkGry08k9y|ao2`zv4-aZ%CUC>% z6WBR?aN|ab{S=C^Yky+d^lfNZ^P2r;Rsi4lOX=tlWxocctg-6_C0--D6WKs<`()W? ziJ#>0wvASt))k$dpADgZ5tAl`X+0u+830Jg&brKif3Gxo<1sLJX>Dn#VP{Bgxeu+` z?v#edAs1OB`+H|SvZ;OO1Zf9y7@wbXDVXEsQMd=ee_|$K(ZE7 z`z86%lHOaUQI?F(eR#WDYo9a}e?TCDb?PclMKvP$Q&NuS^~b0C;f371CwWe>&|{fiJZ0WwT}mCPbwz(- z*>DJDo_kRux$GiK;GG5aK5Tmy!T#RfXYt|+)T8KpSiK}KFS|k-X-*Fa@$QMWNW)$! zzWiFjLp>z72VevXurV5Cz16rd&B@vB8r^NZu$kUnXmrXGvKP%N63Dj1o&v%)Jrony1JbSo#sK82Y9&VktuF`j@^TlwnV1dK!mriZ)yALszqZh z?lcnpDE`3o=G`$YYE*0MrdrYHE(=DRn{$uNdcaGl+8yowDk)EX$#3?9)yLTkl$K>FP%GC#Hq9 z@xdZmfhqkl1I*MEYLlhfIdyQsZ~LEvSXhMK<>o#drB_o{U;lw0{iLFH3|+1hN25#w zk%&yH(l1FPeQI$9X8EC?YA4kFR8&-~b>4DK6A1_{Ih;RS;;AwSj9wcAPL~?QGl`=T ze8Znwc1wyS-8tE-ifBiyr@NKrFubU}r^q8Bf0*c_MIWK1XmZr#2; zR>m!+cC;<8dAT-F>v*Ah-wLf|?XT=NqpYR;>gcqc|4heqg^V)wNvea@l5#5d$d&W4 z=!xHCt%gruz944ddoZN;HCmfX?7i%T2Q6DfAq4Sc=ari`um50e6%Tp$Nfw2@fBX5d zC}#u^vc^ctQ2EVhueEBKu2k+@jhaffTH9&dkV(VX-MyBAT~S+Rm@2mGArH3M;d#js z`oY8dW=PaVu>?}()tmLNeOgmZYqEo$;EB^;ygPGXMIeH1rlEusZ6Mo6hn-^d!G;&rFpLF>*_7!-AeU%a=SaQtOqb{(p z(}k7M5fy(=`$elpDi2I%OF3 zC2%VuuF)!r+~FYxvy*w(e`uPlBS!Z9*Ov@d4mf6De)FDp>O2wZwIb!3j-#cLa-+HO zjv_W^7iEw6(;_tObRGH41%qj2iDocvy8{6Mduy#rdnfYuhR=F0Dw+eA{2(Ku?O5-7 znH;4xr62E)zIh>&K|_z6=GDwEN!fvv^N7aJ!wb#4t?#tp5SN#(c>%xt>g1wc z>PXU8ww)YCg%2jA8~Q(VhDy2G-FJ|YODXsFL9T}K(6rz`tvVtiU8`sh&go)Gc3$H$ zs(;T?q!+VG@U8GshAiL*4BQsfdht*yGi~xnMZ39Dtr+HpI>!@=iI~k(_(-Z{xUBG` zc2$s5Xx;R|uZ4CRRZIO-Zj%Ut9`{{Bwt5Dd6~Kcg%-z%66*{(&wb(VW%L8M|L@G|i z=iZZS-HJ5lvu*xQQAtJEwNVOB`~3bR1i{SYsovqyqurk#_S0YMsR)v`8V=U(+$N&a z7*MlKIERvm__BVypSZ{Q9aEr`6nq;r^j7l}CWlMId89>g-^W|ZTHC+UxcScAhm`qR zFJ5XCzvGU~|Fm*?f-HN$2Tcmjifr<*lL$6}oe{9NcniqC1(KhE1;oV%b{nG?@kF zM1k!u#9X%Tv)VJOW`^E9rTdogT?;~I=scMP(VS4rkhq!54I&(I+|n#~pLRgj#+Ube z`QSp}sk9z7m4f{CGFS0h(LuHUPyYkD&A~?G{>m&yTH+FuZPzUnNWRj*Cjn=e9vVE* zgF|puthxcE&kFzpS}m40lwbcjCtef2QBJbVEA#$F#pz9wVYT#G=8GL`6*F^@lv20x zr+fya+p>kBn-T1O@L?dHQUXgfG8ZKmH)%hO1IRHw*O9Dz(`CcYWRK*leH9y0V=g6n zK)<(RO8?C+_5Pf?so^tg0r%bSgzMbKw*rgHV>ygrl*3t~%*TCWta3X|%0=~zkwPfe zaK^v{1wi09jGi9+6ZbVZ1U6Y3A{iXA7tA?V50CfBHWF!2V__dEjk`z5Z!f-E1gBNm!t`IGE_@@R z-xU|L{>*i=nZO4wpqwhv`SOFf$2lLLRtmm#<@0g3)fXAj(z+}}$FtBcrUBhCLY?lW zKY#klg^(0HFYu6zLoEBxa^}Dv#NTHGOJtn|?9u#KDre;|dWNqG6=ya!mk|WtaXn+w zXyzgk{vi_%y%`~BB>OQ}1kNM3SxD2$u$ra3ykqjhAyJ|<_jc;rbxLVCacefMaCk(f zc2eDbk(ie{E0bL6>U;>&3?BYD6_IUyEnE{5e(MQMAxf>N&kjKNp;g00ki@Pb&Bjr3 zh;ir9C7WU3-u-FqI#-pd_{nGvKZg9r@q7;7q{IOEt27E#cl%DN%zL5|){0ypxzeN( zEU$=I2Qo!Hf*A*NrCwbe6GV+{z6lC@({ksf0~M%HCk+l7=L_vw^{isTj?@%7*+YM> zE#G3{(ua%;392wKX?O6_bqW}Ox3GDl+Vntptu4NBE{A;Gd-1X+ml5-g&n1ycs4DQY z-PmRk#TU5_jY6yVe>{m_T@A)2P6|dYESW;k_jaL~_AkGaz0Q4&yrYOhiK0x=eC1la z2REKqC>DIKeR+T0qKQOV)!HP!kJE2uom1Xmnb_Tp3UcOgS9YoS?sdmGsd4$JU`w%2 z0J3}Aqc=Ai3f|0m$M9^N@JvFXSTkyDRNh26&a8mDw<$Rawp=vEQ$6z*@c%W zDUP!4a0#0@bLt(T1zy#$$Qvf5g* z@G)dX9j1iRXqN1eM;PozBU^fWdF7IEX0*h{uiOjj58QVi3_uK5PPTOx5@^$WP1%%C zYD*Z+)d86_-&brT^RZ@_ziY4t9Ls7XW(eMszLcWJF9`k7E@Nzvcd;U~m!*k>3ZZv80xcubH z+ri~(5b86)rN+dBFmF4-)JqTzNXQG_QXR`~BFHuodXw|gfn|y%(mjAXjR#*B*y46l z`Mp9XFI4RIBoE8uKF&h|^>|*J+XY9JeIl}A;g)r5O_vFJxR3&o@;-1SCZIo=>`lN6 zL8nj}-6}#I54>L)B}xGQ+6gt%qF7+7EZ-{`GB1NZllS?6%BFvER zhaZDv?KoEexHN2o#u!7JIJCQF?yxIhU{>*Ac0#?^%t*D z=*{QQVpvUusu2>ZR{g|}`{87AMC-Xb&zPGzh9jt__nEgEg*UX^FtstGnxvmSnF z<%v*n4|U*u4tb5BAkfYY3mzuHnWf`j*EOwJJ`oJ@f`m@k-+gkGlf^3%BN5hBAsS6n zBg|xdIdU?P^d&xDHt(9Y;VJv1DL61KNVVVJ?foGYazE!@t{m4%p;>rL()mqijz@^Y zYY{(GuE#2`u{5a=FZB_3(?cx?m52E7s79dT#Pmv->r`_M#L2ev5}y&#nL;>t&cM?R zs0vAhiPqtAEP@Za+7#3lc-SnF^GS&rUEpI_yB`%yKX0o=Xp}CE@%x5?v;81$lu#P8 zuP5mrKJ1foi7L3Rrf@CSjv-Z~8z$N$IVLNPF3sFcpu&!9gIPRyy5{@r_vOi6+Yjt1 zM55x>*eNyJ3l3DYRM^bik0T!cY?D(d$cVN`Mr5^da&Qbl)$9Zpn4@Y6qbS0q{7-1azAZ&-Vg_VMyg%B%Vm^5B z>ub0cqIRajAIJWf4r=lDIt8@3k(#h(%+*NCY&iImWi!_F$5Tx{)s-=6hOxRScaX96 z8EwO>Epi7|+vjYutamEEhHfz_r`|W0kNV=%r*fz}a+in4=6VcCpN#>O_Pv=hVU8jf z@crw=GCg?Sc&Q0ls>9{C{*mtGqnTcQZwW4X-eL&kLG~lMVUSd^!8xJu8FFi1m`?rJW6^EVg z>J4y@8E!%hwT$My+R>l5yd+!_!T<$^i@ur{oiu7GF0!%$;NklqeNNgqGxS7P@mS!5EIVDrPK z%gWr^P|Wlw5AHwrJc6KCqs`FK{T;Sh@(spEU7O!sY)lJ$EJK|Z_%;Ry#&L<3SDY7* z+I8>x6xmH%Hf{I-05l(nJSi47C(9K>PgobKYy&wPS5;r&+Rf>5<}6VJy-6yK%l!%} z)R>)(hb9>A5iJsg*uRYDpC{$flQfuUPZ4=?b2#Kn#uU!>-~b4D)K zovg$0cW94I_Y{6K>Lv{>5I}37*EqK(D?RLu=}%wl^PWC(f6^9FS%tdOR?@&V2Lc>( zd?wy#PwH%ZZ&8p8*EO2cEa7Mu_0O*}sF6GW9CYV}y?aeTL0+$!y-nQ!Dc-95vo{ZJ zoZ+sLX}z@(_qGoP9Gi)p{8|^gTYE-JCaQ{f!|GSk+5B#fgnr05%_2^pj0{&P^yS%``ocI-J+5qi9cW@o z8I;b4*oc%VcJkI>l;pSw@`uRR!^>57Pl483h6wBaLlU*a-AS!-QKucRd5W8$Ftu*~ zQxc)5y8JM4w*il0Lc#L?q3kTfvRb#cEl4*=BMs6Z zh=4TGozfjr(%l`>-Q6K6-QC^YDcud5oWveiT9ZY2McwUh4BkVFW3F+xab52t}<(%krw{)DWuk4(>GT*y2ad}s9 z`aMcRBdkpVPY$Z^Q$8qQLn0EEz7JVTZQJIAGB{rQ7{@Xq}-gH-CH@RkpkG zJbv^?u1Qi<_pfm#RapRO%)%a_1B38B_c2wg=LXb10+iI&yPES@Vm{F)6>PV1*HoCz z)yOjZRx*5-7Z1F*++V)DPI|mEbHJK7-Fr1xnwkAz)G5+-k0!OVx zx{>v~^@%qm`R$`AqeutjUspxX7T7=n*s_Ba5~E`_AtjGP8Z_niNX`c^bhjB;xuZ$%_)>>-in*XIr!SV;cSIVt}2quGr)_ix2 z3NgVb=%>_Rz~01t?!EC375)H<7bNZ;HR5j64xAK zb^Ne???rzd8e}sBdtB`Gs~tCs#vi@oIqgkekQ=0BpZd+BX8K;z94JsRTu&B<;S{ay zF`LSWChXo$9n2Hb(7;1nBKBGbOa9A?fD3xXfiP@bm3JA79)_q`)t3xf(z~red-Nl@ zMltpE^>qu@%YOgb2Ib*Sju0;do)XV`KQ?8*k7){5rf+vH9!ssJ55@SyUsV!!|-K zE>E{acd?rd==YBd$#YXOPlc0`n{^rQ8CQE>5pf1IB2i%gF0k081g znGU^&_r|u`cpWVPM@<-gmhm#Ao%b5ICwEK7=~87M!Dqgkah)H`zJ$S`fWx3w^|o8a z0jHp#czYe}(MEX?B2RbAQ*AJ7IfHjun3oXMP=@dNg670enHB@XKQz=&T&23bp#;OA z&Q$zvFnh9XSe27zyWsk!b!PPFWvx&IoXa-N&TLWQOe&)ZHlHTfhM?&Djx=#=v!RZ~ zqr4pIK+-$J5Il}IA(NZ|sI)l% zTDZ&9JP9ycp>OJtUKhP8VSMCEN9Rmfo$zXX3DLq#rO~Z5Juxe_H=UDWVeTlqnWDQ z$J#*j_8vlB)KuV{bGh{C&%}1NViYZrP{q=Xn@oT6Jx;C%!>>HqQ>bOAYe)2^MN|TDSztG4w z-p^F|h`}6)e}{mF+E-53q^qZ=B(qBM`3(rCH>8h8z)hy3H9PyIn} zWbXNu{EL@U|4>-AzbNbz)h`M={D;EM)bm02B$6=)-K@AkwcbqVN-y1tVk$>jLMt4W z>-GCvI$rD`fS4$^_R8jJ&N45zF1bLaNeuqDeN&(|O(r*6>-qqZ)Qw*dir4vBB)=<^ zfqZjVPP6N4oF9Fu-IFVe%~lYL)vk;;OO9129tX-cXICty^%J5tUK>!O_7nCyWKvg;eGgc?gBQX)aXQpU7%2;9Tkik{VI33WwT!)O`x(s3TTLD?uj^8R;0qn{&1_j zrQm9u$@k|PdunMSV_vklKJBRYW>y20;iFvZ$D=r8aecU_d^tEhImTIeLe~GjIy)At zsR_*;luS6?LR1z>yYxvWtVsEZqEbU6r|pb*e5URbaPnng@v|R48O7%2<{mSAu=v5q z-U7Tcy(8uJ)u?CTrE`98bMtXFYBX}_vOe^6dHCfn9fJ(UG&c=bu-_)^Hm{9y)ld=} zB3JPP{zm)Jd;!)lVWT-AUE(L>M_85PyAsqUOy%QAYH}Ys9NhP=qB=?>)9~7;LQ_T$ zbmNNy_Pz2s)`kuN)qg!j?2Dn|DyheD!E6q}%$n(i+}_FN48OJ1mynR~hzhDL5Eh;q zRw6Cr7do0w1K#HAo+&* zrSNWVY2)7Eu-ffI0agrBe4VFr7}(u$b?>Wi*<3 z(HBJlKV7Vbadt&vvXdjOY4#L9W%xDKkkvRO`gU5GbE>MOG&a`Y2kz_Ttq$ai)SJ6+ zo!fyw_d@e8RN9`Vn>(D+E;J}eV14$P zAl3|}a*rrH8QU$BrJV5K*E zGZw^pN)%jbRwNqZj|zn<>STdSoBmK1LIGUDnZEGSgp1UdCJ!SFj}v`4?!sBrc83$l zG1Mv`9tZQhjGQka5%7Einwp5}iyriqt1Is-1`gzZXc*u8#GGn;uFsfL4Zxff%imZo zX-dueVabM@wZGfkc&(ynE0s-Z`P>B@eie8Vn0(dpNdOl1`>@?fUO)o4#F zEEGk<+|YOT*WJE|mkSlU?n%JA$)$XLg?B^>3yL6!3F7(q!*TIETLQBb!-)v$V? z+mo8l$p9uVB}mN19t_8)YXZet`sU#>7L=)}sZr-{i7}SjZ3nt7LWW*E^%D&;rH zXq%q%Zh%?mf)vd|ZskMOapX5R;tDn`Yl{du=Q93`iRP)}vTPS=X*)igjVM?P&ExRN z3mmyGDQVSJI z0{1uOOnx%W5`bxbcxIXvW1yY#1hsBYP1ov7;_cmk9bEe5$WQ$y0}=Yxe@K`tkF1WBm;}&j(b=y zx5Jw|@cC_$-<`n{7zdSv(j~FEsn3Lq)emZ``SXo!md=9vI*CAn1PGtRh#Sw=WE`it(xVIlaE$=!U(vW zL{)b5GIX^Ob(+V_G6;Lq`NTswu8KyfF(>+pA+;`!Ra2Qhi7}X&+(sK}Wj1$oMENm~ z<72Lr1L&^8Us?N$Foxh`)}(uQ_c^=L3`j&CjN$QB<^<19>C~FwI5+!wX;hbfuoSP0 zehiPdO4pl|zEJt$Car|-rrPKT-xo^*Te50IM3KJGUB3hVt8dbxqgL7K)N^}Kz_ZegqyOC+ia=s)3WX}yClziW|uyC z5*qHoZ0EcYsA`rvYE=dx`!hF*in7)Jpv#k&5k#=LX+G;aCps(YX$044Yl2fJd2G@d z@|Al`OyauZeZwiN8iX>-`Vb#Nb-i*8GF3Zf90b+Q87v-FiWX@z7xR7DfO1P6o-F5+va=kny}RyvNcgu z(nq`PGvhfEl8goz)qz1u^#GJHuu`&T}R)tRpvY)IOgii|UoEXa?HWjVz&Jw71& z(h$onM=zc&=NGLET72+(LVXa}tvF8wUKUm;+yB8)z;vO8aO&t0iMU|7!w{onwcM0M zFl3iVf{KF+)mrr6W?~E>n=x?I#$~G_|E*IOqhfFhE$TjHnhn)VDEb{~zXD5^tLTH| zx~$24?=E7@ArH+Ht7>fg8gdXm8wB=Lq2IXHBZP8dN=B|3EUZ^Y2j6~b7v{hop&Yq} zgxClAqJEjX$`Fk@No;*_9A;1$dHbiTc4$#9^aO_`Lnozs5$Nm9&QZhwr|(4qkwN_a9P?V(;HWr-%Mhp zmA-T9m5EccrKD|~RLvrabx?37BvP}UuP^{ohL=;Z+|o|FbnrA;&onPaPi9RLD|$|H zI{H?l=rJpZKok6Q+l#Z&5vrdCJfhB%PFbkUb{{Ex>Vf{nEB-f}fyjOjQmc|f<_fut zA_uZKZc!TI2Do$U*D{_&O=ffV-=!@ppYK&t*Pjrw8qy&EfROCgdjaDlQa$7D8GZeF z`4cJlG|pd4@GJF~A78#TZR5M%c?du5a(}cI;oEy!hEUU_5u7NQcrscKZKC5(Jg&8} z?PI?H&hJSinO55d#aqV?s2%U(+jBvySvOOY#p$O*h!paJt3>ngmCi{G!5}J_hG`_MUVzaGQn` zW{RnqSNVwKx6HM%%e7X54d)RyEbB>5$}luAjc#Ih0y)z2Myua-Tb;{NtTd-JGiOkY z9=KdiN%H*0aq2Fe2My4`E16DgemB+Jp>@eX(W5u_CxlF4U)hI4!Qn9?DQ70q(BSf; ztMC>s23n^&(+zWZ+=HL4pUM>rsPiiy20XJvz3+!wY5eGaeG+>K7(=ZVR|#|5!DrB6 zSdSQ*1TRrqjI`Q(D!1zs2ScXnGg1Kif#nX+UXjp?mMF9VyfmH;Ru-AlGxT?x>k`?!x zP%57$Zdq)cd@t5GP*1VB8<%W{&5mz!Z?X?7JVEIDARDPG_R;0@Ky-2rfG-YeXja5gea<=vC zjk2R7D+wtXFXdx=U!b{L&eewh`o|QXG%K?8?Qm3CR)uO?8bLWNJ1qVHR1dRQS^>RU zIyr8i3hpN75#XJyQ#8smYE{(S{nt|18gfI4FV+3JN*KML)*PoJbH)8!AQ5}kI^}7a zrM0OL0nAEOI1$w#giLp`D7JjV;i=(+D9{91*sKD=QP-G2(frr3c;<{OA1EHDKg-B$ z@zNZ8mvh@1yN3IHDIYhW^cd!^ei*5=AJ_?xp`CZ(JhXUi4dsZoz_BoTi%Rru+DrWi zklC*f|;NT}kPO;(fvR9Pfiw!EBUt{PWmjAH%9?zIEU<4G_=~iY# zAo=5FIcUs)B6R^(w$#A)EF%V3?yFLE_-5H36Hhk(W97zbT5in7tf&<)1 z``C$S7yY({!6hh5OFPIY;Aa@rQA$BoMmH1|LrEVx@j1wHKI(AcN+xhJ=j>AB7EYGv z-BWP5#r()V1D*yj@7iseUqEKA3qK7z-GL8Fp$2PA3UYbRe(P>l==G$G-K1FDAT(%>oWxKuW*<1rXIXNjc z+_R0lx&RfQuB=d@3C8BCypJhXI63nw%Up)EY$V0H9a0(juEJc#WjM#^-{RU68q+YE zDd6cg-nDjLlt&)-K_BlmhojhQDwhhQT|{6%34R+cZrTWmfb08X=VRjVBwEhEHA2j`}EvhNFeiE5=nwI7%NvG8+d(6!v2tqP4`B{=Id{&&(vEN)8vHB(zBB5%|V*f8&E@5t>J0bRf-;rhO6h!$c}hS z?nqX@n(Mtea-Xh)jZzb5^_2~}bxkLBxD%FXpSD#*t z|HBJu{_w(^$L25b3{sGbB7z7wpuPDNrSNE4M<}EOW+@AAA!u;_(l#moN84n#fr)WB z!`)xBd+}r9Td`TjoQ=!J=;Xa=vlfSY!uuWRZ_*+(_vBvL_X0lguRGX-JR0w=be5Bl z6=;8%o6=nkZJ{Tm5R1U?kPnWMyZ!&tF@5fl`Er5!tMrW7Rm{z)a-Hqlv^) zk`9ggWgY@6!w%KE@4R$4&Ms8kW)&eK=@$I5VA^4HO1+bZsi^q@*9^h|Y@GP6uF)P_ zIHF~hE)30M@+QuO99(i$MF+s70#tQ^f1JyKU(O};WLGor@VUJAp$pCnvcGQ<=JpBu zg$wQe;KJASz!(5O{OL!;)72}j$%0QE2V?Je)!!B#Z;qhfyHffD%zKu6=Am%L%F}ZO z))uD6i4-m?mkjH}t7RLyV3Bmv6uu&tUl#-}@JEqrpGK)Bh%TO=PofXgs!CA=Xe zO%%XFhEz+-QQ{w5XIDyr>#0BFur;h*)OH}|xC6Ha zTd=mP#?Exfjc)E%&o;c#t;>_1Q0t_1)k5etRTSWd3bMQTUKQv)J$gYBDV1gLcgW|vL~$G5=W{G)fO;W{t`Co0`SHlx zJ9RjjBhx(6+$XkI48Qr^ z9vVK?pvTZDOnuP<4x{g9+X3e6kG(!|Q5FH-jhDIRim4~WQ>AUC$L5VxWE8A=B75J$ zD%g8sMr?eAPr9Wz;zIRZm+v62nT=uk(FeT7^@pLSr$+`8Jye^7CyE34m|P7u^>t1P zW&_YBl|m^#GC$-I6M{$ogpWoy_Cd(zI`~}Z!s{q&qkXT*7D6jy+_r~sfo2QHHf<8d zroFZ~r4zsLLpk2~3GpzM$#kYD%YPV_EDGs$sauM#Wb2y--jc-!sD+9oz*8F#){2I)FOez({XoNBv$(Fg6SA|h{)v8MEyb2Zn>VH@$@Ts^i4f172>T zi4?Z{UMYAG8oY~M4TdcDdvWhZOrymadghmtyovDp(>(;{&@uUA7$P>{vVATie{xC5f=@f70?NJ$Jgf|ncgp?Ec1KN3 z1yQKUV}}vr{BA>2xJ4JTH(OnyF^-69>?Y*{kXm5dff#hqu|wHZO=vuQ6%(l`DZVk? zZu%@WwYBj@Q!$sM`u0|s0#bHuJIy4F%t^pOF?Mj}BfQoUz01fr^xxko_##nW=W1oc zdYkk;@bE*#vqwp0_Ek95IY`5Jw^Xy20ch^OW;C2o>~KlDhQ17lIT5sBIlVa>GH-r_ zY^1I5AG^*x(kRmIzmC+)U7Ns7c;q2UjZea&Gq2!GEvGiF3i*MrTogrrg7h_ zC(g$5rx&(@`K)$D>Pxgs;#e2^$nk3>dF_NA21w_z?{irTX&94Q+oas@lC9YT4yHGn zvYT`}!fy9zcTo1mmaF0-;9v>>4km)`A`|Mak@!?KSLi+Iqh*FCi~be{_Y~eg4yO5q ze4VLcPUW5Qjgm^Sq2unYI%@1LeH5GBQ9!%$d!rpmh3+&>l4{d<4Orz=sdfqZSh~a& z|GRh0uDUlODgg_Pcr;Q+4IiMUM+G_|qeqJ7-H??k7M|3CFvW`VX!Bl=B3|6uSktA6 z?NwyE2xCog&^gm)+oPT$)racXq`VMW*XTbZ?yt;tVKjRj+w5PFZDMayErjlt+-blj zXJFu0u#GJ#HByU=x-UzYTALFBdJ>iGD+WfU(r{3?gRYUD_8;;P@IaYtpb zNAkP(-BXtUk&dVQz7FPae9n(y8Ijo?YD_6?ct3hY_ci9OTJOFWVvCnQQd@PQ7)(IYpA@AkqfQ6nB!V;$z2N%_Va$5}Xl5qZi!Xn#-Tf&>l3vu_%m+v^M5W zH~V1pZbnwQv|2MrZ_&d$YvlwqbDYts^=~^67SizxvUVa8_kj?FX7|u?M?%8Ee3kF9 zT5}+N9%?NMI1Q#?D1LkGG~?nE;ADA3}#WK^Kya{7_hfT|dj!3xlm zSU(UGL|AV|PiGs`;Nl$i?}?~T_E)fvpZ8vSz3Xg=o;kktrjXSd$dkS%u7H*+%=n%# zc1jRlO~lWjTtx;ITM{nP*BOXhYI$MgVy#}e=W7GZ82OsoxH)!za8naBl=k>Q?=J68 zeh69z7g_%HmmaCX$J4SCx`6dlkPMjoK`KhFB-5K6dnsFnXBjEk=58u%FP0(Cd7&cH zRg->>3McX9-9qa>=n;K;$14M79KY7_=thT&wjPx#2J-v%V^mjYleQ18vgGCU*GM9w zyJwX#Tdw}86qk*U^Ry?er&ga{yBxm>Noc6XjdEb>GMpL0!SY#cglZDdlTPCZxTTHF zmcdyz8V=<+$ZWWK3wm1v{c1JyO0W&*N5Jx7+x^8NMp^vKYuTgOk=%ayk)fkw^~UHFql)=$=IyEjsW8m?EUJ*kMm=9RE% zm|SWG&3RNLWAjGNC{_6(WCy?f2xgWnkS9EVCF2&H(wN!z&!lC;UtBIT6vrWRua!cimCo`0sv8 zY+KOm(|zSPDr?`xS~x+yAmP2Ic0P-W3Wj%HCLSrwDG+Rb{x218rIvW*e4#Q9X8^1` zj=LXyu$43xc4T29n(IEUv(n<^&qMwf<F1Fg_1`5r7*GqjD+y z*?j$>fV1I%@i$whP$T}|9}4`NV==%LycavM+WbGh7={acF$venXTIb2nf>#*!%@KJ z+OFibpB?2xAyQ(H(!bT0Rp=4J7yG*C7W*{Wi z-aDRqVQA1T$NrDRfCy=MoPiOT!oi7?>!O! z1LFeHctDKatrpwrdKuD8*~^JyrEx8Sxk^*gzAtlG^kB9IbU$XQEI2AmXWO)F*n>SS zbrnT6<)>!Hn&WGlj;EvKM`93Ik(I-Jy2Fo*g9r>dF$tYf^x+o5%n=c(8<%?t(`b0zyHQBvj7HPq<0@9pY?LtS2eb1D)CiJ$`(h$S2twlGHukgS z+zJ(4340iI5-u@Oy0a?o)OosO;vymuol1?Lez+CFzUxiXb2iA=FuZq+B?mD%Asx+s z=hpsJHrE|h`;@Q7?Fi)j$FxR}n)brMWHEs0r?Pzd0PTx5MdOK&7o*g^F5 z^-uK0xm?d4C>1L|!&ey2qI9_&H`!yA8V}?7hcxAyMKrFrFWukz{*?iFJ`ke9{|8jvV3Rcp+Vmt4$Hv$C>UR+u4R3M<%@!H<@Lefj%i+Am8r_@lG3f*+1w62O}M zw`5nGy*NxMj>g4>$#&~PFs&7le$)WYQE&6=E;2{l3u@o*{^qpxw_b-%dH8O7z$;TQ z^s7!~Xny1)fVV@nZU>Zz^3C`)Fs~lT6&u!Y0E-qm?xEAVHCi>kP@#|#wZ0^ldi0D= zrsi#3t{YsXk({4jS=o;AeW-NwGe}q8`K(%yRc%hiX}1dM+-Ohnn%B;j=1TB_@}gN< zkLh(@Fc5U53f905m2q638S+5CI&!;~PNI}<@Sef@;41bM;ioQeU+NglvJ8i|`rD4UAm+8)n0MYnF`;dNafO`TpupqHdV)zvP*tkyT&3^h4kUB1HtLg;ZkkC#2p7 zP}9}ICfmYo&&-`oE>8+ieGd-~7Zg-;GPJOD=_EZ$qe$F2`8U(}xt{;h_$h(*^UxmL`QORf%C) za}|LT`@wr`OrXyA2E0{TJ>BYs*BL=UJ5{FB`sv4Q^d^heSjN(*2*@o0Lm{9H#$hBf zvQ7W?<38vA@#6&fkHWD6HGplr*4-BT@puBhRvX!Rn!zYhyohXHDuu;wUv59~-P%C5 zDDqm|5Jz9!)eT@Xrro(AogvX?oT?%y4c%Enpb7#JPyj2!p`GaYquT9ja5eXFpScL< zE%z2Re7vzd?)LD)+@Nk=(a7>Y#1jmC4N_@5=?#_i%K1uyQLaRpADy%+>o^>=#%Rz@ z3-(eYAK-_r3g6REo2W-E-Haa0Q3YRMW-SnJF~M$7N=BMMYn;tI<%fU=zYcM6-Mu~`bObC3ec$*?F#J3 zN(bekB&lcljebWFR5^PIj|tR^D7%eGt_FP|9zL1P*82t)!vmkviUc;6x;ss1yMwbf zS`>(xj^~^75=pq_M0l%)WJWV?tr8nZH5WsDmdA+hUMfLEkvENBl5urdTwwawiOXcf zXQg(|EgkBu0GzlbP?{hANO6*Y6c_kH26rAZ&ZK0(_#oywQV_&}P%Pz`VxmL^$98`K z9Dam&bz=A3@pJSSe}XxST@;Ji)zcivXA~zItJ`}|={w#?gQJ~3^P!{Au0Z9de#bA1 zl);F`d(61a8)$^tY(wLu!+y~Q;y}9Uqx|;fa*?;)h?Qpp#Lf{Apy4*{2V#Y=L}rU9 zQwsv{INb3J*{gGvd)f$ZH{lm6zA^zU+vGlPaTySB_;F=(huu?JTMfVwJWrl(;CvAs zFtC}w(G@G*SpWt@c(!5}EU8l$BwALKhOzqx<0FspKP4AXt~A(#F;MhR7RdVmoUL?s zyQuA;*15L)^U*D;fUW7&o0k3eMP$Or1bRtCS|tw@uc!P>d!pV`v#XW}g=I+RSW|?* zp%d_00wgzbm-gHLklg;w1N;4z2cA8lM^)<_AG7X5tlg)*aZD-gVltgTYLkv15TTHl z)91(mLMLst)fFSKWR|9yWz5# z0&>l#1a^!D>67=U0X4;MfOuj)9L;D}beCV{MPi7?EK>sP0_;oG#YQXU@i>CmJzO@s zjLI5PM&mK4#aZK)1|^NQ6~gvaMn}|d%QetKN{0*i#MDYvprG^B-L*}l<|`{&(hV|q zdJi#GMpsww>ck`2yP&4ZLh%D1YP{_$7`ne2tgU%Rfxo?K@;SR^ZJsr~82LuMy6LWqiMvC3r&$fBxrK3yJ;ln4Q4$DE;_Z6Z@+MIdv-W>pu>r--ltbI4<&NG zUjntjEOTGi%|caJzxq0MU6$i&wXRS_L;e;?HJobjC36K+{~qANXyPQI=QV=#`4 z`jsfNQn`Rgeh5#ikZJm)Lf}FtFbOkY-?NX3BKG1PDoK08&8T>)s%>vkwTxc!{9Ssx zU#F^(<#6uG7a8&wQK+e>n;xpiK(}lPvgil)j1o|e(k!ONJWgY1ezYT`BMHkxpRH_v z!=dLbQmi}o8@-#7<`9qvVM69FGBJp_kFU_Fm5C(RRb>_6dVZyEPxB;qR+0gO5t=k^ zK`NF;+5NDfc@;%kVx-B$7;5>VUaEzh!5l`pK17>O>k1TLL}<*)Yj1wXZ^gnhx5ZKs zw3;6v5b-&^yD;X+@A@jnr`7SPS-ji5qzU{(p zyMiMt6#M6`MmcOBE(+)=va$r2j_U@evTL!k^Ays9!@eUs(;x2eA8_ODzC7sAa|Gc* z4hcQ7vh#iX^(o+psp93+5AC{=dwPfWH{>hUaRFmHlw(%ugFSGV zP5n3HIXnDUnTd(%QNGhAT!-t_S(Gqx;-yh>E#{70zxy`4;QAtD@?#2K>HYjs=Ymr^ zqYr(=wIdu%_0bC9HEd?=`0Mk_B9%arZmIaFj~-V`K$17;;o)vdK*+{Nbgw*v@NmRMO#~xh!u5Yt#dI}ou zZ4xsfxI`!)8cBqCskUaVwp&XqNrjlOUILAKcR)Jxci3KjD#b3=vNn&n5shivFPF90 zE#6v-Q%4Q!r>9W1V440@fisPSckT!zZ0`uN!?yBeMp!6Bu5zcI%Hgb3PCpY!`uC=W zV6^6w!z;B>7_rg>@@Ft{R!jhuY{X`bTPkOUoUJ#?nZdmZn{)w$>;gS?G(SYlbOY8&yvtuog z!9?nian#8K82#8_rrjm@^p?=2fvm=9&!?sF?_p3PHjvwpiB&xP*QMF6<$133HVhCI zus$EonyY5S6q26QpkjXzfk%L@jKU)FroYg_nJPwpz(!qoH{PI6bHGeJywF5*rC9rf|l9&6tojr)q%(B4${YlT!O?f0KoA#;VS@_#0jofGH};^8+I`|0hQ(wxUPe(s$ZFweH;ff8e1s5%*}lPkc%LT4sNL zzd943qs@=5S0I?QdV+gQp;qxfAIM}$HpxTPTKB02y+8KXzdx8=@}DGVaqDx0u=OS6 z$Ium1sdQ(D8v9)$5)yX$*ohxj6%t?kY)#S~#@~!oSP=K5a&avLhxh^uUSYpu4Ayg$ z|1TdbI3dF)6~IAl1FtuF6Cn$JML}m{R6IbzHMcKm!pKUfUO%2mX4c!|&6!PTa{epZ z{cFdqWD{xHCU1suHfrt6d-C77Z#HMW7fciT6ehv{#wijBonBGuT7bmM$R#vvM{N@EZt=eak$)uG3-1q|j zC4e33mQ%-;NPz%@s9M4^1qq4P$_*CgF4Co%)zu6ZN(uMp8U>TiPV!#>N?iI(D(gCI zO;q^QT>V!Eq?c4pcX*CAM-X}<(Wre&*vcf!IGm`I+A5d(^N21^m+o!+Fzr6D7^{m{ z0z4ujFmSFq1ONp?JY>cUp;D`#z}KnZd#JKV zIvmEnYoW+Bwf8xI@&g)Se9ANj(-T4x|I1N(K0ZJ2B;}%nA32>QG!8mNk+qi#&9xqC zL0N`tEVBdF;YA-Vp)qqDyl- z4Dql0%!U+_DLbG}!g~Hg|BU^b$jTb9lF?HaRjGD1MH;(%PwsKJSakouUk*q+AcI!# zqyC=zB^S{q5WoPAUJhm#to(Xj;8=x&ML_XK}@s^iz>&bbHPdcZzVrxh`c|!ki23;lwp1pm$RGCyo~_zziqb_D@mwE^+m z>GwNJm@LMLF+6CJcb$qcPEZGSVKg!wJ7dq`B#Y;8()O=#5(}h~IISR`Klv0VTHjuz z{9q5A2AwQA3f&04hLlxRMpZ^A%=*gl~aUs>?UGB z?ZZ*T`W0z_c|^AR_u|xSlfU*uYyf+O7X$Q1wXktu`hpIoa{@vcGt~nmk`tK%KGKn2;U8tp{A!JBBzxv#iSP%7sEu|Df?a2veWFzG1RQ8@e%AeI z^Hn_4{D-kmOQq~LC>_VIkN$%i{XDzn5x{ORo-&cu1!5qMm4DzaySACcs!1IRHCkCj zB*2uD`kzL$C2UM_Ys?ra*&FQ_^g34>@wRPI`ENf@kblwmv*$PnL=`qr%1mnsaU?zb zXUoCf6Zid_t=g2%o;)mSRAzm>wF+=Sx{Ui0Zakb$n z=lt+C-7U*_M6@3FpPQwd1dNpZ@Izp+zG#f;=cvf&tLuLUoL;mrU`5ssfaPV zt2$U|)H-V80N!qW9JZPegIAu*GqsU@5f!Lyz0XonI@Tc1TWy+f_G5Dw9Qwq~O-ei=6K6FnT%$BD3kvb$Iy~2?H$hzjGPCe)E<< zA#Q1<80H4k2Z%H-^m!=uqx+p~zAsBpbP?hO_VW;%=Ufvu3&^EyKRDw65lwahpGW3kmTV4(%tn5tV8N-zMIu{ zpN}zLF1L}<9BglbS-^iiTs-!LTfUXE+mCRQ%gl&YumHm}LQ>hm?-21k{Sfg3tjyq? zaX+f>WjANiE;_XW3)4k6-R%%iKl!RJ*7Izy;346<9Qo8Q=8iTyoNjsaHKDDZP8J^T zj1qr`gGD8Op~Yq9J^6G8y+x;Jzy$x-kgR_%NnLP|@1`>c%GJ+zZ<=9b7t%~RhZs<{ ze3`GoQ?C9(I9IAQ{@MP>D^wEz2i7Ki;lxyPF(&=imSc-0k&#waCYc=YPS_9sj4Xjn zU^dY%%irCp42w#F%fJ3R8vW0P$3EdXZ93sz(`_+AxbtbpduqIr%f~Y^@)ew8W3z#Z z?gq_s7pundAi+ol@D6Bl2r=eWul0q6CsyT1q8vJ2fp2+;k5&eF|E@01i40>QiQ;?|0`rBg&Y{Hw)R z^*m~(1lTDRWHE!#{G74zkjO)ug#Q{JA2G4Y)XzbK4rzL_ zPr*o~Fy`^C(Tr5O|JGn4#`EaU$wCD$=i=hGdR?(dRm86+=Jb_a6M^AV0B%6CxPTE$ zIIWdxh(26qY~C#c5cSruh^t&I`lD+!ReRp*KlVAy*>m0AU$PZY_RjYFZsky2iqUC@ z&5UQ*bg|$*-lhvAm%GGnP0nu}Cxnj8<`k3a(a^yEZ8xs( z%5r+WW|XiwD6tINP=BZJ<4efw?@E>m0Pob*if6xh`_9`jOT z>5>qErQGM;_E3AM@e}0D>9!v*o|}819doAMjVp=ON`UcdEE~!~Y{9NN$Rtb>a^OfBxUK2ot5$K*_d$8{%2EKp_p_rE+r%*T0yvX+!y< z=)TLDeS|rJfP#|JGT$zc_K6OyD*a=F=Fpd&g)-s+jT$IGKYjtU%#jakO^Y2?k?ZDV zT1qKhqgNp)%Utt1{+g^SBb1Tt94@t~^Wz+AKc# zaAhQ7%94mpj&W2diIvGY2o)Y|VURODa%?l?T8zk$#mq7Yt(it6gxMUq%`o;mtv1Q` zANcY5KFX1 z&3JSys%-#pSH6gb8!H~?u$HDottVMVK$XfW&@byf92h<|5`*fI@ zAvJPhuLP0aDo+sHbJ%5}`B%-J(WsYFq09S;mP4fnW0piC!r(zSK;V~PulOCH_r|So zt73%w1;;J$%5l3+e<~{F@@}8C=P&G0)_+ia0loGB)pYRt`r=0 z#&G-osq}KOE@4uP-`^f5s)UXopZ!j%C#ZI%aHV>OlJmK>_$y9SOp=3t{i2<-sgYCG zXw37+Td{>I8xuYuIw|>nKOPD9{_%GjrO%B`-U+I| zDJLs1hQ{k!=(kR)lPFU-^+369o8h{UMBCiYC25apZuO~vJS(>Ixw1g8NO~a^(h7Su z6EzvO0xs~A^QM-F_rKO}+rp0v9GYz7g!V2Tio&)jAlvKlfo|v6UKL!svTcYz9xERQ?A&NfO9LP{=!GCm%$FsF+n{R>(wKXCco zMs3N!XUfkZ^>^;wd?$jyxgLk2ML*rhw5khDRXB^$@6APb{{=HM24wzu_4g%UIk9k3 zhNEA;29_^o0FZSP8Y%aD?39TNbUc-Ae4Uvw4X0n|(Xp7TdWI?0bS!thI}b2N>Y~in zk@W^w~BP49@vsOJTp`lR@f83d_Xo4{S)OVIhlZ6%i2o3r!$n#;}g1>D_~_o1%Gos z^Nze%-dZBex1hIvGkZ%RkU1IX+G*))LIkrza%|ZcKEs6j?Lpz6Jp^#hjZUhhAe_Ci zi{m3N5vCdR@9La`({>wUSMy?TtBh1&mpg-tuGVdK=ONt8{KSMl#gaXzUp$X+VmcBK zgpaC`e=NcVFzaK;On8V;(oro_0h6cobjdltE&^uaMKKtyCB|B5=W#xkCJJ?ashBpd ztLbF&s;RL}pH)yoU9>2h2Zn}O(UEX}dC=SMq zg}TUQOp)TEX{roqNKJAY7X@X06s+~9_w8i|daXH2|L{4(qBSmJ-CS|u6aQ=DZyxlV zkwj0NbY$La6m2&u(yEyV_};9tNr_v5oxe|q%E^eLqkKexf`UR97ZX;1f`S3QwQrCR-oDLtWJ;i*P-@MDgyh79 zgn)APHpXU_Mo>^Ol95EAO;E*VK`Q@s~ z!fAee5EicbxnCOMX8~S6OI<$m{ZXWR4Dp`9Bt zl8(x6a#1iPr34R@jH3xhy4D-$qpT8d^MQq%;m1?+6o`r^Bql~ljywT3LR_lUMyxe3 zES?`-rQP ztIzXuT>M3b^2R205Jj2~q}-L~Lcmy)#UYwHnDq+gi+Z(EPr0oARJf2^Fz82GMO22` zlom^rt1Et4SH<`NRdiF7Lz?(KZ@=HCwE)Hsm(POlO_+-UqwK;se2OLpcfLhde;EMT zM;I+)g?*=D(mBPyGTL(6A(%t~r-l?wjLU|H5pD6wQyx%0`&0 zym5AS)(^ZAj=XfX~p%CN1B)*jTyca@#;B&gUg12Rm+BV*M-~FMZaA~J6%VA!9>$jYr9}Tg^NK@ z2l_WvVI~S-#dm5fKqXY6J9iQuZ~lb5lN>Ij9% zL6MFy24xbtweD%B_Y3YjicFAZ1WkX!R;iuSFXQhtr=KfuAlMLO*SKjl8=Pb6zNVv?vBiDbzyr13b;SW%c`Sk^f1 zNN1tKxd@XH`+{hs{W#O1s3Bz`njts6ioMSI>~=oFq-L={#fbA6u(RSNsY$I!PslRJ zSAjP%d~pfC9}GS0p*VfNlHkP*4|)tz{$~H(wk;5b(igBPf}Dp$bwy?Ig_~+L-XflO z2&kN3E^?DCB0pU+T#P%zb;x-rbf`%|K_Nt;T5uqtUr-={D}fkK_r;0IE4O6wt66)U zaB6x(YD1cHm~-@PqD)cRRNDda_4fyJcl2kpXU69|CdEaOl%lUn+PU8gRX?s5IQ z8T4!kYVq$Q?E6XjS^Fh%!@^3!Dp7QdswEPJR#w7YI#4{W zSg8Q2_*3FL%`m%eW@!F%lB3v8bn6Ok6?p|Y7nz1~M*0`yhX1axw2gGEakX*VuGQ$c zh9xby9If0*BT^%}?qVr%X}W&T+-8=q=-HmsBKJ|at9LULk@C;HfYVneQpXSb#}iGt z%-Ll{gvB(otN9)xkb=yh%tq~I?UX;rZG2vZ?M3Zs9a69PFAfb{X~ScNs#W7PkLZqQ zerVeW%jipJ2MPmTBnT9=*@;V1nJV)d%~Hk&ET4-8X`)s5Un zQR?j*ur;l!&9j7)x4FcbO57EKSw~YXGEB5AR*mxXaLg5Tr%kWSYc93}29G{B%DfmK z8K#-FOyBxuq-s@Z`D)d!5_(8__nNn^xPj*MD|ahwIl19Uyi(_X`id zJ)`bC&z-KcwmpV0_VD&qkLPzQcL66l2d^BR3_#XwwrtP@m94;f2~bc@Y@#{)T_&Au zXW7OLVhbES3Ll{$YA9L>nj5w`0X3>sW|$0mX0!KUkJ;yG^68H~zzDnszGL}c>UZhq zYlS#P!LpU9FD%`3`g+0^cKS$IDOqh+YOaKz3 zd?6wGYT(i@XKZQg^?{Ry>Zn*)I*x=htvrdBzD3KNf;P^1%r31~ zPp0Fma03nEU`on#{BU_gIJT8%1m{)S6>6*0u%)w>4XrS1k&{08F(Rvs>EegvD zQYrdrt+~}|e5A3dF}TEi)H-)UrA#|Yf1(X?@)9Cw`T6U3HE5BLE|ELYW?5|Y%i4lE zmd0e=pzHLKv<~$&wTmH1a%&Qe{o zC!(F`pQs{k1Lua9?)w7cq}z%(Egct0XP48^1&453>9h4*$DayJuj##_dem)&3ubm!m z$Ux(p_o-#y`C^}9te}5*EQzp!LyOaX_TVs%GiW|?n*G^2v!k!Y>HfBE`NSpnY~@7kc)yjV_3>DHw{0GBb&A2K_7HPkbr(C7 zcR9cnT?THvYiplwx^hj@nNOgMl_hpWZPs~;y&6c$>@K=4+G%@eV;w)vB056dZ+Y?T zzskg$!t2aj7U9_6r?{V{J_eIC2e8s@+2+=8f|A%fn6 zXC8j-;p{0M?JbPA`8#yPv@3bm-z`1SZZG%b{LCSt(&+~fz}qeeDk?zzIY!7HfMuwJ zl{1MLn;{E|@J5Wle4-@IXb~MD&=p1GpMmk2D1%C)u8#dMZ9)zcz|7FWo&D}z7%SUr z)W~5^VXzksIS;hyA6pl@E`__`$gzvH+2?VhAZ4PBtj7+hs$JyE0@!njw_U7?k(#)% zv@{g;TN?=qI@k;f?yUv=c6@w0prBymf}jxJ&e(5*E0*c%yHJDA!y`oiigys4TqQ&MwOla}H#u(6`kGqlk+qI0#f{YwOj$Cc}?X=UW7 z2XwWvw07WfRq^6FJwp{e|E-o%~F3fZ`_9pa+A#A3+}azefI#Nc=m@|LT2buG!Pnz&JYlFPY6~m!|VUy$U)PBh&^=nJY3R~d+xlrZutt%+3 z7^s!%81u(N_Icce&ZG7FACJdVx+b`qEAi8WleN`GZf@olLL3~NH0O!j)r^e7PbRdm49yYfA`j5 zhQLGg$bpl7%lQU>Yw(A{JSV_MgE66ofk%0to%=29E<7nwgxlq8Y%aIIzhA;V_#L2d zW@g4xdYI0>@%n0SSX6v)Fg`9$A8)307y&MK^`#~U>9d4Ce*@{dKYECKmmy8RiQ_gN zq{Szd)$G%ktyHI@gXd@vI*$oNdJvmpKWrFL!Qb793jFd^=QjM|;qHq5U@f6+ztEzx zP?G)_?{{~BP|$P(5jV`Zy4Up0ip=%?Gi2g9nopqPg>RbmAq&Dni&NE38ej69Z6~QnX_70CuV?~H*M^|tl3?(_;u$l)9E@gjdbUU ze1FLTRd^mtGLfy#%k}v4-FA3*-rb!mwPLg53_d2>COrrgHIzstjfg>%OC}nn0HMn; z8~@|g?c@1!!_H#UB?$9lrb60cj@7ubhB1l)2oxiA4B*L{uSYFlthL+hMeeq|4;x09 z3W`kIKTH#u82mx$ByVW%A^|W9x%Q_3PLgXq8-CMHWiuBZ=e>dB<@P4^x)^;wE}uxR zs&d|l0M&E8ly)}pm}Q6#OLTH;LMy4V8XRt;Qed;mS&KCRi8pb+mp&jBUJo_NEU@-^ zn1+X?!=U;6wWoU>_43{`K3W;xS?QNtj#OoE%{DCTJ)qWDF)JK#$|py4YX6Zq+vv3d zvm=eo2CFeLjok#r!V7C)1ZV19QVf`0Dp>1D$BR3m8PXOavSLTcEaOokQeQe#vZYSY zD(JNtfXZ_?1+qN{W_Wl5+`Ttx2sUpXvX!{CpJ~9&5ArXzvikVnKB4lYfSDoyJ^m21&-ucijEp0Hi6DJK3tcyOMB;-T74 z1Rw<87%cq9J2KJ)*XxpM><0NB1jjwA#)*8FwY=^?)XC9#9ZBS`tiFeaUAf?5foFH6 z7{n_(`gUQ(LN8gIeqLGy+`E!->O@$?cT-0;9g;d=c4e;N%snR@&2SU#WK22Dkq3{8 zJw3sj1G{dD|kKj14ApPjyYdARxU9Li}dd=ug|?)6!Q>n4$foA>a09m z4~fLSe0Zr%Hhy8%%x6uYQI3B?`)G>{f)s?|4u$6%o;=)K59K$iD>LY>;v8u~3J&SP} z%r=pyiC3Mm*kS`}8?=JGDcc`gwkgnHHJ{J{kLN(+-4-bnNK8GTzfShD|H;St)bM4V zWWTvA2-*1;*z%Y|qsg4rns>6=VBnj(LYc8s^SK|Nqd;XQ%R$Cq_jS|Gc)6Fm_(J)! zK%C1guD{u3Fkm{mA?VT;ib{=l!=a)5WBWV!Vx5|2eL>BFv-zHD=5rYSe~QZ z(R4q{lieR3JX5?Xlr}{Jm#O>O0IzoYftUe4^t7y_&bTA2Jr&v3T4U&!r(j|KjQjbo zhbsq`Th{)aDL&(~uP&$*pGEr9=gPIx-d!GR%7^Or70aAxD3uzDpDu)99>c0sXbUB% zCBWb!J8zZCmw!b6^{aycSZ^^)?l^^iw_8J9pnPQ<0F4X*I#$Iv^V?&-)3fC*^U_?Z z_rN$=?#McvFBdXs@&|a%I@P%ysn`#LrU-d2KbVh5&4Sa|u-eH2Fp_{q#VQ3gaBJ~& z)dveXZSl{wmFU690s#_%yuT}b{idex zcz2G)SPDN~>$L1_^-%Tbdaj-@BJycSy*ZZg&a($3*KSK^C}zY&$D7v3*O$&RGA=I8baJy`<%0$) zEYox2note&Ag0>`>=F!;zx|i{T8k`(J0~*w6O0IiWEci6i^bm!fJRA7a^Zxj6!M}o z%J`=>7W+E%~@jZC6t_zz{}0U3S56BI=c|N3lSsj}OTNRx$y4IZje+^(QSzVe`_6R0TDX<#mjv%Ky@vf zSgR$>a1#PDnOyfxLV=T6i=KZHaxYeS`T6}sRa0OTmn!ALKbeHD?eSIY>Z}0${x~0i z#IZjmxt|_IWVzL;4YM9Yg55y2<^8b5e3cx??%p03CyQI4wI$SR>Qgu8) zSIEs8u`8Dx+ej|E9B(UYz1;pB&Qg4=8@4Du!7#TL!yY@EOHfug`X-2gz`9tII(YDS zk^1`lC`+p)BXfI(g>x3(IP5KUHgXr+I3QZN6^wyk?kqT)ynruSX6o2Hpp;C{+{AF+i>CS0Y!rTWJFKohXPVd4_P|$DTM_oh8B6Ww%{M)TEoa0kz zs0jK|IQ!S~zD$g&wodPNF(D&HL~KYWOyCz!xE{f8i0_EwH17frVc=R|<+g%~EI+0e zuF)7FM7vNoP8hqo)W z++hiP!dnaec^-RXo2y)kfvZr)qq2NYP#J~?@Ms@iHGa1ni!bd(5t_$PA(2ar~6o@ULS#<|$h{ z*(eCrdsn}8OTAhJeP6xAKtM#OqD^HjFv)aPLI`ds*Fcy;+uJqJ&S5&=L_P4!+?!Ta zEJT@}%gN9|hxHdO7JBa{4miwqcRH*hvB5${NQ;tufZz;{HBNhDw!CY6=n4yepD01A zQ4J%ez z=8pN^wYy!k=6#j_S@09lQhma#lyq`6bze}%Fel#(D4D+U-oZSJI(nl46p+=e@MXbB zx9P-F#n)|B3`3+<`>OKN%mw|gaAWS=T#Q%n!gm;O8bp-CkuT)Au_38pb&mZIjUxV` z414(88W_%m599qhJ+40yBtx95AK)ttXO0mvJ2@y65a~fN6r=>-|3igZ;TMwDN=N10 z>vnjh>B$57X2hxpz>O(W&4bYGeS0rC#$-F`6$MQ3ctu%vX6zu|vA=FZRU}CJ*r%7_ykj0tsy9$M$~5 z8Cc37c!AD+XP~i>bm687z{K=BpE}VZNxvHPgOoHnUS5J5JQo&DJhcyh;*0)d>FV5c znvJdSCU$)SJB}AGry!g5L?~Ddeoj`WQ0@sQPqZa(Sx0`-1|JdqL5DDP*BDKdvRc%k zg>(ZRD|FYQN5972CocCcpMk%epk25Q0-iYQWq@f*Bs~U;ikMsk*Oj1Tw<~P9}Xrv2``iMtojAZWu zt2}6KDHdytEP~#t_iZGCb>#wwUG^t7Vj5sn?T#vvXCIYCmtp!#<`~pt+codA5vT_q zWj-10A`K_>P&7UWv?=TN=*-YgAvM6Fu#f&k*^BRxTPo7jUReUPm(%kle?>Fx@bwQA zcMcyO8>M=6_8#T-=V!sfl%&CLCSXDe2!k@7(nUEx6RBDY3CKoOO?;5K_|k$->Cy;k zaf4-g;*QmP@$KrDPRsBUc_NO(Xsr3y!s1_w7#A%zSndtvR8S}vn;wihZ+>z!%d)<= zHwUXh2Wr_>4^#KvtC*!|dUjbiKOsJ{k0bb+k^m*&M__-}uuXFm6wqZqx`Gs(jbci0 zY|R5P(cx-Zy!3JPdFYvgH>jMASq(8crZGdrzsLd0$NQ4tum6~6Kuf#*CvX2NTmS8a z)cZMu!z@D1L8mSG6TyFT`t=AAekF!}nC*xP14WLvB>&&3{Wcd|0Q8_!6Vuzc|IYv4 zc;BFH6UUp>?!V=ME~GJIK8BIl2m==JU$BJ|w?HrjbTRy`ihApe)a-$%n7AW=g!~%b zSljdI1i*sQS6s4J+`H5t?`7`onL3=!@P6;vygKh~WgzdM($!XY!9ULPwrmukdK9*p zxI7M?-u0AZ$t|f`kAS}z@vU9~B-G2jw_GV}f(cp7WLhk51%5eHMbn+W7u{D5k9mZo)4M@;mX#i-L|n%DgqF}Cb@WJnQ> z$Nl!}5lCTQuHT_^C3l>u(RO%`0=6K$Z^8=J({VbGuQ^xrQ@sVgNWCdL6muhG-Zjz;54P{rZs+I^GV`?K)D^VP%d zqopLeR{QylqNj{d)y5ae1ooC?S?7VuAw3r-nvVGwZpteg)k3+oZ*gGuC&5usnY*OO z-|2~6*M|Lh3Mt7_5nV4mzs+|ABBSjwQ4)Rf?RBD8^L{WP<|N+en@wdPU+;5^Uw4hO z5kJXuy)*}kdf!R4O=qs0JJ5U@BL0r2@_P0fF9oKR&LP>m-#;#_?oD&wMGDm9(=slL zkIaDp#Ii3b4E8%?S-2eIi)Dk=mb`$Gi9fmffcu-1O>6d{(OfKH1<{2?6GPG^b)DUw zKuhNY*(Cam$D3zQM*}b*2M0DojQl|_3SuKS-~Lp62zI(VFyQd=`gNmIsyl zKbClMD0_-C{6;9e0G%n;&W=AzXj*phrel@Y8X06QN^N{luY+b6Sw|H-TA{v~P^dO)ivxc#dvbZEFLfwtbe**YxL5$F`QEm> z!=UaKuQlqI=?m(jjfa-CiCUc$?mu?y1Co1zT^_@y5wLP^w}-VhdJ+cIEH>_hKdHMQ zu`=mrHTV5eS8Dl%tW&F`2U9th4*uQK`8d->cgUXwi#z>5gZ=tRWTqZb#Mg&-L-zwo zFmX%=qyJ{fnR+!G*u&MB$9I+QDITDczX+`1?>6(i>; z7ASj{!aEVniQUYaqUrFhp)G?2QgxAn!R5!=>z84qq3>aYlJ8vJ5sffGQMmxv+y}|rLu_AMa-IR>LoMF8tAd23%+GfN&+bei#zi(W&UPtTMlao1+LRVZVT{#Z` z>IDelZ_l+m{x#bb-Z5IM5S&v;dA8{&_L4`%xxBx5wSG^K4bGItOf((X`eZ=z754yg zF+_ws5??r(QF+%P%8-JgbCP$lBGMTz`VmZiGB0^AkJ$XOeDXL|4^|$KQKjElT|(TP zt(R?ldb<|1FC3^ft=%d!Zhczd9dw^<)K&q(*JCJ8tv$-s4>?sZGhUci+zd1anmYo-M$qoT%2I;8>$!cgAZ(j0gMxT~sIo z_6bGy-f%6be4gnSnH;!N|4H46#eF2BQbo=n1jh?z>2r)nKa~}|ldT%x=DG;n8(f4Z zvs%Sutc$WVOd0vnFvgi&>#vc`Bb3;wXPf|B$$^lIpM>7pTLUANG6`xA4rbK2RG2~# zjPN3j1K7eL|7qo5jAJ7Ax=4a?Rj?An>dnlTx1YYG=|ZOEs`bdfVtt~R1a3TArt!Ak8Ai8m zS5v7wXl{9J*`EQ1OPwtw^-3Yi6S5Hp=IeL*hvw?vg_y`6ZUulAyq?b53{z%>^mQp5 zo0AXpd=8eF1Q7P+EcpeILEDqU8f~1?2=|0)XRE0N^qS5E^7L$}fpsvaa^o!P?}%y> zeYH0CV_jBEvL%~VbX&RXUb+f|o+qMBR)AvA{Ol2u?_+5>4szw>tI%Q4NPH(#X$cB2 zf24@J#hI+>#$j*%_`;|avL2xGsW4V(GJN%L6&I9NRTmh^sby89Az z#hG~cC2v^nSZrk}m{rpG1~Q-zd5|x2Pfx~h?3NqgKb(EO{V}DY7tXM$4?y4lB)Y}N z7Z|Ph@T2|e@oh*^*_9$54RMsX$&98h^*Wua42LbY_e6{Pd!#@`bG3`$52oqzkQ-dpeEQBEzJ@JW15n70E6 zZNBe`EB(D3UGeH*0h(*&u9qzXv^ho>iIV;9{8GDaW->$|5K z;5YoL!Jo#OW3UHn!-_^gpI8%>SjFdq%>jw`m@3W-v2oa6)lSZSuOl*FV&N(B`}w$R z6h>EB2aFNk;$5j`K6nFZ(GsA2uZ`T?pU1Y&FdUt6Is`0P4hKrStfW;3n(G1Vo~?7y zVPtoY7gOHKYDE?&*EUd7lHWQX@gd1Ej(+x~&&5xMs*f{$S;{A>1zV1h`JS;2U@h0i zTdePy>$e2Qr`N}~UfhA48GlUPTPd`S$2Wf7FLdQFM2v zR|!Q3jj2QoK%|`xtS-b$y;Mbs{Is8ks$K&H%U@i^x1GS#iKg#3BBFPV+=4nH$6Z_A zn{93Pb>lb37rzZT|BxidjRA`%&s6S$hXD$8;;^dHQ(GGf3oZ99#No6_f$I}|N`LKX z4Zi010z{K&4skJdn%^Zaj&?%b@TF$*kLo?0RcMA*wP6?j@;SM-)2bUzs%+3MH-2pJ zgy@)PTK;y@yZwduszylKz^pa!zvAR(2C{~SXn;a`P9H;Ayn4&pf8ZR9eEmapnc8t| z4YIz-)9USFpv}!`-DC{Y33Ao>Ynt7=A>=@E~mDl&-wF0Qh~Y^ znAoI1(o~6bXcyrn{@D-Dv-5jbGWx6TArRSD8YY4ouf~vs5}63^*irWkCKSq2mmi-4 z=)2S~p!2GdKw@dfKO{|Db6`RzsR=Cb<-$BcSL>FcPOb_bJkNY_>#jGzp;EhnnlS%{J)W?&%t;_u%T9ElkM6qtXB zc!IWDIZ~><_{hE)XQ{y;v5rGEv~>P7+VjpmGcNe8jXw7){j>+Y!!L3OSQFH|Gfau? z`s%MlkY1ItBzq;ph%R$b3en~0^yyqcQb4PRUiO>E&5&JCeVC1Cs#Ai6zKym$SO^`UCfR$_Ua&)C`PC=)KV>n5C6h9CU(!l0-u zOTP)5tsjIlF%9EgROYXeULMPT+$5e;`_^$DB-Qzi{ciMFShC2