mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 10:55:38 +02:00
525 lines
19 KiB
PowerShell
525 lines
19 KiB
PowerShell
#ImportOrder 220
|
|
|
|
#########################################################################################
|
|
#
|
|
# Apple Update Group
|
|
#
|
|
#########################################################################################
|
|
|
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("TypeNotFound","", Justification = "")]
|
|
class ConditionalAccessGroup : IntunePolicyGroupBase
|
|
{
|
|
ConditionalAccessGroup() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._ID = "ConditionalAccess"
|
|
$this._Name = "Conditional Access"
|
|
$this._Icon = "ConditionalAccess"
|
|
}
|
|
}
|
|
|
|
#########################################################################################
|
|
#
|
|
# Conditional Access Policies
|
|
#
|
|
#########################################################################################
|
|
|
|
# region Conditional Access Policies
|
|
class ConditionalAccessType : IntunePolicyTypeBase
|
|
{
|
|
ConditionalAccessType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "ConditionalAccessGroup")
|
|
$this._PolicyName = "Conditional Access"
|
|
$this._ID = "ConditionalAccess"
|
|
$this._HasPlatform = $false
|
|
$this._API = "identity/conditionalAccess/policies"
|
|
# Entra object - no roleScopeTagIds in the Graph schema (a PATCH
|
|
# no-ops), so no scope-tag support.
|
|
$this._ScopeTagProperty = ""
|
|
$this._Dependencies = @("NamedLocations","Applications","TermsOfUse","AuthenticationStrengths","AssignmentFilters")
|
|
$this._Permissions = @("Policy.ReadWrite.ConditionalAccess")
|
|
$this._ObjectClass = "ConditionalAccessObject"
|
|
$this._ExpandAssignmentsList = $false
|
|
$this._SupportsAssignments = $false
|
|
$this._HasPageSizeSupport = $false
|
|
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
}
|
|
|
|
[Hashtable]PreImportCommand([PSCustomObject]$PolicyObject)
|
|
{
|
|
# Tenant-lockout guard: rewrite the imported policy's state per the
|
|
# ConditionalAccessState setting (default: disabled). Logic lives in
|
|
# Internal/IntuneManager.ps1 so it is unit-testable.
|
|
Set-CAPolicyImportState $PolicyObject
|
|
|
|
if($PolicyObject.grantControls.authenticationStrength)
|
|
{
|
|
$PolicyObject.JsonObject.grantControls.operator = "AND"
|
|
#$tmpObj = Get-GraphObjectFromFile $file
|
|
|
|
#$authSetting = [PSCustomObject]@{
|
|
# id = $tmpObj.grantControls.authenticationStrength.id
|
|
#}
|
|
#$PolicyObject.JsonObject.grantControls.authenticationStrength = $authSetting
|
|
}
|
|
|
|
if($PolicyObject.JsonObject.sessionControls.disableResilienceDefaults -eq $false)
|
|
{
|
|
$PolicyObject.JsonObject.sessionControls.disableResilienceDefaults = $null
|
|
}
|
|
|
|
return $null
|
|
}
|
|
|
|
PostExportCommand([PSCustomObject]$PolicyObject, [String]$PathToFile)
|
|
{
|
|
$ids = @()
|
|
foreach($id in ($PolicyObject.JsonObject.conditions.users.includeGroups + $PolicyObject.JsonObject.conditions.users.excludeGroups))
|
|
{
|
|
if($id -in $ids) { continue }
|
|
elseif($id -eq "GuestsOrExternalUsers") { continue }
|
|
elseif($id -eq "All") { continue }
|
|
elseif($id -eq "None") { continue }
|
|
|
|
$ids += $id
|
|
Add-GraphMigrationObject $id "groups" "Group" ([IO.Path]::GetDirectoryName($PathToFile)) $PolicyObject._TokenId
|
|
}
|
|
|
|
foreach($id in ($PolicyObject.JsonObject.conditions.users.includeUsers + $PolicyObject.JsonObject.conditions.users.excludeUsers))
|
|
{
|
|
if($id -in $ids) { continue }
|
|
elseif($id -eq "GuestsOrExternalUsers") { continue }
|
|
elseif($id -eq "All") { continue }
|
|
elseif($id -eq "None") { continue }
|
|
|
|
$ids += $id
|
|
Add-GraphMigrationObject $id "users" "User" ([IO.Path]::GetDirectoryName($PathToFile)) $PolicyObject._TokenId
|
|
}
|
|
}
|
|
}
|
|
|
|
Class ConditionalAccessObject : IntunePolicyBase
|
|
{
|
|
ConditionalAccessObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
ConditionalAccessObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "ConditionalAccessType")
|
|
}
|
|
}
|
|
|
|
#########################################################################################
|
|
#
|
|
# Authentication Strengths
|
|
#
|
|
#########################################################################################
|
|
|
|
# region Authentication Strengths
|
|
class AuthenticationStrengthsType : IntunePolicyTypeBase
|
|
{
|
|
AuthenticationStrengthsType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "ConditionalAccessGroup")
|
|
$this._PolicyName = "Authentication Strengths"
|
|
$this._ID = "AuthenticationStrengths"
|
|
$this._HasPlatform = $false
|
|
$this._API = "identity/conditionalAccess/authenticationStrengths/policies"
|
|
$this._ImportOrder = 45
|
|
$this._Permissions = @("Policy.ReadWrite.ConditionalAccess")
|
|
$this._ObjectClass = "AuthenticationStrengthObject"
|
|
$this._ExpandAssignmentsList = $false
|
|
$this._SupportsAssignments = $false
|
|
$this._TopItems = 0
|
|
$this._Icon = "ConditionalAccess"
|
|
$this._HasPageSizeSupport = $false
|
|
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
}
|
|
|
|
[Hashtable]PreImportCommand([PSCustomObject]$PolicyObject)
|
|
{
|
|
if($PolicyObject.Object.policyType -ne "custom")
|
|
{
|
|
Write-Log "Built-in Authentication Strength objects cannot be imported" 2
|
|
@{ "Import" = $false }
|
|
}
|
|
|
|
return $null
|
|
}
|
|
}
|
|
|
|
Class AuthenticationStrengthObject : IntunePolicyBase
|
|
{
|
|
AuthenticationStrengthObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
AuthenticationStrengthObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "AuthenticationStrengthsType")
|
|
}
|
|
}
|
|
|
|
#########################################################################################
|
|
#
|
|
# Authentication Context
|
|
#
|
|
#########################################################################################
|
|
|
|
# region Authentication Context
|
|
class AuthenticationContextType : IntunePolicyTypeBase
|
|
{
|
|
AuthenticationContextType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "ConditionalAccessGroup")
|
|
$this._PolicyName = "Authentication Context"
|
|
$this._ID = "AuthenticationContext"
|
|
$this._HasPlatform = $false
|
|
$this._HasModified = $false
|
|
# Endpoint rejects a name $filter (verified live 2026-08-27) - searches filter client-side.
|
|
$this._SupportsNameFilter = $false
|
|
$this._API = "identity/conditionalAccess/authenticationContextClassReferences"
|
|
$this._PropertiesToRemove = @("@odata.type")
|
|
$this._SkipRemoveProperties = @('Id')
|
|
$this._ImportOrder = 46
|
|
$this._Permissions = @("Policy.ReadWrite.ConditionalAccess")
|
|
$this._ObjectClass = "AuthenticationContextObject"
|
|
$this._ExpandAssignmentsList = $false
|
|
$this._SupportsAssignments = $false
|
|
$this._TopItems = 0
|
|
$this._Icon = "ConditionalAccess"
|
|
$this._HasPageSizeSupport = $false
|
|
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
}
|
|
}
|
|
|
|
Class AuthenticationContextObject : IntunePolicyBase
|
|
{
|
|
AuthenticationContextObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
AuthenticationContextObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "AuthenticationContextType")
|
|
}
|
|
}
|
|
|
|
#########################################################################################
|
|
#
|
|
# Authentication Context
|
|
#
|
|
#########################################################################################
|
|
|
|
# region Authentication Context
|
|
class NamedLocationType : IntunePolicyTypeBase
|
|
{
|
|
NamedLocationType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "ConditionalAccessGroup")
|
|
$this._PolicyName = "Named Locations"
|
|
$this._ID = "NamedLocations"
|
|
$this._HasPlatform = $false
|
|
$this._API = "identity/conditionalAccess/namedLocations"
|
|
# Entra object - no roleScopeTagIds in the Graph schema.
|
|
$this._ScopeTagProperty = ""
|
|
$this._ImportOrder = 50
|
|
$this._Permissions = @("Policy.ReadWrite.ConditionalAccess")
|
|
$this._ObjectClass = "NamedLocationObject"
|
|
$this._ExpandAssignmentsList = $false
|
|
$this._SupportsAssignments = $false
|
|
$this._HasPageSizeSupport = $false
|
|
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
}
|
|
}
|
|
|
|
Class NamedLocationObject : IntunePolicyBase
|
|
{
|
|
NamedLocationObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
NamedLocationObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "NamedLocationType")
|
|
}
|
|
}
|
|
|
|
|
|
#########################################################################################
|
|
#
|
|
# Terms of use
|
|
#
|
|
#########################################################################################
|
|
|
|
# region Terms of use
|
|
class TermsOfUseType : IntunePolicyTypeBase
|
|
{
|
|
TermsOfUseType() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Init()
|
|
{
|
|
$this._PolicyGroup = (Get-SingletonObject "ConditionalAccessGroup")
|
|
$this._PolicyName = "Terms of use"
|
|
$this._ID = "TermsOfUse"
|
|
$this._HasPlatform = $false
|
|
$this._HasModified = $false
|
|
# Endpoint rejects a name $filter (verified live 2026-08-27) - searches filter client-side.
|
|
$this._SupportsNameFilter = $false
|
|
$this._API = "identityGovernance/termsOfUse/agreements"
|
|
# Entra object - no roleScopeTagIds in the Graph schema.
|
|
$this._ScopeTagProperty = ""
|
|
$this._ImportOrder = 75
|
|
$this._Expand = "files"
|
|
$this._QueryList = "?`$expand=files"
|
|
$this._Permissions = @("Agreement.ReadWrite.All")
|
|
$this._ObjectClass = "TermsOfUseObject"
|
|
$this._ExpandAssignmentsList = $false
|
|
$this._SupportsAssignments = $false
|
|
|
|
if($null -ne $this._PolicyGroup) {
|
|
$this._PolicyGroup.AddPolicyType($this)
|
|
}
|
|
}
|
|
|
|
[Hashtable]PreImportCommand([PSCustomObject]$PolicyObject)
|
|
{
|
|
$pkgPath = Get-SettingValue "IntuneAppPackagesFolder"
|
|
|
|
if(-not $pkgPath -or [IO.Directory]::Exists($pkgPath) -eq $false)
|
|
{
|
|
Write-Log "Intune app directory is either missing or does not exist" 2
|
|
}
|
|
|
|
# The agreement document is resolved in this order, per localization:
|
|
# 1. fileData.data already on the object - an export carries the PDF
|
|
# inline, fetched per localization by the sub-resource contract.
|
|
# 2. <fileName> next to the exported json (FileInfo), then in the app
|
|
# packages folder - for a json that was exported without the data.
|
|
# 3. The source object, for an in-memory COPY (below).
|
|
# Refusing rather than proceeding matters: an agreement created without
|
|
# its document lists fine but /file, /files and /file/localizations all
|
|
# 404, and a later list with $expand=files returns 500 for the WHOLE
|
|
# collection. Three of those were found in the test tenant on 2026-09-06
|
|
# and had to be deleted by hand.
|
|
#
|
|
# An earlier version of this block required the PDF on disk whenever
|
|
# FileInfo was set and ignored the embedded data. That only held together
|
|
# because Clone() used to drop FileInfo, so an import from disk never
|
|
# reached it with FileInfo populated. Once Clone() kept FileInfo, every
|
|
# import of an export with an inline PDF was refused.
|
|
$hasData = { param($f) ($f.PSObject.Properties['fileData'] -and $f.fileData -and
|
|
$f.fileData.PSObject.Properties['data'] -and $f.fileData.data) }
|
|
|
|
if($PolicyObject.FileInfo) {
|
|
foreach($file in $PolicyObject.Object.Files)
|
|
{
|
|
if(& $hasData $file) { continue }
|
|
|
|
$pdfFile = $null
|
|
if($PolicyObject.FileInfo.Directory.FullName)
|
|
{
|
|
$pdfFile = [IO.Path]::Combine($PolicyObject.FileInfo.Directory.FullName, "$($file.fileName)")
|
|
}
|
|
if(($null -eq $pdfFile -or [IO.File]::Exists($pdfFile) -eq $false) -and $pkgPath)
|
|
{
|
|
$pdfFile = [IO.Path]::Combine($pkgPath, "$($file.fileName)")
|
|
}
|
|
if($pdfFile -and [IO.File]::Exists($pdfFile))
|
|
{
|
|
Write-Log "Add file data: $pdfFile"
|
|
$bytes = [IO.File]::ReadAllBytes($pdfFile)
|
|
$file | Add-Member -MemberType NoteProperty -Name 'fileData' -Value ([PSCustomObject]@{ data = [Convert]::ToBase64String($bytes) }) -Force
|
|
}
|
|
else
|
|
{
|
|
Write-Log "Terms of use file $($file.fileName) not found next to the export or in the app packages folder" 2
|
|
}
|
|
}
|
|
}
|
|
|
|
$touFiles = @($PolicyObject.Object.Files)
|
|
$touMissing = @($touFiles | Where-Object { -not (& $hasData $_) })
|
|
|
|
if($touFiles.Count -gt 0 -and $touMissing.Count -gt 0)
|
|
{
|
|
$touSource = $PolicyObject._ClonedFromObject
|
|
if($touSource -and $touSource.Id)
|
|
{
|
|
Write-Log "Terms of use '$($PolicyObject.Name)': agreement file(s) not loaded - fetching them from the source object"
|
|
$touTokenId = 0
|
|
if($touSource.PSObject.Properties['_TokenId'] -and $null -ne $touSource._TokenId) {
|
|
$touTokenId = [int]$touSource._TokenId
|
|
}
|
|
elseif($PolicyObject.PSObject.Properties['_TokenId'] -and $null -ne $PolicyObject._TokenId) {
|
|
$touTokenId = [int]$PolicyObject._TokenId
|
|
}
|
|
try { Invoke-PolicySubresourceFetch -Policies @($touSource) -TokenId $touTokenId | Out-Null }
|
|
catch { Write-LogError "Failed to fetch terms of use file data from the source object" $_.Exception }
|
|
|
|
foreach($touFile in $touMissing)
|
|
{
|
|
$srcFile = @($touSource.Object.Files) | Where-Object { $_.id -eq $touFile.id } | Select-Object -First 1
|
|
if($srcFile -and $srcFile.PSObject.Properties['fileData'] -and $srcFile.fileData -and $srcFile.fileData.data)
|
|
{
|
|
$touFile | Add-Member -MemberType NoteProperty -Name 'fileData' -Value ([PSCustomObject]@{ data = $srcFile.fileData.data }) -Force
|
|
}
|
|
}
|
|
|
|
$touMissing = @($touFiles | Where-Object { -not (& $hasData $_) })
|
|
}
|
|
}
|
|
|
|
if($touFiles.Count -eq 0 -or $touMissing.Count -gt 0)
|
|
{
|
|
Write-Log "Terms of use '$($PolicyObject.Name)': the agreement document is missing and could not be loaded from the source. The object will not be imported - creating it would leave an agreement with no document, which breaks the whole Terms of Use list." 2
|
|
return @{"Import" = $false}
|
|
}
|
|
return $null
|
|
}
|
|
|
|
PostExportCommand([PSCustomObject]$PolicyObject, [String]$PathToFile)
|
|
{
|
|
if(-not $PathToFile) { return }
|
|
$fi = [IO.FileInfo]$PathToFile
|
|
# File binary was fetched into fileData.data by TermsOfUseObject's
|
|
# sub-resource contract during hydration. Write each to disk; no re-fetch.
|
|
foreach($file in @($PolicyObject.Object.Files))
|
|
{
|
|
$data = $null
|
|
if($file.PSObject.Properties['fileData'] -and $file.fileData -and $file.fileData.PSObject.Properties['data']) {
|
|
$data = $file.fileData.data
|
|
}
|
|
if($data)
|
|
{
|
|
Write-Log "Save file $($file.FileName)"
|
|
$fileName = [IO.Path]::Combine($fi.DirectoryName, "$($file.FileName)")
|
|
[IO.File]::WriteAllBytes($fileName, [System.Convert]::FromBase64String($data))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Class TermsOfUseObject : IntunePolicyBase
|
|
{
|
|
# Maps each in-flight file-data request key back to its file object so
|
|
# ApplySubResourceBatchResult can attach the fetched binary.
|
|
Hidden [Hashtable]$_SubResourceState = $null
|
|
|
|
TermsOfUseObject([PSCustomObject]$JsonObj) : Base($JsonObj) { $this.Init() }
|
|
|
|
TermsOfUseObject() : Base()
|
|
{
|
|
$this.Init()
|
|
}
|
|
|
|
Hidden Init()
|
|
{
|
|
$this._PolicyType = (Get-SingletonObject "TermsOfUseType")
|
|
|
|
# Agreement file binaries aren't in the body; fetch each localization's
|
|
# fileData via the sub-resource contract.
|
|
$this._HasSubResourceBatch = $true
|
|
}
|
|
|
|
# The agreements/<id>/file/localizations('<fid>')/fileData/data API lives
|
|
# ONLY here — was previously duplicated in Sync-BulkExportTermsOfUseFiles
|
|
# (Internal/PolicyHydrateExtras.ps1) and TermsOfUseType.PostExportCommand.
|
|
[PSCustomObject[]] GetSubResourceBatchRequests([int]$Phase)
|
|
{
|
|
if($Phase -ne 1) { return @() }
|
|
$this._SubResourceState = @{}
|
|
$reqs = [System.Collections.Generic.List[PSCustomObject]]::new()
|
|
foreach($file in @($this.Object.Files)) {
|
|
if(-not $file.id) { continue }
|
|
$existing = $null
|
|
if($file.PSObject.Properties['fileData'] -and $file.fileData -and $file.fileData.PSObject.Properties['data']) {
|
|
$existing = $file.fileData.data
|
|
}
|
|
if($existing) { continue }
|
|
$key = "toufile_$($file.id)"
|
|
$this._SubResourceState[$key] = $file
|
|
[void]$reqs.Add([PSCustomObject]@{
|
|
Key = $key
|
|
Url = "agreements/$($this.Id)/file/localizations('$($file.id)')/fileData/data"
|
|
})
|
|
}
|
|
return $reqs.ToArray()
|
|
}
|
|
|
|
[PSCustomObject[]] ApplySubResourceBatchResult([int]$Phase, [string]$Key, $Body)
|
|
{
|
|
if($Phase -ne 1 -or $null -eq $this._SubResourceState -or -not $this._SubResourceState.ContainsKey($Key)) { return @() }
|
|
$file = $this._SubResourceState[$Key]
|
|
|
|
$data = $null
|
|
if($Body -is [string]) { $data = $Body }
|
|
elseif($Body -and $Body.PSObject.Properties['value']) { $data = $Body.value }
|
|
elseif($Body -and $Body.PSObject.Properties['data']) { $data = $Body.data }
|
|
|
|
if($data) {
|
|
if($file.PSObject.Properties['fileData'] -and $file.fileData) {
|
|
if($file.fileData.PSObject.Properties['data']) { $file.fileData.data = $data }
|
|
else { $file.fileData | Add-Member -MemberType NoteProperty -Name 'data' -Value $data -Force }
|
|
}
|
|
else {
|
|
Add-Member -InputObject $file -MemberType NoteProperty -Name 'fileData' -Value ([PSCustomObject]@{ data = $data }) -Force
|
|
}
|
|
}
|
|
return @()
|
|
}
|
|
}
|