diff --git a/Core.psm1 b/Core.psm1 index 2f73e2a..47cb84d 100644 --- a/Core.psm1 +++ b/Core.psm1 @@ -12,7 +12,7 @@ This module handles the WPF UI function Get-ModuleVersion { - '3.0.0' + '3.0.1' } function Start-CoreApp @@ -1138,8 +1138,8 @@ function Add-ViewItem { param($viewItem) - $objSection = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewItem.ViewID } - if(-not $objSection) + $viewObject = $global:viewObjects | Where { $_.ViewInfo.Id -eq $viewItem.ViewID } + if(-not $viewObject) { if(($arrMenuInlcude -and $arrMenuInlcude -notcontains $viewItem.ViewID) -or ($arrMenuExlcude -and $arrMenuExlcude -contains $viewItem.ViewID)) { return } @@ -1153,17 +1153,9 @@ function Add-ViewItem $viewItem | Add-Member -NotePropertyName "ImportOrder" -NotePropertyValue 1000 } - if(-not $global:PermissionScope) { $global:PermissionScope = @() } foreach($scope in $viewItem.Permissons) { - if($global:PermissionScope -notcontains $scope) { $global:PermissionScope += $scope } - } - - foreach($required in @("openid","profile","email","User.ReadWrite.All","Group.ReadWrite.All","RoleManagement.Read.Directory")) #,"https://management.azure.com/user_impersonation") ) - { - if($required -in $global:PermissionScope) { continue } - $global:PermissionScope += $required - Write-LogDebug "Adding required scope $required" + if($viewObject.ViewInfo.Permissions -is [Object[]] -and $viewObject.ViewInfo.Permissions -notcontains $scope) { $viewObject.ViewInfo.Permissions += $scope } } if($viewItem.Icon -or [IO.File]::Exists(($global:AppRootFolder + "\Xaml\Icons\$($viewItem.Id).xaml"))) @@ -1172,7 +1164,7 @@ function Add-ViewItem $viewItem | Add-Member -NotePropertyName "IconImage" -NotePropertyValue $ctrl } - $objSection.ViewItems += $viewItem + $viewObject.ViewItems += $viewItem } function Show-View @@ -1221,7 +1213,7 @@ function Show-View { $global:txtSplashText.Text = "Authenticate" [System.Windows.Forms.Application]::DoEvents() - & $viewObject.ViewInfo.Authenticate + & $viewObject.ViewInfo.Authenticate } if($viewObject.ViewInfo.Activating) diff --git a/Extensions/Compare.psm1 b/Extensions/Compare.psm1 index 5259171..1154299 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.0' + '1.0.1' } function Invoke-InitializeModule diff --git a/Extensions/Documentation.psm1 b/Extensions/Documentation.psm1 index 9ef21b2..518ad5c 100644 --- a/Extensions/Documentation.psm1 +++ b/Extensions/Documentation.psm1 @@ -20,7 +20,7 @@ $global:documentationProviders = @() function Get-ModuleVersion { - '1.0.0' + '1.0.1' } function Invoke-InitializeModule @@ -411,6 +411,10 @@ function Get-ObjectTypeString { return (Get-LanguageString "SecurityTemplate.conditionalAccess") } + elseif($objTypeId -eq "EndpointAnalytics") + { + return (Get-LanguageString "SettingDetails.healthMonScopeBootPerf") + } elseif($objTypeId -eq "EndpointSecurity") { return (Get-LanguageString "PolicyType.EndpointSecurityTemplate.default") diff --git a/Extensions/EndpointManager.psm1 b/Extensions/EndpointManager.psm1 index 7ee804d..fa72890 100644 --- a/Extensions/EndpointManager.psm1 +++ b/Extensions/EndpointManager.psm1 @@ -10,7 +10,7 @@ This module is for the Endpoint Manager/Intune View. It manages Export/Import/Co #> function Get-ModuleVersion { - '3.1.0' + '3.1.2' } function Invoke-InitializeModule @@ -20,7 +20,7 @@ function Invoke-InitializeModule Title = "Endpoint Manager/Intune" Id = "EndpointManager" Values = @() - Priority = 10 + Priority = 10 }) Add-SettingsObject (New-Object PSObject -Property @{ @@ -72,6 +72,22 @@ function Invoke-InitializeModule SubPath = "EndpointManager" }) "EndpointManager" + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Show Delete button" + Key = "EMAllowDelete" + Type = "Boolean" + DefaultValue = $false + Description = "Allow deleting individual objectes" + }) "EndpointManager" + + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Show Bulk Delete " + Key = "EMAllowBulkDelete" + Type = "Boolean" + DefaultValue = $false + Description = "Allow using bulk delete to delete all objects of selected types" + }) "EndpointManager" + $viewPanel = Get-XamlObject ($global:AppRootFolder + "\Xaml\EndpointManagerPanel.xaml") -AddVariables Set-EMViewPanel $viewPanel @@ -89,6 +105,8 @@ function Invoke-InitializeModule Authenticate = { Invoke-EMAuthenticateToMSAL } AppInfo = (Get-GraphAppInfo "EMAzureApp" "d1ddf0e4-d672-4dae-b554-9d5bdfd93547") SaveSettings = { Invoke-EMSaveSettings } + + Permissions = @() }) Add-ViewObject $global:EMViewObject @@ -166,6 +184,7 @@ function Invoke-InitializeModule PostImportCommand = { Start-PostImportIntuneBranding @args } PostGetCommand = { Start-PostGetIntuneBranding @args } PostExportCommand = { Start-PostExportIntuneBranding @args } + PreDeleteCommand = { Start-PreDeleteIntuneBranding @args } Permissons=@("DeviceManagementApps.ReadWrite.All") Icon = "Branding" SkipRemoveProperties = @('Id') # Id is removed by PreImport. Required for default profile @@ -202,6 +221,7 @@ function Invoke-InitializeModule ViewID = "IntuneGraphAPI" PreImportCommand = { Start-PreImportESP @args } PostExportCommand = { Start-PostExportESP @args } + PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } # Note: Uses same PreDelete as restrictions QUERYLIST = "`$filter=endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") SkipRemoveProperties = @('Id') @@ -217,6 +237,7 @@ function Invoke-InitializeModule QUERYLIST = "`$filter=not endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" PostExportCommand = { Start-PostExportEnrollmentRestrictions @args } PreImportCommand = { Start-PreImportEnrollmentRestrictions @args } + PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") SkipRemoveProperties = @('Id') AssignmentsType = "enrollmentConfigurationAssignments" @@ -522,13 +543,26 @@ function Invoke-InitializeModule ImportOrder = 15 GroupId = "TenantAdmin" }) + + Add-ViewItem (New-Object PSObject -Property @{ + Title = "Health Scripts" + Id = "DeviceHealthScripts" + ViewID = "IntuneGraphAPI" + QUERYLIST = "`$filter=isGlobalScript%20eq%20false" # Looks like filters are not working for deviceHealthScripts + API = "/deviceManagement/deviceHealthScripts" + PreDeleteCommand = { Start-PreDeleteDeviceHealthScripts @args } + Permissons=@("DeviceManagementConfiguration.ReadWrite.All") + GroupId = "EndpointAnalytics" + Icon = "Report" + AssignmentsType = "deviceHealthScriptAssignments" + }) } function Invoke-EMAuthenticateToMSAL { $global:EMViewObject.AppInfo = Get-GraphAppInfo "EMAzureApp" "d1ddf0e4-d672-4dae-b554-9d5bdfd93547" Set-MSALCurrentApp $global:EMViewObject.AppInfo - & $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) + & $global:msalAuthenticator.Login -Account (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) -Permissions $global:EMViewObject.Permissions } function Invoke-EMDeactivateView @@ -566,16 +600,38 @@ function Invoke-EMSaveSettings Connect-MSALUser -Account $global:MSALToken.Account.Username Write-Status "" } + + # Hide/Show Delete button + $allowDelete = Get-SettingValue "EMAllowDelete" + $global:btnDelete.Visibility = (?: ($allowDelete -eq $true) "Visible" "Collapsed") + + # Hide/Show Delete on Bulk menu + $allowBulkDelete = Get-SettingValue "EMAllowBulkDelete" + $mnuBulk = $mnuMain.Items | Where Name -eq "EMBulk" + + if($mnuBulk) + { + $mnuBulkDelete = $mnuBulk.Items | Where Name -eq "mnuBulkDelete" + if($mnuBulkDelete) + { + $mnuBulkDelete.Visibility = (?: ($allowBulkDelete -eq $true) "Visible" "Collapsed") + } + } } function Set-EMViewPanel { param($panel) + # ToDo: Create View specific pannel and move this to graph Add-XamlEvent $panel "btnView" "Add_Click" -scriptBlock ([scriptblock]{ Show-GraphObjectInfo }) + Add-XamlEvent $panel "btnDelete" "Add_Click" -scriptBlock ([scriptblock]{ + Remove-GraphObjects + }) + Add-XamlEvent $panel "btnCopy" "Add_Click" -scriptBlock ([scriptblock]{ Copy-GraphObject }) @@ -603,11 +659,14 @@ function Set-EMViewPanel }) Invoke-FiterBoxChanged ($panel.FindName("txtFilter")) - + + $allowDelete = Get-SettingValue "EMAllowDelete" + Set-XamlProperty $panel "btnDelete" "Visibility" (?: ($allowDelete -eq $true) "Visible" "Collapsed") $global:dgObjects.add_selectionChanged({ Set-XamlProperty $this.Parent "btnView" "IsEnabled" (?: ($global:dgObjects.SelectedItem -eq $null) $false $true) Set-XamlProperty $this.Parent "btnCopy" "IsEnabled" (?: ($global:dgObjects.SelectedItem -eq $null) $false $true) + Set-XamlProperty $this.Parent "btnDelete" "IsEnabled" (?: ($global:dgObjects.SelectedItem -eq $null -and $global:curObjectType.AllowDelete -ne $false) $false $true) }) # ToDo: Move this to the view object @@ -922,6 +981,15 @@ function Start-PostExportIntuneBranding } } +function Start-PreDeleteIntuneBranding +{ + param($obj, $objectType) + + if($obj.isDefaultProfile -eq $true) + { + @{ "Delete" = $false } + } +} #endregion @@ -1613,6 +1681,16 @@ function Start-PreImportEnrollmentRestrictions Remove-Property $obj "Id" } } + +function Start-PreDeleteEnrollmentRestrictions +{ + param($obj, $objectType) + + if($obj.Priority -eq 0) + { + @{ "Delete" = $false } + } +} #endregion #region ScopeTags @@ -1633,6 +1711,20 @@ function Start-PreImportAssignmentsAutoPilot } #endregion +#region Health Scripts + +function Start-PreDeleteDeviceHealthScripts +{ + param($obj, $objectType) + + if($obj.IsGlobal -eq $true) + { + @{ "Delete" = $false } + } +} + +#endregion + #region Generic functions function Save-EMDefaultPolicy diff --git a/Extensions/EndpointManagerInfo.psm1 b/Extensions/EndpointManagerInfo.psm1 index 3510ae8..e674a19 100644 --- a/Extensions/EndpointManagerInfo.psm1 +++ b/Extensions/EndpointManagerInfo.psm1 @@ -10,7 +10,7 @@ This module is for the Endpoint Info View. It shows read-only objects in Intune #> function Get-ModuleVersion { - '3.1.0' + '3.1.1' } function Invoke-InitializeModule @@ -27,6 +27,7 @@ function Invoke-InitializeModule Authenticate = { Invoke-EMInfoAuthenticateToMSAL } AppInfo = (Get-GraphAppInfo "EM" "d1ddf0e4-d672-4dae-b554-9d5bdfd93547") SaveSettings = { Invoke-EMSaveSettings } + Permissions = @() }) Add-ViewObject $global:EMInfoViewObject @@ -80,7 +81,7 @@ function Invoke-InitializeModule ShowButtons = @("View") Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") }) - + } function Invoke-EMInfoActivatingView @@ -99,6 +100,6 @@ function Invoke-EMInfoAuthenticateToMSAL $usr = (?? $global:MSALToken.Account.UserName (Get-Setting "" "LastLoggedOnUser")) if($usr) { - & $global:msalAuthenticator.Login -Account $usr + & $global:msalAuthenticator.Login -Account $usr -Permissions $global:EMInfoViewObject.Permissions } } \ No newline at end of file diff --git a/Extensions/MSALAuthentication.psm1 b/Extensions/MSALAuthentication.psm1 index 84fb743..92a4d58 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.0' + '3.0.1' } $global:msalAuthenticator = $null @@ -59,6 +59,14 @@ function Invoke-InitializeModule Description = "Default permissions of the selected app will be used when logging on. Some objects might not be accessable" }) "MSAL" + Add-SettingsObject (New-Object PSObject -Property @{ + Title = "Add Azure Role Read permissions" + Key = "AzureADRoleRead" + Type = "Boolean" + DefaultValue = $false + 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-MSALPrereq #$script:MSALDLLMissing = $true #!!!! @@ -75,6 +83,7 @@ function Get-MSALAuthenticationObject Logout = { Disconnect-MSALUser } ProfilePicture = { Get-MSALProfileEllipse @args } ShowErrors = { Show-MSALError } + Permissions = @("openid","profile","email","User.ReadWrite.All","Group.ReadWrite.All") #"RoleManagement.Read.Directory" } } @@ -511,7 +520,9 @@ function Connect-MSALUser [switch] $Interactive, - $Account + $Account, + + $Permissions = @() # Addidional permissions required by the current view object ) # No login during first time the app is started @@ -559,8 +570,29 @@ function Connect-MSALUser } else { - $Scopes = [string[]]$global:PermissionScope + #$Scopes = [string[]]$global:PermissionScope + $reqScopes = [string[]]$global:msalAuthenticator.Permissions $useDefaultPermissions = $false + + $resolveRoles = ((Get-SettingValue "AzureADRoleRead" $false) -eq $true) + + if($resolveRoles -and $global:msalAuthenticator.Permissions -notcontains "RoleManagement.Read.Directory") + { + # Adds the required permission for reading AAD directory roles + $reqScopes += "RoleManagement.Read.Directory" + } + + if($Permissions.Count -gt 0) + { + $script:curViewPermissions = $Permissions + } + $reqScopes += $script:curViewPermissions + + foreach($tmpScope in $script:curViewPermissions) + { + if($reqScopes -notcontains $tmpScope) { $reqScopes += $tmpScope } + } + $Scopes = [String[]]$reqScopes } $global:MSALApp = Get-MSALApp $global:appObj @@ -645,14 +677,14 @@ function Connect-MSALUser #AADSTS65001 if($script:authenticationFailure.Classification -eq "ConsentRequired") { - $Scopes = [string[]]$global:PermissionScope + $Scopes = [string[]]$reqScopes } else { - if($useDefaultPermissions -eq $false -and $authResult -and ($global:PermissionScope | measure).Count -gt 0 -and $global:promptConsentRequested -notcontains $authResult.TenantId) + if($useDefaultPermissions -eq $false -and $authResult -and ($reqScopes | measure).Count -gt 0 -and $global:promptConsentRequested -notcontains $authResult.TenantId) { $missingScopes = @() - foreach($scope in $global:PermissionScope) + foreach($scope in $reqScopes) { $tmpScope = $scope.Split('/')[-1] if($tmpScope -eq ".default") { continue } @@ -1270,7 +1302,7 @@ function Get-MSALProfileEllipse $dg = [System.Windows.Controls.DataGrid]::new() $dg.ItemsSource = ($tokenArr | Select Name, Value) - Show-ModalForm "Token info" $dg + Show-ModalForm "Token info" $dg } Add-XamlEvent $tmpObj "lnkAccessTokenInfo" "add_Click" { diff --git a/Extensions/MSGraph.psm1 b/Extensions/MSGraph.psm1 index bc74f5f..5349315 100644 --- a/Extensions/MSGraph.psm1 +++ b/Extensions/MSGraph.psm1 @@ -10,7 +10,7 @@ This module manages Microsoft Grap fuctions like calling APIs, managing graph ob #> function Get-ModuleVersion { - '3.1.0' + '3.1.1' } $global:MSGraphGlobalApps = @( @@ -697,19 +697,28 @@ function Show-GraphBulkExportForm Add-GraphExportExtensions $script:exportForm 0 - $script:lstObjectsToExport = $script:exportForm.FindName("lstObjectsToExport") - if($script:lstObjectsToExport) - { - $script:lstObjectsToExport.ItemsSource = $script:exportObjects + $column = Get-GridCheckboxColumn "Selected" + $global:dgObjectsToExport.Columns.Add($column) - Add-XamlEvent $script:exportForm "chkCheckAll" "add_click" ({ - foreach($item in $script:exportObjects) - { + $column.Header.IsChecked = $true # All items are checked by default + $column.Header.add_Click({ + foreach($item in $global:dgObjectsToExport.ItemsSource) + { $item.Selected = $this.IsChecked } - $script:lstObjectsToExport.Items.Refresh() - }) - } + $global:dgObjectsToExport.Items.Refresh() + } + ) + + # Add Object type column + $binding = [System.Windows.Data.Binding]::new("Title") + $column = [System.Windows.Controls.DataGridTextColumn]::new() + $column.Header = "Object type" + $column.IsReadOnly = $true + $column.Binding = $binding + $global:dgObjectsToExport.Columns.Add($column) + + $global:dgObjectsToExport.ItemsSource = $script:exportObjects Add-XamlEvent $script:exportForm "btnClose" "add_click" ({ $script:exportForm = $null @@ -739,11 +748,11 @@ function Show-GraphBulkExportForm { $folder = Get-GraphObjectFolder $item.ObjectType (Get-XamlProperty $script:exportForm "txtExportPath" "Text") (Get-XamlProperty $script:exportForm "chkAddObjectType" "IsChecked") (Get-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked") - $objects = @(Get-GraphObjects -Url $url -property $objectType.ViewProperties -objectType $objectType) + $objects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) foreach($obj in $objects) { - Write-Status "Export $($item.Title): $((Get-GraphObjectName $obj))" -Force - Export-GraphObject $obj.Object $item.ObjectType $folder + Write-Status "Export $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force + Export-GraphObject $obj.Object $item.ObjectType $folder } Save-Setting "" "LastUsedFullPath" $folder } @@ -876,19 +885,36 @@ function Show-GraphBulkImportForm Add-GraphImportExtensions $script:importForm 0 - $script:lstObjectsToImport = $script:importForm.FindName("lstObjectsToImport") - if($script:lstObjectsToImport) - { - $script:lstObjectsToImport.ItemsSource = $script:importObjects + $column = Get-GridCheckboxColumn "Selected" + $global:dgObjectsToImport.Columns.Add($column) - Add-XamlEvent $script:importForm "chkCheckAll" "add_click" ({ - foreach($item in $script:importObjects) - { + $column.Header.IsChecked = $true # All items are checked by default + $column.Header.add_Click({ + foreach($item in $global:dgObjectsToImport.ItemsSource) + { $item.Selected = $this.IsChecked } - $script:lstObjectsToImport.Items.Refresh() - }) - } + $global:dgObjectsToImport.Items.Refresh() + } + ) + + # Add Object type column + $binding = [System.Windows.Data.Binding]::new("Title") + $column = [System.Windows.Controls.DataGridTextColumn]::new() + $column.Header = "Object type" + $column.IsReadOnly = $true + $column.Binding = $binding + $global:dgObjectsToImport.Columns.Add($column) + + # Add Order column + $binding = [System.Windows.Data.Binding]::new("ObjectType.ImportOrder") + $column = [System.Windows.Controls.DataGridTextColumn]::new() + $column.Header = "Import order" + $column.IsReadOnly = $true + $column.Binding = $binding + $global:dgObjectsToImport.Columns.Add($column) + + $global:dgObjectsToImport.ItemsSource = $script:importObjects Add-XamlEvent $script:importForm "btnClose" "add_click" ({ $script:importForm = $null @@ -934,6 +960,11 @@ function Show-GraphBulkImportForm { [System.Windows.MessageBox]::Show("No objects were imported. Verify folder and exported files", "Error", "OK", "Error") } + else + { + Show-GraphObjects + Write-Status "" + } }) if((Get-XamlProperty $script:importForm "txtImportPath" "Text")) @@ -994,6 +1025,113 @@ function Add-GraphImportExtensions } } +function Show-GraphBulkDeleteForm +{ + $script:deleteForm = Get-XamlObject ($global:AppRootFolder + "\Xaml\BulkDeleteForm.xaml") -AddVariables + if(-not $script:deleteForm) { return } + + $script:deleteObjects = @() + foreach($objType in $global:lstMenuItems.ItemsSource) + { + if(-not $objType.Title) { continue } + + if($objType.ShowButtons -is [Object[]] -and $objType.ShowButtons -notcontains "Delete") { continue } + + $script:deleteObjects += New-Object PSObject -Property @{ + Title = $objType.Title + Selected = $false + ObjectType = $objType + } + } + + $column = Get-GridCheckboxColumn "Selected" + $global:dgBulkDeleteObjects.Columns.Add($column) + + $column.Header.IsChecked = $false # All items are NOT checked by default + $column.Header.add_Click({ + foreach($item in $global:dgBulkDeleteObjects.ItemsSource) + { + $item.Selected = $this.IsChecked + } + $global:dgBulkDeleteObjects.Items.Refresh() + } + ) + + # Add title column + $binding = [System.Windows.Data.Binding]::new("Title") + $column = [System.Windows.Controls.DataGridTextColumn]::new() + $column.Header = "Title" + $column.IsReadOnly = $true + $column.Binding = $binding + $global:dgBulkDeleteObjects.Columns.Add($column) + + $ocList = [System.Collections.ObjectModel.ObservableCollection[object]]::new(@($script:deleteObjects)) + $global:dgBulkDeleteObjects.ItemsSource = [System.Windows.Data.CollectionViewSource]::GetDefaultView($ocList) + + Add-XamlEvent $script:deleteForm "btnClose" "add_click" ({ + $script:deleteForm = $null + Show-ModalObject + }) + + Add-XamlEvent $script:deleteForm "btnDelete" "add_click" ({ + + $selCount = (($global:dgBulkDeleteObjects.ItemsSource | Where Selected -eq $true) | measure).Count + + if($selCount -eq 0) + { + [System.Windows.MessageBox]::Show("No object types selected`n`nSelect types you want to delete", "Error", "OK", "Error") + return + } + + if(([System.Windows.MessageBox]::Show("Are you sure you want to delete all objects of the selected type(s)?`n`n$selCount type(s) selected", "Delete Objects?", "YesNo", "Warning")) -ne "Yes") + { + return + } + + Write-Status "Delete objects" -Block + Write-Log "****************************************************************" + Write-Log "Start bulk delete" + Write-Log "****************************************************************" + + foreach($item in ($global:dgBulkDeleteObjects.ItemsSource | Where Selected -eq $true)) + { + Write-Log "----------------------------------------------------------------" + Write-Log "Delete $($item.ObjectType.Title) objects" + Write-Log "----------------------------------------------------------------" + + $url = $item.ObjectType.API + if($item.ObjectType.QUERYLIST) + { + $url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())" + } + + try + { + Write-Status "Get $($item.Title) objects" -Force + $objects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) + foreach($obj in $objects) + { + Write-Status "Delete $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force + Remove-GraphObject $obj.Object $obj.ObjectType $folder + } + } + catch + { + Write-LogError "Failed when deleting $($item.Title) objects" $_.Exception + } + } + + Write-Log "****************************************************************" + Write-Log "Bulk delete finished" + Write-Log "****************************************************************" + Show-GraphObjects + Write-Status "" + }) + + + Show-ModalForm "Bulk Delete" $script:deleteForm -HideButtons +} + function Get-GraphFileObjects { param($path, $Exclude = @("*_settings.json","*_assignments.json"), $SelectedStatus = $true, $ObjectType = $global:curObjectType) @@ -1788,6 +1926,82 @@ function Import-GraphObject $newObj } +function Remove-GraphObjects +{ + $objectsToDelete = @() + if(($global:dgObjects.ItemsSource | Where IsSelected -eq $true).Count -gt 0) + { + # Delete checked items + $objectsToDelete += ($global:dgObjects.ItemsSource | Where IsSelected -eq $true) + } + elseif($global:dgObjects.SelectedItem) + { + # Delete the selected item + $objectsToDelete += $global:dgObjects.SelectedItem + } + + if($objectsToDelete.Count -eq 0) + { + [System.Windows.MessageBox]::Show("No object selected`n`nSelect items you want to delete", "Error", "OK", "Error") + return + } + + if(([System.Windows.MessageBox]::Show("Are you sure you want to delete $($objectsToDelete.Count) $($global:curObjectType.Title) object(s)?", "Delete Objects?", "YesNo", "Warning")) -ne "Yes") + { + return + } + + foreach($tmpObj in $objectsToDelete) + { + Remove-GraphObject $tmpObj.Object $tmpObj.ObjectType + } + + Show-GraphObjects + Write-Status "" +} + +function Remove-GraphObject +{ + param($objToRemove, $objectType) + + $strAPI = $null + if($objectType.PreDeleteCommand) + { + $ret = & $objectType.PreDeleteCommand $objToRemove $objectType + if($ret -is [HashTable]) + { + if($ret.ContainsKey("Delete") -and $ret["Delete"] -eq $false) + { + # Delete handled manually or aborted + return $false + } + + if($ret.ContainsKey("API")) + { + $strAPI = $ret["API"] + } + } + } + + if($strAPI) + { + $api = $strAPI + } + elseif($objectType.DELETEAPI) + { + $api = $objectType.DELETEAPI + } + else + { + $api = $objectType.API + } + + Write-Status "Delete $((Get-GraphObjectName $objToRemove $objectType))" + $strAPI = ($api + "/$($objToRemove.Id)") + Write-Log "Delete $($objectType.Title) object $((Get-GraphObjectName $objToRemove $objectType))" + Invoke-GraphRequest -Url $strAPI -HttpMethod "DELETE" -ODataMetadata "none" +} + function Copy-GraphObject { if(-not $dgObjects.SelectedItem) @@ -1940,14 +2154,25 @@ function Add-GraphBulkMenu $menuItem = [System.Windows.Controls.MenuItem]::new() $menuItem.Header = "_Bulk" $menuItem.Name = "EMBulk" + $subItem = [System.Windows.Controls.MenuItem]::new() $subItem.Header = "_Export" $subItem.Add_Click({Show-GraphBulkExportForm}) $menuItem.AddChild($subItem) | Out-Null + $subItem = [System.Windows.Controls.MenuItem]::new() $subItem.Header = "_Import" $subItem.Add_Click({Show-GraphBulkImportForm}) $menuItem.AddChild($subItem) | Out-Null + + $subItem = [System.Windows.Controls.MenuItem]::new() + $subItem.Header = "_Delete" + $subItem.Name = "mnuBulkDelete" + $allowBulkDelete = Get-SettingValue "EMAllowBulkDelete" + # Add it hidden even if not enabled, the save settings will enable it + $subItem.Visibility = (?: ($allowBulkDelete -eq $true) "Visible" "Collapsed") + $subItem.Add_Click({Show-GraphBulkDeleteForm}) + $menuItem.AddChild($subItem) | Out-Null $mnuMain.Items.Insert(1,$menuItem) | Out-Null } \ No newline at end of file diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 596e814..e3b3982 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -1,5 +1,25 @@ # Release Notes +## 3.1.2 - 2021-06-20 + +**New features** + +- Delete and Bulk Delete - Delete selected items or delete ALL items of selected object types + + **Note:** This must be enabled in the settings. They are not visible by default. + + **WARNING:** Use this carefully! It will delete profiles and policies in Intune. + +- Support for new object Health Scripts + +- Object permissions is now handled by ViewObject and authentication provider. This is to support future view extensions. + +**Fixes** + +* Azure Role Read permission can be disabled in settings +* Minor UI changes e.g. List Boxes for bulk Import/Export changed to DataGrid +* Minor bulk export fixes + ## 3.1.1 - 2021-06-16 **New features** diff --git a/Xaml/BulkDeleteForm.xaml b/Xaml/BulkDeleteForm.xaml new file mode 100644 index 0000000..be523e6 --- /dev/null +++ b/Xaml/BulkDeleteForm.xaml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + +