mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 10:55:38 +02:00
1655 lines
68 KiB
PowerShell
1655 lines
68 KiB
PowerShell
enum APIAccess
|
|
{
|
|
Full
|
|
Limited
|
|
None
|
|
}
|
|
|
|
enum OMetadata
|
|
{
|
|
Full
|
|
Minimal
|
|
None
|
|
Skip
|
|
}
|
|
|
|
# A group of APIs eg Device Configurations has Settings Catalog, Templates etc.
|
|
class IntunePolicyGroupBase
|
|
{
|
|
Hidden [string]$_ID = ""
|
|
Hidden [string]$_Name = ""
|
|
Hidden [Object]$_Icon = $null
|
|
Hidden [IntunePolicyTypeBase[]]$_PolicyTypes = @()
|
|
# Permission state for the left-nav colour, stamped by
|
|
# Update-IntuneAccessLevels (Internal/AccessLevel.ps1). Full renders
|
|
# normally, Limited orange, None red. Groups aggregate their member types:
|
|
# None only when nothing in the group is usable. AccessInfo is the tooltip
|
|
# breakdown. Both stay at their defaults when there is no token to diff
|
|
# against, so an unreadable token shows no colour rather than all red.
|
|
[APIAccess]$AccessType = [APIAccess]::Full
|
|
[string]$AccessInfo = ""
|
|
Hidden [Object]$IconImage = $null
|
|
# Column list for the group view. $null = derive from the member types
|
|
# (Get-IntuneDefaultColumns, Internal/IntuneViewColumns.ps1); set only to
|
|
# hard-override the derivation.
|
|
Hidden [string[]]$_ViewProperties = $null
|
|
# Columns the whole group can fill, appended after Policy type - e.g.
|
|
# "ScriptType=Script type". Keep to values every member type supplies.
|
|
Hidden [string[]]$_ExtraColumns = @()
|
|
# $false hides the Policy type column, $true forces it; $null = show it when
|
|
# the group has more than one member type.
|
|
Hidden [object]$_ShowPolicyTypeColumn = $null
|
|
# $false hides Platform, $true forces it even when most members cannot fill it
|
|
# (Tenant administration: assignment filters are worth the blanks); $null = the
|
|
# half-the-members rule.
|
|
Hidden [object]$_ShowPlatformColumn = $null
|
|
# $null = all buttons / all bulk operations. Set to e.g. @("Export","View")
|
|
# for read-only groups (UI button gating + bulk-form type lists both check it).
|
|
Hidden [object]$_ShowButtons = $null
|
|
|
|
IntunePolicyGroupBase()
|
|
{
|
|
if($this.GetType().Name -eq "IntunePolicyGroupBase") {
|
|
throw "Abstract class. Object cannot be created"
|
|
}
|
|
elseif($script:SingletonObjects.ContainsKey($this.GetType().Name) -eq $true) {
|
|
throw "Only one $($this.GetType().Name) object can be created"
|
|
}
|
|
|
|
Add-SingletonObject $this.GetType().Name $this
|
|
|
|
([IntunePolicyGroupBase]$this).Init()
|
|
}
|
|
|
|
# Hidden Functions
|
|
Hidden Init()
|
|
{
|
|
Add-ObjectProperty $this "ID" { $this._ID }
|
|
Add-ObjectProperty $this "Title" { $this._Name }
|
|
Add-ObjectProperty $this "Icon" { Get-StringOrDefault $this._Icon $this._ID }
|
|
#Add-ObjectProperty $this "IconImage" { return $this._IconImage }
|
|
Add-ObjectProperty $this "PolicyTypes" { $this._PolicyTypes }
|
|
Add-ObjectProperty $this "ViewProperties" { ?? $this._ViewProperties (Get-IntuneDefaultColumns $this) }
|
|
Add-ObjectProperty $this "ShowButtons" { $this._ShowButtons }
|
|
}
|
|
|
|
# Public Functions
|
|
[void]AddPolicyType([IntunePolicyTypeBase]$PolicyType)
|
|
{
|
|
if(-not $PolicyType) { return }
|
|
|
|
if($null -eq ($this._PolicyTypes | Where-Object ID -eq $PolicyType.ID)) {
|
|
$this._PolicyTypes += $PolicyType
|
|
}
|
|
else {
|
|
Write-Log "Cannot add PolicyType to PolicyGroup. $($PolicyType.ID) ($($PolicyType.Title)) alredy added" 2
|
|
}
|
|
}
|
|
}
|
|
|
|
# Represents an API
|
|
class IntunePolicyTypeBase
|
|
{
|
|
Hidden [IntunePolicyGroupBase]$_PolicyGroup = $null
|
|
Hidden [String]$_Name = $null
|
|
|
|
# See IntunePolicyGroupBase.AccessType. For a type this is derived straight
|
|
# from _Permissions vs the token's granted scopes/roles.
|
|
[APIAccess]$AccessType = [APIAccess]::Full
|
|
[string]$AccessInfo = ""
|
|
|
|
Hidden [String]$_ID = $null
|
|
Hidden [String]$_APITitle = $null
|
|
Hidden [String]$_APIVersion = $null
|
|
Hidden [String]$_API = $null
|
|
Hidden [String]$_APIPOST = $null
|
|
Hidden [String]$_APIPUT = $null
|
|
Hidden [String]$_APIDELETE = $null
|
|
Hidden [String]$_PolicyName = $null
|
|
Hidden [String]$_PolicyBaseName = $null
|
|
Hidden [String[]]$_PropertiesToRemove = $null
|
|
Hidden [String[]]$_PropertiesToRemoveForUpdate = $null
|
|
# Properties the PROPERTY compare should not match on for this type -
|
|
# server-maintained state that legitimately differs between two
|
|
# identically-configured objects (rendered greyed-out, like the global
|
|
# isAssigned skip). Membership-style props skipped here should still be
|
|
# covered semantically by the type's documentation compare.
|
|
Hidden [String[]]$_ComparePropertiesToSkip = @()
|
|
Hidden [String[]]$_SkipRemoveProperties = @()
|
|
Hidden [String[]]$_Permissions = $null
|
|
# Intune RBAC resource category (the middle of Microsoft.Intune_<Cat>_<Action>)
|
|
# used by the access marking's Layer 2 (Internal/EffectivePermissions.ps1).
|
|
# $null = derive from _API; set only where the derivation is wrong.
|
|
Hidden [String]$_ResourceCategory = $null
|
|
# Column list for this type's view. $null = derive (Get-IntuneDefaultColumns,
|
|
# Internal/IntuneViewColumns.ps1) from the flags below; set only to
|
|
# hard-override the derivation.
|
|
Hidden [string[]]$_ViewProperties = $null
|
|
# Rows of this type resolve a Platform, per row or via _PlatformName.
|
|
Hidden [bool]$_HasPlatform = $true
|
|
# The endpoint returns lastModifiedDateTime / modifiedDateTime.
|
|
Hidden [bool]$_HasModified = $true
|
|
# Column that tells rows of this ONE type apart - "PolicyName=Type" where the
|
|
# object class sets a per-row name, "ApplicationType=Type" for apps. It takes
|
|
# the place of the generic Policy type column, which would repeat itself.
|
|
Hidden [string]$_SubTypeColumn = $null
|
|
# Type-specific additions, e.g. "ApplicationTypeGroup=App type".
|
|
Hidden [string[]]$_ExtraColumns = @()
|
|
Hidden [String]$_Expand = $null
|
|
Hidden [String]$_Icon = $null
|
|
Hidden [Object]$IconImage = $null
|
|
Hidden [String[]]$_Dependencies = $null
|
|
|
|
Hidden [String]$_QueryList = $null
|
|
Hidden [Boolean]$_QuerySearch = $false
|
|
Hidden [Boolean]$_NavigationProperties = $false
|
|
|
|
Hidden [Boolean]$_ExpandAssignments = $true
|
|
Hidden [Boolean]$_ExpandAssignmentsList = $true
|
|
Hidden [Boolean]$_SupportsAssignments = $true
|
|
Hidden [String]$_AssignmentsType = "assignments"
|
|
# Action segment used when writing assignments ({API}/{id}/<action>).
|
|
# Most types use the canonical "assign" action; policySets have no
|
|
# /assign segment - their update action takes a full `assignments`
|
|
# replacement list with the same semantics.
|
|
Hidden [String]$_AssignAction = "assign"
|
|
# When $true, per-policy assignments are fetched via
|
|
# {API}/{id}?$expand=assignments instead of the {API}/{id}/assignments
|
|
# navigation URL (policySets: the navigation GET returns 400, only the
|
|
# expand variant works - same family as _ExpandAssignmentsList).
|
|
Hidden [Boolean]$_AssignmentsViaExpand = $false
|
|
# Optional override consulted by the Bulk Assignments tool. When the
|
|
# default Get-BulkAssignmentObjectType heuristic (AssignmentsType / Id
|
|
# lookup) can't derive a Graph @odata.type for this PolicyType's
|
|
# assignment entries, set this field to the explicit
|
|
# "#microsoft.graph.XAssignment" string. The lookup logs a warning when
|
|
# both this override and the heuristic return null — that's the signal
|
|
# a new type needs to set it.
|
|
Hidden [String]$_AssignmentObjectType = $null
|
|
Hidden [String[]]$_AssignmentPropertiesToKeep = $null
|
|
Hidden [String[]]$_AssignmentTargetPropertiesToKeep = $null
|
|
|
|
Hidden [Uint32]$_ImportOrder = 1000
|
|
Hidden [OMetadata]$_ODataMetadata = [OMetadata]::Full
|
|
Hidden [Boolean]$_SkipAddIDOnFileName = $false
|
|
Hidden [Boolean]$_ScopeTagsReturnedInList = $true
|
|
Hidden [String]$_ScopeTagProperty = "roleScopeTagIds"
|
|
Hidden [String]$_ObjectClass = $null
|
|
|
|
Hidden [String]$_CopyDefaultName = $null
|
|
|
|
Hidden [Boolean]$_HasDescription = $true
|
|
Hidden [Boolean]$_RequiresFullObject = $true
|
|
|
|
Hidden [Boolean]$_VerifyObject = $false
|
|
|
|
# True when this type's CheckPolicy authoritatively matches/rejects a *file* object
|
|
# by @odata.type (a complete matcher), rather than relying on the base @odata.id
|
|
# matcher which is blind to files. When true, a CheckPolicy=$false is a real
|
|
# rejection, so the single-narrowed-candidate folder-trust fallback in
|
|
# Get-PoliciesTypeFromObject is skipped. Leave $false for types whose CheckPolicy
|
|
# cannot reliably identify their own file objects (the base @odata.id matcher, or
|
|
# ApplicationType's assignments-context heuristic).
|
|
Hidden [Boolean]$_StrictODataTypeCheck = $false
|
|
|
|
Hidden [String]$_NameProperty = "displayName"
|
|
# Whether this type's endpoint honours a server-side name filter
|
|
# (contains/tolower on _NameProperty). Verified live for most Intune
|
|
# endpoints; set to $false on the ones that answer HTTP 400/500 so the
|
|
# search filters client-side instead. See GetNameFilterClause.
|
|
Hidden [Boolean]$_SupportsNameFilter = $true
|
|
Hidden [String]$_IdProperty = "id"
|
|
|
|
Hidden [String]$_Folder = $null
|
|
|
|
# Optional default platform name for this policy type. When set, the per-row
|
|
# IntunePolicyBase.Platform getter falls back to this value if the row didn't
|
|
# set its own _PlatformName. Used by DeviceEnrollment subtypes (ESP, WHfB,
|
|
# Co-Mgmt, Windows Restore) so the platform doesn't have to be derived from
|
|
# @odata.type on every Object instance.
|
|
Hidden [String]$_PlatformName = $null
|
|
|
|
Hidden [String[]]$_PolicyFileAttributes = @()
|
|
|
|
Hidden [Uint32]$_PolicyTypeOrder = 100
|
|
|
|
# $null = all buttons. Set to e.g. @("Export","View") for read-only types
|
|
# (the Intune Info group). Gates the main-view buttons and bulk-form lists.
|
|
Hidden [object]$_ShowButtons = $null
|
|
|
|
# Set when the API returns ONE object instead of a value collection (e.g.
|
|
# androidManagedStoreAccountEnterpriseSettings, deviceManagement/settings).
|
|
# Get-GraphPolicies wraps the body as a single row. Combine with
|
|
# _HasPageSizeSupport = $false so no $top is appended to the list URL.
|
|
Hidden [Boolean]$_SingleObject = $false
|
|
Hidden [Uint32]$_TopItems = 1000
|
|
|
|
Hidden [Boolean]$_HasPageSizeSupport = $true
|
|
|
|
IntunePolicyTypeBase()
|
|
{
|
|
if($this.GetType().Name -eq "IntunePolicyTypeBase") {
|
|
throw "Abstract class. Object cannot be created"
|
|
}
|
|
elseif($script:SingletonObjects.ContainsKey($this.GetType().Name) -eq $true) {
|
|
throw "Only one $($this.GetType().Name) object can be created"
|
|
}
|
|
|
|
Add-SingletonObject $this.GetType().Name $this
|
|
|
|
([IntunePolicyTypeBase]$this).Init()
|
|
}
|
|
|
|
Static [PSCustomObject]Get([IntunePolicyBase]$GraphObject)
|
|
{
|
|
return $GraphObject.Object
|
|
}
|
|
|
|
[String]GetListURL()
|
|
{
|
|
return ([IntunePolicyTypeBase]$this).GetListURL($null)
|
|
}
|
|
|
|
[String]GetListURL([string]$NameFilter)
|
|
{
|
|
$params = @()
|
|
$stringURI = $this._API
|
|
if($this._QueryList) {
|
|
$stringURI += $this._QueryList
|
|
}
|
|
|
|
# Server-side name search. _QueryList already carries a $filter on some
|
|
# types (the Settings Catalog siblings filter by templateFamily), and a
|
|
# URL may only have ONE $filter - so merge into the existing clause with
|
|
# 'and' rather than appending a second parameter. The existing clause is
|
|
# parenthesised because those are 'or' chains and 'and' binds tighter.
|
|
$nameClause = ([IntunePolicyTypeBase]$this).GetNameFilterClause($NameFilter)
|
|
if($nameClause) {
|
|
$existingMatch = if($this._QueryList) { [Regex]::Match($this._QueryList, '\$filter=([^&]+)') } else { $null }
|
|
if($existingMatch -and $existingMatch.Success) {
|
|
# Splice by index - no regex replacement, so quotes and
|
|
# parentheses in either clause are safe.
|
|
$merged = "`$filter=($($existingMatch.Groups[1].Value)) and $nameClause"
|
|
$stringURI = $this._API +
|
|
$this._QueryList.Substring(0, $existingMatch.Index) +
|
|
$merged +
|
|
$this._QueryList.Substring($existingMatch.Index + $existingMatch.Length)
|
|
}
|
|
else {
|
|
$params += "`$filter=$nameClause"
|
|
}
|
|
}
|
|
if((Get-SettingValue "ExpandAssignments") -eq $true -and $this.ExpandAssignmentsList -ne $false)
|
|
{ # Expand assignments so they can be used in custom columns
|
|
if(-not $this._QueryList -or ($this._QueryList.IndexOf('$expand=',[System.StringComparison]::InvariantCultureIgnoreCase)) -eq -1)
|
|
{
|
|
$params += "`$expand=assignments"
|
|
}
|
|
}
|
|
|
|
# ToDo: Add PageSize Setting
|
|
$pageSizeStr = Get-SettingValue "GraphPageSize" $this._TopItems
|
|
try {
|
|
$pageSize = [uint32]$pageSizeStr
|
|
}
|
|
catch {
|
|
$pageSize = 20
|
|
}
|
|
|
|
if($this.HasPageSizeSupport -eq $false)
|
|
{
|
|
# Do nothing...
|
|
}
|
|
elseif($pageSize -gt 0 -and (-not $this._QueryList -or ($this._QueryList.IndexOf('$top=',[System.StringComparison]::InvariantCultureIgnoreCase) -eq -1))) {
|
|
$params += "`$top=$pageSize"
|
|
}
|
|
|
|
$url = $stringURI
|
|
if($params.Count -gt 0) {
|
|
$url += (?: ([String]::IsNullOrEmpty($this._QueryList)) "?" "&")
|
|
$url += ($params -join "&")
|
|
}
|
|
|
|
# NOTE: %OrganizationId% is intentionally NOT substituted here. It used to be
|
|
# replaced with $script:OrganizationId (the *default* tenant), which broke
|
|
# cross-tenant lookups when the caller supplied -TokenId pointing at a different
|
|
# token. Substitution now happens in Invoke-MSGraphAPI using the call's TokenId.
|
|
|
|
return $url
|
|
}
|
|
|
|
# Base URL used when fetching a single policy's assignments ({base}/{id}/assignments
|
|
# or {base}/{id}?$expand=assignments). Defaults to the list API. Types whose list
|
|
# collection is polymorphic must override this: $expand and the assignments
|
|
# navigation both resolve against the collection's *declared* type, so a base type
|
|
# that doesn't declare `assignments` returns 400 for either form (App protection -
|
|
# managedAppPolicies is Collection(managedAppPolicy), which has no navigation
|
|
# properties at all). Return $null to skip the fetch for an object that cannot
|
|
# carry assignments.
|
|
[String]GetAssignmentsBaseURL([PSCustomObject]$PolicyObject)
|
|
{
|
|
return $this._API
|
|
}
|
|
|
|
[PSCustomObject]GetListBatchObject()
|
|
{
|
|
return ([IntunePolicyTypeBase]$this).GetListBatchObject($null)
|
|
}
|
|
|
|
[PSCustomObject]GetListBatchObject([string]$NameFilter)
|
|
{
|
|
return [PSCustomObject]@{
|
|
id = $this.Id
|
|
method = "GET"
|
|
url = ([IntunePolicyTypeBase]$this).GetListURL($NameFilter).TrimStart('/')
|
|
headers = @{"Accept"="application/json;odata.metadata=$($this._ODataMetadata)"}
|
|
}
|
|
}
|
|
|
|
# Server-side name search. Returns the OData filter clause for $NameFilter,
|
|
# or an empty string when this type cannot be searched that way.
|
|
#
|
|
# contains(tolower(prop),'term') rather than startswith(prop,'term'):
|
|
# verified live 2026-08-27 that Graph's startswith is case-SENSITIVE on
|
|
# nearly every one of these endpoints (a search for "test" found nothing
|
|
# named "[Testing] ..."), while the tolower/contains form is accepted and
|
|
# returns exactly what a client-side substring match would. It also keeps
|
|
# the substring semantics the picker had before it searched server-side.
|
|
#
|
|
# Types whose endpoint rejects any $filter set _SupportsNameFilter = $false
|
|
# and are filtered client-side by Get-GraphPolicies instead.
|
|
[String]GetNameFilterClause([string]$NameFilter)
|
|
{
|
|
if([String]::IsNullOrWhiteSpace($NameFilter)) { return "" }
|
|
if($this._SupportsNameFilter -ne $true) { return "" }
|
|
# Single-object endpoints return the object itself, not a collection -
|
|
# there is nothing to filter and Graph rejects the attempt.
|
|
if($this._SingleObject -eq $true) { return "" }
|
|
|
|
$prop = Get-StringOrDefault $this._NameProperty "displayName"
|
|
# Escaping (quote doubling, percent-encoding, PS5.1/PS7 normalisation) is
|
|
# ConvertTo-ODataStringLiteral's job - one rule for every literal.
|
|
$literal = ConvertTo-ODataStringLiteral $NameFilter.ToLowerInvariant()
|
|
return "contains(tolower($prop),$literal)"
|
|
}
|
|
|
|
# Hidden Functions
|
|
Hidden Init()
|
|
{
|
|
Add-ObjectProperty $this "ID" { $this._ID }
|
|
Add-ObjectProperty $this "PolicyGroup" { $this._PolicyGroup }
|
|
Add-ObjectProperty $this "APIVersion" { Get-StringOrDefault $this._APIVersion "Beta" }
|
|
Add-ObjectProperty $this "API" { $this._API }
|
|
Add-ObjectProperty $this "APIPOST" { ?? $this._APIPOST $this._API }
|
|
Add-ObjectProperty $this "APIPUT" { ?? $this._APIPUT $this._API }
|
|
Add-ObjectProperty $this "APIDELETE" { ?? $this._APIDELETE $this._API }
|
|
Add-ObjectProperty $this "PolicyName" { $this._PolicyName }
|
|
Add-ObjectProperty $this "PolicyBaseName" { ?? $this._PolicyBaseName $this._PolicyName }
|
|
Add-ObjectProperty $this "PropertiesToRemove" { $this._PropertiesToRemove }
|
|
Add-ObjectProperty $this "PropertiesToRemoveForUpdate" { $this._PropertiesToRemoveForUpdate }
|
|
Add-ObjectProperty $this "ComparePropertiesToSkip" { $this._ComparePropertiesToSkip }
|
|
Add-ObjectProperty $this "SkipRemoveProperties" { $this._SkipRemoveProperties }
|
|
Add-ObjectProperty $this "Permissions" { $this._Permissions }
|
|
Add-ObjectProperty $this "ViewProperties" { ?? $this._ViewProperties (Get-IntuneDefaultColumns $this) }
|
|
Add-ObjectProperty $this "Expand" { $this._Expand }
|
|
Add-ObjectProperty $this "Dependencies" { $this._Dependencies }
|
|
Add-ObjectProperty $this "Icon" { Get-StringOrDefault $this._Icon $this._ID }
|
|
#Add-ObjectProperty $this "IconImage" { $this._IconImage }
|
|
Add-ObjectProperty $this "QueryList" { $this._QueryList }
|
|
Add-ObjectProperty $this "QuerySearch" { Get-BoolOrDefault $this._QuerySearch $false }
|
|
Add-ObjectProperty $this "ExpandAssignments" { $this._ExpandAssignments }
|
|
Add-ObjectProperty $this "ExpandAssignmentsList" { $this._ExpandAssignmentsList }
|
|
Add-ObjectProperty $this "SupportsAssignments" { $this._SupportsAssignments }
|
|
Add-ObjectProperty $this "ScopeTagsReturnedInList" { $this._ScopeTagsReturnedInList }
|
|
Add-ObjectProperty $this "ImportOrder" { $this._ImportOrder }
|
|
Add-ObjectProperty $this "ODataMetadata" { $this._ODataMetadata }
|
|
Add-ObjectProperty $this "AssignmentsType" { $this._AssignmentsType }
|
|
Add-ObjectProperty $this "AssignAction" { ?? $this._AssignAction "assign" }
|
|
Add-ObjectProperty $this "AssignmentsViaExpand" { Get-BoolOrDefault $this._AssignmentsViaExpand $false }
|
|
Add-ObjectProperty $this "AssignmentObjectType" { $this._AssignmentObjectType }
|
|
Add-ObjectProperty $this "AssignmentPropertiesToKeep" { $this._AssignmentPropertiesToKeep }
|
|
Add-ObjectProperty $this "AssignmentTargetPropertiesToKeep" { $this._AssignmentTargetPropertiesToKeep }
|
|
Add-ObjectProperty $this "SkipAddIDOnFileName" { Get-BoolOrDefault $this._SkipAddIDOnFileName $false }
|
|
Add-ObjectProperty $this "ScopeTagProperty" { Get-StringOrDefault $this._ScopeTagProperty }
|
|
Add-ObjectProperty $this "NavigationProperties" { $this._NavigationProperties }
|
|
Add-ObjectProperty $this "HasDescription" { $this._HasDescription }
|
|
Add-ObjectProperty $this "CopyDefaultName" { $this._CopyDefaultName }
|
|
Add-ObjectProperty $this "NameProperty" { $this._NameProperty }
|
|
Add-ObjectProperty $this "SupportsNameFilter" { $this._SupportsNameFilter }
|
|
Add-ObjectProperty $this "IDProperty" { $this._IDProperty }
|
|
|
|
Add-ObjectProperty $this "VerifyObject" { $this._VerifyObject }
|
|
Add-ObjectProperty $this "StrictODataTypeCheck" { $this._StrictODataTypeCheck }
|
|
|
|
Add-ObjectProperty $this "Folder" { ?? $this._Folder $this._ID }
|
|
|
|
Add-ObjectProperty $this "Title" { ?? $this._APITitle $this._PolicyName }
|
|
|
|
Add-ObjectProperty $this "HasPageSizeSupport" { $this._HasPageSizeSupport }
|
|
Add-ObjectProperty $this "ShowButtons" { $this._ShowButtons }
|
|
Add-ObjectProperty $this "SingleObject" { Get-BoolOrDefault $this._SingleObject $false }
|
|
}
|
|
|
|
[IntunePolicyBase]GetObject([PSCustomObject]$JsonObj)
|
|
{
|
|
if($this._ObjectClass) {
|
|
$policyObject = (New-Object -TypeName $this._ObjectClass -ArgumentList $JsonObj)
|
|
if($this.VerifyObject -and $this.CheckPolicy($JsonObj) -eq $false) {
|
|
$policyObject = $null
|
|
}
|
|
# When several PolicyType siblings share the same _ObjectClass (e.g. all
|
|
# deviceEnrollmentConfigurations subtypes wrap rows in DeviceEnrollmentObject),
|
|
# the object's Init() picks a single hard-coded singleton — usually the base
|
|
# class. Override with the *calling* subtype so the policy's PolicyType.Folder
|
|
# and other type-keyed lookups resolve to the bucket that actually claimed it.
|
|
if($null -ne $policyObject) {
|
|
$policyObject._PolicyType = $this
|
|
$this.LogResolvedPolicy($JsonObj)
|
|
}
|
|
return $policyObject
|
|
}
|
|
else {
|
|
Write-Log "Object class is missing for $($this.Title) $($JsonObj."$($this.NameProperty)")"
|
|
}
|
|
return $null
|
|
}
|
|
|
|
[IntunePolicyBase]GetObject([String]$Json)
|
|
{
|
|
$jsonObj = (ConvertFrom-Json $Json)
|
|
return ($this.GetObject($jsonObj))
|
|
}
|
|
|
|
[IntunePolicyBase]GetObject([IO.FileInfo]$File)
|
|
{
|
|
try {
|
|
$tmpObject = (New-Object -TypeName $this._ObjectClass -ArgumentList $File)
|
|
if($this.VerifyObject -and $this.CheckPolicy($tmpObject.JsonObject) -eq $false) {
|
|
$tmpObject = $null
|
|
}
|
|
if($null -ne $tmpObject) {
|
|
$tmpObject._PolicyType = $this
|
|
$this.LogResolvedPolicy($tmpObject.JsonObject)
|
|
}
|
|
return $tmpObject
|
|
}
|
|
catch {}
|
|
return $null
|
|
}
|
|
|
|
AddExportProperties([IntuneManagerExportSettings]$ExportSettings)
|
|
{
|
|
|
|
}
|
|
|
|
AddImportProperties([IntuneManagerImportSettings]$ImportSettings)
|
|
{
|
|
|
|
}
|
|
|
|
[Boolean]CheckPolicy([PSCustomObject]$PolicyObject)
|
|
{
|
|
if($PolicyObject.'@odata.id') {
|
|
if($PolicyObject.'@odata.id'.StartsWith($this._API.TrimStart('/'))) {
|
|
return $true
|
|
}
|
|
}
|
|
|
|
return $false
|
|
}
|
|
|
|
# Uniform "found policy" breadcrumb, logged once per object at the point a
|
|
# type claims it - both the list-load path (Get-GraphPolicies) and the
|
|
# file-load path (Get-GraphPolicyFromFile) route every resolved object
|
|
# through GetObject, so this is the single chokepoint. It replaces the
|
|
# ad-hoc Write-Log lines that a handful of CheckPolicy overrides used to
|
|
# carry: those fired only for the subset of types that both overrode
|
|
# CheckPolicy AND happened to include the line, so the log was verbose for
|
|
# some types and silent for others. Reporting from here makes it consistent
|
|
# for every type. The discriminators (@odata.type / @odata.id, plus the two
|
|
# template/setting keys some types match on) are appended only when present.
|
|
LogResolvedPolicy([PSCustomObject]$PolicyObject)
|
|
{
|
|
$name = $PolicyObject."$($this.NameProperty)"
|
|
|
|
$parts = @()
|
|
if($PolicyObject.'@odata.type') { $parts += "@odata.type: $($PolicyObject.'@odata.type')" }
|
|
if($PolicyObject.'@odata.id') { $parts += "@odata.id: $($PolicyObject.'@odata.id')" }
|
|
if($PolicyObject.templateReference.templateFamily) { $parts += "templateFamily: $($PolicyObject.templateReference.templateFamily)" }
|
|
if($PolicyObject.settingDefinitionId) { $parts += "settingDefinitionId: $($PolicyObject.settingDefinitionId)" }
|
|
|
|
$basis = if($parts.Count -gt 0) { " Found based on $($parts -join ', ')" } else { "" }
|
|
# Verbose channel: this fires once per resolved object (every type, every
|
|
# list/file load), so it is diagnostic noise in the normal log. Write-LogDebug
|
|
# only emits when the Debug setting is on, keeping the default log clean while
|
|
# the breadcrumb is still available when troubleshooting type resolution.
|
|
Write-LogDebug "Found policy $name ($($this.Title)).$basis"
|
|
}
|
|
|
|
# Single-policy entry point preserved for back-compat with Compare and a
|
|
# handful of subtype overrides (e.g. windows10CustomConfiguration OMA
|
|
# decrypt). Routes through Invoke-PolicyHydrate so there is exactly one
|
|
# hydration path; the body fetch / nav-property expansion / sub-resource
|
|
# fan-out all live in the unified orchestrator.
|
|
[Boolean]GetFullObject($PolicyObject)
|
|
{
|
|
if($PolicyObject._IsFullObject) { return $true }
|
|
if($null -eq $PolicyObject._TokenId) { return $false }
|
|
|
|
Invoke-PolicyHydrate -Policies @($PolicyObject) -TokenId $PolicyObject._TokenId
|
|
|
|
if(-not $PolicyObject._IsFullObject) {
|
|
Write-Warning "Failed to get full object for $($PolicyObject.Name)"
|
|
return $false
|
|
}
|
|
return $true
|
|
}
|
|
|
|
# Returns a hashtable describing how to compare settings of this policy type, or $null for default property comparison.
|
|
# Expected keys when non-null:
|
|
# Prop : property name on .Object holding the settings array (required)
|
|
# GetKey : scriptblock { param($s) ... } returning a stable string key per setting (required)
|
|
# GetValue : scriptblock { param($s) ... } returning the display value per setting (required)
|
|
# GetCategory : scriptblock { param($s) ... } returning the category label (optional)
|
|
# Hydrate : scriptblock { param($policy) ... } loading any data missing after GetFullObject (optional)
|
|
[Hashtable]GetCompareConfig()
|
|
{
|
|
return $null
|
|
}
|
|
|
|
PostExportCommand([PSCustomObject]$PolicyObject, [String]$PathToFile)
|
|
{
|
|
|
|
}
|
|
|
|
Hidden PreExportCommand([IntunePolicyBase]$PolicyObject, [PSCustomObject]$ExportObject)
|
|
{
|
|
|
|
}
|
|
|
|
[Hashtable]PreImportCommand([PSCustomObject]$PolicyObject)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
[Hashtable]PreImportAssignmentsCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
PostImportCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject)
|
|
{
|
|
|
|
}
|
|
|
|
PostImportAssignmentsCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject, [PSCustomObject]$ImportedAssignments)
|
|
{
|
|
|
|
}
|
|
|
|
PostCopyCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject)
|
|
{
|
|
|
|
}
|
|
|
|
PostBulkImportCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject)
|
|
{
|
|
|
|
}
|
|
|
|
[Hashtable]PreDeleteCommand([IntunePolicyBase]$PolicyObject)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
[Hashtable]PreUpdateCommand([PSCustomObject]$PolicyObject, [PSCustomObject]$SourceObject)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
[Hashtable]PreReplaceCommand([IntunePolicyBase]$PolicyObject)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
PostReplaceCommand([IntunePolicyBase]$PolicyObject, [IntunePolicyBase]$SourceObject)
|
|
{
|
|
|
|
}
|
|
|
|
[IntunePolicyBase[]]PreImportPolicies([IntunePolicyBase[]]$PolicyObjects)
|
|
{
|
|
return @($PolicyObjects)
|
|
}
|
|
}
|
|
|
|
class IntunePolicyBase
|
|
{
|
|
# Hidden Properties
|
|
Hidden [IO.FileInfo]$FileInfoObject = $null
|
|
Hidden [PSCustomObject]$JsonObject = $null
|
|
Hidden [IntunePolicyTypeBase]$_PolicyType = $null
|
|
Hidden [String]$_PolicyName = $null
|
|
Hidden [boolean]$_IsFullObject = $false
|
|
# Set to $true on subclasses that implement GetSubResourceBatchRequests /
|
|
# ApplySubResourceBatchResult so the unified hydrator
|
|
# (Invoke-PolicySubresourceFetch) can fan out their extra GETs in $batch
|
|
# form during Invoke-PolicyHydrate.
|
|
Hidden [boolean]$_HasSubResourceBatch = $false
|
|
# Set by Start-GraphBulkExport when another policy in the same type bucket
|
|
# shares this one's displayName, so GetFileName appends `_<id>` and the
|
|
# collision doesn't silently overwrite a sibling on disk.
|
|
Hidden [boolean]$_NeedsIdInFilename = $false
|
|
Hidden [string]$_PlatformName = $null
|
|
Hidden [PSCustomObject]$_ClonedFromObject = $null
|
|
Hidden [int]$_TokenID = $null
|
|
Hidden [String[]]$_ScopeTags = $()
|
|
Hidden [String]$_ScopeTagsString = $null
|
|
|
|
[string]$TenantId = $null
|
|
|
|
|
|
IntunePolicyBase() {
|
|
([IntunePolicyBase]$this).Init()
|
|
}
|
|
|
|
IntunePolicyBase([String]$Json) {
|
|
([IntunePolicyBase]$this).Init($Json)
|
|
}
|
|
|
|
IntunePolicyBase([PSCustomObject]$JsonObj) {
|
|
([IntunePolicyBase]$this).Init($JsonObj)
|
|
}
|
|
|
|
IntunePolicyBase([IO.FileInfo]$FileInfo) {
|
|
([IntunePolicyBase]$this).Init($FileInfo)
|
|
}
|
|
|
|
# Hidden Functions
|
|
Hidden Init()
|
|
{
|
|
Add-ObjectProperty $this "ID" { return $this.GetId() }
|
|
Add-ObjectProperty $this "Name" { return $this.GetName() } { $this.SetName($args[0]) }
|
|
Add-ObjectProperty $this "PolicyType" { $this._PolicyType }
|
|
Add-ObjectProperty $this "PolicyName" { $this.GetPolicyName() }
|
|
Add-ObjectProperty $this "PolicyBaseName" { $this.GetPolicyBaseName() }
|
|
# Per-row _PlatformName wins; if absent, fall back to the type's default.
|
|
# Lets multiple subtypes share one Object class (e.g. DeviceEnrollment
|
|
# buckets all use DeviceEnrollmentObject) and still report the right
|
|
# Platform per bucket via type-level _PlatformName.
|
|
Add-ObjectProperty $this "Platform" {
|
|
if($this._PlatformName) { return $this._PlatformName }
|
|
if($this._PolicyType -and $this._PolicyType._PlatformName) { return $this._PolicyType._PlatformName }
|
|
return $null
|
|
}
|
|
Add-ObjectProperty $this "Object" { $this.JsonObject }
|
|
# Bound by the grid (and therefore searchable by the filter) on every view.
|
|
Add-ObjectProperty $this "Description" { if($this.JsonObject) { $this.JsonObject.description } }
|
|
Add-ObjectProperty $this "IsFromFile" { $this.GetFromFile() }
|
|
Add-ObjectProperty $this "FileInfo" { ([PSCustomObject]$this.FileInfoObject) }
|
|
Add-ObjectProperty $this "IsFullObject" { $this._IsFullObject }
|
|
Add-ObjectProperty $this "JsonString" { if($this.JsonObject) { ConvertTo-Json $this.JsonObject -Depth 50} }
|
|
Add-ObjectProperty $this "TokenId" { $this._TokenId }
|
|
Add-ObjectProperty $this "Created" {
|
|
if($this.JsonObject.createdDateTime) {
|
|
Get-Date $this.JsonObject.createdDateTime
|
|
}
|
|
else { "" }
|
|
}
|
|
|
|
Add-ObjectProperty $this "LastModified" {
|
|
if($this.JsonObject.lastModifiedDateTime) {
|
|
Get-Date $this.JsonObject.lastModifiedDateTime
|
|
}
|
|
elseif($this.JsonObject.modifiedDateTime) {
|
|
Get-Date $this.JsonObject.modifiedDateTime
|
|
}
|
|
else { "" }
|
|
}
|
|
|
|
Add-ObjectProperty $this "ScopeTags" {
|
|
if($null -eq $this._ScopeTags) {
|
|
$this._ScopeTags = Get-GraphScopeTags $this
|
|
$this._ScopeTagsString = $this._ScopeTags -join ","
|
|
}
|
|
return $this._ScopeTagsString
|
|
}
|
|
|
|
# Scope-tag NAMES as an array (same lazy resolution as ScopeTags, which
|
|
# returns the comma-joined string). Lets callers use array operators —
|
|
# e.g. a documentation filter `$_.ScopeTagNames -contains 'Production'`.
|
|
Add-ObjectProperty $this "ScopeTagNames" {
|
|
if($null -eq $this._ScopeTags) {
|
|
$this._ScopeTags = Get-GraphScopeTags $this
|
|
$this._ScopeTagsString = $this._ScopeTags -join ","
|
|
}
|
|
return $this._ScopeTags
|
|
}
|
|
|
|
# Both fields can carry a comma-separated flags value, so translation
|
|
# goes through Get-PlatformDisplayName rather than a direct key lookup.
|
|
if($this.JsonObject.platforms) {
|
|
$this._PlatformName = Get-PlatformDisplayName $this.JsonObject.platforms
|
|
}
|
|
elseif($this.JsonObject.platformType) {
|
|
$this._PlatformName = Get-PlatformDisplayName $this.JsonObject.platformType
|
|
}
|
|
|
|
if(-not $this._PlatformName -and $this.JsonObject.'@OData.Type'){
|
|
$this._PlatformName = Get-PolicyPlatformName $this.JsonObject.'@OData.Type'
|
|
}
|
|
}
|
|
|
|
Hidden Init([string]$Json)
|
|
{
|
|
if($Json) { $this.LoadJson($Json) }
|
|
([IntunePolicyBase]$this).Init()
|
|
}
|
|
|
|
Hidden Init([PSCustomObject]$JsonObject) {
|
|
$this.JsonObject = $JsonObject
|
|
([IntunePolicyBase]$this).Init()
|
|
}
|
|
|
|
Hidden Init([IO.FileInfo]$FileInfo)
|
|
{
|
|
if($FileInfo -and $FileInfo.Exists) {
|
|
Write-LogDebug "Loading object from file $($FileInfo.FullName)"
|
|
# ReadAllText, not Get-Content: this is the load path for every policy
|
|
# read from disk (import, compare, file-based documentation). Bare
|
|
# Get-Content decodes as the ANSI codepage on PS5.1, which mangles any
|
|
# non-ASCII policy name or description on the way in.
|
|
$Json = [IO.File]::ReadAllText($FileInfo.FullName)
|
|
$this.LoadJson($Json)
|
|
$this.FileInfoObject = $FileInfo
|
|
}
|
|
else
|
|
{
|
|
$errorString = "Failed to load object from file. File $($FileInfo.FullName) not found"
|
|
Write-Log $errorString 3
|
|
throw $errorString
|
|
}
|
|
([IntunePolicyBase]$this).Init()
|
|
}
|
|
|
|
Hidden LoadJson([String]$Json)
|
|
{
|
|
$this.JsonObject = $Json | ConvertFrom-Json
|
|
}
|
|
|
|
Hidden [String] GetName()
|
|
{
|
|
return $this.JsonObject."$($this.PolicyType._NameProperty)"
|
|
}
|
|
|
|
Hidden SetName($Name)
|
|
{
|
|
$this.JsonObject."$($this.PolicyType._NameProperty)" = $Name
|
|
}
|
|
|
|
Hidden [String] GetId()
|
|
{
|
|
return $this.JsonObject.id
|
|
}
|
|
|
|
Hidden [Boolean]GetFromFile()
|
|
{
|
|
return ($null -ne $this.FileInfoObject)
|
|
}
|
|
|
|
Hidden [String]GetPolicyName()
|
|
{
|
|
return ?? $this._PolicyName $this._PolicyType.PolicyName
|
|
}
|
|
|
|
Hidden [String]GetPolicyBaseName()
|
|
{
|
|
return ?? $this._PolicyBaseName $this._PolicyType.PolicyBaseName
|
|
}
|
|
|
|
# Public functions
|
|
[String]GetObjectURL()
|
|
{
|
|
$params = @()
|
|
$uri = [uri]"$($this.PolicyType.API)/$($this.id)"
|
|
|
|
$expand = @()
|
|
# NOTE: the property check below reads `.ExpandAssignments` not
|
|
# `.ExpandAssignmentsList`. This LOOKS like a typo — and an earlier
|
|
# session "fixed" it to ExpandAssignmentsList — but the wrong-name
|
|
# behaviour is load-bearing here:
|
|
# * `_ExpandAssignmentsList=$false` is meant to skip $expand=assignments
|
|
# on the LIST URL because some Graph endpoints reject it there.
|
|
# * For the per-id BODY URL, `$expand=assignments` is the only working
|
|
# path for types whose `/assignments` sub-resource returns 400
|
|
# (managedAppPolicies, notificationMessageTemplates).
|
|
# The typo'd check resolves to `$null -ne $false` (true), so the body
|
|
# URL keeps the assignments expand regardless of the list-URL opt-out.
|
|
# Correcting the name regressed AppProtection's actual assignment
|
|
# rows to null on bulk export.
|
|
if($this.JsonObject.'assignments@odata.navigationLink' -and $this.PolicyType.ExpandAssignments -ne $false)
|
|
{
|
|
$expand += "assignments"
|
|
}
|
|
|
|
if($this.JsonObject.'apps@odata.navigationLink')
|
|
{
|
|
$expand += "apps"
|
|
}
|
|
|
|
if($this.JsonObject.'settings@odata.navigationLink')
|
|
{
|
|
$expand += "settings"
|
|
}
|
|
|
|
if($this.JsonObject.'roleAssignments@odata.navigationLink')
|
|
{
|
|
$expand += "roleAssignments"
|
|
}
|
|
|
|
if($this.JsonObject.'privacyAccessControls@odata.associationLink')
|
|
{
|
|
$expand += "microsoft.graph.windows10GeneralConfiguration/privacyAccessControls"
|
|
}
|
|
|
|
if($this.PolicyType.Expand)
|
|
{
|
|
foreach($objExpand in $this.PolicyType.Expand.Split(","))
|
|
{
|
|
if($objExpand -notin $expand) { $expand += $objExpand}
|
|
}
|
|
}
|
|
|
|
if($expand.Count -gt 0) {
|
|
$params += ('$expand=' + ($expand -join ",")) # ToDo: Check if expand is set
|
|
}
|
|
|
|
$url = $uri.OriginalString
|
|
if($params.Count -gt 0) {
|
|
$url += (?: ([String]::IsNullOrEmpty($uri.Query)) "?" "&")
|
|
$url += ($params -join "&")
|
|
}
|
|
|
|
return $url
|
|
}
|
|
|
|
[Boolean]Get()
|
|
{
|
|
if($this._IsFullObject) { return $true }
|
|
if($null -eq $this._TokenId) { return $false }
|
|
Invoke-PolicyHydrate -Policies @($this) -TokenId $this._TokenId
|
|
return $this._IsFullObject
|
|
}
|
|
|
|
# Bulk-export sub-resource contract. Override in subclasses that need to
|
|
# fetch extra resources beyond the main object body (e.g. relationships,
|
|
# script content, target apps). Phase 1 is the entry point; phases 2+ come
|
|
# from follow-ups returned by ApplySubResourceBatchResult.
|
|
# Each request: [PSCustomObject]@{ Key = '<unique-within-this-policy>'; Url = '<graph-relative>'; Method = 'GET' (optional); Headers = @{} (optional) }
|
|
[PSCustomObject[]] GetSubResourceBatchRequests([int]$Phase)
|
|
{
|
|
return @()
|
|
}
|
|
|
|
# Routes a batched response back into the policy. Returns follow-up requests
|
|
# for the next phase (empty array = done). $Body is $null for non-2xx
|
|
# responses; the orchestrator already logged the HTTP status.
|
|
[PSCustomObject[]] ApplySubResourceBatchResult([int]$Phase, [string]$Key, $Body)
|
|
{
|
|
return @()
|
|
}
|
|
|
|
# Called once per opted-in policy by Invoke-PolicySubresourceFetch AFTER all
|
|
# phases complete. Override to build a derived property from the responses
|
|
# already applied via ApplySubResourceBatchResult (e.g. AppConfiguration's
|
|
# #CustomRefTargetedApps, which needs every targeted-app body resolved before
|
|
# it can be assembled). Default no-op. Runs even when GetSubResourceBatchRequests
|
|
# returned nothing, so a class can finalize from cache/state alone.
|
|
[void] FinalizeSubResources()
|
|
{
|
|
}
|
|
|
|
GetAssignments()
|
|
{
|
|
if(-not $this.Object.Assignments -and $null -ne $this._TokenId)
|
|
{
|
|
$url = "$($this._PolicyType.API)/$($this.id)/assignments"
|
|
$assignments = (Invoke-MSGraphAPI -Url $url -TokenId $this._TokenId).Value
|
|
if($assignments)
|
|
{
|
|
$this.Object.Assignments = $assignments
|
|
}
|
|
}
|
|
}
|
|
|
|
# Returns the specific objects this policy depends on.
|
|
# Each entry: [PSCustomObject]@{ TypeId = "<policy type ID>"; Id = "<object GUID>" }
|
|
# Override in derived classes to enable targeted dependency loading.
|
|
# The base implementation returns an empty array (falls back to type-level _Dependencies).
|
|
[PSCustomObject[]] GetDependencyReferences()
|
|
{
|
|
return @()
|
|
}
|
|
|
|
[String]GetFileName()
|
|
{
|
|
# `$this.` is required: a bare `GetFileName($null)` is parsed as a COMMAND
|
|
# invocation, not a method call, so this overload threw "The term
|
|
# 'GetFileName' is not recognized" for every caller that omitted the path.
|
|
return ($this.GetFileName($null))
|
|
}
|
|
|
|
[String]GetFileName([String]$Path)
|
|
{
|
|
$fileName = $this.Name.Trim('.')
|
|
# Append `_<id>` to the filename when EITHER the user-facing setting
|
|
# AddIDToExportFile is on (matches the OLD project's behaviour) OR the
|
|
# bulk-export collision detector flagged this specific policy because
|
|
# another policy in the same type bucket shares its displayName.
|
|
# Without the collision case, exporting N same-named policies would
|
|
# silently overwrite the same file N times and only the last one would
|
|
# survive on disk (seen in a lab tenant: 81 CA policies -> 39 unique
|
|
# displayName -> 42 lost without this guard).
|
|
$forceId = (Get-SettingValue "AddIDToExportFile") -eq $true -or $this._NeedsIdInFilename -eq $true
|
|
if($forceId -and $this.Id -and $this.PolicyType.SkipAddIDOnFileName -ne $true) {
|
|
$fileName = ($fileName + "_" + $this.Id)
|
|
}
|
|
$fileName = "$((Remove-InvalidFileNameChars $fileName)).json"
|
|
|
|
if($Path) {
|
|
$fileName = [IO.Path]::Combine($Path, $fileName)
|
|
}
|
|
|
|
return $fileName
|
|
}
|
|
|
|
[string]ExportToFile([String]$ParentFolder)
|
|
{
|
|
return ($this.ExportToFile($ParentFolder, $null))
|
|
}
|
|
|
|
[string]ExportToFile([String]$ParentFolder, [String]$FileName)
|
|
{
|
|
$returnValue = $null
|
|
|
|
if(-not $FileName) { $FileName = $this.GetFileName($ParentFolder) }
|
|
|
|
# Clone JsonObject via JSON round-trip so PreExportCommand can mutate
|
|
# without affecting the in-memory policy. Same pattern as Clone().
|
|
$exportObject = ConvertFrom-Json (ConvertTo-Json $this.JsonObject -Depth 50)
|
|
|
|
$this.PolicyType.PreExportCommand($this, $exportObject)
|
|
|
|
# Central serializer so the policy file honours the same
|
|
# SortJsonProperties / ExportJsonFormat settings as the MigrationTable
|
|
# and the sidecars. It previously called ConvertTo-Json directly, which
|
|
# meant SortJsonProperties was silently ignored for policy exports.
|
|
$json = ConvertTo-GraphExportJson $exportObject -Depth 50
|
|
|
|
# Organization values -> placeholders. Internal/ExportTokens.ps1 owns the
|
|
# replace, the "Replace organization values in export files" setting and
|
|
# the hidden ExportReplaceTokens key that narrows which values are
|
|
# replaced.
|
|
#
|
|
# Masked against THIS policy's own organization, not the default token's:
|
|
# exporting a policy listed from tenant A while tenant B is the default
|
|
# otherwise masked B's id in A's data and left A's real tenant id in the
|
|
# file. Same reasoning as Save-GraphObjectToFile does for the sidecars.
|
|
$orgInfo = Get-GraphObjectOrganizationInfo $this
|
|
$json = Convert-GraphOrganizationValueToToken $json `
|
|
-OrganizationId $orgInfo.OrganizationId `
|
|
-OrganizationName $orgInfo.OrganizationName
|
|
|
|
|
|
try
|
|
{
|
|
# [IO.File]::WriteAllText is ~10x faster than Out-File for this hot path
|
|
# (called once per exported policy). Out-File goes through PowerShell's
|
|
# pipeline + .NET StreamWriter wrapper with Format-Table conversion; the
|
|
# direct .NET call skips all that.
|
|
#
|
|
# Encoding comes from the "Export file encoding" setting, which every
|
|
# exported file shares - policy JSON, the MigrationTable and the
|
|
# assignment sidecars. This used to hard-code UTF-8 WITH BOM, and the
|
|
# sidecars used a bare Out-File (UTF-16LE on PS5.1). Consumers that read
|
|
# the export as plain UTF-8 - git, the Intune portal, other tooling -
|
|
# choke on a BOM, so the default is now UTF-8 without one.
|
|
[System.IO.File]::WriteAllText($fileName, $json, (Get-ExportFileEncoding))
|
|
$returnValue = $fileName
|
|
}
|
|
catch
|
|
{
|
|
Write-LogError "Failed to save file $fileName" $_.Exception
|
|
}
|
|
|
|
return $returnValue
|
|
}
|
|
|
|
Hidden SetDescription($Description)
|
|
{
|
|
if($this.HasDescription -ne $false) {
|
|
$this.JsonObject.description = $Description
|
|
}
|
|
}
|
|
|
|
Hidden [IntunePolicyBase]Clone()
|
|
{
|
|
$cloned = $this._PolicyType.GetObject((ConvertTo-Json $this.JsonObject -Depth 50))
|
|
|
|
$cloned._ClonedFromObject = ?? $this._ClonedFromObject $this
|
|
|
|
$cloned.TenantId = $this.TenantId
|
|
|
|
# Import runs against the clone, and PreImportCommand hooks read FileInfo to
|
|
# find files that sit next to the exported json - the ADMX/ADML pair, the
|
|
# Terms of Use PDFs. Rebuilding the clone from JSON alone dropped it, so
|
|
# those lookups saw $null and silently fell back to the app packages folder.
|
|
$cloned.FileInfoObject = $this.FileInfoObject
|
|
|
|
return $cloned
|
|
}
|
|
|
|
Hidden [IntunePolicyBase]CopyObject([String]$Name, [String]$Description, [int]$ToTokenId)
|
|
{
|
|
# Backward-compat overload — inherits scope tags from the source.
|
|
return $this.CopyObject($Name, $Description, $ToTokenId, $null)
|
|
}
|
|
|
|
Hidden [IntunePolicyBase]CopyObject([String]$Name, [String]$Description, [int]$ToTokenId, [string[]]$ScopeTagIds)
|
|
{
|
|
Write-Log "Copy $($this.PolicyType.Title) object $($this.Name)"
|
|
|
|
if($this.IsFromFile -eq $false -and $this.IsFullObject -eq $false -and $this._RequiresFullObject -ne $false) {
|
|
$this.Get() | Out-Null
|
|
}
|
|
|
|
$clonedObject = $this.Clone()
|
|
|
|
if($clonedObject)
|
|
{
|
|
if($clonedObject._RequiresFullObject -eq $true) {
|
|
[void]$clonedObject.Get()
|
|
}
|
|
|
|
$clonedObject.SetName($Name)
|
|
if($Description) {
|
|
$clonedObject.SetDescription($Description)
|
|
}
|
|
|
|
# Override scope tags on the clone when the caller passed an explicit
|
|
# list. $null = leave as-is (inherit). Empty array = explicit reset
|
|
# (Intune will re-apply Default automatically in that case).
|
|
if($null -ne $ScopeTagIds -and $clonedObject.PolicyType.ScopeTagProperty) {
|
|
$stProp = [string]$clonedObject.PolicyType.ScopeTagProperty
|
|
if($clonedObject.JsonObject.PSObject.Properties[$stProp]) {
|
|
$clonedObject.JsonObject.$stProp = @($ScopeTagIds)
|
|
} else {
|
|
$clonedObject.JsonObject | Add-Member -MemberType NoteProperty -Name $stProp -Value @($ScopeTagIds) -Force
|
|
}
|
|
}
|
|
|
|
Add-GraphNavigationProperties $this
|
|
|
|
$newObj = $clonedObject.ImportObject($ToTokenId, $null)
|
|
if(-not $newObj)
|
|
{
|
|
return $null
|
|
}
|
|
|
|
if($newObj.PolicyType.NavigationProperties -eq $true) {
|
|
Set-GraphNavigationProperties $newObj $this
|
|
}
|
|
|
|
$newObj.PolicyType.PostCopyCommand($newObj, $this)
|
|
|
|
return $newObj
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
Hidden [PSCustomObject]ImportObject([int]$TokenId, [HashTable]$BulkImport)
|
|
{
|
|
# Get-OperationTokenInfo, not Get-TokenInfo: the latter treats 0 as "list
|
|
# every token", and 0 is also how a caller spells "the default token". With
|
|
# two tenants signed in that made $tokenInfo an ARRAY, so .TenantId below
|
|
# restored %OrganizationId% as two space-joined guids.
|
|
$tokenInfo = Get-OperationTokenInfo $TokenId
|
|
|
|
Write-Log "Import $($this.PolicyType.Title) object '$($this.Name)' to $($tokenInfo.TenantName)"
|
|
|
|
$clonedObject = $this.Clone()
|
|
|
|
Remove-GraphPropertiesForImport $clonedObject $clonedObject.JsonObject
|
|
|
|
$params = @{}
|
|
$strAPI = (?? $clonedObject.PolicyType.APIPOST $clonedObject.PolicyType.API)
|
|
$method = "POST"
|
|
|
|
$ret = $clonedObject.PolicyType.PreImportCommand($clonedObject)
|
|
|
|
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"])
|
|
}
|
|
}
|
|
|
|
# Resolve scope tags on the cloned body before serialization (ImportScopeTags
|
|
# setting + same/cross-tenant remap). Keeps source IDs same-tenant, remaps
|
|
# cross-tenant, or resets to Default when the setting is off.
|
|
Set-GraphImportScopeTags $clonedObject $TokenId
|
|
|
|
$json = ConvertTo-Json $clonedObject.JsonObject -Depth 50
|
|
if($tokenInfo -and $clonedObject._ClonedFromObject.TenantId -ne $tokenInfo.TenantId)
|
|
{
|
|
# Call Update-JsonForEnvironment before importing the object
|
|
# E.g. PolicySets contains references, AppConfiguration policies reference apps etc.
|
|
$json = Update-JsonForEnvironment $json $clonedObject $TokenId
|
|
}
|
|
|
|
# Placeholders -> values. Always runs: an export made when the replace
|
|
# setting was on carries placeholders whatever it is set to now.
|
|
# Internal/ExportTokens.ps1 decides which placeholders are restored.
|
|
#
|
|
# Restored to the TARGET tenant of this import, not to whichever tenant
|
|
# happens to hold the default token: importing into tenant B on a
|
|
# non-default token used to write tenant A's GUID into B's policy.
|
|
$json = Convert-GraphOrganizationTokenToValue $json `
|
|
-OrganizationId $tokenInfo.TenantId `
|
|
-OrganizationName $tokenInfo.TenantName
|
|
|
|
if($BulkImport) {
|
|
Write-Status "Add $($this.Name) ($($this.PolicyName)) with id $($this.Id) to Bulk Import list"
|
|
|
|
$bulkImportObject = [PSCustomObject]@{
|
|
id = ($this.Name + "_" + (New-Guid).Guid.SubString(0,8))
|
|
method = $method
|
|
url = $strAPI.TrimStart('/')
|
|
headers = @{"Content-Type"="application/json;odata.metadata=none"}
|
|
body = $json | ConvertFrom-Json #[System.Text.Encoding]::UTF8.GetBytes($json)
|
|
}
|
|
$BulkImport.Add($bulkImportObject, [PSCustomObject]@{
|
|
ImportObject = $clonedObject
|
|
FromObject = $this
|
|
})
|
|
return $null
|
|
}
|
|
|
|
$newObj = $null
|
|
|
|
$responseObject = $clonedObject.ImportObject($strAPI, $json, $method, $TokenId, $params)
|
|
|
|
$newObj = $clonedObject.ProcessImportResponse($TokenId, $responseObject, $method)
|
|
|
|
return $newObj
|
|
}
|
|
|
|
Hidden [PSCustomObject]ImportObject([string]$API,[string]$Json, [string]$Method, [int]$TokenID, [Hashtable]$Params)
|
|
{
|
|
$responseObject = (Invoke-MSGraphAPI -Url $API -Content $Json -HttpMethod $Method -TokenId $TokenID -FullResponseObject @Params)
|
|
|
|
return $responseObject
|
|
}
|
|
|
|
Hidden [IntunePolicyBase]UpdateObject([IntunePolicyBase]$ExistingObject, [int]$TokenId)
|
|
{
|
|
if($null -eq $ExistingObject) {
|
|
Write-Log "Cannot update $($this.PolicyType.Title) object '$($this.Name)'. Existing object was not supplied." 3
|
|
return $null
|
|
}
|
|
|
|
# One token, never the whole list - see ImportObject above.
|
|
$tokenInfo = Get-OperationTokenInfo $TokenId
|
|
Write-Log "Update $($this.PolicyType.Title) object '$($ExistingObject.Name)' from imported object '$($this.Name)'"
|
|
|
|
$updateObject = $this.Clone()
|
|
$targetId = $ExistingObject.Id
|
|
if(-not $targetId) {
|
|
Write-Log "Cannot update $($this.PolicyType.Title) object '$($this.Name)'. Existing object has no id." 3
|
|
return $null
|
|
}
|
|
|
|
if($updateObject.JsonObject.PSObject.Properties['id']) {
|
|
$updateObject.JsonObject.id = $targetId
|
|
}
|
|
else {
|
|
$updateObject.JsonObject | Add-Member -MemberType NoteProperty -Name 'id' -Value $targetId -Force
|
|
}
|
|
$updateObject._TokenID = $TokenId
|
|
if($tokenInfo) { $updateObject.TenantId = $tokenInfo.TenantId }
|
|
|
|
$params = @{}
|
|
$strAPI = (?? $updateObject.PolicyType.APIPUT $updateObject.PolicyType.API) + "/$targetId"
|
|
$method = "PATCH"
|
|
|
|
$ret = $updateObject.PolicyType.PreUpdateCommand($updateObject, $ExistingObject)
|
|
if($ret -is [HashTable])
|
|
{
|
|
if($ret.ContainsKey("Update") -and $ret["Update"] -eq $false)
|
|
{
|
|
Write-Log "Update skipped by $($updateObject.PolicyType.Title) pre-update command for '$($ExistingObject.Name)'" 2
|
|
return $null
|
|
}
|
|
|
|
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"])
|
|
}
|
|
}
|
|
|
|
Remove-GraphPropertiesForImport $updateObject $updateObject.JsonObject
|
|
# isAssigned is a computed status flag many endpoints refuse to accept
|
|
# back on PATCH - the original project stripped it on every update.
|
|
Remove-Property $updateObject.JsonObject "isAssigned"
|
|
foreach($propertyToRemove in @($updateObject.PolicyType.PropertiesToRemoveForUpdate)) {
|
|
Remove-Property $updateObject.JsonObject $propertyToRemove
|
|
}
|
|
|
|
$json = ConvertTo-Json $updateObject.JsonObject -Depth 50
|
|
if($tokenInfo -and $updateObject._ClonedFromObject.TenantId -ne $tokenInfo.TenantId)
|
|
{
|
|
$json = Update-JsonForEnvironment $json $updateObject $TokenId
|
|
}
|
|
|
|
# Placeholders -> values. Always runs: an export made when the replace
|
|
# setting was on carries placeholders whatever it is set to now.
|
|
# Internal/ExportTokens.ps1 decides which placeholders are restored.
|
|
#
|
|
# Restored to the TARGET tenant of this import, not to whichever tenant
|
|
# happens to hold the default token: importing into tenant B on a
|
|
# non-default token used to write tenant A's GUID into B's policy.
|
|
$json = Convert-GraphOrganizationTokenToValue $json `
|
|
-OrganizationId $tokenInfo.TenantId `
|
|
-OrganizationName $tokenInfo.TenantName
|
|
|
|
$responseObject = Invoke-MSGraphAPI -Url $strAPI -Content $json -HttpMethod $method -TokenId $TokenId -FullResponseObject @params
|
|
if($responseObject.Success) {
|
|
$ExistingObject._TokenID = $TokenId
|
|
if($tokenInfo) { $ExistingObject.TenantId = $tokenInfo.TenantId }
|
|
[void]$ExistingObject.Get()
|
|
|
|
Write-Log "$($ExistingObject.PolicyType.Title) object updated successfully: $($ExistingObject.Name) ($($ExistingObject.Id))"
|
|
|
|
Import-GraphObjectAssignment $ExistingObject $this
|
|
|
|
return $ExistingObject
|
|
}
|
|
|
|
# Include the Graph error body - StatusDescription alone ("Bad
|
|
# Request") says nothing about WHY the update was rejected.
|
|
$errorDetail = ""
|
|
try {
|
|
$errorContent = if($responseObject.Content -is [string]) { $responseObject.Content | ConvertFrom-Json } else { $responseObject.Content }
|
|
if($errorContent.error.message) { $errorDetail = " - $($errorContent.error.message)" }
|
|
} catch { }
|
|
Write-Log "Failed to update $($ExistingObject.PolicyType.Title) object '$($ExistingObject.Name)' ($($ExistingObject.Id)). $($responseObject.StatusCode) $($responseObject.StatusDescription)$errorDetail" 3
|
|
return $null
|
|
}
|
|
|
|
Hidden [PSCustomObject]ProcessImportResponse([int]$TokenId, [PSCustomObject]$Response, [String]$Method)
|
|
{
|
|
$newObj = $null
|
|
# One token, never the whole list - see ImportObject above.
|
|
$tokenInfo = Get-OperationTokenInfo $TokenId
|
|
if($Response.Success)
|
|
{
|
|
if($method -eq "POST" -and $Response.Content) {
|
|
$newObj = $this.PolicyType.GetObject($Response.Content)
|
|
if(-not $newObj -and $this.PolicyType._ObjectClass) {
|
|
# GetObject runs CheckPolicy for _VerifyObject types and can
|
|
# reject a minimal POST response body (e.g. Settings Catalog
|
|
# create responses may omit templateReference). We KNOW the
|
|
# object belongs to this type - we just created it - so
|
|
# construct it directly without verification.
|
|
$newObj = New-Object -TypeName $this.PolicyType._ObjectClass -ArgumentList $Response.Content
|
|
}
|
|
}
|
|
else {
|
|
[void]$this.Get()
|
|
$newObj = $this
|
|
}
|
|
if($tokenInfo) {
|
|
$newObj._TokenID = $tokenInfo.Id
|
|
$newObj.TenantId = $tokenInfo.TenantId
|
|
}
|
|
else {
|
|
$newObj._TokenID = $TokenId
|
|
try {
|
|
$userInfo = (Get-AuthProvider).GetUserInfo($TokenId)
|
|
if($userInfo -and $userInfo.TenantId) { $newObj.TenantId = $userInfo.TenantId }
|
|
} catch { }
|
|
}
|
|
|
|
Write-Log "$($newObj.PolicyType.Title) object imported successfully: $($newObj.Name) ($($newObj.Id))"
|
|
|
|
# PostImportCommand post-processes using SOURCE data that was stripped
|
|
# from this clone before the create POST (AdminTemplate definitionValues,
|
|
# app script/content file info, ...). $this here is the stripped clone,
|
|
# so hand it the ORIGINAL via _ClonedFromObject; fall back to $this for a
|
|
# direct (non-clone) call.
|
|
$sourceObj = $this._ClonedFromObject
|
|
if($null -eq $sourceObj) { $sourceObj = $this }
|
|
$newObj.PolicyType.PostImportCommand($newObj, $sourceObj)
|
|
|
|
Import-GraphObjectAssignment $newObj $this
|
|
}
|
|
return $newObj
|
|
}
|
|
|
|
Hidden [bool]Set([string]$Json)
|
|
{
|
|
return ($this.Set($null, $Json, $null, $null))
|
|
}
|
|
|
|
Hidden [bool]Set([string]$API, [string]$Json)
|
|
{
|
|
return ($this.Set($API, $Json, $null, $null))
|
|
}
|
|
|
|
Hidden [bool]Set([string]$API, [string]$Json, [string]$Method, [Hashtable]$Params)
|
|
{
|
|
if($null -eq $Json) {
|
|
Write-Log "Cannot update object without Json data"
|
|
return $false
|
|
}
|
|
|
|
if(-not $API) {
|
|
$API = (?? $this.PolicyType.APIPOST $this.PolicyType.API) + "/$($this.Id)"
|
|
}
|
|
|
|
if($null -ne $this._TokenID) {
|
|
$tokenId = $this._TokenID
|
|
}
|
|
else {
|
|
Write-Log "Updating object $($this.Name) ($($this.Id)) with default token. TokenId not set on object" 2
|
|
$tokenId = 0
|
|
}
|
|
|
|
if(-not $Method) {
|
|
$Method = "PATCH"
|
|
}
|
|
|
|
$responseObject = (Invoke-MSGraphAPI -Url $API -Content $Json -HttpMethod $Method -TokenId $tokenId -FullResponseObject @Params)
|
|
if($responseObject.Success) {
|
|
Write-Log "$($this.Name) ($($this.Id)) updated successfully"
|
|
[void]$this.Get()
|
|
}
|
|
|
|
return $responseObject.Success
|
|
}
|
|
|
|
Hidden [bool]Delete([HashTable]$BulkDelete)
|
|
{
|
|
$deleteAPI = $null
|
|
$ret = $this.PolicyType.PreDeleteCommand($this)
|
|
if($ret -is [HashTable])
|
|
{
|
|
if($ret.ContainsKey("Delete") -and $ret["Delete"] -eq $false)
|
|
{
|
|
# Delete handled manually or aborted
|
|
return $false
|
|
}
|
|
|
|
if($ret.ContainsKey("API"))
|
|
{
|
|
$deleteAPI = $ret["API"]
|
|
}
|
|
}
|
|
|
|
if(-not $deleteAPI) {
|
|
$deleteAPI = $this.PolicyType.APIDELETE
|
|
}
|
|
|
|
if($BulkDelete -is [HashTable])
|
|
{
|
|
Write-Status "Add $($this.Name) ($($this.PolicyName)) with id $($this.Id) to Bulk Delete list"
|
|
$url = ($deleteAPI + "/$($this.Id)")
|
|
$bulkDeleteObject = [PSCustomObject]@{
|
|
id = $this.Id
|
|
method = "DELETE"
|
|
url = $url.TrimStart('/')
|
|
headers = @{"Accept"="application/json;odata.metadata=none"}
|
|
}
|
|
$BulkDelete.Add($bulkDeleteObject, $this)
|
|
}
|
|
else {
|
|
Write-Status "Delete $($this.Name) ($($this.PolicyName)) with id $($this.Id)"
|
|
$deleteAPI = ($deleteAPI + "/$($this.Id)")
|
|
$response = Invoke-MSGraphAPI -Url $deleteAPI -HttpMethod "DELETE" -ODataMetadata "none" -FullResponseObject
|
|
if($response.Success) {
|
|
Write-LogDebug "Policy deleted successfully"
|
|
}
|
|
return $response.Success
|
|
}
|
|
return $true
|
|
}
|
|
}
|
|
|
|
<#
|
|
class IntuneImportExportPolicyBase : PSCustomObject, System.ComponentModel.INotifyPropertyChanged
|
|
{
|
|
[Boolean]$IsSelected = $true
|
|
[Boolean]$_AddObjectType = $null
|
|
[Boolean]$_AddCompanyName = $null
|
|
[System.Collections.ArrayList]$PropertyChanged = @()
|
|
|
|
IntuneImportExportPolicyBase() {
|
|
$this.PSObject._AddObjectType = (Get-SettingValue "AddObjectType")
|
|
$this.PSObject._AddCompanyName = (Get-SettingValue "AddCompanyName")
|
|
|
|
Add-ObjectProperty $this "AddObjectType" { return $this.PSObject._AddObjectType } {
|
|
param([Boolean]$Value)
|
|
$this.PSObject._AddObjectType = $Value;
|
|
$this.PSObject.psobject.NotifyPropertyChanged('AddObjectType')
|
|
}
|
|
|
|
Add-ObjectProperty $this "AddCompanyName" { return $this.PSObject._AddObjectType } {
|
|
param([Boolean]$Value)
|
|
$this.psobject._AddCompanyName = $Value;
|
|
$this.psobject.NotifyPropertyChanged('AddCompanyName')
|
|
}
|
|
}
|
|
|
|
add_PropertyChanged([System.ComponentModel.PropertyChangedEventHandler]$handler)
|
|
{
|
|
$this.psobject.PropertyChanged.Add($handler)
|
|
}
|
|
remove_PropertyChanged([System.ComponentModel.PropertyChangedEventHandler]$handler)
|
|
{
|
|
$this.psobject.PropertyChanged.Remove($handler)
|
|
}
|
|
|
|
NotifyPropertyChanged([string]$propname)
|
|
{
|
|
if ($this.psobject.PropertyChanged)
|
|
{
|
|
$evargs = [System.ComponentModel.PropertyChangedEventArgs]::new($propname)
|
|
$this.psobject.PropertyChanged.Invoke($this, $evargs) # invokes every member
|
|
}
|
|
}
|
|
}
|
|
|
|
class IntuneExportPolicy : IntuneImportExportPolicyBase
|
|
{
|
|
[string]$_ExportFolder = $null
|
|
[Boolean]$_AddExportAssignments = $false
|
|
|
|
IntuneExportPolicy() : Base()
|
|
{
|
|
$this.PSObject._ExportFolder = (?? (Get-SettingStoreValue "" "LastUsedRoot") (Get-SettingValue "RootFolder"))
|
|
$this.PSObject._AddExportAssignments = (Get-SettingValue "ExportAssignments")
|
|
|
|
Add-ObjectProperty $this "ExportFolder" { return $this.PSObject._ExportFolder } ({
|
|
param([String]$Value)
|
|
$this.psobject._ExportFolder = $Value;
|
|
$this.psobject.NotifyPropertyChanged('ExportFolder')
|
|
})
|
|
|
|
Add-ObjectProperty $this "AddExportAssignments" { return $this.PSObject._AddExportAssignments } {
|
|
param([Boolean]$Value)
|
|
$this.psobject._AddExportAssignments = $Value;
|
|
$this.psobject.NotifyPropertyChanged('AddExportAssignments')
|
|
}
|
|
}
|
|
}
|
|
#>
|
|
|
|
class IntuneManagerExportSettings
|
|
{
|
|
[Boolean]$AddObjectType = $true
|
|
[Boolean]$AddCompanyName = $true
|
|
[string]$Filter = $null
|
|
[Boolean]$ExportAssignments = $false
|
|
# Depth of group-membership recursion. 1 (default) = only the group directly
|
|
# assigned to the policy. 2 = also exports groups that are direct members
|
|
# of the assigned group. 3 = one more level. Cycle-safe via the existing
|
|
# per-folder MigrationTable dedup index.
|
|
[Int]$ExportNestedGroupLevels = 1
|
|
|
|
Hidden [string]$ExportFolder = $null
|
|
|
|
IntuneManagerExportSettings()
|
|
{
|
|
$this.AddObjectType = (Get-SettingValue "AddObjectType")
|
|
$this.AddCompanyName = (Get-SettingValue "AddCompanyName")
|
|
$this.ExportAssignments = (Get-SettingValue "ExportAssignments")
|
|
$this.ExportFolder = (?? (Get-SettingStoreValue "" "LastUsedRoot") (Get-SettingValue "RootFolder"))
|
|
|
|
# Setting is registered as String so the form can use a free-form
|
|
# editor; coerce to Int with a floor of 1 here so consumers can treat
|
|
# it as a plain integer.
|
|
$rawDepth = Get-SettingValue "ExportNestedGroupLevels" "1"
|
|
$parsed = 0
|
|
if([int]::TryParse([string]$rawDepth, [ref]$parsed) -and $parsed -ge 1) {
|
|
$this.ExportNestedGroupLevels = $parsed
|
|
} else {
|
|
$this.ExportNestedGroupLevels = 1
|
|
}
|
|
}
|
|
|
|
Save()
|
|
{
|
|
Save-SettingStoreValue "" "LastUsedRoot" $this.ExportFolder
|
|
}
|
|
}
|
|
|
|
class IntuneManagerImportSettings
|
|
{
|
|
[string]$ImportFolder = $null
|
|
[String]$MigrationTableInfo = $false
|
|
[Boolean]$ImportAssignments = $false
|
|
[Boolean]$ImportScopeTags = $false
|
|
[Boolean]$ReplaceDependencyIDs = $false
|
|
[Boolean]$SameTenant = $false
|
|
[String]$ImportType = $null
|
|
|
|
IntuneManagerImportSettings()
|
|
{
|
|
$path = Get-SettingStoreValue "" "LastUsedFullPath"
|
|
$this.ImportFolder =(?? $path (Get-SettingValue "RootFolder"))
|
|
$this.ImportAssignments = (Get-SettingValue "ImportAssignments")
|
|
$this.ImportScopeTags = (Get-SettingValue "ImportScopeTags")
|
|
$this.ImportType = (Get-SettingValue "ImportType" "alwaysImport")
|
|
$this.ReplaceDependencyIDs = (Get-SettingValue "ResolveReferenceInfo") -ne $false
|
|
}
|
|
|
|
Save()
|
|
{
|
|
# Persist the per-import toggles to the SAME SubPath they are registered
|
|
# under ("IntuneManager") so the import pipeline reads them back via
|
|
# Get-SettingValue. (Was a broken copy of the export Save() that wrote a
|
|
# non-existent $this.ExportFolder.)
|
|
Save-SettingStoreValue "IntuneManager" "ImportAssignments" $this.ImportAssignments
|
|
Save-SettingStoreValue "IntuneManager" "ImportScopeTags" $this.ImportScopeTags
|
|
Save-SettingStoreValue "IntuneManager" "ResolveReferenceInfo" $this.ReplaceDependencyIDs
|
|
if($this.ImportType) { Save-SettingStoreValue "IntuneManager" "ImportType" $this.ImportType }
|
|
}
|
|
}
|
|
|
|
# State holder for Bulk Scope Tags. UI binds to it; Set-GraphBulkScopeTags
|
|
# reads it. Kept separate from IntuneManagerImportSettings because the two
|
|
# operations have unrelated knobs and a shared class would force every UI to
|
|
# carry the union.
|
|
class IntuneManagerScopeTagSettings
|
|
{
|
|
# Action applied to every selected policy:
|
|
# Add — union of current tag list + ScopeTagIds (no-op if all already present)
|
|
# Replace — overwrite current list with ScopeTagIds
|
|
# Remove — remove ScopeTagIds from the current list
|
|
[string]$Action = "Add"
|
|
|
|
# IDs of the scope tags the user chose in the picker. May be empty when
|
|
# CleanupOrphans is the only thing the user wants done.
|
|
[String[]]$ScopeTagIds = @()
|
|
|
|
# Name filter — matched as regex (falls back to literal substring if not
|
|
# a valid regex). Empty = touch every listed policy.
|
|
[string]$Filter = $null
|
|
|
|
# When true, any tag id on a policy that doesn't resolve to a real tag in
|
|
# the tenant's current catalogue is stripped from the resulting list. Lets
|
|
# users clean up references to deleted tags as a side-effect of any action,
|
|
# or run a pure cleanup pass (Action=Add with no ScopeTagIds).
|
|
[Boolean]$CleanupOrphans = $false
|
|
}
|
|
|
|
# State holder for Bulk Assignments (Phase 1: group / exclusion-group / all-
|
|
# devices / all-users targets with optional assignment filters; no app intent
|
|
# or per-platform settings yet — those land in later phases).
|
|
class IntuneManagerAssignmentSettings
|
|
{
|
|
# Action applied to every selected policy:
|
|
# Add — union of current assignments + chosen Assignments (no-op if
|
|
# all already present, compared by target type + groupId +
|
|
# filterId + filterType)
|
|
# Replace — overwrite current assignments with chosen Assignments
|
|
# Remove — remove the chosen Assignments from the current list
|
|
[string]$Action = "Add"
|
|
|
|
# User-chosen assignments to apply. Each entry is a PSCustomObject:
|
|
# @{
|
|
# TargetType = 'groupAssignmentTarget' | 'exclusionGroupAssignmentTarget'
|
|
# | 'allDevicesAssignmentTarget' | 'allLicensedUsersAssignmentTarget'
|
|
# GroupId = <string> (group/exclusion targets only)
|
|
# GroupName = <string> (display only)
|
|
# FilterId = <string> (optional, group/exclusion only)
|
|
# FilterName = <string> (display only)
|
|
# FilterType = 'include' | 'exclude' (when FilterId set)
|
|
# }
|
|
# PSCustomObject[] (not a typed class) so the UI can mutate in place and
|
|
# the public command can ingest without an extra projection step.
|
|
[PSCustomObject[]]$Assignments = @()
|
|
|
|
# Name filter — matched as regex (falls back to literal substring if not
|
|
# a valid regex). Empty = touch every listed policy.
|
|
[string]$Filter = $null
|
|
}
|