From 4add87884a38b2f0157ba73e27f9bc82ce2c7d93 Mon Sep 17 00:00:00 2001 From: Mikael Karlsson <43226266+Micke-K@users.noreply.github.com> Date: Sun, 17 Oct 2021 14:02:08 +1100 Subject: [PATCH] 3.3.0 Beta release --- CloudAPIPowerShellManagement.psd1 | Bin 8538 -> 8538 bytes CloudAPIPowerShellManagement.psm1 | 16 +- Core.psm1 | 330 ++++++++++++++++++++++++-- Extensions/Compare.psm1 | 272 ++++++++++++++++++++- Extensions/DocumentationWord.psm1 | 3 +- Extensions/IntuneAssignments.psm1 | 3 +- Extensions/MSALAuthentication.psm1 | 148 +++++++++--- Extensions/MSGraph.psm1 | 86 ++++++- README.md | 28 ++- ReleaseNotes.md | 40 ++++ Start-IntuneManagement.ps1 | 8 +- Start-WithJson.cmd | 1 + Xaml/BulkDeleteForm.xaml | 18 +- Xaml/BulkExportForm.xaml | 18 +- Xaml/BulkImportForm.xaml | 31 ++- Xaml/CompareExportOptions.xaml | 10 +- Xaml/CompareExportedFilesOptions.xaml | 51 ++++ Xaml/SettingsForm.xaml | 3 +- 18 files changed, 959 insertions(+), 107 deletions(-) create mode 100644 Start-WithJson.cmd create mode 100644 Xaml/CompareExportedFilesOptions.xaml diff --git a/CloudAPIPowerShellManagement.psd1 b/CloudAPIPowerShellManagement.psd1 index d7bcbf226811ea0cbb9b7cc4f4293a3d20aea897..b074b7b95890162b6940de30edbaa6cbb302eacd 100644 GIT binary patch delta 14 VcmccRbjxW&4HKjB=31t^asV(m1;_vZ delta 14 VcmccRbjxW&4HKi`=31t^asV(a1;zjX diff --git a/CloudAPIPowerShellManagement.psm1 b/CloudAPIPowerShellManagement.psm1 index bd2c25a..d02d602 100644 --- a/CloudAPIPowerShellManagement.psm1 +++ b/CloudAPIPowerShellManagement.psm1 @@ -69,7 +69,11 @@ function Initialize-CloudAPIManagement [string] $View = "", [switch] - $ShowConsoleWindow + $ShowConsoleWindow, + [switch] + $JSonSettings, + [string] + $JSonFile ) $global:wpfNS = "xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'" @@ -99,6 +103,16 @@ function Initialize-CloudAPIManagement Hide-Console } + if($JSonSettings -eq $true) + { + $global:UseJSonSettings = $true + $global:JSonSettingFile = $JSonFile + } + else + { + $global:UseJSonSettings = $false + } + $global:txtSplashText.Text = "Unblock files" [System.Windows.Forms.Application]::DoEvents() Unblock-AllFiles $PSScriptRoot diff --git a/Core.psm1 b/Core.psm1 index 9d39e39..e6bd3c7 100644 --- a/Core.psm1 +++ b/Core.psm1 @@ -6,13 +6,12 @@ Core UI and Settings fatures for the CloudAPIPowerShellManager solution This module handles the WPF UI .NOTES - Version: 3.1.0 Author: Mikael Karlsson #> function Get-ModuleVersion { - '3.1.7' + '3.3.0' } function Start-CoreApp @@ -35,10 +34,18 @@ function Start-CoreApp # Load all modules in the Modules folder $global:modulesPath = [IO.Path]::GetDirectoryName($PSCommandPath) + "\Extensions" - #Import-Module ($PSScriptRoot + "\Core.psm1") -Force -Global - Add-DefaultSettings + if($global:UseJSonSettings -eq $true) + { + Initialize-JsonSettings + } + + if($global:UseJSonSettings -eq $false) + { + Write-Log "Use settings in registry" + } + Write-Log "#####################################################################################" Write-Log "Application started" Write-Log "#####################################################################################" @@ -53,7 +60,7 @@ function Start-CoreApp exit 1 } - $global:Debug = Get-SettingValue "Debug" + Initialize-Settings $global:currentViewObject = $null $global:FirstTimeRunning = ((Get-Setting "" "FirstTimeRunning" "true") -eq "true") $global:MainAppStarted = $false @@ -795,34 +802,230 @@ function Expand-FileName #endregion -#region Reg functions +#region Save/Read Settings functions ######################################################################## # -# Reg functions +# Save/Read Settings # ######################################################################## +function Initialize-Settings +{ + param([switch]$Updated) + + $global:Debug = Get-SettingValue "Debug" + $global:logFile = $null + $global:logFileMaxSize = $null + + if($Updated -eq $true) + { + Invoke-ModuleFunction "Invoke-SettingsUpdated" + } +} + +function Initialize-JsonSettings +{ + if(-not $global:JSonSettingFile) + { + $global:JSonSettingFile = "$($env:LOCALAPPDATA)\CloudAPIPowerShellManagement\Settings.json" + $fi = [IO.FileInfo]$global:JSonSettingFile + if($fi.Exists -eq $false) + { + Export-Settings $fi.FullName + } + } + else + { + $fi = [IO.FileInfo]$global:JSonSettingFile + if($fi.Exists -eq $false) + { + try + { + Write-Host "Settings file $($fi.FullName) does not exist. Create empty settings" + @{} | ConvertTo-Json | Out-File -FilePath $global:JSonSettingFile -Force -Encoding utf8 + } + catch + { + Clear-JsonSettingsValues + Write-LogError "Failed to create json setting file $($fi.FullName). Veirfy write access. Registry settings will be used." $_.Exception + } + } + } + + $fi = [IO.FileInfo]$global:JSonSettingFile + if($fi.Exists -eq $true) + { + try + { + $global:JsonSettingsObj = (ConvertFrom-Json (Get-Content -Path $fi.FullName -Raw)) + Write-Log "Use json settings file: $($fi.FullName)" + return + } + catch + { + Clear-JsonSettingsValues + Write-LogError "Failed to read json setting file $($fi.FullName). Registry settings will be used." $_.Exception + } + } + else + { + Clear-JsonSettingsValues + Write-LogError "Could not find json setting file $($fi.FullName). Registry settings will be used" + } + +} + +function Clear-JsonSettingsValues +{ + # Failed - Revert back to reg settings + $global:JsonSettingsObj = $null + $global:JSonSettingFile = $null + $global:UseJSonSettings = $false +} function Save-Setting { - param($SubPath, $Key, $Value, $Type = "String") + param($SubPath = "", $Key = "", $Value, $Type = "String") - $regPath = Get-RegPath $SubPath - if((Test-Path $regPath) -eq $false) + if($global:JsonSettingsObj -and $global:JSonSettingFile) { - New-Item (Get-RegPath $SubPath) -Force -ErrorAction SilentlyContinue | Out-Null + if($SubPath) + { + $arrParts = $SubPath.Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + + foreach($part in $arrParts) + { + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + $parentSetting.$part = @() + $parentSetting = $parentSetting.$part + } + } + + try + { + if($null -eq $Value) + { + if(($parentSetting.PSObject.Properties | Where Name -eq $Key)) + { + $parentSetting.PSObject.Properties.Remove($Key) | Out-Null + } + } + else + { + if($Type -eq "String" -and $null -ne $value) + { + $Value = $value.ToString() + } + elseif($Type -eq "DWord" -and $null -ne $Value) + { + $Value = [Int]::Parse($Value) + } + + if(-not ($parentSetting.PSObject.Properties | Where Name -eq $Key)) + { + $parentSetting | Add-Member -MemberType NoteProperty -Name $Key -Value $Value + } + else + { + $parentSetting.$Key = $Value + } + } + + $global:JsonSettingsObj | ConvertTo-Json -Depth 20 | Out-File -LiteralPath $global:JSonSettingFile -Force -Encoding utf8 + } + catch + { + Write-LogError "Failed to save json setting value $Key" $_.Exception + } + } + else + { + $regPath = Get-RegPath $SubPath + if((Test-Path $regPath) -eq $false) + { + New-Item (Get-RegPath $SubPath) -Force -ErrorAction SilentlyContinue | Out-Null + } + + New-ItemProperty -Path $regPath -Name $Key -Value $Value -Type $Type -Force | Out-Null } - New-ItemProperty -Path $regPath -Name $Key -Value $Value -Type $Type -Force | Out-Null } function Get-Setting { - param($SubPath, $Key, $defautValue) + param($SubPath = "", $Key = "", $defautValue) - try - { - $val = Get-ItemPropertyValue -Path (Get-RegPath $SubPath) -Name $Key -ErrorAction SilentlyContinue + if(-not $key) + { + return } - catch { } + + $val = $null + + if($global:JsonSettingsObj) + { + try + { + if($SubPath) + { + $arrParts = $SubPath.Split(@('/','\')) + } + else + { + $arrParts = @() + } + + $parentSetting = $global:JsonSettingsObj + $found = $true + + foreach($part in $arrParts) + { + if(($parentSetting.PSObject.Properties | Where Name -eq $part)) + { + $parentSetting = $parentSetting.$part + } + else + { + $found = $false + break + } + } + + if($null -ne $parentSetting.$Key -and $found) + { + $val = $parentSetting.$Key + } + } + catch + { + Write-LogError "Failed to read json setting value $Key" $_.Exception + } + } + else + { + try + { + $val = Get-ItemPropertyValue -Path (Get-RegPath $SubPath) -Name $Key -ErrorAction SilentlyContinue + } + catch + { + if($_.Exception.HResult -ne -2147024809) # Skip reporting missing values + { + Write-LogError "Failed to read registry setting value $Key" $_.Exception + } + } + } + if(-not $val) { $defautValue @@ -845,6 +1048,77 @@ function Get-RegPath $path } + +function Export-Settings +{ + param($fileName) + + try + { + $fi = [IO.FileInfo]$fileName + if($fi.Directory.Exists -eq $false) + { + $fi.Directory.Create() + } + } + catch + { + Write-LogError "Failed to create folder for settings file" $_.Exception + return + } + + $settingObj = [ordered]@{} + Add-RegKeyToSettings $settingObj "HKCU:\Software\CloudAPIPowerShellManagement" + $json = $settingObj | ConvertTo-Json -Depth 20 + try + { + $json | Out-File -filePath $fileName -encoding utf8 -Force -ErrorAction Stop + } + catch + { + Write-LogError "Failed to save json setting file" $_.Exception + } +} + +function Add-RegKeyToSettings +{ + param($settingObj, $regKey) + + try + { + $keyObj = Get-Item -Path $regKey + foreach($keyValue in ($keyObj.GetValueNames() | Sort)) + { + try + { + $settingObj.Add($keyValue, $keyObj.GetValue($keyValue)) + } + catch + { + Write-LogError "Failed to add setting from reg key $keyValue in $regKey" $_.Exception + } + } + + foreach($subKey in ($keyObj.GetSubKeyNames() | Sort)) + { + + $settingObjSub = [ordered]@{} + $settingObj.Add($subKey, $settingObjSub) + try + { + Add-RegKeyToSettings $settingObjSub ($regKey + '\' + $subKey) + } + catch + { + Write-LogError "Failed to add setting for reg subkey $subKey in $regKey" $_.Exception + } + } + } + catch + { + Write-LogError "Failed to add reg keys to json settings" $_.Exception + } +} #endregion #region Setting functions @@ -1028,13 +1302,30 @@ function Show-SettingsForm Add-XamlEvent $settingsForm "btnSave" "Add_Click" ({ Save-AllSettings - $global:Debug = Get-SettingValue "Debug" }) Add-XamlEvent $settingsForm "btnClose" "Add_Click" ({ Show-ModalObject }) + if($JsonSettingsObj) + { + Set-XamlProperty $settingsForm "btnExport" "Visibility" "Collapsed" + } + else + { + Add-XamlEvent $settingsForm "btnExport" "Add_Click" ({ + $sf = [System.Windows.Forms.SaveFileDialog]::new() + $sf.FileName = $script:currentObjName + $sf.DefaultExt = "*.json" + $sf.Filter = "Json (*.json)|*.json|All files (*.*)|*.*" + if($sf.ShowDialog() -eq "OK") + { + Export-Settings $sf.FileName + } + }) + } + $tmp = $global:appSettingSections | Where-Object Id -eq "General" if($tmp.Values.Count -gt 0) { @@ -1174,6 +1465,9 @@ function Save-AllSettings { & $global:currentViewObject.ViewInfo.SaveSettings } + + Initialize-Settings -Updated + Start-Sleep -Seconds 1 # It goes to quick...ToDo: Do this in a better way Write-Status "" } diff --git a/Extensions/Compare.psm1 b/Extensions/Compare.psm1 index 6e2daab..e4a53cb 100644 --- a/Extensions/Compare.psm1 +++ b/Extensions/Compare.psm1 @@ -11,7 +11,7 @@ Objects can be compared based on Properties or Documentatation info. function Get-ModuleVersion { - '1.0.7' + '1.0.8' } function Invoke-InitializeModule @@ -50,16 +50,16 @@ function Add-CompareProvider if($global:compareProviders.Count -eq 0) { $global:compareProviders += [PSCustomObject]@{ - Name = "Exported File" + Name = "Intune Objects with Exported Files" Value = "export" ObjectCompare = { Compare-ObjectsBasedonProperty @args } BulkCompare = { Start-BulkCompareExportObjects @args } ProviderOptions = "CompareExportOptions" - Activate = { Invoke-ActivateCompareExportObjects @args } + Activate = { Invoke-ActivateCompareWithExportObjects @args } } $global:compareProviders += [PSCustomObject]@{ - Name = "Named Objects" + Name = "Named Objects in Intune" Value = "name" BulkCompare = { Start-BulkCompareNamedObjects @args } ProviderOptions = "CompareNamedOptions" @@ -67,6 +67,15 @@ function Add-CompareProvider RemoveProperties = @("Id") } + $global:compareProviders += [PSCustomObject]@{ + Name = "Files in Exported Folders" + Value = "exportedFolders" + ObjectCompare = { Compare-ObjectsBasedonProperty @args } + BulkCompare = { Start-BulkCompareExportFolders @args } + ProviderOptions = "CompareExportedFilesOptions" + Activate = { Invoke-ActivateCompareExportedObjects @args } + } + $global:compareProviders += [PSCustomObject]@{ Name = "Existing objects" Value = "existing" @@ -269,7 +278,9 @@ function Set-CompareProviderOptions } } -function Invoke-ActivateCompareExportObjects +# Compare Intune object with exported folder + +function Invoke-ActivateCompareWithExportObjects { param($providerOptions, $firstTime) @@ -292,6 +303,39 @@ function Invoke-ActivateCompareExportObjects } } +# Compare two exported folders +function Invoke-ActivateCompareExportedObjects +{ + param($providerOptions, $firstTime) + + if($firstTime) + { + $path = Get-Setting "" "LastUsedFullPath" + if($path) + { + $path = [IO.Directory]::GetParent($path).FullName + } + Set-XamlProperty $providerOptions "txtExportPathSource" "Text" (?? $path (Get-SettingValue "RootFolder")) + Set-XamlProperty $providerOptions "txtExportPathCompare" "Text" (Get-SettingValue "ExportPathCompare") + + Add-XamlEvent $providerOptions "browseExportPathSource" "add_click" ({ + $folder = Get-Folder (Get-XamlProperty $this.Parent "txtExportPathSource" "Text") "Select root folder for source" + if($folder) + { + Set-XamlProperty $this.Parent "txtExportPathSource" "Text" $folder + } + }) + + Add-XamlEvent $providerOptions "browseExportPathCompare" "add_click" ({ + $folder = Get-Folder (Get-XamlProperty $this.Parent "txtExportPathCompare" "Text") "Select folder to compare the source with" + if($folder) + { + Set-XamlProperty $this.Parent "txtExportPathCompare" "Text" $folder + } + }) + } +} + function Invoke-ActivateCompareNamesObjects { param($providerOptions, $firstTime) @@ -468,6 +512,8 @@ function Start-BulkCompareExportObjects Write-Log "Start bulk Exported Objects compare" Write-Log "****************************************************************" $compareObjectsResult = @() + + $txtNameFilter = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtCompareNameFilter" "Text").Trim() $rootFolder = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtExportPath" "Text") $compareProps = $script:defaultCompareProps @@ -521,12 +567,20 @@ function Start-BulkCompareExportObjects Write-Log "Object from file '$($fileObj.FullName)' has no Id property. Compare not supported" 2 continue } + + $objName = Get-GraphObjectName $fileObj.Object $fileObj.ObjectType + + if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + { + continue + } + $curObject = $graphObjects | Where { $_.Object.Id -eq $fileObj.Object.Id } if(-not $curObject) { # Add objects that are exported but deleted - Write-Log "Object '$((Get-GraphObjectName $fileObj.Object $fileObj.ObjectType))' with id $($fileObj.Object.Id) not found in Intune. Deleted?" 2 + Write-Log "Object '$($objName)' with id $($fileObj.Object.Id) not found in Intune. Deleted?" 2 $compareProperties = @([PSCustomObject]@{ Object1Value = $null Object2Value = (Get-GraphObjectName $fileObj.Object $item.ObjectType) @@ -554,13 +608,19 @@ function Start-BulkCompareExportObjects # Add objects that are not exported if(($compareObjectsResult | Where { $_.Id -eq $graphObj.Id})) { continue } + $objName = Get-GraphObjectName $graphObj.Object $item.ObjectType + if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + { + continue + } + $compareObjectsResult += [PSCustomObject]@{ Object1 = $curObject.Object Object2 = $null ObjectType = $item.ObjectType Id = $graphObj.Id Result = @([PSCustomObject]@{ - Object1Value = (Get-GraphObjectName $graphObj.Object $item.ObjectType) + Object1Value = $objName Object2Value = $null Match = $false }) @@ -594,18 +654,18 @@ function Start-BulkCompareExportObjects if($outputType -eq "objectType") { - Save-BulkCompareResults $compResultValues (Join-Path $rootFolder "Compare_$(((Get-Date).ToString("yyyyMMdd-HHmm"))).csv") $compareProps + Save-BulkCompareResults $compResultValues (Join-Path $folder "Compare_$(((Get-Date).ToString("yyyyMMdd-HHmm"))).csv") $compareProps } } else { - Write-Log "Folder $folder not found. Skipping import" 2 + Write-Log "Folder $folder not found. Skipping compare" 2 } } if($outputType -eq "all" -and $compResultValues.Count -gt 0) { - Save-BulkCompareResults $compResultValues (Join-Path $folder "Compare_$(((Get-Date).ToString("yyyyMMDD-HHmm"))).csv") $compareProps + Save-BulkCompareResults $compResultValues (Join-Path $rootFolder "Compare_$(((Get-Date).ToString("yyyyMMDD-HHmm"))).csv") $compareProps } Write-Log "****************************************************************" @@ -618,6 +678,185 @@ function Start-BulkCompareExportObjects } } +function Start-BulkCompareExportFolders +{ + Write-Log "****************************************************************" + Write-Log "Start bulk Exported Folders compare" + Write-Log "****************************************************************" + $compareObjectsResult = @() + + $txtNameFilter = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtCompareNameFilter" "Text").Trim() + $rootFolderSource = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtExportPathSource" "Text") + $rootFolderCompare = (Get-XamlProperty $global:ccContentProviderOptions.Content "txtExportPathCompare" "Text") + + $compareProps = $script:defaultCompareProps + + foreach($removeProp in $global:cbCompareProvider.SelectedItem.RemoveProperties) + { + $compareProps.Remove($removeProp) | Out-Null + } + + foreach($removeProp in $global:cbCompareType.SelectedItem.RemoveProperties) + { + $compareProps.Remove($removeProp) | Out-Null + } + + if(-not $rootFolderSource -or -not $rootFolderCompare) + { + [System.Windows.MessageBox]::Show("Both folders must be specified", "Error", "OK", "Error") + return + } + + if([IO.Directory]::Exists($rootFolderSource) -eq $false) + { + [System.Windows.MessageBox]::Show("Root folder $rootFolderSource does not exist", "Error", "OK", "Error") + return + } + + if([IO.Directory]::Exists($rootFolderCompare) -eq $false) + { + [System.Windows.MessageBox]::Show("Root folder $rootFolderCompare does not exist", "Error", "OK", "Error") + return + } + + $outputType = $global:cbCompareSave.SelectedValue + Save-Setting "Compare" "SaveType" $outputType + + $compResultValues = @() + + foreach($item in ($global:dgObjectsToCompare.ItemsSource | where Selected -eq $true)) + { + Write-Status "Compare $($item.ObjectType.Title) objects" -Force -SkipLog + Write-Log "----------------------------------------------------------------" + Write-Log "Compare $($item.ObjectType.Title) objects" + Write-Log "----------------------------------------------------------------" + + $folderSource = Join-Path $rootFolderSource $item.ObjectType.Id + $folderCompare = Join-Path $rootFolderCompare $item.ObjectType.Id + + if([IO.Directory]::Exists($folderSource)) + { + Save-Setting "" "LastUsedFullPath" $folderSource + + $fileCompareObjs = @(Get-GraphFileObjects $folderCompare -ObjectType $item.ObjectType) + + foreach ($fileSourceObj in @(Get-GraphFileObjects $folderSource -ObjectType $item.ObjectType)) + { + $objName = Get-GraphObjectName $fileSourceObj.Object $item.ObjectType + if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + { + continue + } + + if(-not $fileSourceObj.Object.Id) + { + Write-Log "Object from file '$($fileSourceObj.FullName)' has no Id property. Compare not supported" 2 + continue + } + + $compareObject = $fileCompareObjs | Where { $_.Object.Id -eq $fileSourceObj.Object.Id } + + if(-not $compareObject) + { + # Add objects that are exported but deleted + Write-Log "Object '$($objName)' with id $($fileSourceObj.Object.Id) not found in Intune. Deleted?" 2 + $compareProperties = @([PSCustomObject]@{ + Object1Value = $null + Object2Value = (Get-GraphObjectName $fileSourceObj.Object $fileSourceObj.ObjectType) + Match = $false + }) + } + else + { + $fileSourceObj.Object | Add-Member Noteproperty -Name "@ObjectFromFile" -Value $true -Force + $compareObject.Object | Add-Member Noteproperty -Name "@ObjectFromFile" -Value $true -Force + $compareProperties = Compare-Objects $compareObject.Object $fileSourceObj.Object $item.ObjectType + } + + $compareObjectsResult += [PSCustomObject]@{ + Object1 = $compareObject.Object + Object2 = $fileSourceObj.Object + ObjectType = $item.ObjectType + Id = $fileSourceObj.Object.Id + Result = $compareProperties + } + } + + foreach($fileCompareObj in $fileCompareObjs) + { + # Add objects that were not exported in source folder + if(($compareObjectsResult | Where { $_.Id -eq $fileCompareObj.Object.Id})) { continue } + + $objName = Get-GraphObjectName $fileCompareObj.Object $item.ObjectType + if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + { + continue + } + + $compareObjectsResult += [PSCustomObject]@{ + Object1 = $fileCompareObj.Object + Object2 = $null + ObjectType = $item.ObjectType + Id = $fileCompareObj.Object.Id + Result = @([PSCustomObject]@{ + Object1Value = (Get-GraphObjectName $fileCompareObj.Object $item.ObjectType) + Object2Value = $null + Match = $false + }) + } + } + + if($outputType -eq "objectType") + { + $compResultValues = @() + } + + foreach($compObj in @($compareObjectsResult | Where { $_.ObjectType.Id -eq $item.ObjectType.Id })) + { + $objName = Get-GraphObjectName (?? $compObj.Object1 $compObj.Object2) $item.ObjectType + foreach($compValue in $compObj.Result) + { + $compResultValues += [PSCustomObject]@{ + ObjectName = $objName + Id = $compObj.Id + Type = $compObj.ObjectType.Title + ODataType = $compObj.Object1.'@OData.Type' + Property = $compValue.PropertyName + Value1 = $compValue.Object1Value + Value2 = $compValue.Object2Value + Category = $compValue.Category + SubCategory = $compValue.SubCategory + Match = $compValue.Match + } + } + } + + if($outputType -eq "objectType") + { + Save-BulkCompareResults $compResultValues (Join-Path $folderSource "Compare_$(((Get-Date).ToString("yyyyMMdd-HHmm"))).csv") $compareProps + } + } + else + { + Write-Log "Folder $folderSource not found. Skipping compare" 2 + } + } + + if($outputType -eq "all" -and $compResultValues.Count -gt 0) + { + Save-BulkCompareResults $compResultValues (Join-Path $rootFolderSource "Compare_$(((Get-Date).ToString("yyyyMMDD-HHmm"))).csv") $compareProps + } + + Write-Log "****************************************************************" + Write-Log "Bulk compare Exported Folders finished" + Write-Log "****************************************************************" + Write-Status "" + if($compareObjectsResult.Count -eq 0) + { + [System.Windows.MessageBox]::Show("No objects were comparced. Verify folder and exported files", "Error", "OK", "Error") + } +} + function Save-BulkCompareResults { param($compResultValues, $file, $props) @@ -1064,6 +1303,11 @@ function Compare-ObjectsBasedonDocumentation $val1 = $prop.$settingsValue $prop2 = $docObj2.Settings | Where { $_.EntityKey -eq $prop.EntityKey -and $_.Category -eq $prop.Category -and $_.SubCategory -eq $prop.SubCategory -and $_.Enabled -eq $prop.Enabled } $val2 = $prop2.$settingsValue + if($val1 -isnot [array] -and $val2 -is [array] -and $val2.Count -gt 1) + { + Write-Log "Multiple compare results returend for $($prop.Name). Using first result" 2 + $val2 = $val2[0] + } Add-CompareProperty $prop.Name $val1 $val2 $prop.Category $prop.SubCategory } @@ -1075,7 +1319,13 @@ function Compare-ObjectsBasedonDocumentation $addedProperties += ($prop.EntityKey + $prop.Category + $prop.SubCategory) $val2 = $prop.$settingsValue $prop2 = $docObj1.Settings | Where { $_.EntityKey -eq $prop.EntityKey -and $_.Category -eq $prop.Category -and $_.SubCategory -eq $prop.SubCategory -and $_.Enabled -eq $prop.Enabled } - $val1 = $prop2.$settingsValue + $val1 = $prop2.$settingsValue + if($val2 -isnot [array] -and $val1 -is [array] -and $val1.Count -gt 1) + { + Write-Log "Multiple compare results returend for $($prop.Name). Using first result" 2 + $val1 = $val1[0] + } + Add-CompareProperty $prop.Name $val1 $val2 $prop.Category $prop.SubCategory } } diff --git a/Extensions/DocumentationWord.psm1 b/Extensions/DocumentationWord.psm1 index 02b0042..82912bf 100644 --- a/Extensions/DocumentationWord.psm1 +++ b/Extensions/DocumentationWord.psm1 @@ -3,7 +3,7 @@ #https://docs.microsoft.com/en-us/office/vba/api/overview/word function Get-ModuleVersion { - '1.0.4' + '1.0.5' } function Invoke-InitializeModule @@ -117,6 +117,7 @@ function Invoke-WordPreProcessItems Save-Setting "Documentation" "WordExportProperties" $global:cbWordDocumentationProperties.SelectedValue Save-Setting "Documentation" "WordCustomDisplayProperties" $global:txtWordCustomProperties.Text Save-Setting "Documentation" "WordDocumentTemplate" $global:txtWordDocumentTemplate.Text + Save-Setting "Documentation" "WordDocumentName" $global:txtWordDocumentName.Text Save-Setting "Documentation" "WordAddCategories" $global:chkWordAddCategories.IsChecked Save-Setting "Documentation" "WordAddSubCategories" $global:chkWordAddSubCategories.IsChecked diff --git a/Extensions/IntuneAssignments.psm1 b/Extensions/IntuneAssignments.psm1 index 5f76f29..6424254 100644 --- a/Extensions/IntuneAssignments.psm1 +++ b/Extensions/IntuneAssignments.psm1 @@ -9,7 +9,7 @@ Module for listing Intune assignments #> function Get-ModuleVersion { - '1.0.1' + '1.0.2' } function Invoke-InitializeModule @@ -170,6 +170,7 @@ function Get-EMIntuneAssignments } else { + $assignmentObj = $assignment.target.groupId Write-Warning "Could not find a group with ID $($assignment.target.groupId)" } $included = $assignment.target.'@odata.type' -eq "#microsoft.graph.groupAssignmentTarget" diff --git a/Extensions/MSALAuthentication.psm1 b/Extensions/MSALAuthentication.psm1 index 0d618a5..40fbe6e 100644 --- a/Extensions/MSALAuthentication.psm1 +++ b/Extensions/MSALAuthentication.psm1 @@ -10,7 +10,7 @@ This module manages Authentication for the application with MSAL. It is also res #> function Get-ModuleVersion { - '3.0.5' + '3.3.0' } $global:msalAuthenticator = $null @@ -18,10 +18,28 @@ function Invoke-InitializeModule { $script:MSALAllApps = @() $global:MSALToken = $null - $global:MSALAuthority = $null + $global:MSALTenantId = $null $script:AccessableTenants = $null $global:SkipTokenCacheHelperEx = $null + $script:lstAADEnvironments = @( + [PSCustomObject]@{ + Name = "Azure AD Public" + Value = "public" + URL = "login.microsoftonline.com" + }, + [PSCustomObject]@{ + Name = "Azure AD US Government" + Value = "usGov" + URL = "login.microsoftonline.us" + }, + [PSCustomObject]@{ + Name = "Azure AD China" + Value = "china" + URL = "login.partner.microsoftonline.cn" + } + ) + $global:appSettingSections += (New-Object PSObject -Property @{ Title = "MSAL" Id = "MSAL" @@ -68,6 +86,14 @@ function Invoke-InitializeModule Description = "Request Azure AD Role read permission when getting the token. This can be use to resolve the SIDs to Azure Roles for the wids property on the Access Token. Note: This might trigger a consent prompt" }) "MSAL" + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Azure Login" + Key = "AzureLogin" + Type = "List" + ItemsSource = $script:lstAADEnvironments + DefaultValue = "public" + }) "MSAL" + Add-MSALPrereq #$script:MSALDLLMissing = $true #!!!! @@ -91,9 +117,19 @@ function Get-MSALAuthenticationObject $global:msalAuthenticator } +function Invoke-SettingsUpdated +{ + Initialize-MSALSettings +} + +function Initialize-MSALSettings +{ + +} + function Clear-MSALCurentUserVaiables { - $global:MSALAuthority = $null + $global:MSALTenantId = $null } function Get-MSALCurrentApp @@ -485,19 +521,42 @@ function Get-MsalAuthenticationToken $authResult } +function Get-MSALLoginEnvironment +{ + $loginValue = Get-SettingValue "AzureLogin" "public" + $loginEnv = $script:lstAADEnvironments | Where value -eq $loginValue + return (?? $loginEnv.Environment "login.microsoftonline.com") +} function Get-MSALApp { - param($appInfo) + param($appInfo, $loginHint) $msalApp = $script:MSALAllApps | Where { $_.ClientId -eq $appInfo.ClientID -and (-not $appInfo.RedirectUri -or $_.AppConfig.RedirectUri -eq $appInfo.RedirectUri)} - - if(-not $msalApp) + + $tenant = ?? $appInfo.TenantId "organizations" + + if($loginHint.Environment) { - Write-Log "Add MSAL App $($appInfo.ClientID) $((?? $appInfo.TenantId $appInfo.Authority))" + $authority = "https://$($loginHint.Environment)/$tenant/" + } + elseif($appInfo.Authority) + { + $authority = $appInfo.Authority + } + else + { + $authority = "https://$((Get-MSALLoginEnvironment))/$tenant/" + } + + if(-not $msalApp -or $msalApp.Authority -ne $authority) + { + Write-Log "Add MSAL App $($appInfo.ClientID) $authority" $appBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($appInfo.ClientID) - - if($appInfo.TenantId) { [void]$appBuilder.WithAuthority("https://login.microsoftonline.com/$($appInfo.TenantId)/") } - elseif ($appInfo.Authority) { [void]$appBuilder.WithAuthority($appInfo.Authority) } + + [void]$appBuilder.WithAuthority($authority) + #if($appInfo.TenantId) { [void]$appBuilder.WithAuthority("https://$((?? $loginHint.Environment (Get-MSALLoginEnvironment)))/$($appInfo.TenantId)/") } + #elseif ($appInfo.Authority) { [void]$appBuilder.WithAuthority($appInfo.Authority) } + if($appInfo.RedirectUri) { [void]$appBuilder.WithRedirectUri($appInfo.RedirectUri) } [void] $appBuilder.WithClientName("CloudAPIPowerShellManagement") @@ -514,6 +573,18 @@ function Get-MSALApp return $msalApp } +function Get-MSALAppAuthority +{ + try + { + ([uri]$global:MSALApp.Authority).Authority + } + catch + { + Get-MSALLoginEnvironment + } +} + function Connect-MSALUser { param( @@ -529,7 +600,9 @@ function Connect-MSALUser [switch] $Interactive, - $Account + $Account, + + $Tenant ) # No login during first time the app is started @@ -543,11 +616,10 @@ function Connect-MSALUser return } - if(-not $global:appObj.TenantId -and -not $global:appObj.Authority) - { - Write-Log "Tenant id/Authority is missing. Cannot authenticate" 3 - return - } + #if(-not $global:appObj.TenantId -and -not $global:appObj.Authority) + #{ + # Write-Log "Tenant id/Authority is missing. Cannot authenticate" 3 + #} if ($global:SkipTokenCacheHelperEx -ne $true -and -not ("TokenCacheHelperEx" -as [type])) { @@ -598,20 +670,21 @@ function Connect-MSALUser $Scopes = [String[]]$reqScopes } - $global:MSALApp = Get-MSALApp $global:appObj + $global:MSALApp = Get-MSALApp $global:appObj $Account $loginHint = "" $global:MSALAccounts = $global:MSALApp.GetAccountsAsync().GetAwaiter().GetResult() if($Account) { - $loginHint = $global:MSALAccounts | Where UserName -eq $Account - if($global:MSALToken -and $global:MSALToken.Account.UserName -ne $Account) + $userName = ?? $Account.UserName $Account + $loginHint = $global:MSALAccounts | Where UserName -eq $userName + if($global:MSALToken -and $global:MSALToken.Account.UserName -ne $userName) { # We're logging in with someone else... Clear-MSALCurentUserVaiables } } - + # If we force interactive login the skip setting loginHint to force the user select account if(-not $loginHint -and $Interactive -ne $true) { @@ -640,8 +713,8 @@ function Connect-MSALUser $prompConsent = $false $authResult = $null - $tenantId = $global:appObj.TenantId - $authority = ?? $global:MSALAuthority $global:appObj.Authority + $tenantId = ?? $global:MSALTenantId $global:appObj.TenantId + #$authority = ?? $global:MSALApp.Authority $global:appObj.Authority try { @@ -652,8 +725,8 @@ function Connect-MSALUser { $aquireTokenObj = $global:MSALApp.AcquireTokenSilent($Scopes, $loginHint) if($ForceRefresh) { [void]$aquireTokenObj.WithForceRefresh($ForceRefresh) } - if ($tenantId) { [void] $aquireTokenObj.WithAuthority("https://login.microsoftonline.com/$($TenantId)/") } - if ($authority) { [void]$aquireTokenObj.WithAuthority($authority) } + if ($tenantId) { [void]$aquireTokenObj.WithAuthority("https://$((Get-MSALAppAuthority))/$($tenantId)/") } + else { [void]$aquireTokenObj.WithAuthority($global:MSALApp.Authority) } $authResult = Get-MsalAuthenticationToken $aquireTokenObj @@ -709,7 +782,10 @@ function Connect-MSALUser } } } - catch {} + catch + { + Write-LogError "Failed to perform silent login" $_.Exception + } # Interactive login is only allowed once the app has started. Skip if silent login failed during startup if($global:MainAppStarted -and ((-not $authResult -and $Silent -ne $true) -or $prompConsent)) @@ -726,12 +802,12 @@ function Connect-MSALUser if ($tenantId) { Write-Log "Tenant id: $tenantId" - [void] $aquireTokenObj.WithAuthority("https://login.microsoftonline.com/$tenantId)/") + [void]$aquireTokenObj.WithAuthority("https://$((Get-MSALAppAuthority))/$tenantId/") } - elseif ($authority) + else { - Write-Log "Authority: $authority" - [void]$aquireTokenObj.WithAuthority($authority) + Write-Log "Authority: $($global:MSALApp.Authority)" + [void]$aquireTokenObj.WithAuthority($global:MSALApp.Authority) } if($loginHintName) @@ -786,8 +862,8 @@ function Connect-MSALUser # Can we reuse the app used for login? $appBuilder = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($global:appObj.ClientID) - if($tenantId) { [void]$appBuilder.WithAuthority("https://login.microsoftonline.com/$($tenantId)") } - elseif ($authority) { [void]$appBuilder.WithAuthority($authority) } + if($tenantId) { [void]$appBuilder.WithAuthority("https://$((Get-MSALAppAuthority))/$($tenantId)") } + else { [void]$appBuilder.WithAuthority($global:MSALApp.Authority) } if($global:appObj.RedirectUri) { [void]$appBuilder.WithRedirectUri($global:appObj.RedirectUri) } $app = $appBuilder.Build() @@ -977,7 +1053,7 @@ function Get-MSALProfileEllipse Write-Status "Logging in with $($this.Tag.UserName)" Hide-Popup Clear-MSALCurentUserVaiables - Connect-MSALUser -Account $this.Tag.UserName + Connect-MSALUser -Account $this.Tag #!!!.UserName if($global:curObjectType) { @@ -1200,7 +1276,7 @@ function Get-MSALProfileEllipse Write-Status "Logging in with $($this.Tag.UserName)" Hide-Popup Clear-MSALCurentUserVaiables - Connect-MSALUser -Account $this.Tag.UserName + Connect-MSALUser -Account $this.Tag #!!!.UserName if($global:curObjectType) { @@ -1311,9 +1387,9 @@ function Get-MSALProfileEllipse $lnkButton.add_Click({ Write-Status "Logging in to $($this.Tag.DisplayName)" # Set authority to selected tenant - $global:MSALAuthority = "https://login.microsoftonline.com/$($this.Tag.tenantId)/" - Hide-Popup - Connect-MSALUser -Account $global:MSALToken.Account.Username + $global:MSALTenantId = $this.Tag.tenantId + Hide-Popup + Connect-MSALUser -Account ($global:MSALAccounts | Where UserName -eq $global:MSALToken.Account.Username) if($global:curObjectType) { diff --git a/Extensions/MSGraph.psm1 b/Extensions/MSGraph.psm1 index 78ddc75..29d2ffd 100644 --- a/Extensions/MSGraph.psm1 +++ b/Extensions/MSGraph.psm1 @@ -10,13 +10,14 @@ This module manages Microsoft Grap fuctions like calling APIs, managing graph ob #> function Get-ModuleVersion { - '3.1.7' + '3.1.8' } $global:MSGraphGlobalApps = @( + #Authority="https://login.microsoftonline.com/organizations/" (New-Object PSObject -Property @{Name="";ClientId="";RedirectUri="";Authority=""}), - (New-Object PSObject -Property @{Name="Microsoft Intune PowerShell";ClientId="d1ddf0e4-d672-4dae-b554-9d5bdfd93547";RedirectUri="urn:ietf:wg:oauth:2.0:oob";Authority="https://login.microsoftonline.com/organizations/"}), - (New-Object PSObject -Property @{Name="Microsoft Graph PowerShell";ClientId="14d82eec-204b-4c2f-b7e8-296a70dab67e";RedirectUri="https://login.microsoftonline.com/common/oauth2/nativeclient";Authority="https://login.microsoftonline.com/organizations/"}) + (New-Object PSObject -Property @{Name="Microsoft Intune PowerShell";ClientId="d1ddf0e4-d672-4dae-b554-9d5bdfd93547";RedirectUri="urn:ietf:wg:oauth:2.0:oob"; }), + (New-Object PSObject -Property @{Name="Microsoft Graph PowerShell";ClientId="14d82eec-204b-4c2f-b7e8-296a70dab67e";RedirectUri="https://login.microsoftonline.com/common/oauth2/nativeclient";}) ) function Invoke-InitializeModule @@ -150,6 +151,15 @@ function Invoke-InitializeModule DefaultValue = $false Description = "This will enable the option to update/replace an existing object during import" }) "ImportExport" + + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Add ID to export file" + Key = "AddIDToExportFile" + Type = "Boolean" + DefaultValue = $false + Description = "This will add object ID to the export file to support objects with the same name e.g. ObjectName_ObjectId.json" + }) "ImportExport" } function Get-GraphAppInfo @@ -192,6 +202,16 @@ function Invoke-GraphAuthenticationUpdated $global:migFileObj = $null } +function Invoke-SettingsUpdated +{ + Initialize-GraphSettings +} + +function Initialize-GraphSettings +{ + +} + function Invoke-GraphRequest { param ( @@ -296,7 +316,7 @@ function Invoke-GraphRequest { # Code to test paging - Force each page to size specified in top parameter below # Kept for reference - + if(($url.IndexOf('?')) -eq -1) { $url = "$($url.Trim())?" @@ -844,7 +864,8 @@ function Show-GraphBulkExportForm Set-XamlProperty $script:exportForm "txtExportPath" "Text" (?? (Get-Setting "" "LastUsedRoot") (Get-SettingValue "RootFolder")) Set-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked" (Get-SettingValue "AddCompanyName") - Set-XamlProperty $script:exportForm "chkExportAssignments" "IsChecked" (Get-SettingValue "ExportAssignments") + Set-XamlProperty $script:exportForm "chkExportAssignments" "IsChecked" (Get-SettingValue "ExportAssignments") + #Set-XamlProperty $script:exportForm "txtExportNameFilter" "Text" (Get-Setting "" "ExportNameFilter") Add-XamlEvent $script:exportForm "browseExportPath" "add_click" ({ $folder = Get-Folder (Get-XamlProperty $script:exportForm "txtExportPath" "Text") "Select root folder for export" @@ -903,6 +924,9 @@ function Show-GraphBulkExportForm Write-Log "****************************************************************" Write-Log "Start bulk export" Write-Log "****************************************************************" + + $global:AADObjectCache = $null + foreach($item in $script:exportObjects) { if($item.Selected -ne $true) { continue } @@ -911,6 +935,10 @@ function Show-GraphBulkExportForm Write-Log "Export $($item.ObjectType.Title) objects" Write-Log "----------------------------------------------------------------" + $txtNameFilter = $global:txtExportNameFilter.Text.Trim() + Save-Setting "" "ExportNameFilter" $txtNameFilter + if($txtNameFilter) { Write-Log "Name filter: $txtNameFilter" } + try { $folder = Get-GraphObjectFolder $item.ObjectType (Get-XamlProperty $script:exportForm "txtExportPath" "Text") (Get-XamlProperty $script:exportForm "chkAddObjectType" "IsChecked") (Get-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked") @@ -918,7 +946,14 @@ function Show-GraphBulkExportForm $objects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) foreach($obj in $objects) { - Write-Status "Export $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force + $objName = Get-GraphObjectName $obj.Object $obj.ObjectType + + if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + { + continue + } + + Write-Status "Export $($item.Title): $objName" -Force Export-GraphObject $obj.Object $item.ObjectType $folder } Save-Setting "" "LastUsedFullPath" $folder @@ -1064,7 +1099,8 @@ function Show-GraphBulkImportForm Set-XamlProperty $script:importForm "chkImportScopes" "IsChecked" (Get-SettingValue "ImportScopeTags") Set-XamlProperty $script:importForm "cbImportType" "ItemsSource" $script:lstImportTypes Set-XamlProperty $script:importForm "cbImportType" "SelectedValue" (Get-SettingValue "ImportType" "alwaysImport") - + #Set-XamlProperty $script:importForm "txtImportNameFilter" "Text" (Get-Setting "" "ImportNameFilter") + if((Get-SettingValue "AllowUpdate") -eq $true) { Set-XamlProperty $script:importForm "lblImportType" "Visibility" "Visible" @@ -1140,6 +1176,10 @@ function Show-GraphBulkImportForm Get-GraphDependencyDefaultObjects $importedObjects = 0 + $txtNameFilter = $global:txtImportNameFilter.Text.Trim() + Save-Setting "" "ImportNameFilter" $txtNameFilter + if($txtNameFilter) { Write-Log "Name filter: $txtNameFilter" } + $allowUpdate = ((Get-SettingValue "AllowUpdate") -eq $true) foreach($item in ($script:importObjects | where Selected -eq $true | sort-object -property @{e={$_.ObjectType.ImportOrder}})) @@ -1172,6 +1212,13 @@ function Show-GraphBulkImportForm foreach ($fileObj in @($filesToImport)) { + $objName = Get-GraphObjectName $fileObj.Object $item.ObjectType + + if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + { + continue + } + if($allowUpdate -and $global:cbImportType.SelectedValue -ne "alwaysImport" -and $graphObjects -and (Reset-GraphObjet $fileObj $graphObjects)) { $importedObjects++ @@ -1274,6 +1321,8 @@ function Show-GraphBulkDeleteForm $script:deleteForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\BulkDeleteForm.xaml") -AddVariables if(-not $script:deleteForm) { return } + Set-XamlProperty $script:deleteForm "txtDeleteNameFilter" "Text" (Get-Setting "" "txtDeleteNameFilter") + $script:deleteObjects = @() foreach($objType in $global:lstMenuItems.ItemsSource) { @@ -1343,13 +1392,24 @@ function Show-GraphBulkDeleteForm Write-Log "Delete $($item.ObjectType.Title) objects" Write-Log "----------------------------------------------------------------" + $txtNameFilter = $global:txtDeleteNameFilter.Text.Trim() + Save-Setting "" "DeleteNameFilter" $txtNameFilter + if($txtNameFilter) { Write-Log "Name filter: $txtNameFilter" } + try { Write-Status "Get $($item.Title) objects" -Force $objects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) foreach($obj in $objects) { - Write-Status "Delete $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force + $objName = Get-GraphObjectName $obj.Object $obj.ObjectType + + if($txtNameFilter -and $objName -notmatch [RegEx]::Escape($txtNameFilter)) + { + continue + } + + Write-Status "Delete $($item.Title): $objName" -Force -SkipLog Remove-GraphObject $obj.Object $obj.ObjectType $folder } } @@ -1875,7 +1935,7 @@ function Add-GraphMigrationObject $global:migFileObj = $null } - if(-not $global:migFileObj) + if(-not $global:migFileObj -or ([IO.File]::Exists($migFileName) -eq $false)) { if(-not ([IO.File]::Exists($migFileName))) { @@ -2238,7 +2298,13 @@ function Export-GraphObject Remove-Property $obj "Assignments" } - $obj | ConvertTo-Json -Depth 20 | Out-File -LiteralPath ([IO.Path]::Combine($exportFolder, (Remove-InvalidFileNameChars "$((Get-GraphObjectName $obj $objectType)).json"))) -Force + $fileName = Get-GraphObjectName $obj $objectType + if((Get-SettingValue "AddIDToExportFile") -eq $true -and $obj.Id) + { + $fileName = ($fileName + "_" + $obj.Id) + } + + $obj | ConvertTo-Json -Depth 20 | Out-File -LiteralPath ([IO.Path]::Combine($exportFolder, (Remove-InvalidFileNameChars "$($fileName).json"))) -Force if($objectType.PostExportCommand) { diff --git a/README.md b/README.md index 56b57d3..b0b7821 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ Before logging on: * The app will use the Intune PowerShell Azure Enterprise Application by default but request all permissions required by the script. The will most likely cause a consent prompt since it uses more permission than the Intune module. Enable **Use Default Permissions** in Settings to only request the current permissions granted to the Enterprise App. **Note:** Using default permission might reduce functionality e.g. permissions for one or more object types might be missing -* Enable **Get Tenant List** in Settings if accessing multiple environments with the same account. This might cause a Consent prompt +* Enable **Get Tenant List** in Settings if accessing multiple environments with the same account e.g. a guest account in other tenants. This might cause a Consent prompt -Start the script by running **Start.cmd**, **Start-WithConsole.cmd** or **Start-IntuneManagement.ps1**. **Start-WithConsole.cmd** will leave the command prompt window open so you can see the log while running the app. +Start the script by running **Start.cmd**, **Start-WithJson.cmd**, **Start-WithConsole.cmd** or **Start-IntuneManagement.ps1**. **Start-WithConsole.cmd** will leave the command prompt window open so you can see the log while running the app. ## Documentation @@ -171,6 +171,28 @@ See [Change Log](ReleaseNotes.md) for more information ## Authentication See [MSAL Info](MSALInfo.md) for more information about authentication +## Settings + +Settings for the script is default stored in the registry. However, the script supports settings to be stored in a json file so it can be copied between computers. Registry settings can be exported in the Settings dialog. + +To use settings based on a json file: + +``` +Start-IntuneManagement.ps1 -JSonSettings [-JSonFile ] +``` + +If only -JSonSettings is used the script will use the default json setting file: + +``` +%LOCALAPPDATA%\CloudAPIPowerShellManagement\Settings.json +``` + +Use -JSonFile for custom location of the file + +Start-WithJson.cmd is included as an example on how to start the script with json settings. + +**Note:** If the file can't be created, the script will revert back registry. Make sure that the script can write to the file. It is not recommended to store the file in a folder that requires UAC to get write permissions. + ## Supported Intune objects * App Configurations (App and Device) * App Protection @@ -235,7 +257,7 @@ Android Store Apps are **not** imported. The Create API is documented in Microso Using multiple tenants support causes multiple logins/consent prompts the first time if 'Microsoft Graph PowerShell' is used. Querying the API for tenant list uses a different scope that is not included by default in the 'Microsoft Graph PowerShell' app. -~~Using multiple tenants support *might* cause and endless loop in the login screen and cause duplicate accounts in token cache. Actual cause is not found yet but it happens on rare occasions and it looks like it happens when a guest account is used. Workaround: Cancel the login, restart the script, logout and restart the script again.~~ - Not seen this in a long time. Please create issue if this happens +~~Using multiple tenants support *might* cause and endless loop in the login screen and cause duplicate accounts in token cache. Actual cause is not found yet but it happens on rare occasions and it looks like it happens when a guest account is used. Workaround: Cancel the login, restart the script, logout and restart the script again.~~ - Not seen this in a long time. Please create an issue if this happens When multi tenant settings is Enabled/Disabled, the Profile Info is not updated until the account is changed or app is restarted. Profile Info popup is built after logon. diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 6b531d9..e802a4b 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,45 @@ # Release Notes +## 3.3.0 (Beta) - 2021-10-17 + +This is a **BETA** release. It contains core changes for Authentication and Settings management. Please report any issues [here](https://github.com/Micke-K/IntuneManagement/issues). + +**New features** + +- Support for Settings in Json files + Settings can now be stored in json files and copied between devices. + + See [Readme](README.md#Settings) on how to use this feature + This is based on [Issue 33](https://github.com/Micke-K/IntuneManagement/issues/33) + +- Bulk Compare for exported folders + + The tool can now compare two exported folders + This is based on [Issue 32](https://github.com/Micke-K/IntuneManagement/issues/32) + +- Support for Azure AD US Government cloud and Azure AD China cloud. Default is Azure AD Public cloud. + + Change cloud in Settings + **Note:** This is a major change to the authentication. This may have an impact if a custom configured Azure app is used. + This is based on [Issue 26](https://github.com/Micke-K/IntuneManagement/issues/26). Please report any problem, progress or testing with US Government/China cloud or if there are any issues when a custom configured Azure app is used. + +- Export can now add Id to the name of the backup file + + This can be used if there are multiple objects with the same name. + + This can be enabled in Settings. Backup file name will be _.json. + +- Export/Import/Compare/Delete now supports name filter + Objects are filtered based on escaped RegEx -nomatch expression so wildcards are not supported. + +- IntuneAssignments report will now include the id of deleted groups + +**Fixes** + +* Fixed an issue in Export. Groups were not exported if exporting multiple times and multiple folders during the same session. +* Fixed an issue in Compare where the csv file was not stored in the correct folder +* Fixed an issue in Compare where the comparing object may return System[]. This can happen if the generated files has multiple documentation items for a property. First result will be used. + ## 3.2.3 - 2021-10-07 **New features** diff --git a/Start-IntuneManagement.ps1 b/Start-IntuneManagement.ps1 index 012a929..27c53b2 100644 --- a/Start-IntuneManagement.ps1 +++ b/Start-IntuneManagement.ps1 @@ -1,7 +1,11 @@ [CmdletBinding(SupportsShouldProcess=$True)] param( [switch] - $ShowConsoleWindow + $ShowConsoleWindow, + [switch] + $JSonSettings, + [string] + $JSonFile ) Import-Module ($PSScriptRoot + "\CloudAPIPowerShellManagement.psd1") -Force -Initialize-CloudAPIManagement -View "IntuneGraphAPI" -ShowConsoleWindow:($ShowConsoleWindow) \ No newline at end of file +Initialize-CloudAPIManagement -View "IntuneGraphAPI" -ShowConsoleWindow:($ShowConsoleWindow) -JSonSettings:($JSonSettings) -JSonFile $JSonFile diff --git a/Start-WithJson.cmd b/Start-WithJson.cmd new file mode 100644 index 0000000..a7c3cb0 --- /dev/null +++ b/Start-WithJson.cmd @@ -0,0 +1 @@ +cmd /c powershell -ex bypass -File "%~DP0Start-IntuneManagement.ps1" -JSonSettings diff --git a/Xaml/BulkDeleteForm.xaml b/Xaml/BulkDeleteForm.xaml index be523e6..d057429 100644 --- a/Xaml/BulkDeleteForm.xaml +++ b/Xaml/BulkDeleteForm.xaml @@ -1,19 +1,31 @@ + + + + + - + + + + + - + + - + + + + + - + - + - + - + - + - + diff --git a/Xaml/BulkImportForm.xaml b/Xaml/BulkImportForm.xaml index 9d0a3ea..b420483 100644 --- a/Xaml/BulkImportForm.xaml +++ b/Xaml/BulkImportForm.xaml @@ -15,6 +15,7 @@ + @@ -37,39 +38,45 @@ + + + + - + -