diff --git a/.github/workflows/radius-module-ci.yml b/.github/workflows/radius-module-ci.yml new file mode 100644 index 000000000..ce133aa67 --- /dev/null +++ b/.github/workflows/radius-module-ci.yml @@ -0,0 +1,156 @@ +name: Radius Module CI + +on: + pull_request: + # Sequence of patterns matched against refs/heads + branches: + - "master" + paths: + - "scripts/automation/Radius/**" + types: [opened, synchronize, reopened, labeled, unlabeled] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + Filter-Branch: + runs-on: ubuntu-latest + if: contains(github.event.pull_request.labels.*.name, 'Radius Module') + steps: + - run: echo "Building JumpCloud Radius Module Event 'JumpCloudModule_'" + Check-PR-Labels: + needs: ["Filter-Branch"] + runs-on: ubuntu-latest + outputs: + RELEASE_TYPE: ${{ steps.validate.outputs.RELEASE_TYPE }} + steps: + - name: Validate-PR-Version-Labels + id: validate + shell: pwsh + run: | + $PR_LABEL_LIST=$(curl -s "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" | jq -r '.[].name') + if ("PowerShell Radius Module" -in $PR_LABEL_LIST) { + write-host "Starting Build for PowerShell Radius Module Release" + } + # validate type from label list: + $types = @('major', 'minor', 'patch', 'manual') + $typeCount = 0 + foreach ($item in $PR_LABEL_LIST) { + if ($item -in $types) { + write-host "$item" + $typeCount += 1 + $RELEASE_TYPE = $item + } + } + + if ($typeCount -eq 1) { + echo "RELEASE_TYPE=$RELEASE_TYPE" >> $env:GITHUB_OUTPUT + } else { + throw "Multiple or invalid release types were found on PR" + exit 1 + } + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + Validate-Env-Variables: + needs: ["Filter-Branch", "Check-PR-Labels"] + runs-on: ubuntu-latest + steps: + - env: + RELEASE_TYPE: ${{ needs.Check-PR-Labels.outputs.RELEASE_TYPE }} + shell: pwsh + run: | + # validate release type variables + $env:RELEASE_TYPE | Should -BeIn @('major','minor','patch','manual') + Setup-Build-Dependencies: + needs: ["Filter-Branch", "Check-PR-Labels", "Validate-Env-Variables"] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Setup PowerShell Module Cache + id: cacher + uses: actions/cache@v4 + with: + path: "~/.local/share/powershell/Modules/" + key: PS-Radius-Dependencies + - name: Install dependencies + if: steps.cacher.outputs.cache-hit != 'true' + shell: pwsh + env: + PESTER_APIKEY: ${{ secrets.PESTER_APIKEY }} + PESTER_ORGID: ${{ secrets.PESTER_ORGID }} + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + + If (!(Get-PackageProvider -Name:('NuGet') -ListAvailable -ErrorAction:('SilentlyContinue'))) { + Write-Host ('[status]Installing package provider NuGet'); + Install-PackageProvider -Name:('NuGet') -Scope:('CurrentUser') -Force + } + + $PSDependencies = @{ + 'PowerShellGet' = @{Repository = 'PSGallery'; RequiredVersion = '3.0.12-beta' } + 'PSScriptAnalyzer' = @{Repository = 'PSGallery'; RequiredVersion = '1.19.1' } + 'PlatyPS' = @{Repository = 'PSGallery'; RequiredVersion = '0.14.2' } + 'JumpCloud' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} + 'JumpCloud.SDK.V1' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} + 'JumpCloud.SDK.V2' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} + 'JumpCloud.SDK.DirectoryInsights' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} + } + + foreach ($RequiredModule in $PSDependencies.Keys) { + If ([System.String]::IsNullOrEmpty((Get-InstalledModule | Where-Object { $_.Name -eq $RequiredModule }))) { + Write-Host("[status]Installing module: '$RequiredModule'; version: $($PSDependencies[$RequiredModule].RequiredVersion) from $($PSDependencies[$RequiredModule].Repository)") + if ($($PSDependencies[$RequiredModule].RequiredVersion) -eq "latest"){ + Install-Module -Name $RequiredModule -Repository:($($PSDependencies[$RequiredModule].Repository)) -AllowPrerelease -Force + } else { + Install-Module -Name $RequiredModule -Repository:($($PSDependencies[$RequiredModule].Repository)) -RequiredVersion:($($PSDependencies[$RequiredModule].RequiredVersion)) -AllowPrerelease -Force + } + } + } + + Validate-Module: + needs: ["Setup-Build-Dependencies", "Check-PR-Labels"] + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + scripts + - uses: actions/cache@v4 + with: + path: "~/.local/share/powershell/Modules/" + key: PS-Radius-Dependencies + - env: + RELEASE_TYPE: ${{ needs.Check-PR-Labels.outputs.RELEASE_TYPE }} + shell: pwsh + run: | + . "./scripts/automation/Radius/Tests/Invoke-Pester.ps1" -ModuleValidation + Test-Module: + needs: ["Setup-Build-Dependencies", "Check-PR-Labels", "Validate-Module"] + runs-on: ubuntu-latest + # environment: Test Radius CI + timeout-minutes: 75 + strategy: + fail-fast: false + name: Run Pester Tests and Upload Results + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + scripts + - uses: actions/cache@v4 + with: + path: "~/.local/share/powershell/Modules/" + key: PS-Radius-Dependencies + - name: Test PWSH Radius Module + shell: pwsh + env: + PESTER_APIKEY: ${{ secrets.PESTER_APIKEY }} + PESTER_ORGID: ${{ secrets.PESTER_ORGID }} + run: | + $items = get-childItem -path "./scripts/automation/Radius/" + foreach ($item in $items){ + write-host "$($item.FullName)" + } + # Invoke Pester + . "./scripts/automation/Radius/Tests/Invoke-Pester.ps1" -JumpCloudApiKey "$env:PESTER_APIKEY" -ExcludeTagList "ModuleValidation" diff --git a/.github/workflows/radius-release-workflow.yml b/.github/workflows/radius-release-workflow.yml new file mode 100644 index 000000000..43a0d55e5 --- /dev/null +++ b/.github/workflows/radius-release-workflow.yml @@ -0,0 +1,203 @@ +name: Release and Publish Radius Module +on: + pull_request: + types: + - closed + branches: + - "master" + paths: + - "scripts/automation/Radius/**" + +jobs: + Check-If-Merged: + if: github.event.pull_request.merged == false + runs-on: ubuntu-latest + steps: + - name: Check if Merged + run: echo {GITHUB_HEAD_REF} merged into master + + Filter-Branch: + runs-on: ubuntu-latest + if: contains(github.event.pull_request.labels.*.name, 'Radius Module') + steps: + - run: echo "Building Radius Module Event 'RadiusModule_'" + + Check-PR-Labels: + needs: [Filter-Branch, Check-If-Merged] + runs-on: ubuntu-latest + steps: + - name: Validate-PR-Version-Labels + id: validate + shell: pwsh + run: | + $PR_LABEL_LIST=$(curl -s "https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" | jq -r '.[].name') + if ("Radius Module" -in $PR_LABEL_LIST) { + Write-Host "Starting Build for Radius Module Release" + } else { + Write-Host "Missing Radius Module Label, not continuing Release workflow" + exit 1 + } + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + Setup-Build-Dependencies: + needs: ["Filter-Branch", "Check-PR-Labels"] + runs-on: windows-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Setup Radius Module Cache + id: cacher + uses: actions/cache@v4 + with: + path: 'C:\Users\runneradmin\Documents\PowerShell\Modules\' + key: PS-Dependencies + - name: Install dependencies + if: steps.cacher.outputs.cache-hit != 'true' + shell: pwsh + run: | + Set-PSRepository PSGallery -InstallationPolicy Trusted + If (!(Get-PackageProvider -Name:('NuGet') -ListAvailable -ErrorAction:('SilentlyContinue'))) { + Write-Host ('[status]Installing package provider NuGet'); + Install-PackageProvider -Name:('NuGet') -Scope:('CurrentUser') -Force + } + $PSDependencies = @{ + 'PowerShellGet' = @{Repository = 'PSGallery'; RequiredVersion = '3.0.12-beta' } + 'PackageManagement' = @{Repository = 'PSGallery'; RequiredVersion = '1.4.8.1' } + 'PSScriptAnalyzer' = @{Repository = 'PSGallery'; RequiredVersion = '1.19.1' } + 'PlatyPS' = @{Repository = 'PSGallery'; RequiredVersion = '0.14.2' } + 'AWS.Tools.Common' = @{Repository = 'PSGallery'; RequiredVersion = '4.1.122' } + 'AWS.Tools.CodeArtifact' = @{Repository = 'PSGallery'; RequiredVersion = '4.1.122' } + 'JumpCloud.SDK.V1' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} + 'JumpCloud.SDK.V2' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} + 'JumpCloud.SDK.DirectoryInsights' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} + } + foreach ($RequiredModule in $PSDependencies.Keys) { + If ([System.String]::IsNullOrEmpty((Get-InstalledModule | Where-Object { $_.Name -eq $RequiredModule }))) { + Write-Host("[status]Installing module: '$RequiredModule'; version: $($PSDependencies[$RequiredModule].RequiredVersion) from $($PSDependencies[$RequiredModule].Repository)") + if ($($PSDependencies[$RequiredModule].RequiredVersion) -eq "latest"){ + Install-Module -Name $RequiredModule -Repository:($($PSDependencies[$RequiredModule].Repository)) -AllowPrerelease -Force + } else { + Install-Module -Name $RequiredModule -Repository:($($PSDependencies[$RequiredModule].Repository)) -RequiredVersion:($($PSDependencies[$RequiredModule].RequiredVersion)) -AllowPrerelease -Force -AllowClobber + } + } + } + + Build-Nuspec-Nupkg: + needs: Setup-Build-Dependencies + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: 'C:\Users\runneradmin\Documents\PowerShell\Modules\' + key: PS-Dependencies + - name: Build Nuspec + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + . "${{ github.workspace }}/scripts/automation/Radius/deploy/BuildNuspecFromPsd1.ps1" -RequiredModulesRepo PSGallery + - name: Pack nuspec + shell: pwsh + run: | + nuget pack "${{ github.workspace }}/scripts/automation/Radius/JumpCloud.Radius.nuspec" -Properties NoWarn=NU5111,NU5110 -OutputDirectory "${{ github.workspace }}/scripts/automation/Radius/" + - name: Validate NuPkg File + shell: pwsh + run: | + $NupkgPathDirectory = (Get-ChildItem -Path:("${{ github.workspace }}/scripts/automation/Radius/JumpCloud.Radius.*.nupkg")).Directory + $nupkgPath = (Get-ChildItem -Path:("${{ github.workspace }}/scripts/automation/Radius/JumpCloud.Radius*.nupkg")).FullName + Write-Host "NuPkg Path: $nupkgPath" + mkdir $NupkgPathDirectory/nupkg_module + unzip $nupkgPath -d $NupkgPathDirectory/nupkg_module + $moduleRootFiles = Get-ChildItem -File -Path:("$NupkgPathDirectory/nupkg_module") + $moduleRootDirectories = Get-ChildItem -Directory -Path:("$NupkgPathDirectory/nupkg_module") + $moduleRootDirectory = "$NupkgPathDirectory/nupkg_module" + Write-Host "Module Files:\n$moduleRootFiles" + Write-Host "Module Directories:\n$moduleRootDirectories" + # Validate that the nuspec directory contains a Functions directory with a Public/ Private directory + "Functions" | should -bein $moduleRootDirectories.name + # the public and private directories should be inside the Functions directory + "Public" | should -bein (Get-ChildItem -Directory -Path:("$moduleRootDirectory/Functions")).name + "Private" | should -bein (Get-ChildItem -Directory -Path:("$moduleRootDirectory/Functions")).name + # Validate that the nuspec directory contains an Extensions directory + "Extensions" | should -bein $moduleRootDirectories.name + + - name: Upload Nupkg + uses: ./.github/actions/upload-secure-artifact + with: + name: radius-module-nupkg + path: D:/a/support/support/scripts/automation/Radius/JumpCloud.Radius*.nupkg + retention-days: 1 + + Manual-Approval-Release: + needs: ["Check-PR-Labels", "Setup-Build-Dependencies"] + environment: PublishToPSGallery + runs-on: ubuntu-latest + steps: + - name: Manual Approval for Release + run: echo "Awaiting approval from required reviewers before continuing" + + Draft-GH-Release: + needs: [Build-Nuspec-Nupkg, Manual-Approval-Release] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build Draft Release + run: | + VERSION=$(grep -Po "ModuleVersion\s*=\s*'\K[0-9]+\.[0-9]+\.[0-9]+" ${{ github.workspace }}/scripts/automation/Radius/JumpCloud.Radius.psd1) + TITLE="JumpCloud Radius Module v$VERSION" + CHANGELOG=$(cat ${{ github.workspace }}/scripts/automation/Radius/Changelog.md |awk "/^## $VERSION/{ f = 1; next } /## [0-9]+.[0-9]+.[0-9]+/{ f = 0 } f") + TAG="radius_v$VERSION" + BODY="$TITLE $CHANGELOG" + + (gh release view $TAG && echo "Release exists for $TAG") || gh release create $TAG --title "$TITLE" --notes "$BODY" --draft + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + Deploy-Nupkg: + needs: [Build-Nuspec-Nupkg, Manual-Approval-Release] + runs-on: windows-latest + steps: + - name: Download nupkg artifact + uses: actions/download-artifact@v4 + with: + name: radius-module-nupkg + path: D:/a/support/support/scripts/automation/Radius/ + - name: Publish + shell: pwsh + run: | + # add nuget source for PSGallery: + dotnet nuget add source "https://www.powershellgallery.com/api/v2/package" --name PSGallery + # get nupkg artifact: + $nupkgPath = (Get-ChildItem -Path:("D:/a/support/support/scripts/automation/Radius/JumpCloud.Radius*.nupkg")).FullName + # test + $nupkgPath | Should -Exist + Write-Host "Nupkg Artifact Restored: $nupkgPath" + # nuget push from here: + dotnet nuget push $nupkgPath --source PSGallery --api-key $env:NuGetApiKey + env: + NuGetApiKey: ${{ secrets.NUGETAPIKEY }} + + Cleanup-Cache: + needs: Deploy-Nupkg + runs-on: ubuntu-latest + steps: + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + echo "Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index d95e267a8..ac7c39173 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -42,7 +42,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - Setup-Build-Dependancies: + Setup-Build-Dependencies: needs: ["Filter-Branch", "Check-PR-Labels"] runs-on: windows-latest timeout-minutes: 10 @@ -53,7 +53,7 @@ jobs: uses: actions/cache@v4 with: path: 'C:\Users\runneradmin\Documents\PowerShell\Modules\' - key: PS-Dependancies + key: PS-Dependencies - name: Install dependencies if: steps.cacher.outputs.cache-hit != 'true' shell: pwsh @@ -70,9 +70,9 @@ jobs: 'PlatyPS' = @{Repository = 'PSGallery'; RequiredVersion = '0.14.2' } 'AWS.Tools.Common' = @{Repository = 'PSGallery'; RequiredVersion = '4.1.122' } 'AWS.Tools.CodeArtifact' = @{Repository = 'PSGallery'; RequiredVersion = '4.1.122' } - 'JumpCloud.SDK.V1' = @{Repository = 'PSGallery'; RequiredVersion = '0.0.35'} - 'JumpCloud.SDK.V2' = @{Repository = 'PSGallery'; RequiredVersion = '0.0.39'} - 'JumpCloud.SDK.DirectoryInsights' = @{Repository = 'PSGallery'; RequiredVersion = '0.0.23'} + 'JumpCloud.SDK.V1' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} + 'JumpCloud.SDK.V2' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} + 'JumpCloud.SDK.DirectoryInsights' = @{Repository = 'PSGallery'; RequiredVersion = 'latest'} } foreach ($RequiredModule in $PSDependencies.Keys) { If ([System.String]::IsNullOrEmpty((Get-InstalledModule | Where-Object { $_.Name -eq $RequiredModule }))) { @@ -86,14 +86,14 @@ jobs: } Build-Nuspec-Nupkg: - needs: Setup-Build-Dependancies + needs: Setup-Build-Dependencies runs-on: windows-latest steps: - uses: actions/checkout@v4 - uses: actions/cache@v4 with: path: 'C:\Users\runneradmin\Documents\PowerShell\Modules\' - key: PS-Dependancies + key: PS-Dependencies - name: Build Nuspec shell: pwsh run: | @@ -126,7 +126,7 @@ jobs: retention-days: 1 Manual-Approval-Release: - needs: ["Check-PR-Labels", "Setup-Build-Dependancies"] + needs: ["Check-PR-Labels", "Setup-Build-Dependencies"] environment: PublishToPSGallery runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 2835b809e..e8988ab8d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ *test_results # Ignore support.wiki *support.wiki +# Ignore Extensions +/scripts/automation/Radius/Extensions/* # Ignore Config /PowerShell/JumpCloud Module/Config.Json # Ignore Radius Certs @@ -16,6 +18,15 @@ /scripts/automation/Radius/*/*.srl /scripts/automation/Radius/Config.ps1 /scripts/automation/Radius/users.json +/scripts/automation/Radius/settings.json +/scripts/automation/Radius/data/* +/scripts/automation/Radius/Cert/Backups/*.pem +/scripts/automation/Radius/Cert/Backups/*.zip +/scripts/automation/Radius/*.encrypted +/scripts/automation/Radius/log.txt +/scripts/automation/Radius/reports/* +/scripts/automation/Radius/config.json + __pycache__/ *.pyc # Ignore PWSH CSV Import/Update Files: diff --git a/PowerShell/Deploy/Build-HelpFiles.ps1 b/PowerShell/Deploy/Build-HelpFiles.ps1 index 5bce8cffe..2413e09c0 100755 --- a/PowerShell/Deploy/Build-HelpFiles.ps1 +++ b/PowerShell/Deploy/Build-HelpFiles.ps1 @@ -220,8 +220,9 @@ Try { None "@ - (Get-Content -Path "$FolderPath_enUS/JumpCloud-help.xml" -Raw).Replace($ProgressActionXML1, '') | Set-Content "$FolderPath_enUS/JumpCloud-help.xml" - (Get-Content -Path "$FolderPath_enUS/JumpCloud-help.xml" -Raw).Replace($ProgressActionXML2, '') | Set-Content "$FolderPath_enUS/JumpCloud-help.xml" + (Get-Content -Path "$FolderPath_enUS/$ModuleName-help.xml" -Raw).Replace($ProgressActionXML1, '') | Set-Content "$FolderPath_enUS/$ModuleName-help.xml" + (Get-Content -Path "$FolderPath_enUS/$ModuleName-help.xml" -Raw).Replace($ProgressActionXML2, '') | Set-Content "$FolderPath_enUS/$ModuleName-help.xml" + } Catch { Write-Error ($_) } diff --git a/PowerShell/Deploy/Build-Module.ps1 b/PowerShell/Deploy/Build-Module.ps1 index 507d25cf1..3227986f5 100755 --- a/PowerShell/Deploy/Build-Module.ps1 +++ b/PowerShell/Deploy/Build-Module.ps1 @@ -1,18 +1,28 @@ [CmdletBinding()] param ( - # Validate Set for ReleaseType - [ValidateSet('Major', 'Minor', 'Patch')] - [Parameter(Mandatory = $true)] + [Parameter()] + [String] + $GitSourceBranch, + [Parameter()] + [String] + $GitSourceRepo, + [Parameter()] [String] $ReleaseType, [Parameter()] + [String] + $ModuleName, + [Parameter()] + [string] + $RequiredModulesRepo, + [Parameter()] [Boolean] $ManualModuleVersion ) -. "$PSScriptRoot/Get-Config.ps1" +. "$PSScriptRoot/Get-Config.ps1" -GitSourceBranch:($GitSourceBranch) -GitSourceRepo:($GitSourceRepo) -ReleaseType:($ReleaseType) -RequiredModulesRepo:($RequiredModulesRepo) # Region Checking PowerShell Gallery module version Write-Host ('[status]Check PowerShell Gallery for module version info') -$PSGalleryInfo = Get-PSGalleryModuleVersion -Name:("JumpCloud") -ReleaseType:($RELEASETYPE) #('Major', 'Minor', 'Patch') +$PSGalleryInfo = Get-PSGalleryModuleVersion -Name:($ModuleName) -ReleaseType:($RELEASETYPE) #('Major', 'Minor', 'Patch') # Check to see if ManualModuleVersion parameter is set to true if ($ManualModuleVersion) { $ManualModuleVersionRetrieval = Get-Content -Path:($FilePath_psd1) | Where-Object { $_ -like '*ModuleVersion*' } @@ -24,38 +34,6 @@ if ($ManualModuleVersion) { } Write-Host ('[status]PowerShell Gallery Name:' + $PSGalleryInfo.Name + ';CurrentVersion:' + $PSGalleryInfo.Version + '; NextVersion:' + $ModuleVersion ) # EndRegion Checking PowerShell Gallery module version - -## Get the module version from the psd1 file and changelog -$VersionPsd1Regex = [regex]"(?<=ModuleVersion\s*=\s*')(([0-9]+)\.([0-9]+)\.([0-9]+))" -$VersionMatchPsd1 = Select-String -Path:($FilePath_psd1) -Pattern:($VersionPsd1Regex) -$PSD1Version = $VersionMatchPsd1.Matches.Value - - -$FilePath_ModuleChangelog = Join-Path -Path $PSScriptRoot -ChildPath '..\ModuleChangelog.md' -$ModuleChangelog = Get-Content -Path:($FilePath_ModuleChangelog) -$ModuleChangelogVersionRegex = "([0-9]+)\.([0-9]+)\.([0-9]+)" -$ModuleChangelogVersionMatch = ($ModuleChangelog | Select-Object -First 1) | Select-String -Pattern:($ModuleChangelogVersionRegex) -$ModuleChangelogVersion = $ModuleChangelogVersionMatch.Matches.Value - -# Update ModuleChangelog.md File: -If ($ModuleChangelogVersion -ne $PSD1Version) { - # add a new version section to the module ModuleChangelog.md - Write-Host "[Status]: Appending new changelog for version: $PSD1Version" - $NewModuleChangelogRecord = New-ModuleChangelog -LatestVersion:($PSD1Version) -ReleaseNotes:('{{Fill in the Release Notes}}') -Features:('{{Fill in the Features}}') -Improvements:('{{Fill in the Improvements}}') -BugFixes('{{Fill in the Bug Fixes}}') - - ($NewModuleChangelogRecord + ($ModuleChangelog | Out-String)).Trim() | Set-Content -Path:($FilePath_ModuleChangelog) -Force -} else { - # Get content between latest version and last - $ModuleChangelogContent = Get-Content -Path:($FilePath_ModuleChangelog) | Select -First 3 - $ReleaseDateRegex = [regex]'(?<=Release Date:\s)(.*)' - $ReleaseDateRegexMatch = $ModuleChangelogContent | Select-String -Pattern $ReleaseDateRegex - $ReleaseDate = $ReleaseDateRegexMatch.Matches.Value - $todaysDate = $(Get-Date -UFormat:('%B %d, %Y')) - if (($ReleaseDate) -and ($ReleaseDate -ne $todaysDate)) { - write-host "[Status] Updating Changelog date: $ReleaseDate to: $todaysDate)" - $ModuleChangelog.Replace($ReleaseDate, $todaysDate) | Set-Content $FilePath_ModuleChangelog - } -} # Region Building New-JCModuleManifest Write-Host ('[status]Building New-JCModuleManifest') New-JCModuleManifest -Path:($FilePath_psd1) ` @@ -70,13 +48,4 @@ $NewModuleChangelogRecord = New-ModuleChangelog -LatestVersion:($ModuleVersion) If (!(($ModuleChangelog | Select-Object -First 1) -match $ModuleVersion)) { ($NewModuleChangelogRecord + ($ModuleChangelog | Out-String)).Trim() | Set-Content -Path:($FilePath_ModuleChangelog) -Force } -# EndRegion Updating module change log - -Write-Host "Building help files" -ForegroundColor Green -."$PSScriptRoot/Build-HelpFiles.ps1" -ModuleName "JumpCloud" -ModulePath (Get-Item -Path:($FilePath_psd1)).directory - -Write-Host "Building Pester test files" -ForegroundColor Green -."$PSScriptRoot/Build-PesterTestFiles.ps1" - -Write-Host "Building module" -ForegroundColor Green -."$PSScriptRoot/SdkSync/jcapiToSupportSync.ps1" +# EndRegion Updating module change log \ No newline at end of file diff --git a/PowerShell/Deploy/Build-PesterTestFiles.ps1 b/PowerShell/Deploy/Build-PesterTestFiles.ps1 index b2e086dfe..02a539ffa 100755 --- a/PowerShell/Deploy/Build-PesterTestFiles.ps1 +++ b/PowerShell/Deploy/Build-PesterTestFiles.ps1 @@ -1,3 +1,10 @@ +[CmdletBinding()] +param ( + [Parameter()] + [string] + $RequiredModulesRepo +) + . "$PSScriptRoot/Get-Config.ps1" ################################################################################ # This script creates a new test file for each function in the PowerShell Module diff --git a/PowerShell/Deploy/SdkSync/jcapiToSupportSync.ps1 b/PowerShell/Deploy/SdkSync/jcapiToSupportSync.ps1 index 854aaaf6e..b1184744d 100644 --- a/PowerShell/Deploy/SdkSync/jcapiToSupportSync.ps1 +++ b/PowerShell/Deploy/SdkSync/jcapiToSupportSync.ps1 @@ -1,3 +1,9 @@ +[CmdletBinding()] +param ( + [Parameter()] + [string] + $RequiredModulesRepo +) . "$PSScriptRoot/../Get-Config.ps1" ########################################################################### $ApprovedFunctions = [Ordered]@{ diff --git a/PowerShell/JumpCloud Module/LICENSE b/PowerShell/JumpCloud Module/LICENSE index a00f8dacf..186fa3bcb 100644 --- a/PowerShell/JumpCloud Module/LICENSE +++ b/PowerShell/JumpCloud Module/LICENSE @@ -1,5 +1,6 @@ -JumpCloud -Copyright (c) 2019-2022 JumpCloud +MIT License + +Copyright (c) 2025 JumpCloud, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -8,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/PowerShell/LICENSE b/PowerShell/LICENSE index 155ffbf68..186fa3bcb 100644 --- a/PowerShell/LICENSE +++ b/PowerShell/LICENSE @@ -1,5 +1,6 @@ -JumpCloud -Copyright (c) 2019-2020 JumpCloud +MIT License + +Copyright (c) 2025 JumpCloud, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -8,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/automation/Radius/Changelog.md b/scripts/automation/Radius/Changelog.md index 2560f009e..eff891985 100644 --- a/scripts/automation/Radius/Changelog.md +++ b/scripts/automation/Radius/Changelog.md @@ -1,3 +1,62 @@ +## 2.1.0 + +Release Date: July 3, 2025 + +#### RELEASE NOTES + +``` +This release is a minor update to the Radius Cert Deployment tool. This version of the tool is now a PowerShell Module, which allows for easier updates and management of the tool. The module can be installed and updated from the PowerShell Gallery. +``` + +#### Features: + +- The Radius Cert Deployment tool is now a PowerShell Module +- The module and the cert locations are now configurable via a new public function `Set-JCRConfig` +- All configuration settings which were previously stored in `config.ps1` are now variables configured with the `Set-JCRConfig` function +- The module can be installed and updated from the PowerShell Gallery using `Install-Module JumpCloud.Radius` + +#### Bug Fixes: + +- Fixed an issue where the macOS CommandNames were not stored correctly in the users.json file. +- Fixed an issue where the tool would not work correctly when only one user was assigned to the radius user group. + +## 2.0.0 + +Release Date: January 30, 2024 + +#### RELEASE NOTES + +``` +This release offers a significant overhaul for the Radius Cert Deployment tool, many new underlying functions have been introduced 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 a table to keep track of generated/deployed certificates when using the tool +- Added password validation (re-enter) when generating root certificate +- Added Start-GenerateRootCert menu + - Functionalities added + - New: creates new root cert. If there is an existing root cert, user gets prompted to overwrite the cert + - Replace: replaces the current cert. If you replace root cert, it will contain a different serial number and user certs generated with the previous CA will no longer authenticate + - Renew: renewing the root CA will contain the same serial number and CA subject headers. User certs generated with the previous CA will continue to authenticate. + +## 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 @@ -5,7 +64,7 @@ Release Date: December 1, 2023 #### RELEASE NOTES ``` -In macOS, it's possible for a user to define their username as `user1234` or `USER1234`. When JumpCloud takes of a user it'll perform a case insensive string comparison and take over the account that matches the username from JumpCloud. +In macOS, it's possible for a user to define their username as `user1234` or `USER1234`. When JumpCloud takes of a user it'll perform a case insensitive string comparison and take over the account that matches the username from JumpCloud. Commands executed by JumpCloud in macOS run as shell scripts `/bin/bash` by default, this shell does not perform case-insensitive string comparisons. This patch version of the Radius Certificate Utility addresses this limitation by explicitly changing the `bash` match patterns to be case-insensitive. ``` @@ -109,7 +168,7 @@ Release Date: March 21, 2023 #### RELEASE NOTES ``` -Iniital release of the Passwordless Radius User Certificate Generation automation scritps +Initial release of the Passwordless Radius User Certificate Generation automation scripts ``` #### FEATURES: diff --git a/scripts/automation/Radius/Config.ps1 b/scripts/automation/Radius/Config.ps1 deleted file mode 100644 index a99a2aaaa..000000000 --- a/scripts/automation/Radius/Config.ps1 +++ /dev/null @@ -1,113 +0,0 @@ -# READ/ WRITE API KEY -$JCAPIKEY = 'YOURAPIKEY' -# JUMPCLOUD ORGID -$JCORGID = 'YOURORGID' -# JUMPCLOUD USER GROUP ID -$JCUSERGROUP = 'YOURJCUSERGROUP' -# USER CERT PASSWORD (this password is sent to the devices via JumpCloud Commands) -$JCUSERCERTPASS = 'secret1234!' -# USER CERT Validity Length (days) -$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" -# 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' -# Enter Cert Subject Headers (do not enter strings with spaces) -$Subj = [PSCustomObject]@{ - countryCode = "US" - stateCode = "CO" - Locality = "Boulder" - Organization = "JumpCloud" - OrganizationUnit = "Solutions_Architecture" - CommonName = "JumpCloud.com" -} - -# Cert Type Generation Options: -# Chose One Of: -# EmailSAN -# EmailDN -# UsernameCn (Default) -$CertType = "UsernameCn" - -################################################################################ -# Do not modify below -################################################################################ - -$UserAgent_ModuleVersion = '1.0.7' -$UserAgent_ModuleName = 'PasswordlessRadiusConfig' -#Build the UserAgent string -$UserAgent_ModuleName = "JumpCloud_$($UserAgent_ModuleName).PowerShellModule" -$Template_UserAgent = "{0}/{1}" -$UserAgent = $Template_UserAgent -f $UserAgent_ModuleName, $UserAgent_ModuleVersion -# When we import this config, this function will run and validate the openSSL binary location -function Get-OpenSSLVersion { - [CmdletBinding()] - param ( - [Parameter()] - [system.string] - $opensslBinary - ) - begin { - try { - $version = Invoke-Expression "& '$opensslBinary' version" - } catch { - throw "Something went wrong... Could not find openssl or the path is incorrect. Please update the `$opensslBinary variable in the config.ps1 file to the correct path" - } - - # Required OpenSSL Version - $OpenSSLVersion = [version]"3.0.0" - - # Determine Libre or Open SSL: - if ($version -match "LibreSSL") { - Throw "LibreSSL does not meet the requirements of this application, please install OpenSSL v3.0.0 or later" - } else { - [version]$Version = (Select-String -InputObject $version -Pattern "([0-9]+)\.([0-9]+)\.([0-9]+)").matches.value - } - - # Determine if windows: - if ([System.Environment]::OSVersion.Platform -match "Win") { - # If env variable exists, skip check for subsequent runs of ./config.ps1 - if ($env:OPENSSL_MODULES) { - $binItems = Get-ChildItem -Path $env:OPENSSL_MODULES - if ("legacy.dll" -in $binItems.Name) { - Write-Host "legacy.dll module set through environment variable" - } else { - Throw "The required OpenSSL 'legacy.dll' file was not found in the bin path $PathDirectory. This is required to create certificates. `nIf this module file is located elsewhere, you may specify the path to that directory in this powershell session using this command: '`$env:OPENSSL_MODULES = C:/Path/To/Directory' " - } - } else { - # Try to point to the Legacy.dll file - Throw "The required OpenSSL 'legacy.dll' file is required for this project. This module file is required to create certificates. `nIf this module file is located elsewhere, you may specify the path to that directory in this powershell session using this command: '`$env:OPENSSL_MODULES = C:/Path/To/openSSL_Directory/' Where the legacy.dll file is in openSSL_Directory " - - } - - } - } - process { - if ($version -lt $OpenSSLVersion) { - Throw "The installed version of OpenSSL: OpenSSL $Version, does not meet the requirements of this application, please install a later version of at least $Type $Version" - exit 1 - } - } -} -Get-OpenSSLVersion -opensslBinary $opensslBinary - -# Validate no spaces in $Subj -foreach ($subjObj in $subj.psObject.Properties) { - if ($subjObj.value -match " ") { - throw "Subject Header: $($subjObj.Name):$($subjObj.value) Contains a space character. subject headers cannot contain spaces, please remove and re-run" - } -} -# Validate API KEY, OrgID, SystemGroupID, length -if (($JCAPIKEY).Length -ne 40) { - throw "The entered JumpCloud Api Key is not the expected length" -} -if (($JCORGID).Length -ne 24) { - throw "The entered JumpCloud Organization ID is not the expected length" -} -if (($JCUSERGROUP).Length -ne 24) { - throw "The entered JumpCloud UserGroup ID is not the expected length" -} diff --git a/scripts/automation/Radius/Extensions/extensions-emailSAN.cnf b/scripts/automation/Radius/Extensions/extensions-emailSAN.cnf index 18f598a5f..be3123204 100644 --- a/scripts/automation/Radius/Extensions/extensions-emailSAN.cnf +++ b/scripts/automation/Radius/Extensions/extensions-emailSAN.cnf @@ -11,7 +11,7 @@ OU = MyDivision CN = www.company.com [v3_req] -authorityKeyIdentifier = keyid:issuer +authorityKeyIdentifier = keyid:always,issuer:always keyUsage = digitalSignature extendedKeyUsage = clientAuth #For Client cert with email in the subject alternative name diff --git a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 b/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 deleted file mode 100644 index 797c55c9c..000000000 --- a/scripts/automation/Radius/Functions/JCRadiusCertDeployment.psm1 +++ /dev/null @@ -1,9 +0,0 @@ -# Load all functions from public and private folders -$Private = @( Get-ChildItem -Path "$PSScriptRoot/Private/*.ps1" -Recurse) -Foreach ($Import in $Private) { - Try { - . $Import.FullName - } Catch { - Write-Error -Message "Failed to import function $($Import.FullName): $_" - } -} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 new file mode 100644 index 000000000..37ac9e8e5 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Deploy-UserCertificate.ps1 @@ -0,0 +1,855 @@ +function Deploy-UserCertificate { + [CmdletBinding()] + param ( + # Input from users.json + [Parameter(HelpMessage = 'An individual or array of user objects from users.json', Mandatory)] + [System.Object[]] + $userObject, + # 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, + # when specified will generated a new command for the given user + [Parameter(HelpMessage = 'When specified, this parameter will generate new commands for the user from the "userObject" parameter')] + [bool] + $forceGenerateCommands, + # prompt replace existing certificate + [Parameter(HelpMessage = 'When specified, this parameter will prompt for user input and ask if generated commands should be invoked on associated systems')] + [switch] + $prompt + ) + + begin { + $workToBeDone = [PSCustomObject]@{ + remainingMacOSDevices = $null + remainingWindowsSDevices = $null + removeQueuedCommands = $null + removeCommands = $null + forceGenerateMacOSCommands = $false + forceGenerateWindowsCommands = $false + macOSCommandID = $null + windowsCommandID = $null + commandIDsToRemove = New-Object System.Collections.ArrayList + commandQueueIDsToRemove = New-Object System.Collections.ArrayList + commandQueueIDsDuplicates = New-Object System.Collections.ArrayList + } + + $status_commandGenerated = $false + $result_deployed = $false + + switch ($forceInvokeCommands) { + $true { + $invokeCommands = $true + } + $false { + $invokeCommands = $false + } + } + switch ($forceGenerateCommands) { + $true { + $workToBeDone.forceGenerateMacOSCommands = $true + $workToBeDone.forceGenerateWindowsCommands = $true + } + $false { + $workToBeDone.forceGenerateMacOSCommands = $false + $workToBeDone.forceGenerateWindowsCommands = $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 { + foreach ($user in $userObject) { + ## first determine if the local cert sha is the same as the cert sha in the admin console: + # Write-Warning "processing user: $($user.username)" + # Write-Warning "user: $user" + + # Get the commands for this user: + $radiusCommandsByUser = Get-CommandByUsername -username $user.username + if ($radiusCommandsByUser) { + $macOSCommands = ($radiusCommandsByUser | Where-Object { $_.Name -match "MacOSX" }) + $windowsOSCommands = ($radiusCommandsByUser | Where-Object { $_.Name -match "Windows" }) + } + + # Get the queued commands for the user + $queuedRadiusCommandsByUser = Get-queuedCommandByUser -username $user.username + if ($queuedRadiusCommandsByUser) { + $windowsQueuedCommands = $queuedRadiusCommandsByUser | Where-Object { $_.name -match "Windows" } + $macOSQueuedCommands = $queuedRadiusCommandsByUser | Where-Object { $_.name -match "macOS" } + } else { + $windowsQueuedCommands = $null + $macOSQueuedCommands = $null + } + + # Get the users certificate Details: + # Get certificate and zip to upload to Commands + $userCertFiles = Get-ChildItem -Path (Resolve-Path -Path "$($global:JCRConfig.radiusDirectory.value)/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 = "$($global:JCRConfig.radiusDirectory.value)/UserCerts/$($user.userName)-client-signed.zip" + # get certInfo for commands: + $certInfo = Get-CertInfo -UserCerts -username $user.username + # Determine if the commands have matching SHA1 values: + + + if ($macOSCommands) { + # verify the certs match for macOS systems: + foreach ($maOSRadiusCommand in $macOSCommands) { + # if we want to forceGenerate new commands, add all commands to the remove list + switch ($workToBeDone.forceGenerateMacOSCommands) { + $true { + # add the command to the list to remove + $workToBeDone.commandIDsToRemove.Add($maOSRadiusCommand.Id) | Out-Null + + } + $false { + if ((-Not $workToBeDone.macOSCommandID) -AND ($maOSRadiusCommand.trigger -eq $certInfo.sha1)) { + # set the existing command IDs: + # There is a potential for this to be a bug, if duplicate commands with the same SHA1 trigger exist, this code just selects the first one and removes the second in the next iteration + $workToBeDone.macOSCommandID = ($radiusCommandsByUser | Where-Object { $_.Name -match "MacOSX" }).Id | Select-Object -First 1 + } elseif (($workToBeDone.macOSCommandID) -AND ($maOSRadiusCommand.trigger -eq $certInfo.sha1)) { + # add the duplicate command to the list to remove + $workToBeDone.commandIDsToRemove.Add($maOSRadiusCommand.Id) | Out-Null + } else { + # add the command to the list to remove + $workToBeDone.commandIDsToRemove.Add($maOSRadiusCommand.Id) | Out-Null + } + } + } + } + } else { + $workToBeDone.macOSCommandID = $null + } + if ($windowsOSCommands) { + # verify the certs match for windows systems: + foreach ($windowsOSRadiusCommands in $windowsOSCommands) { + switch ($workToBeDone.forceGenerateWindowsCommands) { + $true { + # add the command to the list to remove + $workToBeDone.commandIDsToRemove.Add($windowsOSRadiusCommands.Id) | Out-Null + } + $false { + if ((-Not $workToBeDone.windowsCommandID) -AND ($windowsOSRadiusCommands.trigger -eq $certInfo.sha1)) { + # set the existing command IDs: + # There is a potential for this to be a bug, if duplicate commands with the same SHA1 trigger exist, this code just selects the first one and removes the second in the next iteration + $workToBeDone.windowsCommandID = ($radiusCommandsByUser | Where-Object { $_.Name -match "Windows" }).Id | Select-Object -First 1 + } elseif (($workToBeDone.windowsCommandID) -AND ($windowsOSRadiusCommands.trigger -eq $certInfo.sha1)) { + # add the duplicate command to the list to remove + $workToBeDone.commandIDsToRemove.Add($windowsOSRadiusCommands.Id) | Out-Null + } else { + # add the command to the list to remove + $workToBeDone.commandIDsToRemove.Add($windowsOSRadiusCommands.Id) | Out-Null + } + } + } + } + } else { + $workToBeDone.windowsCommandID = $null + } + + + + if ($windowsQueuedCommands) { + if ($workToBeDone.windowsCommandID) { + # Get queued commands that are from a previous command; delete those + foreach ($queuedCommand in $windowsQueuedCommands) { + if ($queuedCommand.command -eq $workToBeDone.windowsCommandID) { + # TODO: nothing to do here? + $workToBeDone.commandQueueIDsDuplicates.Add($queuedCommand.Id) | Out-Null + } else { + # if the queued commands does not match the cert command, remove all these queued commands + $workToBeDone.commandQueueIDsToRemove.Add($queuedCommand.Id) | Out-Null + } + } + } else { + foreach ($queuedCommand in $windowsQueuedCommands) { + # if there are commands in queue for the user but no matching cert command, remove all the items in the queue + $workToBeDone.commandQueueIDsToRemove.Add($queuedCommand.Id) | Out-Null + } + } + } + if ($macOSQueuedCommands) { + if ($workToBeDone.windowsCommandID) { + + foreach ($queuedCommand in $macOSQueuedCommands) { + if ($queuedCommand.command -eq $workToBeDone.macOSCommandID) { + # TODO: nothing to do here? + $workToBeDone.commandQueueIDsDuplicates.Add($queuedCommand.Id) | Out-Null + } else { + # if the queued commands does not match the cert command, remove all these queued commands + $workToBeDone.commandQueueIDsToRemove.Add($queuedCommand.Id) | Out-Null + } + } + } else { + foreach ($queuedCommand in $macOSQueuedCommands) { + # if there are commands in queue for the user but no matching cert command, remove all the items in the queue + $workToBeDone.commandQueueIDsToRemove.Add($queuedCommand.Id) | Out-Null + } + } + } + + # remove the commands: + if ($workToBeDone.commandIDsToRemove) { + foreach ($commandIDToRemove in $workToBeDone.commandIDsToRemove) { + Remove-JcSdkCommand -Id $commandIDToRemove | Out-Null + } + } + # remove queued commands: + if ($workToBeDone.commandQueueIDsToRemove) { + foreach ($commandQueueIDToRemove in $workToBeDone.commandQueueIDsToRemove) { + Clear-JCQueuedCommand -workflowId $commandQueueIDToRemove | Out-Null + } + } + # remove queued commands: + if ($workToBeDone.commandQueueIDsDuplicates) { + foreach ($commandQueueIDToRemove in $workToBeDone.commandQueueIDsDuplicates) { + Clear-JCQueuedCommand -workflowId $commandQueueIDToRemove | Out-Null + } + } + ## determine the systems that need the cert + try { + $userCertHashData = $Global:JCRCertHash["$($user.certInfo.sha1)"] + + } catch { + $userCertHashData = $null + } + if ($userCertHashData) { + # set the remaining systems that don't have the cert: + # macOS + $workToBeDone.remainingMacOSDevices = $user.systemAssociations | Where-Object { ($_.systemID -notin $userCertHashData.systemId ) -And ($_.osFamily) -eq "macOS" } + # windows + $workToBeDone.remainingWindowsSDevices = $user.systemAssociations | Where-Object { ($_.systemID -notin $userCertHashData.systemId ) -And ($_.osFamily) -eq "Windows" } + + # if there's work to be done but not a valid cert command, generate the command + if (($workToBeDone.remainingMacOSDevices) -AND (-Not $workToBeDone.macOSCommandID)) { + $workToBeDone.forceGenerateMacOSCommands = $true + } + # if there's work to be done but not a valid cert command, generate the command + if (($workToBeDone.remainingWindowsSDevices) -AND (-Not $workToBeDone.windowsCommandID)) { + $workToBeDone.forceGenerateWindowsCommands = $true + } + # regenerate the cert to keep it on the console: + if ((-Not $workToBeDone.remainingMacOSDevices) -AND (-Not $workToBeDone.macOSCommandID)) { + $workToBeDone.forceGenerateMacOSCommands = $true + } + if ((-Not $workToBeDone.remainingWindowsSDevices) -AND (-Not $workToBeDone.windowsCommandID)) { + $workToBeDone.forceGenerateWindowsCommands = $true + } + } else { + # set the remaining devices to the association list from users.json + # macOS + $workToBeDone.remainingMacOSDevices = $user.systemAssociations | Where-Object { ($_.osFamily) -eq "macOS" } + # Windows + $workToBeDone.remainingWindowsSDevices = $user.systemAssociations | Where-Object { ($_.osFamily) -eq "Windows" } + # if there's work to be done but not a valid cert command, generate the command + if (($workToBeDone.remainingMacOSDevices) -AND (-Not $workToBeDone.macOSCommandID)) { + $workToBeDone.forceGenerateMacOSCommands = $true + } + # if there's work to be done but not a valid cert command, generate the command + if (($workToBeDone.remainingWindowsSDevices) -AND (-Not $workToBeDone.windowsCommandID)) { + $workToBeDone.forceGenerateWindowsCommands = $true + } + # regenerate the cert to keep it on the console: + if ((-Not $workToBeDone.remainingMacOSDevices) -AND (-Not $workToBeDone.macOSCommandID)) { + $workToBeDone.forceGenerateMacOSCommands = $true + } + if ((-Not $workToBeDone.remainingWindowsSDevices) -AND (-Not $workToBeDone.windowsCommandID)) { + $workToBeDone.forceGenerateWindowsCommands = $true + } + } + # now clear out the user command associations from users.json + $user.commandAssociations = @() + + If (-Not $certInfo) { + Write-host "$($user.username) did not have a certificate generated" + $status_commandGenerated = $false + $result_deployed = $false + continue + } else { + # explicitly validate that the subject header exists + if (-Not $certInfo.subject) { + Write-host "$($user.username) has a certificate file but no subject was found" + $status_commandGenerated = $false + $result_deployed = $false + continue + } + # explicitly validate that the serial number exists + if (-Not $certInfo.serial) { + Write-host "$($user.username) has a certificate file but no serial number was found" + $status_commandGenerated = $false + $result_deployed = $false + continue + } + } + + if (($workToBeDone.forcegenerateMacOSCommands) -OR ($workToBeDone.forceGenerateWindowsCommands)) { + # determine certType + switch ($($global:JCRConfig.certType.value)) { + 'EmailSAN' { + # set cert identifier to SAN email of cert + $sanID = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -in $($userCrt) -ext subjectAltName -noout" + $regex = 'email:(.*?)$' + $JCR_SUBJECT_HEADERSMatch = Select-String -InputObject "$($sanID)" -Pattern $regex + $certIdentifier = $JCR_SUBJECT_HEADERSMatch.matches.Groups[1].value + # in macOS search user certs by email + $macCertSearch = 'e' + # On windows devices, search certs by Subject Alternative Name this is required to remove previously generated certs + $windowsCertHereString = @" + `$SANMatch = `$cert.Extensions | Where-Object { `$_.Oid.FriendlyName -eq "Subject Alternative Name" } + if (`$SANMatch){ + if ((`$SANMatch).format(`$true) -match "$($certIdentifier)") { + if (`$(`$cert.serialNumber) -eq "$($certInfo.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 + } + } + } +"@ + + } + 'EmailDN' { + # Else set cert identifier to email of cert subject + $regex = 'emailAddress\s*=\s*(.*?)$' + $JCR_SUBJECT_HEADERSMatch = Select-String -InputObject "$($certInfo.Subject)" -Pattern $regex + $certIdentifier = $JCR_SUBJECT_HEADERSMatch.matches.Groups[1].value + # in macOS search user certs by email + $macCertSearch = 'e' + # On windows devices, search certs by Subject + $windowsCertHereString = @" + if (`$cert.subject -match "$($certIdentifier)") { + if (`$(`$cert.serialNumber) -eq "$($certInfo.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 + } + } +"@ + } + 'UsernameCn' { + # if username just set cert identifier to username + $certIdentifier = $($user.userName) + # in macOS search user certs by common name (username) + $macCertSearch = 'c' + # On windows devices, search certs by Subject + $windowsCertHereString = @" + if (`$cert.subject -match "$($certIdentifier)") { + if (`$(`$cert.serialNumber) -eq "$($certInfo.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 + } + } +"@ + } + } + # Create the zip + Compress-Archive -Path $userPfx -DestinationPath $userPfxZip -CompressionLevel NoCompression -Force + } + + + if ($workToBeDone.forcegenerateMacOSCommands) { + # Get the macOS system ids + $systemIds = (Get-SystemsThatNeedCertWork -userData $user -osType "macOS") + if ($systemIds.count -gt 0) { + + # 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="$($certInfo.serial)" +networkSsid="$($global:JCRConfig.networkSSID.value)" +# 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 $($global:JCRConfig.certSecretPass.value) -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 security preference is set: +IFS=';' read -ra network <<< "`$(`$global:JCRConfig.networkSSID.value)" +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 + +# update si table +# The conf file is JSON and can be parsed using JSON.parse() in a supported language. +conf="`$(cat /opt/jc/jcagent.conf)" +regex='\"systemKey\":\"([a-zA-Z0-9_]+)\"' +if [[ `${conf} =~ `$regex ]] ; then +systemKey="`${BASH_REMATCH[1]}" +fi +# get the certUUID +regex='\"certuuid\":\"([a-zA-Z0-9_]+)\"' +if [[ `${conf} =~ `$regex ]] ; then +certUUID="`${BASH_REMATCH[1]}" +fi + +# get the key /cert locations +keyLocation="/opt/jc/client.key" +certLocation="/opt/jc/client.crt" +caCertLocation="/opt/jc/ca.crt" + +# Get json certificate data from osquery and add missing windows certificate columns +certJson=`$(/opt/jc/bin/jcosqueryi --json "select *, '' AS sid, '' AS store, '' AS store_id, '' AS store_location, '' AS username from certificates;") + +# post +curl --cert `$certLocation --key `$keyLocation --cacert `$caCertLocation \ +-X POST 'https://agent.jumpcloud.com/systeminsights/snapshots/certificates' \ +-H "x-system-id: `$systemKey" \ +-H "x-ssl-client-dn: /CN=`$certUUID/O=JumpCloud" \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "data": '"`$certJson"' +}' + +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 = "$($certInfo.sha1)" + 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 + + } catch { + $status_commandGenerated = $false + # throw $_ + } + # Find newly created command and add system as target + $Command = Get-JcSdkCommand -Filter @("trigger:eq:$($certInfo.sha1)", "commandType:eq:mac") + $workToBeDone.macOSCommandID = $Command.Id + + $CommandTable = [PSCustomObject]@{ + commandId = $workToBeDone.macOSCommandID + 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" + $status_commandGenerated = $true + + + } + } + + if (-Not ($workToBeDone.forceGenerateMacOSCommands) -And ($workToBeDone.macOSCommandID)) { + $systemIds = (Get-SystemsThatNeedCertWork -userData $user -osType "macOS") + + $Command = Get-JcSdkCommand -Filter @("trigger:eq:$($certInfo.sha1)", "commandType:eq:mac") + $CommandTable = [PSCustomObject]@{ + commandId = $workToBeDone.macOSCommandID + commandName = $command.name + commandPreviouslyRun = $false + commandQueued = $false + systems = $systemIds + } + + $user.commandAssociations += $CommandTable + + } + + if (($invokeCommands) -And ($workToBeDone.remainingMacOSDevices)) { + try { + $commandStart = Start-JcSdkCommand -Id $workToBeDone.macOSCommandID -SystemIds $workToBeDone.remainingMacOSDevices.systemId | Out-Null + $result_deployed = $true + } catch { + $result_deployed = $false + } + } + # set the command associations + if (($workToBeDone.remainingMacOSDevices) -And ($workToBeDone.macOSCommandID)) { + + $workToBeDone.remainingMacOSDevices | ForEach-Object { + try { + + $commandAssociation = Set-JcSdkCommandAssociation -CommandId:("$($workToBeDone.macOSCommandID)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null + } catch { + "already exists/ couldn't add" | Out-Null + } + } + + } + if ($workToBeDone.forceGenerateWindowsCommands) { + # Get the Windows system ids + $systemIds = (Get-SystemsThatNeedCertWork -userData $user -osType "windows") + # If there are no systemIds to process, skip generating the command: + if ($systemIds.count -gt 0) { + # 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 $($global:JCRConfig.certSecretPass.value) -AsPlainText -Force +`$ScriptBlockInstall = { `$password = ConvertTo-SecureString -String $($global:JCRConfig.certSecretPass.value) -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){ + $windowsCertHereString + } +} +`$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" +} + +# update si table +# define curl path: +`$curlPath = "C:\ProgramData\chocolatey\bin\curl.exe" +if (Test-Path -Path `$curlPath) { + + # Parse the systemKey from the cong file. + `$config = get-content 'C:\Program Files\JumpCloud\Plugins\Contrib\jcagent.conf' + `$regex = 'systemKey\":\"(\w+)\"' + `$systemKey = [regex]::Match(`$config, `$regex).Groups[1].Value + # get the certUUID + `$regex = 'certuuid\":\"(\w+)\"' + `$certUUID = [regex]::Match(`$config, `$regex).Groups[1].Value + + # Get the key/ cert location + `$keyLocation = "C:\Program Files\JumpCloud\Plugins\Contrib\client.key" + `$certLocation = "C:\Program Files\JumpCloud\Plugins\Contrib\client.crt" + `$caCertLocation = "C:\Program Files\JumpCloud\Plugins\Contrib\ca.crt" + + # Get json certificate data from osquery + `$certs = . "C:\Program Files\JumpCloud\jcosqueryi" --json "select * from certificates" + # format the data + `$certsText = "{``"data``":`$certs}" + # save the data to a temp file + `$certJsonFile = "C:\Windows\Temp\jsonFile.txt" + `$certsText | Out-File -FilePath `$certJsonFile -Force -Encoding utf8 + # submit the request + C:\ProgramData\chocolatey\bin\curl.exe --cert "`$certLocation" --key "`$keyLocation" --cacert "`$caCertLocation" --header "x-system-id: `$systemKey" --header "x-ssl-client-dn: /CN=`$certUUID/O=JumpCloud" --header "Content-type:application/json " -d @C:\Windows\Temp\jsonFile.txt --url 'https://agent.jumpcloud.com/systeminsights/snapshots/certificates' + # remove the temp file + Remove-Item -Path `$certJsonFile +} else { + Write-Host "Curl might not be installed, system insights will update certificate information on this device within an hour" +} +} 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 = "$($certInfo.sha1)" + 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 + + } catch { + $status_commandGenerated = $false + } + # Find newly created command and add system as target + $Command = Get-JcSdkCommand -Filter @("trigger:eq:$($certInfo.sha1)", "commandType:eq:windows") + $workToBeDone.windowsCommandID = $command.Id + + $CommandTable = [PSCustomObject]@{ + commandId = $workToBeDone.windowsCommandID + commandName = $command.name + commandPreviouslyRun = $false + commandQueued = $false + systems = $systemIds + } + + $user.commandAssociations += $CommandTable + # Write-Host "[status] Successfully created $($Command.name): User - $($user.userName); OS - Windows" + $status_commandGenerated = $true + } + } + if (-Not ($workToBeDone.forceGenerateWindowsCommands) -And ($workToBeDone.windowsCommandID)) { + $systemIds = (Get-SystemsThatNeedCertWork -userData $user -osType "windows") + + $Command = Get-JcSdkCommand -Filter @("trigger:eq:$($certInfo.sha1)", "commandType:eq:windows") + $CommandTable = [PSCustomObject]@{ + commandId = $workToBeDone.windowsCommandID + commandName = $command.name + commandPreviouslyRun = $false + commandQueued = $false + systems = $systemIds + } + + $user.commandAssociations += $CommandTable + + } + if (($invokeCommands) -AND ($workToBeDone.remainingWindowsSDevices)) { + try { + $commandStart = Start-JcSdkCommand -Id $workToBeDone.windowsCommandID -SystemIds $workToBeDone.remainingWindowsSDevices.systemId | Out-Null + $result_deployed = $true + } catch { + $result_deployed = $false + } + } + # set the command associations + if (($workToBeDone.remainingWindowsSDevices) -And ($workToBeDone.windowsCommandID)) { + $workToBeDone.remainingWindowsSDevices | ForEach-Object { + try { + $commandAssociation = Set-JcSdkCommandAssociation -CommandId:("$($workToBeDone.windowsCommandID)") -Op 'add' -Type:('system') -Id:("$($_.systemId)") | Out-Null + } catch { + "already exists/ couldn't add" | Out-Null + } + } + + } + } + # TODO: get the userIndex only? + $userObjectFromTable, $userIndex = Get-UserFromTable -userid $user.userid + + switch ($invokeCommands) { + $true { + if ($user.commandAssociations) { + $user.commandAssociations | ForEach-Object { $_.commandPreviouslyRun = $true } + # set the deployed status to true, set the date + if (Get-Member -inputObject $user.certInfo -name "deployed" -MemberType Properties) { + # if ($userObjectFromTable.certInfo.deployed) { + $user.certInfo.deployed = $true + } else { + $user.certInfo | Add-Member -Name 'deployed' -Type NoteProperty -Value $false + + } + if (Get-Member -inputObject $user.certInfo -name "deploymentDate" -MemberType Properties) { + # if ($userObjectFromTable.certInfo.deploymentDate) { + $user.certInfo.deploymentDate = (Get-Date -Format "o") + + } else { + $user.certInfo | Add-Member -Name 'deploymentDate' -Type NoteProperty -Value (Get-Date -Format "o") + } + } + } + $false { + $result_deployed = $false + } + Default { + } + + + + } + } + + + end { + $resultTable = [ordered]@{ + 'Username' = $user.username; + 'Command Generated' = $status_commandGenerated; + 'Command Deployed' = $result_deployed + } + $workDone = [PSCustomObject]@{ + userIndex = $userIndex + commandAssociationsObject = $user.commandAssociations + certInfoObject = $user.certInfo + } + + return $resultTable, $workDone + } +} diff --git a/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertInfo.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertInfo.ps1 new file mode 100644 index 000000000..99fdab862 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertInfo.ps1 @@ -0,0 +1,131 @@ +function Get-CertInfo { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = 'When specified this function will return certificate information for the root CA located in /Cert', ParameterSetName = 'CA', Mandatory = $true)] + [switch] + $RootCA, + [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(HelpMessage = 'When specified this function will return a single users certificate information for a cert located in /UserCerts', ParameterSetName = 'User', Mandatory = $false)] + [system.string] + $username + ) + begin { + if ($RootCA) { + # Find the RootCA Path + $foundCerts = Resolve-Path -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/*cert*.pem" -ErrorAction SilentlyContinue + } + + if ($UserCerts) { + + # Find all userCert paths + if ($username) { + $foundCerts = Resolve-Path -Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts/$username-$($global:JCRConfig.certType.value)*.crt" -ErrorAction SilentlyContinue + + } else { + $foundCerts = New-Object System.Collections.ArrayList + $global:JCRRadiusMembers.username | ForEach-Object { + $foundCert = Resolve-Path -Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts/$_-$($global:JCRConfig.certType.value)*.crt" -ErrorAction SilentlyContinue + if ($foundCert) { + $foundCerts.Add($foundCert) | Out-Null + } + } + } + } + + $certObj = New-Object System.Collections.ArrayList + } + process { + # If no cert is found, return null + if (!$foundCerts) { + # Write-Warning "No certificates found in $($global:JCRConfig.radiusDirectory.value)/Cert or $($global:JCRConfig.radiusDirectory.value)/UserCerts" + $certHash = $null + } else { + if ($RootCA) { + # Check if cert and key name is radius_ca_cert.pem and radius_ca_key.pem if not, rename it + if ($foundCerts.Name -notmatch "radius_ca_cert.pem") { + Rename-Item -Path $foundCerts -NewName "radius_ca_cert.pem" + $foundCerts = Resolve-Path -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/*cert*.pem" -ErrorAction SilentlyContinue + } + # Get the key path and rename it if needed + $foundKey = Resolve-Path -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/*key.pem" -ErrorAction SilentlyContinue + if ($foundKey.Name -notmatch "radius_ca_key.pem") { + Rename-Item -Path $foundKey -NewName "radius_ca_key.pem" + } + + # Create hashtable to contain cert info + # TODO: pscustomobject instead of hash + $certHash = @{} + # Use openssl to gather serial, subject, issuer, and enddate information + $certInfo = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -in `"$($foundCerts.Path)`" -enddate -serial -subject -issuer -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+', ' ' + $date = ([datetime]::ParseExact($date , "MMM d HH:mm:ss yyyy GMT", $null)).ToUniversalTime() + $property.notAfter = Get-Date $date.ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z' + } + + $certHash += $property + } + + # Add hash to certObj array + $certObj += $certHash + } elseif ($UserCerts) { + foreach ($cert in $foundCerts) { + # Create hashtable to contain cert info + $certHash = [PSCustomObject]@{} + # Use openssl to gather serial, subject, issuer and enddate information + $certInfo = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) 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 + switch ($($property.keys)) { + 'notAfter' { + $date = $property.notAfter + # $date = $date.replace('GMT', '').Trim() + $date = $date -replace '\s+', ' ' + $date = [datetime]::ParseExact($date , "MMM d HH:mm:ss yyyy GMT", $null) + $property.notAfter = Get-Date $date.ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z' + + } + 'sha1 Fingerprint' { + $property.Values = ($($property.Values)).ToLower().Replace(":", "") + $property.keys = 'sha1' + } + Default { + } + } + $certHash | Add-Member -Name $property.keys -Type NoteProperty -Value "$($property.Values)" + } + # lastly add the username of the certificate to the hash: + $certFile = Get-Item $($cert.Path) + if (('username' -notin $MyInvocation.BoundParameters) -AND (-Not [System.String]::IsNullOrEmpty($certFile.name))) { + # Write-Host "Attempting to parse username from string: $($certFile.name)" + $matchNames = $certFile.name | Select-String -Pattern "(.*)-$($global:JCRConfig.certType.value).*" + if ($matchNames.Matches.groups) { + $username = $matchNames.Matches.groups[1].value + } + } + + $certHash | Add-Member -Name 'username' -Type NoteProperty -Value $username + $certHash | Add-Member -Name 'generated' -Type NoteProperty -Value (Get-Date $certFile.LastWriteTime.ToUniversalTime() -UFormat '+%Y-%m-%dT%H:%M:%S.000Z') + # Add hash to certObj array if the user is a member of the userGroup + if ($username -in $global:JCRRadiusMembers.username) { + $certObj.add( $certHash) | Out-Null + } + } + } + } + } + end { + return $certObj + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Get-CertKeyPass.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertKeyPass.ps1 similarity index 88% rename from scripts/automation/Radius/Functions/Private/Get-CertKeyPass.ps1 rename to scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertKeyPass.ps1 index e4f295c55..107c18e0c 100644 --- a/scripts/automation/Radius/Functions/Private/Get-CertKeyPass.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-CertKeyPass.ps1 @@ -1,5 +1,6 @@ function Get-CertKeyPass { - $foundKeyPem = Resolve-Path -Path "$JCScriptRoot/Cert/*key.pem" + #TODO: params required to test if a CA password is correct + $foundKeyPem = Resolve-Path -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/*key.pem" Write-Host "Found key: $($foundKeyPem)" if ($foundKeyPem -match "ca_key") { @@ -16,7 +17,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/CertDeployment/Get-ExpiringCertInfo.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-ExpiringCertInfo.ps1 new file mode 100644 index 000000000..66874877a --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-ExpiringCertInfo.ps1 @@ -0,0 +1,33 @@ +function Get-ExpiringCertInfo { + param ( + # array of certificates + [Parameter(Mandatory)] + [System.Object] + $certInfo, + [Parameter(Mandatory)] + [System.Int32] + $cutoffDate + ) + begin { + $expiringCerts = New-Object System.Collections.ArrayList + $currentTime = (Get-Date -Format "o") + } + process { + foreach ($cert in $certInfo) { + $startDate = [datetime]$currentTime + $endDate = [datetime]$cert.notAfter + $certTimespan = New-Timespan -Start $startDate -End $endDate + # $cert + if ($certTimespan.days -lt 15) { + Write-Debug "$($cert.userName)'s certificate will expire in $($certTimespan.Days) days" + $expiringCerts.add($cert) | Out-Null + } else { + Write-Debug "$($cert.userName)'s certificate will expire in $($certTimespan.Days) days" + } + } + } + + end { + return $expiringCerts + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/CertDeployment/Get-SubjectHeaderValue.ps1 b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-SubjectHeaderValue.ps1 new file mode 100644 index 000000000..14e002659 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertDeployment/Get-SubjectHeaderValue.ps1 @@ -0,0 +1,15 @@ +# Helper function to extract a subject header value from a subject string +function Get-SubjectHeaderValue { + param ( + [Parameter(Mandatory)] + [string]$SubjectString, + [Parameter(Mandatory)] + [string]$Header + ) + # Regex: match key, optional spaces, '=', optional spaces, then capture value up to next comma or end + $pattern = "$Header\s*=\s*([^,]+)" + if ($SubjectString -match $pattern) { + return $Matches[1].Trim() + } + return $null +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/CertGeneration/Generate-UserCert.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Generate-UserCert.ps1 new file mode 100644 index 000000000..fd3206588 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertGeneration/Generate-UserCert.ps1 @@ -0,0 +1,115 @@ +function Generate-UserCert { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = 'The type of certificate to generate, either: "EmailSAN", "EmailDN" or "UsernameCN"', Mandatory = $true)] + [ValidateSet("EmailSAN", "EmailDn", "UsernameCN")] + [system.String] + $certType, + [Parameter(Mandatory = $true, + HelpMessage = "Path to one or more locations.")] + [ValidateNotNullOrEmpty()] + [string] + $rootCAKey, + [Parameter(Mandatory = $true, + HelpMessage = "Path to one or more locations.")] + [ValidateNotNullOrEmpty()] + [string] + $rootCA, + [Parameter(Mandatory = $true, + HelpMessage = "User Object Containing, id, username, email")] + [System.Object] + $user + ) + begin { + if (-Not (Test-Path -Path $rootCAKey)) { + Throw "RootCAKey could not be found in project directory, have you run Generate-Cert.ps1?" + exit 1 + } + if (-Not (Test-Path -Path $rootCA)) { + Throw "RootCA could not be found in project directory, have you run Generate-Cert.ps1?" + exit 1 + } + } + process { + # Set Extension Path + $extensionsDir = Join-Path $JCRScriptRoot "Extensions" + $expectedFile = "extensions-$certType.cnf" + $ExtensionPath = Get-ChildItem -Path $extensionsDir -Filter "extensions-*.cnf" | Where-Object { $_.Name -ieq $expectedFile } | Select-Object -First 1 + + if (-not $ExtensionPath) { + Throw "Extension file '$expectedFile' not found in '$extensionsDir'." + } + $ExtensionPath = $ExtensionPath.FullName + + # User Certificate Signing Request: + $userCertsDir = Join-Path $global:JCRConfig.radiusDirectory.value "UserCerts" + $userCSR = Get-CaseInsensitiveFile -Directory $userCertsDir -FileName "$($user.username)-cert-req.csr" + $userKey = Get-CaseInsensitiveFile -Directory $userCertsDir -FileName "$($user.username)-$($certType)-client-signed.key" + $userCert = Get-CaseInsensitiveFile -Directory $userCertsDir -FileName "$($user.username)-$($certType)-client-signed-cert.crt" + $userPfx = Get-CaseInsensitiveFile -Directory $userCertsDir -FileName "$($user.username)-client-signed.pfx" + + + switch ($certType) { + 'EmailSAN' { + # replace extension subjectAltName + $extContent = Get-Content -Path $ExtensionPath -Raw + $extContent -replace ("subjectAltName.*", "subjectAltName = email:$($user.email)") | Set-Content -Path $ExtensionPath -NoNewline -Force + # Get CSR & Key + # Write-Host "[status] Get CSR & Key" + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) req -newkey rsa:2048 -nodes -keyout `"$userKey`" -subj `"/C=$($($global:JCRConfig.certSubjectHeader.Value.CountryCode))/ST=$($($global:JCRConfig.certSubjectHeader.Value.StateCode))/L=$($($global:JCRConfig.certSubjectHeader.Value.Locality))/O=$($JCORGID)/OU=$($($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit))`" -out `"$userCsr`"" + + # take signing request, make cert # specify extensions requets + # Write-Host "[status] take signing request, make cert # specify extensions requets" + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -req -extfile `"$ExtensionPath`" -days $($global:JCRConfig.userCertValidityDays.value) -in `"$userCsr`" -CA `"$rootCA`" -CAkey `"$rootCAKey`" -passin pass:$($env:certKeyPassword) -CAcreateserial -out `"$userCert`" -extensions v3_req" + + # validate the cert we cant see it once it goes to pfx + # Write-Host "[status] validate the cert we cant see it once it goes to pfx" + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -text -in `"$userCert`"" + # legacy needed if we take a cert like this then pass it out + # Write-Host "[status] legacy needed if we take a cert like this then pass it out" + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) pkcs12 -export -out `"$userPfx`" -inkey `"$userKey`" -in `"$userCert`" -passout pass:$($JCR_USER_CERT_PASS) -legacy" + } + 'EmailDn' { + # Create Client cert with email in the subject distinguished name + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) genrsa -out `"$userKey`" 2048" + # Generate User CSR + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) req -nodes -new -key `"$rootCAKey`" -passin pass:$($env:certKeyPassword) -out `"$userCsr`" -subj /C=$($($global:JCRConfig.certSubjectHeader.Value.CountryCode))/ST=$($($global:JCRConfig.certSubjectHeader.Value.StateCode))/L=$($($global:JCRConfig.certSubjectHeader.Value.Locality))/O=$($JCORGID)/OU=$($($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit))/CN=$($($global:JCRConfig.certSubjectHeader.Value.CommonName))" + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) req -new -key `"$userKey`" -out `"$userCsr`" -config `"$ExtensionPath`" -subj `"/C=$($($global:JCRConfig.certSubjectHeader.Value.CountryCode))/ST=$($($global:JCRConfig.certSubjectHeader.Value.StateCode))/L=$($($global:JCRConfig.certSubjectHeader.Value.Locality))/O=$($JCORGID)/OU=$($($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit))/CN=/emailAddress=$($user.email)`"" + + # Gennerate User Cert + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -req -in `"$userCsr`" -CA `"$rootCA`" -CAkey `"$rootCAKey`" -days $($global:JCRConfig.userCertValidityDays.value) -passin pass:$($env:certKeyPassword) -CAcreateserial -out `"$userCert`" -extfile `"$ExtensionPath`"" + + # Combine key and cert to create pfx file + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) pkcs12 -export -out `"$userPfx`" -inkey `"$userKey`" -in `"$userCert`" -passout pass:$($JCR_USER_CERT_PASS) -legacy" + # Output + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -text -in `"$userCert`"" + # Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) pkcs12 -clcerts -nokeys -in `"$userPfx`" -passin pass:$($JCR_USER_CERT_PASS)" + } + 'UsernameCN' { + # Create Client cert with email in the subject distinguished name + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) genrsa -out `"$userKey`" 2048" + # Generate User CSR + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) req -nodes -new -key `"$rootCAKey`" -passin pass:$($env:certKeyPassword) -out `"$userCsr`" -subj /C=$($($global:JCRConfig.certSubjectHeader.Value.CountryCode))/ST=$($($global:JCRConfig.certSubjectHeader.Value.StateCode))/L=$($($global:JCRConfig.certSubjectHeader.Value.Locality))/O=$($JCORGID)/OU=$($($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit))/CN=$($($global:JCRConfig.certSubjectHeader.Value.CommonName))" + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) req -new -key `"$userKey`" -out `"$userCsr`" -config `"$ExtensionPath`" -subj `"/C=$($($global:JCRConfig.certSubjectHeader.Value.CountryCode))/ST=$($($global:JCRConfig.certSubjectHeader.Value.StateCode))/L=$($($global:JCRConfig.certSubjectHeader.Value.Locality))/O=$($JCORGID)/OU=$($($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit))/CN=$($user.username)`"" + + # Gennerate User Cert + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -req -in `"$userCsr`" -CA `"$rootCA`" -CAkey `"$rootCAKey`" -days $($global:JCRConfig.userCertValidityDays.value) -CAcreateserial -passin pass:$($env:certKeyPassword) -out `"$userCert`" -extfile `"$ExtensionPath`"" + + # Combine key and cert to create pfx file + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) pkcs12 -export -out `"$userPfx`" -inkey `"$userKey`" -in `"$userCert`" -inkey `"$userKey`" -passout pass:$($JCR_USER_CERT_PASS) -legacy" + # Output + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -text -in `"$userCert`"" + # Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) pkcs12 -clcerts -nokeys -in `"$userPfx`" -passin pass:$($JCR_USER_CERT_PASS)" + } + } + + } + end { + # Clean Up User Certs Directory remove non .crt files + # `"$userCert`"Files = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts" + # `"$userCert`"Files | Where-Object { $_.Name -notmatch ".pfx" } | ForEach-Object { + # Remove-Item -path $_.fullname + # } + + } +} diff --git a/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 new file mode 100644 index 000000000..e501a87f2 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertGeneration/Invoke-UserCertProcess.ps1 @@ -0,0 +1,121 @@ +Function Invoke-UserCertProcess { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = 'The user object from users.json', ParameterSetName = 'radiusMember')] + [System.object] + $radiusMember, + [Parameter(ParameterSetName = 'selectedUserObject')] + [System.String] + $selectedUserObject, + [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(HelpMessage = 'When specified, existing certificates will be replaced')] + [switch] + $forceReplaceCert, + # prompt replace existing certificate + [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" + } + } + 'userObject' { + $MatchedUser = $GLOBAL:JCRUsers[$selectedUserObject.userid] + } + } + + # get the user from user.json + $userObject, $userIndex = Get-UserFromTable -userID $MatchedUser.id + # Test if the file exists: + switch (Test-Path "$($global:JCRConfig.radiusDirectory.value)/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 certificate 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 $($global:JCRConfig.certType.value) -user $MatchedUser -rootCAKey (Resolve-Path -path "$($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_key.pem") -rootCA (Resolve-Path -path "$($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem") 2>&1 | out-null + # validate that the cert was written correctly: + #TODO: validate and return as variable + + #TODO: cert should be written with $($global:JCRConfig.certType.value), if other certs exist, remove them. + $CertInfo = Get-CertInfo -UserCerts -username $MatchedUser.username + if ($CertInfo.count -gt 1) { + $foundCerts = Get-ChildItem -path (Resolve-Path -path "$($global:JCRConfig.radiusDirectory.value)/UserCerts/$($MatchedUser.username)-*.crt") + $orderedCerts = $foundCerts | Sort-Object -Property LastWriteTime -Descending | select -Skip 1 + foreach ($cert in $orderedCerts) { + Remove-Item $cert.FullName + } + } + } + + # 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 + } +} diff --git a/scripts/automation/Radius/Functions/Private/CertGeneration/Set-JCRExtensionFile.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Set-JCRExtensionFile.ps1 new file mode 100644 index 000000000..97f922b99 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertGeneration/Set-JCRExtensionFile.ps1 @@ -0,0 +1,80 @@ +function Set-JCRExtensionFile { + [CmdletBinding()] + param () + + $extensionsDir = Join-Path $JCRScriptRoot "Extensions" + $exts = Get-ChildItem -Path (Resolve-Path -Path $extensionsDir) -Filter "extensions-*.cnf" + $emailDNHereString = @" +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name + +[req_distinguished_name] +C = $($global:JCRConfig.certSubjectHeader.Value.CountryCode) +ST = $($global:JCRConfig.certSubjectHeader.Value.StateCode) +L = $($global:JCRConfig.certSubjectHeader.Value.Locality) +O = $($global:JCRConfig.certSubjectHeader.Value.Organization) +OU = $($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit) +CN = $($global:JCRConfig.certSubjectHeader.Value.CommonName) + +[v3_req] +keyUsage = digitalSignature +extendedKeyUsage = clientAuth +authorityInfoAccess = OCSP;URI:http://localhost:9000 +"@ + $emailSANHereString = @" +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name + +[req_distinguished_name] +C = $($global:JCRConfig.certSubjectHeader.Value.CountryCode) +ST = $($global:JCRConfig.certSubjectHeader.Value.StateCode) +L = $($global:JCRConfig.certSubjectHeader.Value.Locality) +O = $($global:JCRConfig.certSubjectHeader.Value.Organization) +OU = $($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit) +CN = $($global:JCRConfig.certSubjectHeader.Value.CommonName) + +[v3_req] +authorityKeyIdentifier = keyid:always,issuer:always +keyUsage = digitalSignature +extendedKeyUsage = clientAuth +#For Client cert with email in the subject alternative name +subjectAltName = email:username@domain.com +"@ + + $usernameCNHereString = @" +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name + +[req_distinguished_name] +C = $($global:JCRConfig.certSubjectHeader.Value.CountryCode) +ST = $($global:JCRConfig.certSubjectHeader.Value.StateCode) +L = $($global:JCRConfig.certSubjectHeader.Value.Locality) +O = $($global:JCRConfig.certSubjectHeader.Value.Organization) +OU = $($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit) +CN = $($global:JCRConfig.certSubjectHeader.Value.CommonName) + +[v3_req] +keyUsage = digitalSignature +extendedKeyUsage = clientAuth +authorityInfoAccess = OCSP;URI:http://localhost:9000 +"@ + + foreach ($ext in $exts) { + Write-Host "Updating Subject Headers for $($ext.Name)" + # replace the content of the extension file with the appropriate string based on the certType + switch ($ext.Name) { + 'extensions-emailDN.cnf' { + Set-Content -Path $ext.FullName -Value $emailDNHereString -NoNewline -Force + } + 'extensions-emailSAN.cnf' { + Set-Content -Path $ext.FullName -Value $emailSANHereString -NoNewline -Force + } + 'extensions-usernameCN.cnf' { + Set-Content -Path $ext.FullName -Value $usernameCNHereString -NoNewline -Force + } + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/CertGeneration/Test-JCRExtensionFile.ps1 b/scripts/automation/Radius/Functions/Private/CertGeneration/Test-JCRExtensionFile.ps1 new file mode 100644 index 000000000..3eb6e3e5e --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertGeneration/Test-JCRExtensionFile.ps1 @@ -0,0 +1,35 @@ +function Test-JCRExtensionFile { + [CmdletBinding()] + param () + + $extensionsDir = Join-Path $JCRScriptRoot "Extensions" + $exts = Get-ChildItem -Path (Resolve-Path -Path $extensionsDir) -Filter "extensions-*.cnf" + $allValid = $true + + foreach ($ext in $exts) { + Write-Host "Validating Subject Headers for $($ext.Name)" + $extContent = Get-Content -Path $ext.FullName -Raw + + $expected = @{ + 'C' = $global:JCRConfig.certSubjectHeader.Value.CountryCode + 'ST' = $global:JCRConfig.certSubjectHeader.Value.StateCode + 'L' = $global:JCRConfig.certSubjectHeader.Value.Locality + 'O' = $global:JCRConfig.certSubjectHeader.Value.Organization + 'OU' = $global:JCRConfig.certSubjectHeader.Value.OrganizationUnit + 'CN' = $global:JCRConfig.certSubjectHeader.Value.CommonName + } + + foreach ($key in $expected.Keys) { + $pattern = "$key\s*=\s*(.+)" + if ($extContent -match $pattern) { + $actual = $Matches[1].Trim() + if ($actual -ne $expected[$key]) { + $allValid = $false + } + } else { + $allValid = $false + } + } + } + return $allValid +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/CertJson/Get-CertBySHA.ps1 b/scripts/automation/Radius/Functions/Private/CertJson/Get-CertBySHA.ps1 new file mode 100644 index 000000000..69addef4c --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertJson/Get-CertBySHA.ps1 @@ -0,0 +1,39 @@ + + +function Get-CertBySHA { + [CmdletBinding()] + param ( + [Parameter()] + [string] + $sha1 + ) + + begin { + # define list + $list = New-Object System.Collections.ArrayList + + + } + + process { + # for macOS certs search + $macCertResultList = Get-JcSdkSystemInsightCertificate -Filter "sha1:eq:$sha1" + # for windows certs search with uppercase: + $windowsCertResultList = Get-JcSdkSystemInsightCertificate -Filter "sha1:eq:$($sha1.toUpper())" + foreach ($cert in $macCertResultList) { + $list.Add($cert) | Out-Null + } + foreach ($cert in $windowsCertResultList | Where-Object { $_.StoreId -notmatch "_Classes" }) { + $list.Add($cert) | Out-Null + } + if ($list) { + $userDeploymentTable = New-DeploymentTable -resultList $list + } + + } + + end { + return $userDeploymentTable + + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/CertJson/Get-CertHash.ps1 b/scripts/automation/Radius/Functions/Private/CertJson/Get-CertHash.ps1 new file mode 100644 index 000000000..79c47dbb4 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertJson/Get-CertHash.ps1 @@ -0,0 +1,26 @@ + +function Get-CertHash { + begin { + $allCerts = Get-CertInfo -UserCerts + $shaFuncDef = ${function:Get-CertBySHA}.ToString() + $deploymentTableDef = ${function:New-DeploymentTable}.ToString() + $resultsArrayP = [System.Collections.Concurrent.ConcurrentDictionary[string, object]]::new() + } + process { + $allCerts | Foreach-Object -ThrottleLimit 5 -Parallel { + $resultsArrayP = $using:resultsArrayP + ${function:Get-CertBySHA} = $using:shaFuncDef + ${function:New-DeploymentTable} = $using:deploymentTableDef + $item = get-CertBySHA -sha1 $PSItem.sha1 + if ($item) { + $resultsArrayP.AddOrUpdate("$($PSItem.sha1)", $item, { $item } ) | Out-Null + } + } + + } + end { + return $resultsArrayP + + } + +} diff --git a/scripts/automation/Radius/Functions/Private/Get-CommandObjectTable.ps1 b/scripts/automation/Radius/Functions/Private/CertResults/Get-CommandObjectTable.ps1 similarity index 97% rename from scripts/automation/Radius/Functions/Private/Get-CommandObjectTable.ps1 rename to scripts/automation/Radius/Functions/Private/CertResults/Get-CommandObjectTable.ps1 index 2ab94b4c7..58909dd86 100644 --- a/scripts/automation/Radius/Functions/Private/Get-CommandObjectTable.ps1 +++ b/scripts/automation/Radius/Functions/Private/CertResults/Get-CommandObjectTable.ps1 @@ -95,9 +95,9 @@ Function Get-CommandObjectTable { } # Iterate through all the associated commands - foreach ($command in $commandsObject.commandAssociations) { + foreach ($command in $commandsObject.commandAssociations | Where-Object { $_ -ne $null }) { # Check to see if the current command is pending/queued - if ($command.commandId -in $QueuedCommands.command) { + if (($command.commandId -in $QueuedCommands.command)) { # 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)) { if (!(Get-JcSdkCommandAssociation -CommandId $command.commandId -Targets system)) { $CommandTable = @{ commandName = $command.commandName diff --git a/scripts/automation/Radius/Functions/Private/CertResults/Get-InstalledCertsFromUsersJson.ps1 b/scripts/automation/Radius/Functions/Private/CertResults/Get-InstalledCertsFromUsersJson.ps1 new file mode 100644 index 000000000..595d5fa95 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertResults/Get-InstalledCertsFromUsersJson.ps1 @@ -0,0 +1,31 @@ +function Get-InstalledCertsFromUsersJson { + [CmdletBinding()] + param ( + [Parameter()] + [System.Object[]] + $userData + ) + + begin { + $CertificateStatus = New-Object System.Collections.ArrayList + + } + process { + foreach ($user in $userData) { + + $systemTotalCount = $($User.systemAssociations).count + $installCount = $($user.deploymentInfo).count + $CertificateStatus.add( [PSCustomObject]@{ + Username = $($User.username) + CertificateGenerated = if ($User.certInfo.sha1) { "$([char]0x1b)[92mYes" } + TotalSystems = $systemTotalCount + InstallStatus = if ($systemTotalCount -eq 0) { "$([char]0x1b)[91m0/0" } elseif ($installCount -eq $systemTotalCount) { "$([char]0x1b)[92m$installCount/$systemTotalCount" } else { "$([char]0x1b)[93m$installCount/$systemTotalCount" } + } + ) | Out-Null + } + + } + end { + $CertificateStatus + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/CertResults/Get-SystemsThatNeedCertWork.ps1 b/scripts/automation/Radius/Functions/Private/CertResults/Get-SystemsThatNeedCertWork.ps1 new file mode 100644 index 000000000..af0eab2d6 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertResults/Get-SystemsThatNeedCertWork.ps1 @@ -0,0 +1,30 @@ +function Get-SystemsThatNeedCertWork { + [CmdletBinding()] + param ( + [Parameter()] + [System.Object[]] + $userData, + [Parameter()] + [System.String] + $osType + ) + + begin { + $systemIDList = New-Object System.Collections.ArrayList + } + process { + + foreach ($user in $userData) { + $userSystemAssociations = $user.systemAssociations | Where-Object { $_.osFamily -eq $osType } + $userSystemsCompleted = ($user.deploymentInfo).SystemId + foreach ($system in $userSystemAssociations) { + if ($system.systemId -notin $userSystemsCompleted) { + $systemIDList.Add($system) | Out-Null + } + } + } + } + end { + return $systemIDList + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/CertResults/Get-UsersThatNeedCertWork.ps1 b/scripts/automation/Radius/Functions/Private/CertResults/Get-UsersThatNeedCertWork.ps1 new file mode 100644 index 000000000..8b7087f1a --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/CertResults/Get-UsersThatNeedCertWork.ps1 @@ -0,0 +1,34 @@ +function Get-UsersThatNeedCertWork { + [CmdletBinding()] + param ( + [Parameter()] + [System.Object[]] + $userData + ) + + begin { + $userList = New-Object System.Collections.ArrayList + } + process { + + foreach ($user in $userData) { + $userSystemAssociations = $user.systemAssociations | Where-Object { $_.osFamily -ne "Ubuntu" } | Sort-Object + $userSystemsCompleted = $user.deploymentInfo.systemId + + if (-not $userSystemAssociations) { + # if a user does not have a system associated, add them to the list + $userList.Add($user) | Out-Null + } + foreach ($system in $userSystemAssociations) { + if ($system.systemId -notin $userSystemsCompleted) { + # if user has a single system in their association list that's not in the completed list, add to the return list + $userList.Add($user) | Out-Null + break + } + } + } + } + end { + return $userList + } +} \ 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 88% rename from scripts/automation/Radius/Functions/Private/Clear-JCQueuedCommand.ps1 rename to scripts/automation/Radius/Functions/Private/Commands/Clear-JCQueuedCommand.ps1 index 23807b64e..c931c7aa4 100644 --- a/scripts/automation/Radius/Functions/Private/Clear-JCQueuedCommand.ps1 +++ b/scripts/automation/Radius/Functions/Private/Commands/Clear-JCQueuedCommand.ps1 @@ -8,7 +8,7 @@ function Clear-JCQueuedCommand { 'x-api-key' = $Env:JCApiKey 'x-org-id' = $Env:JCOrgId } - $response = Invoke-RestMethod -Uri "https://console.jumpcloud.com/api/v2/commandqueue/$workflowId" -Method DELETE -Headers $headers -UserAgent $UserAgent + $response = Invoke-RestMethod -Uri "https://console.jumpcloud.com/api/v2/commandqueue/$workflowId" -Method DELETE -Headers $headers -UserAgent $global:JCRSettings.userAgent } end { return $response 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..3e9ab9ec8 --- /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(Mandatory)] + [system.string] + $username + ) + + begin { + # define searchFilter + $SearchFilter = @{ + searchTerm = "RadiusCert-Install:${username}:" + fields = @('name', 'trigger', 'commandType') + } + + } + + process { + # Get command Results + $commandResults = Search-JcSdkCommand -SearchFilter $SearchFilter -Fields "name trigger commandType" + } + + end { + return $commandResults + } +} \ No newline at end of file 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/Commands/Get-QueuedCommandByUser.ps1 b/scripts/automation/Radius/Functions/Private/Commands/Get-QueuedCommandByUser.ps1 new file mode 100644 index 000000000..146c523d3 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Commands/Get-QueuedCommandByUser.ps1 @@ -0,0 +1,33 @@ +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 + } +} 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..a5f71b698 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Commands/Invoke-CommandByUserId.ps1 @@ -0,0 +1,49 @@ +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 -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 = Get-SystemsThatNeedCertWork -userData $userObject -osType "macOS" + # get list of Windows systems + $windows_systemIds = Get-SystemsThatNeedCertWork -userData $userObject -osType "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 -And $macOSArray) { + $macInvokedCommands = Start-JcSdkCommand -Id $macOS_commandId -SystemIds $macOSArray + } + + if ($windows_commandId -And $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/Private/Commands/Invoke-CommandsRetry.ps1 b/scripts/automation/Radius/Functions/Private/Commands/Invoke-CommandsRetry.ps1 new file mode 100644 index 000000000..f8c95a1da --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Commands/Invoke-CommandsRetry.ps1 @@ -0,0 +1,49 @@ +Function Invoke-CommandsRetry { + begin { + $RetryCommands = @() + $userArray = Get-UserJsonData + $queuedCommands = Get-JCQueuedCommands + $SearchFilter = @{ + searchTerm = 'RadiusCert-Install' + fields = @('name') + } + $commandResults = Search-JcSdkCommandResult -SearchFilter $SearchFilter + $groupedCommandResults = $commandResults | Sort-Object -Property responseTime -Descending | Group-Object name, SystemId + $mostRecentCommandResults = $groupedCommandResults | ForEach-Object { $_.Group | Select-Object -First 1 } + } + process { + # Prompt to rerun commands that have failed or expired + Foreach ($user in ($userArray) | Where-Object { $_.commandAssociations -ne $null }) { + $userIndex = $userArray.userId.IndexOf($user.userID) + $failedCommands = $mostRecentCommandResults | Where-Object DataExitCode -NE 0 + $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 -Format "o") + 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" + } + } + } + } + } + } + end { + return $RetryCommands + } +} \ No newline at end of file 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/Config/Confirm-JCRConfig.ps1 b/scripts/automation/Radius/Functions/Private/Config/Confirm-JCRConfig.ps1 new file mode 100644 index 000000000..0a3a7b6d3 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Config/Confirm-JCRConfig.ps1 @@ -0,0 +1,97 @@ +function Confirm-JCRConfig { + [CmdletBinding()] + param ( + [Parameter( + Mandatory = $false, + HelpMessage = 'Switch to pass into the function when first loading the module, the function will not write an error if this parameter is true' + )] + [switch]$loadModule + ) + begin { + if (-not $global:JCRConfig) { + $global:JCRConfig = Get-JCRConfig -asObject + } + $requiredAttributesNotSet = @{} + } + + process { + # validate config settings + foreach ($setting in $global:JCRConfig.PSObject.Properties) { + $settingName = $setting.Name + $settingValue = $setting.Value + # check to see if the key is required and if the value is null + switch ($settingName) { + 'openSSLBinary' { + if ($settingValue.value -eq $null) { + $requiredAttributesNotSet += @{ $settingName = $settingValue.placeholder } + } else { + $openSSLValid = Get-OpenSSLVersion -opensslBinary $settingValue.value + if (-not $openSSLValid) { + if (-not $loadModule) { + throw "The `'$settingName`' value is not a valid OpenSSL binary path.`nThe value: `'$($settingValue.value)`' is not valid." + } else { + Write-Warning "The `'$settingName`' value is not a valid OpenSSL binary path.`nThe value: `'$($settingValue.value)`' is not valid." + } + } + } + } + 'certSubjectHeader' { + # check if the cert subject header is set + if ($settingValue.value -eq $null) { + $requiredAttributesNotSet += @{ $settingName = $settingValue.placeholder } + } else { + # check if the hashtable has all required keys + $requiredKeys = @('CountryCode', 'StateCode', 'Locality', 'Organization', 'OrganizationUnit', 'CommonName') + foreach ($key in $requiredKeys) { + if ($global:JCRConfig.certSubjectHeader.Value.$($key) -eq $null) { + $requiredAttributesNotSet += @{ $settingName = $settingValue.placeholder } + break + } + # validate that the value has no spaces, throw + if ($global:JCRConfig.certSubjectHeader.Value.$($key) -match '\s') { + if (-not $loadModule) { + throw "The `'$settingName`' value contains spaces.`nThe value: `'$($global:JCRConfig.certSubjectHeader.Value.$($key))`' for `'$key`' cannot contain spaces." + } else { + Write-Warning "The `'$settingName`' value contains spaces.`nThe value: `'$($global:JCRConfig.certSubjectHeader.Value.$($key))`' for `'$key`' cannot contain spaces." + + } + } + } + } + } + Default { + if ($settingValue.required -eq $true -and $settingValue.value -eq $null) { + $requiredAttributesNotSet += @{ $settingName = $settingValue.placeholder } + } + } + } + } + } + + + end { + if ($requiredAttributesNotSet.count -gt 0) { + $requiredAttributesNotSet = $requiredAttributesNotSet | Sort-Object + $requiredAttributesNotSetString = $requiredAttributesNotSet.Keys -join "," + Write-Warning @" +There are required settings for this module that have not yet been set with the Set-JCRConfig function. +The module requires you set: $requiredAttributesNotSetString + +To set these run the following command (changing the default settings for your own organization): + +`$settings = @{ +$($requiredAttributesNotSet.GetEnumerator() | ForEach-Object { +"`t$($_.Key) = $($_.Value)" + [System.Environment]::NewLine +})} + +Set-JCRConfig @settings + +"@ + if (-not $loadModule) { + throw "Please set these variables with the Set-JCRConfig cmdlet" + } else { + Write-Warning "Please set these variables with the Set-JCRConfig cmdlet" + } + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Config/Get-JCRConfig.ps1 b/scripts/automation/Radius/Functions/Private/Config/Get-JCRConfig.ps1 new file mode 100644 index 000000000..b295c5db3 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Config/Get-JCRConfig.ps1 @@ -0,0 +1,43 @@ +function Get-JCRConfig { + [CmdletBinding()] + param ( + [Parameter( + DontShow, + HelpMessage = 'Returns Config.json with value, copy, write properties' + )] + [switch] + $asObject + ) + + begin { + $moduleRoot = $JCRScriptRoot + $configFilePath = Join-Path -Path $ModuleRoot -ChildPath 'Config.json' + + if (-Not (Test-Path -Path $configFilePath)) { + Write-Host "write new settings file $configFilePath" + # Create new file with default settings + New-JCRConfig + } + } + + process { + if (-Not $asObject) { + $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 + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Config/New-JCRConfig.ps1 b/scripts/automation/Radius/Functions/Private/Config/New-JCRConfig.ps1 new file mode 100644 index 000000000..d7c01138f --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Config/New-JCRConfig.ps1 @@ -0,0 +1,27 @@ +function New-JCRConfig { + [CmdletBinding()] + param ( + [Parameter( + HelpMessage = 'To Force Re-Creation of the Config file, set the $force parameter to $true' + )] + [switch] + $force + ) + + begin { + $ModuleRoot = (Get-Item -Path:($PSScriptRoot)).Parent.Parent.Parent.FullName + $configFilePath = Join-Path -Path $ModuleRoot -ChildPath 'Config.json' + $date = (Get-Date).ToUniversalTime() + } + process { + # Define Default Settings for the Config file + $config = $global:JCRConfigTemplate + } + end { + if ((Test-Path -Path $configFilePath) -And ($force)) { + $config | ConvertTo-Json | Out-File -FilePath $configFilePath + } else { + $config | ConvertTo-Json | Out-File -FilePath $configFilePath + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Config/Set-JCRRadiusDirectory.ps1 b/scripts/automation/Radius/Functions/Private/Config/Set-JCRRadiusDirectory.ps1 new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/automation/Radius/Functions/Private/Config/Test-JCRRadiusDirectory.ps1 b/scripts/automation/Radius/Functions/Private/Config/Test-JCRRadiusDirectory.ps1 new file mode 100644 index 000000000..f39dba3fb --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Config/Test-JCRRadiusDirectory.ps1 @@ -0,0 +1,48 @@ +function Test-JCRRadiusDirectory { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Path + ) + + # Resolve the full path + $resolvedPath = Resolve-Path -Path $Path -ErrorAction SilentlyContinue + + if (-not $resolvedPath) { + Write-Error "The path '$Path' does not exist." + return $false + } + + if (-not (Test-Path -Path $resolvedPath -PathType Container)) { + Write-Error "The path '$resolvedPath' is not a directory." + return $false + } + + # validate that the $resolvedPath is not the same as the JCRScriptRoot + # Write-Warning "Resolved path: $resolvedPath | JCRScriptRoot: $global:JCRScriptRoot" + if ("$resolvedPath" -eq $($global:JCRScriptRoot)) { + Write-Error "The path '$resolvedPath' cannot be the same as the JCRScriptRoot. This could lead to certificate data loss if the module is updated or reinstalled. Please set the 'RadiusDirectory' to different directory.`nSet-JCRConfig -RadiusDirectory ''" + return $false + } + + $certDir = Join-Path $resolvedPath "Cert" + $userCertsDir = Join-Path $resolvedPath "UserCerts" + + $certExists = Test-Path -Path $certDir -PathType Container + $userCertsExists = Test-Path -Path $userCertsDir -PathType Container + + if (-not $certExists) { + Write-Host "The directory 'Cert' does not exist in '$resolvedPath'." + # create the directory if it does not exist + New-Item -Path $certDir -ItemType Directory | Out-Null + Write-Host "Created directory 'Cert' in '$resolvedPath'." + } + if (-not $userCertsExists) { + Write-Host "The directory 'UserCerts' does not exist in '$resolvedPath'." + # create the directory if it does not exist + New-Item -Path $userCertsDir -ItemType Directory | Out-Null + Write-Host "Created directory 'UserCerts' in '$resolvedPath'." + } + + return $true +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Config/Update-JCRConfig.ps1 b/scripts/automation/Radius/Functions/Private/Config/Update-JCRConfig.ps1 new file mode 100644 index 000000000..e80472ee7 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Config/Update-JCRConfig.ps1 @@ -0,0 +1,69 @@ +function Update-JCRConfig { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [object] + $settings + ) + + begin { + # Config should be in /PowerShell/JumpCloudModule/Config.json + $moduleRoot = $JCRScriptRoot + $configFilePath = join-path -path $ModuleRoot -childpath 'Config.json' + + if (test-path -path $configFilePath) { + # Get Contents + $config = Get-JCRConfig -asObject + } else { + # Create new file with default settings + New-JCRConfig + $config = Get-JCRConfig -asObject + } + + Write-Host "---------Update settings--------------" + Write-Host "[status] Module Path : $($moduleCheck.Path)" + Write-Host "[Status] JCRConfig Settings:" + foreach ($setting in $settings.PSObject.Properties) { + Write-Host ("$($setting.Name): $($setting.Value.value)") + } + Write-Host "-----------------------" + } + + process { + foreach ($newSetting in $config.PSObject.properties) { + foreach ($copiedSetting in $settings.PSObject.properties) { + if ($newSetting.name -eq $copiedSetting.name) { + Write-Host "Updating setting: $($newSetting.name)" + $newSettingValue = $newSetting.Value + $copiedSettingValue = $copiedSetting.Value + + if ($newSettingValue.value -ne $copiedSettingValue.value) { + Write-Host "Old Value: $($newSettingValue.value) New Value: $($copiedSettingValue.value)" + $config.$($newSetting.name).value = $settings.$($copiedSetting.name).value + } + + + # # If the new property is in the copied settings property list: + # foreach ($newProperty in $newSetting.value.PSObject.properties) { + # foreach ($copiedProperty in $copiedSetting.value.PSObject.properties) { + # # If the property names match & the new property is eligible to be copied, copy it + # if ( ($newProperty.name -eq $copiedProperty.name) -And ($newProperty.Value.copy -eq $true)) { + # # If the values are different, copy the values + # if ( $newProperty.value.value -ne $copiedProperty.value.value) { + # write-host "Copying property: $($newProperty.name) from $($copiedSetting.name) to $($newSetting.name)" + # Write-Host "Old Value: $($newProperty.value.value) New Value: $($copiedProperty.value.value)" + # $config.$($newsetting.name).$($newProperty.name).value = $settings.$($copiedSetting.name).$($copiedProperty.name).value + # } + # } + # } + # } + } + } + } + } + + end { + $config | ConvertTo-Json | Out-File -FilePath $configFilePath + } +} diff --git a/scripts/automation/Radius/Functions/Private/DeploymentTable/New-DeploymentTable.ps1 b/scripts/automation/Radius/Functions/Private/DeploymentTable/New-DeploymentTable.ps1 new file mode 100644 index 000000000..5dc73b05f --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/DeploymentTable/New-DeploymentTable.ps1 @@ -0,0 +1,25 @@ +function New-DeploymentTable { + [CmdletBinding()] + param ( + [Parameter()] + [System.object[]] + $resultList + ) + begin { + $results = New-Object System.Collections.ArrayList + } + process { + # Get User to System Associations: + foreach ($system in $resultList) { + $DeploymentTable = [PSCustomObject]@{ + systemId = $system.systemId + path = $system.path + } + $results.Add($DeploymentTable) | Out-Null + } + + } + end { + return $results + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Generate-UserCert.ps1 b/scripts/automation/Radius/Functions/Private/Generate-UserCert.ps1 deleted file mode 100644 index 66c796bb4..000000000 --- a/scripts/automation/Radius/Functions/Private/Generate-UserCert.ps1 +++ /dev/null @@ -1,107 +0,0 @@ -function Generate-UserCert { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true)] - [ValidateSet("EmailSAN", "EmailDn", "UsernameCN")] - [system.String] - $CertType, - [Parameter(Mandatory = $true, - HelpMessage = "Path to one or more locations.")] - [ValidateNotNullOrEmpty()] - [string] - $rootCAKey, - [Parameter(Mandatory = $true, - HelpMessage = "Path to one or more locations.")] - [ValidateNotNullOrEmpty()] - [string] - $rootCA, - [Parameter(Mandatory = $true, - HelpMessage = "User Object Containing, id, username, email")] - [System.Object] - $user - ) - begin { - . "$JCScriptRoot/config.ps1" - if (-Not (Test-Path -Path $rootCAKey)) { - Throw "RootCAKey could not be found in project directory, have you run Generate-Cert.ps1?" - exit 1 - } - if (-Not (Test-Path -Path $rootCA)) { - Throw "RootCA could not be found in project direcotry, have you run Generate-Cert.ps1?" - exit 1 - } - } - process { - # Set Extension Path - $ExtensionPath = "$JCScriptRoot/Extensions/extensions-$($CertType).cnf" - # User Certificate Signing Request: - $userCSR = "$JCScriptRoot/UserCerts/$($user.username)-cert-req.csr" - # Set key, crt, pfx variables: - $userKey = "$JCScriptRoot/UserCerts/$($user.username)-$($CertType)-client-signed.key" - $userCert = "$JCScriptRoot/UserCerts/$($user.username)-$($CertType)-client-signed-cert.crt" - $userPfx = "$JCScriptRoot/UserCerts/$($user.username)-client-signed.pfx" - - switch ($CertType) { - 'EmailSAN' { - # replace extension subjectAltName - $extContent = Get-Content -Path $ExtensionPath -Raw - $extContent -replace ("subjectAltName.*", "subjectAltName = email:$($user.email)") | Set-Content -Path $ExtensionPath -NoNewline -Force - # Get CSR & Key - Write-Host "[status] Get CSR & Key" - Invoke-Expression "$opensslBinary req -newkey rsa:2048 -nodes -keyout $userKey -subj `"/C=$($subj.countryCode)/ST=$($subj.stateCode)/L=$($subj.Locality)/O=$($JCORGID)/OU=$($subj.OrganizationUnit)`" -out $userCSR" - - # take signing request, make cert # specify extensions requets - Write-Host "[status] take signing request, make cert # specify extensions requets" - Invoke-Expression "$opensslBinary x509 -req -extfile $ExtensionPath -days $JCUSERCERTVALIDITY -in $userCSR -CA $rootCA -CAkey $rootCAKey -passin pass:$($env:certKeyPassword) -CAcreateserial -out $userCert -extensions v3_req" - - # validate the cert we cant see it once it goes to pfx - Write-Host "[status] validate the cert we cant see it once it goes to pfx" - Invoke-Expression "$opensslBinary x509 -noout -text -in $userCert" - # legacy needed if we take a cert like this then pass it out - Write-Host "[status] legacy needed if we take a cert like this then pass it out" - Invoke-Expression "$opensslBinary pkcs12 -export -out $userPfx -inkey $userKey -in $userCert -passout pass:$($JCUSERCERTPASS) -legacy" - } - 'EmailDn' { - # Create Client cert with email in the subject distinguished name - Invoke-Expression "$opensslBinary genrsa -out $userKey 2048" - # Generate User CSR - Invoke-Expression "$opensslBinary req -nodes -new -key $rootCAKey -passin pass:$($env:certKeyPassword) -out $($userCSR) -subj /C=$($subj.countryCode)/ST=$($subj.stateCode)/L=$($subj.Locality)/O=$($JCORGID)/OU=$($subj.OrganizationUnit)/CN=$($subj.CommonName)" - Invoke-Expression "$opensslBinary req -new -key $userKey -out $userCsr -config $ExtensionPath -subj `"/C=$($subj.countryCode)/ST=$($subj.stateCode)/L=$($subj.Locality)/O=$($JCORGID)/OU=$($subj.OrganizationUnit)/CN=/emailAddress=$($user.email)`"" - - # Gennerate User Cert - Invoke-Expression "$opensslBinary x509 -req -in $userCsr -CA $rootCA -CAkey $rootCAKey -days $JCUSERCERTVALIDITY -passin pass:$($env:certKeyPassword) -CAcreateserial -out $userCert -extfile $ExtensionPath" - - # Combine key and cert to create pfx file - Invoke-Expression "$opensslBinary pkcs12 -export -out $userPfx -inkey $userKey -in $userCert -passout pass:$($JCUSERCERTPASS) -legacy" - # Output - Invoke-Expression "$opensslBinary x509 -noout -text -in $userCert" - # invoke-expression "$opensslBinary pkcs12 -clcerts -nokeys -in $userPfx -passin pass:$($JCUSERCERTPASS)" - } - 'UsernameCN' { - # Create Client cert with email in the subject distinguished name - Invoke-Expression "$opensslBinary genrsa -out $userKey 2048" - # Generate User CSR - Invoke-Expression "$opensslBinary req -nodes -new -key $rootCAKey -passin pass:$($env:certKeyPassword) -out $($userCSR) -subj /C=$($subj.countryCode)/ST=$($subj.stateCode)/L=$($subj.Locality)/O=$($JCORGID)/OU=$($subj.OrganizationUnit)/CN=$($subj.CommonName)" - Invoke-Expression "$opensslBinary req -new -key $userKey -out $userCSR -config $ExtensionPath -subj `"/C=$($subj.countryCode)/ST=$($subj.stateCode)/L=$($subj.Locality)/O=$($JCORGID)/OU=$($subj.OrganizationUnit)/CN=$($user.username)`"" - - # Gennerate User Cert - Invoke-Expression "$opensslBinary x509 -req -in $userCSR -CA $rootCA -CAkey $rootCAKey -days $JCUSERCERTVALIDITY -CAcreateserial -passin pass:$($env:certKeyPassword) -out $userCert -extfile $ExtensionPath" - - # Combine key and cert to create pfx file - Invoke-Expression "$opensslBinary pkcs12 -export -out $userPfx -inkey $userKey -in $userCert -inkey $userKey -passout pass:$($JCUSERCERTPASS) -legacy" - # Output - Invoke-Expression "$opensslBinary x509 -noout -text -in $userCert" - # invoke-expression "$opensslBinary pkcs12 -clcerts -nokeys -in $userPfx -passin pass:$($JCUSERCERTPASS)" - } - } - - } - end { - # Clean Up User Certs Directory remove non .crt files - # $userCertFiles = Get-ChildItem -Path "$JCScriptRoot/UserCerts" - # $userCertFiles | Where-Object { $_.Name -notmatch ".pfx" } | ForEach-Object { - # Remove-Item -path $_.fullname - # } - - } -} diff --git a/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 b/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 deleted file mode 100644 index d1275d71a..000000000 --- a/scripts/automation/Radius/Functions/Private/Get-CertInfo.ps1 +++ /dev/null @@ -1,92 +0,0 @@ -function Get-CertInfo { - param ( - [switch]$RootCA, - [switch]$UserCerts - ) - begin { - # Import the Config.ps1 variables - . "$JCScriptRoot/Config.ps1" - - if ($RootCA) { - # Find the RootCA Path - $foundCerts = Resolve-Path -Path "$JCScriptRoot/Cert/*cert*.pem" -ErrorAction SilentlyContinue - } - - if ($UserCerts) { - # Find all userCert paths - $foundCerts = Resolve-Path -Path "$JCScriptRoot/UserCerts/*.crt" -ErrorAction SilentlyContinue - } - - $certObj = @() - } - process { - # If no cert is found, return null - if (!$foundCerts) { - $certHash = $null - } else { - if ($RootCA) { - # Check if cert and key name is radius_ca_cert.pem and radius_ca_key.pem if not, rename it - if ($foundCerts.Name -notmatch "radius_ca_cert.pem") { - Rename-Item -Path $foundCerts -NewName "radius_ca_cert.pem" - $foundCerts = Resolve-Path -Path "$JCScriptRoot/Cert/*cert*.pem" -ErrorAction SilentlyContinue - } - # Get the key path and rename it if needed - $foundKey = Resolve-Path -Path "$JCScriptRoot/Cert/*key.pem" -ErrorAction SilentlyContinue - if ($foundKey.Name -notmatch "radius_ca_key.pem") { - Rename-Item -Path $foundKey -NewName "radius_ca_key.pem" - } - - # Create hashtable to contain cert info - $certHash = @{} - # Use openssl to gather serial, subject, issuer, and enddate information - $certInfo = Invoke-Expression "$opensslBinary x509 -in $($foundCerts.Path) -enddate -serial -subject -issuer -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) - } - - $certHash += $property - } - - # Add hash to certObj array - $certObj += $certHash - } elseif ($UserCerts) { - foreach ($cert in $foundCerts) { - # Create hashtable to contain cert info - $certHash = @{} - # Use openssl to gather serial, subject, issuer and enddate information - $certInfo = Invoke-Expression "$opensslBinary x509 -in $($cert.Path) -enddate -serial -subject -issuer -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) - } - - $certHash += $property - } - - # Add hash to certObj array - $certObj += $certHash - } - } - } - } - end { - return $certObj - } -} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Get-ExpiringCertInfo.ps1 b/scripts/automation/Radius/Functions/Private/Get-ExpiringCertInfo.ps1 deleted file mode 100644 index 96229d877..000000000 --- a/scripts/automation/Radius/Functions/Private/Get-ExpiringCertInfo.ps1 +++ /dev/null @@ -1,12 +0,0 @@ -function Get-ExpiringCertInfo { - param ( - $certInfo, - $cutoffDate - ) - process { - $expiringCerts = $certInfo | Where-Object -Property notAfter -LT $cutoffDate - } - end { - return $expiringCerts - } -} \ No newline at end of file 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/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..a6ee2b358 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/HashFunctions/Test-UserFromHash.ps1 @@ -0,0 +1,59 @@ +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 "$JCRScriptRoot/data/userHash.json" | ConvertFrom-Json -AsHashtable + } + } + process { + switch ($PSCmdlet.ParameterSetName) { + 'userid' { + # validate that the userID is in the radiusMembership hash: + if ($Global:JCRRadiusMembers.userID.IndexOf($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()) + 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: + if ($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 + } +} \ 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/Invoke-CommandsRetry.ps1 b/scripts/automation/Radius/Functions/Private/Invoke-CommandsRetry.ps1 deleted file mode 100644 index c2a96a504..000000000 --- a/scripts/automation/Radius/Functions/Private/Invoke-CommandsRetry.ps1 +++ /dev/null @@ -1,52 +0,0 @@ -Function Invoke-CommandsRetry { - [CmdletBinding()] - param ( - [Parameter()] - [System.string] - $jsonFile - ) - begin { - $RetryCommands = @() - $commandsObject = Get-Content -Raw -Path $jsonFile | ConvertFrom-Json -Depth 6 - $queuedCommands = Get-JCQueuedCommands - $SearchFilter = @{ - searchTerm = 'RadiusCert-Install' - fields = @('name') - } - $commandResults = Search-JcSdkCommandResult -SearchFilter $SearchFilter - $groupedCommandResults = $commandResults | Sort-Object -Property responseTime -Descending | Group-Object name, SystemId - $mostRecentCommandResults = $groupedCommandResults | ForEach-Object { $_.Group | Select-Object -First 1 } - } - process { - # Prompt to rerun commands that have failed or expired - Foreach ($command in $commandsObject.commandAssociations) { - $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 - $RetryCommands += $command.commandId - } - } catch { - Write-Error "$($command.commandId) could not be invoked" - } - } - } - } - } - end { - # write out/ update jsonFile - $commandsObject | ConvertTo-Json -Depth 6 | Out-File $jsonFile - return $RetryCommands - } -} \ No newline at end of file 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..9d5af6b1f --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Menus/Get-ResponsePrompt.ps1 @@ -0,0 +1,41 @@ +function Get-ResponsePrompt { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = "The message prompt to display to the console", Mandatory)] + [System.String] + $message, + [Parameter(HelpMessage = "Change messaging if running in CLI")] + [System.Boolean] + $cli = $false + ) + + + $promptForInvokeInput = $true + while ($promptForInvokeInput) { + if ($cli) { + $invokeCommands = Read-Host "$message`nPlease type: 'y'/'n' (or 'E' to exit)" + } else { + $invokeCommands = Read-Host "$message`nPlease type: 'y'/'n' (or 'E' to return to Main Menu)" + } + switch ($invokeCommands) { + 'e' { + if ($cli) { + Write-Host "Exiting..." + } else { + Write-Host "Returning to Main Menu..." + } + $promptForInvokeInput = $false + return 'exit' + } + 'n' { + return $false + } + 'y' { + return $true + } + default { + Write-host "Invalid input`nPlease type 'y'/ 'n' (or 'E' to exit)" -ForegroundColor Red + } + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Menus/PadCenter.ps1 b/scripts/automation/Radius/Functions/Private/Menus/PadCenter.ps1 new file mode 100644 index 000000000..256dbb0d5 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Menus/PadCenter.ps1 @@ -0,0 +1,14 @@ +function PadCenter { + param ( + [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) +} \ 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..668eb8a30 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Menus/Show-CertDeploymentMenu.ps1 @@ -0,0 +1,18 @@ +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 certificate deployment status. `n`t$([char]0x1b)[96mNOTE: This will display every user, their associated devices and which systems have successfully installed the current certificate." + Write-Host "2: Press '2' to view command results. `n`t$([char]0x1b)[96mNOTE: This will display all command results and their status" + Write-Host "3: Press '3' to view failed command runs. `n`t$([char]0x1b)[96mNOTE: This will display all failed command results and their status messages." + Write-Host "4: Press '4' 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/Menus/Show-DistributionMenu.ps1 b/scripts/automation/Radius/Functions/Private/Menus/Show-DistributionMenu.ps1 new file mode 100644 index 000000000..33e6f55c4 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Menus/Show-DistributionMenu.ps1 @@ -0,0 +1,37 @@ +function Show-DistributionMenu { + [CmdletBinding()] + param ( + [Parameter()] + [System.Object] + $certObjectArray, + [Parameter()] + [System.Int32] + $usersThatNeedCertCount, + [Parameter()] + [System.Int32] + $TotalUserCount + ) + + $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 ($certObjectArray) { + Write-Host $(PadCenter -string ' Certificate Information ' -char '-') + 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 + Write-Host "Users that have all their certificates installed: $([int]$TotalUserCount-[int]$usersThatNeedCertCount) of $TotalUserCount" + + } + # ==== instructions ==== + 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 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-GenerationMenu.ps1 b/scripts/automation/Radius/Functions/Private/Menus/Show-GenerationMenu.ps1 new file mode 100644 index 000000000..75cbe8d0b --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Menus/Show-GenerationMenu.ps1 @@ -0,0 +1,31 @@ +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 -Format "o") -End ([dateTime]("$($_.notAfter)"))).Days + } + }, @{name = "Expires On"; expression = { + [datetime]($_.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 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-RadiusMainMenu.ps1 b/scripts/automation/Radius/Functions/Private/Menus/Show-RadiusMainMenu.ps1 new file mode 100644 index 000000000..62a18b89f --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Menus/Show-RadiusMainMenu.ps1 @@ -0,0 +1,64 @@ +function Show-RadiusMainMenu { + param ( + [string]$Title = ' JumpCloud Radius Cert Deployment ' + ) + # Get cert information + $rootCAInfo = Get-CertInfo -rootCa + $userCertInfo = Get-CertInfo -UserCerts + + # Find all certs that will expire between current date and cut off date + try { + $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $global:JCRConfig.certExpirationWarningDays.value + } catch { + Write-Debug "No user certs exist, there are no user certs which will expire soon" + } + + # Get UserGroup information from Config.json + $radiusUserGroup = Get-JcSdkUserGroup -Id $global:JCRConfig.userGroup.value | Select-Object Name + $radiusUserGroupMemberCount = (Get-JcSdkUserGroupMember -GroupId $global:JCRConfig.userGroup.value).Count + + # Get SSID information from Config.json + $radiusSSID = ($global:JCRConfig.networkSSID.value).replace(';', ' ') + + # Output for Users + Clear-Host + + # ==== TITLE ==== + Write-Host $(PadCenter -string $Title -char '=') + If (($global:JCRConfig -eq $null) -or (-not (Confirm-JCRConfig))) { + Write-Host $(PadCenter -string "Edit the required settings with `Set-JCRConfig` before continuing this script `n" -char ' ') -ForegroundColor Yellow + } + # /==== TITLE ==== + + # ==== ROOT CA ==== + Write-Host $(PadCenter -string ' Root CA ' -char '-') + if ($rootCAInfo -eq $null) { + Write-Host $(PadCenter -string "No Root CA detected`n" -char ' ') -ForegroundColor Yellow + } else { + 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 ($Global:expiringCerts) { + Write-Host $(PadCenter -string "$($Global:expiringCerts.Count) user certs will expire in 15 days `n" -char ' ') -ForegroundColor Red + } + Write-Host $(PadCenter -string " Details " -char '-') + # /==== ROOT CA ==== + + # ==== GROUP/SSID/Global Variables ==== + Write-Host $(PadCenter -string "Radius User Group: $($radiusUserGroup.Name)" -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 + if ($IsMacOS) { + Write-Host $(PadCenter -string "Last Updated User/System Data: $($global:JCRConfig.lastUpdate.value)" -char " ") -ForegroundColor Green + } If ($isWindows) { + Write-Host $(PadCenter -string "Last Updated User/System Data: $($global:JCRConfig.lastUpdate.value)" -char " ") -ForegroundColor Green + } + + Write-Host $(PadCenter -string "-" -char '-') + Write-Host "1: Press '1' to generate/update 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/Menus/Show-RadiusProgress.ps1 b/scripts/automation/Radius/Functions/Private/Menus/Show-RadiusProgress.ps1 new file mode 100644 index 000000000..e0abe6287 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Menus/Show-RadiusProgress.ps1 @@ -0,0 +1,33 @@ +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++) { + $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)" + + } else { + $propertyvalues = @($previousOperationResult.Values) + $propertyvalues += "$($completedItems) / $($TotalItems)" + + } + + write-host ($headerString -f $propertyValues) + if ($completedItems -eq $totalItems) { + Write-Host $(PadCenter -string "" -char '-') + } + +} diff --git a/scripts/automation/Radius/Functions/Private/Menus/Show-RootCAGenerationMenu.ps1 b/scripts/automation/Radius/Functions/Private/Menus/Show-RootCAGenerationMenu.ps1 new file mode 100644 index 000000000..8709b9c68 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Menus/Show-RootCAGenerationMenu.ps1 @@ -0,0 +1,45 @@ +function Show-RootCAGenerationMenu { + $title = ' JumpCloud Radius Root CA Cert Deployment ' + Clear-Host + Write-Host $(PadCenter -string $Title -char '=') + Write-Host $(PadCenter -string "Select an option below to generate/regenerate root certificate`n" -char ' ') -ForegroundColor Yellow + + $rootCAInfo = Get-CertInfo -rootCa + Write-Host $(PadCenter -string ' Root CA ' -char '-') + if ($rootCAInfo -eq $null) { + Write-Host $(PadCenter -string "No Root CA detected`n" -char ' ') -ForegroundColor Yellow + } else { + 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 root CA is expiring within 30 days, display warning + $expirationDate = [datetime]$rootCAInfo.notAfter + $currentDate = Get-Date + $daysRemaining = ($expirationDate - $currentDate).Days + + # Check if expiration is less root cert warning days + if ($daysRemaining -lt $JCR_ROOT_CERT_Expire_Warning_Days) { + Write-Host $(PadCenter -string "WARNING: The root certificate is expiring in $daysRemaining day(s) on $expirationDate! Please press '2' to regenerate your root cert.") -ForegroundColor Yellow + } + } + + Write-Host $(PadCenter -string ' Root Certificate Generation Options ' -char '-') + + + + if (Test-Path -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem") { + # Generate new root CA + Write-Host "1: Press '1' to replace current root certificate. `n`t$([char]0x1b)[96mNOTE: you will be prompted to overwrite any previously generated certificates. This will generate a new root CA `n`twith a new serial number and user certs generated with the previous CA will no longer authenticate." + # Regenerate with the same serial number + Write-Host "2: Press '2' to renew root certificate. `n`t$([char]0x1b)[96mNOTE: renewing the root CA will contain the same serial number and CA subject headers. User certs generated with`n`tthe previous CA will continue to authenticate." + # Return to main menu + Write-Host "E: Press 'E' to return to main menu." + } else { + # Generate new root CA + Write-Host "1: Press '1' to generate new root certificate" + # Return to main menu + 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-StatusMessage.ps1 b/scripts/automation/Radius/Functions/Private/Menus/Show-StatusMessage.ps1 new file mode 100644 index 000000000..25f008834 --- /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 3 +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Module/Get-JCRUserAgent.ps1 b/scripts/automation/Radius/Functions/Private/Module/Get-JCRUserAgent.ps1 new file mode 100644 index 000000000..35f9ced9c --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Module/Get-JCRUserAgent.ps1 @@ -0,0 +1,13 @@ +function Get-JCRUserAgent { + $ModuleRoot = (Get-Item -Path:($PSScriptRoot)).Parent.Parent.Parent.FullName + $psd1Path = Join-Path -Path $ModuleRoot -ChildPath 'JumpCloud.Radius.psd1' + $psd1 = Import-PowerShellDataFile -Path $psd1Path + $UserAgent_ModuleVersion = $psd1.ModuleVersion + $UserAgent_ModuleName = 'PasswordlessRadiusConfig' + #Build the UserAgent string + $UserAgent_ModuleName = "JumpCloud_$($UserAgent_ModuleName).PowerShellModule" + $Template_UserAgent = "{0}/{1}" + $UserAgent = $Template_UserAgent -f $UserAgent_ModuleName, $UserAgent_ModuleVersion + # When we import this config, this function will run and validate the openSSL binary location + return $UserAgent +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Module/Get-OpenSSLVersion.ps1 b/scripts/automation/Radius/Functions/Private/Module/Get-OpenSSLVersion.ps1 new file mode 100644 index 000000000..70886d5ec --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Module/Get-OpenSSLVersion.ps1 @@ -0,0 +1,59 @@ +function Get-OpenSSLVersion { + [CmdletBinding()] + param ( + [Parameter()] + [system.string] + $opensslBinary + ) + begin { + $conditionsNotMet = $false + try { + $version = Invoke-Expression "& '$opensslBinary' version" + } catch { + Write-Warning "OpenSSL Not Found`nThe module could not find 'openssl' or the path is incorrect. Please update the 'OpenSSLBinary' setting for this module with the Set-JCRConfig cmdlet:`nWindows: Set-JCRConfig -openSSLBinary 'C:\Path\To\OpenSSL\bin\openssl.exe'`nMacOS/Linux: Set-JCRConfig -openSSLBinary '/opt/homebrew/bin/openssl'" + $conditionsNotMet = $true + } + + # Required OpenSSL Version + $OpenSSLVersion = [version]"3.0.0" + + # Determine Libre or Open SSL: + if ($version -match "LibreSSL") { + Write-Error "LibreSSL does not meet the requirements of this application, please install OpenSSL v3.0.0 or later" + $conditionsNotMet = $true + } else { + [version]$Version = (Select-String -InputObject $version -Pattern "([0-9]+)\.([0-9]+)\.([0-9]+)").matches.value + } + + # Determine if windows: + if ([System.Environment]::OSVersion.Platform -match "Win") { + if ($env:OPENSSL_MODULES) { + $binItems = Get-ChildItem -Path $env:OPENSSL_MODULES + if ("legacy.dll" -notin $binItems.Name) { + Write-Error "The required OpenSSL 'legacy.dll' file was not found in the bin path $PathDirectory. This is required to create certificates. `nIf this module file is located elsewhere, you may specify the path to that directory in this powershell session using this command: '`$env:OPENSSL_MODULES = C:/Path/To/Directory' " + $conditionsNotMet = $true + } + } else { + # Try to point to the Legacy.dll file + Write-Error "The required OpenSSL 'legacy.dll' file is required for this project. This module file is required to create certificates. `nIf this module file is located elsewhere, you may specify the path to that directory in this powershell session using this command: '`$env:OPENSSL_MODULES = C:/Path/To/openSSL_Directory/' Where the legacy.dll file is in openSSL_Directory " + $conditionsNotMet = $true + + } + + } + } + process { + if ($version -lt $OpenSSLVersion) { + Write-Warning "The installed version of OpenSSL: OpenSSL $Version, does not meet the requirements of this application, please install a later version of at least $Type $Version" + $conditionsNotMet = $true + } + } + end { + # Return false if the version is less than the required version + if ($conditionsNotMet) { + return $false + } else { + return $true + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Module/Validate-JCRModuleRequirements.ps1 b/scripts/automation/Radius/Functions/Private/Module/Validate-JCRModuleRequirements.ps1 new file mode 100644 index 000000000..4181aae89 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Module/Validate-JCRModuleRequirements.ps1 @@ -0,0 +1,6 @@ +function Validate-JCRModuleRequirements { + $openSSLValidated = Get-OpenSSLVersion -opensslBinary $global:JCRConfig.openSSLBinary.value + if ($openSSLValidated -eq $false) { + throw "OpenSSL validation failed. Please check the OpenSSL path and version." + } +} diff --git a/scripts/automation/Radius/Functions/Private/Reports/Get-LatestUserToDeviceReport.ps1 b/scripts/automation/Radius/Functions/Private/Reports/Get-LatestUserToDeviceReport.ps1 new file mode 100644 index 000000000..2a71aa8de --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Reports/Get-LatestUserToDeviceReport.ps1 @@ -0,0 +1,54 @@ +Function Get-LatestUserToDeviceReport { + [CmdletBinding()] + param ( + [Parameter()] + [System.Int32] + $minutes + ) + begin { + # get UTC time now: + $utcTimeNow = Get-Date ([datetime]::UtcNow) -UFormat "%m/%d/%Y %r" + + $headers = @{ + "accept" = "application/json"; + "x-api-key" = $Env:JCApiKey; + "x-org-id" = $Env:JCOrgId + } + + } + process { + $reportList = Get-JCsdkReport -Sort 'CREATED_AT' + $userAndDeviceReports = $reportList | Where-Object { $_.Type -eq "ods-users-to-devices" } + if ($userAndDeviceReports) { + # get the first report + $firstUAndDReport = $userAndDeviceReports | Select-Object -First 1 + $timespan = New-TimeSpan -end $utcTimeNow -start $firstUAndDReport.Created_At + # if time between now and the last generated report is >=24, create a new report + if ($timespan.Minutes -ge $minutes) { + # write-host "last report generated $($timespan.Minutes) minutes ago; generating new user to device report" + $requestedReport = New-JcSdkReport -ReportType 'users-to-devices' + $latestReport = Get-ReportByID -reportID $requestedReport.Id + } else { + # write-host "last report generated $($timespan.Minutes) minutes ago" + # return the last report + $latestReport = Get-ReportByID -reportID $firstUAndDReport.Id + } + + } else { + # if there are no reports create a new one: + # write-host "generating new user to device report" + $requestedReport = New-JcSdkReport -ReportType 'users-to-devices' + $latestReport = Get-ReportByID -reportID $requestedReport.Id + } + + # get the json artifact: + # download json + $artifactID = ($latestReport.artifacts | Where-Object { $_.format -eq 'json' }).id + $reportID = $latestReport.id + $reportContent = Invoke-RestMethod -Uri "https://api.jumpcloud.com/insights/directory/v1/reports/$reportID/artifacts/$artifactID/content" -Method GET -Headers $headers + } + end { + # return the latest user report + return $reportContent + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/Reports/Get-ReportByID.ps1 b/scripts/automation/Radius/Functions/Private/Reports/Get-ReportByID.ps1 new file mode 100644 index 000000000..df3077253 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Reports/Get-ReportByID.ps1 @@ -0,0 +1,25 @@ +Function Get-ReportByID { + [CmdletBinding()] + param ( + [Parameter()] + [System.String] + $reportID + ) + begin { + # write-host "attempting to get report by id: $reportId" + } + process { + do { + $reportList = Get-JCsdkReport -Sort 'CREATED_AT' + $foundReport = $reportList | Where-Object { $_.Id -eq $reportID } + if ($foundReport.Status -eq "PENDING") { + Write-Warning "[status] waiting 10s for jumpcloud report to complete" + start-sleep -Seconds 10 + } + + } until ($foundReport.Status -eq "COMPLETED") + } + end { + return $foundReport + } +} \ No newline at end of file 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..5a3547013 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Settings/Set-JCRAssociationHash.ps1 @@ -0,0 +1,49 @@ +function Set-JCRAssociationHash { + [CmdletBinding()] + param ( + [Parameter()] + [system.string] + $userId + ) + begin { + # get the data + $associationFile = "$JCRScriptRoot/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 = if ($systemDetails.osFamily -eq "darwin") { + "macOS" + } elseif ($systemDetails.osFamily -eq "windows") { + "windows" + } + 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 new file mode 100644 index 000000000..f0bcd5183 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/Settings/Update-JCRUsersJson.ps1 @@ -0,0 +1,85 @@ +function Update-JCRUsersJson { + [CmdletBinding()] + param ( + [Parameter()] + [switch] + $force + ) + begin { + } + process { + # validate that the system association data is correct in users.json: + # $userArray = Get-UserJsonData + $i = 0 + foreach ($user in $Global:JCRRadiusMembers) { + $i++ + Write-Progress -activity "Getting User Certificate Info" -status "$($user.username): $i of $($Global:JCRRadiusMembers.Count)" -percentComplete (($i / $Global:JCRRadiusMembers.Count) * 100) + $MatchedUser = $GLOBAL:JCRUsers[$user.userID] + if (-not $MatchedUser) { + Write-Warning "Could not find user with userID: $($user.userID) in JCRUsers" + } + $userArrayObject, $userIndex = Get-UserFromTable -userID $user.userID + try { + $InstalledCerts = $Global:JCRCertHash["$($userArrayObject.certInfo.sha1)"] + + } catch { + + $InstalledCerts = $null + } + + if ($userIndex -ge 0) { + # $userArrayObject + # 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 = $incomingList + Set-UserTable -index $userIndex -username $MatchedUser.username -localUsername $MatchedUser.systemUsername -systemAssociationsObject ($incomingList) -deploymentObject $InstalledCerts + } else { + # 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) -deploymentObject $InstalledCerts + } + } + } catch { + <#Do this if a terminating exception happens#> + $difference = $null + } + + } else { + # case for new user + New-UserTable -id $user.userID -username $MatchedUser.username -localUsername $matchedUser.systemUsername + } + } + # validate users that should no longer be recorded: + $userArray = Get-UserJsonData + foreach ($user in $userArray) { + # If userID from users.json is no longer in RadiusMembers.keys, then: + If (($user.userId -notin $Global:JCRRadiusMembers.userID) ) { + # Get User From Table + $userObject, $userIndex = Get-UserFromTable -userID $user.userId + # Remove the User From Table + $userArray = $userArray | Where-Object { $_.userID -ne $user.userId } + } + } + } + end { + # Clear-Host + Set-UserJsonData -userArray $userArray + } +} \ 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/Show-RadiusMainMenu.ps1 b/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 deleted file mode 100644 index fba4f9122..000000000 --- a/scripts/automation/Radius/Functions/Private/Show-RadiusMainMenu.ps1 +++ /dev/null @@ -1,34 +0,0 @@ -function Show-RadiusMainMenu { - param ( - [string]$Title = 'JumpCloud Radius Cert Deployment' - ) - # Get cert information - $rootCAInfo = Get-CertInfo -rootCa - $userCertInfo = Get-CertInfo -UserCerts - - # Determine cut off date for expiring certs - $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 - - # Output for Users - Clear-Host - Write-Host "================ $Title ================" - Write-Host "$([char]0x1b)[33mEdit the variables in Config.ps1 before continuing this script `n" - if ($rootCAInfo -eq $null) { - Write-Host "$([char]0x1b)[33mNo Root CA detected`n" - } else { - Write-Host "$([char]0x1b)[32mRoot CA Serial Number: $($rootCAInfo.serial)" - Write-Host "$([char]0x1b)[32mRoot CA Expiration: $($rootCAInfo.notAfter)`n" - } - if ($expiringCerts) { - Write-Host "$([char]0x1b)[91m$($($expiringCerts.subject).Count) user certs will expire in 15 days `n" - } - - 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 "Q: Press 'Q' to quit." -} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Private/System/Get-CaseInsensitiveFile.ps1 b/scripts/automation/Radius/Functions/Private/System/Get-CaseInsensitiveFile.ps1 new file mode 100644 index 000000000..28041287b --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/System/Get-CaseInsensitiveFile.ps1 @@ -0,0 +1,11 @@ +function Get-CaseInsensitiveFile { + param ( + [Parameter(Mandatory)] + [string]$Directory, + [Parameter(Mandatory)] + [string]$FileName + ) + $file = Get-ChildItem -Path $Directory -File | Where-Object { $_.Name -ieq $FileName } | Select-Object -First 1 + if ($file) { return $file.FullName } + else { return (Join-Path $Directory $FileName) } # If not found, return the intended path for creation +} \ 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..077d6c3a6 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/SystemTable/New-SystemTable.ps1 @@ -0,0 +1,34 @@ +function New-SystemTable { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [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 = [ordered]@{ + systemId = $system.systemId + hostname = $system.hostname + osFamily = if (($system.osFamily -eq "darwin") -or ($system.osFamily -eq "macOS")) { + "macOS" + } elseif ($system.osFamily -eq "windows") { + "windows" + } + } + $systemAssociations += $systemTable + } + + } + end { + return $systemAssociations + } +} \ 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..87b1055b5 --- /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 "$JCRScriptRoot/users.json" -PathType Leaf) { + $content = (Get-Content -Raw -Path "$JCRScriptRoot/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..bdd39f53d --- /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 "$JCRScriptRoot/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 new file mode 100644 index 000000000..cb89f823d --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/UserTable/Get-UserFromTable.ps1 @@ -0,0 +1,36 @@ +function Get-UserFromTable { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [System.String] + $userId + ) + begin { + $userArray = Get-UserJsonData + # 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 "userId: $($userId) was not found in users.json" + } + } 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 + } + } +} 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..dfac6949f --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/UserTable/New-UserTable.ps1 @@ -0,0 +1,46 @@ +function New-UserTable { + [CmdletBinding()] + param ( + [Parameter()] + [System.String] + $id, + [Parameter()] + [System.String] + $username, + [Parameter()] + [System.String] + $localUsername + ) + begin { + $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 + $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 { + 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 new file mode 100644 index 000000000..57a509fd5 --- /dev/null +++ b/scripts/automation/Radius/Functions/Private/UserTable/Set-UserTable.ps1 @@ -0,0 +1,97 @@ +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, + [Parameter()] + [System.Object] + $deploymentObject + ) + begin { + # Get User Array: + $userArray = Get-UserJsonData + + # CUT-3470: Check to see if the userArray is not an array, cast it to one + if ($userArray -isnot [array]) { + Write-Debug "userArray is $($userArray.GetType().Name), casting to arrayList" + $array = New-Object System.Collections.ArrayList + $array.add($userArray) | Out-Null + $userArray = $array + } + + if ($PSBoundParameters.ContainsKey('index')) { + $userIndex = $index + $userObject = $userArray[$index] + } + if (($PSBoundParameters.ContainsKey('lookup'))) { + # Get User From Table + $userObject, $userIndex = Get-UserFromTable -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) { + $systemAssociationsInfo = $systemAssociationsObject + } else { + $systemAssociationsInfo = $userObject.systemAssociations + } + if ($commandAssociationsObject) { + $commandAssociationsInfo = $commandAssociationsObject + } else { + $commandAssociationsInfo = $userObject.commandAssociations + } + if ($certInfoObject) { + $certInfo = $certInfoObject + } else { + $certInfo = $userObject.certInfo + } + if ($deploymentObject) { + $deploymentInfo = $deploymentObject + } else { + $deploymentInfo = $null + } + } + process { + # build the userTable object + $userTable = [PSCustomObject]@{ + userId = $userObject.userId + userName = $userObject.username + localUsername = $userObject.localUsername + systemAssociations = $systemAssociationsInfo + commandAssociations = $commandAssociationsInfo + certInfo = $certInfo + deploymentInfo = $deploymentInfo + } + # set the user table to new object + $userArray[$userIndex] = $userTable + + } + end { + # update the userTable + Set-UserJsonData -userArray $userArray + } +} 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/Config/Set-JCRConfig.ps1 b/scripts/automation/Radius/Functions/Public/Config/Set-JCRConfig.ps1 new file mode 100644 index 000000000..2bce976ba --- /dev/null +++ b/scripts/automation/Radius/Functions/Public/Config/Set-JCRConfig.ps1 @@ -0,0 +1,94 @@ +function Set-JCRConfig { + [CmdletBinding()] + param ( + ) + DynamicParam { + # $ModuleRoot = (Get-Item -Path:($PSScriptRoot)).Parent.Parent.Parent.FullName + # $configFilePath = Join-Path -Path $ModuleRoot -ChildPath 'Config.json' + + # if (Test-Path -Path $configFilePath) { + # Create the dictionary + $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary + # Foreach key in the supplied config file: + foreach ($setting in $global:JCRConfigTemplate.keys) { + $settingName = $setting + $settingValue = $global:JCRConfigTemplate[$setting] + # Skip create dynamic params for these not-writable properties: + if ($settingValue.Write -eq $false) { + continue + } + # Set the dynamic parameters' name + # write-host "adding dynamic param: $key$($item) $($config[$key][$item]['value'].getType().Name)" + $ParamName_Filter = "$($settingName)" + # Create the collection of attributes + $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute] + + # Set the type of the parameter + $paramType = $settingValue.type + + # Create and set the parameters' attributes + $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute + $ParameterAttribute.Mandatory = $false + $ParameterAttribute.HelpMessage = "sets the $($settingName) config for the module" + # 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 { + # Config should be in /PowerShell/JumpCloudModule/Config.json + $ModuleRoot = (Get-Item -Path:($PSScriptRoot)).Parent.Parent.Parent.FullName + $configFilePath = Join-Path -Path $ModuleRoot -ChildPath 'Config.json' + # config should be loaded from the module + + if (-NOT $global:JCRConfig) { + # create the config file from template + New-JCRConfig + # set the variable + $global:JCRConfig = Get-JCRConfig -asObject + } + } + + process { + $params = $PSBoundParameters + # update config settings + foreach ($param in $params.Keys) { + # validate the parameters + switch ($param) { + 'radiusDirectory' { + # validate the directory + $validRadiusDir = Test-JCRRadiusDirectory -Path $params[$param] + if (-not $validRadiusDir) { + Write-Error "The radius directory path '$($params[$param])' is not valid or does not exist. Please create this directory before trying again." + } + } + } + # set the value of the config setting to the value passed into this function + if ($global:JCRConfig.PSObject.Properties.Name -contains $param) { + $global:JCRConfig.$param.value = $params[$param] + } else { + # Add the property with a hashtable structure (assuming you want to match existing pattern) + $global:JCRConfig | Add-Member -MemberType NoteProperty -Name $param -Value $global:JCRConfigTemplate[$param] + # now update the value: + $global:JCRConfig.$param.value = $params[$param] + } + } + # validate the config settings + } + + end { + try { + $global:JCRConfig | ConvertTo-Json -Depth 10 | Out-File -FilePath $configFilePath + Confirm-JCRConfig + } catch { + Write-Error "Failed to validate JCRConfig File: $_" + throw + } + } +} \ 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 deleted file mode 100644 index 1cd42ee19..000000000 --- a/scripts/automation/Radius/Functions/Public/Distribute-UserCerts.ps1 +++ /dev/null @@ -1,434 +0,0 @@ -# Import Global Config: -. "$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 6 -# Check to see if previous commands exist -$RadiusCertCommands = Get-JCCommand | Where-Object { $_.Name -like 'RadiusCert-Install*' } - -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 "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 - } - } - 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 - } - } - } - } - `$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" - } -} - -# 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]" -} - -$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" - -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 diff --git a/scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 b/scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 deleted file mode 100644 index ae41962b1..000000000 --- a/scripts/automation/Radius/Functions/Public/Generate-RootCert.ps1 +++ /dev/null @@ -1,64 +0,0 @@ -# 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" - -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 - -# 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..." - } else { - Write-Host "Exiting " - break - } -} -$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 deleted file mode 100644 index e83a06068..000000000 --- a/scripts/automation/Radius/Functions/Public/Generate-UserCerts.ps1 +++ /dev/null @@ -1,113 +0,0 @@ -# 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" - } else { - Write-Host "CA-Key password is incorrect" - 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 - -$SystemHash = Get-JCSystem -returnProperties displayName, os - -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 - } - -} else { - Write-Host "[status] users.json file not found" - $userArray = @() -} - -# Get user membership of group -$groupMembers = Get-GroupMembership -groupID $JCUSERGROUP -if ($groupMembers) { - Write-Host "[status] Found $($groupmembers.count) users in Radius User Group" -} - -# 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" -} - -# 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 - } - $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" - } - $userArray += $userTable - } -} - -$userArray | ConvertTo-Json -Depth 6 | Out-File "$JCScriptRoot\users.json" - diff --git a/scripts/automation/Radius/Functions/Public/Get-JCRCertReport.ps1 b/scripts/automation/Radius/Functions/Public/Get-JCRCertReport.ps1 new file mode 100644 index 000000000..cc4a1f676 --- /dev/null +++ b/scripts/automation/Radius/Functions/Public/Get-JCRCertReport.ps1 @@ -0,0 +1,80 @@ +function Get-JCRCertReport { + param( + [Parameter(Mandatory)] + [ValidateScript({ + $directory = Split-Path -Path $_ -Parent + if (-not (Test-Path -Path $directory -PathType Container)) { + throw "The directory '$directory' does not exist." + } + if (-not ($_ -like '*.csv')) { + throw "The specified path '$_' does not end with '.csv'." + } + return $true + })] + [System.IO.FileInfo]$ExportFilePath + ) + if ($Global:JCRSettings.sessionImport -eq $false) { + Get-JCRGlobalVars + $Global:JCRSettings.sessionImport = $true + } + + # Initialize an empty array to store the results + $reportData = New-Object System.Collections.ArrayList + + $radiusMembersPath = Join-Path -Path $JCRScriptRoot -ChildPath "data/radiusMembers.json" + $certHashPath = Join-Path -Path $JCRScriptRoot -ChildPath "data/certHash.json" + $associationHashPath = Join-Path -Path $JCRScriptRoot -ChildPath "data/associationHash.json" + + if (!(Test-Path $radiusMembersPath)) { + Write-Error "radiusMembers.json not found at $radiusMembersPath" + continue # Skip to the next group if file not found + } + if (!(Test-Path $certHashPath)) { + Write-Error "certHash.json not found at $certHashPath" + continue # Skip to the next group if file not found + } + if (!(Test-Path $associationHashPath)) { + Write-Error "associationHash.json not found at $associationHashPath" + continue # Skip to the next group if file not found + } + + + $radiusMembers = Get-Content $radiusMembersPath | ConvertFrom-Json + $certHashes = Get-Content $certHashPath | ConvertFrom-Json + $associationHash = Get-Content $associationHashPath | ConvertFrom-Json + + + foreach ($user in $radiusMembers) { + $userSystemAssociations = $associationHash.$($user.userID).systemAssociations + $userCerts = Get-CertInfo -UserCerts -username $user.username + foreach ($system in $userSystemAssociations) { + $reportEntry = [ordered]@{} + $reportEntry.username = $user.username + $reportEntry.userid = $user.userid + $reportEntry.systemHostname = $system.hostname + $reportEntry.systemID = $system.systemId + $reportEntry.systemOS = $system.osFamily + + + # Check if certificate is installed on the device + $certInstalled = $false + $certificateSerialNumber = $userCerts.serial + $certificateExpirationDate = $userCerts.notAfter + + if ($certHashes.$($userCerts.sha1).systemId -contains $system.systemId) { + $certInstalled = $true + } + + $reportEntry.certSerialNumber = $certificateSerialNumber + $reportEntry.certExpirationDate = $certificateExpirationDate + $reportEntry.certInstalled = $certInstalled + + $reportData.Add([pscustomobject]$reportEntry) | Out-Null + } + } + + + # Export to CSV + $reportData | Export-Csv -Path $ExportFilePath -NoTypeInformation + Write-Host "Certificate report generated at: $ExportFilePath" +} \ 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 new file mode 100644 index 000000000..02b1e5983 --- /dev/null +++ b/scripts/automation/Radius/Functions/Public/Get-JCRGlobalVars.ps1 @@ -0,0 +1,284 @@ +function Get-JCRGlobalVars { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = "Force update all cached users, systems, associations, radius group members")] + [switch] + $force, + [Parameter(HelpMessage = "Skips the user to system association cache, which may take a long time on larger organizations")] + [switch] + $skipAssociation, + [Parameter(HelpMessage = "Updates the system to user association cache manually using the graph api")] + [switch] + $associateManually, + [Parameter(HelpMessage = "Updates just a single user's associations manually using the graph api")] + [System.String] + $associationUsername + ) + begin { + Write-Verbose 'Verifying JCAPI Key' + if ($JCAPIKEY.length -ne 40) { + Connect-JCOnline -force + } + # ensure the data directory exists to cache the json files: + if (-not (Test-Path "$JCRScriptRoot/data")) { + Write-Host "[status] Creating Data Directory" + New-Item -ItemType Directory -Path "$JCRScriptRoot/data" + } + + if (-Not $global:JCRConfig) { + $global:JCRConfig = Get-JCRConfig + } + + # get settings file + if ($IsMacOS) { + try { + $lastUpdateTimeSpan = New-TimeSpan -Start $($global:JCRConfig.lastUpdate.value) -End (Get-Date) + } catch { + Write-Host "[status] lastUpdate value is null, updating global variables" + $update = $true + $updateAssociation = $true + Set-JCRConfig -lastUpdate (Get-Date) + } + } + if ($ifWindows) { + try { + $lastUpdateTimeSpan = New-TimeSpan -Start $($global:JCRConfig.lastUpdate.value) -End (Get-Date) + } catch { + Write-Host "[status] lastUpdate value is null, updating global variables" + $update = $true + $updateAssociation = $true + Set-JCRConfig -lastUpdate (Get-Date) + } + } + if ($lastUpdateTimeSpan.TotalHours -gt 24) { + $update = $true + $updateAssociation = $true + } else { + $update = $false + $updateAssociation = $false + } + if ($force) { + $update = $true + switch ($skipAssociation) { + $true { + $updateAssociation = $false + } + $false { + $updateAssociation = $true + } + } + switch ($associateManually) { + $true { + $updateAssociation = $false + $setAssociations = $true + } + $false { + $updateAssociation = $true + $setAssociations = $false + } + } + } + + # also validate that the data files are non-null, if they are, force update] + $requiredHashFiles = @('radiusMembers.json', 'systemHash.json', 'userHash.json', 'certHash.json') + foreach ($file in $requiredHashFiles) { + if (Test-Path -Path "$JCRScriptRoot/data/$file") { + $fileContents = Get-Content "$JCRScriptRoot/data/$file" + } else { + Write-Host "[status] $JCRScriptRoot/data/$file file does not exist, updating global variables" + $update = $true + } + # if the file is null force update + if ([string]::IsNullOrEmpty($fileContents)) { + Write-Host "[status] $JCRScriptRoot/data/$file file is null, updating global variables" + $update = $true + } + } + + $requiredAssociationHashFiles = @('associationHash.json') + foreach ($file in $requiredAssociationHashFiles) { + if (Test-Path -Path "$JCRScriptRoot/data/$file") { + $fileContents = Get-Content "$JCRScriptRoot/data/$file" + switch ($skipAssociation) { + $true { + # Write-Host "[status] $JCRScriptRoot/data/$file will be skipped" + $updateAssociation = $false + } + } + } else { + Write-Host "[status] $JCRScriptRoot/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] $JCRScriptRoot/data/$file file is null, updating global variables" + $update = $true + $updateAssociation = $true + } else { + switch ($skipAssociation) { + $true { + # Write-Host "[status] $JCRScriptRoot/data/$file will be skipped" + $updateAssociation = $false + } + } + } + } + } + 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:JCRConfig.userGroup.value + # add the username to the membership hash + $radiusMemberList = New-Object System.Collections.ArrayList + foreach ($member in $radiusMembers) { + $radiusMemberList.Add( + [PSCustomObject]@{ + 'userID' = $member.toID + 'username' = $users[$member.toID].username + } + ) | Out-Null + } + if ($updateAssociation) { + # get the latest report, generate a new report if it's been more than 10 mins + $reportContent = Get-LatestUserToDeviceReport -minutes 10 + # 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 "$JCRScriptRoot/data/associationHash.json" + } else { + $userAssociationList = Get-Content -Raw -Path "$JCRScriptRoot/data/associationHash.json" | ConvertFrom-Json -Depth 6 -AsHashtable + + } + if ($setAssociations) { + # create the hashtable: + $userAssociationList = New-Object System.Collections.Hashtable + foreach ($user in $radiusMemberList) { + $userSystemMembership = Get-JcSdkUserTraverseSystem -UserId $user.userID + if ($userSystemMembership) { + $userAssociationList.add( + $user.userID, @{ + 'systemAssociations' = @($userSystemMembership | Select-Object -Property @{Name = 'systemId'; Expression = { $_.id } }, @{Name = 'hostname'; Expression = { $systems[$_.id].hostname } }, @{Name = 'osFamily'; Expression = { + $osFamilyValue = $systems[$_.id].osFamily + if ($osFamilyValue -eq 'darwin') { + "macOS" + } elseif ($osFamilyValue -eq 'Windows') { + "Windows" + } else { + $osFamilyValue + } + } + }); + 'userData' = @($user | Select-Object -Property @{Name = 'email'; Expression = { $users[$user.userID].email } }, @{Name = 'username'; Expression = { $users[$user.userID].username } }) + }) | Out-Null + } + + } + # write out the association hash + $userAssociationList | ConvertTo-Json -Depth 10 | Out-File "$JCRScriptRoot/data/associationHash.json" + } + if ($associationUsername) { + $userAssociationList = Get-Content -Raw -Path "$JCRScriptRoot/data/associationHash.json" | ConvertFrom-Json -Depth 6 -AsHashtable + $matchedUser = $radiusMemberList | Where-Object { $_.username -eq $associationUsername } + if (-Not $matchedUser) { + Write-Warning "user not found" + } else { + $userSystemMembership = Get-JcSdkUserTraverseSystem -UserId $matchedUser.userID + if ($userSystemMembership) { + # check if the user exists + if ($userAssociationList[$matchedUser.userID]) { + $userAssociationList[$matchedUser.userID].'systemAssociations' = @($userSystemMembership | Select-Object -Property @{Name = 'systemId'; Expression = { $_.id } }, @{Name = 'hostname'; Expression = { $systems[$_.id].hostname } }, @{Name = 'osFamily'; Expression = { + $osFamilyValue = $systems[$_.id].osFamily + if ($osFamilyValue -eq 'darwin') { + "macOS" + } elseif ($osFamilyValue -eq 'Windows') { + "Windows" + } else { + $osFamilyValue + } + } + }); + } else { + Write-Warning "user not found in association list" + $userAssociationList.add( + $matchedUser.UserId, @{ + 'systemAssociations' = @($userSystemMembership | Select-Object -Property @{Name = 'systemId'; Expression = { $_.id } }, @{Name = 'hostname'; Expression = { $systems[$_.id].hostname } }, @{Name = 'osFamily'; Expression = { + $osFamilyValue = $systems[$_.id].osFamily + if ($osFamilyValue -eq 'darwin') { + "macOS" + } elseif ($osFamilyValue -eq 'Windows') { + "Windows" + } else { + $osFamilyValue + } + } + }); + 'userData' = @($matchedUser | Select-Object -Property @{Name = 'email'; Expression = { $users[$user.userID].email } }, @{Name = 'username'; Expression = { $users[$matchedUser.userID].username } }) + }) | Out-Null + } + } + # + } + # write out the association hash + $userAssociationList | ConvertTo-Json -Depth 10 | Out-File "$JCRScriptRoot/data/associationHash.json" + } + # update certHash in parallel: + $certHash = Get-CertHash + + # finally write out the data to file: + $users | ConvertTo-Json -Depth 100 -Compress | Out-File "$JCRScriptRoot/data/userHash.json" + $systems | ConvertTo-Json -Depth 10 | Out-File "$JCRScriptRoot/data/systemHash.json" + $radiusMemberList | ConvertTo-Json | Out-File "$JCRScriptRoot/data/radiusMembers.json" + $certHash | ConvertTo-Json | Out-File "$JCRScriptRoot/data/certHash.json" + } + $false { + # 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 "$JCRScriptRoot/data/associationHash.json" | ConvertFrom-Json -Depth 6 -AsHashtable + } + } + } + end { + switch ($update) { + $true { + # set global vars + $Global:JCRUsers = $users + $Global:JCRSystems = $systems + $Global:JCRAssociations = $userAssociationList + [array]$Global:JCRRadiusMembers = $radiusMemberList + $Global:JCRCertHash = $certHash + # update the settings date + Set-JCRConfig -lastUpdate (Get-Date) + # update users.json + Update-JCRUsersJson + } + $false { + # set global vars from local cache + $Global:JCRUsers = Get-Content -Path "$JCRScriptRoot/data/userHash.json" | ConvertFrom-Json -AsHashtable + $Global:JCRSystems = Get-Content -Path "$JCRScriptRoot/data/systemHash.json" | ConvertFrom-Json -AsHashtable + $Global:JCRAssociations = Get-Content -Path "$JCRScriptRoot/data/associationHash.json" | ConvertFrom-Json -AsHashtable + [array]$Global:JCRRadiusMembers = Get-Content -Path "$JCRScriptRoot/data/radiusMembers.json" | ConvertFrom-Json -AsHashtable + $Global:JCRCertHash = Get-Content -Path "$JCRScriptRoot/data/certHash.json" | ConvertFrom-Json -AsHashtable + # update users.json + Update-JCRUsersJson + } + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Public/Module/Update-JCRModule.ps1 b/scripts/automation/Radius/Functions/Public/Module/Update-JCRModule.ps1 new file mode 100644 index 000000000..c0c4ddc12 --- /dev/null +++ b/scripts/automation/Radius/Functions/Public/Module/Update-JCRModule.ps1 @@ -0,0 +1,103 @@ +function Update-JCRModule { + [CmdletBinding()] + param ( + [Parameter(HelpMessage = 'ByPasses user prompts.')][Switch]$Force, + [Parameter(HelpMessage = 'Set the PSRepository')][System.String]$Repository = 'PSGallery' + + ) + begin { + # JumpCloud Module Name + $ModuleName = 'JumpCloud.Radius' + } + process { + # get the latest module + $latestModule = Find-Module -Name $ModuleName -Repository $Repository + # get the currently installed module + $currentModule = Get-InstalledModule -Name $ModuleName + + # compare the versions + if ($latestModule.Version -gt $currentModule.Version) { + Write-Host "A new version of the $ModuleName module is available: $($latestModule.Version) (current version: $($currentModule.Version))." + Write-Host "You can update the module by running: Update-Module -Name $ModuleName" + } else { + Write-Host "You have the latest version of the $ModuleName module: $($currentModule.Version)." + } + + # prompts to update if force param is not set + if (-not $Force) { + Do { + Write-Host ('Enter ''Y'' to update the ' + $ModuleName + ' PowerShell module to the latest version or enter ''N'' to cancel:') + Write-Host (' ') -NoNewline + $UserInput = Read-Host + } + Until ($UserInput.ToUpper() -in ('Y', 'N')) + } Else { + $UserInput = 'Y' + } + + # if the user pressed "N" then exit + if ($UserInput.ToUpper() -eq 'N') { + Write-Host "Update cancelled." + continue + } else { + # else get the module config from the current module: + # TODO: get the json config with the private function not created yet + $savedJCRSettings = Get-JCRConfig -asObject + Write-Warning "copying settings from the current module to the new one, this will overwrite the current settings file." + $savedJCRSettings | Format-List + Write-Warning "userGroupID = $($savedJCRSettings.userGroup.value)" + + # now, attempt to update the module + try { + Update-Module -Name $ModuleName + + Write-Host "The $ModuleName module has been updated to the latest version: $($latestModule.Version)." + + + + + } catch { + Write-Error "Failed to update the module: $_" + } + + # Get the installed module versions + $installedModules = Get-InstalledModule -Name $moduleName -AllVersions + + # Remove the modules from the current session + Get-Module -Name:($ModuleName) -ListAvailable -All | Remove-Module -Force + + # uninstall the older versions + foreach ($module in $installedModules) { + if ($module.Version -ne $latestModule.Version) { + Uninstall-Module -Name $ModuleName -RequiredVersion $module.Version -Force + } + } + + # now validate the modules + $currentModule = Get-InstalledModule -Name $ModuleName + if ($currentModule.Version -eq $latestModule.Version) { + Write-Host "The $ModuleName module has been updated to version $($currentModule.Version)." + } else { + Write-Error "Failed to update the module to the latest version." + } + Import-module -Name $ModuleName -Force + + # update the settings file config.json + Update-JCRConfig -settings $savedJCRSettings + # re-import the settings file variable + $global:JCRConfig = Get-JCRConfig -asObject + + if (-not (Test-JCRExtensionFile)) { + Write-Host "Extensions file is not valid, updating it now..." + Set-JCRExtensionFile + } else { + Write-Host "Extensions file is valid, no need to update" + } + + } + } + end { + + } + +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 b/scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 deleted file mode 100644 index 4405a427f..000000000 --- a/scripts/automation/Radius/Functions/Public/Monitor-CertDeployment.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -# Import Global Config: -. "$JCScriptRoot/config.ps1" -Connect-JCOnline $JCAPIKEY -force - -################################################################################ -# 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') - -################################################################################ -# 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 } diff --git a/scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 new file mode 100644 index 000000000..b9ea09175 --- /dev/null +++ b/scripts/automation/Radius/Functions/Public/Start-DeployUserCerts.ps1 @@ -0,0 +1,313 @@ + +function Start-DeployUserCerts { + [CmdletBinding(DefaultParameterSetName = 'gui')] + param ( + # Type of certs to distribute, All, New or byUsername + [Parameter(HelpMessage = 'Type of cert deployment to initiate', ParameterSetName = 'cli', Mandatory)] + [ValidateSet("All", "New", "ByUsername")] + [system.String] + $type, + # username + [Parameter(HelpMessage = 'The JumpCloud username of a user to deploy a certificate', ParameterSetName = 'cli')] + [System.String] + $username, + # Force invoke commands after generation + [Parameter(HelpMessage = 'Switch to force invoke generated commands on systems', ParameterSetName = 'cli')] + [switch] + $forceInvokeCommands, + # Force invoke commands after generation + [Parameter(HelpMessage = 'Switch to force generate new commands on systems', ParameterSetName = 'cli')] + [switch] + $forceGenerateCommands + ) + + if ($Global:JCRSettings.sessionImport -eq $false) { + Get-JCRGlobalVars + $Global:JCRSettings.sessionImport = $true + } + + # Import the users.json file and convert to PSObject + $userArray = Get-UserJsonData + # identify users that need their certs still deployed + $usersWithoutLatestCert = Get-UsersThatNeedCertWork -userData $userArray + # set the script root variable to use for parallel functions: + $JCRScriptRoot = $Global:JCRScriptRoot + + do { + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-DistributionMenu -CertObjectArray $userArray.certInfo -usersThatNeedCert $usersWithoutLatestCert.count -totalUserCount $userArray.count + $confirmation = Read-Host "Please make a selection" + + # This can be updated later if necessary but for now if using the GUI, the $forceGenerateCommands switch will always be false + # Thus the GUI will never overwrite commands unless the SHA1 value does not match the local cert SHA1 + switch ($forceGenerateCommands) { + $true { + $generateCommands = $true + } + $false { + $generateCommands = $false + } + } + } + '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 + } + } + switch ($forceGenerateCommands) { + $true { + $generateCommands = $true + } + $false { + $generateCommands = $false + } + } + } + } + + switch ($confirmation) { + '1' { + # case for all users + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $invokeCommands = Get-ResponsePrompt -message "Would you like to invoke commands after they've been generated?" + if (($invokeCommands -ne $true) -And ($invokeCommands -ne $false)) { + return + } + } + } + + # set thread safe variables: + $resultArray = [System.Collections.Concurrent.ConcurrentBag[object]]::new() + $workDoneArray = [System.Collections.Concurrent.ConcurrentBag[object]]::new() + + $userArray | Foreach-Object -ThrottleLimit 20 -Parallel { + # set the required variables + $JCAPIKEY = $using:JCAPIKEY + $JCORGID = $using:JCORGID + $JCRScriptRoot = $using:JCRScriptRoot + + # Import the private functions + $Private = @( Get-ChildItem -Path "$JCRScriptRoot/Functions/Private/*.ps1" -Recurse ) + foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + + # set the required global variables + $global:JCRConfig = @{ + 'radiusDirectory' = @{ + value = $using:JCRConfig.radiusDirectory.value + } + 'certType' = @{ + value = $using:JCRConfig.certType.value + } + 'openSSLBinary' = @{ + value = $using:JCRConfig.openSSLBinary.value + } + } + $global:JCRSettings = @{ + userAgent = $using:JCRSettings.userAgent + } + $Global:JCRUsers = $using:JCRUsers + $Global:JCRSystems = $using:JCRSystems + $Global:JCRAssociations = $using:JCRAssociations + $Global:JCRRadiusMembers = $using:JCRRadiusMembers + $Global:JCRCertHash = $using:JCRCertHash + + # set the thread safe variables + $resultArray = $using:resultArray + $workDoneArray = $using:workDoneArray + + # deploy user certs: + $result, $workDone = Deploy-UserCertificate -userObject $_ -forceInvokeCommands $using:invokeCommands -forceGenerateCommands $using:generateCommands + # keep track of results & work done + $resultArray.Add($result) + $WorkDoneArray.Add($workDone) + } + + # update the userTable: + foreach ($item in $workDoneArray) { + Set-UserTable -index $item.userIndex -commandAssociationsObject $item.commandAssociationsObject -certInfoObject $item.certInfoObject + } + + # print the progress: + $resultCount = $resultArray.Count + $resultItemCount = 1 + foreach ($item in $resultArray) { + Show-RadiusProgress -completedItems ($resultItemCount) -totalItems $resultArray.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $item + $resultItemCount++ + } + + # return after an action if cli, else stay in function + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-StatusMessage -Message "Finished Distributing Certificates" + } + '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) + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $invokeCommands = Get-ResponsePrompt -message "Would you like to invoke commands after they've been generated?" + if (($invokeCommands -ne $true) -And ($invokeCommands -ne $false)) { + return + } + } + } + if (-Not $usersWithoutLatestCert) { + $usersWithoutLatestCert = Get-UsersThatNeedCertWork -userData $userArray + } + + # set thread safe variables: + $resultArray = [System.Collections.Concurrent.ConcurrentBag[object]]::new() + $workDoneArray = [System.Collections.Concurrent.ConcurrentBag[object]]::new() + # foreach user: + $usersWithoutLatestCert | Foreach-Object -ThrottleLimit 20 -Parallel { + # set the required variables + $JCAPIKEY = $using:JCAPIKEY + $JCORGID = $using:JCORGID + $JCRScriptRoot = $using:JCRScriptRoot + + # set the required global variables + $global:JCRConfig = @{ + 'radiusDirectory' = @{ + value = $using:JCRConfig.radiusDirectory.value + } + 'certType' = @{ + value = $using:JCRConfig.certType.value + } + 'openSSLBinary' = @{ + value = $using:JCRConfig.openSSLBinary.value + } + } + $global:JCRSettings = @{ + userAgent = $using:JCRSettings.userAgent + } + $Global:JCRUsers = $using:JCRUsers + $Global:JCRSystems = $using:JCRSystems + $Global:JCRAssociations = $using:JCRAssociations + $Global:JCRRadiusMembers = $using:JCRRadiusMembers + $Global:JCRCertHash = $using:JCRCertHash + + # set the thread safe variables + $resultArray = $using:resultArray + $workDoneArray = $using:workDoneArray + + # Import the private functions + $Private = @( Get-ChildItem -Path "$JCRScriptRoot/Functions/Private/*.ps1" -Recurse ) + foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + + # deploy user certs: + $result, $workDone = Deploy-UserCertificate -userObject $_ -forceInvokeCommands $using:invokeCommands -forceGenerateCommands $using:generateCommands + # keep track of results & work done + $resultArray.Add($result) + $WorkDoneArray.Add($workDone) + } + + # update the userTable: + foreach ($item in $workDoneArray) { + Set-UserTable -index $item.userIndex -commandAssociationsObject $item.commandAssociationsObject -certInfoObject $item.certInfoObject + } + + # print the progress: + $resultCount = $resultArray.Count + $resultItemCount = 1 + foreach ($item in $resultArray) { + Show-RadiusProgress -completedItems ($resultItemCount) -totalItems $resultArray.Count -ActionText "Distributing Radius Certificates" -previousOperationResult $item + $resultItemCount++ + } + # 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 { + 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 + } + } + if ($confirmUser) { + # Get the userobject + index from users.json + $userObject, $userIndex = Get-UserFromTable -userID $confirmUser.id + # Add user to a list for processing + $UserSelectionArray = $userArray[$userIndex] + # write-warning "Selected User: $($UserSelectionArray.username) with userID: $($UserSelectionArray.userID)" + # Process existing commands/ Generate new commands/ Deploy new Certificate + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $result, $workDone = Deploy-UserCertificate -userObject $UserSelectionArray -prompt + } + 'cli' { + $result, $workDone = Deploy-UserCertificate -userObject $UserSelectionArray -forceInvokeCommands $invokeCommands -forceGenerateCommands $generateCommands + } + } + # update user json + Set-UserTable -index $workDone.userIndex -commandAssociationsObject $workDone.commandAssociationsObject -certInfoObject $workDone.certInfoObject + + # show progress + Show-RadiusProgress -completedItems $UserSelectionArray.count -totalItems $UserSelectionArray.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 + } + } + } + } + } while ($confirmation -ne 'E') +} \ No newline at end of file diff --git a/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 b/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 new file mode 100644 index 000000000..f9817b6d6 --- /dev/null +++ b/scripts/automation/Radius/Functions/Public/Start-GenerateRootCert.ps1 @@ -0,0 +1,350 @@ +Function Start-GenerateRootCert { + [CmdletBinding(DefaultParameterSetName = 'gui')] + param ( + # Cert Key Password + [Parameter(HelpMessage = 'The root certificate key password', ParameterSetName = 'cli')] + [string] + $certKeyPassword, + # Parameter to "New" or "Replace" the root certificate validateSet ('1', '2', '3', 'E') + [Parameter(HelpMessage = 'Select an option to generate or replace the root certificate', ParameterSetName = 'cli')] + [ValidateSet('New', 'Replace', 'Renew')] + [string] $generateType = 'New', + # Force invoke commands after generation + [Parameter(HelpMessage = 'When specified, this parameter will replace certificates if they already exist on the current filesystem', ParameterSetName = 'cli')] + [switch] + $force + + + ) + + if ($Global:JCRSettings.sessionImport -eq $false) { + Get-JCRGlobalVars + $Global:JCRSettings.sessionImport = $true + } + + + # this script will generate a Self Signed CA (root cert) to be imported on the + # Radius CBA-BYO Authentication UI + + ################################################################################ + # Do Not Edit Below: + ################################################################################ + # Set-Location $JCRScriptRoot + + # 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 "$($global:JCRConfig.radiusDirectory.value)/Cert") { + Write-Host "Cert Path Exists" + } else { + Write-Host "Creating Cert Path" + New-Item -ItemType Directory -Path "$($global:JCRConfig.radiusDirectory.value)/Cert" + } + + # If cert backup folder does not exist, create it + if (Test-Path -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/Backups") { + Write-Host "Backup Path Exists" + } else { + Write-Host "Creating Backup Path" + New-Item -ItemType Directory -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/Backups" + } + + $CertPath = Resolve-Path "$($global:JCRConfig.radiusDirectory.value)/Cert" + $outKey = "$CertPath/radius_ca_key.pem" + $outCA = "$CertPath/radius_ca_cert.pem" + $timestamp = Get-Date -Format "yyyyMMddHHmmss" + + # If parameterSetName is CLI + if ($PSCmdlet.ParameterSetName -eq 'cli') { + switch ($GenerateType) { + 'New' { + $selection = 1 + } + 'Replace' { + # Same functionality as 'New' + $selection = 1 + } + 'Renew' { + $selection = 2 + } + } + } else { + Show-RootCAGenerationMenu + $selection = Read-Host "Please make a selection" + } + + + + switch ($selection) { + # Generate new root certificate + '1' { + if (Test-Path -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem") { + Write-Host "Root Cert already exists" + # If the force switch is set, force the generation of a new root certificate + if (!$force) { + if ($PSCmdlet.ParameterSetName -eq 'cli') { + $overwritePrompt = Get-ResponsePrompt -message "Do you want to overwrite the existing CA Cert? This will generate a new root CA with a new serial number and user certs generated with the previous CA will no longer authenticate." -cli $true + } else { + $overwritePrompt = Get-ResponsePrompt -message "Do you want to overwrite the existing CA Cert? This will generate a new root CA with a new serial number and user certs generated with the previous CA will no longer authenticate." + } + } else { + $overwritePrompt = $true + } + + switch ($overwritePrompt) { + $true { + Write-Host "Overwriting Root Cert..." -ForegroundColor Yellow + + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $env:certKeyPassword = "" + # Loop until the passwords match + do { + # Prompt for password + Write-Host "NOTE: Please save your root certificate password in a password manager" -foregroundcolor Yellow + $secureCertKeyPass = Read-Host -Prompt "Enter a password for the certificate key" -AsSecureString + + # Reprompt for password + $secureCertKeyPass2ReEntry = Read-Host -Prompt "Re-enter the password for the certificate key" -AsSecureString + + # Convert SecureString to plain text to validate + $plainCertKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText + $plainCertKeyPassReEntry = ConvertFrom-SecureString $secureCertKeyPass2ReEntry -AsPlainText + + # Validate that the passwords match + if ($plainCertKeyPass -ne $plainCertKeyPassReEntry) { + Write-Host "Passwords do not match. Please try again." -foregroundcolor Red + } else { + Write-Host "Password set successfully" -foregroundcolor Green + $certKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText + } + } while ($plainCertKeyPass -ne $plainCertKeyPassReEntry) + } + 'cli' { + $certKeyPass = $certKeyPassword + } + } + + # Copy the current root cert to the backups folder and zip it + try { + Copy-Item -Path "$CertPath/radius_ca_cert.pem" -Destination "$CertPath/Backups/radius_ca_cert_$timestamp.pem" + Copy-Item -Path "$CertPath/radius_ca_key.pem" -Destination "$CertPath/Backups/radius_ca_key_$timestamp.pem" + + # Zip the root cert and key files + $zipPath = "$CertPath/Backups/newOverwrite_radius_ca_cert_backup_$timestamp.zip" + Compress-Archive -Path "$CertPath/Backups/radius_ca_cert_$timestamp.pem", "$CertPath/Backups/radius_ca_key_$timestamp.pem" -DestinationPath $zipPath + + Remove-Item -Path "$CertPath/Backups/radius_ca_cert_$timestamp.pem" + Remove-Item -Path "$CertPath/Backups/radius_ca_key_$timestamp.pem" + + } catch { + Write-Error "Error backing up the current root cert and key. $($_.Exception.Message)" + exit + } + # Save the pass phrase in the env: + $env:certKeyPassword = $certKeyPass + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) req -x509 -newkey rsa:2048 -days $($global:JCRConfig.caCertValidityDays.value) -keyout `"$outKey`" -out `"$outCA`" -passout pass:$($env:certKeyPassword) -subj /C=$($($global:JCRConfig.certSubjectHeader.Value.CountryCode))/ST=$($($global:JCRConfig.certSubjectHeader.Value.StateCode))/L=$($($global:JCRConfig.certSubjectHeader.Value.Locality))/O=$($($global:JCRConfig.certSubjectHeader.Value.Organization))/OU=$($($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit))/CN=$($($global:JCRConfig.certSubjectHeader.Value.CommonName))" + # REM PEM pass phrase: myorgpass + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -in `"$outCA`" -noout -text" + # openssl x509 -in ca-cert.pem -noout -text + # Update Extensions Distinguished Names: + if (-not (Test-JCRExtensionFile)) { + Write-Host "Extensions file is not valid, updating it now..." + Set-JCRExtensionFile + } else { + Write-Host "Extensions file is valid, no need to update" + } + } + $false { + return + } + 'exit' { + return + } + } + + } else { + # Generate new root certificate + Write-Host "Generating new CA Cert..." + + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $env:certKeyPassword = "" + # Loop until the passwords match + do { + # Prompt for password + Write-Host "NOTE: Please save your root certificate password in a password manager" -foregroundcolor Yellow + $secureCertKeyPass = Read-Host -Prompt "Enter a password for the certificate key" -AsSecureString + + # Reprompt for password + $secureCertKeyPass2ReEntry = Read-Host -Prompt "Re-enter the password for the certificate key" -AsSecureString + + # Convert SecureString to plain text to validate + $plainCertKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText + $plainCertKeyPassReEntry = ConvertFrom-SecureString $secureCertKeyPass2ReEntry -AsPlainText + + # Validate that the passwords match + if ($plainCertKeyPass -ne $plainCertKeyPassReEntry) { + Write-Host "Passwords do not match. Please try again." -foregroundcolor Red + } else { + Write-Host "Password set successfully" -foregroundcolor Green + $certKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText + } + } while ($plainCertKeyPass -ne $plainCertKeyPassReEntry) + } + 'cli' { + $certKeyPass = $certKeyPassword + } + } + # Save the pass phrase in the env: + $env:certKeyPassword = $certKeyPass + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) req -x509 -newkey rsa:2048 -days $($global:JCRConfig.caCertValidityDays.value) -keyout `"$outKey`" -out `"$outCA`" -passout pass:$($env:certKeyPassword) -subj /C=$($($global:JCRConfig.certSubjectHeader.Value.CountryCode))/ST=$($($global:JCRConfig.certSubjectHeader.Value.StateCode))/L=$($($global:JCRConfig.certSubjectHeader.Value.Locality))/O=$($($global:JCRConfig.certSubjectHeader.Value.Organization))/OU=$($($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit))/CN=$($($global:JCRConfig.certSubjectHeader.Value.CommonName))" + # REM PEM pass phrase: myorgpass + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -in `"$outCA`" -noout -text" + # openssl x509 -in ca-cert.pem -noout -text + # Update Extensions Distinguished Names: + if (-not (Test-JCRExtensionFile)) { + Write-Host "Extensions file is not valid, updating it now..." + Set-JCRExtensionFile + } else { + Write-Host "Extensions file is valid, no need to update" + } + + return + } + } + # Renew current root certificate + '2' { + $env:certKeyPassword = $certKeyPass + # Check if there is a current CA cert + if (Test-Path -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem") { + + if (!$force) { + if ($PSCmdlet.ParameterSetName -eq 'cli') { + $replacePrompt = Get-ResponsePrompt -message "Do you want to renew the existing CA Cert? renewing the root CA will contain the same serial number and CA subject headers. User certs generated with the previous CA will continue to authenticate." -cli $true + } else { + $replacePrompt = Get-ResponsePrompt -message "Do you want to renew the existing CA Cert? renewing the root CA will contain the same serial number and CA subject headers. User certs generated with the previous CA will continue to authenticate." + } + } else { + $replacePrompt = $true + } + + switch ($replacePrompt) { + $true { + Write-Host "Renewing Root Cert..." -ForegroundColor Yellow + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $env:certKeyPassword = "" + # Loop until the passwords match + $secureCertKeyPass = Read-Host -Prompt "Enter the current password for the certificate key" -AsSecureString + $certKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText + do { + $result = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) rsa -in `"$outKey`" -passin pass:$($certKeyPass) -check" + if ($result -match "RSA key ok") { + Write-Host "Password validated! Proceeding..." + $env:certKeyPassword = $certKeyPass + $passwordValid = $true + } else { + Write-Host "Incorrect private key password!" -ForegroundColor Red + $passwordValid = $false # Continue loop if password is incorrect + $secureCertKeyPass = Read-Host "Please enter a valid password for the current private key" -AsSecureString + $certKeyPass = ConvertFrom-SecureString $secureCertKeyPass -AsPlainText + } + } while (-not $passwordValid) # Continue looping until the password is correct + } + 'cli' { + $certKeyPass = $certKeyPassword + # Validate that the password works with the old CA cert + # Attempt to read the private key with the password + do { + $result = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) rsa -in `"$outKey`" -passin pass:$($certKeyPass) -check" + + if ($result -match "RSA key ok") { + Write-Host "Password validated! Proceeding..." + $env:certKeyPassword = $certKeyPass + $passwordValid = $true # Exit condition for the loop + } else { + Write-Host "Incorrect password" -ForegroundColor Red + $passwordValid = $false # Continue loop if password is incorrect + $certKeyPass = Read-Host "Please enter a valid password for the current private key" -AsSecureString + $certKeyPass = ConvertFrom-SecureString $certKeyPass -AsPlainText + } + } while (-not $passwordValid) # Continue looping until the password is correct + } + } + + # Copy the current root cert to the backups folder and zip it + try { + Copy-Item -Path "$CertPath/radius_ca_cert.pem" -Destination "$CertPath/Backups/radius_ca_cert_$timestamp.pem" + Copy-Item -Path "$CertPath/radius_ca_key.pem" -Destination "$CertPath/Backups/radius_ca_key_$timestamp.pem" + + # Zip the root cert and key files + $zipPath = "$CertPath/Backups/renew_radius_ca_cert_backup_$timestamp.zip" + Compress-Archive -Path "$CertPath/Backups/radius_ca_cert_$timestamp.pem", "$CertPath/Backups/radius_ca_key_$timestamp.pem" -DestinationPath $zipPath + + Remove-Item -Path "$CertPath/Backups/radius_ca_cert_$timestamp.pem" + Remove-Item -Path "$CertPath/Backups/radius_ca_key_$timestamp.pem" + + } catch { + Write-Error "Error backing up the current root cert and key. $($_.Exception.Message)" + exit + } + $certConfPath = "$CertPath/radius_ca_cert2.conf" + $csrOutPath = "$CertPath/radius_ca_cert.csr" + $select = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -in `"$($outCA)`" -serial -noout" + $serial = $select -replace "serial=", "" + + # Validate that the password works with the old CA cert + # Attempt to read the private key with the password + + try { + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -x509toreq -in $($outCA) -signkey $($outKey) -out $csrOutPath -passin pass:$($env:certKeyPassword)" + + } catch { + # Exit + Write-Error "Error creating CSR file $($_.Exception.Message)" + exit + } + + $string = @" +[ v3_ca ] +basicConstraints= CA:TRUE +subjectKeyIdentifier= hash +authorityKeyIdentifier= keyid:always,issuer:always +"@ | Out-File "$certConfPath" + + try { + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) req -x509 -newkey rsa:2048 -days $($global:JCRConfig.caCertValidityDays.value) -keyout `"$outKey`" -out `"$outCA`" -passout pass:$($env:certKeyPassword) -subj /C=$($($global:JCRConfig.certSubjectHeader.Value.CountryCode))/ST=$($($global:JCRConfig.certSubjectHeader.Value.StateCode))/L=$($($global:JCRConfig.certSubjectHeader.Value.Locality))/O=$($($global:JCRConfig.certSubjectHeader.Value.Organization))/OU=$($($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit))/CN=$($($global:JCRConfig.certSubjectHeader.Value.CommonName))" + + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -req -days $($global:JCRConfig.caCertValidityDays.value) -in $csrOutPath -set_serial `"0x$serial`" -signkey `"$outKey`" -out `"$outCA`" -extfile $certConfPath -extensions v3_ca -passin pass:$($env:certKeyPassword)" + + # Message of success + Write-Host "Root Certificate Renewed Successfully!" -ForegroundColor Yellow + # Cleanup + Remove-Item -Path $certConfPath + Remove-Item -Path $csrOutPath + } catch { + # Error replacing the certificate + Write-Error "Error replacing the certificate. $($_.Exception.Message)" + } + Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -in `"$outCA`" -enddate -serial -noout " + } + $false { + return + } + 'exit' { + return + } + } + } else { + Write-Host "No Root Cert detected. Please generate a new CA Cert. Returning to main menu..." -ForegroundColor Yellow + return + } + } + # Return to main menu + 'E' { + return + } + } +} + + diff --git a/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 new file mode 100644 index 000000000..9c8a5c7ea --- /dev/null +++ b/scripts/automation/Radius/Functions/Public/Start-GenerateUserCerts.ps1 @@ -0,0 +1,238 @@ +# todo: rename to be PS-ApprovedVerb "New-UserCert" +function Start-GenerateUserCerts { + [CmdletBinding(DefaultParameterSetName = 'gui')] + param ( + # Type of certs to distribute, All, New or byUsername + [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(HelpMessage = 'The JumpCloud username of an individual user', ParameterSetName = 'cli')] + [System.String] + $username, + # Force invoke commands after generation + [Parameter(HelpMessage = 'When specified, this parameter will replace certificates if they already exist on the current filesystem', ParameterSetName = 'cli')] + [switch] + $forceReplaceCerts + ) + + if ($Global:JCRSettings.sessionImport -eq $false) { + Get-JCRGlobalVars + $Global:JCRSettings.sessionImport = $true + } + Write-Host "[status] Starting User Certificate Generation" + Write-Host "[status] Using Radius Directory: $($global:JCRConfig.radiusDirectory.value)" + #### 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 "$($global:JCRConfig.radiusDirectory.value)/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 { + # Get CA-Key password + Write-Host "CA-Key password not found in the ENV" + Get-CertKeyPass + } + + # get userArray or initialize + $userArray = Get-UserJsonData + + # Create UserCerts dir + if (Test-Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts") { + Write-Host "[status] User Cert Directory Exists" + } else { + Write-Host "[status] Creating User Cert Directory" + New-Item -ItemType Directory -Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts" + Write-Host "[status] User Cert Directory Created at: $($global:JCRConfig.radiusDirectory.value)/UserCerts" + if (Test-Path ("$($global:JCRConfig.radiusDirectory.value)/UserCerts")) { + Write-Host "[status] User Cert Directory Created Successfully" + } else { + Write-Warning "[error] User Cert Directory could not be created at: $($global:JCRConfig.radiusDirectory.value)/UserCerts" + } + } + #### end function setup + + Do { + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-GenerationMenu + $confirmation = Read-Host "Please make a selection" + } + '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 { + $replaceCerts = $true + } + $false { + $replaceCerts = $false + } + } + } + } + + 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 $($global:JCRConfig.certType.value) + 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 + } + } + } + '2' { + 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 + } + } + if ($confirmUser) { + # Get the userobject + index from users.json + $userObject, $userIndex = Get-UserFromTable -userID $confirmUser.id + switch ($forceReplaceCerts) { + $true { + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $($global:JCRConfig.certType.value) -forceReplaceCert + } + 'cli' { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $($global:JCRConfig.certType.value) -forceReplaceCert + } + } + } + $false { + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $($global:JCRConfig.certType.value) -Prompt + } + 'cli' { + $result = Invoke-UserCertProcess -radiusMember $userObject -certType $($global:JCRConfig.certType.value) + } + } + } + } + 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 + } + } + } + + '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 + } + 'exit' { + return + } + } + } + } + # Get each RadiusMember User: + for ($i = 0; $i -lt $JCRRadiusMembers.count; $i++) { + $result = Invoke-UserCertProcess -radiusMember $JCRRadiusMembers[$i] -certType $($global:JCRConfig.certType.value) -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 + # recalculate expiring certs: + $userCertInfo = Get-CertInfo -UserCerts + # Get expiring certs: + $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $global:JCRConfig.certExpirationWarningDays.value + 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 $($global:JCRConfig.certType.value) -forceReplaceCert + Show-RadiusProgress -completedItems ($i + 1) -totalItems $ExpiringCerts.count -ActionText "Generating Radius Certificates" -previousOperationResult $result + + # recalculate expiring certs: + $userCertInfo = Get-CertInfo -UserCerts + } + $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $global:JCRConfig.certExpirationWarningDays.value + switch ($PSCmdlet.ParameterSetName) { + 'gui' { + Show-StatusMessage -Message "Finished Generating Certificates" + } + 'cli' { + return + } + } + } + 'E' { + Write-Host "Returning to main menu" + } + default { + Write-Host "Invalid Choice. Please try again" + } + } + + } while ($confirmation -ne 'E') +} + + + diff --git a/scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 b/scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 new file mode 100644 index 000000000..fe8ed67e9 --- /dev/null +++ b/scripts/automation/Radius/Functions/Public/Start-MonitorCertDeployment.ps1 @@ -0,0 +1,36 @@ +Function Start-MonitorCertDeployment { + + + ################################################################################ + # Do not modify below + ################################################################################ + # Import the functions + # Import-Module "$JCRScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force + # Define jsonData file + $jsonFile = "$JCRScriptRoot/users.json" + + + + # Show user selection + do { + Show-CertDeploymentMenu + $option = Read-Host "Please make a selection" + switch ($option) { + '1' { + $data = Get-UserJsonData + $certResults = Get-InstalledCertsFromUsersJson -userData $data + $certResults | Format-Table + pause + } '2' { + Get-CommandObjectTable -Detailed -jsonFile $jsonFile + Pause + } '3' { + Get-CommandObjectTable -Failed -jsonFile $jsonFile + Pause + } '4' { + $retryCommands = Invoke-CommandsRetry + Pause + } + } + } until ($option.ToUpper() -eq 'E') +} diff --git a/scripts/automation/Radius/Functions/Public/Start-RadiusDeployment.ps1 b/scripts/automation/Radius/Functions/Public/Start-RadiusDeployment.ps1 new file mode 100644 index 000000000..a548b8bb2 --- /dev/null +++ b/scripts/automation/Radius/Functions/Public/Start-RadiusDeployment.ps1 @@ -0,0 +1,41 @@ +function Start-RadiusDeployment { + # Import Global Config: + Write-Verbose 'Verifying JCAPI Key' + if ($JCAPIKEY.length -ne 40) { + Connect-JCOnline -force + } + + # validate the setting from the module have been set + Confirm-JCRConfig -ErrorAction stop + + if ($Global:JCRSettings.sessionImport -eq $false) { + Get-JCRGlobalVars + $Global:JCRSettings.sessionImport = $true + } + + # Show user selection + do { + #Output-Certs + Show-RadiusMainMenu + $selection = Read-Host "Please make a selection" + switch ($selection) { + '1' { + Start-GenerateRootCert + } '2' { + Start-GenerateUserCerts + } '3' { + Start-DeployUserCerts + } '4' { + Start-MonitorCertDeployment + } '5' { + Get-JCRGlobalVars -force + } '8' { + Get-JCRGlobalVars -force -associateManually + } '9' { + $theUser = Read-Host "Enter the username of the user to manually update their association data" + Get-JCRGlobalVars -force -associationUsername $theUser + } + } + Pause + } until ($selection.ToUpper() -eq 'Q') +} \ No newline at end of file diff --git a/scripts/automation/Radius/JumpCloud.Radius.nuspec b/scripts/automation/Radius/JumpCloud.Radius.nuspec new file mode 100644 index 000000000..0b47f75f4 --- /dev/null +++ b/scripts/automation/Radius/JumpCloud.Radius.nuspec @@ -0,0 +1,29 @@ + + + + JumpCloud.Radius + 2.1.0 + Module for managing JumpCloud Radius user certificates. + JumpCloud Customer Tools Team + JumpCloud + + + false + (c) JumpCloud. All rights reserved. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scripts/automation/Radius/JumpCloud.Radius.psd1 b/scripts/automation/Radius/JumpCloud.Radius.psd1 new file mode 100644 index 000000000..7947ab4ea --- /dev/null +++ b/scripts/automation/Radius/JumpCloud.Radius.psd1 @@ -0,0 +1,135 @@ +# +# Module manifest for module 'JumpCloud.Radius' +# +# Generated by: JumpCloud Customer Tools Team +# +# Generated on: 7/8/2025 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'JumpCloud.Radius.psm1' + +# Version number of this module. +ModuleVersion = '2.1.0' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '71bfaf58-3326-4512-9a7f-a2d9dc19d6b5' + +# Author of this module +Author = 'JumpCloud Customer Tools Team' + +# Company or vendor of this module +CompanyName = 'JumpCloud' + +# Copyright statement for this module +Copyright = '(c) JumpCloud. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'Module for managing JumpCloud Radius user certificates.' + +# Minimum version of the PowerShell engine required by this module +PowerShellVersion = '7.0.0' + +# 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 = @('JumpCloud') + +# 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 = 'Get-JCRCertReport', 'Get-JCRGlobalVars', 'Start-DeployUserCerts', + 'Start-GenerateRootCert', 'Start-GenerateUserCerts', + 'Start-MonitorCertDeployment', 'Start-RadiusDeployment', + 'Set-JCRConfig', 'Update-JCRModule' + +# 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/JumpCloud.Radius.psm1 b/scripts/automation/Radius/JumpCloud.Radius.psm1 new file mode 100644 index 000000000..f3b098837 --- /dev/null +++ b/scripts/automation/Radius/JumpCloud.Radius.psm1 @@ -0,0 +1,153 @@ +# Load all functions from private folders +$Private = @( Get-ChildItem -Path "$PSScriptRoot/Functions/Private/*.ps1" -Recurse) +Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } +} + +# Load all public functions: +$Public = @( Get-ChildItem -Path "$PSScriptRoot/Functions/Public/*.ps1" -Recurse) +Foreach ($Import in $Public) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } +} + +# setup: +# build required users.json file: +# set script root: +$global:JCRScriptRoot = "$PSScriptRoot" + +# from the settings file we should have a location for the certs and user certs + + + +# Export module member +Export-ModuleMember -Function $Public.BaseName -Alias * + +# Set the module config +$global:JCRConfigTemplate = @{ + 'userGroup' = @{ + value = $null; + write = $true; + copy = $true; + required = $true; + placeholder = ''; + type = 'string' + } + 'certSecretPass' = @{ + value = $null; + write = $true; + copy = $true; + required = $true; + placeholder = ''; + type = 'string' + } + 'userCertValidityDays' = @{ + value = 365; + write = $true; + copy = $true; + required = $true; + placeholder = '<365>'; + type = 'int' + } + 'caCertValidityDays' = @{ + value = 1095; + write = $true; + copy = $true; + required = $true; + placeholder = '<1095>'; + type = 'int' + } + 'certExpirationWarningDays' = @{ + value = 15; + write = $true; + copy = $true; + required = $true; + placeholder = '<15>'; + type = 'int' + } + 'networkSSID' = @{ + value = $null; + write = $true; + copy = $true; + required = $true; + placeholder = ''; + type = 'string' + } + 'certType' = @{ + value = $null; + write = $true; + copy = $true; + required = $true; + placeholder = ''; + type = 'string' + } + 'radiusDirectory' = @{ + value = $null; + write = $true; + copy = $true; + required = $true; + placeholder = ''; + type = 'string' + } + 'lastUpdate' = @{ + value = $null; + write = $true; + copy = $true; + required = $false; + placeholder = $null; + type = 'string' + } + 'openSSLBinary' = @{ + value = $null; + write = $true; + copy = $true; + required = $true; + placeholder = ''; + type = 'string' + } + 'certSubjectHeader' = @{ + value = @{ + CountryCode = $null + StateCode = $null + Locality = $null + Organization = $null + OrganizationUnit = $null + CommonName = $null + } + write = $true + copy = $true + required = $true + placeholder = '@{ + CountryCode = "" + StateCode = "" + Locality = "" + Organization = "" + OrganizationUnit = "" + CommonName = "" + }'; + type = 'hashtable' + } +} + +# # Set the module non-configurable settings +$global:JCRSettings = @{ + 'userAgent' = Get-JCRUserAgent; + 'sessionImport' = $false; +} + +# From the saved config file, get the settings and set them in the module as $config +$global:JCRConfig = Get-JCRConfig -asObject +# Get-JCRConfig + +# validate the config settings (skip throw on first load with 'loadModule' param) +Confirm-JCRConfig -loadModule + +# TODO: Check the OpenSSL version +# Get-OpenSSLVersion -opensslBinary $global:JCRConfig.openSSLBinary.value \ No newline at end of file diff --git a/scripts/automation/Radius/LICENSE b/scripts/automation/Radius/LICENSE new file mode 100644 index 000000000..186fa3bcb --- /dev/null +++ b/scripts/automation/Radius/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 JumpCloud, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/scripts/automation/Radius/Start-RadiusDeployment.ps1 b/scripts/automation/Radius/Start-RadiusDeployment.ps1 deleted file mode 100644 index 28aa5d585..000000000 --- a/scripts/automation/Radius/Start-RadiusDeployment.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -# Import Global Config: -. "$psscriptroot/config.ps1" -Connect-JCOnline $JCAPIKEY -force - -################################################################################ -# Do not modify below -################################################################################ -# set script root -$global:JCScriptRoot = $PSScriptRoot - -# Import the functions -Import-Module "$JCScriptRoot/Functions/JCRadiusCertDeployment.psm1" -DisableNameChecking -Force - -# Show user selection -do { - #Output-Certs - Show-RadiusMainMenu - $selection = Read-Host "Please make a selection" - switch ($selection) { - '1' { - . "$JCScriptRoot/Functions/Public/Generate-RootCert.ps1" - } '2' { - . "$JCScriptRoot/Functions/Public/Generate-UserCerts.ps1" - } '3' { - . "$JCScriptRoot/Functions/Public/Distribute-UserCerts.ps1" - } '4' { - . "$JCScriptRoot/Functions/Public/Monitor-CertDeployment.ps1" - } - } - Pause -} until ($selection.ToUpper() -eq 'Q') \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/HelperFunctions.ps1 b/scripts/automation/Radius/Tests/HelperFunctions.ps1 new file mode 100644 index 000000000..0e7eee85a --- /dev/null +++ b/scripts/automation/Radius/Tests/HelperFunctions.ps1 @@ -0,0 +1,53 @@ +Function New-RandomUser () { + [CmdletBinding(DefaultParameterSetName = 'NoAttributes')] + param + ( + [Parameter(Mandatory, Position = 0)] + [String] + $Domain, + + [Parameter(ParameterSetName = 'Attributes')] ##Test this to see if this can be modified. + [switch] + $Attributes + + ) + + if (($PSCmdlet.ParameterSetName -eq 'NoAttributes')) { + $username = -join ((65..90) + (97..122) | Get-Random -Count 8 | ForEach-Object { [char]$_ }) + $email = $username + "@$Domain.com" + + $RandomUser = [ordered]@{ + FirstName = 'Pester' + LastName = 'Test' + Username = $username + Email = $email + Password = 'Temp123!' + } + + $NewRandomUser = New-Object PSObject -Property $RandomUser + } + + if (($PSCmdlet.ParameterSetName -eq 'Attributes')) { + $username = -join ((65..90) + (97..122) | Get-Random -Count 8 | ForEach-Object { [char]$_ }) + $email = $username + "@$Domain.com" + + $RandomUser = [ordered]@{ + FirstName = 'Pester' + LastName = 'Test' + Username = $username + Email = $email + Password = 'Temp123!' + NumberOfCustomAttributes = 3 + Attribute1_name = 'Department' + Attribute1_value = 'Sales' + Attribute2_name = 'Office' + Attribute2_value = '456789' + Attribute3_name = 'Lang' + Attribute3_value = 'French' + } + $NewRandomUser = New-Object PSObject -Property $RandomUser + } + + + return $NewRandomUser +} \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/Invoke-Pester.ps1 b/scripts/automation/Radius/Tests/Invoke-Pester.ps1 new file mode 100644 index 000000000..b94c70e53 --- /dev/null +++ b/scripts/automation/Radius/Tests/Invoke-Pester.ps1 @@ -0,0 +1,110 @@ +# 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 = 1)][System.String[]]$ExcludeTagList + , [Parameter(ParameterSetName = 'SingleOrgTests', Mandatory = $false, ValueFromPipelineByPropertyName = $true, Position = 2)][System.String[]]$IncludeTagList + , [Parameter(ParameterSetName = 'ModuleValidation', Mandatory = $true, ValueFromPipelineByPropertyName = $true, Position = 3)][switch]$ModuleValidation +) + + + +# 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 +} + +# Load private functions +Write-Host ('[status]Load private functions: ' + "$PSScriptRoot/../Functions/Private/*.ps1") +Import-Module -Name "$PSScriptRoot/../JumpCloud.Radius.psd1" -Force +Write-Host ('[status]Load public functions: ' + "$PSScriptRoot/../Functions/Public/*.ps1") +Get-ChildItem -Path:("$PSScriptRoot/../Functions/Private/*.ps1") -Recurse | ForEach-Object { . $_.FullName } + +# Determine the parameter set path +if ($PSCmdlet.ParameterSetName -eq 'ModuleValidation') { + $IncludeTags = "ModuleValidation" + $PesterRunPaths = @( + "$PSScriptRoot/ModuleValidation/" + ) +} else { + $env:JCAPIKEY = $JumpCloudApiKey + Connect-JCOnline -JumpCloudApiKey:($env:JCAPIKEY) -force + Write-Host "Begin Org Setup Before Tests:" + . "$PSScriptRoot/SetupRadiusOrg.ps1" +} + +if (-Not $PesterRunPaths) { + $PesterRunPaths = @( + "$PSScriptRoot" + ) +} + + + +# 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 "-----------------------" +Write-Host "[Status] JCRConfig Settings:" +foreach ($setting in $global:JCRConfig.PSObject.Properties) { + Write-Host ("$($setting.Name): $($setting.Value.value)") +} +Write-Host "-----------------------" + +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/ModuleValidation/ModuleVersion.Tests.ps1 b/scripts/automation/Radius/Tests/ModuleValidation/ModuleVersion.Tests.ps1 new file mode 100644 index 000000000..854ee1e29 --- /dev/null +++ b/scripts/automation/Radius/Tests/ModuleValidation/ModuleVersion.Tests.ps1 @@ -0,0 +1,55 @@ +Describe "Module Version Tests" -Tag "ModuleValidation" { + BeforeEach { + $psd1Path = "$PSScriptRoot/../../JumpCloud.Radius.psd1" + $configPath = "$PSScriptRoot/../../Config.json" + # remove the config file if it exists + if (Test-Path -Path $configPath) { + Remove-Item -Path $configPath -Force + } + } + + It "The userAgent should be set in the module settings" { + $PSD1 = Test-ModuleManifest -Path $psd1Path + Import-Module $psd1Path -Force + # the user agent should be set to the module version + $global:JCRSettings.'userAgent' | Should -Match "$($PSD1.version)" + # the string should be in the format of JumpCloud_ModuleName.ModuleVersion + $userAgentRegex = 'JumpCloud_PasswordlessRadiusConfig.PowerShellModule/[0-9]+.[0-9]+.[0-9]+' + $global:JCRSettings.'userAgent' | Should -Match $userAgentRegex + } + + Context "Module Config Tests" { + It "when the config file does not exist, importing the module should only write warning messages" { + # Import the module + { Import-Module $psd1Path -Force } | Should -Not -Throw + # Check that the config file does not exist + # Test-Path -Path $configPath | Should -Be $false + # Check that the module config is set to the default values + $global:JCRConfig.userGroup.value | Should -Be $null + } + It "Setting the settings to some series of values should not throw an error" { + # First create a new radiusDirectory + $radiusDirectory = Join-Path -Path $HOME -ChildPath "RADIUS" + if (-Not (Test-Path -Path $radiusDirectory)) { + New-Item -ItemType Directory -Path $radiusDirectory | Out-Null + } + $settings = @{ + certType = "UsernameCn" + radiusDirectory = "~/RADIUS" + certSecretPass = "secret1234!" + networkSSID = "TP-Link_SSID" + userGroup = "5f3171a9232e1113939dd6a2" + openSSLBinary = 'openssl' + certSubjectHeader = @{ + CountryCode = "US" + StateCode = "CO" + Locality = "Boulder" + Organization = "JumpCloud" + OrganizationUnit = "Customer_Tools" + CommonName = "JumpCloud.com" + } + } + Set-JCRConfig @settings + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/Private/Config/Confirm-JCRConfig.Tests.ps1 b/scripts/automation/Radius/Tests/Private/Config/Confirm-JCRConfig.Tests.ps1 new file mode 100644 index 000000000..d2f61a82e --- /dev/null +++ b/scripts/automation/Radius/Tests/Private/Config/Confirm-JCRConfig.Tests.ps1 @@ -0,0 +1,140 @@ +Describe 'Confirm-JCRConfig Tests' -Tag "Acceptance" { + BeforeAll { + # Load all functions from private folders + if (-not (test-path -path $JCRScriptRoot -errorAction silentlyContinue)) { + write-host "JCRScriptRoot not set, setting it to the parent directory of the script root" + + # until we've found the correct parent path traversing up the directory tree + do { + $JCRScriptRoot = Split-Path -Path $PSScriptRoot -Parent + # check if the JumpCloud.Radius.psd1 file exists in the parent directory + if (Test-Path -Path "$JCRScriptRoot/JumpCloud.Radius.psd1") { + break + } + # if not, traverse up one more level + $PSScriptRoot = $JCRScriptRoot + } while (-not (Test-Path -Path "$JCRScriptRoot/JumpCloud.Radius.psd1")) + + } + $Private = @( Get-ChildItem -Path "$JCRScriptRoot/Functions/Private/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + # Import the module to set the global variable + Import-Module -Name "$JCRScriptRoot/JumpCloud.Radius.psd1" -Force + + # get the current config.json contents + $configFilePath = Join-Path -Path $JCRScriptRoot -ChildPath 'Config.json' + $configBefore = Get-Content -Path $configFilePath + } + + It "should confirm the config file exists" { + # Check if the config file exists + $configFilePath = Join-Path -Path $JCRScriptRoot -ChildPath 'Config.json' + Test-Path -Path $configFilePath | Should -Be $true + } + It "should confirm the config file is valid JSON" { + # Check if the config file is valid JSON + $configFilePath = Join-Path -Path $JCRScriptRoot -ChildPath 'Config.json' + $configContent = Get-Content -Path $configFilePath -Raw + { ConvertFrom-Json -InputObject $configContent } | Should -Not -Throw + } + Context "When there is no config file" { + BeforeEach { + # Remove the config file if it exists + $configFilePath = Join-Path -Path $JCRScriptRoot -ChildPath 'Config.json' + if (Test-Path -Path $configFilePath) { + Remove-Item -Path $configFilePath -Force + } + } + + It "should create a new config file" { + # Call the function to create a new config file + New-JCRConfig + # Check if the config file exists + Test-Path -Path $configFilePath | Should -Be $true + # the config file should be valid JSON + $configContent = Get-Content -Path $configFilePath -Raw + { ConvertFrom-Json -InputObject $configContent } | Should -Not -Throw + # Check if the config file contains the expected keys + } + Context "Individually Set all of the required settings but one, Confirm-JCRConfig should still throw" { + BeforeEach { + # Create a new config file + New-JCRConfig -force + # Get all the required settings + $requiredSettings = $Global:JCRConfigTemplate.GetEnumerator() | Where-Object { $_.Value.required -eq $true } + foreach ($setting in $requiredSettings) { + # Set each required setting + if ($setting.Value.type -eq 'hashtable') { + $stringData = $setting.Value.placeholder + + # Remove the leading and trailing '@{' and '}' + $cleanStringData = $stringData -replace '^@{|}$', '' + + # Convert the string data to a hashtable + $hashTable = ConvertFrom-StringData $cleanStringData + + $param = @{ $setting.Key = $hashTable } + } else { + switch ($setting.Key) { + 'radiusDirectory' { + $param = @{ $setting.Key = "$HOME" } + } + Default { + $param = @{ $setting.Key = $setting.Value.placeholder.replace('<', '').replace('>', '') } + } + } + } + Set-JCRConfig @param + + # set one of the required settings to null + if ($setting.Key -eq 'openSSLBinary') { + $param = @{ $setting.Key = $null } + Set-JCRConfig @param + } + } + } + } + } + Context "Validation for individual setting" { + It "Should throw when a subject header contains a space" { + # Set the certSubjectHeader with a space in the CommonName + $param = @{ + certSubjectHeader = @{ + CountryCode = 'US' + StateCode = 'CA' + Locality = 'San Francisco' + Organization = 'JumpCloud' + OrganizationUnit = 'Engineering' + CommonName = 'Test User' # Contains a space + } + } + { Set-JCRConfig @param } | Should -Throw + } + It "Should throw when a string value is set for an 'int' type setting" { + # Set a string value for an int type setting + $param = @{ caCertValidityDays = 'NotAnInt' } + { Set-JCRConfig @param } | Should -Throw + } + } + AfterAll { + # Restore the original config file contents + $configFilePath = Join-Path -Path $JCRScriptRoot -ChildPath 'Config.json' + if (Test-Path -Path $configFilePath) { + Set-Content -Path $configFilePath -Value $configBefore + $Global:JCRConfig = Get-JCRConfig -asObject + } + + Write-Host "-----------------------" + Write-Host "[Status] JCRConfig Settings:" + foreach ($setting in $global:JCRConfig.PSObject.Properties) { + Write-Host ("$($setting.Name): $($setting.Value.value)") + } + Write-Host "-----------------------" + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/Public/Get-JCRCertReport.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Get-JCRCertReport.Tests.ps1 new file mode 100644 index 000000000..03f6a74f3 --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Get-JCRCertReport.Tests.ps1 @@ -0,0 +1,37 @@ +Describe 'User Cert Report' -Tag "Reports" { + BeforeAll { + # Load all functions from private folders + $Private = @( Get-ChildItem -Path "$JCRScriptRoot/Functions/Private/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + # import helper functions: + . "$PSScriptRoot/../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 + } + Get-JCRGlobalVars -Force -associateManually + Start-GenerateRootCert -certKeyPassword "TestCertificate123!@#" -generateType "new" -force + Start-GenerateUserCerts -type All -forceReplaceCerts + Start-DeployUserCerts -type All -forceInvokeCommands + } + Context "Report Generation" { + It "Generates the Report" { + # Export the report + Get-JCRCertReport -ExportFilePath "$JCRScriptRoot/testReport.csv" + $report = Import-Csv -Path "$JCRScriptRoot/testReport.csv" + $report | Should -Not -BeNullOrEmpty + } + It "Checks for invalid Path" { + # Export the report + { Get-JCRCertReport -ExportFilePath "$JCRScriptRoot/testReport" } | Should -Throw + { Get-JCRCertReport -ExportFilePath "testReport" } | Should -Throw + { Get-JCRCertReport -ExportFilePath "$JCRScriptRoot/testReport.csv" } | Should -Not -Throw + } + } +} \ No newline at end of file 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..514ca52c6 --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Get-JCRGlobalVars.Tests.ps1 @@ -0,0 +1,241 @@ +Describe "Get Global Variable Data Tests" -Tag "Cache" { + BeforeAll { + $dataPath = "$JCRScriptRoot/data/" + $requiredFiles = @( + 'associationHash.json', + 'radiusMembers.json', + 'systemHash.json', + 'userHash.json' + 'certHash.json' + ) + # explicitly import the settings file functions for these tests: + $Private = @( Get-ChildItem -Path "$JCRScriptRoot/Functions/Private/Settings/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + Start-GenerateRootCert -certKeyPassword "TestCertificate123!@#" -generateType "new" -force + } + 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))) { + Write-Host "Generating data:" + Get-JCRGlobalVars + } + + } + 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-JCRConfig + $timeSpan = New-TimeSpan -Start (Get-Date).AddHours(-24) -End $($global:JCRConfig.lastUpdate.value) + # 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-JCRConfig -lastUpdate (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-JCRConfig + $timeSpan = New-TimeSpan -Start (Get-Date).AddHours(-24) -End $($global:JCRConfig.lastUpdate.value) + # 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-JCRConfig -lastUpdate (Get-Date).AddHours(-25) + Start-Sleep 2 + $settingsData = Get-JCRConfig + $timeSpan = New-TimeSpan -Start $($global:JCRConfig.lastUpdate.value) -End (Get-Date).AddHours(-24) + Write-Host "Time between 24 hrs and settings file: $($timeSpan.TotalHours)" + } + # run Get-JCRGlobalVars + Get-JCRGlobalVars -Force -associateManually + + # 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 settings write date" { + # check the files before + $filesBefore = Get-ChildItem -Path $dataPath + # run Get-JCRGlobalVars with force param + Start-Sleep -Seconds 2 + Get-JCRGlobalVars -Force -associateManually + + # 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 + } + } + 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 + + } + } + } + } + Context "Tests that users.json is updated correctly if a cert is found on a user's computer" { + It "Validates that the deployment info for some user is updated when a cert is installed on all the user's devices" { + $userArray = Get-UserJsonData + # get the cert sha of one user with system associations: + $userFromList = $userArray | Where-Object { $_.systemAssociations -ne $Null } | select -first 1 + # force an update of that user's cert + Start-GenerateUserCerts -type ByUsername -username $($userFromList.username) -forceReplaceCerts + # Update-JCRUsersJson + # Get the user array again + $userArray = Get-UserJsonData + $userFromList = $userArray | Where-Object { $_.userId -eq $userFromList.userID } + # create a cert hash object + $certHash = @{ + $($userFromList.certInfo.sha1) = New-Object System.Collections.ArrayList + } + foreach ($systemAssociation in $userFromList.systemAssociations) { + $certHash[$userFromList.certInfo.sha1].Add( + [PSCustomObject]@{ + systemId = $systemAssociation.systemId + path = "nullNotNeededForTesting" + } + ) + } + # write the mocked certHash to the data file: + $certHash | ConvertTo-Json | Out-File "$JCRScriptRoot/data/certHash.json" + $Global:JCRCertHash = Get-Content -path "$JCRScriptRoot/data/certHash.json" | ConvertFrom-Json -AsHashtable + # update the users json file + Update-JCRUsersJson + $userArray = Get-UserJsonData + $after = $userArray | Where-Object { $_.userId -eq $userFromList.userID } + # The users.json file should be updated to show that the user has an installed certificate + foreach ($systemId in $userFromList.systemAssociations.systemId) { + $systemId | Should -BeIn $after.deploymentInfo.systemId + } + + } + + } + Context "Tests that the RadiusMembers.json file is updated when users are added or removed from the user group" { + BeforeAll { + # create a new user + $user = New-RandomUser -Domain "pesterRadius" | New-JCUser + } + It "when a user is added to the group, they should be added to the RadiusMembers.json & users.json file" { + # get the RadiusMembers.json before + # get the user.json file before + # add a user to the radius Group + Add-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $user.id + Start-Sleep 1 + # update the cache forcefully + Get-JCRGlobalVars -force -skipAssociation -associateManually + Start-Sleep 1 + # the new user should be in the membership list + $global:JCRRadiusMembers.username | Should -Contain $user.username + + # the new user should be in user.json list + $userData = Get-UserJsonData + $userData.username | Should -Contain $user.username + + + + + } + It "when a user is removed from the group, they should be removed to the RadiusMembers.json & users.json file" { + # get the RadiusMembers.json before + # get the user.json file before + # add a user to the radius Group + Remove-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $user.id + Start-Sleep 1 + # update the cache forcefully + Get-JCRGlobalVars -force -skipAssociation -associateManually + Start-Sleep 1 + # the new user should be in the membership list + $global:JCRRadiusMembers.username | Should -Not -Contain $user.username + + # the new user should be in user.json list + $userData = Get-UserJsonData + $userData.username | Should -Not -Contain $user.username + } + } +} diff --git a/scripts/automation/Radius/Tests/Public/Module/Update-JCRModule.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Module/Update-JCRModule.Tests.ps1 new file mode 100644 index 000000000..0eab9a19d --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Module/Update-JCRModule.Tests.ps1 @@ -0,0 +1,295 @@ +Describe 'Module Update' -Tag "Module" { + BeforeAll { + # Load all functions from private folders + if (-not (test-path -path $JCRScriptRoot -errorAction silentlyContinue)) { + write-host "JCRScriptRoot not set, setting it to the parent directory of the script root" + + # until we've found the correct parent path traversing up the directory tree + do { + $JCRScriptRoot = Split-Path -Path $PSScriptRoot -Parent + # check if the JumpCloud.Radius.psd1 file exists in the parent directory + if (Test-Path -Path "$JCRScriptRoot/JumpCloud.Radius.psd1") { + break + } + # if not, traverse up one more level + $PSScriptRoot = $JCRScriptRoot + } while (-not (Test-Path -Path "$JCRScriptRoot/JumpCloud.Radius.psd1")) + + } + $Private = @( Get-ChildItem -Path "$JCRScriptRoot/Functions/Private/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + + # local repo name: + $localRepoName = 'LocalPSRepo' + # get the registered repositories: + $repositories = Get-PSRepository + $localRepoPath = "~/$localRepoName" + if ($localRepoName -notin $repositories.Name) { + If (-not (Test-Path -Path $localRepoPath)) { + # create the local file share + New-Item -ItemType Directory -Path $localRepoPath -Force + } + $fullRepoPath = Resolve-Path $localRepoPath + # Register a file share on the local machine + $registerPSRepositorySplat = @{ + Name = $localRepoName + SourceLocation = $fullRepoPath.Path + ScriptSourceLocation = $fullRepoPath.Path + InstallationPolicy = 'Trusted' + } + Register-PSRepository @registerPSRepositorySplat + } else { + Write-Host "LocalPSRepo already exists" + } + + $requiredModules = @( + 'JumpCloud.SDK.V1', + 'JumpCloud.SDK.V2', + 'JumpCloud.SDK.DirectoryInsights', + 'JumpCloud' + ) + foreach ($module in $requiredModules) { + # Populate the local module path with the JumpCloud Module from PS Gallery + $latestJumpCloudModule = Find-Module -Name "$module" -Repository 'PSGallery' -ErrorAction Stop + + # if the nuget package already exists in the local repo, skip downloading + $foundModule = Find-Module -Name "$module" -Repository $localRepoName -ErrorAction SilentlyContinue #| Out-Null + if ($foundModule.Name -eq $module) { + Write-Host "Module $module already exists in the local repo: $localRepoPath" + $filesFound = Get-ChildItem -Path $localRepoPath -Filter "$module*.nupkg" + #print the file names: + foreach ($file in $filesFound) { + Write-Host "Found file: $($file.FullName) in local repo: $localRepoPath" + } + continue + } + $content = Invoke-WebRequest -UseBasicParsing -Uri "https://www.powershellgallery.com/api/v2/package/$module/$($latestJumpCloudModule.version)" ` + -Headers @{ + "authority" = "www.powershellgallery.com" + "method" = "GET" + "path" = "/api/v2/package/$module/$($latestJumpCloudModule.version)" + "scheme" = "https" + "accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7" + "accept-encoding" = "gzip, deflate, br, zstd" + "accept-language" = "en-US,en;q=0.9,es;q=0.8" + "referer" = "https://www.powershellgallery.com/packages/$module/$($latestJumpCloudModule.version)" + } + $binaryData = $content.Content + $filePath = Resolve-Path -Path $localRepoPath + # Save the binary data to a file + [System.IO.File]::WriteAllBytes("$($filePath.path)/$module.$($latestJumpCloudModule.version).zip", $binaryData) + # create a local module path if it does not exist + if (-not (Test-Path -Path "$($filePath.Path)/temp/$module")) { + New-Item -Path "$($filePath.Path)/temp/$module" -ItemType Directory + # create the version directory within the module path if it does not exist + New-Item -Path "$($filePath.Path)/temp/$module/$($latestJumpCloudModule.version)" -ItemType Directory + # Unzip the file to the local module path + Expand-Archive -Path "$($filePath.path)/$module.$($latestJumpCloudModule.version).zip" -DestinationPath "$localRepoPath/temp/$module/$($latestJumpCloudModule.version)" -Force + } + publish-module -Path "$($filePath.Path)/temp/$module/$($latestJumpCloudModule.version)" -Repository $localRepoName + # remove the zip file after publishing + Remove-Item -Path "$($filePath.path)/$module.$($latestJumpCloudModule.version).zip" -Force + # remove the module from the local module path if it exists + Remove-Item -Path "$($filePath.path)/temp.$module" -Recurse -Force -ErrorAction SilentlyContinue + # Print the LocalPSRepo File Contents: + Write-Host "# LocalPSRepo File Contents after $module insert #" + Get-ChildItem -Path $localRepoPath | ForEach-Object { + # print the file type, name and size + Write-Host "$($_.PSObject.TypeNames[0]) | $($_.Name) | $($_.Length) bytes" + } + } + + # Now publish the JumpCloud.Radius module to the local repo + $devModulePath = "$JCRScriptRoot" + $psd1Path = Join-Path $JCRScriptRoot "JumpCloud.Radius.psd1" + $Psd1 = Import-PowerShellDataFile -Path:("$psd1Path") + $moduleVersion = $Psd1.ModuleVersion + $radiusModule = "JumpCloud.Radius" + $filePath = Resolve-Path -Path $localRepoPath + $radiusModuleDirectory = "$($filePath.Path)/temp/$radiusModule" + + # remove the module if it exists + if (Test-Path -Path $radiusModuleDirectory) { + # remove the module directory if it exists + Remove-Item -Path $radiusModuleDirectory -Recurse -Force + } + # remove the .nupkg if it exists + $localNugetPkgs = Get-ChildItem -Path $localRepoPath -Filter "$radiusModule*.nupkg" + foreach ($pkg in $localNugetPkgs) { + Remove-Item -Path $pkg.FullName -Force + } + New-Item -Path "$radiusModuleDirectory" -ItemType Directory + # create the version directory within the module path if it does not exist + New-Item -Path "$radiusModuleDirectory/$moduleVersion" -ItemType Directory + # Copy all the contents from the parent folder to the destination folder + Copy-Item -Path $devModulePath/* -Destination "$radiusModuleDirectory/$moduleVersion" -Recurse -Force -Exclude "Cert", "UserCerts", "images", "data", "users.json", "reports", "Tests", "deploy", "key.encrypted", "keyCert.encrypted", "log.txt", "changelog.md", "config.json" + Publish-Module -Name "$radiusModuleDirectory/$moduleVersion" -Repository $localRepoName -Force -RequiredVersion $moduleVersion + + # Print the LocalPSRepo File Contents: + Write-Host "# LocalPSRepo File Contents #" + Get-ChildItem -Path $localRepoPath | ForEach-Object { + # print the file type, name and size + Write-Host "$($_.PSObject.TypeNames[0]) | $($_.Name) | $($_.Length) bytes" + } + } + Context 'Module can be installed from the local repo' { + BeforeAll { + # Get installed Radius Modules: + $installedModules = Get-InstalledModule -Name $radiusModule -AllVersions -ErrorAction SilentlyContinue + # Uninstall the module if it exists + foreach ($module in $installedModules) { + Uninstall-Module -Name $module.Name -Force -RequiredVersion $module.version + } + } + It 'Install Module' { + # Install the module from the local repo + write-host "Installing module $radiusModule from local repo $localRepoName with version $moduleVersion" + + install-Module -Name "JumpCloud.Radius" -Repository $localRepoName -RequiredVersion $moduleVersion + # check that the module is installed: + $installedModule = Get-InstalledModule -Name 'JumpCloud.Radius' + $installedModule | Should -Not -BeNullOrEmpty + $installedModule.version | Should -Be $moduleVersion + } + AfterAll { + $moduleCheck = Get-Module -Name $radiusModule + if ($moduleCheck) { + Write-Host "Removing module $radiusModule from the session" + Remove-Module -Name $radiusModule -Force + } else { + Write-Host "Module $radiusModule is not loaded in the session" + } + } + } + + Context 'When a new version of the module is available' { + BeforeAll { + # remove the module from the module directory + Remove-Item -Path $radiusModuleDirectory -Recurse -Force + Install-Module -Name $radiusModule -Repository $localRepoName -RequiredVersion $moduleVersion + + # import the installed Module + Write-Host "Importing module $radiusModule from local repo $localRepoName with version $moduleVersion" + Import-Module -Name $radiusModule -force + + $moduleCheck = Get-Module -Name $radiusModule + $moduleCheck | Should -Not -BeNullOrEmpty + $moduleCheck.version | Should -Be $moduleVersion + + # populate the config: + $settings = @{ + certType = "UsernameCn" + certSecretPass = "secret1234!" + radiusDirectory = "$(Resolve-Path $HOME/RADIUS)" + networkSSID = "TP-Link_SSID" + userGroup = "111111111111111111111111" + openSSLBinary = 'openssl' + certSubjectHeader = @{ + CountryCode = "US" + StateCode = "CO" + Locality = "Boulder" + Organization = "JumpCloud" + OrganizationUnit = "Customer_Tools" + CommonName = "JumpCloud.com" + } + } + + Set-JCRConfig @settings + + Write-Host "-----------------------" + Write-Host "[status] Module Path : $($moduleCheck.Path)" + Write-Host "[Status] JCRConfig Settings:" + foreach ($setting in $global:JCRConfig.PSObject.Properties) { + Write-Host ("$($setting.Name): $($setting.Value.value)") + } + Write-Host "-----------------------" + + # update the module manifest version to simulate a new version + $newVersion = "$(([version]$moduleVersion).major).$(([version]$moduleVersion).minor).$(([version]$moduleVersion).build + 1)" + Update-ModuleManifest -Path $psd1Path -ModuleVersion $newVersion + + # move the module to the local module path and publish it to the local repo + # create the module directory within the local module path if it does not exist + if (-NOT (Test-Path -Path $radiusModuleDirectory)) { + New-Item -Path $radiusModuleDirectory -ItemType Directory + # create the version directory within the module path if it does not exist + New-Item -Path "$radiusModuleDirectory/$newVersion" -ItemType Directory + # Copy all the contents from the parent folder to the destination folder + Write-Warning "Copying module files from $devModulePath to the local repo for version $newVersion" + Copy-Item -Path $devModulePath/* -Destination "$radiusModuleDirectory/$newVersion" -Recurse -Force -Exclude "Cert", "UserCerts", "images", "data", "users.json", "reports", "Tests", "deploy", "key.encrypted", "keyCert.encrypted", "log.txt", "changelog.md", "config.json" + Publish-Module -Name "$radiusModuleDirectory/$newVersion" -Repository $localRepoName -Force -RequiredVersion $newVersion + } + + } + It 'Module can be updated from the local repo' { + # update the module from the local repo + $configFileBefore = Get-JCRConfig -asObject + Update-JCRModule -Force -Repository $localRepoName + # $updateModuleSplat = @{ + # Name = $moduleName + # RequiredVersion = $newVersion + # Force = $true + # } + # Update-Module @updateModuleSplat -ErrorAction Stop | Out-Null + # check that the module is updated: + $updatedModule = Get-InstalledModule -Name $radiusModule + $updatedModule | Should -Not -BeNullOrEmpty + $updatedModule.version | Should -Be $newVersion + + # test that the config.json file contains the data from the previous module version + $configFileAfter = Get-JCRConfig -asObject + + foreach ($property in $configFileBefore.PSObject.Properties) { + # if the property is a hashtable, check each key-value pair + if ($property.value.type -eq "hashtable") { + foreach ($key in $configFileAfter.$($property.Name).value.PSObject.Properties) { + $configFileAfter.$($property.Name).value.$($key.name) | Should -Be $key.Value + # Validate that the value is the same as before + } + continue + } else { + + $configFileAfter.$($property.Name).value | Should -Be $property.value.value + } + } + + # Validate that the extension files in /Extensions are updated from JCRConfig + # print the $JCRScriptRoot + Write-Host "JCRScriptRoot: $JCRScriptRoot" + $extensionsDir = Join-Path $JCRScriptRoot "Extensions" + Write-Host "Extensions Directory: $extensionsDir" + # Validate the extension files exist: + $extensionsFiles = Get-ChildItem -Path (Resolve-Path -Path $extensionsDir) -Filter "extensions-*.cnf" + $extensionsFiles | Should -Not -BeNullOrEmpty + foreach ($file in $extensionsFiles) { + Write-Host "Found extension file: $($file.FullName)" + $fileContent = Get-Content -Path $file.FullName -Raw + # Validate that the file contains the expected headers + $expectedHeaders = @( + "C = $($global:JCRConfig.certSubjectHeader.Value.CountryCode)", + "ST = $($global:JCRConfig.certSubjectHeader.Value.StateCode)", + "L = $($global:JCRConfig.certSubjectHeader.Value.Locality)", + "O = $($global:JCRConfig.certSubjectHeader.Value.Organization)", + "OU = $($global:JCRConfig.certSubjectHeader.Value.OrganizationUnit)", + "CN = $($global:JCRConfig.certSubjectHeader.Value.CommonName)" + ) + foreach ($header in $expectedHeaders) { + $fileContent | Should -Match $header + } + } + # Validate that the extensions files are valid with the function + $extensionsValid = Test-JCRExtensionFile + $extensionsValid | Should -BeTrue + } + } + AfterAll { + # reset the module version + Update-ModuleManifest -Path $psd1Path -ModuleVersion $moduleVersion + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 b/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 new file mode 100644 index 000000000..6e1b31121 --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Start-DeployUserCerts.Tests.ps1 @@ -0,0 +1,797 @@ +Describe 'Distribute User Cert Tests' -Tag 'Distribute' { + BeforeAll { + # Load all functions from private folders + $Private = @( Get-ChildItem -Path "$JCRScriptRoot/Functions/Private/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + # import helper functions: + . "$PSScriptRoot/../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 + } + Get-JCRGlobalVars -Force -associateManually + Start-GenerateRootCert -certKeyPassword "TestCertificate123!@#" -generateType "new" -force + + } + Context 'Distribute all certificates for all users forcibly' { + BeforeAll { + # clear certs: + $certs = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/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 for users that have a system association + 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 + Write-Warning "$($user.username) created with id: $($user.id))" + $dateBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') + + Write-Warning "Add $($user.username) to radius Group with id: $($global:JCRConfig.userGroup.value)" + Add-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $user.id + $userMembers = Get-jcusergroupmember -byid $global:JCRConfig.userGroup.value + + foreach ($member in $userMembers) { + Write-Warning "$($member.username) is in the $($member.GroupName) Group" + } + + # update membership + Start-Sleep 1 + Get-JCRGlobalVars -force -associationUsername $user.username + 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' { + BeforeAll { + # clear certs: + $certs = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/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 + Start-Sleep 1 + $userArray = Get-UserJsonData + foreach ($user in $userArray) { + # cert should be deployed for users that have a system association + 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' { + + } + + } + Context 'Distribute new certificates for new users forcibly' { + BeforeAll { + # clear certs: + $certs = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/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:JCRConfig.userGroup.value -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 -force -associationUsername $user.username + + # wait one second to write to the file + Start-Sleep 1 + + # update the json file + 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 + } + } + 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:JCRConfig.userGroup.value -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 + + $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 + + } + + } + Context 'Distribute new certificates for a single user forcibly' { + it 'a user with system associations receives a new command is created and deployed' { + + } + it 'a user without system associations receives 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 receives a new command is created and is not deployed' { + + } + it 'a user without system associations receives a new command is not created and not deployed' { + + } + + } + Context 'Cert Commands are generated for EmailSAN, EmailDN, UsernameCn type certs' { + BeforeAll { + # Member content | Get the user with 2 system associations mac and windows + Get-JCRGlobalVars -force -skipAssociation -associateManually + $certTypeUser = $Global:JCRRadiusMembers | Get-Random -Count 1 + Write-Warning "begin while loop" + while ($Global:JCRAssociations[$($certTypeUser.userID)].systemAssociations.count -ne 2) { + $certTypeUser = $Global:JCRRadiusMembers | Get-Random -Count 1 + } + } + It 'EmailSAN certs are created and command generated with correct identifiers' { + # set the user cert validity to just 10 days + Set-JCRConfig -certType "EmailSAN" + # Get Cert Before + $CertInfoBefore = Get-CertInfo -UserCerts -username $certTypeUser.username + # Generate the user cert: + Start-GenerateUserCerts -type ByUsername -username $($certTypeUser.username) -forceReplaceCerts + # CertInfo After + $CertInfoAfter = Get-CertInfo -UserCerts -username $certTypeUser.username + # Cert Subject headers should be contain required EmailSAN identifier: + $CertInfoBefore.subject | Should -Not -Be $CertInfoAfter + $foundCert = Get-ChildItem -path "$($global:JCRConfig.radiusDirectory.value)/UserCerts/$($certTypeUser.username)-EmailSAN*.crt" + $foundCert.count | Should -Be 1 + $CertSANInfo = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -in $($foundCert.fullname) -ext subjectAltName -noout" + # The cert info should contain the subject alternative name of the user's email + $CertSANInfo -match "email:" | Should -match "email:$($Global:JCRUsers[$($certTypeUser.userID)].email)" + # Create the new commands + Start-DeployUserCerts -type ByUsername -username $certTypeUser.username + # Go fetch the mac command for the user + $macCommand = Get-JCCommand -name "RadiusCert-Install:$($certTypeUser.username):MacOSX" + $windowsCommand = Get-JCCommand -name "RadiusCert-Install:$($certTypeUser.username):Windows" + + # macCommand should contain SN + $snPattern = 'currentCertSN=\"(.*)\"' + $snMacMatch = $macCommand.Command | Select-String -Pattern $snPattern + $snMacMatch.matches.groups[1].value | Should -Be $CertInfoAfter.serial + # windows should contain SN + $snPattern = '\(\$\(\$cert\.serialNumber\) -eq \"(.*)\"\)' + $snWinMatch = $windowsCommand.Command | Select-String -Pattern $snPattern + $snWinMatch.matches.groups[1].value | Should -Be $CertInfoAfter.serial + } + It 'EmailDN certs are created and command generated with correct identifiers' { + # set the cert type + Set-JCRConfig -certType "EmailDN" + # Get Cert Before + $CertInfoBefore = Get-CertInfo -UserCerts -username $certTypeUser.username + # Generate the user cert: + Start-GenerateUserCerts -type ByUsername -username $($certTypeUser.username) -forceReplaceCerts + # CertInfo After + $CertInfoAfter = Get-CertInfo -UserCerts -username $certTypeUser.username + # Cert Subject headers should be contain required EmailDN identifier: + $CertInfoBefore.subject | Should -Not -Be $CertInfoAfter + $CertInfoAfter.subject | Should -Match "$($Global:JCRUsers[$($certTypeUser.userID)].email)" + # Create the new commands + Start-DeployUserCerts -type ByUsername -username $certTypeUser.username + # Go fetch the mac command for the user + $macCommand = Get-JCCommand -name "RadiusCert-Install:$($certTypeUser.username):MacOSX" + $windowsCommand = Get-JCCommand -name "RadiusCert-Install:$($certTypeUser.username):Windows" + + # macCommand should contain SN + $snPattern = 'currentCertSN=\"(.*)\"' + $snMacMatch = $macCommand.Command | Select-String -Pattern $snPattern + $snMacMatch.matches.groups[1].value | Should -Be $CertInfoAfter.serial + # windows should contain SN + $snPattern = '\(\$\(\$cert\.serialNumber\) -eq \"(.*)\"\)' + $snWinMatch = $windowsCommand.Command | Select-String -Pattern $snPattern + $snWinMatch.matches.groups[1].value | Should -Be $CertInfoAfter.serial + } + It 'UsernameCn certs are created and command generated with correct identifiers' { + # set the cert type + Set-JCRConfig -certType "usernameCn" + # Get Cert Before + $CertInfoBefore = Get-CertInfo -UserCerts -username $certTypeUser.username + # Generate the user cert: + Start-GenerateUserCerts -type ByUsername -username $($certTypeUser.username) -forceReplaceCerts + # CertInfo After + $CertInfoAfter = Get-CertInfo -UserCerts -username $certTypeUser.username + # Cert Subject headers should be contain required UsernameCn identifier: + $CertInfoBefore.subject | Should -Not -Be $CertInfoAfter + $CertInfoAfter.subject | Should -Match "$($certTypeUser.username)" + # Create the new commands + Start-DeployUserCerts -type ByUsername -username $certTypeUser.username + # Go fetch the mac command for the user + $macCommand = Get-JCCommand -name "RadiusCert-Install:$($certTypeUser.username):MacOSX" + $windowsCommand = Get-JCCommand -name "RadiusCert-Install:$($certTypeUser.username):Windows" + + # macCommand should contain SN + $snPattern = 'currentCertSN=\"(.*)\"' + $snMacMatch = $macCommand.Command | Select-String -Pattern $snPattern + $snMacMatch.matches.groups[1].value | Should -Be $CertInfoAfter.serial + # windows should contain SN + $snPattern = '\(\$\(\$cert\.serialNumber\) -eq \"(.*)\"\)' + $snWinMatch = $windowsCommand.Command | Select-String -Pattern $snPattern + $snWinMatch.matches.groups[1].value | Should -Be $CertInfoAfter.serial + + } + AfterAll { + # set the cert type + Set-JCRConfig -certType "usernameCn" + } + } + Context 'Duplicate Command / Command Result Tests' { + BeforeEach { + # create a user that has both a mac and windows association + $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:JCRConfig.userGroup.value -UserID $user.id + # get random system + $windowsSystem = Get-JCSystem -os windows | Get-Random -Count 1 + $macSystem = Get-JCSystem -os "Mac OS X" | Get-Random -Count 1 + Add-JCSystemUser -UserID $user.id -SystemID $windowsSystem.id + Add-JCSystemUser -UserID $user.id -SystemID $macSystem.id + + # update membership + Get-JCRGlobalVars -skipAssociation -force + # todo: manually update association table to account for new membership + Set-JCRAssociationHash -userId $user.id + Update-JCRUsersJson + + # Generate a user certificate for the user: + Start-GenerateUserCerts -type ByUsername -username $user.username + + # Get the SHA1 hash for the user's cert: + $certData = Get-CertInfo -userCerts -username $user.username + + } + It 'When a duplicate commands with differing trigger SHA1 hashes exists, the command with the old SHA1 hash should be removed' { + # Set a different SHA1 value to simulate an older cert command + $oldSha = "$($certData.sha1)1111" + # Mock some commands with the trigger to already exist if they do not exist + $macCommandBody = @{ + Name = "RadiusCert-Install:$($user.username):MacOSX" + Command = "sha1234" + launchType = "trigger" + User = "000000000000000000000000" + trigger = "$($oldSha)" + commandType = "mac" + timeout = 600 + TimeToLiveSeconds = 864000 + } + $newMacCommand = New-JcSdkCommand @macCommandBody + $macCmdBefore = Get-JcSdkCommand -Filter @("trigger:eq:$($oldSha)", "commandType:eq:mac") + + # invoke the commands manually to simulate the command queue containing items: + Start-JcSdkCommand -Id $macCmdBefore.Id -SystemIds $macSystem.id + + $windowsCommandBody = @{ + Name = "RadiusCert-Install:$($user.username):Windows" + Command = "sha1234" + launchType = "trigger" + User = "000000000000000000000000" + trigger = "$($oldSha)" + commandType = "windows" + timeout = 600 + TimeToLiveSeconds = 864000 + } + $newWindowsCommand = New-JcSdkCommand @windowsCommandBody + $windowsCmdBefore = Get-JcSdkCommand -Filter @("trigger:eq:$($oldSha)", "commandType:eq:windows") + + # invoke the commands manually to simulate the command queue containing items: + Start-JcSdkCommand -Id $WindowsCmdBefore.Id -SystemIds $windowsSystem.id + + # Get the queued commands: + $queuedCmdsBefore = Get-QueuedCommandByUser -username $user.username + + # Run Start Deploy User Certs by username + Start-DeployUserCerts -type ByUsername -username $user.username + + # After running, validate that the commands before execution, no longer exist + $macCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + $windowsCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + # Get the queued after: + $queuedCmdsAfter = Get-QueuedCommandByUser -username $user.username + # test that the commands should not exist: + $macCmdAfter.id | Should -Not -Contain $macCmdBefore.id + $windowsCmdAfter.id | Should -Not -Contain $windowsCmdBefore.id + $macCmdAfter | Should -Not -BeNullOrEmpty + $windowsCmdAfter | Should -Not -BeNullOrEmpty + { Get-JcSdkCommand -Id $macCmdBefore.id } | Should -Throw # in other words the command should not exist + { Get-JcSdkCommand -Id $windowsCmdBefore.id } | Should -Throw # in other words the command should not exist + # test that the queued commands should not exist: + $queuedCmdsAfter.id | Should -Not -Contain $queuedCmdsBefore.Id + $queuedCmdsAfter.id | Should -BeNullOrEmpty + # user.json should have the newID in command associations. + $allUserData = Get-UserJsonData + $testUserData = $allUserData | Where-Object { $_.username -eq $user.username } + $testUserData.commandAssociations.commandId | Should -Not -Contain $macCmdBefore.id + $testUserData.commandAssociations.commandId | Should -Not -Contain $windowsCmdBefore.id + } + It 'When a single command with the same trigger SHA1 hashes exists, a new cert command should not should be generated' { + # Mock some commands with the trigger to already exist if they do not exist + $macCommandBody = @{ + Name = "RadiusCert-Install:$($user.username):MacOSX" + Command = "sha1234" + launchType = "trigger" + User = "000000000000000000000000" + trigger = "$($certData.sha1)" + commandType = "mac" + timeout = 600 + TimeToLiveSeconds = 864000 + } + $newMacCommand = New-JcSdkCommand @macCommandBody + $macCmdBefore = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + + # invoke the commands manually to simulate the command queue containing items: + Start-JcSdkCommand -Id $macCmdBefore.Id -SystemIds $macSystem.id + + $windowsCommandBody = @{ + Name = "RadiusCert-Install:$($user.username):Windows" + Command = "sha1234" + launchType = "trigger" + User = "000000000000000000000000" + trigger = "$($certData.sha1)" + commandType = "windows" + timeout = 600 + TimeToLiveSeconds = 864000 + } + $newWindowsCommand = New-JcSdkCommand @windowsCommandBody + $windowsCmdBefore = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + + # invoke the commands manually to simulate the command queue containing items: + Start-JcSdkCommand -Id $WindowsCmdBefore.Id -SystemIds $windowsSystem.id + + # Get the queued commands: + $queuedCmdsBefore = Get-QueuedCommandByUser -username $user.username + + # Run Start Deploy User Certs by username + Start-DeployUserCerts -type ByUsername -username $user.username + + # After running, validate that the commands before execution, no longer exist + $macCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + $windowsCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + # Get the queued after: + $queuedCmdsAfter = Get-QueuedCommandByUser -username $user.username + # test that the commands should not exist: + $macCmdAfter.id | Should -Contain $macCmdBefore.id + $windowsCmdAfter.id | Should -Contain $windowsCmdBefore.id + $macCmdAfter | Should -Not -BeNullOrEmpty + $windowsCmdAfter | Should -Not -BeNullOrEmpty + { Get-JcSdkCommand -Id $macCmdBefore.id } | Should -Not -Throw # in other words the command should not exist + { Get-JcSdkCommand -Id $windowsCmdBefore.id } | Should -Not -Throw # in other words the command should not exist + # test that the queued commands should not exist: + $queuedCmdsAfter.id | Should -Not -Contain $queuedCmdsBefore.Id + $queuedCmdsAfter.id | Should -BeNullOrEmpty + # user.json should have the newID in command associations. + $allUserData = Get-UserJsonData + $testUserData = $allUserData | Where-Object { $_.username -eq $user.username } + $testUserData.commandAssociations.commandId | Should -Contain $macCmdBefore.id + $testUserData.commandAssociations.commandId | Should -Contain $windowsCmdBefore.id + + } + It 'When duplicate commands with the same trigger SHA1 hashes exists, one should be removed, a new command should not be generated' { + # Mock some commands with the trigger to already exist if they do not exist + $possibleMacIDs = New-Object System.Collections.ArrayList + $possibleWindowsIDs = New-Object System.Collections.ArrayList + # define command body for macOS commands + $macCommandBody = @{ + Name = "RadiusCert-Install:$($user.username):MacOSX" + Command = "sha1234" + launchType = "trigger" + User = "000000000000000000000000" + trigger = "$($certData.sha1)" + commandType = "mac" + timeout = 600 + TimeToLiveSeconds = 864000 + } + # add a mac command to the org using the command body + $newMacCommand = New-JcSdkCommand @macCommandBody + + # update the command body to differentiate the next command: + $macCommandBody.Command = "sha12345" + + # add a second mac command to the org using the command body + $newMacCommand = New-JcSdkCommand @macCommandBody + + # get the commands for the user: + $macCommandsBefore = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + + # for each command + foreach ($cmd in $macCommandsBefore) { + # add to the list + $possibleMacIDs.Add($cmd.id) + } + # the number of macOS Commands for this user cert and it's identifying sha should be 2 + $possibleMacIDs.count | Should -Be 2 + + # invoke the commands manually to simulate the command queue containing items: + Start-JcSdkCommand -Id $possibleMacIDs[0] -SystemIds $macSystem.id + + # define windows command body + $windowsCommandBody = @{ + Name = "RadiusCert-Install:$($user.username):Windows" + Command = "sha1234" + launchType = "trigger" + User = "000000000000000000000000" + trigger = "$($certData.sha1)" + commandType = "windows" + timeout = 600 + TimeToLiveSeconds = 864000 + } + # create the first windows command + $newWindowsCommand = New-JcSdkCommand @windowsCommandBody + + # update the command body to differentiate the next command: + $windowsCommandBody.Command = "sha12345" + + # create the second windows command + $newWindowsCommand = New-JcSdkCommand @windowsCommandBody + + $windowsCommandsBefore = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + + # for each command + foreach ($cmd in $windowsCommandsBefore) { + # add to the list + $possibleWindowsIDs.Add($cmd.id) + } + $possibleWindowsIDs.count | Should -Be 2 + + # invoke the commands manually to simulate the command queue containing items: + Start-JcSdkCommand -Id $possibleWindowsIDs[0] -SystemIds $windowsSystem.id + + # Get the queued commands: + $queuedCmdsBefore = Get-QueuedCommandByUser -username $user.username + + # Run Start Deploy User Certs by username + Start-DeployUserCerts -type ByUsername -username $user.username + + # After running, validate that the commands before execution, no longer exist + $macCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + $windowsCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + # Get the queued after: + $queuedCmdsAfter = Get-QueuedCommandByUser -username $user.username + # test that one of the commands should exist: + + # $macCmdBefore.id | Should -BeIn $macCmdAfter.id + $macCmdAfter.count | Should -Be 1 + $macCmdAfter.id | should -BeIn $possibleMacIDs + + # $windowsCmdBefore.id | Should -BeIn $windowsCmdAfter.id + $windowsCmdAfter.count | Should -Be 1 + $windowsCmdAfter.id | should -BeIn $possibleWindowsIDs + + # test that the queued commands should not exist: + $queuedCmdsAfter.id | Should -Not -Contain $queuedCmdsBefore.Id + $queuedCmdsAfter.id | Should -BeNullOrEmpty + # user.json should have the newID in command associations. + $allUserData = Get-UserJsonData + $testUserData = $allUserData | Where-Object { $_.username -eq $user.username } + + $testUserData.commandAssociations.commandId | Should -Contain $windowsCmdAfter.id + $testUserData.commandAssociations.commandId | Should -Contain $macCmdAfter.id + } + It "When the commands are generated the names of the command should match the predetermined naming structure" { + # Run Start Deploy User Certs by username + Start-DeployUserCerts -type ByUsername -username $user.username + + $macCommand = Get-JcSdkCommand -Filter @("trigger:eq:$($($certData.sha1))", "commandType:eq:mac") + $windowsCommand = Get-JcSdkCommand -Filter @("trigger:eq:$($($certData.sha1))", "commandType:eq:windows") + + # macCommand name should be set correctly + $macCommand.Name | Should -Match "RadiusCert-Install:$($user.username):MacOSX" + # windowsCommand name should be set correctly + $windowsCommand.Name | Should -Match "RadiusCert-Install:$($user.username):Windows" + + # validate that the command names are recorded correctly in the users.json file + $userData = Get-UserFromTable -userId $user.id + $userData.commandAssociations.commandName | Should -Contain "RadiusCert-Install:$($user.username):MacOSX" + $userData.commandAssociations.commandName | Should -Contain "RadiusCert-Install:$($user.username):Windows" + } + } + Context 'Force Generate Certificate Tests' { + BeforeEach { + # create a user that has both a mac and windows association + $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:JCRConfig.userGroup.value -UserID $user.id + # get random system + $windowsSystem = Get-JCSystem -os windows | Get-Random -Count 1 + $macSystem = Get-JCSystem -os "Mac OS X" | Get-Random -Count 1 + Add-JCSystemUser -UserID $user.id -SystemID $windowsSystem.id + Add-JCSystemUser -UserID $user.id -SystemID $macSystem.id + + # update membership + Get-JCRGlobalVars -skipAssociation -force + # todo: manually update association table to account for new membership + Set-JCRAssociationHash -userId $user.id + Update-JCRUsersJson + + # Generate a user certificate for the user: + Start-GenerateUserCerts -type ByUsername -username $user.username + + # Get the SHA1 hash for the user's cert: + $certData = Get-CertInfo -userCerts -username $user.username + + # Mock some commands with the trigger to already exist if they do not exist + $macCommandBody = @{ + Name = "RadiusCert-Install:$($user.username):MacOSX" + Command = "sha1234" + launchType = "trigger" + User = "000000000000000000000000" + trigger = "$($certData.sha1)" + commandType = "mac" + timeout = 600 + TimeToLiveSeconds = 864000 + } + $newMacCommand = New-JcSdkCommand @macCommandBody + $macCmdBefore = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + + # invoke the commands manually to simulate the command queue containing items: + Start-JcSdkCommand -Id $macCmdBefore.Id -SystemIds $macSystem.id + + $windowsCommandBody = @{ + Name = "RadiusCert-Install:$($user.username):Windows" + Command = "sha1234" + launchType = "trigger" + User = "000000000000000000000000" + trigger = "$($certData.sha1)" + commandType = "windows" + timeout = 600 + TimeToLiveSeconds = 864000 + } + $newWindowsCommand = New-JcSdkCommand @windowsCommandBody + $windowsCmdBefore = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + + # invoke the commands manually to simulate the command queue containing items: + Start-JcSdkCommand -Id $WindowsCmdBefore.Id -SystemIds $windowsSystem.id + + # Get the queued commands: + $queuedCmdsBefore = Get-QueuedCommandByUser -username $user.username + + } + It 'When forceGenerate switch is specified, the existing commands & queued commands should be removed' { + # Run Start Deploy User Certs by username + Start-DeployUserCerts -type ByUsername -username $user.username -forceGenerateCommands + + # After running, validate that the commands before execution, no longer exist + $macCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + $windowsCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + # Get the queued after: + $queuedCmdsAfter = Get-QueuedCommandByUser -username $user.username + # test that the commands should not exist: + $macCmdAfter.id | Should -Not -Contain $macCmdBefore.id + $windowsCmdAfter.id | Should -Not -Contain $windowsCmdBefore.id + $macCmdAfter | Should -Not -BeNullOrEmpty + $windowsCmdAfter | Should -Not -BeNullOrEmpty + { Get-JcSdkCommand -Id $macCmdBefore.id } | Should -Throw # in other words the command should not exist + { Get-JcSdkCommand -Id $windowsCmdBefore.id } | Should -Throw # in other words the command should not exist + # test that the queued commands should not exist: + $queuedCmdsAfter.id | Should -Not -Contain $queuedCmdsBefore.Id + $queuedCmdsAfter.id | Should -BeNullOrEmpty + # user.json should have the newID in command associations. + $allUserData = Get-UserJsonData + $testUserData = $allUserData | Where-Object { $_.username -eq $user.username } + $testUserData.commandAssociations.commandId | Should -Not -Contain $macCmdBefore.id + $testUserData.commandAssociations.commandId | Should -Not -Contain $windowsCmdBefore.id + } + It 'When forceGenerate & forceInvoke switches are specified, the existing commands & queued commands should be removed; new commands queues should be queued' { + # Run Start Deploy User Certs by username + Start-DeployUserCerts -type ByUsername -username $user.username -forceGenerateCommands -forceInvokeCommands + + # After running, validate that the commands before execution, no longer exist + $macCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + $windowsCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + # Get the queued after: + $queuedCmdsAfter = Get-QueuedCommandByUser -username $user.username + # test that the commands should not exist: + $macCmdAfter.id | Should -Not -Contain $macCmdBefore.id + $windowsCmdAfter.id | Should -Not -Contain $windowsCmdBefore.id + $macCmdAfter | Should -Not -BeNullOrEmpty + $windowsCmdAfter | Should -Not -BeNullOrEmpty + { Get-JcSdkCommand -Id $macCmdBefore.id } | Should -Throw # in other words the command should not exist + { Get-JcSdkCommand -Id $windowsCmdBefore.id } | Should -Throw # in other words the command should not exist + # test that the queued commands should not exist: + $queuedCmdsAfter.id | Should -Not -Contain $queuedCmdsBefore.Id + $queuedCmdsAfter.id | Should -Not -BeNullOrEmpty + # user.json should have the newID in command associations. + $allUserData = Get-UserJsonData + $testUserData = $allUserData | Where-Object { $_.username -eq $user.username } + $testUserData.commandAssociations.commandId | Should -Not -Contain $macCmdBefore.id + $testUserData.commandAssociations.commandId | Should -Not -Contain $windowsCmdBefore.id + } + } + context 'Deploy by all' { + It "deploys user certs by all" { + Start-DeployUserCerts -type "All" -forceGenerateCommands -forceInvokeCommands + $allUserDataBefore = Get-UserJsonData + Start-Sleep 5 + Start-DeployUserCerts -type "All" -forceGenerateCommands -forceInvokeCommands + $allUserDataAfter = Get-UserJsonData + + foreach ($user in $allUserDataBefore) { + if ($user.certInfo.deploymentDate) { + $user.certInfo.deploymentDate | Should -BeLessThan ($allUserDataAfter | Where-Object { $_.userName -eq $user.UserName }).certInfo.deploymentDate + } + } + } + + } + Context "Certs generated for users with users with localUsernames and special characters" { + + It "Generates a command for a user with a localUsername (systemUsername)" { + # create a user that has both a mac and windows association + $user = New-RandomUser -Domain "pesterRadius" | New-JCUser + # manually set the user + $headers = @{ + "x-api-key" = "$env:JCApiKey" + "content-type" = "application/json" + } + # set a unique systemUsername for the user + $body = @{ + 'systemUsername' = "$($user.username)$($user.unix_guid)" + } | ConvertTo-Json + # update the user + $response = Invoke-RestMethod -Uri "https://console.jumpcloud.com/api/systemusers/$($user.id)" -Method PUT -Headers $headers -ContentType 'application/json' -Body $body + # get the before date + $dateBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') + # add user to membership group + Add-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $user.id + # get random system + $windowsSystem = Get-JCSystem -os windows | Get-Random -Count 1 + $macSystem = Get-JCSystem -os "Mac OS X" | Get-Random -Count 1 + # associate the system + Add-JCSystemUser -UserID $user.id -SystemID $windowsSystem.id + Add-JCSystemUser -UserID $user.id -SystemID $macSystem.id + + + # update membership + Get-JCRGlobalVars -skipAssociation -force + # todo: manually update association table to account for new membership + Set-JCRAssociationHash -userId $user.id + Update-JCRUsersJson + + # Generate a user certificate for the user: + Start-GenerateUserCerts -type ByUsername -username $user.username + + # Get the SHA1 hash for the user's cert: + $certData = Get-CertInfo -userCerts -username $user.username + + # Run Start Deploy User Certs by username + Start-DeployUserCerts -type ByUsername -username $user.username + + # get the commands + $windowsCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + $macCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + # validate that the correct local user name is found in the command body: + $macCmdAfter.command | Should -Match "userCompare=`"$($response.systemUsername)`"" + $windowsCmdAfter.command | Should -Match "-eq `"$($response.systemUsername)`"" + + } + It "Generates a command for a user with a hyphen in their username" { + # create a user that has both a mac and windows association + $user = New-RandomUser -Domain "pesterRadius" | New-JCUser + # manually update the user with a hyphen in their username + $user = Set-JCSdkUser -id $($user.id) -username "$($user.username)-$($user.username)" + # get the before date + $dateBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') + # add user to membership group + Add-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $user.id + # get random system + $windowsSystem = Get-JCSystem -os windows | Get-Random -Count 1 + $macSystem = Get-JCSystem -os "Mac OS X" | Get-Random -Count 1 + # associate the system + Add-JCSystemUser -UserID $user.id -SystemID $windowsSystem.id + Add-JCSystemUser -UserID $user.id -SystemID $macSystem.id + + # update membership + Get-JCRGlobalVars -skipAssociation -force + # todo: manually update association table to account for new membership + Set-JCRAssociationHash -userId $user.id + Update-JCRUsersJson + + # Generate a user certificate for the user: + Start-GenerateUserCerts -type ByUsername -username $user.username + + # Get the SHA1 hash for the user's cert: + $certData = Get-CertInfo -userCerts -username $user.username + + # Run Start Deploy User Certs by username + Start-DeployUserCerts -type ByUsername -username $user.username + + # get the commands + $windowsCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:windows") + $macCmdAfter = Get-JcSdkCommand -Filter @("trigger:eq:$($certData.sha1)", "commandType:eq:mac") + # validate that the correct local user name is found in the command body: + $macCmdAfter.command | Should -Match "userCompare=`"$($user.username)`"" + $windowsCmdAfter.command | Should -Match "-eq `"$($user.username)`"" + } + } +} \ No newline at end of file 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..cdae417b6 --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Start-GenerateRootCert.Tests.ps1 @@ -0,0 +1,137 @@ +Describe "Generate Root Certificate Tests" -Tag "GenerateRootCert" { + BeforeEach { + # If the /Cert/ folder is not empty, clear the directory + $items = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/Cert" + if ($items) { + foreach ($item in $items) { + # If the item is the 'backup' folder, process its contents separately + Remove-Item -Path $item.FullName -Recurse -Force -Confirm:$false + } + } + } + Context "A new certificate can be generated" { + It 'Generates a new root certificate' { + Start-GenerateRootCert -certKeyPassword "testCertificate123!@#" -generateType "new" -force + + # both the key and the cert should be generated + $itemsAfter = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/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 "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem -subject" + + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "C") | Should -Match $global:JCRConfig.certSubjectHeader.Value.CountryCode + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "ST") | Should -Match $global:JCRConfig.certSubjectHeader.Value.StateCode + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "L") | Should -Match $global:JCRConfig.certSubjectHeader.Value.Locality + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "O") | Should -Match $global:JCRConfig.certSubjectHeader.Value.Organization + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "OU") | Should -Match $global:JCRConfig.certSubjectHeader.Value.OrganizationUnit + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "CN") | Should -Match $global:JCRConfig.certSubjectHeader.Value.CommonName + } + } + + # Overwrite the existing certificate + + Context "Overwrite an existing root certificate when creating a new one" { + It 'Generates a new root certificate when there is an existing one' { + # Create a new root certificate + Start-GenerateRootCert -certKeyPassword "testCertificate123!@#" -generateType "New" -force + # get existing cert serial: + $origSN = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem -serial" + # Force overwrite the existing certificate + Start-GenerateRootCert -certKeyPassword "testCertificate123!@#" -generateType "New" -force + # get new SN + $newSN = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/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 -Path "$($global:JCRConfig.radiusDirectory.value)/Cert" + $itemsAfter.BaseName | Should -Contain "radius_ca_cert" + $itemsAfter.BaseName | Should -Contain "radius_ca_key" + + # Validate that the backup zip file was created + $backupFiles = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/Backups" + # Should contain a zip file + $backupFiles.Extension | Should -Contain ".zip" + + #validate the subject matches what's defined in config: + $CA_subject = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem -subject" + + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "C") | Should -Match $global:JCRConfig.certSubjectHeader.Value.CountryCode + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "ST") | Should -Match $global:JCRConfig.certSubjectHeader.Value.StateCode + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "L") | Should -Match $global:JCRConfig.certSubjectHeader.Value.Locality + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "O") | Should -Match $global:JCRConfig.certSubjectHeader.Value.Organization + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "OU") | Should -Match $global:JCRConfig.certSubjectHeader.Value.OrganizationUnit + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "CN") | Should -Match $global:JCRConfig.certSubjectHeader.Value.CommonName + } + } + + Context "An existing certificate can be replaced" { + It 'Replaces a root certificate' { + # Create a new root certificate + Start-GenerateRootCert -certKeyPassword "testCertificate123!@#" -generateType "new" -force + # get existing cert serial: + $origSN = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem -serial" + # Replace root certificate + Start-GenerateRootCert -certKeyPassword "testCertificate123!@#" -generateType "replace" -force + # get new SN + $newSN = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/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 -Path "$($global:JCRConfig.radiusDirectory.value)/Cert" + $itemsAfter.BaseName | Should -Contain "radius_ca_cert" + $itemsAfter.BaseName | Should -Contain "radius_ca_key" + + # Validate that the backup zip file was created + $backupFiles = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/Backups" + # Should contain a zip file + $backupFiles.Extension | Should -Contain ".zip" + + #validate the subject matches what's defined in config: + $CA_subject = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem -subject" + + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "C") | Should -Match $global:JCRConfig.certSubjectHeader.Value.CountryCode + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "ST") | Should -Match $global:JCRConfig.certSubjectHeader.Value.StateCode + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "L") | Should -Match $global:JCRConfig.certSubjectHeader.Value.Locality + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "O") | Should -Match $global:JCRConfig.certSubjectHeader.Value.Organization + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "OU") | Should -Match $global:JCRConfig.certSubjectHeader.Value.OrganizationUnit + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "CN") | Should -Match $global:JCRConfig.certSubjectHeader.Value.CommonName + } + + } + Context "An existing certificate can be renewed" { + It 'Renews a root certificate' { + # Generate new CA + Start-GenerateRootCert -certKeyPassword "testCertificate123!@#" -generateType "new" -force + # get existing cert serial: + $origSN = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem -serial" + # Renew CA + Start-GenerateRootCert -certKeyPassword "testCertificate123!@#" -generateType "renew" -force + # get new SN + $newSN = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/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 -Be $newSN + # both the key and the cert should be generated + $itemsAfter = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/Cert" + $itemsAfter.BaseName | Should -Contain "radius_ca_cert" + $itemsAfter.BaseName | Should -Contain "radius_ca_key" + + # Validate that the backup zip file was created + $backupFiles = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/Cert/Backups" + # Should contain a zip file + $backupFiles.Extension | Should -Contain ".zip" + + #validate the subject matches what's defined in config: + $CA_subject = Invoke-Expression "$($global:JCRConfig.openSSLBinary.value) x509 -noout -in $($global:JCRConfig.radiusDirectory.value)/Cert/radius_ca_cert.pem -subject" + + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "C") | Should -Match $global:JCRConfig.certSubjectHeader.Value.CountryCode + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "ST") | Should -Match $global:JCRConfig.certSubjectHeader.Value.StateCode + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "L") | Should -Match $global:JCRConfig.certSubjectHeader.Value.Locality + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "O") | Should -Match $global:JCRConfig.certSubjectHeader.Value.Organization + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "OU") | Should -Match $global:JCRConfig.certSubjectHeader.Value.OrganizationUnit + (Get-SubjectHeaderValue -SubjectString $CA_subject -Header "CN") | Should -Match $global:JCRConfig.certSubjectHeader.Value.CommonName + } + + } +} \ 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 new file mode 100644 index 000000000..5ba6705e4 --- /dev/null +++ b/scripts/automation/Radius/Tests/Public/Start-GenerateUserCerts.Tests.ps1 @@ -0,0 +1,275 @@ +Describe 'Generate User Cert Tests' -Tag "GenerateUserCerts" { + BeforeAll { + # Load all functions from private folders + $Private = @( Get-ChildItem -Path "$JCRScriptRoot/Functions/Private/*.ps1" -Recurse) + Foreach ($Import in $Private) { + Try { + . $Import.FullName + } Catch { + Write-Error -Message "Failed to import function $($Import.FullName): $_" + } + } + Start-GenerateRootCert -certKeyPassword "testCertificate123!@#" -generateType "new" -force + } + Context 'Certs forcibly re-generated for all users' { + It 'Certs re-generated have actually been re-written for all users' { + # first generate user certs + Start-GenerateUserCerts -type All -forceReplaceCerts + # capture the current time and cert times. + $timeBefore = (Get-Date).ToString('MM/dd/yyyy HH:mm:ss') + $certsBefore = Get-CertInfo -UserCerts + # wait one second. + Start-Sleep 1 + Start-GenerateUserCerts -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' { + BeforeAll { + + } + It 'When a new user is added to the radius group, the tool will generate a new cert' { + # create a new user + $user = New-RandomUser -Domain "pesterRadius" | New-JCUser + # add a user to the radius Group + Add-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $user.id + # Get the certs before + $certsBefore = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts" + # wait one moment + Start-Sleep 1 + # update the cache + Get-JCRGlobalVars -force -skipAssociation -associateManually + # 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 + $Global:JCRUsers.keys | should -Contain $user.id + # Generate the user cert: + Start-GenerateUserCerts -type ByUsername -username $($user.username) -forceReplaceCerts + # Get the certs after + $certsAfter = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/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:JCRConfig.userGroup.value -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 who have certs that are about set to expire soon' { + BeforeAll { + # import necessary functions: + . "$JCRScriptRoot/Functions/Private/CertDeployment/Get-CertInfo.ps1" + . "$JCRScriptRoot/Functions/Private/CertDeployment/Get-ExpiringCertInfo.ps1" + # set the user cert validity to just 10 days + Set-JCRConfig -userCertValidityDays 10 + + # update cache + Get-JCRGlobalVars -force -skipAssociation + + # 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 + # Find all certs that will expire between current date and cut off date + $Global:expiringCerts = Get-ExpiringCertInfo -certInfo $userCertInfo -cutoffDate $global:JCRConfig.certExpirationWarningDays.value + + } + It 'Certs that are set to expire soon can be updated programmatically' { + # at this point expiring certs should be populated from beforeAll block + $Global:expiringCerts | Should -Not -BeNullOrEmpty + # set the user cert validity to 90 days + Set-JCRConfig -userCertValidityDays 90 + # Get the certs before generation minus the .zip if it exists + $certsBefore = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/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 $global:JCRConfig.certExpirationWarningDays.value + # 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 "$($global:JCRConfig.radiusDirectory.value)/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 { + # set the user cert validity to 90 days + Set-JCRConfig -userCertValidityDays 90 + } + + + } + Context 'Certs generated for users with users with localUsernames and special characters' { + + BeforeEach { + # create a new user + $user = New-RandomUser -Domain "pesterRadius" | New-JCUser + + } + It 'A user with a localUsername (SystemUsername) will generate a cert' { + # manually set the user + $headers = @{ + "x-api-key" = "$env:JCApiKey" + "content-type" = "application/json" + } + # set a unique systemUsername for the user + $body = @{ + 'systemUsername' = "$($user.username)$($user.unix_guid)" + } | ConvertTo-Json + # update the user + $response = Invoke-RestMethod -Uri "https://console.jumpcloud.com/api/systemusers/$($user.id)" -Method PUT -Headers $headers -ContentType 'application/json' -Body $body + # add a user to the radius Group + Add-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $user.id + # Get the certs before + $certsBefore = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts" + # wait one moment + Start-Sleep 1 + # update the cache + Get-JCRGlobalVars -force -skipAssociation -associateManually + # 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 + $Global:JCRUsers.keys | should -Contain $user.id + # Generate the user cert: + Start-GenerateUserCerts -type ByUsername -username $($user.username) -forceReplaceCerts + # Get the certs after + $certsAfter = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts" + # filter by username + $UserCerts = $certsAfter | Where-Object { $_.Name -match "$($user.username)" } + # the files and each type of expected cert file should exist + # specifically for this test, the username should not be the localUsername (systemUsername) + $UserCerts.Name | Should -Match $user.username + $userCerts.Name | Should -Not -Match $response.systemUsername + $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 + + } + It 'A user with a hyphen in their username will generate a cert' { + # manually update the user with a hyphen in their username + $user = Set-JcSdkUser -Id $($user.id) -Username "$($user.username)-$($user.username)" + # add a user to the radius Group + Add-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $user.id + # Get the certs before + $certsBefore = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts" + # wait one moment + Start-Sleep 1 + # update the cache + Get-JCRGlobalVars -force -skipAssociation -associateManually + # 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 + $Global:JCRUsers.keys | should -Contain $user.id + # Generate the user cert: + Start-GenerateUserCerts -type ByUsername -username $($user.username) -forceReplaceCerts + # Get the certs after + $certsAfter = Get-ChildItem -Path "$($global:JCRConfig.radiusDirectory.value)/UserCerts" + # filter by username + $UserCerts = $certsAfter | Where-Object { $_.Name -match "$($user.username)" } + # the files and each type of expected cert file should exist + # specifically for this test, the username should not be the localUsername (systemUsername) + $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 + } + AfterEach { + # cleanup + Remove-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -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 when userGroup only contains 1 user' { + BeforeAll { + # Save users in userGroup to variable for later + $RadiusMembers = Get-JCUserGroupMember -ByID $global:JCRConfig.userGroup.value + + # Remove all members from UserGroup + $RadiusMembers | ForEach-Object { + $userRemoval = Remove-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $_.UserID + } + + # Add One Member back to the Group + $SingleUser = $RadiusMembers | Select-Object -First 1 + $userAdd = Add-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $SingleUser.UserID + } + It "When the Radius UserGroup only contains 1 user, the generation functions will not error" { + # update the cache + Get-JCRGlobalVars -force -skipAssociation -associateManually + + Start-Sleep 1 + + # the updated Radius Members cache should only contain 1 user + $global:JCRRadiusMembers.username.Count | Should -Be 1 + + # the new user should be in the membership list: + $global:JCRRadiusMembers.username | Should -Contain $SingleUser.username + + # Check for non-terminating errors + Start-GenerateUserCerts -type ByUsername -username $($SingleUser.username) -forceReplaceCerts -ErrorVariable err + $err.Count | Should -Be 0 + } + AfterAll { + # Remove the single User + $userRemoval = Remove-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $SingleUser.UserID + + # Add original members back to the UserGroup + $userAdd = $RadiusMembers | ForEach-Object { Add-JCUserGroupMember -GroupID $global:JCRConfig.userGroup.value -UserID $_.UserID } + + # update cache + Get-JCRGlobalVars -force -skipAssociation + } + } +} \ No newline at end of file diff --git a/scripts/automation/Radius/Tests/SetupRadiusOrg.ps1 b/scripts/automation/Radius/Tests/SetupRadiusOrg.ps1 new file mode 100644 index 000000000..1cda1157b --- /dev/null +++ b/scripts/automation/Radius/Tests/SetupRadiusOrg.ps1 @@ -0,0 +1,101 @@ +# import helper functions: +. "$PSScriptRoot/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 + +# remove existing radius commands in test: +Write-Warning "Removing Pester Radius Commands" +$commandsToRemove = Get-JCCommand -Name "RadiusCert-Install:*" +foreach ($commandToRemove in $commandsToRemove) { + Remove-JCCommand -CommandID $commandToRemove._id -force | out-null +} + +# 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" + +# set a rootKeyPassword +$env:certKeyPassword = "testCertificate123!@#" + +# update config: +Write-Warning "Updating Config File" + +# Create a new Radius directory +$radiusDirectory = Join-Path -Path $HOME -ChildPath "RADIUS" +if (-Not (Test-Path -Path $radiusDirectory)) { + New-Item -ItemType Directory -Path $radiusDirectory | Out-Null +} + +# Update the userGroupID: +$settings = @{ + certType = "UsernameCn" + certSecretPass = "secret1234!" + radiusDirectory = "$(Resolve-Path $HOME/RADIUS)" + networkSSID = "TP-Link_SSID" + userGroup = $radiusUserGroup.id + openSSLBinary = 'openssl' + certSubjectHeader = @{ + CountryCode = "US" + StateCode = "CO" + Locality = "Boulder" + Organization = "JumpCloud" + OrganizationUnit = "Customer_Tools" + CommonName = "JumpCloud.com" + } +} + +Set-JCRConfig @settings +# update the openSSL path: +if ($IsMacOS) { + $brewList = brew list openssl@3 + if (-Not ($brewList)) { + Write-Warning "OpenSSL v3 is not installed on this system. Attempting to install..." + try { + brew install openssl@3 + } catch { + Write-Host could not install openssl + } + } + + $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:" +} + +$env:certKeyPassword = "TestCertificate123!@#" +Import-Module "$psscriptRoot/../JumpCloud.Radius.psd1" -Force \ No newline at end of file diff --git a/scripts/automation/Radius/deploy/BuildNuspecFromPsd1.ps1 b/scripts/automation/Radius/deploy/BuildNuspecFromPsd1.ps1 new file mode 100644 index 000000000..a6a356887 --- /dev/null +++ b/scripts/automation/Radius/deploy/BuildNuspecFromPsd1.ps1 @@ -0,0 +1,195 @@ + +# $nuspecFiles = @{ src = 'en-Us/**;Private/**;Public/**;JumpCloud.psd1;JumpCloud.psm1;LICENSE'; } +$nuspecFiles = @( + @{src = "en-Us/**/*.*"; target = "en-Us" }, + @{src = "Functions/Public/**/*.ps1"; target = "Functions/Public" }, + @{src = "Functions/Private/**/*.ps1"; target = "Functions/Private" }, + @{src = "Extensions/**/*.cnf"; target = "Extensions" }, + @{src = "JumpCloud.Radius.psd1" }, + @{src = "JumpCloud.Radius.psm1" }, + @{src = "LICENSE" }, + @{src = "readme.md" } +) +# Adapted from PowerShell Get +# https://github.com/PowerShell/PowerShellGetv2/blob/7de99ee0c38611556e5c583ffaca98bb1922a0d4/src/PowerShellGet/private/functions/New-NuspecFile.ps1 +function New-NuspecFile { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$OutputPath, + + [Parameter(Mandatory = $true)] + [string]$Id, + + [Parameter(Mandatory = $true)] + [string]$Version, + + [Parameter()] + [String]$buildNumber, + + [Parameter(Mandatory = $true)] + [string]$Description, + + [Parameter(Mandatory = $true)] + [string[]]$Authors, + + [Parameter()] + [string[]]$Owners, + + [Parameter()] + [string]$ReleaseNotes, + + [Parameter()] + [bool]$RequireLicenseAcceptance, + + [Parameter()] + [string]$Copyright, + + [Parameter()] + [string[]]$Tags, + + [Parameter()] + [string]$LicenseUrl, + + [Parameter()] + [string]$ProjectUrl, + + [Parameter()] + [string]$IconUrl, + + [Parameter()] + [PSObject[]]$Dependencies, + + [Parameter()] + [PSObject[]]$Files + + ) + Set-StrictMode -Off + + Write-Verbose "Calling New-NuspecFile" + + $nameSpaceUri = "http://schemas.microsoft.com/packaging/2011/08/nuspec.xsd" + [xml]$xml = New-Object System.Xml.XmlDocument + + $xmlDeclaration = $xml.CreateXmlDeclaration("1.0", "utf-8", $null) + $xml.AppendChild($xmlDeclaration) | Out-Null + + #create top-level elements + $packageElement = $xml.CreateElement("package", $nameSpaceUri) + $metaDataElement = $xml.CreateElement("metadata", $nameSpaceUri) + # warn we're over 4000 characters for standard nuget servers + $tagsString = $Tags -Join " " + if ($tagsString.Length -gt 4000) { + Write-Warning -Message "Tag list exceeded 4000 characters and may not be accepted by some Nuget feeds." + } + + Write-Host "env:Source = $($env:Source)`r`nglobal:Source = $($global:Source)" + # Append buildNumber + if ($env:Source -eq "CodeArtifact") { + $date = (Get-Date).ToString("yyyyMMddHHmm") + # $BuildString = "build$($env:GITHUB_RUN_NUMBER)datetime$($date)" + $build = $($env:GITHUB_RUN_NUMBER) + $Version = $Version + ".$($build)" + "-$date" + Write-Host "Building Module Version: $Version" + } else { + Write-Host "Building Module Version: $Version" + } + + $metaDataElementsHash = [ordered]@{ + id = $Id + version = $Version + description = $Description + authors = $Authors -Join "," + owners = $Owners -Join "," + releaseNotes = $ReleaseNotes + requireLicenseAcceptance = $RequireLicenseAcceptance.ToString().ToLower() + copyright = $Copyright + tags = $tagsString + } + + if ($LicenseUrl) { + $metaDataElementsHash.Add("licenseUrl", $LicenseUrl) + } + if ($ProjectUrl) { + $metaDataElementsHash.Add("projectUrl", $ProjectUrl) + } + if ($IconUrl) { + $metaDataElementsHash.Add("iconUrl", $IconUrl) + } + + foreach ($key in $metaDataElementsHash.Keys) { + $element = $xml.CreateElement($key, $nameSpaceUri) + $elementInnerText = $metaDataElementsHash.item($key) + $element.InnerText = $elementInnerText + + $metaDataElement.AppendChild($element) | Out-Null + } + + + if ($Dependencies) { + $dependenciesElement = $xml.CreateElement("dependencies", $nameSpaceUri) + + foreach ($dependency in $Dependencies) { + $element = $xml.CreateElement("dependency", $nameSpaceUri) + # $element. + $element.SetAttribute("id", $dependency) + if ($dependency.version) { + $element.SetAttribute("version", $dependency.version) + } + + $dependenciesElement.AppendChild($element) | Out-Null + } + $metaDataElement.AppendChild($dependenciesElement) | Out-Null + } + + if ($Files) { + $filesElement = $xml.CreateElement("files", $nameSpaceUri) + + foreach ($file in $Files) { + $element = $xml.CreateElement("file", $nameSpaceUri) + $element.SetAttribute("src", $file.src) + if ($file.target) { + $element.SetAttribute("target", $file.target) + } + if ($file.exclude) { + $element.SetAttribute("exclude", $file.exclude) + } + + $filesElement.AppendChild($element) | Out-Null + } + } + + $packageElement.AppendChild($metaDataElement) | Out-Null + if ($filesElement) { + $packageElement.AppendChild($filesElement) | Out-Null + } + + $xml.AppendChild($packageElement) | Out-Null + + $nuspecFullName = Join-Path -Path $OutputPath -ChildPath "$Id.nuspec" + $xml.save($nuspecFullName) + + Write-Output $nuspecFullName +} +# Set Variables for New-NuspecFile +$psd1Path = "$PSScriptRoot/../JumpCloud.Radius.psd1" +$Psd1 = Import-PowerShellDataFile -Path:("$psd1Path") +$params = @{ + OutputPath = "$PSScriptRoot/../" + Id = $(Get-Item ($psd1Path)).BaseName + buildNumber = $env:GITHUB_RUN_NUMBER + Version = $Psd1.ModuleVersion + Authors = $Psd1.Author + Owners = $Psd1.CompanyName + Description = $Psd1.Description + ReleaseNotes = $Psd1.PrivateData.PSData.ReleaseNotes + # RequireLicenseAcceptance = ($requireLicenseAcceptance -eq $true) + Copyright = $Psd1.Copyright + Tags = $Psd1.PrivateData.PSData.Tags + LicenseUrl = $Psd1.PrivateData.PSData.LicenseUri + ProjectUrl = $Psd1.PrivateData.PSData.ProjectUri + IconUrl = $Psd1.PrivateData.PSData.IconUri + Dependencies = $Psd1.RequiredModules + Files = $nuspecFiles +} +New-NuspecFile @params \ No newline at end of file diff --git a/scripts/automation/Radius/deploy/build.ps1 b/scripts/automation/Radius/deploy/build.ps1 new file mode 100644 index 000000000..91e9aed5b --- /dev/null +++ b/scripts/automation/Radius/deploy/build.ps1 @@ -0,0 +1,52 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true, HelpMessage = 'The type of build to create.')] + [ValidateSet('Major', 'Minor', 'Patch')] + [System.String] + $buildType = 'Patch' +) + +# Define the module path and output path +$modulePath = "$PSScriptRoot/../" +$outputPath = "$PSScriptRoot/output" +$rootPath = "$PSScriptRoot/../../../../" + +# Ensure the output directory exists +if (-not (Test-Path -Path $outputPath)) { + New-Item -ItemType Directory -Path $outputPath | Out-Null +} + +# Get the public functions from the module +$publicFunctions = Get-ChildItem -Path "$modulePath/Functions/Public" -Recurse -Filter '*.ps1' + +# Get the psd1 file for the module +$psd1Path = "$modulePath/JumpCloud.Radius.psd1" +$Psd1 = Import-PowerShellDataFile -Path:("$psd1Path") + +$moduleManifest = @{ + ModuleVersion = $Psd1.ModuleVersion + RootModule = 'JumpCloud.Radius.psm1' + GUID = $Psd1.GUID + Author = $Psd1.Author + CompanyName = $psd1.CompanyName + Copyright = $Psd1.Copyright + Description = $Psd1.Description + PowerShellVersion = $Psd1.PowerShellVersion + RequiredModules = $Psd1.RequiredModules + FunctionsToExport = $publicFunctions.basename + Path = $psd1Path + +} + +# update the module manifest with public functions and generation date +Update-ModuleManifest @moduleManifest + +# Package the module into a .nupkg file +. $PSScriptRoot/BuildNuspecFromPsd1.ps1 + +# generate docs +$helpFileFunctionPath = Join-Path $rootPath "PowerShell/Deploy/Build-HelpFiles.ps1" + +. $helpFileFunctionPath -ModuleName 'JumpCloud.Radius' -ModulePath $modulePath + +Write-Host "Module packaged successfully to $outputPath" \ No newline at end of file diff --git a/scripts/automation/Radius/docs/Get-JCRCertReport.md b/scripts/automation/Radius/docs/Get-JCRCertReport.md new file mode 100644 index 000000000..1f267dc1c --- /dev/null +++ b/scripts/automation/Radius/docs/Get-JCRCertReport.md @@ -0,0 +1,65 @@ +--- +external help file: JumpCloud.Radius-help.xml +Module Name: JumpCloud.Radius +online version: / +schema: 2.0.0 +--- + +# Get-JCRCertReport + +## SYNOPSIS + +This cmdlet generates a report of RADIUS certificates for JumpCloud devices and their associated users. + +## SYNTAX + +``` +Get-JCRCertReport [-ExportFilePath] [] +``` + +## DESCRIPTION + +This cmdlet generates a report of RADIUS certificates for JumpCloud devices and their associated users. + +The report includes details such as certificate installation status, and user associations. + +## EXAMPLES + +### Example 1 + +```powershell +Get-JCRCertReport -ExportFilePath "C:\Reports\RadiusCertReport.csv" +``` + +This command generates a report of RADIUS certificates and exports it to a CSV file at the specified path. + +## PARAMETERS + +### -ExportFilePath + +Specifies the file path where the report will be exported. + +```yaml +Type: System.IO.FileInfo +Parameter Sets: (All) +Aliases: + +Required: True +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/scripts/automation/Radius/docs/Get-JCRGlobalVars.md b/scripts/automation/Radius/docs/Get-JCRGlobalVars.md new file mode 100644 index 000000000..e26102363 --- /dev/null +++ b/scripts/automation/Radius/docs/Get-JCRGlobalVars.md @@ -0,0 +1,112 @@ +--- +external help file: JumpCloud.Radius-help.xml +Module Name: JumpCloud.Radius +online version: / +schema: 2.0.0 +--- + +# Get-JCRGlobalVars + +## SYNOPSIS + +This function retrieves and updates global variables related to JumpCloud Radius deployment, including user associations and system caches. + +## SYNTAX + +``` +Get-JCRGlobalVars [-force] [-skipAssociation] [-associateManually] [[-associationUsername] ] + [] +``` + +## DESCRIPTION + +This function retrieves and updates global variables related to JumpCloud Radius deployment, including user associations and system caches. + +## EXAMPLES + +### Example 1 + +```powershell +Get-JCRGlobalVars -force +``` + +This command forces an update of all cached users, systems, associations, and RADIUS group members. + +## PARAMETERS + +### -associateManually + +Updates the system to user association cache manually using the graph api + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -associationUsername + +Updates just a single user's associations manually using the graph api + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -force + +Force update all cached users, systems, associations, radius group members + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -skipAssociation + +Skips the user to system association cache, which may take a long time on larger organizations + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/scripts/automation/Radius/docs/JumpCloud.Radius.md b/scripts/automation/Radius/docs/JumpCloud.Radius.md new file mode 100644 index 000000000..e8df21626 --- /dev/null +++ b/scripts/automation/Radius/docs/JumpCloud.Radius.md @@ -0,0 +1,41 @@ +--- +Module Name: JumpCloud.Radius +Module Guid: 71bfaf58-3326-4512-9a7f-a2d9dc19d6b5 +Download Help Link: +Help Version: 2.1.0 +Locale: en-Us +--- + +# JumpCloud.Radius Module +## Description +Module for managing JumpCloud Radius user certificates. + +## JumpCloud.Radius Cmdlets +### [Get-JCRCertReport](Get-JCRCertReport.md) +This cmdlet generates a report of RADIUS certificates for JumpCloud devices and their associated users. + +### [Get-JCRGlobalVars](Get-JCRGlobalVars.md) +This function retrieves and updates global variables related to JumpCloud Radius deployment, including user associations and system caches. + +### [Set-JCRConfig](Set-JCRConfig.md) +This function sets the configuration for the JumpCloud Radius module, allowing you to specify various parameters such as certificate subject headers, network SSID, last update timestamp, certificate secret password, OpenSSL binary path, user certificate validity days, radius directory, CA certificate validity days, user group, certificate expiration warning days, and certificate type. + +### [Start-DeployUserCerts](Start-DeployUserCerts.md) +This function initiates the deployment of user certificates for JumpCloud Managed Users. + +### [Start-GenerateRootCert](Start-GenerateRootCert.md) +This function generates a root certificate for the JumpCloud Radius module, allowing you to create or replace the root certificate as needed. + +### [Start-GenerateUserCerts](Start-GenerateUserCerts.md) +This function generates user certificates for JumpCloud Managed Users, allowing you to specify the type of certificate generation and whether to replace existing certificates. + +### [Start-MonitorCertDeployment](Start-MonitorCertDeployment.md) +This function monitors the deployment of certificates for JumpCloud Managed Users, ensuring that the deployment process is tracked and any issues are logged. + +### [Start-RadiusDeployment](Start-RadiusDeployment.md) +This function is the root menu for the GUI portion of the JumpCloud Radius module. It provides a user interface for managing Radius deployments, including generating root certificates, user certificates, and monitoring deployments. + +### [Update-JCRModule](Update-JCRModule.md) +This function updates the JumpCloud Radius module, ensuring that the latest configurations and settings are applied. + + diff --git a/scripts/automation/Radius/docs/Set-JCRConfig.md b/scripts/automation/Radius/docs/Set-JCRConfig.md new file mode 100644 index 000000000..c871292d1 --- /dev/null +++ b/scripts/automation/Radius/docs/Set-JCRConfig.md @@ -0,0 +1,243 @@ +--- +external help file: JumpCloud.Radius-help.xml +Module Name: JumpCloud.Radius +online version: / +schema: 2.0.0 +--- + +# Set-JCRConfig + +## SYNOPSIS + +This function sets the configuration for the JumpCloud Radius module, allowing you to specify various parameters such as certificate subject headers, network SSID, last update timestamp, certificate secret password, OpenSSL binary path, user certificate validity days, radius directory, CA certificate validity days, user group, certificate expiration warning days, and certificate type. + +## SYNTAX + +``` +Set-JCRConfig [-caCertValidityDays ] + [-certSubjectHeader ] [-openSSLBinary ] [-lastUpdate ] [-networkSSID ] + [-radiusDirectory ] [-certExpirationWarningDays ] [-userCertValidityDays ] + [-userGroup ] [-certType ] [-certSecretPass ] [] +``` + +## DESCRIPTION + +This function sets the configuration for the JumpCloud Radius module. It allows you to specify various parameters such as certificate subject headers, network SSID, last update timestamp, certificate secret password, OpenSSL binary path, user certificate validity days, radius directory, CA certificate validity days, user group, certificate expiration warning days, and certificate type. + +## EXAMPLES + +### Example 1 + +```powershell +$settings = @{ + radiusDirectory = "/Users/username/RADIUS" + certType = "UsernameCn" + certSubjectHeader @{ + CountryCode = "Your_Country_Code" + StateCode = "Your_State_Code" + Locality = "Your_City" + Organization = "Your_Organization_Name" + OrganizationUnit = "Your_Organization_Unit" + CommonName = "Your_Common_Name" + } + certSecretPass = "secret1234!" + networkSSID = "Your_SSID" + userGroup = "5f3171a9232e1113939dd6a2" + openSSLBinary = '/opt/homebrew/bin/openssl' +} + +Set-JCRConfig @settings +``` + +This command sets the required configuration for the JumpCloud Radius module using the specified settings. + +## PARAMETERS + +### -caCertValidityDays + +sets the caCertValidityDays config for the module + +```yaml +Type: System.Int32 +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -certExpirationWarningDays + +sets the certExpirationWarningDays config for the module + +```yaml +Type: System.Int32 +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -certSecretPass + +sets the certSecretPass config for the module + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -certSubjectHeader + +sets the certSubjectHeader config for the module + +```yaml +Type: System.Collections.Hashtable +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -certType + +sets the certType config for the module + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -lastUpdate + +sets the lastUpdate config for the module + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -networkSSID + +sets the networkSSID config for the module + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -openSSLBinary + +sets the openSSLBinary config for the module + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -radiusDirectory + +sets the radiusDirectory config for the module + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -userCertValidityDays + +sets the userCertValidityDays config for the module + +```yaml +Type: System.Int32 +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -userGroup + +sets the userGroup config for the module + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/scripts/automation/Radius/docs/Start-DeployUserCerts.md b/scripts/automation/Radius/docs/Start-DeployUserCerts.md new file mode 100644 index 000000000..ffe5a51e7 --- /dev/null +++ b/scripts/automation/Radius/docs/Start-DeployUserCerts.md @@ -0,0 +1,119 @@ +--- +external help file: JumpCloud.Radius-help.xml +Module Name: JumpCloud.Radius +online version: / +schema: 2.0.0 +--- + +# Start-DeployUserCerts + +## SYNOPSIS + +This function initiates the deployment of user certificates for JumpCloud Managed Users. + +## SYNTAX + +### gui (Default) +``` +Start-DeployUserCerts [] +``` + +### cli +``` +Start-DeployUserCerts -type [-username ] [-forceInvokeCommands] [-forceGenerateCommands] + [] +``` + +## DESCRIPTION + +Typically called using a CLI method in scripts this function initiates the deployment of user certificates for JumpCloud Managed Users. It can be used to generate new commands or invoke existing ones based on the specified parameters. + +## EXAMPLES + +### Example 1 + +```powershell +Start-DeployUserCerts -type "New" -forceInvokeCommands +``` + +This command initiates the deployment of new user certificates and forces the invocation of generated commands on the systems. + +## PARAMETERS + +### -forceGenerateCommands + +Switch to force generate new commands on systems + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: cli +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -forceInvokeCommands + +Switch to force invoke generated commands on systems + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: cli +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -type + +Type of cert deployment to initiate + +```yaml +Type: System.String +Parameter Sets: cli +Aliases: +Accepted values: All, New, ByUsername + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -username + +The JumpCloud username of a user to deploy a certificate + +```yaml +Type: System.String +Parameter Sets: cli +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/scripts/automation/Radius/docs/Start-GenerateRootCert.md b/scripts/automation/Radius/docs/Start-GenerateRootCert.md new file mode 100644 index 000000000..c39c1f14a --- /dev/null +++ b/scripts/automation/Radius/docs/Start-GenerateRootCert.md @@ -0,0 +1,103 @@ +--- +external help file: JumpCloud.Radius-help.xml +Module Name: JumpCloud.Radius +online version: / +schema: 2.0.0 +--- + +# Start-GenerateRootCert + +## SYNOPSIS + +This function generates a root certificate for the JumpCloud Radius module, allowing you to create or replace the root certificate as needed. + +## SYNTAX + +### gui (Default) +``` +Start-GenerateRootCert [] +``` + +### cli +``` +Start-GenerateRootCert [-certKeyPassword ] [-generateType ] [-force] + [] +``` + +## DESCRIPTION + +This function generates a root certificate for the JumpCloud Radius module. It allows you to specify whether to create a new certificate, replace an existing one, or renew the current certificate. The function can also handle the root certificate key password and force replacement of existing certificates if specified. + +## EXAMPLES + +### Example 1 + +```powershell +Start-GenerateRootCert -generateType "New" -certKeyPassword "your_password" -force +``` + +This command generates a new root certificate for the JumpCloud Radius module, using the specified key password and forcing replacement of any existing certificates. + +## PARAMETERS + +### -certKeyPassword + +The root certificate key password + +```yaml +Type: System.String +Parameter Sets: cli +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -force + +When specified, this parameter will replace certificates if they already exist on the current filesystem + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: cli +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -generateType + +Select an option to generate or replace the root certificate + +```yaml +Type: System.String +Parameter Sets: cli +Aliases: +Accepted values: New, Replace, Renew + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/scripts/automation/Radius/docs/Start-GenerateUserCerts.md b/scripts/automation/Radius/docs/Start-GenerateUserCerts.md new file mode 100644 index 000000000..1ac85fb77 --- /dev/null +++ b/scripts/automation/Radius/docs/Start-GenerateUserCerts.md @@ -0,0 +1,106 @@ +--- +external help file: JumpCloud.Radius-help.xml +Module Name: JumpCloud.Radius +online version: / +schema: 2.0.0 +--- + +# Start-GenerateUserCerts + +## SYNOPSIS + +This function generates user certificates for JumpCloud Managed Users, allowing you to specify the type of certificate generation and whether to replace existing certificates. + +## SYNTAX + +### gui (Default) +``` +Start-GenerateUserCerts [] +``` + +### cli +``` +Start-GenerateUserCerts -type [-username ] [-forceReplaceCerts] + [] +``` + +## DESCRIPTION + +This function generates user certificates for JumpCloud Managed Users. It allows you to specify the type of certificate generation, such as generating certificates for all users, new users, by username, or for users with expiring certificates. The function can also replace existing certificates if specified. + +## EXAMPLES + +### Example 1 + +```powershell +Start-GenerateUserCerts -type "New" +``` + +This command generates user certificates for users who have not yet had certificates generated. + +## PARAMETERS + +### -forceReplaceCerts + +When specified, this parameter will replace certificates if they already exist on the current filesystem + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: cli +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -type + +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". + +```yaml +Type: System.String +Parameter Sets: cli +Aliases: +Accepted values: All, New, ByUsername, ExpiringSoon + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -username + +The JumpCloud username of an individual user + +```yaml +Type: System.String +Parameter Sets: cli +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/scripts/automation/Radius/docs/Start-MonitorCertDeployment.md b/scripts/automation/Radius/docs/Start-MonitorCertDeployment.md new file mode 100644 index 000000000..390ddceeb --- /dev/null +++ b/scripts/automation/Radius/docs/Start-MonitorCertDeployment.md @@ -0,0 +1,47 @@ +--- +external help file: JumpCloud.Radius-help.xml +Module Name: JumpCloud.Radius +online version: / +schema: 2.0.0 +--- + +# Start-MonitorCertDeployment + +## SYNOPSIS + +This function monitors the deployment of certificates for JumpCloud Managed Users, ensuring that the deployment process is tracked and any issues are logged. + +## SYNTAX + +``` +Start-MonitorCertDeployment [] +``` + +## DESCRIPTION + +This function monitors the deployment of certificates for JumpCloud Managed Users. It is designed to track the progress of certificate deployment. + +## EXAMPLES + +### Example 1 + +```powershell +Start-MonitorCertDeployment +``` + +This command starts monitoring the certificate deployment process for JumpCloud Managed Users. + +## PARAMETERS + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/scripts/automation/Radius/docs/Start-RadiusDeployment.md b/scripts/automation/Radius/docs/Start-RadiusDeployment.md new file mode 100644 index 000000000..1adc60942 --- /dev/null +++ b/scripts/automation/Radius/docs/Start-RadiusDeployment.md @@ -0,0 +1,47 @@ +--- +external help file: JumpCloud.Radius-help.xml +Module Name: JumpCloud.Radius +online version: / +schema: 2.0.0 +--- + +# Start-RadiusDeployment + +## SYNOPSIS + +This function is the root menu for the GUI portion of the JumpCloud Radius module. It provides a user interface for managing Radius deployments, including generating root certificates, user certificates, and monitoring deployments. + +## SYNTAX + +``` +Start-RadiusDeployment [] +``` + +## DESCRIPTION + +This function is the root menu for the GUI portion of the JumpCloud Radius module. It provides a user interface for managing Radius deployments, including generating root certificates, user certificates, and monitoring deployments. The GUI allows users to easily navigate through various options related to Radius management without needing to use command-line parameters. + +## EXAMPLES + +### Example 1 + +```powershell +Start-RadiusDeployment +``` + +This command starts the GUI for managing Radius deployments, allowing users to generate root certificates, user certificates, and monitor deployments through a user-friendly interface. + +## PARAMETERS + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/scripts/automation/Radius/docs/Update-JCRModule.md b/scripts/automation/Radius/docs/Update-JCRModule.md new file mode 100644 index 000000000..347cd3ab0 --- /dev/null +++ b/scripts/automation/Radius/docs/Update-JCRModule.md @@ -0,0 +1,79 @@ +--- +external help file: JumpCloud.Radius-help.xml +Module Name: JumpCloud.Radius +online version: / +schema: 2.0.0 +--- + +# Update-JCRModule + +## SYNOPSIS + +This function updates the JumpCloud Radius module, ensuring that the latest configurations and settings are applied. + +## SYNTAX + +``` +Update-JCRModule [-Force] [[-Repository] ] [] +``` + +## DESCRIPTION + +This function will check if there are any updates available for the JumpCloud Radius module and apply them if necessary. It can also bypass user prompts if the `-Force` parameter is specified. + +## EXAMPLES + +### Example 1 + +```powershell +Update-JCRModule -Force +``` + +This command forces the update of the JumpCloud Radius module without any user prompts. + +## PARAMETERS + +### -Force + +ByPasses user prompts. + +```yaml +Type: System.Management.Automation.SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Repository + +Set the PSRepository + +```yaml +Type: System.String +Parameter Sets: (All) +Aliases: + +Required: False +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None +## OUTPUTS + +### System.Object +## NOTES + +## RELATED LINKS diff --git a/scripts/automation/Radius/docs/about_JumpCloud.Radius.md b/scripts/automation/Radius/docs/about_JumpCloud.Radius.md new file mode 100644 index 000000000..43bbf7373 --- /dev/null +++ b/scripts/automation/Radius/docs/about_JumpCloud.Radius.md @@ -0,0 +1,28 @@ +# JumpCloud.Radius + +## about_JumpCloud.Radius + +# SHORT DESCRIPTION + +PowerShell module to generate and manage RADIUS certs for JumpCloud Managed Users. + +# LONG DESCRIPTION + +This module provides cmdlets for generating and managing RADIUS certificates for users in JumpCloud. It simplifies the process of creating, deploying, and renewing certificates, ensuring secure authentication for clients connecting to JumpCloud Radius-Backed networks. + +# EXAMPLES + +``` +# Open the GUI menu to begin the process of generating and managing RADIUS certs. +Start-RadiusDeployment +``` + +# TROUBLESHOOTING NOTE + +This module requires the JumpCloud Module to be installed and configured. Ensure you have the necessary permissions and API keys set up in your JumpCloud account. + +Please view the reademe.md for this module for more information on how to set up the environment and prerequisites. + +# SEE ALSO + +# KEYWORDS diff --git a/scripts/automation/Radius/en-Us/JumpCloud.Radius-help.xml b/scripts/automation/Radius/en-Us/JumpCloud.Radius-help.xml new file mode 100644 index 000000000..7184359b0 --- /dev/null +++ b/scripts/automation/Radius/en-Us/JumpCloud.Radius-help.xml @@ -0,0 +1,1254 @@ + + + + + Get-JCRCertReport + Get + JCRCertReport + + This cmdlet generates a report of RADIUS certificates for JumpCloud devices and their associated users. + + + + This cmdlet generates a report of RADIUS certificates for JumpCloud devices and their associated users. + The report includes details such as certificate installation status, and user associations. + + + + Get-JCRCertReport + + ExportFilePath + + Specifies the file path where the report will be exported. + + System.IO.FileInfo + + System.IO.FileInfo + + + None + + + + + + + ExportFilePath + + Specifies the file path where the report will be exported. + + System.IO.FileInfo + + System.IO.FileInfo + + + None + + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + Get-JCRCertReport -ExportFilePath "C:\Reports\RadiusCertReport.csv" + + This command generates a report of RADIUS certificates and exports it to a CSV file at the specified path. + + + + + + Online Version: + / + + + + + + Get-JCRGlobalVars + Get + JCRGlobalVars + + This function retrieves and updates global variables related to JumpCloud Radius deployment, including user associations and system caches. + + + + This function retrieves and updates global variables related to JumpCloud Radius deployment, including user associations and system caches. + + + + Get-JCRGlobalVars + + associationUsername + + Updates just a single user's associations manually using the graph api + + System.String + + System.String + + + None + + + associateManually + + Updates the system to user association cache manually using the graph api + + + System.Management.Automation.SwitchParameter + + + False + + + force + + Force update all cached users, systems, associations, radius group members + + + System.Management.Automation.SwitchParameter + + + False + + + + skipAssociation + + Skips the user to system association cache, which may take a long time on larger organizations + + + System.Management.Automation.SwitchParameter + + + False + + + + + + associateManually + + Updates the system to user association cache manually using the graph api + + System.Management.Automation.SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + associationUsername + + Updates just a single user's associations manually using the graph api + + System.String + + System.String + + + None + + + force + + Force update all cached users, systems, associations, radius group members + + System.Management.Automation.SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + + skipAssociation + + Skips the user to system association cache, which may take a long time on larger organizations + + System.Management.Automation.SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + Get-JCRGlobalVars -force + + This command forces an update of all cached users, systems, associations, and RADIUS group members. + + + + + + Online Version: + / + + + + + + Set-JCRConfig + Set + JCRConfig + + This function sets the configuration for the JumpCloud Radius module, allowing you to specify various parameters such as certificate subject headers, network SSID, last update timestamp, certificate secret password, OpenSSL binary path, user certificate validity days, radius directory, CA certificate validity days, user group, certificate expiration warning days, and certificate type. + + + + This function sets the configuration for the JumpCloud Radius module. It allows you to specify various parameters such as certificate subject headers, network SSID, last update timestamp, certificate secret password, OpenSSL binary path, user certificate validity days, radius directory, CA certificate validity days, user group, certificate expiration warning days, and certificate type. + + + + Set-JCRConfig + + caCertValidityDays + + sets the caCertValidityDays config for the module + + System.Int32 + + System.Int32 + + + None + + + certExpirationWarningDays + + sets the certExpirationWarningDays config for the module + + System.Int32 + + System.Int32 + + + None + + + certSecretPass + + sets the certSecretPass config for the module + + System.String + + System.String + + + None + + + certSubjectHeader + + sets the certSubjectHeader config for the module + + System.Collections.Hashtable + + System.Collections.Hashtable + + + None + + + certType + + sets the certType config for the module + + System.String + + System.String + + + None + + + lastUpdate + + sets the lastUpdate config for the module + + System.String + + System.String + + + None + + + networkSSID + + sets the networkSSID config for the module + + System.String + + System.String + + + None + + + openSSLBinary + + sets the openSSLBinary config for the module + + System.String + + System.String + + + None + + + + radiusDirectory + + sets the radiusDirectory config for the module + + System.String + + System.String + + + None + + + userCertValidityDays + + sets the userCertValidityDays config for the module + + System.Int32 + + System.Int32 + + + None + + + userGroup + + sets the userGroup config for the module + + System.String + + System.String + + + None + + + + + + caCertValidityDays + + sets the caCertValidityDays config for the module + + System.Int32 + + System.Int32 + + + None + + + certExpirationWarningDays + + sets the certExpirationWarningDays config for the module + + System.Int32 + + System.Int32 + + + None + + + certSecretPass + + sets the certSecretPass config for the module + + System.String + + System.String + + + None + + + certSubjectHeader + + sets the certSubjectHeader config for the module + + System.Collections.Hashtable + + System.Collections.Hashtable + + + None + + + certType + + sets the certType config for the module + + System.String + + System.String + + + None + + + lastUpdate + + sets the lastUpdate config for the module + + System.String + + System.String + + + None + + + networkSSID + + sets the networkSSID config for the module + + System.String + + System.String + + + None + + + openSSLBinary + + sets the openSSLBinary config for the module + + System.String + + System.String + + + None + + + + radiusDirectory + + sets the radiusDirectory config for the module + + System.String + + System.String + + + None + + + userCertValidityDays + + sets the userCertValidityDays config for the module + + System.Int32 + + System.Int32 + + + None + + + userGroup + + sets the userGroup config for the module + + System.String + + System.String + + + None + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + $settings = @{ + radiusDirectory = "/Users/username/RADIUS" + certType = "UsernameCn" + certSubjectHeader @{ + CountryCode = "Your_Country_Code" + StateCode = "Your_State_Code" + Locality = "Your_City" + Organization = "Your_Organization_Name" + OrganizationUnit = "Your_Organization_Unit" + CommonName = "Your_Common_Name" + } + certSecretPass = "secret1234!" + networkSSID = "Your_SSID" + userGroup = "5f3171a9232e1113939dd6a2" + openSSLBinary = '/opt/homebrew/bin/openssl' +} + +Set-JCRConfig @settings + + This command sets the required configuration for the JumpCloud Radius module using the specified settings. + + + + + + Online Version: + / + + + + + + Start-DeployUserCerts + Start + DeployUserCerts + + This function initiates the deployment of user certificates for JumpCloud Managed Users. + + + + Typically called using a CLI method in scripts this function initiates the deployment of user certificates for JumpCloud Managed Users. It can be used to generate new commands or invoke existing ones based on the specified parameters. + + + + Start-DeployUserCerts + + forceGenerateCommands + + Switch to force generate new commands on systems + + + System.Management.Automation.SwitchParameter + + + False + + + forceInvokeCommands + + Switch to force invoke generated commands on systems + + + System.Management.Automation.SwitchParameter + + + False + + + + type + + Type of cert deployment to initiate + + + All + New + ByUsername + + System.String + + System.String + + + None + + + username + + The JumpCloud username of a user to deploy a certificate + + System.String + + System.String + + + None + + + + + + forceGenerateCommands + + Switch to force generate new commands on systems + + System.Management.Automation.SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + forceInvokeCommands + + Switch to force invoke generated commands on systems + + System.Management.Automation.SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + + type + + Type of cert deployment to initiate + + System.String + + System.String + + + None + + + username + + The JumpCloud username of a user to deploy a certificate + + System.String + + System.String + + + None + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + Start-DeployUserCerts -type "New" -forceInvokeCommands + + This command initiates the deployment of new user certificates and forces the invocation of generated commands on the systems. + + + + + + Online Version: + / + + + + + + Start-GenerateRootCert + Start + GenerateRootCert + + This function generates a root certificate for the JumpCloud Radius module, allowing you to create or replace the root certificate as needed. + + + + This function generates a root certificate for the JumpCloud Radius module. It allows you to specify whether to create a new certificate, replace an existing one, or renew the current certificate. The function can also handle the root certificate key password and force replacement of existing certificates if specified. + + + + Start-GenerateRootCert + + certKeyPassword + + The root certificate key password + + System.String + + System.String + + + None + + + force + + When specified, this parameter will replace certificates if they already exist on the current filesystem + + + System.Management.Automation.SwitchParameter + + + False + + + generateType + + Select an option to generate or replace the root certificate + + + New + Replace + Renew + + System.String + + System.String + + + None + + + + + + + certKeyPassword + + The root certificate key password + + System.String + + System.String + + + None + + + force + + When specified, this parameter will replace certificates if they already exist on the current filesystem + + System.Management.Automation.SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + generateType + + Select an option to generate or replace the root certificate + + System.String + + System.String + + + None + + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + Start-GenerateRootCert -generateType "New" -certKeyPassword "your_password" -force + + This command generates a new root certificate for the JumpCloud Radius module, using the specified key password and forcing replacement of any existing certificates. + + + + + + Online Version: + / + + + + + + Start-GenerateUserCerts + Start + GenerateUserCerts + + This function generates user certificates for JumpCloud Managed Users, allowing you to specify the type of certificate generation and whether to replace existing certificates. + + + + This function generates user certificates for JumpCloud Managed Users. It allows you to specify the type of certificate generation, such as generating certificates for all users, new users, by username, or for users with expiring certificates. The function can also replace existing certificates if specified. + + + + Start-GenerateUserCerts + + forceReplaceCerts + + When specified, this parameter will replace certificates if they already exist on the current filesystem + + + System.Management.Automation.SwitchParameter + + + False + + + + type + + 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". + + + All + New + ByUsername + ExpiringSoon + + System.String + + System.String + + + None + + + username + + The JumpCloud username of an individual user + + System.String + + System.String + + + None + + + + + + forceReplaceCerts + + When specified, this parameter will replace certificates if they already exist on the current filesystem + + System.Management.Automation.SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + + type + + 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". + + System.String + + System.String + + + None + + + username + + The JumpCloud username of an individual user + + System.String + + System.String + + + None + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + Start-GenerateUserCerts -type "New" + + This command generates user certificates for users who have not yet had certificates generated. + + + + + + Online Version: + / + + + + + + Start-MonitorCertDeployment + Start + MonitorCertDeployment + + This function monitors the deployment of certificates for JumpCloud Managed Users, ensuring that the deployment process is tracked and any issues are logged. + + + + This function monitors the deployment of certificates for JumpCloud Managed Users. It is designed to track the progress of certificate deployment. + + + + Start-MonitorCertDeployment + + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + Start-MonitorCertDeployment + + This command starts monitoring the certificate deployment process for JumpCloud Managed Users. + + + + + + Online Version: + / + + + + + + Start-RadiusDeployment + Start + RadiusDeployment + + This function is the root menu for the GUI portion of the JumpCloud Radius module. It provides a user interface for managing Radius deployments, including generating root certificates, user certificates, and monitoring deployments. + + + + This function is the root menu for the GUI portion of the JumpCloud Radius module. It provides a user interface for managing Radius deployments, including generating root certificates, user certificates, and monitoring deployments. The GUI allows users to easily navigate through various options related to Radius management without needing to use command-line parameters. + + + + Start-RadiusDeployment + + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + Start-RadiusDeployment + + This command starts the GUI for managing Radius deployments, allowing users to generate root certificates, user certificates, and monitor deployments through a user-friendly interface. + + + + + + Online Version: + / + + + + + + Update-JCRModule + Update + JCRModule + + This function updates the JumpCloud Radius module, ensuring that the latest configurations and settings are applied. + + + + This function will check if there are any updates available for the JumpCloud Radius module and apply them if necessary. It can also bypass user prompts if the `-Force` parameter is specified. + + + + Update-JCRModule + + Repository + + Set the PSRepository + + System.String + + System.String + + + None + + + Force + + ByPasses user prompts. + + + System.Management.Automation.SwitchParameter + + + False + + + + + + + Force + + ByPasses user prompts. + + System.Management.Automation.SwitchParameter + + System.Management.Automation.SwitchParameter + + + False + + + + Repository + + Set the PSRepository + + System.String + + System.String + + + None + + + + + + None + + + + + + + + + + System.Object + + + + + + + + + + + + + + -------------------------- Example 1 -------------------------- + Update-JCRModule -Force + + This command forces the update of the JumpCloud Radius module without any user prompts. + + + + + + Online Version: + / + + + + + diff --git a/scripts/automation/Radius/en-Us/about_JumpCloud.Radius.help.txt b/scripts/automation/Radius/en-Us/about_JumpCloud.Radius.help.txt new file mode 100644 index 000000000..34d9b7fae --- /dev/null +++ b/scripts/automation/Radius/en-Us/about_JumpCloud.Radius.help.txt @@ -0,0 +1,26 @@ +TOPIC + about_jumpcloud.radius + +SHORT DESCRIPTION + PowerShell module to generate and manage RADIUS certs for JumpCloud Managed + Users. + +LONG DESCRIPTION + This module provides cmdlets for generating and managing RADIUS certificates + for users in JumpCloud. It simplifies the process of creating, deploying, + and renewing certificates, ensuring secure authentication for clients + connecting to JumpCloud Radius-Backed networks. + +EXAMPLES + # Open the GUI menu to begin the process of generating and managing RADIUS certs. + Start-RadiusDeployment + +TROUBLESHOOTING NOTE + This module requires the JumpCloud Module to be installed and configured. + Ensure you have the necessary permissions and API keys set up in your + JumpCloud account. + Please view the reademe.md for this module for more information on how to + set up the environment and prerequisites. + +SEE ALSO +KEYWORDS diff --git a/scripts/automation/Radius/images/command_list.png b/scripts/automation/Radius/images/command_list.png new file mode 100644 index 000000000..5d2b41031 Binary files /dev/null and b/scripts/automation/Radius/images/command_list.png differ diff --git a/scripts/automation/Radius/images/create_task.png b/scripts/automation/Radius/images/create_task.png new file mode 100644 index 000000000..22c8bed99 Binary files /dev/null and b/scripts/automation/Radius/images/create_task.png differ diff --git a/scripts/automation/Radius/images/deployMenu.png b/scripts/automation/Radius/images/deployMenu.png new file mode 100644 index 000000000..f09ad2f8b Binary files /dev/null and b/scripts/automation/Radius/images/deployMenu.png differ diff --git a/scripts/automation/Radius/images/expireCert.png b/scripts/automation/Radius/images/expireCert.png index 8a10099df..5ba786f8b 100644 Binary files a/scripts/automation/Radius/images/expireCert.png and b/scripts/automation/Radius/images/expireCert.png differ diff --git a/scripts/automation/Radius/images/expireCertFromWindow.png b/scripts/automation/Radius/images/expireCertFromWindow.png new file mode 100644 index 000000000..e4fcca051 Binary files /dev/null and b/scripts/automation/Radius/images/expireCertFromWindow.png differ diff --git a/scripts/automation/Radius/images/log_example.png b/scripts/automation/Radius/images/log_example.png new file mode 100644 index 000000000..68698ca68 Binary files /dev/null and b/scripts/automation/Radius/images/log_example.png differ diff --git a/scripts/automation/Radius/images/mainMenu.png b/scripts/automation/Radius/images/mainMenu.png index 956dc0e58..4d5e43a27 100644 Binary files a/scripts/automation/Radius/images/mainMenu.png and b/scripts/automation/Radius/images/mainMenu.png differ diff --git a/scripts/automation/Radius/images/mainMenuNoCA.png b/scripts/automation/Radius/images/mainMenuNoCA.png index add306174..aa1eae043 100644 Binary files a/scripts/automation/Radius/images/mainMenuNoCA.png and b/scripts/automation/Radius/images/mainMenuNoCA.png differ diff --git a/scripts/automation/Radius/images/mgr_running.png b/scripts/automation/Radius/images/mgr_running.png new file mode 100644 index 000000000..d278cc9b6 Binary files /dev/null and b/scripts/automation/Radius/images/mgr_running.png differ diff --git a/scripts/automation/Radius/images/pass_prompt.png b/scripts/automation/Radius/images/pass_prompt.png new file mode 100644 index 000000000..fe67a26d0 Binary files /dev/null and b/scripts/automation/Radius/images/pass_prompt.png differ diff --git a/scripts/automation/Radius/images/task_action.png b/scripts/automation/Radius/images/task_action.png new file mode 100644 index 000000000..803c6a7a7 Binary files /dev/null and b/scripts/automation/Radius/images/task_action.png differ diff --git a/scripts/automation/Radius/images/task_running.png b/scripts/automation/Radius/images/task_running.png new file mode 100644 index 000000000..d3ad316c1 Binary files /dev/null and b/scripts/automation/Radius/images/task_running.png differ diff --git a/scripts/automation/Radius/images/task_schedule.png b/scripts/automation/Radius/images/task_schedule.png new file mode 100644 index 000000000..65f87181e Binary files /dev/null and b/scripts/automation/Radius/images/task_schedule.png differ diff --git a/scripts/automation/Radius/images/user_cert_list.png b/scripts/automation/Radius/images/user_cert_list.png new file mode 100644 index 000000000..bbf4e6701 Binary files /dev/null and b/scripts/automation/Radius/images/user_cert_list.png differ diff --git a/scripts/automation/Radius/multi_group_radius.ps1 b/scripts/automation/Radius/multi_group_radius.ps1 new file mode 100644 index 000000000..ec939f16d --- /dev/null +++ b/scripts/automation/Radius/multi_group_radius.ps1 @@ -0,0 +1,130 @@ +# define the PSD1 path: +$psd1Path = "$PSScriptRoot/JumpCloud.Radius.psd1" +$logPath = "$PSScriptRoot/log.txt" +# define data file path: +$dataFilePath = "$PSScriptRoot/data/radiusMembers.json" +$certHashFilePath = "$PSScriptRoot/data/certHash.json" + +Function Write-ToLog { + [CmdletBinding()] + Param + ( + [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)][ValidateNotNullOrEmpty()][Alias("LogContent")][string]$Message + , [Parameter(Mandatory = $false)][Alias('LogPath')][string]$Path = "$logPath" + ) + Begin { + $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + # If attempting to write to a log file in a folder/path that doesn't exist create the file including the path. + If (!(Test-Path $Path)) { + Write-Verbose "Creating $Path." + New-Item $Path -Force -ItemType File + } + # check that the log file is not too large: + $currentLog = get-item $path + if ($currentLog.Length -ge 5000000) { + # if log is larger than 5MB, rename the log to log.old.txt and create a new log file + copy-item -path $path -destination "$path.old" -force + New-Item $Path -Force -ItemType File + } + + } + process { + Switch ($Level) { + 'Error' { + Write-Error $Message + $LevelText = 'ERROR:' + } + 'Warn' { + Write-Warning $Message + $LevelText = 'WARNING:' + } + 'Info' { + Write-Verbose $Message + $LevelText = 'INFO:' + } + } + } + end { + # Write log entry to $Path + "$FormattedDate $LevelText $Message" | Out-File -FilePath $Path -Append + } +} + +Write-ToLog -Message ('########### Begin Radius Deployment ###########') + +Write-ToLog -Message ('Begin setting environment variables') +If (!(Test-Path "$PSScriptRoot/keyCert.encrypted")) { + throw "The keyCert.encrypted file does not exist at path: $PSScriptRoot/keyCert.encrypted" +} +If (!(Test-Path "$PSScriptRoot/key.encrypted")) { + throw "The key.encrypted file does not exist at path: $PSScriptRoot/key.encrypted" +} + +$EncryptedCertData = Get-Content "$PSScriptRoot/keyCert.encrypted" +$env:certKeyPassword = $EncryptedCertData | ConvertTo-SecureString | ConvertFrom-SecureString -AsPlainText +$EncryptedData = Get-Content "$PSScriptRoot/key.encrypted" +$env:JCAPIKEY = $EncryptedData | ConvertTo-SecureString | ConvertFrom-SecureString -AsPlainText +# validate that the JumpCloud API key is set as an ENV var +if ( -not $env:certKeyPassword) { + throw "the Cert Key Password is not set, please set the cert key password as an Env variable" +} +# validate API key +if ( -not $env:JCAPIKEY) { + throw "the Api Key is not set, please set the API key as an Env variable" +} else { + Write-ToLog -Message ("Connecting to JumpCloud Organization") + import-module JumpCloud + Connect-JCOnline -JumpCloudApiKey $env:JCAPIKEY -force +} + +# Define list of Radius User Group IDs: +$radiusUserGroups = @( + @{"US-Radius" = '5f3171a9232e1113939dd6a2' } +) + +# For each group, update the config and +foreach ($radiusGroup in $radiusUserGroups) { + <# $currentItemName is the current item #> + Write-ToLog -Message ("Processing Radius User Group: $($radiusGroup.keys) | $($radiusGroup.values) ") + Write-Warning "Processing Radius User Group: $($radiusGroup.keys) | $($radiusGroup.values) " + # Update the userGroupID: + Set-JCRConfig -userGroup $radiusGroup.values + # remove the radius members data file: + if (Test-Path -Path $dataFilePath) { + Remove-Item $dataFilePath -Force + } + # remove the cert hash data file: + if (Test-Path -Path $certHashFilePath) { + Remove-Item $certHashFilePath -Force + } + # force import the radius module + Import-Module "$psd1Path" -Force + # this will generate a new user-to-association report and update the cached data of your radius group membership + Write-ToLog -Message ("Begin updating global variables") + Get-JCRGlobalVars -force *>> $logPath + + # # next generate user certificates for "new" users only — users who have not yet had a certificate generated + Write-ToLog -Message ("Begin Certificate Generation") + Start-GenerateUserCerts -type "New" *>> $logPath + Write-ToLog -Message ("Finished Certificated Generation") + + # # Some users will have a cert already but it might be expiring soon, if those users are set to expire within 15 days, generate a new cert + Write-ToLog -Message ("Begin Replacement of Certs Expiring Soon") + Start-GenerateUserCerts -type ExpiringSoon -forceReplaceCerts *>> $logPath + Write-ToLog -Message ("Finished Replacement of Certs Expiring Soon") + + Write-ToLog -Message ("Begin Certificate Deployment") + # # distribute those new certificates + Start-DeployUserCerts -type "New" -forceInvokeCommands *>> $logPath + Write-ToLog -Message ("End Certificate Deployment") + + # # Write Report + if (-Not (test-path -Path "$PSScriptRoot/reports/")) { + New-Item -Path "$PSScriptRoot/" -ItemType Directory -Name reports + } + Write-ToLog -Message ("Begin Report Generation") + Get-JCRCertReport -ExportFilePath "$PSScriptRoot/reports/$($radiusGroup.keys)_report.csv" + Write-ToLog -Message ("End Report Generation") +} +Write-ToLog -Message ('########### End Radius Deployment ###########') +exit \ No newline at end of file diff --git a/scripts/automation/Radius/readme.md b/scripts/automation/Radius/readme.md index daa41fd0c..94a972fe4 100644 --- a/scripts/automation/Radius/readme.md +++ b/scripts/automation/Radius/readme.md @@ -4,14 +4,15 @@ This set of PowerShell automations are designed to help administrators generate ## Requirements -This automation has been tested with OpenSSL 3.0.7. OpenSSL 3.x.x is required to generate the Radius Authentication user certificates. The following items are required to use this automation workflow +This automation has been tested with OpenSSL 3.1.1. OpenSSL 3.x.x is required to generate the Radius Authentication user certificates. The following items are required to use this automation workflow - PowerShell 7.x.x ([PowerShell 7](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.3)) -- OpenSSL 3.x.x (Tested with 3.0.7) (see macOS/ Windows requirements below) +- OpenSSL 3.x.x (Tested with 3.1.1) (see macOS/ Windows requirements below) - [JumpCloud PowerShell Module](https://www.powershellgallery.com/packages/JumpCloud) - Certificate Authority (CA) (either from a vendor or self-generated) -- Variables in `config.ps1` updated +- Module Settings Configured with `Set-JCRConfig` cmdlet +- JumpCloud Organization API Key (Read/ Write Access Required) - JumpCloud API Key Set (Read/ Write Access Required) - JumpCloud ORG ID Set - JumpCloud User Group containing users and assigned to a Radius Server @@ -22,14 +23,14 @@ macOS ships with a version of OpenSSL titled LibreSSL. LibreSSL is sufficient to To install the latest version of OpenSSL on mac, install the [Homebrew package manager](https://brew.sh/) and install the following [formulae](https://formulae.brew.sh/formula/openssl@3) -Some packages or applications in macOS rely on the pre-configured LibreSSL distribution. To use the Homebrew distribution of OpenSSL in this project, simply change the `$openSSLBinary` variable to point to the Homebrew bin location ex: +Some packages or applications in macOS rely on the pre-configured LibreSSL distribution. To use the Homebrew distribution of OpenSSL in this project, simply change the `openSSLBinary` setting with `Set-JCRConfig` to point to the Homebrew bin location ex: -In `Config.ps1` change `$opensslBinary` to point to `'/usr/local/Cellar/openssl@3/3.0.7/bin/openssl'` +Update the `openSSLBinary` to point to `'/usr/local/Cellar/openssl@3/3.1.1/bin/openssl'`. ex: ```powershell -$opensslBinary = '/usr/local/Cellar/openssl@3/3.0.7/bin/openssl' +Set-JCRConfig -openSSLBinary '/opt/homebrew/opt/openssl@3/bin/openssl' ``` ### Windows Requirements @@ -63,46 +64,148 @@ After installing PowerShell 7.x.x, install the [JumpCloud PowerShell Module](htt At the time of this writing JumpCloud Module 2.1.3 was the latest version. Please ensure you are at least running this version of the PowerShell Module. -### Set the Radius Config File +### Set the Radius Configuration Settings -Before Running the `Start-RadiusDeployment.ps1` script, the environment variables for your JumpCloud Organization must first be set. Open the `config.ps1` file with a text editor. +Before Running the `Start-RadiusDeployment` function, the environment variables for your JumpCloud Organization must first be set. All of the required settings can be set at once or individually. -#### Set Your API Key ID +To set all of the settings at once, run the `Set-JCRConfig` cmdlet with a hashtable containing all of the required settings. The following example shows how to set all of the required settings in one command: -Change the variable `$JCAPIKEY` to an [API Key](https://support.jumpcloud.com/s/article/jumpcloud-apis1) from an administrator in your JumpCloud Tenant. An administrator API Key with at least [read/write access](https://support.jumpcloud.com/support/s/article/JumpCloud-Roles) is required. +```pwsh +$settings = @{ + radiusDirectory = "/Users/username/RADIUS" + certType = "UsernameCn" + certSubjectHeader @{ + CountryCode = "Your_Country_Code" + StateCode = "Your_State_Code" + Locality = "Your_City" + Organization = "Your_Organization_Name" + OrganizationUnit = "Your_Organization_Unit" + CommonName = "Your_Common_Name" + } + certSecretPass = "secret1234!" + networkSSID = "Your_SSID" + userGroup = "5f3171a9232e1113939dd6a2" + openSSLBinary = '/opt/homebrew/bin/openssl' +} + +Set-JCRConfig @settings +``` + +#### Set or update the Radius Directory + +The Radius Directory is the location where all generated CAs and User Certificates will be stored. This directory should be set to a location on your system where you have read/write access. + +To set the Radius Directory, run the following command in a PowerShell terminal window: + +```powershell +Set-JCRConfig -radiusDirectory '/Users/username/RADIUS' +``` + +#### Set or update the User Cert Validity Days + +The user certificate validity days is the number of days a user certificate will be valid for before it expires. The default value is 365 days. + +To set the user certificate validity days, run the following command in a PowerShell terminal window: + +```powershell +Set-JCRConfig -userCertValidityDays 365 +``` + +#### Set or update the CA Cert Validity Days + +The CA certificate validity days is the number of days a CA certificate will be valid for before it expires. The default value is 1095 days. + +To set the CA certificate validity days, run the following command in a PowerShell terminal window: + +```powershell +Set-JCRConfig -caCertValidityDays 1095 +``` + +#### Set or update the Certificate Expiration Warning Days + +The certificate expiration warning days is the number of days before a user certificate expires that a warning will be displayed in the main menu. The default value is 15 days. + +To set the certificate expiration warning days, run the following command in a PowerShell terminal window: + +```powershell +Set-JCRConfig -certExpirationWarningDays 15 +``` + +#### Set or update the certificate secret password -#### Set Your Organization ID +The certificate secret password is used to protect the private key of the user certificates. This password is required when generating user certificates and should be kept secure. -Change the variable `$JCORGID` to the [organization ID value](https://support.jumpcloud.com/s/article/Settings-in-the-JumpCloud-Admin-Portal#AccessOrgID) from your JumpCloud Tenant. +To set the certificate secret password, run the following command in a PowerShell terminal window: -#### Set Your User Group ID +```powershell +Set-JCRConfig -certSecretPass 'Your_Secret_Password' +``` -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. +#### Set or update the Radius User Group + +To change the JumpCloud user group with access to the Radius server use the `Set-JCRConfig` cmdlet. 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` +`https://console.jumpcloud.com/#/groups/user/5f3171a9232e1113939dd6a2/details` + +The ID of the selected userGroup is the 24 character string between `/user/` and `/details`: `5f3171a9232e1113939dd6a2` + +To set the user group ID, run the following command in a PowerShell terminal window: -The ID of the selected userGroup is the 24 character string between `/user/` and `/details`: `5f808a1bb544064831f7c9fb` +```powershell +Set-JCRConfig -userGroup '5f3171a9232e1113939dd6a2' +``` -#### Set your network SSID Name +#### Set or update the network SSID Name -Change the variable `$NETWORKSSID` to the name of the SSID network your clients will connect to. On macOS hosts, the user certificate will be set to automatically authenticate to this SSID when the end user selects the WiFi Network. Multiple SSIDs can be provided as a single string with SSID names separated by a space, ex: "CorpNetwork_Denver CorpNetwork_Boulder". **Note: The SSID and user certificate are only associated with macOS system commands are generated. This parameter does not affect windows generated commands** +On macOS hosts, the user certificate will be set to automatically authenticate to this SSID when the end user selects the WiFi Network. Multiple SSIDs can be provided as a single string with SSID names separated by a semicolon, ex: "CorpNetwork_Denver;CorpNetwork_Boulder;CorpNetwork_Boulder 5G;Guest Network". **Note: The SSID and user certificates are only associated with macOS system commands. This parameter does not affect windows generated commands** + +To set the network SSID, run the following command in a PowerShell terminal window: + +```powershell +Set-JCRConfig -networkSSID 'Your_SSID' +``` #### Set the openSSL Binary location Depending on the host system and how OpenSSL is installed, this variable can either point to a path or call the binary with just the name `openssl`. -[For macOS systems](#macos-requirements), this will likely need to be set to the openSSL binary installation path like `'/opt/homebrew/opt/openssl@3/bin/openssl'` or `'/usr/local/Cellar/openssl@3/3.0.7/bin/openssl'` if installed through Homebrew. +[For macOS systems](#macos-requirements), this will likely need to be set to the openSSL binary installation path like `'/opt/homebrew/opt/openssl@3/bin/openssl'` or `'/usr/local/Cellar/openssl@3/3.1.1/bin/openssl'` if installed through Homebrew. + +Else, for Windows systems, installing OpenSSL and setting an environment variable described in [Windows Requirements](#Windows-Requirements) should be sufficient. -Else, for Windows systems, installing OpenSSL and setting an environment variable described in [Windows Requirements](#Windows-Requirements) should be sufficient. (i.e no additional changes to `$opensslBinary` necessary) +To set the OpenSSL binary location, run the following command in a PowerShell terminal window: + +```powershell +Set-JCRConfig -openSSLBinary '/opt/homebrew/opt/openssl@3/bin/openssl' +``` #### Set Your Certificate Subject Headers -Change the default values provided in the `$Subj` variable to Country, State, Locality, Organization, Organization Unit and Common Name values for your organization. **Note: subject headers must not contain spaces** +Set the Country, State, Locality, Organization, Organization Unit and Common Name subject headers for your organization. **Note: subject headers must not contain spaces** + +To set the subject headers, run the following command in a PowerShell terminal window: + +```powershell +Set-JCRConfig -certSubjectHeader @{ + CountryCode = "Your_Country_Code" + StateCode = "Your_State_Code" + Locality = "Your_City" + Organization = "Your_Organization_Name" + OrganizationUnit = "Your_Organization_Unit" + CommonName = "Your_Common_Name" +} +``` #### Set Desired User Certificate Type -Change the `$certType` variable to either `EmailSAN`, `EmailDN` or `UsernameCn` +Set the type of user cert to generate to either `EmailSAN`, `EmailDN` or `UsernameCn` + +To set the user certificate type, run the following command in a PowerShell terminal window: + +```powershell +Set-JCRConfig -certType 'UsernameCn' +``` ##### Email Subject Alternative Name (EmailSAN) @@ -162,6 +265,12 @@ A Certificate Authority (CA) is required for passwordless Radius Authentication. The first option in the menu will present options to generate a self-signed CA. The resulting file `radius_ca_cert.pem` in the `projectDirectory/Radius/Cert` directory. When generating a self signed CA, a password prompt is displayed, this password is used to protect the CA from unauthorized access. Choose a secure but memorable password, during the session this password will be stored as an environment variable as it is required to generate user certificates. +#### Renewing a self-signed certificate + +The second option in the root certificate menu allows you to renew the current CA certificate. During the renewal process, you will be prompted to enter the password for the current private key. This ensures that administrators can renew expiring CA certificates while maintaining the ability to authenticate user certificates issued by the previous CA. The renewed certificate will retain the same serial number and CA headers. + +![Root Cert Menu](./images/rootCertMenu.png) + #### Importing a certificate To Import your own CA, the certificate and key files can be copied to the `projectDirectory/Radius/Cert` directory. **Note: Please ensure the certificate and key name ends with `key.pem` and `cert.pem` (ex. `radius_ca_cert.pem` or `radius_ca_key.pem`)** @@ -182,17 +291,27 @@ 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 `userGroup` setting 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: +If local user certificates are set to expire within 15 days, a notification is displayed on the main menu and the certificate generation window: ![certs due to expire](./images/expireCert.png) +Selection option 2 from the main menu presents the various choices to generate/ re-generate user certificates: + +![certs due to expire from window](./images/expireCertFromWindow.png) + At any time user certificates can be manually removed from the `/projectDirectory/Radius/UserCerts/` directory and regenerated using option 2 from the main menu. User certificates can be continuously re-applied to devices using option 3 to distribute user certificates. ## Certificate Distribution -Option 3 in the main menu will enable admins to distribute user certificates to end user devices. Commands will be generated in your JumpCloud Tenant for each user in the Radius User Group and their corresponding system associations. This script will prompt you to kick off the generated commands. If the commands are invoked, they should be queued for all users in the Radius User Group. These commands are queued with a TTL timeout of 10 days — meaning that if the end user device is offline when the command is queued, for 10 days, the command will sit in the JumpCLoud console and wait for the device to come online before attempting to run. +Option 3 in the main menu will enable admins to distribute user certificates to end user devices. + +![radius re-issue workflow](./images/deployMenu.png) + +Windows and macOS commands will be generated for each user in the Radius User Group. + +This script will prompt you to kick off the generated commands. Commands are queued with a TTL timeout of 10 days — meaning that if the end user device is offline when the command is queued, for 10 days, the command will sit in the JumpCloud console and wait for the device to come online before attempting to run. On the device, certificates are replaced if a command is sent to a device with a newer certificate. i.e. @@ -230,7 +349,7 @@ After a user's certificate has been distributed to a system, those users can the ### macOS -If the `$NETWORKSSID` variable in `Config.ps1` was specified, macOS users will only be prompted once to let `eapolclient` access the private key from the installed certificate. If the end user selects `Always Allow`, the'll not be prompted to enter their password for the entire life cycle of the user certificate, only when new certificates are deployed will end users have to re-enter their login password. +If the `networkSSID` setting is set with `Set-JCRConfig`, macOS users will only be prompted once to let `eapolclient` access the private key from the installed certificate. If the end user selects `Always Allow`, the'll not be prompted to enter their password for the entire life cycle of the user certificate, only when new certificates are deployed will end users have to re-enter their login password. In macOS a user simply needs to select the radius network from the wireless networks dialog prompt. A prompt to select a user certificate should be displayed, select the user certificate from the drop down menu and click "OK" @@ -266,6 +385,109 @@ Before Connecting, users can view the authentication source. Click "Connect" to The user should then be connected to the radius network. +## Automation Scripts + +The generation and distribution of certificates with Radius Certificate Module can be automated with the [multi_group_radius](./multi_group_radius.ps1) script found in this repository. This script should serve as just an example of how to automate this, there are numerous ways in which this can be achieved. + +For the purpose of this example, we'll assume: + +- A self-hosted Windows System is acting as a scripting server +- All of the requirements from the Radius Certificate Module have been met/ software installed on that server +- A scheduled task can be run on this device + +### Automation Setup + +In order to automate these scripts, our scripted example solution needs to know how to get the values of your JumpCloud Organization API Key and the secret password saved when the CA was generated. In the following example, these values will be saved as secure strings. + +On the self-hosted Windows system, create a location on the scripting server to host the Radius project directory. Within this directory and signed in as the user administering these certificates, open a powershell 7 terminal window. + +CD into this location root and run the following + +```powershell +$APIKeyString = "yourAPIKEY" +$APIKeySecureString = ConvertTo-SecureString -String $APIKeyString -AsPlainText -Force +$EncryptedKey = ConvertFrom-SecureString $APIKeySecureString +$EncryptedKey | Out-File -FilePath ".\key.encrypted" +``` + +This should save a "key.encrypted" file containing your API Key to the root of the Radius directory. When the Multi_Group_Radius script runs it'll connected to your organization using the contents of this file and the authentication data stored in the scheduled task. + +Encrypt the CA key password. Copy the following into the same PowerShell window: + +```powershell +$CertKeyPass = "yourCertPass" +$CertKeySecurePass = ConvertTo-SecureString -String $CertKeyPass -AsPlainText -Force +$EncryptedKey = ConvertFrom-SecureString $CertKeySecurePass +$EncryptedKey | Out-File -FilePath ".\keyCert.encrypted" +``` + +This should save a "keyCert.encrypted" file containing your API Key to the root of the Radius directory. This file is used to set your CA Private Key variable during the Multi_Group_Radius operation. + +If the [multi_group_radius](./multi_group_radius.ps1) script is not already in the project root, copy it to the root directory of the Radius project. + +Edit the file and name each Radius User Group you wish to generate and distribute certificates for. + +```powershell +# Define list of Radius User Group IDs: +$radiusUserGroups = @( + @{"US-Radius" = '5f3171a9232e1113939dd6a2' } + @{"US-Dairy-Farmers" = '664e50582d9c0e000143ee97' } + @{"AK-Farmers" = '5f7f418a1f247569e35070f1' } +) +``` + +The names should correspond to the userGroupIDs of each user group. Note. Multiple user groups are not required, if you only wish to automate one single group just list the single user group name and the user group ID in the $radiusUserGroups list. + +#### Running the Multi Group Radius script + +At this point, if you’ve set everything already, you should be able to just change directories into the Radius folder and run the script “MultiGroupRadius.ps1” + +The script will generate and deploy user certificates for each user in the defined groups. + +![script running](./images/mgr_running.png) + +#### Scheduling the Multi Group Radius script + +The Multi Group Radius script can be scheduled with Windows Task Scheduler + +Create a task + +![Create a task](./images/create_task.png) + +Define a trigger time for the task to run + +![Task Schedule](./images/task_schedule.png) + +Add an action to "Start A Program" +Under the Program/ script field add: `"C:\Program Files\PowerShell\7\pwsh.exe"` +Under the Add Arguments add: `-ExecutionPolicy ByPass -File "C:\Users\yourUsername\path\to\Radius\MultiGroupRadius.ps1"` + +![Task Action](./images/task_action.png) + +When you click Okay and save the task it should prompt for your user account password. Enter the value and click OK + +![Password Prompt](./images/pass_prompt.png) + +The task should run at the scheduled time. You can also kick off the task manually by pressing the run button from the task scheduler. + +![Task Running](./images/task_running.png) + +After running the task, user certs for each user group should be added to the `/Radius/UserCerts` directory: + +![User Cert List](./images/user_cert_list.png) + +Every user with a system association should have a command generated and queued for installatioon if they do not already have the current generated certificate installed. + +![Command List](./images/command_list.png) + +#### Logging + +Each time the script runs there should be a corresponding log generated in the Radius Directory titled “log.txt”. Below is a sample screenshot showing one of the two of the user groups I’ve generated/ deployed certs for. + +![Log Example](./images/log_example.png) + +New events will be appended to this log file until the log is greater than 5mb. At that point the log will be copied to a new file log.txt.old and a new log will be created to prevent excess log data from being stored. + ### Troubleshooting #### Clearing Commands Queue