This commit is contained in:
Mikael Karlsson
2021-07-12 20:24:27 +10:00
parent 06688e7193
commit fb46337f98
31 changed files with 907 additions and 182 deletions

View File

@@ -12,7 +12,7 @@ This module handles the WPF UI
function Get-ModuleVersion function Get-ModuleVersion
{ {
'3.1.3' '3.1.4'
} }
function Start-CoreApp function Start-CoreApp
@@ -914,14 +914,17 @@ function Add-SettingCheckBox
function Add-SettingComboBox function Add-SettingComboBox
{ {
param($id, $value, $itemsData) param($id, $value, $settingObj)
$nameProp = ?? $settingObj.DisplayMemberPath "Name"
$valueProp = ?? $settingObj.SelectedValuePath "Value"
$xaml = @" $xaml = @"
<ComboBox $wpfNS Name="$($id)" DisplayMemberPath="Name" SelectedValuePath="ClientId" /> <ComboBox $wpfNS Name="$($id)" DisplayMemberPath="$($nameProp)" SelectedValuePath="$($valueProp)" />
"@ "@
$xamlObj = [Windows.Markup.XamlReader]::Parse($xaml) $xamlObj = [Windows.Markup.XamlReader]::Parse($xaml)
$xamlObj.ItemsSource = $itemsData $xamlObj.ItemsSource = $settingObj.ItemsSource
if($value) if($value)
{ {
$xamlObj.SelectedValue = $value $xamlObj.SelectedValue = $value
@@ -979,7 +982,7 @@ function Add-SettingValue
} }
elseif($settingValue.Type -eq "List") elseif($settingValue.Type -eq "List")
{ {
$settingObj = Add-SettingComboBox $id $value $settingValue.ItemsSource $settingObj = Add-SettingComboBox $id $value $settingValue
} }
else else
{ {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -11,7 +11,7 @@ Objects can be compared based on Properties or Documentatation info.
function Get-ModuleVersion function Get-ModuleVersion
{ {
'1.0.4' '1.0.5'
} }
function Invoke-InitializeModule function Invoke-InitializeModule
@@ -371,13 +371,7 @@ function Invoke-BulkCompareNamedObjects
Write-Log "Compare $($item.ObjectType.Title) objects" Write-Log "Compare $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------" Write-Log "----------------------------------------------------------------"
$url = $item.ObjectType.API $graphObjects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
if($item.ObjectType.QUERYLIST)
{
$url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())"
}
$graphObjects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
$nameProp = ?? $item.ObjectType.NameProperty "displayName" $nameProp = ?? $item.ObjectType.NameProperty "displayName"
@@ -513,13 +507,7 @@ function Start-BulkCompareExportObjects
{ {
Save-Setting "" "LastUsedFullPath" $folder Save-Setting "" "LastUsedFullPath" $folder
$url = $item.ObjectType.API $graphObjects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
if($item.ObjectType.QUERYLIST)
{
$url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())"
}
$graphObjects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
foreach ($fileObj in @(Get-GraphFileObjects $folder -ObjectType $item.ObjectType)) foreach ($fileObj in @(Get-GraphFileObjects $folder -ObjectType $item.ObjectType))
{ {

View File

@@ -1,6 +1,6 @@
function Get-ModuleVersion function Get-ModuleVersion
{ {
'1.0.0' '1.0.1'
} }
function Invoke-InitializeModule function Invoke-InitializeModule
@@ -106,13 +106,7 @@ function Start-BulkCopyObjects
Write-Log "Copy $($item.ObjectType.Title) objects" Write-Log "Copy $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------" Write-Log "----------------------------------------------------------------"
$url = $item.ObjectType.API $graphObjects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
if($item.ObjectType.QUERYLIST)
{
$url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())"
}
$graphObjects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
$nameProp = ?? $item.ObjectType.NameProperty "displayName" $nameProp = ?? $item.ObjectType.NameProperty "displayName"

View File

@@ -20,7 +20,7 @@ $global:documentationProviders = @()
function Get-ModuleVersion function Get-ModuleVersion
{ {
'1.0.3' '1.0.4'
} }
function Invoke-InitializeModule function Invoke-InitializeModule
@@ -3282,13 +3282,7 @@ function Show-DocumentationForm
{ {
Write-Status "Get $($objectType.Title) objects" Write-Status "Get $($objectType.Title) objects"
$url = $objectType.API $graphObjects = @(Get-GraphObjects -property $objectType.ViewProperties -objectType $objectType)
if($objectType.QUERYLIST)
{
$url = "$($url.Trim())?$($objectType.QUERYLIST.Trim())"
}
$graphObjects = @(Get-GraphObjects -Url $url -property $objectType.ViewProperties -objectType $objectType)
if($objectType.PostListCommand) if($objectType.PostListCommand)
{ {

View File

@@ -10,7 +10,7 @@ This module will also document some objects based on PowerShell functions
function Get-ModuleVersion function Get-ModuleVersion
{ {
'1.0.2' '1.0.3'
} }
function Invoke-InitializeModule function Invoke-InitializeModule
@@ -86,6 +86,13 @@ function Invoke-CDDocumentObject
Properties = @("Name","Value","Category","SubCategory") Properties = @("Name","Value","Category","SubCategory")
} }
} }
elseif($type -eq '#microsoft.graph.notificationMessageTemplate')
{
Invoke-CDDocumentNotification $documentationObj
return [PSCustomObject]@{
Properties = @("Name","Value","Category","SubCategory")
}
}
} }
function Get-CDAllManagedApps function Get-CDAllManagedApps
@@ -2155,7 +2162,9 @@ function Get-CDDocumentPolicySetValue
} }
# ToDo: Add support for all PolicySet items # ToDo: Add support for all PolicySet items
} }
#endregion
#region Custom Profile
function Invoke-CDDocumentCustomOMAUri function Invoke-CDDocumentCustomOMAUri
{ {
param($documentationObj) param($documentationObj)
@@ -2264,6 +2273,168 @@ function Invoke-CDDocumentCustomOMAUri
}) })
} }
} }
}
#endregion
#region Notification
function Invoke-CDDocumentNotification
{
param($documentationObj)
$obj = $documentationObj.Object
$objectType = $documentationObj.ObjectType
$script:objectSeparator = ?? $global:cbDocumentationObjectSeparator.SelectedValue ([System.Environment]::NewLine)
$script:propertySeparator = ?? $global:cbDocumentationPropertySeparator.SelectedValue ","
###################################################
# Basic info
###################################################
Add-BasicDefaultValues $obj $objectType
Add-BasicPropertyValue (Get-LanguageString "TableHeaders.configurationType") (Get-LanguageString "Titles.notifications")
###################################################
# Settings
###################################################
$category = Get-LanguageString "TableHeaders.settings"
if($obj.brandingOptions)
{
$brandingOptions = $obj.brandingOptions.Split(',')
}
else
{
$brandingOptions = @()
}
foreach($brandingOption in @('includeCompanyLogo','includeCompanyName','includeContactInformation','includeCompanyPortalLink'))
{
if($brandingOption -eq 'includeCompanyLogo')
{
$label = (Get-LanguageString "NotificationMessage.companyLogo")
}
elseif($brandingOption -eq 'includeCompanyName')
{
$label = (Get-LanguageString "NotificationMessage.companyName")
}
elseif($brandingOption -eq 'includeContactInformation')
{
$label = (Get-LanguageString "NotificationMessage.companyContact")
}
elseif($brandingOption -eq 'includeCompanyPortalLink')
{
$label = (Get-LanguageString "NotificationMessage.iwLink")
}
if(($brandingOption -in $brandingOptions))
{
$value = Get-LanguageString "BooleanActions.enable"
}
else
{
$value = Get-LanguageString "BooleanActions.disable"
}
Add-CustomSettingObject ([PSCustomObject]@{
Name = $label
Value = $value
EntityKey = $brandingOption
Category = $category
SubCategory = $null
})
}
#$subCategory = Get-LanguageString "NotificationMessage.localeLabel"
$subCategory = Get-LanguageString "NotificationMessage.listTitle"
foreach($template in $obj.localizedNotificationMessages)
{
$first,$second = $template.locale.Split('-')
$baseInfo = [cultureinfo]$first
$lng = $baseInfo.EnglishName.ToLower()
if($first -eq 'en')
{
if($second -eq "US")
{
$lng = ($lng + "US")
}
elseif($second -eq "GB")
{
$lng = ($lng + "UK")
}
}
elseif($first -eq 'es')
{
if($second -eq "es")
{
$lng = ($lng + "Spain")
}
elseif($second -eq "mx")
{
$lng = ($lng + "Mexico")
}
}
elseif($first -eq 'fr')
{
if($second -eq "ca")
{
$lng = ($lng + "Canada")
}
elseif($second -eq "fr")
{
$lng = ($lng + "France")
}
}
elseif($first -eq 'pt')
{
if($second -eq "pt")
{
$lng = ($lng + "Portugal")
}
elseif($second -eq "br")
{
$lng = ($lng + "Brazil")
}
}
elseif($first -eq 'zh')
{
if($second -eq "tw")
{
$lng = ($lng + "Traditional")
}
elseif($second -eq "cn")
{
$lng = ($lng + "Simplified")
}
}
elseif($first -eq 'nb')
{
$lng = "norwegian"
}
$label = Get-LanguageString "NotificationMessage.NotificationMessageTemplatesTab.$lng"
if(-not $label) { continue }
$value = $template.subject
if($template.isDefault)
{
$value = ($value + $script:objectSeparator + (Get-LanguageString "NotificationMessage.isDefaultLocale") + ": " + (Get-LanguageString "SettingDetails.trueOption"))
}
$fullValue = ($value + $script:objectSeparator + $template.messageTemplate)
Add-CustomSettingObject ([PSCustomObject]@{
Name = $label
Value = $fullValue
EntityKey = $template.locale
Category = $category
SubCategory = $subCategory
})
}
} }
#endregion #endregion

