Public/Utilities/Backup-JCOrganization.ps1
<#
.Synopsis Backup your JumpCloud organization to local json files .Description This function exports objects and associations from your JumpCloud organization to local json files. Association takes a significant amount of time to gather. The -Format:('csv') is slower than standard json output. .Example Backup all available JumpCloud objects and their associations PS C:\> Backup-JCOrganization -Path:('C:\Temp') .Example Backup all available JumpCloud objects and their associations to CSV (default json) PS C:\> Backup-JCOrganization -Path:('C:\Temp') -Format:('csv') .Example Backup UserGroups and Users with their associations PS C:\> Backup-JCOrganization -Path:('C:\Temp') -Type:('UserGroup','User') -Association .Example Backup UserGroups and Users without their associations PS C:\> Backup-JCOrganization -Path:('C:\Temp') -Type:('UserGroup','User') .Example Backup all available JumpCloud objects with their associations and return metadata PS C:\> $BackupJcOrganizationResults = Backup-JCOrganization -Path:('C:\Temp') -PassThru PS C:\> $BackupJcOrganizationResults.Keys PS C:\> $BackupJcOrganizationResults.User .Link https://github.com/TheJumpCloud/support/tree/master/PowerShell/JumpCloud%20Module/Docs/Backup-JCOrganization.md #> Function Backup-JCOrganization { [CmdletBinding(DefaultParameterSetName = 'All', PositionalBinding = $false)] Param( [Parameter(Mandatory)] [System.String] # File directory for backup output ${Path}, [Parameter(ParameterSetName = 'All')] [System.Management.Automation.SwitchParameter] # Backup all available types and associations ${All}, [Parameter(ParameterSetName = 'Type')] [ValidateSet('ActiveDirectory', 'AppleMdm', 'Application', 'AuthenticationPolicy', 'Command', 'Directory', 'Group', 'GSuite', 'IPList', 'LdapServer', 'Office365', 'Organization', 'Policy', 'RadiusServer', 'SoftwareApp', 'System', 'SystemGroup', 'User', 'UserGroup')] [System.String[]] # JumpCloud objects that you want to backup ${Type}, [Parameter(ParameterSetName = 'Type')] [System.Management.Automation.SwitchParameter] # Use to backup relationship data between diffrent types ${Association}, [Parameter()] [ValidateSet('json', 'csv')] [System.String] # The format of the output files ${Format} = 'json', [Parameter()] [System.Management.Automation.SwitchParameter] # Return object metadata to pipeline ${PassThru} ) Begin { If ([System.String]::IsNullOrEmpty($env:JCApiKey) -or [System.String]::IsNullOrEmpty($env:JCOrgId)) { Connect-JCOnline } $TimerTotal = [Diagnostics.Stopwatch]::StartNew() $Date = Get-Date -Format:("yyyyMMddTHHmmssffff") $ChildPath = "JumpCloud_$($Date)" $Path = Resolve-Path -Path:($Path) $TempPath = Join-Path -Path:($Path) -ChildPath:($ChildPath) $ArchivePath = Join-Path -Path:($Path) -ChildPath:("$($ChildPath).zip") $OutputHash = [ordered]@{} # If the backup directory does not exist, create it If (-not (Test-Path $TempPath)) { $BackupLocation = New-Item -Path:($TempPath) -Name:$($TempPath.BaseName) -ItemType:('directory') $OutputHash.Add('BackupLocation', $BackupLocation) } # When -All is provided use all type options and Association $Types = If ($PSCmdlet.ParameterSetName -eq 'All') { $Association = $true (Get-Command $MyInvocation.MyCommand).Parameters.Type.Attributes.ValidValues } Else { $Type } # Map to define how JCAssociation & JcSdk types relate $JcTypesMap = @{ ActiveDirectory = [PSCustomObject]@{Name = 'active_directory'; AssociationTargets = @('user', 'user_group'); }; AppleMdm = [PSCustomObject]@{Name = 'apple_mdm'; AssociationTargets = @(); }; Application = [PSCustomObject]@{Name = 'application'; AssociationTargets = @('user', 'user_group'); }; AuthenticationPolicy = [PSCustomObject]@{Name = 'authentication_policy'; AssociationTargets = @(); }; Command = [PSCustomObject]@{Name = 'command'; AssociationTargets = @('system', 'system_group'); }; Directory = [PSCustomObject]@{Name = 'directory'; AssociationTargets = @(); }; Group = [PSCustomObject]@{Name = 'group'; AssociationTargets = @(); }; GSuite = [PSCustomObject]@{Name = 'g_suite'; AssociationTargets = @( 'user', 'user_group'); }; IPList = [PSCustomObject]@{Name = 'ip_list'; AssociationTargets = @(); }; LdapServer = [PSCustomObject]@{Name = 'ldap_server'; AssociationTargets = @('user', 'user_group'); }; Office365 = [PSCustomObject]@{Name = 'office_365'; AssociationTargets = @('user', 'user_group'); }; Organization = [PSCustomObject]@{Name = 'organization'; AssociationTargets = @(); }; Policy = [PSCustomObject]@{Name = 'policy'; AssociationTargets = @( 'system', 'system_group'); }; RadiusServer = [PSCustomObject]@{Name = 'radius_server'; AssociationTargets = @('user', 'user_group'); }; SoftwareApp = [PSCustomObject]@{Name = 'software_app'; AssociationTargets = @( 'system', 'system_group'); }; System = [PSCustomObject]@{Name = 'system'; AssociationTargets = @( 'command', 'policy', 'system_group', 'user', 'user_group'); }; SystemGroup = [PSCustomObject]@{Name = 'system_group'; AssociationTargets = @( 'command', 'policy', 'system', 'user', 'user_group'); }; User = [PSCustomObject]@{Name = 'user'; AssociationTargets = @('active_directory', 'application', 'g_suite', 'ldap_server', 'office_365', 'radius_server', 'system', 'system_group', 'user_group'); }; UserGroup = [PSCustomObject]@{Name = 'user_group'; AssociationTargets = @('active_directory', 'application', 'g_suite', 'ldap_server', 'office_365', 'radius_server', 'system', 'system_group', 'user'); }; } } Process { $TimerObject = [Diagnostics.Stopwatch]::StartNew() # Foreach type start a new job and retrieve object records $ObjectJobs = @() ForEach ($JumpCloudType In $Types) { $SourceTypeMap = $JcTypesMap.GetEnumerator() | Where-Object { $_.Key -eq $JumpCloudType } $ObjectBaseName = "{0}" -f $SourceTypeMap.Key $ObjectFileName = "{0}.{1}" -f $ObjectBaseName, $Format $ObjectFullName = "{0}/{1}" -f $TempPath, $ObjectFileName $ObjectJobs += Start-Job -ScriptBlock:( { Param ($SourceTypeMap, $ObjectBaseName, $ObjectFileName, $ObjectFullName, $Format, $Debug); # Logic to handle directories $Command = If ($SourceTypeMap.Key -eq 'GSuite') { "(Get-JcSdkDirectory | Where-Object { `$_.Type -eq '$($SourceTypeMap.Value.Name)' }) | ForEach-Object { Get-JcSdk$($SourceTypeMap.Key) -Id:(`$_.Id)}" } ElseIf ($SourceTypeMap.Key -eq 'Office365') { "(Get-JcSdkDirectory | Where-Object { `$_.Type -eq '$($SourceTypeMap.Value.Name)' }) | ForEach-Object { Get-JcSdk$($SourceTypeMap.Key) -$($SourceTypeMap.Key)Id:(`$_.Id)}" } ElseIf ($SourceTypeMap.Key -eq 'Organization') { "Get-JcSdk{0} -Id:('{1}')" -f $SourceTypeMap.Key, $env:JCOrgId } Else { "Get-JcSdk{0}" -f $SourceTypeMap.Key } If ($PSBoundParameters.Debug) { Write-Host ("DEBUG: Running: $Command") -ForegroundColor:('Yellow') } $Result = Invoke-Expression -Command:($Command) If (-not [System.String]::IsNullOrEmpty($Result)) { # Write output to file If ($Format -eq 'json') { $Result | ConvertTo-Json -Depth:(100) | Out-File -FilePath:($ObjectFullName) -Force } ElseIf ($Format -eq 'csv') { # Convert object properties of objects to compressed json strings $Result | ForEach-Object { $NewRecord = [PSCustomObject]@{} $_.PSObject.Properties | ForEach-Object { If ($_.TypeNameOfValue -like '*.Models.*' -or $_.TypeNameOfValue -like '*Object*' -or $_.TypeNameOfValue -like '*String`[`]*') { Add-Member -InputObject:($NewRecord) -MemberType:('NoteProperty') -Name:($_.Name) -Value:($_.Value | ConvertTo-Json -Depth:(100) -Compress) } Else { Add-Member -InputObject:($NewRecord) -MemberType:('NoteProperty') -Name:($_.Name) -Value:($_.Value) } } Return $NewRecord } | Export-Csv -NoTypeInformation -Path:($ObjectFullName) -Force } Else { Write-Error ("Unknown format: $Format") } # TODO: Potential use for restore function #| ForEach-Object { $_ | Select-Object *, @{Name = 'JcSdkModel'; Expression = { $_.GetType().FullName } } } ` # Build hash to return data Return @{$ObjectBaseName = $Result } } }) -ArgumentList:($SourceTypeMap, $ObjectBaseName, $ObjectFileName, $ObjectFullName, $Format, $PSBoundParameters.Debug) } $ObjectJobStatus = Wait-Job -Id:($ObjectJobs.Id) $ObjectJobResults = $ObjectJobStatus | Receive-Job # Add the results of objects to outputhash results $ObjectJobResults | ForEach-Object { $_.GetEnumerator() | ForEach-Object { $OutputHash.Add($_.Key, $_.Value) } } $TimerObject.Stop() # Foreach type start a new job and retrieve object association records If ($Association) { $AssociationJobs = @() $TimerAssociations = [Diagnostics.Stopwatch]::StartNew() # Get the backup files we created earlier $BackupFiles = Get-ChildItem -Path:($TempPath) | Where-Object { $_.BaseName -in $Types } ForEach ($BackupFile In $BackupFiles) { # Type mapping lookup $SourceTypeMap = $JcTypesMap.GetEnumerator() | Where-Object { $_.Key -eq $BackupFile.BaseName } # TODO: Figure out how to make this work with x-ms-enum. # $ValidTargetTypes = (Get-Command Get-JcSdk$($SourceTypeMap.Key)Association).Parameters.Targets.Attributes.ValidValues # $ValidTargetTypes = ((Get-Command Get-JcSdk$($SourceTypeMap.Key)Association).Parameters.Targets.ParameterType.DeclaredFields | Where-Object { $_.IsPublic }).Name # Get list of valid target types from Get-JCAssociation $ValidTargetTypes = $SourceTypeMap.Value.AssociationTargets # Lookup file names in $JcTypesMap ForEach ($ValidTargetType In $ValidTargetTypes) { $TargetTypeMap = $JcTypesMap.GetEnumerator() | Where-Object { $_.Value.Name -eq $ValidTargetType } # If the valid target type matches a file name look up the associations for the SourceType and TargetType If ($TargetTypeMap.Key -in $BackupFiles.BaseName) { $AssociationBaseName = "Association-{0}To{1}" -f $SourceTypeMap.Key, $TargetTypeMap.Key $AssociationFileName = "{0}.{1}" -f $AssociationBaseName, $Format $AssociationFullName = "{0}/{1}" -f $TempPath, $AssociationFileName $AssociationJobs += Start-Job -ScriptBlock:( { Param ($SourceTypeMap, $TargetTypeMap, $BackupFile, $AssociationBaseName, $AssociationFileName, $AssociationFullName, $Format, $Debug); $AssociationResults = @() # Get content from the file $BackupRecords = If ($Format -eq 'json') { Get-Content -Path:($BackupFile.FullName) | ConvertFrom-Json } ElseIf ($Format -eq 'csv') { Import-Csv -Path:($BackupFile.FullName) } Else { Write-Error ("Unknown format: $Format") } ForEach ($BackupRecord In $BackupRecords) { # Build Command based upon source and target combinations # *Group commands take "GroupId" as a parameter vs "{Type}Id" # User associations is called Get-JcSdkUserAssociation and Get-JcSdkUserMember If (($SourceTypeMap.Value.Name -eq 'system' -and $TargetTypeMap.Value.Name -eq 'system_group') -or ($SourceTypeMap.Value.Name -eq 'user' -and $TargetTypeMap.Value.Name -eq 'user_group')) { $Command = 'Get-JcSdk{0}Member -{1}Id:("{2}")' -f $SourceTypeMap.Key, $SourceTypeMap.Key.Replace('UserGroup', 'Group').Replace('SystemGroup', 'Group'), $BackupRecord.id If ($PSBoundParameters.Debug) { Write-Host ("DEBUG: Running: $Command") -ForegroundColor:('Yellow') } $AssociationResult = Invoke-Expression -Command:($Command) If (-not [System.String]::IsNullOrEmpty($AssociationResult)) { # The direct association/"Get-JcSdk*Member" endpoints return null for FromId. So manually populate them here. $AssociationResult.Paths | ForEach-Object { $_ | ForEach-Object { If ([System.String]::IsNullOrEmpty($_.FromId)) { $_.FromId = $BackupRecord.id } # The direct association/"Get-JcSdk*Member" endpoints return null for FromType. So manually populate them here. If ([System.String]::IsNullOrEmpty($_.FromType)) { $_.FromType = $SourceTypeMap.Value.Name } } } $AssociationResults += $AssociationResult } } ElseIf (($SourceTypeMap.Value.Name -eq 'system_group' -and $TargetTypeMap.Value.Name -eq 'system') -or ($SourceTypeMap.Value.Name -eq 'user_group' -and $TargetTypeMap.Value.Name -eq 'user')) { $Command = 'Get-JcSdk{0}Membership -{1}Id:("{2}")' -f $SourceTypeMap.Key, $SourceTypeMap.Key.Replace('UserGroup', 'Group').Replace('SystemGroup', 'Group'), $BackupRecord.id If ($PSBoundParameters.Debug) { Write-Host ("DEBUG: Running: $Command") -ForegroundColor:('Yellow') } $AssociationResult = Invoke-Expression -Command:($Command) If (-not [System.String]::IsNullOrEmpty($AssociationResult)) { # The direct association/"Get-JcSdk*Membership" endpoints return null for FromId. So manually populate them here. $AssociationResult.Paths | ForEach-Object { $_ | ForEach-Object { If ([System.String]::IsNullOrEmpty($_.FromId)) { $_.FromId = $BackupRecord.id } # The direct association/"Get-JcSdk*Membership" endpoints return null for FromType. So manually populate them here. If ([System.String]::IsNullOrEmpty($_.FromType)) { $_.FromType = $SourceTypeMap.Value.Name } } } $AssociationResults += $AssociationResult } } Else { $Command = 'Get-JcSdk{0}Association -{1}Id:("{2}") -Targets:("{3}")' -f $SourceTypeMap.Key, $SourceTypeMap.Key.Replace('UserGroup', 'Group').Replace('SystemGroup', 'Group'), $BackupRecord.id, $TargetTypeMap.Value.Name If ($PSBoundParameters.Debug) { Write-Host ("DEBUG: Running: $Command") -ForegroundColor:('Yellow') } $AssociationResult = Invoke-Expression -Command:($Command) If (-not [System.String]::IsNullOrEmpty($AssociationResult)) { $AssociationResult | ForEach-Object { # The direct association/"Get-JcSdk*Association" endpoints return null for FromId. So manually populate them here. If ([System.String]::IsNullOrEmpty($_.FromId)) { $_.FromId = $BackupRecord.id } # The direct association/"Get-JcSdk*Association" endpoints return null for FromType. So manually populate them here. If ([System.String]::IsNullOrEmpty($_.FromType)) { $_.FromType = $SourceTypeMap.Value.Name } } $AssociationResults += $AssociationResult } } } If (-not [System.String]::IsNullOrEmpty($AssociationResults)) { If ($Format -eq 'json') { $AssociationResults | ConvertTo-Json -Depth:(100) | Out-File -FilePath:($AssociationFullName) -Force } ElseIf ($Format -eq 'csv') { # Convert object properties of objects to compressed json strings $AssociationResults | ForEach-Object { $NewRecord = [PSCustomObject]@{} $_.PSObject.Properties | ForEach-Object { If ($_.TypeNameOfValue -like '*.Models.*' -or $_.TypeNameOfValue -like '*Object*' -or $_.TypeNameOfValue -like '*String`[`]*') { Add-Member -InputObject:($NewRecord) -MemberType:('NoteProperty') -Name:($_.Name) -Value:($_.Value | ConvertTo-Json -Depth:(100) -Compress) } Else { Add-Member -InputObject:($NewRecord) -MemberType:('NoteProperty') -Name:($_.Name) -Value:($_.Value) } } Return $NewRecord } | Export-Csv -NoTypeInformation -Path:($AssociationFullName) -Force } Else { Write-Error ("Unknown format: $Format") } # Build hash to return data Return @{$AssociationBaseName = $AssociationResults } } }) -ArgumentList:($SourceTypeMap, $TargetTypeMap, $BackupFile, $AssociationBaseName, $AssociationFileName, $AssociationFullName, $Format, $PSBoundParameters.Debug) } } } If ($AssociationJobs) { $AssociationJobStatus = Wait-Job -Id:($AssociationJobs.Id) $AssociationResults = $AssociationJobStatus | Receive-Job # Add the results of associations to outputhash results $AssociationResults | ForEach-Object { $_.GetEnumerator() | ForEach-Object { $OutputHash.Add($_.Key, $_.Value) } } } $TimerAssociations.Stop() } } End { # Write Out Manifest [ordered]@{ date = $Date; organizationId = $env:JCOrgId; moduleVersion = @(Get-Module -Name:('JumpCloud*') -ListAvailable | Select-Object Name, Version); result = Get-ChildItem -Path:($TempPath) | Sort-Object -Property BaseName | ForEach-Object { [PSCustomObject]@{ Type = $_.BaseName Count = $OutputHash.Item($_.BaseName).Count } } } | ConvertTo-Json -Depth:(100) | Out-File -FilePath:("$($TempPath)/Manifest.json") -Force # Zip results Compress-Archive -Path:($TempPath) -CompressionLevel:('Fastest') -Destination:($ArchivePath) # Clean up temp directory If (Test-Path -Path:($ArchivePath)) { Remove-Item -Path:($TempPath) -Force -Recurse Write-Host ("Backup Success: $($ArchivePath)") -ForegroundColor:('Green') Write-Host("Backup-JCOrganization Results:") -ForegroundColor:('Green') $OutputHash.GetEnumerator() | Sort-Object -Property Name | Select-Object -Skip 1 | ForEach-Object { If ($_.Key) { Write-Host ("$($_.Key): $($_.Value.Count)") -ForegroundColor:('Magenta') } } } $TimerTotal.Stop() If ($TimerObject) { Write-Debug ("Object Run Time: $($TimerObject.Elapsed)") } If ($TimerAssociations) { Write-Debug ("Association Run Time: $($TimerAssociations.Elapsed)") } If ($TimerTotal) { Write-Debug ("Total Run Time: $($TimerTotal.Elapsed)") } If ($PassThru) { Return $OutputHash } Else { Return $OutputHash.BackupLocation } } } |