View File

@@ -10,7 +10,7 @@ This module is for the Endpoint Manager/Intune View. It manages Export/Import/Co
#> #>
function Get-ModuleVersion function Get-ModuleVersion
{ {
'3.1.6' '3.1.7'
} }
function Invoke-InitializeModule function Invoke-InitializeModule
@@ -27,6 +27,7 @@ function Invoke-InitializeModule
Title = "Application" Title = "Application"
Key = "EMAzureApp" Key = "EMAzureApp"
Type = "List" Type = "List"
SelectedValuePath = "ClientId"
ItemsSource = $global:MSGraphGlobalApps ItemsSource = $global:MSGraphGlobalApps
DefaultValue = "" DefaultValue = ""
SubPath = "EndpointManager" SubPath = "EndpointManager"
@@ -72,22 +73,6 @@ function Invoke-InitializeModule
SubPath = "EndpointManager" SubPath = "EndpointManager"
}) "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 $viewPanel = Get-XamlObject ($global:AppRootFolder + "\Xaml\EndpointManagerPanel.xaml") -AddVariables
Set-EMViewPanel $viewPanel Set-EMViewPanel $viewPanel
@@ -157,6 +142,7 @@ function Invoke-InitializeModule
PostFileImportCommand = { Start-PostFileImportEndpointSecurity @args } PostFileImportCommand = { Start-PostFileImportEndpointSecurity @args }
#PreCopyCommand = { Start-PreCopyEndpointSecurity @args } #PreCopyCommand = { Start-PreCopyEndpointSecurity @args }
PostCopyCommand = { Start-PostCopyEndpointSecurity @args } PostCopyCommand = { Start-PostCopyEndpointSecurity @args }
PreUpdateCommand = { Start-PreUpdateEndpointSecurity @args }
Permissons=@("DeviceManagementConfiguration.ReadWrite.All") Permissons=@("DeviceManagementConfiguration.ReadWrite.All")
GroupId = "EndpointSecurity" GroupId = "EndpointSecurity"
}) })
@@ -170,6 +156,7 @@ function Invoke-InitializeModule
Permissons=@("DeviceManagementConfiguration.ReadWrite.All") Permissons=@("DeviceManagementConfiguration.ReadWrite.All")
Dependencies = @("Locations","Notifications") Dependencies = @("Locations","Notifications")
PostExportCommand = { Start-PostExportCompliancePolicies @args } PostExportCommand = { Start-PostExportCompliancePolicies @args }
PreUpdateCommand = { Start-PreUpdateCompliancePolicies @args }
GroupId = "CompliancePolicies" GroupId = "CompliancePolicies"
}) })
@@ -222,6 +209,9 @@ function Invoke-InitializeModule
PreImportCommand = { Start-PreImportESP @args } PreImportCommand = { Start-PreImportESP @args }
PostExportCommand = { Start-PostExportESP @args } PostExportCommand = { Start-PostExportESP @args }
PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } # Note: Uses same PreDelete as restrictions PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } # Note: Uses same PreDelete as restrictions
PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args } # Note: Uses same PreReplaceCommand as restrictions
PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args } # Note: Uses same PostReplaceCommand as restrictions
PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args } # Note: Uses same PreFilesImportCommand as restrictions
QUERYLIST = "`$filter=endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')" QUERYLIST = "`$filter=endsWith(id,'Windows10EnrollmentCompletionPageConfiguration')"
Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") Permissons=@("DeviceManagementServiceConfig.ReadWrite.All")
SkipRemoveProperties = @('Id') SkipRemoveProperties = @('Id')
@@ -238,6 +228,9 @@ function Invoke-InitializeModule
PostExportCommand = { Start-PostExportEnrollmentRestrictions @args } PostExportCommand = { Start-PostExportEnrollmentRestrictions @args }
PreImportCommand = { Start-PreImportEnrollmentRestrictions @args } PreImportCommand = { Start-PreImportEnrollmentRestrictions @args }
PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args } PreDeleteCommand = { Start-PreDeleteEnrollmentRestrictions @args }
PreReplaceCommand = { Start-PreReplaceEnrollmentRestrictions @args }
PostReplaceCommand = { Start-PostReplaceEnrollmentRestrictions @args }
PreFilesImportCommand = { Start-PreFilesImportEnrollmentRestrictions @args }
Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") Permissons=@("DeviceManagementServiceConfig.ReadWrite.All")
SkipRemoveProperties = @('Id') SkipRemoveProperties = @('Id')
AssignmentsType = "enrollmentConfigurationAssignments" AssignmentsType = "enrollmentConfigurationAssignments"
@@ -387,6 +380,7 @@ function Invoke-InitializeModule
CopyDefaultName = "%displayName% Copy" # '-' is not allowed in the name CopyDefaultName = "%displayName% Copy" # '-' is not allowed in the name
Permissons=@("DeviceManagementServiceConfig.ReadWrite.All") Permissons=@("DeviceManagementServiceConfig.ReadWrite.All")
PreImportAssignmentsCommand = { Start-PreImportAssignmentsAutoPilot @args } PreImportAssignmentsCommand = { Start-PreImportAssignmentsAutoPilot @args }
PreDeleteCommand = { Start-PreDeleteAutoPilot @args }
GroupId = "WinEnrollment" GroupId = "WinEnrollment"
}) })
@@ -834,6 +828,58 @@ function Start-PostCopyEndpointSecurity
} }
} }
function Start-PreUpdateEndpointSecurity
{
param($obj, $objectType, $curObject, $fromObj)
if(-not $fromObj.settings) { return }
$strAPI = "/deviceManagement/intents/$($curObject.Object.id)/updateSettings"
$curObject = Get-GraphObject $curObject.Object $objectType
$curValues = @()
foreach($val in $curObject.Object.settings)
{
if($fromObj.settings | Where { $_.definitionId -eq $val.definitionId}) { continue }
# Set all existing values to null
# Note: This will not remove them from the configured list just set them Not Configured
$curValues += [PSCustomObject]@{
'@odata.type' = $val.'@odata.type'
definitionId = $val.definitionId
id = $val.id
valueJson = "null"
}
}
$curValues += $fromObj.settings
<#
if($curValues.Count -gt 0)
{
$tmpObj = [PSCustomObject]@{
settings = $curValues
}
$json = ConvertTo-Json $tmpObj -Depth 10
# Set all existing values to null
# Note: This will not remove them from the configured list just set them Not Configured
Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null
}
#>
$tmpObj = [PSCustomObject]@{
settings = $curValues
}
Start-GraphPreImport $tmpObj.settings
$json = ConvertTo-Json $tmpObj -Depth 10
Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null
Remove-Property $obj "templateId"
}
#endregion #endregion
#region #region
@@ -892,6 +938,23 @@ function Start-PostExportCompliancePolicies
} }
} }
} }
function Start-PreUpdateCompliancePolicies
{
param($obj, $objectType, $curObject, $fromObj)
$strAPI = "/deviceManagement/deviceCompliancePolicies/$($curObject.Object.id)/scheduleActionsForRules"
$tmpObj = [PSCustomObject]@{
deviceComplianceScheduledActionForRules = $obj.scheduledActionsForRule
}
$json = ConvertTo-Json $tmpObj -Depth 10
Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod "POST" | Out-Null
Remove-Property $obj "scheduledActionsForRule"
}
#endregion #endregion
#region Intune Branding functions #region Intune Branding functions
@@ -1242,7 +1305,7 @@ function Start-PostImportAppProtection
$tmp = $newObject."@odata.type".Split('.')[-1] $tmp = $newObject."@odata.type".Split('.')[-1]
$objectClass = Get-GraphObjectClassName $tmp $objectClass = Get-GraphObjectClassName $tmp
$response = Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($obj.Id)/targetApps" -Content "{ apps: $(ConvertTo-Json $global:ImportObjectInfo.Apps -Depth 10)}" -HttpMethod POST Invoke-GraphRequest -Url "/deviceAppManagement/$objectClass/$($obj.Id)/targetApps" -Content "{ apps: $(ConvertTo-Json $global:ImportObjectInfo.Apps -Depth 10)}" -HttpMethod POST | Out-Null
} }
catch {} catch {}
} }
@@ -1504,12 +1567,63 @@ function Start-PreImportPolicySets
{ {
foreach($prop in ($item.PSObject.Properties | Where {$_.Name -notin $keepProperties})) foreach($prop in ($item.PSObject.Properties | Where {$_.Name -notin $keepProperties}))
{ {
#if($prop.Name -in $keepProperties) { continue }
Remove-Property $item $prop.Name Remove-Property $item $prop.Name
} }
#@("itemType","displayName","status","errorCode") | foreach { Remove-Property $item $_ } #@("itemType","displayName","status","errorCode") | foreach { Remove-Property $item $_ }
} }
} }
function Update-EMPolicySetAssignment
{
param($assignment, $sourceObject, $newObject, $objectType)
$api = "/deviceAppManagement/policySets/$($assignment.SourceId)?`$expand=assignments,items"
$psObj = Invoke-GraphRequest -Url $api -ODataMetadata "Minimal"
if(-not $psObj)
{
return
}
$curItem = $psObj.Items | Where payloadId -eq $sourceObject.Id
if(-not $curItem)
{
return
}
$api = "/deviceAppManagement/policySets/$($assignment.SourceId)/update"
$curItemClone = $curItem | ConvertTo-Json -Depth 10 | ConvertFrom-Json
$newItem = $curItem | ConvertTo-Json -Depth 10 | ConvertFrom-Json
$newItem.payloadId = $newObject.Id
if($newItem.guidedDeploymentTags -is [String] -and [String]::IsNullOrEmpty($newItem.guidedDeploymentTags))
{
$newItem.guidedDeploymentTags = @()
}
$keepProperties = @('@odata.type','payloadId','Settings','guidedDeploymentTags')
#itemType? e.g. #microsoft.graph.iosManagedAppProtection
#priority?
foreach($prop in ($newItem.PSObject.Properties | Where {$_.Name -notin $keepProperties}))
{
Remove-Property $newItem $prop.Name
}
$update = @{}
$update.Add('addedPolicySetItems',@($newItem))
$update.Add('updatedPolicySetItems', @())
$update.Add('deletedPolicySetItems',@($curItemClone.Id))
$json = $update | ConvertTo-Json -Depth 10
Write-Log "Update PolicySet $($psObj.displayName) - Replace: $((Get-GraphObjectName $newObject $objectType))"
Invoke-GraphRequest -Url $api -HttpMethod "POST" -Content $json
}
#endregion #endregion
#endregion Locations #endregion Locations
@@ -1718,6 +1832,38 @@ function Start-PreDeleteEnrollmentRestrictions
@{ "Delete" = $false } @{ "Delete" = $false }
} }
} }
function Start-PreReplaceEnrollmentRestrictions
{
param($obj, $objectType, $sourceObj, $fromFile)
if($sourceObj.Priority -eq 0) { @{ "Replace" = $false } }
}
function Start-PostReplaceEnrollmentRestrictions
{
param($obj, $objectType, $sourceObj, $fromFile)
if($sourceObj.Priority -eq 0) { return }
$api = "/deviceManagement/deviceEnrollmentConfigurations/$($obj.id)/setpriority"
$priority = [PSCustomObject]@{
priority = $sourceObj.Priority
}
$json = $priority | ConvertTo-Json -Depth 10
Write-Log "Update priority for $($obj.displayName) to $($sourceObj.Priority)"
Invoke-GraphRequest $api -HttpMethod "POST" -Content $json
}
function Start-PreFilesImportEnrollmentRestrictions
{
param($objectType, $filesToImport)
$filesToImport | sort-object -property @{e={$_.Object.priority}}
}
#endregion #endregion
#region ScopeTags #region ScopeTags
@@ -1736,6 +1882,32 @@ function Start-PreImportAssignmentsAutoPilot
Add-EMAssignmentsToObject $obj $objectType $file $assignments Add-EMAssignmentsToObject $obj $objectType $file $assignments
} }
function Start-PreDeleteAutoPilot
{
param($obj, $objectType)
Write-Log "Delete AutoPilot profile assignments"
if(-not $obj.Assignments)
{
$tmpObj = (Get-GraphObject $obj $objectType).Object
}
else
{
$tmpObj = $obj
}
foreach($assignment in $tmpObj.Assignments)
{
if($assignment.Source -ne "direct") { continue }
$api = "/deviceManagement/windowsAutopilotDeploymentProfiles/$($obj.Id)/assignments/$($assignment.Id)"
Invoke-GraphRequest $api -HttpMethod "DELETE"
}
}
#endregion #endregion
#region Health Scripts #region Health Scripts

View File

@@ -10,7 +10,7 @@ This module manages Microsoft Grap fuctions like calling APIs, managing graph ob
#> #>
function Get-ModuleVersion function Get-ModuleVersion
{ {
'3.1.2' '3.1.3'
} }
$global:MSGraphGlobalApps = @( $global:MSGraphGlobalApps = @(
@@ -26,6 +26,25 @@ function Invoke-InitializeModule
$global:LoadedDependencyObject = $null $global:LoadedDependencyObject = $null
$global:MigrationTableCache = $null $global:MigrationTableCache = $null
$script:lstImportTypes = @(
[PSCustomObject]@{
Name = "Always import"
Value = "alwaysImport"
},
[PSCustomObject]@{
Name = "Skip if object exists"
Value = "skipIfExist"
},
[PSCustomObject]@{
Name = "Replace (Preview)"
Value = "replace"
},
[PSCustomObject]@{
Name = "Update (Experimental)"
Value = "update"
}
)
# Make sure MS Graph settings are added before exiting before App Id and Tenant Id is missing # Make sure MS Graph settings are added before exiting before App Id and Tenant Id is missing
Write-Log "Add settings and menu items" Write-Log "Add settings and menu items"
@@ -83,12 +102,53 @@ function Invoke-InitializeModule
Description = "Convert AD synched groups to Azure AD group during import if the group does not exist" Description = "Convert AD synched groups to Azure AD group during import if the group does not exist"
}) "ImportExport" }) "ImportExport"
Add-SettingsObject (New-Object PSObject -Property @{
Title = "Import type"
Key = "ImportType"
Type = "List"
ItemsSource = $script:lstImportTypes
DefaultValue = "alwaysImport"
}) "ImportExport"
Add-SettingsObject (New-Object PSObject -Property @{ Add-SettingsObject (New-Object PSObject -Property @{
Title = "Import Assignments" Title = "Import Assignments"
Key = "ImportAssignments" Key = "ImportAssignments"
Type = "Boolean" Type = "Boolean"
DefaultValue = $true DefaultValue = $true
Description = "Import assignments when importing objects" Description = "Default value for Import assignments when importing objects"
}) "ImportExport"
Add-SettingsObject (New-Object PSObject -Property @{
Title = "Import Scope (Tags)"
Key = "ImportScopeTags"
Type = "Boolean"
DefaultValue = $true
Description = "Default value for Import Scope (Tags) when importing objects"
}) "ImportExport"
Add-SettingsObject (New-Object PSObject -Property @{
Title = "Show Delete button"
Key = "EMAllowDelete"
Type = "Boolean"
DefaultValue = $false
Description = "Allow deleting individual objectes"
}) "ImportExport"
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"
}) "ImportExport"
Add-SettingsObject (New-Object PSObject -Property @{
Title = "Allow update on import (Preview)"
Key = "AllowUpdate"
Type = "Boolean"
DefaultValue = $false
Description = "This will enable the option to update/replace an existing object during import"
}) "ImportExport" }) "ImportExport"
} }
@@ -127,6 +187,7 @@ function Get-GraphAppInfo
function Invoke-GraphAuthenticationUpdated function Invoke-GraphAuthenticationUpdated
{ {
$global:MigrationTableCache = $null $global:MigrationTableCache = $null
$global:MigrationTableCacheId = $null
$global:LoadedDependencyObjects = $null $global:LoadedDependencyObjects = $null
$global:migFileObj = $null $global:migFileObj = $null
} }
@@ -142,7 +203,7 @@ function Invoke-GraphRequest
$Headers, $Headers,
[ValidateSet("GET","POST","OPTIONS","DELETE", "PATCH")] [ValidateSet("GET","POST","OPTIONS","DELETE", "PATCH","PUT")]
[Alias("Method")] [Alias("Method")]
$HttpMethod = "GET", $HttpMethod = "GET",
@@ -240,6 +301,8 @@ function Invoke-GraphRequest
{ {
throw $global:error[0] throw $global:error[0]
} }
if($HttpMethod -eq "PATCH" -and [String]::IsNullOrempty($ret)) { $ret = $true }
} }
catch catch
{ {
@@ -255,14 +318,16 @@ function Invoke-GraphRequest
function Get-GraphObjects function Get-GraphObjects
{ {
param( param(
[Array] [String]
$Url, $Url,
[Array] [Array]
$property = $null, $property = $null,
[Array] [Array]
$exclude, $exclude,
$SortProperty = "displayName", $SortProperty = "displayName",
$objectType) $objectType,
[switch]
$SingleObject)
$objects = @() $objects = @()
@@ -274,8 +339,30 @@ function Get-GraphObjects
$params.Add('ODataMetadata',$objectType.ODataMetadata) $params.Add('ODataMetadata',$objectType.ODataMetadata)
} }
if(-not $url)
{
$url = $objectType.API
}
if($SingleObject -ne $true -and $objectType.QUERYLIST)
{
if(($url.IndexOf('?')) -eq -1)
{
$url = "$($url.Trim())?$($objectType.QUERYLIST.Trim())"
}
else
{
$url = "$($url.Trim())&$($objectType.QUERYLIST.Trim())" # Risky...does not check that the parameter is already in use
}
}
$graphObjects = Invoke-GraphRequest -Url $url @params $graphObjects = Invoke-GraphRequest -Url $url @params
if($SingleObject -ne $true -and $objectType.PostListCommand)
{
$graphObjects = & $objectType.PostListCommand $graphObjects $objectType
}
if($graphObjects -and ($graphObjects | GM -Name Value -MemberType NoteProperty)) if($graphObjects -and ($graphObjects | GM -Name Value -MemberType NoteProperty))
{ {
$retObjects = $graphObjects.Value $retObjects = $graphObjects.Value
@@ -341,18 +428,7 @@ function Show-GraphObjects
$global:grdTitle.Visibility = "Visible" $global:grdTitle.Visibility = "Visible"
} }
$url = $global:curObjectType.API $graphObjects = @(Get-GraphObjects -property $global:curObjectType.ViewProperties -objectType $global:curObjectType)
if($global:curObjectType.QUERYLIST)
{
$url = "$($url.Trim())?$($global:curObjectType.QUERYLIST.Trim())"
}
$graphObjects = @(Get-GraphObjects -Url $url -property $global:curObjectType.ViewProperties -objectType $global:curObjectType)
if($global:curObjectType.PostListCommand)
{
$graphObjects = & $global:curObjectType.PostListCommand $graphObjects $global:curObjectType
}
if(($graphObjects | measure).Count -eq 0) { return } if(($graphObjects | measure).Count -eq 0) { return }
@@ -486,7 +562,7 @@ function Get-GraphObject
} }
elseif($api.IndexOf("`$expand") -gt 1) elseif($api.IndexOf("`$expand") -gt 1)
{ {
$api = ($api + ",") $api = ($api + ",") # A bit risky...assumes that expand is last in the existing query
} }
else else
{ {
@@ -496,7 +572,7 @@ function Get-GraphObject
$api = ($api + ($expand -join ",")) $api = ($api + ($expand -join ","))
} }
$objInfo = Get-GraphObjects -Url $api -property $objectType.ViewProperties -objectType $objectType $objInfo = Get-GraphObjects -Url $api -property $objectType.ViewProperties -objectType $objectType -SingleObject
if($objInfo -and $objectType.PostGetCommand) if($objInfo -and $objectType.PostGetCommand)
{ {
@@ -747,17 +823,11 @@ function Show-GraphBulkExportForm
Write-Log "Export $($item.ObjectType.Title) objects" Write-Log "Export $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------" Write-Log "----------------------------------------------------------------"
$url = $item.ObjectType.API
if($item.ObjectType.QUERYLIST)
{
$url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())"
}
try try
{ {
$folder = Get-GraphObjectFolder $item.ObjectType (Get-XamlProperty $script:exportForm "txtExportPath" "Text") (Get-XamlProperty $script:exportForm "chkAddObjectType" "IsChecked") (Get-XamlProperty $script:exportForm "chkAddCompanyName" "IsChecked") $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 $item.ObjectType.ViewProperties -objectType $item.ObjectType) $objects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
foreach($obj in $objects) foreach($obj in $objects)
{ {
Write-Status "Export $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force Write-Status "Export $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force
@@ -797,13 +867,44 @@ function Show-GraphImportForm
} }
Set-XamlProperty $script:importForm "txtImportPath" "Text" (?? $path (Get-SettingValue "RootFolder")) Set-XamlProperty $script:importForm "txtImportPath" "Text" (?? $path (Get-SettingValue "RootFolder"))
Set-XamlProperty $script:importForm "chkImportAssignments" "IsChecked" (Get-SettingValue "ImportAssignments")
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")
if((Get-SettingValue "AllowUpdate") -eq $true)
{
Set-XamlProperty $script:importForm "lblImportType" "Visibility" "Visible"
Set-XamlProperty $script:importForm "cbImportType" "Visibility" "Visible"
}
$column = Get-GridCheckboxColumn "Selected"
$global:dgObjectsToImport.Columns.Add($column)
$column.Header.IsChecked = $true # All items are checked by default
$column.Header.add_Click({
foreach($item in $global:dgObjectsToImport.ItemsSource)
{
$item.Selected = $this.IsChecked
}
$global:dgObjectsToImport.Items.Refresh()
}
)
# Add Object type column
$binding = [System.Windows.Data.Binding]::new("fileName")
$column = [System.Windows.Controls.DataGridTextColumn]::new()
$column.Header = "File Name"
$column.IsReadOnly = $true
$column.Binding = $binding
$global:dgObjectsToImport.Columns.Add($column)
Add-XamlEvent $script:importForm "browseImportPath" "add_click" ({ Add-XamlEvent $script:importForm "browseImportPath" "add_click" ({
$folder = Get-Folder (Get-XamlProperty $script:importForm "txtImportPath" "Text") "Select root folder for import" $folder = Get-Folder (Get-XamlProperty $script:importForm "txtImportPath" "Text") "Select root folder for import"
if($folder) if($folder)
{ {
Set-XamlProperty $script:importForm "txtImportPath" "Text" $folder Set-XamlProperty $script:importForm "txtImportPath" "Text" $folder
$global:lstFiles.ItemsSource = @(Get-GraphFileObjects $folder) $global:dgObjectsToImport.ItemsSource = @(Get-GraphFileObjects $folder)
Save-Setting "" "LastUsedFullPath" $folder Save-Setting "" "LastUsedFullPath" $folder
Set-XamlProperty $script:importForm "lblMigrationTableInfo" "Content" (Get-MigrationTableInfo) Set-XamlProperty $script:importForm "lblMigrationTableInfo" "Content" (Get-MigrationTableInfo)
} }
@@ -817,8 +918,20 @@ function Show-GraphImportForm
Add-XamlEvent $script:importForm "btnImportSelected" "add_click" { Add-XamlEvent $script:importForm "btnImportSelected" "add_click" {
Write-Status "Import objects" Write-Status "Import objects"
Get-GraphDependencyDefaultObjects Get-GraphDependencyDefaultObjects
foreach ($fileObj in ($global:lstFiles.ItemsSource | Where Selected -eq $true)) $allowUpdate = ((Get-SettingValue "AllowUpdate") -eq $true)
$filesToImport = $global:dgObjectsToImport.ItemsSource | Where Selected -eq $true
if($global:curObjectType.PreFilesImportCommand)
{ {
$filesToImport = & $global:curObjectType.PreFilesImportCommand $global:curObjectType $filesToImport
}
foreach ($fileObj in $filesToImport)
{
if($allowUpdate -and $global:cbImportType.SelectedValue -ne "alwaysImport" -and (Reset-GraphObjet $fileObj $global:dgObjects.ItemsSource))
{
continue
}
Import-GraphFile $fileObj Import-GraphFile $fileObj
} }
Show-GraphObjects Show-GraphObjects
@@ -826,17 +939,9 @@ function Show-GraphImportForm
Write-Status "" Write-Status ""
} }
Add-XamlEvent $script:importForm "chkCheckAll" "add_click" {
foreach($obj in $global:lstFiles.Items)
{
$obj.Selected = $global:chkCheckAll.IsChecked
}
$global:lstFiles.Items.Refresh()
}
Add-XamlEvent $script:importForm "btnGetFiles" "add_click" { Add-XamlEvent $script:importForm "btnGetFiles" "add_click" {
# Used when the user manually updates the path and the press Get Files # Used when the user manually updates the path and the press Get Files
$global:lstFiles.ItemsSource = @(Get-GraphFileObjects $global:txtImportPath.Text) $global:dgObjectsToImport.ItemsSource = @(Get-GraphFileObjects $global:txtImportPath.Text)
if([IO.Directory]::Exists($global:txtImportPath.Text)) if([IO.Directory]::Exists($global:txtImportPath.Text))
{ {
Save-Setting "" "LastUsedFullPath" $global:txtImportPath.Text Save-Setting "" "LastUsedFullPath" $global:txtImportPath.Text
@@ -848,7 +953,7 @@ function Show-GraphImportForm
if($global:txtImportPath.Text) if($global:txtImportPath.Text)
{ {
$global:lstFiles.ItemsSource = @(Get-GraphFileObjects $global:txtImportPath.Text) $global:dgObjectsToImport.ItemsSource = @(Get-GraphFileObjects $global:txtImportPath.Text)
Set-XamlProperty $script:importForm "lblMigrationTableInfo" "Content" (Get-MigrationTableInfo) Set-XamlProperty $script:importForm "lblMigrationTableInfo" "Content" (Get-MigrationTableInfo)
} }
@@ -867,7 +972,16 @@ function Show-GraphBulkImportForm
} }
Set-XamlProperty $script:importForm "txtImportPath" "Text" (?? $path (Get-SettingValue "RootFolder")) Set-XamlProperty $script:importForm "txtImportPath" "Text" (?? $path (Get-SettingValue "RootFolder"))
#Set-XamlProperty $script:importForm "chkAddCompanyName" "IsChecked" (Get-SettingValue "AddCompanyName") Set-XamlProperty $script:importForm "chkImportAssignments" "IsChecked" (Get-SettingValue "ImportAssignments")
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")
if((Get-SettingValue "AllowUpdate") -eq $true)
{
Set-XamlProperty $script:importForm "lblImportType" "Visibility" "Visible"
Set-XamlProperty $script:importForm "cbImportType" "Visibility" "Visible"
}
Add-XamlEvent $script:importForm "browseImportPath" "add_click" ({ Add-XamlEvent $script:importForm "browseImportPath" "add_click" ({
$folder = Get-Folder (Get-XamlProperty $script:importForm "txtImportPath" "Text") "Select root folder for import" $folder = Get-Folder (Get-XamlProperty $script:importForm "txtImportPath" "Text") "Select root folder for import"
@@ -938,6 +1052,8 @@ function Show-GraphBulkImportForm
Get-GraphDependencyDefaultObjects Get-GraphDependencyDefaultObjects
$importedObjects = 0 $importedObjects = 0
$allowUpdate = ((Get-SettingValue "AllowUpdate") -eq $true)
foreach($item in ($script:importObjects | where Selected -eq $true | sort-object -property @{e={$_.ObjectType.ImportOrder}})) foreach($item in ($script:importObjects | where Selected -eq $true | sort-object -property @{e={$_.ObjectType.ImportOrder}}))
{ {
Write-Status "Import $($item.ObjectType.Title) objects" -Force Write-Status "Import $($item.ObjectType.Title) objects" -Force
@@ -946,10 +1062,34 @@ function Show-GraphBulkImportForm
Write-Log "----------------------------------------------------------------" Write-Log "----------------------------------------------------------------"
$folder = Get-GraphObjectFolder $item.ObjectType (Get-XamlProperty $script:importForm "txtImportPath" "Text") (Get-XamlProperty $script:importForm "chkAddObjectType" "IsChecked") $folder = Get-GraphObjectFolder $item.ObjectType (Get-XamlProperty $script:importForm "txtImportPath" "Text") (Get-XamlProperty $script:importForm "chkAddObjectType" "IsChecked")
$graphObjects = $null
if($allowUpdate -and $global:cbImportType.SelectedValue -ne "alwaysImport")
{
try
{
Write-Status "Get $($item.Title) objects" -Force
$graphObjects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
}
catch {}
}
if([IO.Directory]::Exists($folder)) if([IO.Directory]::Exists($folder))
{ {
foreach ($fileObj in @(Get-GraphFileObjects $folder -ObjectType $item.ObjectType)) $filesToImport = Get-GraphFileObjects $folder -ObjectType $item.ObjectType
if($item.ObjectType.PreFilesImportCommand)
{ {
$filesToImport = & $item.ObjectType.PreFilesImportCommand $item.ObjectType $filesToImport
}
foreach ($fileObj in @($filesToImport))
{
if($allowUpdate -and $global:cbImportType.SelectedValue -ne "alwaysImport" -and $graphObjects -and (Reset-GraphObjet $fileObj $graphObjects))
{
$importedObjects++
continue
}
Import-GraphFile $fileObj Import-GraphFile $fileObj
$importedObjects++ $importedObjects++
} }
@@ -1112,16 +1252,10 @@ function Show-GraphBulkDeleteForm
Write-Log "Delete $($item.ObjectType.Title) objects" Write-Log "Delete $($item.ObjectType.Title) objects"
Write-Log "----------------------------------------------------------------" Write-Log "----------------------------------------------------------------"
$url = $item.ObjectType.API
if($item.ObjectType.QUERYLIST)
{
$url = "$($url.Trim())?$($item.ObjectType.QUERYLIST.Trim())"
}
try try
{ {
Write-Status "Get $($item.Title) objects" -Force Write-Status "Get $($item.Title) objects" -Force
$objects = @(Get-GraphObjects -Url $url -property $item.ObjectType.ViewProperties -objectType $item.ObjectType) $objects = @(Get-GraphObjects -property $item.ObjectType.ViewProperties -objectType $item.ObjectType)
foreach($obj in $objects) foreach($obj in $objects)
{ {
Write-Status "Delete $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force Write-Status "Delete $($item.Title): $((Get-GraphObjectName $obj.Object $obj.ObjectType))" -Force
@@ -1225,72 +1359,7 @@ function Import-GraphFile
if($newObj -and $objClone.Assignments -and $global:chkImportAssignments.IsChecked -eq $true) if($newObj -and $objClone.Assignments -and $global:chkImportAssignments.IsChecked -eq $true)
{ {
$preConfig = $null Import-GraphObjectAssignment $newObj $file.ObjectType $objClone.Assignments $file.FileInfo.FullName | Out-Null
if($file.ObjectType.PreImportAssignmentsCommand)
{
$preConfig = & $file.ObjectType.PreImportAssignmentsCommand $newObj $file.ObjectType $file.FileInfo.FullName $objClone.Assignments
}
###### Import Assignments ######
if($preConfig -isnot [Hashtable]) { $preConfig = @{} }
if($preConfig["Import"] -eq $false) { return } # Assignment managed manually so skip further processing
$api = ?? $preConfig["API"] "$($file.ObjectType.API)/$($newObj.Id)/assign"
$method = ?? $preConfig["Method"] "POST"
$keepProperties = ?? $file.ObjectType.AssignmentProperties @("target")
$keepTargetProperties = ?? $file.ObjectType.AssignmentTargetProperties @("@odata.type","groupId")
$ObjectAssignments = @()
foreach($assignment in $objClone.Assignments)
{
if($assignment.target.UserId -or ($assignment.Source -and $assignment.Source -ne "direct"))
{
# E.g. Source could be PolicySet...so should not be added here
continue
}
$assignment.Id = ""
foreach($prop in $assignment.PSObject.Properties)
{
if($prop.Name -in $keepProperties) { continue }
Remove-Property $assignment $prop.Name
}
foreach($prop in $assignment.target.PSObject.Properties)
{
if($prop.Name -in $keepTargetProperties) { continue }
Remove-Property $assignment.target $prop.Name
}
$ObjectAssignments += $assignment
}
$objClone.Assignments = $ObjectAssignments
if(($objClone.Assignments | measure).Count -gt 0)
{
$json = "{ `"$((?? $file.ObjectType.AssignmentsType "assignments"))`": "
$strAssign = "$((Update-JsonForEnvironment ($objClone.Assignments | ConvertTo-Json -Depth 10)))"
# Array characters [ ] is not included if there is only one assignment
# Added them if they are missing
if($strAssign.Trim().StartsWith("[") -eq $false)
{
$strAssign = (" [ " + $strAssign + " ] ")
}
$json = ($json + $strAssign + "}")
if($json)
{
$objAssign = Invoke-GraphRequest $api -HttpMethod $method -Content $json
}
}
if($assignmentsProcessed -ne $true -and $file.ObjectType.PostImportAssignmentsCommand)
{
& $file.ObjectType.PostImportAssignmentsCommand $newObj $file.ObjectType $file.FileInfo.FullName $objAssign
}
} }
} }
catch catch
@@ -1299,6 +1368,227 @@ function Import-GraphFile
} }
} }
function Reset-GraphObjet
{
param($fileObj, $objectList)
$nameProp = ?? $fileObj.ObjectType.NameProperty "displayName"
$curObject = $objectList | Where { $_.Object.$nameProp -eq $fileObj.Object.$nameProp -and $_.Object.'@OData.Type' -eq $fileObj.Object.'@OData.Type' }
if($global:cbImportType.SelectedValue -eq "skipIfExist" -and ($curObject | measure).Count -gt 0)
{
Write-Log "Objects with name $($fileObj.Object.$nameProp) already exists. Object will not be imported"
return $true
}
elseif(($curObject | measure).Count -gt 1)
{
Write-Log "Multiple objects return with name $($fileObj.Object.$nameProp). Object will not be imported or replaced" 2
return $true
}
elseif(($curObject | measure).Count -eq 1)
{
Write-Log "Update $((Get-GraphObjectName $fileObj.Object $fileObj.ObjectType)) with id $($curObject.Object.Id)"
$objectType = $fileObj.ObjectType
# Clone the object before removing properties
$obj = $fileObj.Object | ConvertTo-Json -Depth 10 | ConvertFrom-Json
Start-GraphPreImport $obj $objectType
Remove-Property $obj "Assignments"
Remove-Property $obj "isAssigned"
if($global:cbImportType.SelectedValue -eq "update")
{
$params = @{}
$strAPI = (?? $objectType.APIPATCH $objectType.API) + "/$($curObject.Object.Id)"
$method = "PATCH"
if($objectType.PreUpdateCommand)
{
$ret = & $objectType.PreUpdateCommand $obj $objectType $curObject $fileObj.Object
if($ret -is [HashTable])
{
if($ret.ContainsKey("Import") -and $ret["Import"] -eq $false)
{
# Import handled manually
return $false
}
if($ret.ContainsKey("API"))
{
$strAPI = $ret["API"]
}
if($ret.ContainsKey("Method"))
{
$method = $ret["Method"]
}
if($ret.ContainsKey("AdditionalHeaders") -and $ret["AdditionalHeaders"] -is [HashTable])
{
$params.Add("AdditionalHeaders",$ret["AdditionalHeaders"])
}
}
}
$json = ConvertTo-Json $obj -Depth 10
if($true) #$global:MigrationTableCacheId -ne $global:Organization.Id)
{
# Call Update-JsonForEnvironment before importing the object
# E.g. PolicySets contains references, AppConfiguration policies reference apps etc.
$json = Update-JsonForEnvironment $json
}
$objectUpdated = (Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod $method @params)
if($objectUpdated)
{
Write-Log "Object updated successfully"
}
if($objectUpdated -and $objectType.PostUpdateCommand)
{
# Reload the updated object
$updatedObject = Get-GraphObject $curObject.Object $objectType
& $objectType.PostUpdateCommand $updatedObject $fileObj
}
return $true
}
elseif($global:cbImportType.SelectedValue -eq "replace")
{
$replace = $true
$import = $true
$delete = $true
if($objectType.PreReplaceCommand)
{
$ret = & $objectType.PreReplaceCommand $obj $objectType $curObject.Object $fileObj
if($ret -is [Hashtable])
{
if($ret["Replace"] -eq $false) { $replace = $false }
if($ret["Import"] -eq $false) { $import = $false }
if($ret["Delete"] -eq $false) { $delete = $false }
}
}
if($import)
{
$newObj = Import-GraphObject $obj $objectType $fileObj.FileInfo.FullName
}
if($newObj -and $replace)
{
if($objectType.PostReplaceCommand)
{
$ret = & $objectType.PostReplaceCommand $newObj $objectType $curObject.Object $fileObj
if($ret -is [Hashtable])
{
if($ret["Delete"] -eq $false) { $delete = $false }
}
}
# Load all information about current object to include assignments
$curObject = Get-GraphObject $curObject.Object $objectType
$refAssignments = $curObject.Object.Assignments | Where { $_.Source -ne "direct" }
if($refAssignments)
{
foreach($refAssignment in $refAssignments)
{
if($refAssignment.Source -eq "policySets")
{
Update-EMPolicySetAssignment $refAssignment $curObject $newObj $objectType
}
}
}
Import-GraphObjectAssignment $newObj $objectType $curObject.Object.Assignments $fileObj.FileInfo.FullName -CopyAssignments | Out-Null
if($delete)
{
Remove-GraphObject $curObject.Object $objectType
}
}
elseif($replace -eq $false) # Might not be 100% correct. Replace -eq $false probably means that the object was patched and not imported eg default enrollment restrictions etc.
{
Write-Log "Failed to import file for $($fileObj.Object.$nameProp) ($($objectType.Title))" 2
}
return $true
}
}
# No object to update. Import the file
return $false
}
function Import-GraphObjectAssignment
{
param($obj, $objectType, $assignments, $fromFile, [switch]$CopyAssignments)
if(($assignments | measure).Count -eq 0) { return }
$preConfig = $null
$clonedAssignments = $assignments | ConvertTo-Json -Depth 10 | ConvertFrom-Json
if($objectType.PreImportAssignmentsCommand)
{
$preConfig = & $objectType.PreImportAssignmentsCommand $obj $objectType $fromFile $clonedAssignments
}
if($preConfig -isnot [Hashtable]) { $preConfig = @{} }
if($preConfig["Import"] -eq $false) { return } # Assignment managed manually so skip further processing
$api = ?? $preConfig["API"] "$($objectType.API)/$($newObj.Id)/assign"
$method = ?? $preConfig["Method"] "POST"
$keepProperties = ?? $objectType.AssignmentProperties @("target")
$keepTargetProperties = ?? $objectType.AssignmentTargetProperties @("@odata.type","groupId")
$ObjectAssignments = @()
foreach($assignment in $clonedAssignments)
{
if(($assignment.target.UserId -and $CopyAssignments -ne $true) -or ($assignment.Source -and $assignment.Source -ne "direct"))
{
# E.g. Source could be PolicySet...so should not be added here
continue
}
$assignment.Id = ""
foreach($prop in $assignment.PSObject.Properties)
{
if($prop.Name -in $keepProperties) { continue }
Remove-Property $assignment $prop.Name
}
foreach($prop in $assignment.target.PSObject.Properties)
{
if($prop.Name -in $keepTargetProperties) { continue }
Remove-Property $assignment.target $prop.Name
}
$ObjectAssignments += $assignment
}
if($ObjectAssignments.Count -eq 0) { return } # No "Direct" assignments
$htAssignments = @{}
$htAssignments.Add((?? $objectType.AssignmentsType "assignments"), @($ObjectAssignments))
$json = $htAssignments | ConvertTo-Json -Depth 10
if($CopyAssignments -ne $true)
{
$json = Update-JsonForEnvironment $json
}
$objAssign = Invoke-GraphRequest $api -HttpMethod $method -Content $json
if($objectType.PostImportAssignmentsCommand)
{
& $objectType.PostImportAssignmentsCommand $obj $objectType $fromFile $objAssign
}
}
#endregion #endregion
#region Migration Info #region Migration Info
@@ -1564,13 +1854,14 @@ function Get-GraphMigrationObjectsFromFile
$migFileName = Get-GraphMigrationTableForImport $migFileName = Get-GraphMigrationTableForImport
if(-not $migFileName) { return } if(-not $migFileName) { return }
$global:MigrationTableCache = @()
$migFileObj = ConvertFrom-Json (Get-Content $migFileName -Raw) $migFileObj = ConvertFrom-Json (Get-Content $migFileName -Raw)
# No need to translate migrated objects in the same environment as exported # No need to translate migrated objects in the same environment as exported
if($migFileObj.TenantId -eq $global:organization.Id) { return } if($migFileObj.TenantId -eq $global:organization.Id) { return }
$global:MigrationTableCache = @()
$global:MigrationTableCacheId = $migFileObj.TenantId
Write-Status "Loading migration objects" Write-Status "Loading migration objects"
if($global:chkImportAssignments.IsChecked -eq $true) if($global:chkImportAssignments.IsChecked -eq $true)
@@ -1940,6 +2231,11 @@ function Import-GraphObject
$newObj = (Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod $method @params) $newObj = (Invoke-GraphRequest -Url $strAPI -Content $json -HttpMethod $method @params)
if($newObj -and $method -eq "POST")
{
Write-Log "$($objectType.Title) object imported successfully with id: $($newObj.Id)"
}
if($newObj -and $objectType.PostImportCommand) if($newObj -and $objectType.PostImportCommand)
{ {
& $objectType.PostImportCommand $newObj $objectType $fromFile & $objectType.PostImportCommand $newObj $objectType $fromFile

View File

@@ -10,7 +10,22 @@ The script also support dependencies e.g. an App Protection is depending on an A
This PowerShell application is based on the foundation modules CloudAPIPowerShellManagement and Core. These modules manages UI, settings, logging etc. The functionality for the application is located in the extension modules. This makes it easy to add/remove features, views etc. Additional features will be added... This PowerShell application is based on the foundation modules CloudAPIPowerShellManagement and Core. These modules manages UI, settings, logging etc. The functionality for the application is located in the extension modules. This makes it easy to add/remove features, views etc. Additional features will be added...
**Security note:** Since the scripts are not signed, a warning might be display when running it and files might be blocked. The script will unblock all files. This is to avoid issues that it fails to load the MSAL library etc. If there are any security concerns, the PowerShell code can be reviewed. The DLL files are downloaded from Microsoft repositories, see links below. These files can be downloaded and replaced. The DLL files *CAN* be removed but MSAL is a pre-requisite for login. The script will try to find the DLL in the Az or MSAL.PS module if not found in the script root directory. DLL files are included to reduce dependencies. **Security note:** Since the scripts are not signed, a warning might be display when running it and files might be blocked. The script will unblock all files. This is to avoid issues that it fails to load the MSAL library etc. If there are any security concerns, the PowerShell code can be reviewed and the DLL files can be downloaded manually from Microsoft repositories, see links below. The DLL files *CAN* be removed but MSAL is a pre-requisite for authentication. The script will try to find the DLL in the Az or MSAL.PS module if not found in the script root directory. DLL files are included to reduce dependencies.
## Starting the App
Before starting the app:
* The CMD files needs to be unblocked before the app can be started. The app can be started without it but Windows will prompt with a security warning.
* The script will unblock all other files
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
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.
## Documentation ## Documentation
@@ -18,6 +33,30 @@ This script has an extension that can document profiles and policies in Intune.
See [Documentation](Documentation.md) for more information See [Documentation](Documentation.md) for more information
## Import
The script can import the exported json files in multiple ways.
* **Always import:** The script will try to import the file. It will not check if it exists.
This is the default behavior
* **Skip if object exists:** The script will look if there is an existing object with the same name and type. It will not import the file if existing object is detected
* **Replace (Preview):** If a existing object is detected, the script will
* Import the file without assignments
* Copy assignments from the existing object
* Run PostReplace commands - Priority will be set for Enrollment Restrictions etc.
* Update PolicySets object(s) to use the new imported object (detected by policySet assignments)
* Delete the original object
* **Update (Experimental):** This will update the existing object.
Note: This is not fully implemented yet. It only works on a few object types
**WARNING:** Use Replace with caution! Replace will delete the existing object after the imported object is updated but could cause issues in the environment if something in the process goes wrong. Verify the process in a test environment before using this!
**Recommendation:** Backup all policies before running Replace/Update.
The Replace/Update feature can be used in a scenario where all profiles/policies are managed in a separate reference (Dev/Test) and then implemented in one or more destination environment. The existing objects will then be reset to have the same settings as the reference environment
**Note:** This must be turned on in Settings by enabling the **Allow update on import (Preview)** setting.
## Comparison ## Comparison
This script has an extension that can compare objects in Intune with exported json files. It will display a data grid with the values and highlight updated values with red. This script has an extension that can compare objects in Intune with exported json files. It will display a data grid with the values and highlight updated values with red.
@@ -114,9 +153,9 @@ Some MSAL functionalities are based on [MSAL.PS Module](https://github.com/Azure
## Known Issues ## Known Issues
Device Configuration and App Configuration objects are split up in different object types. They are using different Graph APIs and each object type in the menu uses one API. This is also why all Endpoint Security objects are of the same object type. They use the same API but are separated based on the Baseline Template Id they us. Device Configuration and App Configuration objects are split up in different object types. They are using different Graph APIs and each object type in the menu uses one API. This is also why all Endpoint Security objects are of the same object type. They use the same API but are separated based on the Baseline Template Id they use.
Android Store Apps are **not** imported. The create method is documented in Microsoft Graph but it's not working. Looks like these apps must be synched from Google Play. Android Store Apps are **not** imported. The Create API is documented in Microsoft Graph but it's not working. Looks like these apps must be synched from Google Play.
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 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.
@@ -134,7 +173,7 @@ See [Documentation](Documentation.md) for issues regarding the documentation pro
## TIP ## TIP
Check the log file for errors. The UI might not show errors why login failed etc. The log uses the Endpoint Configuration Manager (SCCM) format and it is best viewed with CMTrace. An old version can be downloaded [here](https://www.microsoft.com/en-us/download/confirmation.aspx?id=50012). Check the log file for errors. The UI might not show errors why login failed etc. The log uses the Endpoint Configuration Manager (SCCM) format and it is best viewed with CMTrace or OneTrace. An old version of CMTrace can be downloaded [here](https://www.microsoft.com/en-us/download/confirmation.aspx?id=50012).
## License ## License

View File

@@ -1,5 +1,28 @@
# Release Notes # Release Notes
## 3.1.7 - 2021-07-12
**New features**
- Support for documenting Notifications
- **PREVIEW/EXPERIMENTAL** - Support for Replace/Update existing profiles/policies during import.
See the Import section in the [Readme](Readme.md#Import) file for more information
This is based on the feature request in [Issue 17](https://github.com/Micke-K/IntuneManagement/issues/17)
**Fixes**
* Fixed bug that caused an exception when listing App Protection objects and only one object existed in the environment.
See [Issue 15](https://github.com/Micke-K/IntuneManagement/issues/15) for more info
* Import Priority based objects in the priority order specified in the files (Enrolment Restrictions and Autopilot profiles)
* Set default settings for the options in the Import forms (Based on Settings)
* Delete Autopilot profiles with assignments
* Moved the assignments import to a separate function
## 3.1.6 - 2021-07-07 ## 3.1.6 - 2021-07-07
**Fixes** **Fixes**

View File

@@ -14,6 +14,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" /> <ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
@@ -68,9 +69,25 @@
</StackPanel> </StackPanel>
<CheckBox Grid.Column='1' Grid.Row='5' Name='chkReplaceDependencyIDs' VerticalAlignment="Center" IsChecked="true" /> <CheckBox Grid.Column='1' Grid.Row='5' Name='chkReplaceDependencyIDs' VerticalAlignment="Center" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='6' Margin="0,0,5,0" Name="lblImportType" Visibility="Collapsed">
<Label Content="Import Type" />
<Rectangle Style="{DynamicResource InfoIcon}">
<Rectangle.ToolTip>
<TextBlock>
Specify how files are imported<LineBreak />
<Bold>Always import</Bold>: Always try to import the object. No detction of existing object (Default)<LineBreak />
<Bold>Skip if object exists</Bold>: Skip import if there is an existing object with the same name and type<LineBreak />
<Bold>Replace</Bold>: If an object is detected, it will be deleted. The assignments will be copied to the new imported object<LineBreak />
<Bold>Update</Bold>: If an object is detected, settings will be replaced from the import file
</TextBlock>
</Rectangle.ToolTip>
</Rectangle>
</StackPanel>
<ComboBox Name="cbImportType" Margin="0,5,0,0" MinWidth="250" Grid.Row='6' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" Visibility="Collapsed" />
</Grid> </Grid>
<Grid Grid.Row='1' VerticalAlignment="Stretch"> <Grid Grid.Row='1' VerticalAlignment="Stretch" Margin="0,5,0,0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>

View File

@@ -13,6 +13,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" /> <ColumnDefinition Width="Auto" SharedSizeGroup="TitleColumn" />
@@ -59,9 +60,32 @@
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Replaces IDs of dependency objects e.g. App Config references Applications. Increases import time but makes sure objects are imported correctly. Note: References objects must exist!" /> <Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Replaces IDs of dependency objects e.g. App Config references Applications. Increases import time but makes sure objects are imported correctly. Note: References objects must exist!" />
</StackPanel> </StackPanel>
<CheckBox Grid.Column='1' Grid.Row='4' Name='chkReplaceDependencyIDs' VerticalAlignment="Center" IsChecked="true" /> <CheckBox Grid.Column='1' Grid.Row='4' Name='chkReplaceDependencyIDs' VerticalAlignment="Center" IsChecked="true" />
<StackPanel Orientation="Horizontal" Grid.Row='5' Margin="0,0,5,0" Name="lblUpdateExistsinObject" Visibility="Collapsed">
<Label Content="Update existing object" />
<Rectangle Style="{DynamicResource InfoIcon}" ToolTip="Replaces IDs of dependency objects e.g. App Config references Applications. Increases import time but makes sure objects are imported correctly. Note: References objects must exist!" />
</StackPanel>
<CheckBox Grid.Column='1' Grid.Row='5' Name='chkUpdateExistingObject' VerticalAlignment="Center" IsChecked="false" Visibility="Collapsed"/>
<StackPanel Orientation="Horizontal" Grid.Row='5' Margin="0,0,5,0" Name="lblImportType" Visibility="Collapsed">
<Label Content="Import Type" />
<Rectangle Style="{DynamicResource InfoIcon}">
<Rectangle.ToolTip>
<TextBlock>
Specify how files are imported<LineBreak />
<Bold>Always import</Bold>: Always try to import the object. No detction of existing object (Default)<LineBreak />
<Bold>Skip if object exists</Bold>: Skip import if there is an existing object with the same name and type<LineBreak />
<Bold>Replace</Bold>: If an object is detected, it will be deleted. The assignments will be copied to the new imported object<LineBreak />
<Bold>Update</Bold>: If an object is detected, settings will be replaced from the import file
</TextBlock>
</Rectangle.ToolTip>
</Rectangle>
</StackPanel>
<ComboBox Name="cbImportType" Margin="0,5,0,0" MinWidth="250" Grid.Row='5' Grid.Column="1" HorizontalAlignment="Left"
DisplayMemberPath="Name" SelectedValuePath="Value" Visibility="Collapsed" />
</Grid> </Grid>
<Grid Grid.Row='1' VerticalAlignment="Stretch" > <Grid Grid.Row='1' VerticalAlignment="Stretch" Margin="0,5,0,0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
@@ -81,6 +105,8 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<DataGrid Name="dgObjectsToImport" Grid.Column='1' CanUserAddRows="False" AutoGenerateColumns="False" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="White" Margin="0,0,0,5" />
<!--
<ListBox Name="lstFiles" Grid.Column='1' <ListBox Name="lstFiles" Grid.Column='1'
SelectionMode="Single" SelectionMode="Single"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
@@ -101,6 +127,8 @@
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
<CheckBox Grid.Column='1' Grid.Row='2' Margin="7,2,0,0" IsChecked="true" Name="chkCheckAll" ToolTip="Select/Deselect all" /> <CheckBox Grid.Column='1' Grid.Row='2' Margin="7,2,0,0" IsChecked="true" Name="chkCheckAll" ToolTip="Select/Deselect all" />
-->
</Grid> </Grid>
<StackPanel Name="spImportSubMenu" Grid.Row='2' Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,5,0,5"> <StackPanel Name="spImportSubMenu" Grid.Row='2' Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,5,0,5">
<Button Name="btnGetFiles" Content="Get files" Width='100' Margin="5,0,0,0" /> <Button Name="btnGetFiles" Content="Get files" Width='100' Margin="5,0,0,0" />