mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-28 19:05:38 +02:00
IntuneManagement 4.0.0-beta1
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
# Android Managed Store App Configuration documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:1928. Claims
|
||||
# @odata.type='#microsoft.graph.androidManagedStoreAppConfiguration' and
|
||||
# (per the plan's batching) also the legacy androidForWorkMobileAppConfig
|
||||
# variant since both have the same shape.
|
||||
#
|
||||
# Profile applicability translates to one of three workProfile/deviceOwner
|
||||
# variants which becomes the "Profile type" basic-info value.
|
||||
# Outlook ObjectInfo translation deferred until the walker is ported.
|
||||
|
||||
class AppConfigAndroidStoreDocHandler : DocumentationHandlerBase {
|
||||
AppConfigAndroidStoreDocHandler() {
|
||||
$this.ODataTypes = @(
|
||||
'#microsoft.graph.androidManagedStoreAppConfiguration',
|
||||
'#microsoft.graph.androidForWorkMobileAppConfiguration'
|
||||
)
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
# ProfileType maps to a localized "App configuration" suffix
|
||||
$profileString = switch ($obj.profileApplicability) {
|
||||
'default' { Get-LanguageString 'ProfileType.workProfileAndDeviceOwner' }
|
||||
'androidWorkProfile' { Get-LanguageString 'ProfileType.workProfileOnly' }
|
||||
'androidDeviceOwner' { Get-LanguageString 'ProfileType.deviceOwnerOnly' }
|
||||
default { $null }
|
||||
}
|
||||
# Pass profileString as the Profile-type override so Add-BasicDefaultValues
|
||||
# emits one (and only one) Profile type row matching old engine's pattern
|
||||
# at DocumentationCustom.psm1:1955.
|
||||
Add-BasicDefaultValues $PolicyObject $profileString
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
# Targeted apps — resolved to displayNames when catalog available
|
||||
$allApps = Get-CDAllTenantApps
|
||||
$appsList = @()
|
||||
foreach ($id in $obj.targetedMobileApps) {
|
||||
$app = $allApps | Where-Object Id -EQ $id | Select-Object -First 1
|
||||
$appsList += if ($app -and $app.displayName) { $app.displayName } else { $id }
|
||||
}
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.targetedAppLabel') ($appsList -join $Context.ObjectSeparator) 'targetedMobileApps'
|
||||
|
||||
if ($obj.packageId) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.packageId') $obj.packageId 'packageId'
|
||||
}
|
||||
|
||||
# appSupportsOemConfig is the discriminator the portal uses to split OEMConfig
|
||||
# policies into their own blade - surface it so an OEMConfig policy isn't
|
||||
# documented as an ordinary app configuration. NB: TableHeaders.configurationType
|
||||
# renders as "Profile type" and would collide with the row above.
|
||||
if ($obj.appSupportsOemConfig -eq $true) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.policyType') (Get-LanguageString 'ConfigurationTypes.androidForWorkOemConfig') 'appSupportsOemConfig'
|
||||
}
|
||||
|
||||
# connectedAppsEnabled - "Connected apps" toggle. The portal offers
|
||||
# Enabled / Not configured (not Enabled/Disabled).
|
||||
$connKey = if ($obj.connectedAppsEnabled -eq $true) { 'Inputs.enabled' } else { 'Inputs.notConfigured' }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.connectedApps') (Get-LanguageString $connKey) 'connectedAppsEnabled'
|
||||
|
||||
# credentialProviderRoleState - androidAppCredentialProviderRoleState enum
|
||||
# (notConfigured / allowed only). The portal renders the same two options as
|
||||
# the connected-apps toggle: Enabled / Not configured.
|
||||
$credKeys = @{
|
||||
'notConfigured' = 'Inputs.notConfigured'
|
||||
'allowed' = 'Inputs.enabled'
|
||||
}
|
||||
$credRaw = "$($obj.credentialProviderRoleState)"
|
||||
if ($credRaw) {
|
||||
$credKey = $credKeys[$credRaw]
|
||||
$credValue = if ($credKey) { Get-LanguageString $credKey -IgnoreMissing } else { $null }
|
||||
if ([string]::IsNullOrEmpty($credValue)) { $credValue = $credRaw }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.credentialProvider') $credValue 'credentialProviderRoleState'
|
||||
}
|
||||
|
||||
if (-not $obj.payloadJson) { return }
|
||||
|
||||
$payloadData = $null
|
||||
try {
|
||||
$payloadData = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.payloadJson)) | ConvertFrom-Json
|
||||
}
|
||||
catch {
|
||||
Write-LogError 'Failed to parse Android managed-store payloadJson' $_.Exception
|
||||
return
|
||||
}
|
||||
|
||||
# Outlook gets schema-driven translation (port of old DocumentationCustom.psm1:1987-2005).
|
||||
if ($obj.packageId -eq 'com.microsoft.office.outlook') {
|
||||
$hasAccountType = @($payloadData.managedProperty | Where-Object { $_.key -eq 'com.microsoft.outlook.EmailProfile.AccountType' })
|
||||
$outlookSettings = [PSCustomObject]@{ configureEmail = [bool]$hasAccountType }
|
||||
foreach ($mp in @($payloadData.managedProperty)) {
|
||||
$valueProp = $mp.PSObject.Properties | Where-Object Name -Like 'value*' | Select-Object -First 1
|
||||
$outlookSettings | Add-Member -MemberType NoteProperty -Name $mp.key -Value $valueProp.Value -Force
|
||||
}
|
||||
Invoke-DocAppConfigManifest $outlookSettings (Join-Path (Join-Path $script:AppRootFolder 'Config\ObjectInfo') '#AppConfigOutlookDevice.json') $Context
|
||||
}
|
||||
|
||||
# Outlook translation applied above; remaining managedProperty entries
|
||||
# fall through to the additional-settings table.
|
||||
$addedSettings = Get-DocumentedSettings
|
||||
|
||||
# Friendly names / descriptions / enum labels come from the app's own
|
||||
# managed-configuration schema when a tenant is reachable.
|
||||
$schema = Get-CDAndroidAppConfigSchema $obj.packageId
|
||||
|
||||
$additionalSettings = @()
|
||||
$hasDescription = $false
|
||||
foreach ($row in (Expand-AndroidManagedProperties $payloadData.managedProperty '' 0 $schema)) {
|
||||
if ($addedSettings | Where-Object EntityKey -EQ $row.Key) { continue }
|
||||
if ($row.Description) { $hasDescription = $true }
|
||||
|
||||
$additionalSettings += [PSCustomObject]@{
|
||||
Name = $row.Name
|
||||
Key = $row.Key
|
||||
ValueType = $row.ValueType
|
||||
Value = $row.Value
|
||||
Description = $row.Description
|
||||
EntityKey = $row.Key
|
||||
Category = Get-LanguageString 'TACSettings.generalSettings'
|
||||
SubCategory = Get-LanguageString 'SettingDetails.additionalConfiguration'
|
||||
}
|
||||
}
|
||||
if ($additionalSettings.Count -gt 0) {
|
||||
# Keep the raw key visible next to the friendly name, and only add the
|
||||
# description column when the schema actually supplied any.
|
||||
$columns = if ($hasDescription) { @('Name','Key','ValueType','Value','Description') } else { @('Name','Key','ValueType','Value') }
|
||||
Add-CustomTable 'AdditionalSettings' $columns $additionalSettings -Order 110
|
||||
}
|
||||
|
||||
# Permissions table. Portal grid is 4 columns: friendly name, permission
|
||||
# state, raw permission name (prefix stripped) and permission group.
|
||||
$permissions = @()
|
||||
foreach ($p in $obj.permissionActions) {
|
||||
$tail = $p.permission.Split('.')[-1]
|
||||
$permissionStr = if ($tail) {
|
||||
# Language ids drop the underscores (READ_CALENDAR -> readCalendar);
|
||||
# PowerShell member lookup is case-insensitive so the raw upper-case
|
||||
# form resolves too.
|
||||
$lngId = $tail -replace '_',''
|
||||
$resolved = Get-LanguageString "AndroidForWorkAppPermissions.Permissions.$lngId" -IgnoreMissing
|
||||
if ($resolved) { $resolved } else { $tail }
|
||||
} else { $p.permission }
|
||||
|
||||
$actionStr = $p.action
|
||||
$resolvedAction = Get-LanguageString "AndroidForWorkAppPermissions.Action.$($p.action)" -IgnoreMissing
|
||||
if ($resolvedAction) { $actionStr = $resolvedAction }
|
||||
|
||||
$permissions += [PSCustomObject]@{
|
||||
Permission = $permissionStr
|
||||
PermissionState = $actionStr
|
||||
PermissionName = $tail
|
||||
PermissionGroup = Get-AndroidPermissionGroup $tail
|
||||
EntityKey = $p.permission
|
||||
}
|
||||
}
|
||||
if ($permissions.Count -gt 0) {
|
||||
Add-CustomTable 'Permissions' @('Permission','PermissionState','PermissionName','PermissionGroup') $permissions -Order 115 -LanguageId 'AndroidForWorkAppPermissions.permissionsTitle'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Managed-configuration schema for one Managed Google Play app. The portal uses
|
||||
# this to show friendly names instead of raw keys, a description column, the real
|
||||
# data type (choice/multiselect/bundle...) and enum labels via `selections`.
|
||||
#
|
||||
# GET /deviceManagement/androidManagedStoreAppConfigurationSchemas('app:<packageId>')
|
||||
#
|
||||
# Returns a hashtable keyed by schemaItemKey. `nestedSchemaItems` carries the
|
||||
# members of bundles/bundle arrays (linked to their parent by index/parentIndex),
|
||||
# so nested leaves get friendly names too. Cached per run and per package; offline
|
||||
# / source-unavailable returns an empty map and every caller degrades to raw keys.
|
||||
function Get-CDAndroidAppConfigSchema {
|
||||
param([string]$PackageId)
|
||||
|
||||
if (-not $PackageId) { return @{} }
|
||||
|
||||
$ctx = Get-CurrentDocumentationContext
|
||||
if (-not $ctx.PSObject.Properties['_AndroidAppConfigSchemas']) {
|
||||
$ctx | Add-Member -MemberType NoteProperty -Name '_AndroidAppConfigSchemas' -Value (@{}) -Force
|
||||
}
|
||||
if ($ctx._AndroidAppConfigSchemas.ContainsKey($PackageId)) { return $ctx._AndroidAppConfigSchemas[$PackageId] }
|
||||
|
||||
$map = @{}
|
||||
# The schema is generic app metadata (same on every tenant), so this is gated on
|
||||
# connectivity only - not on SourceTenantUnavailable.
|
||||
if (Test-DocumentationGraphAvailable) {
|
||||
try {
|
||||
$url = "/deviceManagement/androidManagedStoreAppConfigurationSchemas('app:$PackageId')"
|
||||
$resp = Invoke-MSGraphAPI -Url $url -ODataMetadata 'minimal' -NoError
|
||||
foreach ($item in @($resp.schemaItems) + @($resp.nestedSchemaItems)) {
|
||||
if ($item.schemaItemKey -and -not $map.ContainsKey($item.schemaItemKey)) {
|
||||
$map[$item.schemaItemKey] = $item
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Failed to load Android app configuration schema for $PackageId" $_.Exception
|
||||
}
|
||||
}
|
||||
$ctx._AndroidAppConfigSchemas[$PackageId] = $map
|
||||
return $map
|
||||
}
|
||||
|
||||
# Android permission -> permission group. Groups are Android platform constants
|
||||
# (not localized - the portal renders them verbatim in its 4th grid column).
|
||||
$script:_androidPermissionGroups = @{
|
||||
'READ_CALENDAR' = 'CALENDAR'; 'WRITE_CALENDAR' = 'CALENDAR'
|
||||
'CAMERA' = 'CAMERA'
|
||||
'READ_CONTACTS' = 'CONTACTS'; 'WRITE_CONTACTS' = 'CONTACTS'; 'GET_ACCOUNTS' = 'CONTACTS'
|
||||
'ACCESS_FINE_LOCATION' = 'LOCATION'; 'ACCESS_COARSE_LOCATION' = 'LOCATION'; 'ACCESS_BACKGROUND_LOCATION' = 'LOCATION'
|
||||
'RECORD_AUDIO' = 'MICROPHONE'
|
||||
'READ_PHONE_STATE' = 'PHONE'; 'CALL_PHONE' = 'PHONE'; 'READ_CALL_LOG' = 'PHONE'; 'WRITE_CALL_LOG' = 'PHONE'
|
||||
'ADD_VOICEMAIL' = 'PHONE'; 'USE_SIP' = 'PHONE'; 'PROCESS_OUTGOING_CALLS' = 'PHONE'
|
||||
'BODY_SENSORS' = 'SENSORS'; 'BODY_SENSORS_BACKGROUND' = 'SENSORS'
|
||||
'SEND_SMS' = 'SMS'; 'RECEIVE_SMS' = 'SMS'; 'READ_SMS' = 'SMS'; 'RECEIVE_WAP_PUSH' = 'SMS'; 'RECEIVE_MMS' = 'SMS'
|
||||
'READ_EXTERNAL_STORAGE' = 'STORAGE'; 'WRITE_EXTERNAL_STORAGE' = 'STORAGE'
|
||||
'POST_NOTIFICATIONS' = 'NOTIFICATIONS'
|
||||
'READ_MEDIA_VIDEO' = 'MEDIA'; 'READ_MEDIA_IMAGES' = 'MEDIA'; 'READ_MEDIA_AUDIO' = 'MEDIA'
|
||||
'BLUETOOTH_CONNECT' = 'DEVICES'; 'NEARBY_WIFI_DEVICES' = 'DEVICES'; 'NEARBY_DEVICES' = 'DEVICES'
|
||||
}
|
||||
|
||||
function Get-AndroidPermissionGroup {
|
||||
param([string]$PermissionName)
|
||||
if (-not $PermissionName) { return $null }
|
||||
$script:_androidPermissionGroups[$PermissionName]
|
||||
}
|
||||
|
||||
# Flatten a Google managed-configuration `managedProperty` array into one row per
|
||||
# LEAF value. The payload supports six value shapes, two of which nest without
|
||||
# bound (portal JSON-editor schema: valueBool / valueInteger / valueString /
|
||||
# valueStringArray / valueBundle / valueBundleArray):
|
||||
#
|
||||
# valueBundle -> { managedProperty: [ ... ] } rendered as "parent.child"
|
||||
# valueBundleArray -> [ { managedProperty: [...] }, ... ] rendered as "parent[0].child"
|
||||
#
|
||||
# Without this, a bundle rendered as the PowerShell object stringification and a
|
||||
# bundle array rendered as a bare "," (the -join of an array of objects).
|
||||
function Expand-AndroidManagedProperties {
|
||||
param($ManagedProperties, [string]$Prefix = '', [int]$Depth = 0, $Schema = @{})
|
||||
|
||||
if ($Depth -gt 10) {
|
||||
Write-Log 'Android app config: managedProperty nesting deeper than 10 levels; remaining levels not documented' 2
|
||||
return
|
||||
}
|
||||
|
||||
$ctx = Get-CurrentDocumentationContext
|
||||
|
||||
foreach ($mp in @($ManagedProperties)) {
|
||||
if (-not $mp) { continue }
|
||||
$key = if ($Prefix) { "$Prefix$($mp.key)" } else { [string]$mp.key }
|
||||
|
||||
$valueProp = $mp.PSObject.Properties | Where-Object Name -Like 'value*' | Select-Object -First 1
|
||||
if (-not $valueProp) { continue }
|
||||
|
||||
# Schema is keyed by the app's own schemaItemKey, not by our dotted path
|
||||
$schemaItem = $Schema[[string]$mp.key]
|
||||
|
||||
switch ($valueProp.Name) {
|
||||
'valueBundle' {
|
||||
Expand-AndroidManagedProperties $valueProp.Value.managedProperty "$key." ($Depth + 1) $Schema
|
||||
}
|
||||
'valueBundleArray' {
|
||||
$idx = 0
|
||||
foreach ($bundle in @($valueProp.Value)) {
|
||||
Expand-AndroidManagedProperties $bundle.managedProperty "$key[$idx]." ($Depth + 1) $Schema
|
||||
$idx++
|
||||
}
|
||||
}
|
||||
default {
|
||||
$val = $valueProp.Value
|
||||
# choice / multiselect store the selection VALUE; the schema carries
|
||||
# the friendly name for each in `selections`
|
||||
if ($schemaItem.selections) {
|
||||
$val = @($val | ForEach-Object {
|
||||
$raw = $_
|
||||
$sel = $schemaItem.selections | Where-Object { "$($_.value)" -eq "$raw" } | Select-Object -First 1
|
||||
if ($sel.name) { $sel.name } else { $raw }
|
||||
})
|
||||
}
|
||||
if ($val -is [array]) { $val = $val -join $ctx.ObjectSeparator }
|
||||
|
||||
[PSCustomObject]@{
|
||||
Key = $key
|
||||
Name = ?? $schemaItem.displayName $key
|
||||
ValueType = if ($schemaItem.dataType) { $schemaItem.dataType } else { $valueProp.Name.Substring(5) }
|
||||
Value = $val
|
||||
Description = $schemaItem.description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([AppConfigAndroidStoreDocHandler]::new())
|
||||
@@ -0,0 +1,192 @@
|
||||
# iOS Mobile App Configuration documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:2065. Claims
|
||||
# @odata.type='#microsoft.graph.iosMobileAppConfiguration'.
|
||||
#
|
||||
# Two main paths:
|
||||
# 1. iOS plist (base64'd encodedSettingXml) — parsed offline, key/value/type
|
||||
# rows emitted directly
|
||||
# 2. settings collection (Outlook-specific or generic appConfig key/value)
|
||||
# The Outlook ObjectInfo translation needs the ObjectInfo JSON walker
|
||||
# (deferred); offline we fall through to raw key=value rows under the
|
||||
# generic "Additional configuration" subcategory.
|
||||
|
||||
class AppConfigMobileAppDocHandler : DocumentationHandlerBase {
|
||||
AppConfigMobileAppDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.iosMobileAppConfiguration')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'SettingDetails.appConfiguration') '@odata.type'
|
||||
Add-BasicPropertyValue (Get-LanguageString 'Inputs.enrollmentTypeLabel') (Get-LanguageString 'EnrollmentType.devicesWithEnrollment') 'enrollmentType'
|
||||
|
||||
$platformId = Get-ObjectPlatformFromType $obj
|
||||
if ($platformId) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString "Platform.$platformId") 'platform'
|
||||
}
|
||||
|
||||
# Targeted apps — resolve IDs to displayNames when the tenant catalog is
|
||||
# available, otherwise emit raw IDs.
|
||||
$allApps = Get-CDAllTenantApps
|
||||
$appsList = @()
|
||||
foreach ($id in $obj.targetedMobileApps) {
|
||||
$app = $allApps | Where-Object Id -EQ $id | Select-Object -First 1
|
||||
$appsList += if ($app -and $app.displayName) { $app.displayName } else { $id }
|
||||
}
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.targetedAppLabel') ($appsList -join $Context.ObjectSeparator) 'targetedMobileApps'
|
||||
|
||||
$category = Get-LanguageString 'TableHeaders.settings'
|
||||
|
||||
if ($obj.encodedSettingXml) {
|
||||
# iOS plist. The portal emits a bare <dict> root but Graph also accepts a
|
||||
# <plist> wrapper, and the portal's validator explicitly allows nested
|
||||
# <dict>/<array> values - so the walk has to recurse.
|
||||
$xml = $null
|
||||
try {
|
||||
$xml = [xml]([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.encodedSettingXml)))
|
||||
}
|
||||
catch {
|
||||
Write-LogError 'Failed to convert iOS encodedSettingXml to XML' $_.Exception
|
||||
return
|
||||
}
|
||||
|
||||
$rootDict = if ($xml.dict) { $xml.dict } elseif ($xml.plist.dict) { $xml.plist.dict } else { $null }
|
||||
if (-not $rootDict) {
|
||||
Write-Log 'iOS app config: encodedSettingXml has no <dict> root; no settings documented' 2
|
||||
return
|
||||
}
|
||||
|
||||
$plistRows = @(Expand-IosPlistDictionary $rootDict)
|
||||
|
||||
# ValueType is not part of the default documentation properties, so also
|
||||
# emit the portal's 3-column grid (key / value type / value).
|
||||
if ($plistRows.Count -gt 0) {
|
||||
$typeRows = foreach ($row in $plistRows) {
|
||||
[PSCustomObject]@{
|
||||
ConfigurationKey = $row.Key
|
||||
ValueType = Get-AppConfigValueTypeName $row.ValueType
|
||||
ConfigurationValue = $row.Value
|
||||
EntityKey = $row.Key
|
||||
}
|
||||
}
|
||||
Add-CustomTable 'AppConfigSettings' @('ConfigurationKey','ValueType','ConfigurationValue') $typeRows -Order 110 -LanguageId 'TableHeaders.settings'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
# Outlook gets schema-driven translation (port of old DocumentationCustom.psm1:2141-2176).
|
||||
$isOutlook = $false
|
||||
foreach ($id in $obj.targetedMobileApps) {
|
||||
$app = $allApps | Where-Object Id -EQ $id | Select-Object -First 1
|
||||
if ($app.displayName -eq 'Microsoft Outlook') { $isOutlook = $true; break }
|
||||
}
|
||||
if (-not $isOutlook -and @($obj.settings | Where-Object { $_.appConfigKey -like 'com.microsoft.outlook*' })) { $isOutlook = $true }
|
||||
if ($isOutlook) {
|
||||
$hasAccountType = @($obj.settings | Where-Object { $_.appConfigKey -eq 'com.microsoft.outlook.EmailProfile.AccountType' })
|
||||
$outlookSettings = [PSCustomObject]@{ configureEmail = [bool]$hasAccountType }
|
||||
foreach ($setting in @($obj.settings)) {
|
||||
$val = if ($setting.appConfigKeyType -eq 'booleanType') { $setting.appConfigKeyValue -eq 'true' } else { $setting.appConfigKeyValue }
|
||||
$outlookSettings | Add-Member -MemberType NoteProperty -Name $setting.appConfigKey -Value $val -Force
|
||||
}
|
||||
Invoke-DocAppConfigManifest $outlookSettings (Join-Path (Join-Path $script:AppRootFolder 'Config\ObjectInfo') '#AppConfigOutlookDevice.json') $Context
|
||||
}
|
||||
|
||||
# Remaining settings fall through to raw key=value rows under the
|
||||
# "Additional configuration" subcategory.
|
||||
$addedSettings = Get-DocumentedSettings
|
||||
$languageTitleId = 'TableHeaders.settings'
|
||||
|
||||
$typeRows = @()
|
||||
foreach ($setting in $obj.settings) {
|
||||
if ($addedSettings | Where-Object EntityKey -EQ $setting.appConfigKey) {
|
||||
$languageTitleId = 'SettingDetails.additionalConfiguration'
|
||||
continue
|
||||
}
|
||||
|
||||
# The portal grid shows the value TYPE as its own column; keep that
|
||||
# (a tokenType value like {{userprincipalname}} is not a literal string).
|
||||
$typeRows += [PSCustomObject]@{
|
||||
ConfigurationKey = $setting.appConfigKey
|
||||
ValueType = Get-AppConfigValueTypeName $setting.appConfigKeyType
|
||||
ConfigurationValue = $setting.appConfigKeyValue
|
||||
EntityKey = $setting.appConfigKey
|
||||
}
|
||||
}
|
||||
if ($typeRows.Count -gt 0) {
|
||||
Add-CustomTable 'AppConfigSettings' @('ConfigurationKey','ValueType','ConfigurationValue') $typeRows -Order 110 -LanguageId $languageTitleId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Localized name for an app-config value type. Accepts both the Graph
|
||||
# mdmAppConfigKeyType members (stringType/integerType/realType/booleanType/
|
||||
# tokenType) and the bare plist element names (string/integer/real/boolean/...).
|
||||
# tokenType has no language string - the portal shows it blank, so the raw value
|
||||
# is a strict improvement.
|
||||
function Get-AppConfigValueTypeName {
|
||||
param([string]$ValueType)
|
||||
|
||||
if (-not $ValueType) { return $null }
|
||||
$key = switch -Regex ($ValueType) {
|
||||
'^string' { 'SettingDetails.string' }
|
||||
'^integer' { 'SettingDetails.integer' }
|
||||
'^real' { 'SettingDetails.real' }
|
||||
'^boolean' { 'SettingDetails.boolean' }
|
||||
default { $null }
|
||||
}
|
||||
if (-not $key) { return $ValueType }
|
||||
$value = Get-LanguageString $key -IgnoreMissing
|
||||
if ([string]::IsNullOrEmpty($value)) { $ValueType } else { $value }
|
||||
}
|
||||
|
||||
# Flatten an iOS plist <dict> into one row per LEAF value. Nested containers are
|
||||
# addressed the way the plist itself addresses them:
|
||||
# <dict> -> "parent.child"
|
||||
# <array> -> "parent[0]"
|
||||
# Without this, a nested container produced an EMPTY value (.'#text' on an element
|
||||
# with element children returns nothing) and the payload was silently lost.
|
||||
function Expand-IosPlistDictionary {
|
||||
param($DictNode, [string]$Prefix = '', [int]$Depth = 0)
|
||||
|
||||
if ($Depth -gt 10) {
|
||||
Write-Log 'iOS app config: plist nesting deeper than 10 levels; remaining levels not documented' 2
|
||||
return
|
||||
}
|
||||
|
||||
$children = @($DictNode.ChildNodes)
|
||||
for ($i = 0; $i -lt $children.Count; $i++) {
|
||||
if ($children[$i].Name -ne 'key') { continue }
|
||||
$name = $children[$i].'#text'
|
||||
$i++
|
||||
if ($i -ge $children.Count) { break }
|
||||
$valueNode = $children[$i]
|
||||
$key = if ($Prefix) { "$Prefix$name" } else { [string]$name }
|
||||
|
||||
switch ($valueNode.Name) {
|
||||
'true' { [PSCustomObject]@{ Key = $key; ValueType = 'boolean'; Value = 'true' } }
|
||||
'false' { [PSCustomObject]@{ Key = $key; ValueType = 'boolean'; Value = 'false' } }
|
||||
'dict' { Expand-IosPlistDictionary $valueNode "$key." ($Depth + 1) }
|
||||
'array' {
|
||||
$idx = 0
|
||||
foreach ($item in @($valueNode.ChildNodes)) {
|
||||
$itemKey = "$key[$idx]"
|
||||
if ($item.Name -eq 'dict') { Expand-IosPlistDictionary $item "$itemKey." ($Depth + 1) }
|
||||
elseif ($item.Name -eq 'true' -or $item.Name -eq 'false') {
|
||||
[PSCustomObject]@{ Key = $itemKey; ValueType = 'boolean'; Value = $item.Name }
|
||||
}
|
||||
else {
|
||||
[PSCustomObject]@{ Key = $itemKey; ValueType = $item.Name; Value = $item.'#text' }
|
||||
}
|
||||
$idx++
|
||||
}
|
||||
}
|
||||
default { [PSCustomObject]@{ Key = $key; ValueType = $valueNode.Name; Value = $valueNode.'#text' } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([AppConfigMobileAppDocHandler]::new())
|
||||
@@ -0,0 +1,48 @@
|
||||
# Assignment Filter documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:3823 (Invoke-CDDocument-
|
||||
# AssignmentFilter, ~35 LOC). Claims @odata.type='#microsoft.graph.deviceAnd
|
||||
# AppManagementAssignmentFilter' and produces a 7-row BasicInfo + 1-row
|
||||
# Settings table (the rule syntax).
|
||||
#
|
||||
# Platform value: app-management platforms (androidMobileApplicationManagement
|
||||
# etc.) resolve to empty strings in Strings-en.json which Get-LanguageString
|
||||
# returns as $null — so BasicInfo emits the row with Value=null, matching the
|
||||
# golden's `"Platform": null` for app filters.
|
||||
|
||||
class AssignmentFilterDocHandler : DocumentationHandlerBase {
|
||||
AssignmentFilterDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.deviceAndAppManagementAssignmentFilter')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
# BasicInfo order: Name, Description, Created, Last modified, Profile type, Platform
|
||||
# (Scope tags appended automatically by the engine's post-step.)
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Filters.filters') '@odata.type'
|
||||
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString "Platform.$($obj.platform)") 'platform'
|
||||
|
||||
# Filter scope: devices vs apps. Disambiguates app-management filters, whose
|
||||
# platform row resolves to null (see header note).
|
||||
$mgmtType = switch ($obj.assignmentFilterManagementType) {
|
||||
'devices' { Get-LanguageString 'Titles.devices' }
|
||||
'apps' { Get-LanguageString 'Titles.apps' }
|
||||
default { $obj.assignmentFilterManagementType }
|
||||
}
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.managementType') $mgmtType 'assignmentFilterManagementType'
|
||||
|
||||
# Settings: a single Rule syntax row
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'Filters.ruleSyntax'
|
||||
Value = $obj.rule
|
||||
EntityKey = 'rule'
|
||||
Category = Get-LanguageString 'SettingDetails.rules'
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([AssignmentFilterDocHandler]::new())
|
||||
@@ -0,0 +1,44 @@
|
||||
# Authentication Context documentation handler.
|
||||
#
|
||||
# Claims @odata.type='#microsoft.graph.authenticationContextClassReference'. Auth
|
||||
# contexts (c1..c99) were previously only referenced by Conditional Access policies
|
||||
# (ID -> displayName, see ConditionalAccessDocHandler); this handler documents the
|
||||
# standalone object: display name, description and whether it is published to apps
|
||||
# (isAvailable).
|
||||
#
|
||||
# Note: AuthenticationContextType strips @odata.type on export (_PropertiesToRemove),
|
||||
# so this handler matches live documentation runs. File-based runs of an exported
|
||||
# auth context lose the discriminator and fall through to NoProvider - a pre-existing
|
||||
# export-cleanup limitation, not addressed here.
|
||||
|
||||
class AuthenticationContextDocHandler : DocumentationHandlerBase {
|
||||
AuthenticationContextDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.authenticationContextClassReference')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
# No plain-noun language string exists for the auth-context type, so use the
|
||||
# PolicyType title for the Profile type row.
|
||||
$nameValue = if ($obj.displayName) { $obj.displayName } else { $PolicyObject.Name }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $nameValue 'displayName'
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') $PolicyObject.PolicyType.Title '@odata.type'
|
||||
if ($obj.description) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $obj.description 'description'
|
||||
}
|
||||
|
||||
if ($null -ne $obj.isAvailable) {
|
||||
$availKey = if ($obj.isAvailable -eq $true) { 'Inputs.enabled' } else { 'Inputs.disabled' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'AzureCA.AuthContext.InfoBlade.publishLabel')
|
||||
Value = (Get-LanguageString $availKey)
|
||||
Category = $null
|
||||
SubCategory = $null
|
||||
EntityKey = 'isAvailable'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([AuthenticationContextDocHandler]::new())
|
||||
@@ -0,0 +1,76 @@
|
||||
# Authentication Strength documentation handler.
|
||||
#
|
||||
# Claims @odata.type='#microsoft.graph.authenticationStrengthPolicy'. Authentication
|
||||
# strengths were previously only referenced as a Conditional Access grant control
|
||||
# (ID -> displayName, see ConditionalAccessDocHandler); this handler documents the
|
||||
# standalone policy object.
|
||||
#
|
||||
# The policy's substance is allowedCombinations: an OR-list of method combinations,
|
||||
# where each combination is an AND-set of authentication methods (comma-joined in
|
||||
# the raw value, e.g. "password,microsoftAuthenticatorPush"). Each method maps to
|
||||
# AzureCA.AuthenticationStrength.Mode.<method>. A few methods (federatedMultiFactor,
|
||||
# federatedSingleFactor) have no Mode string yet, so fall back to a humanised token
|
||||
# rather than emit a raw enum value.
|
||||
|
||||
class AuthenticationStrengthDocHandler : DocumentationHandlerBase {
|
||||
AuthenticationStrengthDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.authenticationStrengthPolicy')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
# BasicInfo: Name + Profile type + Description
|
||||
$nameValue = if ($obj.displayName) { $obj.displayName } else { $PolicyObject.Name }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $nameValue 'displayName'
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'AzureCA.WhatIfBlade.authenticationStrength') '@odata.type'
|
||||
if ($obj.description) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $obj.description 'description'
|
||||
}
|
||||
|
||||
# allowedCombinations: OR-list of AND-combinations. Render each combination
|
||||
# as "Method A + Method B" on its own line.
|
||||
$comboLines = @()
|
||||
foreach ($combo in @($obj.allowedCombinations)) {
|
||||
if ([string]::IsNullOrWhiteSpace($combo)) { continue }
|
||||
$methodNames = @()
|
||||
foreach ($method in ($combo -split ',')) {
|
||||
$m = $method.Trim()
|
||||
if (-not $m) { continue }
|
||||
$methodNames += (Get-AuthenticationMethodLabel $m)
|
||||
}
|
||||
if ($methodNames.Count -gt 0) {
|
||||
$comboLines += ($methodNames -join ' + ')
|
||||
}
|
||||
}
|
||||
|
||||
if ($comboLines.Count -gt 0) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'AzureCA.policyControlAuthenticationStrengthDisplayedName')
|
||||
Value = ($comboLines -join $Context.ObjectSeparator)
|
||||
Category = $null
|
||||
SubCategory = $null
|
||||
EntityKey = 'allowedCombinations'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Map an authentication-method enum token to its localised label, falling back to
|
||||
# a humanised form (federatedMultiFactor -> "Federated Multi Factor") for tokens
|
||||
# that have no Mode string. -IgnoreMissing keeps the log clean for known gaps.
|
||||
function Get-AuthenticationMethodLabel {
|
||||
param([string]$Method)
|
||||
|
||||
if ([string]::IsNullOrEmpty($Method)) { return $Method }
|
||||
|
||||
$label = Get-LanguageString "AzureCA.AuthenticationStrength.Mode.$Method" -IgnoreMissing
|
||||
if (-not [string]::IsNullOrEmpty($label)) { return $label }
|
||||
|
||||
# No Mode string: split camelCase into Title-cased words.
|
||||
$spaced = [regex]::Replace($Method, '(?<=[a-z0-9])(?=[A-Z])', ' ')
|
||||
$ci = [System.Globalization.CultureInfo]::InvariantCulture
|
||||
return (($spaced -split ' ' | Where-Object { $_ } | ForEach-Object { $ci.TextInfo.ToTitleCase($_.ToLower()) }) -join ' ')
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([AuthenticationStrengthDocHandler]::new())
|
||||
@@ -0,0 +1,53 @@
|
||||
# Co-Management Settings documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:3862. Hardcoded
|
||||
# Platform = Windows 10 (Co-Management is Windows-only).
|
||||
|
||||
class CoManagementDocHandler : DocumentationHandlerBase {
|
||||
CoManagementDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.deviceComanagementAuthorityConfiguration')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') ((Get-LanguageString 'WindowsEnrollment.coManagementAuthorityTitle').Trim()) '@odata.type'
|
||||
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString 'Platform.Windows10') 'platform'
|
||||
|
||||
$category = Get-LanguageString 'TableHeaders.settings'
|
||||
$yes = Get-LanguageString 'BooleanActions.yes'
|
||||
$no = Get-LanguageString 'SettingDetails.no'
|
||||
|
||||
$installValue = if ($obj.installConfigurationManagerAgent -eq $true) { $yes } else { $no }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'CoManagementAuthority.installAgent')
|
||||
Value = $installValue
|
||||
EntityKey = 'installConfigurationManagerAgent'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
if ($obj.installConfigurationManagerAgent -eq $true) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'CoManagementAuthority.commandLineArgs')
|
||||
Value = $obj.configurationManagerAgentCommandLineArgument
|
||||
EntityKey = 'configurationManagerAgentCommandLineArgument'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
|
||||
$ownershipValue = if ($obj.managedDeviceAuthority -eq 1) { $yes } else { $no }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'CoManagementAuthority.managedDeviceOwnership')
|
||||
Value = $ownershipValue
|
||||
EntityKey = 'managedDeviceAuthority'
|
||||
Category = $category
|
||||
SubCategory = (Get-LanguageString 'CoManagementAuthority.advancedProperty')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([CoManagementDocHandler]::new())
|
||||
@@ -0,0 +1,73 @@
|
||||
# Custom compliance script documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:4788
|
||||
# (Invoke-CDDocumentDeviceComplianceScript). Claims
|
||||
# @odata.type='#microsoft.graph.deviceComplianceScript'.
|
||||
#
|
||||
# deviceComplianceScript has no ObjectCategories entry, so - like the Scope
|
||||
# Tag handler - basic info rows are emitted manually instead of via
|
||||
# Add-BasicDefaultValues (which would add blank Platform/Profile rows).
|
||||
|
||||
class ComplianceScriptDocHandler : DocumentationHandlerBase {
|
||||
ComplianceScriptDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.deviceComplianceScript')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
if ($PolicyObject.Name) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $PolicyObject.Name 'displayName'
|
||||
}
|
||||
$descValue = if ($obj.description) { $obj.description } else { '' }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $descValue 'description'
|
||||
if ($obj.publisher) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.publisher') $obj.publisher 'publisher'
|
||||
}
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Titles.complianceScriptManagementPreview') 'configurationType'
|
||||
|
||||
$category = Get-LanguageString 'TableHeaders.settings'
|
||||
$valueYes = Get-LanguageString 'BooleanActions.yes'
|
||||
$valueNo = Get-LanguageString 'SettingDetails.no'
|
||||
|
||||
if ($obj.detectionScriptContent -and -not ($Context.Options -and $Context.Options['IncludeScripts'] -eq $false)) {
|
||||
$scriptBody = ''
|
||||
try { $scriptBody = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($obj.detectionScriptContent)) } catch { }
|
||||
if ($scriptBody) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'ProactiveRemediations.Create.Settings.DetectionScriptMultiLineTextBox.label'
|
||||
Value = $scriptBody
|
||||
EntityKey = 'detectionScriptContent'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'WindowsManagement.scriptContextLabel'
|
||||
Value = $(if ($obj.runAsAccount -eq 'system') { $valueNo } else { $valueYes })
|
||||
EntityKey = 'runAsAccount'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'WindowsManagement.enforceSignatureCheckLabel'
|
||||
Value = $(if ($obj.enforceSignatureCheck -eq $false) { $valueNo } else { $valueYes })
|
||||
EntityKey = 'enforceSignatureCheck'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'WindowsManagement.runAs64BitLabel'
|
||||
Value = $(if ($obj.runAs32Bit -eq $true) { $valueNo } else { $valueYes })
|
||||
EntityKey = 'runAs32Bit'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([ComplianceScriptDocHandler]::new())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,113 @@
|
||||
# Custom OMA-URI documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:3519. Emits Name +
|
||||
# Platform basic info, then 4-5 rows per OMA-URI setting (Name, Description,
|
||||
# OMA-URI path, Data type, Value). Encrypted values are skipped offline;
|
||||
# live runs fetch via /deviceConfigurations/.../getOmaSettingPlainTextValue.
|
||||
#
|
||||
# Claims all 4 CustomConfiguration variants in one handler.
|
||||
|
||||
class CustomOMAUriDocHandler : DocumentationHandlerBase {
|
||||
CustomOMAUriDocHandler() {
|
||||
$this.ODataTypes = @(
|
||||
'#microsoft.graph.windows10CustomConfiguration',
|
||||
'#microsoft.graph.androidForWorkCustomConfiguration',
|
||||
'#microsoft.graph.androidWorkProfileCustomConfiguration',
|
||||
'#microsoft.graph.androidCustomConfiguration'
|
||||
)
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
# Note: old code at L3534 has the configurationType BasicPropertyValue
|
||||
# commented out. Faithful port — skipping.
|
||||
|
||||
$platformId = Get-ObjectPlatformFromType $obj
|
||||
if ($platformId) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString "Platform.$platformId") 'platform'
|
||||
}
|
||||
|
||||
$category = Get-LanguageString 'SettingDetails.customPolicyOMAURISettingsName'
|
||||
|
||||
$typeLabelMap = @{
|
||||
'#microsoft.graph.omaSettingString' = 'SettingDetails.stringName'
|
||||
'#microsoft.graph.omaSettingBase64' = 'SettingDetails.base64Name'
|
||||
'#microsoft.graph.omaSettingBoolean' = 'SettingDetails.booleanName'
|
||||
'#microsoft.graph.omaSettingDateTime' = 'SettingDetails.dateTimeName'
|
||||
'#microsoft.graph.omaSettingFloatingPoint' = 'SettingDetails.floatingPointName'
|
||||
'#microsoft.graph.omaSettingInteger' = 'SettingDetails.integerName'
|
||||
'#microsoft.graph.omaSettingStringXml' = 'SettingDetails.stringXMLName'
|
||||
}
|
||||
|
||||
foreach ($setting in $obj.omaSettings) {
|
||||
$sub = $setting.displayName
|
||||
$oma = $setting.omaUri
|
||||
$type = if ($setting.PSObject.Properties['@OData.Type']) { $setting.'@OData.Type' } else { $setting.'@odata.type' }
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'SettingDetails.nameName')
|
||||
Value = $setting.displayName
|
||||
EntityKey = "displayName_$oma"
|
||||
Category = $category; SubCategory = $sub
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'TableHeaders.description')
|
||||
Value = $setting.description
|
||||
EntityKey = "description_$oma"
|
||||
Category = $category; SubCategory = $sub
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'SettingDetails.oMAURIName')
|
||||
Value = $oma
|
||||
EntityKey = "omaUri_$oma"
|
||||
Category = $category; SubCategory = $sub
|
||||
})
|
||||
|
||||
$typeKey = $typeLabelMap[$type]
|
||||
if ($typeKey) {
|
||||
$typeValue = Get-LanguageString $typeKey
|
||||
if ($typeValue) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'SettingDetails.dataTypeName')
|
||||
Value = $typeValue
|
||||
EntityKey = "type_$oma"
|
||||
Category = $category; SubCategory = $sub
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Value row — skip when encrypted unless we can resolve via Graph
|
||||
if ($setting.isEncrypted -ne $true) {
|
||||
$value = $setting.value
|
||||
if ($type -eq '#microsoft.graph.omaSettingStringXml' -and $value) {
|
||||
try { $value = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($value)) } catch { }
|
||||
}
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'SettingDetails.valueName')
|
||||
Value = $value
|
||||
EntityKey = "value_$oma"
|
||||
Category = $category; SubCategory = $sub
|
||||
})
|
||||
}
|
||||
elseif (-not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable) -and $setting.secretReferenceValueId) {
|
||||
try {
|
||||
$url = "/deviceManagement/deviceConfigurations/$($obj.id)/getOmaSettingPlainTextValue(secretReferenceValueId='$($setting.secretReferenceValueId)')"
|
||||
$resp = Invoke-MSGraphAPI -Url $url
|
||||
if ($resp.Value) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'SettingDetails.valueName')
|
||||
Value = $resp.Value
|
||||
EntityKey = "value_$oma"
|
||||
Category = $category; SubCategory = $sub
|
||||
})
|
||||
}
|
||||
}
|
||||
catch { Write-LogError "Failed to resolve encrypted OMA-URI value for $oma" $_.Exception }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([CustomOMAUriDocHandler]::new())
|
||||
@@ -0,0 +1,24 @@
|
||||
# Device Category documentation handler.
|
||||
#
|
||||
# Claims @odata.type='#microsoft.graph.deviceCategory'. Device categories are
|
||||
# name + description only; there is no ObjectCategories entry (so no
|
||||
# Add-BasicDefaultValues - it would emit blank Platform/Profile rows) and no
|
||||
# old-project documenter to port.
|
||||
|
||||
class DeviceCategoryDocHandler : DocumentationHandlerBase {
|
||||
DeviceCategoryDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.deviceCategory')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
if ($PolicyObject.Name) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $PolicyObject.Name 'displayName'
|
||||
}
|
||||
$descValue = if ($obj.description) { $obj.description } else { '' }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $descValue 'description'
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([DeviceCategoryDocHandler]::new())
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
# Device Enrollment Platform Restriction documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:4345. Claims the two
|
||||
# platform-restriction @odata.types:
|
||||
# ...deviceEnrollmentPlatformRestrictionConfiguration (single platform)
|
||||
# ...deviceEnrollmentPlatformRestrictionsConfiguration (all platforms — the
|
||||
# aggregate that emits
|
||||
# one row block per
|
||||
# platform sub-restriction)
|
||||
#
|
||||
# Doesn't handle deviceEnrollmentLimitConfiguration — that has a different
|
||||
# shape (single "Device limit" setting) and lives behind the generic Profile
|
||||
# input provider in the old engine.
|
||||
|
||||
class EnrollmentPlatformRestrictionDocHandler : DocumentationHandlerBase {
|
||||
EnrollmentPlatformRestrictionDocHandler() {
|
||||
$this.ODataTypes = @(
|
||||
'#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration',
|
||||
'#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration'
|
||||
)
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Titles.deviceTypeEnrollmentRestrictions') '@odata.type'
|
||||
|
||||
# platformType (single variant) -> Platform.* language id
|
||||
$singlePlatformLngId = switch ($obj.platformType) {
|
||||
'androidForWork' { 'androidWorkProfile' }
|
||||
'mac' { 'macOS' }
|
||||
'ios' { 'iOS' }
|
||||
'android' { 'android' }
|
||||
'windows' { 'windows' }
|
||||
'tvos' { 'tvOS' }
|
||||
'visionOS' { 'visionOS' }
|
||||
default { $obj.platformType }
|
||||
}
|
||||
|
||||
$isAggregate = $obj.'@odata.type' -eq '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration'
|
||||
if ($isAggregate) {
|
||||
# The default "All users and all devices" config carries one sub-restriction
|
||||
# per platform. Graph exposes 10, including the legacy macRestriction (a dupe
|
||||
# of the version-capable macOSRestriction) and the deprecated
|
||||
# windowsMobileRestriction. Render the current platforms; prefer
|
||||
# macOSRestriction over macRestriction. $platformMap: property -> name key.
|
||||
$platform = Get-LanguageString 'AzureCA.classicPolicyAllPlatforms'
|
||||
$platformMap = [ordered]@{
|
||||
'androidForWorkRestriction' = 'Platform.androidWorkProfile'
|
||||
'androidRestriction' = 'Platform.android'
|
||||
'iosRestriction' = 'Platform.iOS'
|
||||
'macOSRestriction' = 'Platform.macOS'
|
||||
'tvosRestriction' = 'Platform.tvOS'
|
||||
'visionOSRestriction' = 'Platform.visionOS'
|
||||
'windowsRestriction' = 'Platform.windows'
|
||||
'windowsHomeSkuRestriction' = 'Devices.windowsHomeSku'
|
||||
}
|
||||
}
|
||||
else {
|
||||
$platform = Get-LanguageString "Platform.$singlePlatformLngId"
|
||||
$platformMap = [ordered]@{ 'platformRestriction' = "Platform.$singlePlatformLngId" }
|
||||
}
|
||||
|
||||
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') $platform 'platformType'
|
||||
|
||||
$allowStr = Get-LanguageString 'BooleanActions.allow'
|
||||
$blockStr = Get-LanguageString 'BooleanActions.block'
|
||||
$category = Get-LanguageString 'EnrollmentRestrictions.DeviceType.platformSettings'
|
||||
$cantRestrictStr = Get-LanguageString 'EnrollmentRestrictions.DeviceType.cannotRestrict'
|
||||
|
||||
foreach ($prop in $platformMap.Keys) {
|
||||
$restrict = $obj.$prop
|
||||
if (-not $restrict) { continue }
|
||||
|
||||
$nameKey = $platformMap[$prop]
|
||||
$typeStr = Get-LanguageString $nameKey
|
||||
|
||||
# OS version range, blank when unset. macOSRestriction is version-capable,
|
||||
# so (unlike the legacy macRestriction the old handler forced to "cannot
|
||||
# restrict") every platform now reports its actual osMin/osMax range.
|
||||
$version = if ($restrict.osMinimumVersion -or $restrict.osMaximumVersion) {
|
||||
"$($restrict.osMinimumVersion)-$($restrict.osMaximumVersion)"
|
||||
} else { '' }
|
||||
|
||||
# Manufacturer blocking: old code has a typo (`'andriod'` instead of
|
||||
# `'android'`) which means only 'androidWorkProfile' actually emits
|
||||
# the blockedManufacturers list. Everything else — including the
|
||||
# correctly-spelled 'android' (device administrator) — falls
|
||||
# through to "Restriction not supported". Preserving the behavior
|
||||
# because golden fixtures match it; the typo is a known wart in
|
||||
# old code that fixing would silently change output.
|
||||
$blockedManufacturers = if ($nameKey -eq 'Platform.androidWorkProfile') {
|
||||
@($restrict.blockedManufacturers) -join $Context.PropertySeparator
|
||||
} else {
|
||||
$cantRestrictStr
|
||||
}
|
||||
|
||||
# Aggregate variant uses platform name as SubCategory; single variant uses $null
|
||||
$subCategory = if ($isAggregate) { $typeStr } else { $null }
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.type')
|
||||
Value = $typeStr
|
||||
EntityKey = 'platformType'
|
||||
Category = $category; SubCategory = $subCategory
|
||||
})
|
||||
|
||||
$platformAccess = if ($restrict.platformBlocked) { $blockStr } else { $allowStr }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.platform')
|
||||
Value = $platformAccess
|
||||
EntityKey = 'platformBlocked'
|
||||
Category = $category; SubCategory = $subCategory
|
||||
})
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.versions')
|
||||
Value = $version
|
||||
EntityKey = 'versions'
|
||||
Category = $category; SubCategory = $subCategory
|
||||
})
|
||||
|
||||
$personalAccess = if ($restrict.personalDeviceEnrollmentBlocked) { $blockStr } else { $allowStr }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.personal')
|
||||
Value = $personalAccess
|
||||
EntityKey = 'personalDeviceEnrollmentBlocked'
|
||||
Category = $category; SubCategory = $subCategory
|
||||
})
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'EnrollmentRestrictions.DeviceType.deviceManufacturer')
|
||||
Value = $blockedManufacturers
|
||||
EntityKey = 'blockedManufacturers'
|
||||
Category = $category; SubCategory = $subCategory
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([EnrollmentPlatformRestrictionDocHandler]::new())
|
||||
@@ -0,0 +1,206 @@
|
||||
# Managed App Configuration documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:2196. Claims
|
||||
# @odata.type='#microsoft.graph.targetedManagedAppConfiguration'.
|
||||
#
|
||||
# Outlook + Edge ObjectInfo translations (and the Edge bookmark/AllowList/
|
||||
# BlockList delimiter rewrites) deferred until the walker is ported. Offline
|
||||
# falls through to raw customSettings under TACSettings.generalSettings.
|
||||
|
||||
class ManagedAppConfigDocHandler : DocumentationHandlerBase {
|
||||
ManagedAppConfigDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.targetedManagedAppConfiguration')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'SettingDetails.appConfiguration') '@odata.type'
|
||||
|
||||
$customApps, $publishedApps = Get-CDMobileApps $obj.Apps
|
||||
|
||||
Add-BasicPropertyValue (Get-LanguageString 'Inputs.enrollmentTypeLabel') (Get-LanguageString 'EnrollmentType.devicesWithoutEnrollment') 'enrollmentType'
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.publicApps') ($publishedApps -join $Context.ObjectSeparator) 'publishedApps'
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.customApps') ($customApps -join $Context.ObjectSeparator) 'customApps'
|
||||
|
||||
# appGroupType - "Target policy to". The all* variants don't list individual
|
||||
# apps, so surfacing the mode is what tells the reader the scope. Graph's
|
||||
# enum member is allCoreMicrosoftApps; the portal's string is coreMicrosoftApps.
|
||||
$appGroupTypeKeys = @{
|
||||
'selectedPublicApps' = 'AppGroupType.selectedPublicApps'
|
||||
'allApps' = 'AppGroupType.allApps'
|
||||
'allMicrosoftApps' = 'AppGroupType.allMicrosoftApps'
|
||||
'allCoreMicrosoftApps' = 'AppGroupType.coreMicrosoftApps'
|
||||
}
|
||||
$agtRaw = "$($obj.appGroupType)"
|
||||
if ($agtRaw) {
|
||||
$agtKey = $appGroupTypeKeys[$agtRaw]
|
||||
$agtValue = if ($agtKey) { Get-LanguageString $agtKey -IgnoreMissing } else { $null }
|
||||
if ([string]::IsNullOrEmpty($agtValue)) { $agtValue = $agtRaw }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.targetPolicyTo') $agtValue 'appGroupType'
|
||||
}
|
||||
|
||||
# targetedAppManagementLevels - flags enum, returned as a comma-separated
|
||||
# string (e.g. "mdm, androidEnterprise"). Map each flag to its label,
|
||||
# falling back to the raw flag value when no string exists.
|
||||
$mgmtLevelKeys = @{
|
||||
'unspecified' = 'AppProtection.allAppTypes'
|
||||
'unmanaged' = 'AppProtection.appsOnUnmanagedDevices'
|
||||
'mdm' = 'AppProtection.appsOnIntuneManagedDevices'
|
||||
'androidEnterprise' = 'AppProtection.appsInAndroidWorkProfile'
|
||||
'androidEnterpriseDedicatedDevicesWithAzureAdSharedMode' = 'AppProtection.appsOnAndroidEnterpriseDedicatedDevicesWithAzureAdSharedMode'
|
||||
'androidOpenSourceProjectUserAssociated' = 'AppProtection.appsOnAndroidOpenSourceProjectUserAssociated'
|
||||
'androidOpenSourceProjectUserless' = 'AppProtection.appsOnAndroidOpenSourceProjectUserless'
|
||||
}
|
||||
$mgmtRaw = "$($obj.targetedAppManagementLevels)"
|
||||
if ($mgmtRaw) {
|
||||
$mgmtParts = @()
|
||||
foreach ($lvl in ($mgmtRaw -split ',')) {
|
||||
$lvlTrim = $lvl.Trim()
|
||||
if (-not $lvlTrim) { continue }
|
||||
$lvlKey = $mgmtLevelKeys[$lvlTrim]
|
||||
$lvlValue = if ($lvlKey) { Get-LanguageString $lvlKey -IgnoreMissing } else { $null }
|
||||
if ([string]::IsNullOrEmpty($lvlValue)) { $lvlValue = $lvlTrim }
|
||||
$mgmtParts += $lvlValue
|
||||
}
|
||||
if ($mgmtParts.Count -gt 0) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.managementType') ($mgmtParts -join $Context.ObjectSeparator) 'targetedAppManagementLevels'
|
||||
}
|
||||
}
|
||||
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
|
||||
# Outlook / Edge get schema-driven translation via their ObjectInfo files
|
||||
# (port of old DocumentationCustom.psm1:2229-2260). Build a flat settings
|
||||
# object keyed by customSetting name, then walk the matching manifest.
|
||||
$appSettings = [PSCustomObject]@{}
|
||||
foreach ($setting in @($obj.customSettings)) {
|
||||
$appSettings | Add-Member -MemberType NoteProperty -Name $setting.name -Value $setting.value -Force
|
||||
}
|
||||
$objInfoDir = Join-Path $script:AppRootFolder 'Config\ObjectInfo'
|
||||
|
||||
# Unpack every packed/delimited value BEFORE the manifests read them, so the
|
||||
# rewrite also benefits the raw fall-through rows below.
|
||||
# NB Where-Object: on a property-less object .PSObject.Properties.Name is
|
||||
# $null, and @($null) is a one-element array containing $null
|
||||
foreach ($name in @($appSettings.PSObject.Properties.Name | Where-Object { $_ })) {
|
||||
$sep = $script:_mamPackedSettingSeparators[$name]
|
||||
if (-not $sep -or -not $appSettings.$name -or $appSettings.$name -isnot [string]) { continue }
|
||||
$unpacked = $appSettings.$name
|
||||
# Record separator first - doing it the other way round destroys it
|
||||
if ($sep.RecordSep) { $unpacked = $unpacked.Replace($sep.RecordSep, $Context.ObjectSeparator) }
|
||||
if ($sep.FieldSep) { $unpacked = $unpacked.Replace($sep.FieldSep, $Context.PropertySeparator) }
|
||||
$appSettings.$name = $unpacked
|
||||
}
|
||||
|
||||
# App identities differ per platform: Outlook/Edge are packageId on Android,
|
||||
# bundleId on iOS and windowsAppId on Windows. Matching only one of them
|
||||
# silently skipped the whole manifest for the other platforms.
|
||||
if (Test-MamAppTargeted $obj.Apps @('com.microsoft.office.outlook')) {
|
||||
Invoke-DocAppConfigManifest $appSettings (Join-Path $objInfoDir '#AppConfigOutlookApp.json') $Context
|
||||
}
|
||||
if (Test-MamAppTargeted $obj.Apps @('com.microsoft.msedge', 'com.microsoft.emmx', 'com.microsoft.edge')) {
|
||||
Invoke-DocAppConfigManifest $appSettings (Join-Path $objInfoDir '#AppConfigEdgeApp.json') $Context
|
||||
}
|
||||
|
||||
# Settings-catalog settings (the "Settings catalog" wizard step, used by the
|
||||
# Windows MAM flavour). Without this a policy whose entire payload lives in
|
||||
# `settings` documented as a header and nothing else.
|
||||
Invoke-DocMamSettingsCatalog $obj $Context
|
||||
|
||||
# Remaining customSettings fall through to raw key=value rows.
|
||||
$addedSettings = Get-DocumentedSettings
|
||||
$category = Get-LanguageString 'TACSettings.generalSettings'
|
||||
|
||||
foreach ($setting in $obj.customSettings) {
|
||||
if ($addedSettings | Where-Object EntityKey -EQ $setting.name) { continue }
|
||||
# Use the unpacked value when one was produced above
|
||||
$value = if ($null -ne $appSettings.PSObject.Properties[$setting.name]) { $appSettings."$($setting.name)" } else { $setting.value }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = $setting.name
|
||||
Value = $value
|
||||
EntityKey = $setting.name
|
||||
Category = $category
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Document the settings-catalog part of a MAM app configuration.
|
||||
#
|
||||
# The Managed apps wizard has a "Settings catalog" step whose values land in the
|
||||
# `settings` navigation property (Collection(deviceManagementConfigurationSetting))
|
||||
# rather than in customSettings - Windows MAM policies are entirely settings-catalog.
|
||||
# The portal renders it as its own blade ABOVE the classic Settings blade, so these
|
||||
# rows go into a table of their own (negative Order = before the settings table)
|
||||
# instead of being merged into it.
|
||||
#
|
||||
# Resolution is the SettingsCatalog provider's own code - see
|
||||
# Get-SettingsCatalogDocumentationRows. This used to be a copy of it that had
|
||||
# drifted, losing the category grouping.
|
||||
function Invoke-DocMamSettingsCatalog {
|
||||
param($Obj, [DocumentationContext]$Context)
|
||||
|
||||
$cfgSettings = @($Obj.settings)
|
||||
|
||||
$hasDefs = $false
|
||||
foreach ($s in $cfgSettings) {
|
||||
if ($s.settingDefinitions -and ($s.settingDefinitions | Measure-Object).Count -gt 0) { $hasDefs = $true; break }
|
||||
}
|
||||
|
||||
# Source-tenant-specific fetch (by policy id) - same gating as the Settings
|
||||
# Catalog provider. Exports carrying settings inline still work offline via the
|
||||
# walker's generic per-setting definition fallback.
|
||||
if (-not $hasDefs -and $Obj.Id -and -not $Context.SourceTenantUnavailable -and (Test-DocumentationGraphAvailable)) {
|
||||
try {
|
||||
$resp = Invoke-MSGraphAPI -Url "/deviceAppManagement/targetedManagedAppConfigurations('$($Obj.Id)')/settings?`$expand=settingDefinitions&`$top=1000" -AdditionalHeaders (Get-DocAcceptLanguageHeaders $Context) -ODataMetadata 'minimal' -NoError
|
||||
if ($resp -and $resp.Value) { $cfgSettings = @($resp.Value) }
|
||||
}
|
||||
catch {
|
||||
Write-LogError "Failed to fetch settings catalog settings for app configuration $($Obj.Id)" $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
if (@($cfgSettings).Count -eq 0) { return }
|
||||
|
||||
$rows = @(Get-SettingsCatalogDocumentationRows $cfgSettings $Context)
|
||||
if ($rows.Count -eq 0) { return }
|
||||
|
||||
Add-CustomTable 'SettingsCatalog' @('Name','Value') $rows -Order -100 -LanguageId 'SettingDetails.settingsCatalog'
|
||||
}
|
||||
|
||||
# MAM settings whose value packs multiple records/fields into one string.
|
||||
# RecordSep splits repeated records, FieldSep splits fields inside a record.
|
||||
$script:_mamPackedSettingSeparators = @{
|
||||
# title|url pairs, records separated by ||
|
||||
'com.microsoft.intune.mam.managedbrowser.bookmarks' = @{ RecordSep = '||'; FieldSep = '|' }
|
||||
'com.microsoft.intune.mam.managedbrowser.managedTopSites' = @{ RecordSep = '||'; FieldSep = '|' }
|
||||
# plain pipe-separated lists
|
||||
'com.microsoft.intune.mam.managedbrowser.AllowListURLs' = @{ RecordSep = '|' }
|
||||
'com.microsoft.intune.mam.managedbrowser.BlockListURLs' = @{ RecordSep = '|' }
|
||||
'com.microsoft.intune.mam.managedbrowser.disabledFeatures' = @{ RecordSep = '|' }
|
||||
'com.microsoft.intune.mam.managedbrowser.InternalPagesBlockList' = @{ RecordSep = '|' }
|
||||
'com.microsoft.intune.mam.managedbrowser.PopupsAllowedForUrls' = @{ RecordSep = '|' }
|
||||
'com.microsoft.intune.mam.managedbrowser.PopupsBlockedForUrls' = @{ RecordSep = '|' }
|
||||
'com.microsoft.intune.mam.managedbrowser.FileUploadAllowedForUrls' = @{ RecordSep = '|' }
|
||||
'com.microsoft.intune.mam.managedbrowser.FileUploadBlockedForUrls' = @{ RecordSep = '|' }
|
||||
'com.microsoft.intune.mam.managedbrowser.NewTabPageLayout.Custom' = @{ RecordSep = '|' }
|
||||
}
|
||||
|
||||
# True when any targeted app matches one of the given app identifiers on ANY
|
||||
# platform identity (Android packageId / iOS bundleId / Windows windowsAppId).
|
||||
function Test-MamAppTargeted {
|
||||
param($Apps, [string[]]$Identifiers)
|
||||
|
||||
foreach ($app in @($Apps)) {
|
||||
$id = $app.mobileAppIdentifier
|
||||
if (-not $id) { continue }
|
||||
foreach ($candidate in @($id.packageId, $id.bundleId, $id.windowsAppId)) {
|
||||
if ($candidate -and $candidate -in $Identifiers) { return $true }
|
||||
}
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([ManagedAppConfigDocHandler]::new())
|
||||
@@ -0,0 +1,89 @@
|
||||
# Named Location documentation handler (Country + IP variants).
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:2280 (country) + 2323 (IP).
|
||||
# Single class claims both @odata.types since they share the same BasicInfo
|
||||
# header shape and one varies only the settings.
|
||||
|
||||
class NamedLocationDocHandler : DocumentationHandlerBase {
|
||||
NamedLocationDocHandler() {
|
||||
$this.ODataTypes = @(
|
||||
'#microsoft.graph.countryNamedLocation',
|
||||
'#microsoft.graph.ipNamedLocation',
|
||||
'#microsoft.graph.compliantNetworkNamedLocation'
|
||||
)
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'AzureCA.menuItemNamedNetworks') '@odata.type'
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
switch ($obj.'@odata.type') {
|
||||
'#microsoft.graph.countryNamedLocation' { Invoke-NamedLocationCountrySettings $obj $Context }
|
||||
'#microsoft.graph.ipNamedLocation' { Invoke-NamedLocationIPSettings $obj $Context }
|
||||
'#microsoft.graph.compliantNetworkNamedLocation' { Invoke-NamedLocationCompliantNetworkSettings $obj $Context }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-NamedLocationCountrySettings {
|
||||
param($obj, [DocumentationContext]$Context)
|
||||
|
||||
$lookupSuffix = if ($obj.countryLookupMethod -eq 'clientIpAddress') { 'ip' } else { 'gps' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'AzureCA.NamedLocation.Form.CountryLookup.ariaLabel')
|
||||
Value = (Get-LanguageString "AzureCA.NamedLocation.Form.CountryLookup.$lookupSuffix")
|
||||
EntityKey = 'countryLookupMethod'
|
||||
})
|
||||
|
||||
$includeKey = if ($obj.includeUnknownCountriesAndRegions -eq $true) { 'Inputs.enabled' } else { 'Inputs.disabled' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'AzureCA.NamedLocation.Form.Include.label')
|
||||
Value = (Get-LanguageString $includeKey)
|
||||
EntityKey = 'includeUnknownCountriesAndRegions'
|
||||
})
|
||||
|
||||
$countryNames = @()
|
||||
foreach ($code in $obj.countriesAndRegions) {
|
||||
$countryNames += Get-LanguageString "AzureIAMCommon.CountryNames.countryName$($code.ToUpper())"
|
||||
}
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'AzureCA.NamedLocation.Type.countries')
|
||||
Value = ($countryNames -join $Context.ObjectSeparator)
|
||||
EntityKey = 'countriesAndRegions'
|
||||
})
|
||||
}
|
||||
|
||||
function Invoke-NamedLocationIPSettings {
|
||||
param($obj, [DocumentationContext]$Context)
|
||||
|
||||
$trustedKey = if ($obj.isTrusted -eq $true) { 'Inputs.enabled' } else { 'Inputs.disabled' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'AzureCA.NamedLocation.Form.Trusted.label')
|
||||
Value = (Get-LanguageString $trustedKey)
|
||||
EntityKey = 'isTrusted'
|
||||
})
|
||||
|
||||
$ipList = @()
|
||||
foreach ($range in $obj.ipRanges) { $ipList += $range.cidrAddress }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'AzureCA.namedNetworkIpRangesTab')
|
||||
Value = ($ipList -join $Context.ObjectSeparator)
|
||||
EntityKey = 'ipRanges'
|
||||
})
|
||||
}
|
||||
|
||||
function Invoke-NamedLocationCompliantNetworkSettings {
|
||||
param($obj, [DocumentationContext]$Context)
|
||||
|
||||
# Built-in read-only location; its only meaningful setting is the trusted flag.
|
||||
$trustedKey = if ($obj.isTrusted -eq $true) { 'Inputs.enabled' } else { 'Inputs.disabled' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'AzureCA.NamedLocation.Form.Trusted.label')
|
||||
Value = (Get-LanguageString $trustedKey)
|
||||
EntityKey = 'isTrusted'
|
||||
})
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([NamedLocationDocHandler]::new())
|
||||
@@ -0,0 +1,95 @@
|
||||
# Notification message template documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:3660. Emits 4 branding-
|
||||
# option rows ("Show company logo/name/contact/portal link" enable/disable)
|
||||
# plus one row per localized message template with the locale name as label
|
||||
# and the subject+body as value.
|
||||
|
||||
class NotificationDocHandler : DocumentationHandlerBase {
|
||||
NotificationDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.notificationMessageTemplate')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Titles.notifications') '@odata.type'
|
||||
|
||||
$category = Get-LanguageString 'TableHeaders.settings'
|
||||
|
||||
# brandingOptions is a comma-separated string like "includeCompanyLogo,includeCompanyName"
|
||||
# or "none". Split into a hash for membership tests.
|
||||
$brandingFlags = @{}
|
||||
if ($obj.brandingOptions) {
|
||||
foreach ($flag in $obj.brandingOptions.Split(',')) {
|
||||
$brandingFlags[$flag.Trim()] = $true
|
||||
}
|
||||
}
|
||||
|
||||
$brandingLabelMap = [ordered]@{
|
||||
'includeCompanyLogo' = 'NotificationMessage.companyLogo'
|
||||
'includeCompanyName' = 'NotificationMessage.companyName'
|
||||
'includeContactInformation' = 'NotificationMessage.companyContact'
|
||||
'includeCompanyPortalLink' = 'NotificationMessage.iwLink'
|
||||
'includeDeviceDetails' = 'NotificationMessage.deviceDetails'
|
||||
}
|
||||
|
||||
foreach ($flag in $brandingLabelMap.Keys) {
|
||||
$valueKey = if ($brandingFlags.ContainsKey($flag)) { 'BooleanActions.enable' } else { 'BooleanActions.disable' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString $brandingLabelMap[$flag])
|
||||
Value = (Get-LanguageString $valueKey)
|
||||
EntityKey = $flag
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
|
||||
# Localized message templates
|
||||
$subCategory = Get-LanguageString 'NotificationMessage.listTitle'
|
||||
foreach ($template in $obj.localizedNotificationMessages) {
|
||||
$label = Get-NotificationLocaleLabel $template.locale
|
||||
if (-not $label) { continue }
|
||||
|
||||
$value = $template.subject
|
||||
if ($template.isDefault) {
|
||||
$value = $value + $Context.ObjectSeparator + (Get-LanguageString 'NotificationMessage.isDefaultLocale') + ': ' + (Get-LanguageString 'SettingDetails.trueOption')
|
||||
}
|
||||
$fullValue = $value + $Context.ObjectSeparator + $template.messageTemplate
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = $label
|
||||
Value = $fullValue
|
||||
EntityKey = $template.locale
|
||||
Category = $category
|
||||
SubCategory = $subCategory
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Localized message templates are labeled by language name. Most languages
|
||||
# pass through [cultureinfo].EnglishName.ToLower(); a handful with regional
|
||||
# splits (en-US/UK, es-ES/MX, fr-CA/FR, pt-PT/BR, zh-TW/CN, nb-* -> norwegian)
|
||||
# get a suffix. Old code at DocumentationCustom.psm1:3735-3796.
|
||||
function Get-NotificationLocaleLabel {
|
||||
param([string]$Locale)
|
||||
if (-not $Locale) { return $null }
|
||||
|
||||
$first, $second = $Locale.Split('-')
|
||||
try { $lng = ([cultureinfo]$first).EnglishName.ToLower() } catch { return $null }
|
||||
|
||||
switch ($first) {
|
||||
'en' { switch ($second) { 'US' { $lng += 'US' }; 'GB' { $lng += 'UK' } } }
|
||||
'es' { switch ($second) { 'es' { $lng += 'Spain' }; 'mx' { $lng += 'Mexico' } } }
|
||||
'fr' { switch ($second) { 'ca' { $lng += 'Canada' }; 'fr' { $lng += 'France' } } }
|
||||
'pt' { switch ($second) { 'pt' { $lng += 'Portugal' }; 'br' { $lng += 'Brazil' } } }
|
||||
'zh' { switch ($second) { 'tw' { $lng += 'Traditional' }; 'cn' { $lng += 'Simplified' } } }
|
||||
'nb' { $lng = 'norwegian' }
|
||||
}
|
||||
|
||||
return (Get-LanguageString "NotificationMessage.NotificationMessageTemplatesTab.$lng")
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([NotificationDocHandler]::new())
|
||||
@@ -0,0 +1,100 @@
|
||||
# Policy Set documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:3422. Categorizes the
|
||||
# policy-set items (apps / device configs / enrollment) into 3 sections, then
|
||||
# emits one row per item with the item's displayName and a type-specific
|
||||
# value (priority number for ordered items, AAD/AD for autopilot, etc.).
|
||||
|
||||
class PolicySetDocHandler : DocumentationHandlerBase {
|
||||
PolicySetDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.policySet')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'SettingDetails.appConfiguration') '@odata.type'
|
||||
|
||||
$sections = @(
|
||||
[PSCustomObject]@{
|
||||
Category = (Get-LanguageString 'PolicySet.appManagement')
|
||||
Types = @(
|
||||
@{ ODataType = '#microsoft.graph.mobileAppPolicySetItem'; SubKey = 'appTitle' }
|
||||
@{ ODataType = '#microsoft.graph.targetedManagedAppConfigurationPolicySetItem'; SubKey = 'appConfigurationTitle' }
|
||||
@{ ODataType = '#microsoft.graph.managedDeviceMobileAppConfigurationPolicySetItem'; SubKey = 'appConfigurationTitle' }
|
||||
@{ ODataType = '#microsoft.graph.managedAppProtectionPolicySetItem'; SubKey = 'appProtectionTitle' }
|
||||
@{ ODataType = '#microsoft.graph.iosLobAppProvisioningConfigurationPolicySetItem'; SubKey = 'iOSAppProvisioningTitle' }
|
||||
)
|
||||
}
|
||||
[PSCustomObject]@{
|
||||
Category = (Get-LanguageString 'PolicySet.deviceManagement')
|
||||
Types = @(
|
||||
@{ ODataType = '#microsoft.graph.deviceConfigurationPolicySetItem'; SubKey = 'deviceConfigurationTitle' }
|
||||
@{ ODataType = '#microsoft.graph.deviceManagementConfigurationPolicyPolicySetItem'; SubKey = 'SettingDetails.settingsCatalog' }
|
||||
@{ ODataType = '#microsoft.graph.deviceCompliancePolicyPolicySetItem'; SubKey = 'deviceComplianceTitle' }
|
||||
@{ ODataType = '#microsoft.graph.deviceManagementScriptPolicySetItem'; SubKey = 'powershellScriptTitle' }
|
||||
)
|
||||
}
|
||||
[PSCustomObject]@{
|
||||
Category = (Get-LanguageString 'PolicySet.deviceEnrollment')
|
||||
Types = @(
|
||||
@{ ODataType = '#microsoft.graph.enrollmentRestrictionsConfigurationPolicySetItem'; SubKey = 'deviceTypeRestrictionTitle' }
|
||||
@{ ODataType = '#microsoft.graph.windowsAutopilotDeploymentProfilePolicySetItem'; SubKey = 'windowsAutopilotDeploymentProfileTitle' }
|
||||
@{ ODataType = '#microsoft.graph.windows10EnrollmentCompletionPageConfigurationPolicySetItem'; SubKey = 'enrollmentStatusSettingTitle' }
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
foreach ($section in $sections) {
|
||||
foreach ($subType in $section.Types) {
|
||||
foreach ($item in ($obj.items | Where-Object { $_.'@OData.Type' -eq $subType.ODataType -or $_.'@odata.type' -eq $subType.ODataType })) {
|
||||
if ($item.status -eq 'error') {
|
||||
Write-Log "Skipping missing $($subType.ODataType) type with id $($item.id). Error code: $($item.errorCode)" 2
|
||||
continue
|
||||
}
|
||||
|
||||
# SubKey is a bare key under PolicySet.* unless it already
|
||||
# carries a namespace (dotted), letting new item types reuse
|
||||
# strings that live outside the PolicySet section.
|
||||
$subKeyFull = if ($subType.SubKey -like '*.*') { $subType.SubKey } else { "PolicySet.$($subType.SubKey)" }
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = $item.displayName
|
||||
Value = (Get-PolicySetItemValue $item)
|
||||
EntityKey = $item.id
|
||||
Category = $section.Category
|
||||
SubCategory = (Get-LanguageString $subKeyFull)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PolicySetItemValue {
|
||||
param($item)
|
||||
|
||||
$odata = if ($item.PSObject.Properties['@OData.Type']) { $item.'@OData.Type' } else { $item.'@odata.type' }
|
||||
|
||||
if ($odata -in @(
|
||||
'#microsoft.graph.enrollmentRestrictionsConfigurationPolicySetItem',
|
||||
'#microsoft.graph.windows10EnrollmentCompletionPageConfigurationPolicySetItem'
|
||||
)) {
|
||||
return $item.Priority
|
||||
}
|
||||
|
||||
if ($odata -eq '#microsoft.graph.windowsAutopilotDeploymentProfilePolicySetItem') {
|
||||
if ($item.itemType -eq '#microsoft.graph.azureADWindowsAutopilotDeploymentProfile') {
|
||||
return (Get-LanguageString 'Autopilot.DirectoryService.azureAD')
|
||||
}
|
||||
if ($item.itemType -eq '#microsoft.graph.activeDirectoryWindowsAutopilotDeploymentProfile') {
|
||||
return (Get-LanguageString 'Autopilot.DirectoryService.activeDirectoryAD')
|
||||
}
|
||||
}
|
||||
|
||||
# TODO phase-4-followup: other PolicySet item types as fixtures arrive
|
||||
return $null
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([PolicySetDocHandler]::new())
|
||||
@@ -0,0 +1,68 @@
|
||||
# Reusable setting (deviceManagementReusablePolicySetting) documentation
|
||||
# handler.
|
||||
#
|
||||
# Claims @odata.type='#microsoft.graph.deviceManagementReusablePolicySetting'.
|
||||
# Covers the reusable settings surfaced as policy types (currently the Linux
|
||||
# custom-compliance discovery script). The object is a name + description +
|
||||
# settingDefinitionId wrapper around a single settings-catalog setting
|
||||
# instance whose simpleSettingValue carries the payload (base64 script for
|
||||
# the discovery-script definition). No old-project documenter existed.
|
||||
|
||||
class ReusableSettingDocHandler : DocumentationHandlerBase {
|
||||
ReusableSettingDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.deviceManagementReusablePolicySetting')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
if ($PolicyObject.Name) {
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $PolicyObject.Name 'displayName'
|
||||
}
|
||||
$descValue = if ($obj.description) { $obj.description } else { '' }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $descValue 'description'
|
||||
|
||||
$category = Get-LanguageString 'TableHeaders.settings'
|
||||
|
||||
if ($obj.settingDefinitionId) {
|
||||
$definitionLabel = Get-LanguageString 'SettingDetails.settingIdName' -IgnoreMissing
|
||||
if (-not $definitionLabel) { $definitionLabel = 'Setting definition' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = $definitionLabel
|
||||
Value = [string]$obj.settingDefinitionId
|
||||
EntityKey = 'settingDefinitionId'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
|
||||
$rawValue = [string]$obj.settingInstance.simpleSettingValue.value
|
||||
if ($rawValue -and -not ($Context.Options -and $Context.Options['IncludeScripts'] -eq $false)) {
|
||||
# The discovery-script definition stores the script base64-encoded;
|
||||
# fall back to the raw value for definitions that don't.
|
||||
$value = $rawValue
|
||||
try {
|
||||
$decoded = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($rawValue))
|
||||
if ($decoded) { $value = $decoded }
|
||||
}
|
||||
catch { }
|
||||
|
||||
$valueLabel = if ([string]$obj.settingDefinitionId -like '*discoveryscript*') {
|
||||
Get-LanguageString 'ProactiveRemediations.Create.Settings.DetectionScriptMultiLineTextBox.label'
|
||||
}
|
||||
else {
|
||||
$lbl = Get-LanguageString 'SettingDetails.valueName' -IgnoreMissing
|
||||
if ($lbl) { $lbl } else { 'Value' }
|
||||
}
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = $valueLabel
|
||||
Value = $value
|
||||
EntityKey = 'settingInstanceValue'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([ReusableSettingDocHandler]::new())
|
||||
@@ -0,0 +1,186 @@
|
||||
# Role Definition documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:4513. Resolves
|
||||
# permissions to resource/action names via /deviceManagement/resourceOperations
|
||||
# (generic schema - resolved from any connected tenant) and enriches assignments
|
||||
# with directory display names (source-tenant-specific - skipped when the source
|
||||
# tenant is unavailable, emitting raw IDs).
|
||||
|
||||
class RoleDefinitionDocHandler : DocumentationHandlerBase {
|
||||
RoleDefinitionDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.deviceAndAppManagementRoleDefinition')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'RoleAssignment.rolesMenuTitle') '@odata.type'
|
||||
|
||||
# Built-in vs custom role. isBuiltIn is true for Microsoft-supplied roles.
|
||||
if ($null -ne $obj.isBuiltIn) {
|
||||
$builtInValue = if ($obj.isBuiltIn) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'DisplayRoleTypes.builtInRole') $builtInValue 'isBuiltIn'
|
||||
}
|
||||
|
||||
# --- Permissions section: resolve action IDs to resource/action names ---
|
||||
$roleResources = @()
|
||||
# resourceOperations is a GENERIC catalog (resource/action names) - same on
|
||||
# every tenant - so resolved from any connected tenant.
|
||||
if (Test-DocumentationGraphAvailable) {
|
||||
try {
|
||||
$resp = Invoke-MSGraphAPI -Url '/deviceManagement/resourceOperations'
|
||||
$roleResources = @($resp.Value)
|
||||
}
|
||||
catch {
|
||||
Write-LogError 'Failed to fetch /deviceManagement/resourceOperations for role permissions' $_.Exception
|
||||
}
|
||||
}
|
||||
|
||||
$permissionsCategory = Get-LanguageString 'Titles.permissions'
|
||||
|
||||
# Prefer the modern rolePermissions structure: union allowedResourceActions
|
||||
# across ALL rolePermissions/resourceActions. Fall back to the legacy flat
|
||||
# permissions[0].actions list when rolePermissions is absent (older payloads
|
||||
# and some built-in roles only populate the legacy list).
|
||||
$actionIds = @()
|
||||
if ($obj.rolePermissions) {
|
||||
foreach ($rolePermission in @($obj.rolePermissions)) {
|
||||
foreach ($resourceAction in @($rolePermission.resourceActions)) {
|
||||
foreach ($allowed in @($resourceAction.allowedResourceActions)) {
|
||||
if ($allowed -and $actionIds -notcontains $allowed) { $actionIds += $allowed }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($actionIds.Count -eq 0 -and $obj.permissions -and $obj.permissions[0]) {
|
||||
$actionIds = @($obj.permissions[0].actions)
|
||||
}
|
||||
|
||||
if ($roleResources.Count -gt 0 -and $actionIds.Count -gt 0) {
|
||||
# Resolved live: group resolved actions by resourceName
|
||||
$assignedActions = @()
|
||||
foreach ($id in $actionIds) {
|
||||
$r = $roleResources | Where-Object Id -EQ $id | Select-Object -First 1
|
||||
if ($r) { $assignedActions += $r }
|
||||
}
|
||||
|
||||
$byResource = $assignedActions | Select-Object resourceName -Unique | Sort-Object -Property resourceName
|
||||
foreach ($rn in $byResource.resourceName) {
|
||||
$actions = @($assignedActions | Where-Object resourceName -EQ $rn)
|
||||
$resourceId = $actions[0].resource
|
||||
$actionNames = ($actions | ForEach-Object { $_.actionName })
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = $rn
|
||||
Value = ($actionNames -join $Context.ObjectSeparator)
|
||||
EntityKey = $resourceId
|
||||
Category = $permissionsCategory
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
}
|
||||
elseif ($actionIds.Count -gt 0) {
|
||||
# Offline: emit a single row with raw action IDs so the row count
|
||||
# is non-zero and compare-style downstream tools have something
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'Titles.permissions')
|
||||
Value = ($actionIds -join $Context.ObjectSeparator)
|
||||
EntityKey = 'actions'
|
||||
Category = $permissionsCategory
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
|
||||
# --- Assignments section ---
|
||||
# roleAssignments are enriched in place (full assignment + roleScopeTags)
|
||||
# by RoleDefinitionObject's sub-resource contract during hydration, so
|
||||
# each entry already carries displayName/description/members/scopeMembers/
|
||||
# roleScopeTags — no per-assignment Graph fetch here. The
|
||||
# deviceManagement/roleAssignments/<id> API now lives only on the class.
|
||||
# Offline runs still skip this section (matching prior behavior); the
|
||||
# member display-name resolution (getByIds) is shared reference data and
|
||||
# stays online-only.
|
||||
if ($Context.SourceTenantUnavailable -or -not (Test-DocumentationGraphAvailable)) { return }
|
||||
|
||||
$assignmentsCategory = Get-LanguageString 'TableHeaders.assignments'
|
||||
foreach ($info in @($obj.roleAssignments)) {
|
||||
if (-not $info -or [string]::IsNullOrWhiteSpace([string]$info.id)) {
|
||||
Write-Log 'RoleDefinition: skipping role assignment without an id' 2
|
||||
continue
|
||||
}
|
||||
|
||||
# Resolve member + scope IDs to displayNames in one batch
|
||||
$ids = @()
|
||||
foreach ($id in @($info.members + $info.scopeMembers)) {
|
||||
if ($id -and $ids -notcontains $id) { $ids += $id }
|
||||
}
|
||||
$idInfo = @()
|
||||
if ($ids.Count -gt 0) {
|
||||
try {
|
||||
$body = @{ ids = $ids } | ConvertTo-Json
|
||||
$resp = Invoke-MSGraphAPI -Url "/directoryObjects/getByIds?`$select=displayName,id" -Content $body -Method POST
|
||||
$idInfo = @($resp.Value)
|
||||
}
|
||||
catch { Write-LogError 'Failed to resolve role-assignment member display names' $_.Exception }
|
||||
}
|
||||
|
||||
$sub = $info.displayName
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'SettingDetails.nameName')
|
||||
Value = $info.displayName
|
||||
EntityKey = 'displayName'
|
||||
Category = $assignmentsCategory; SubCategory = $sub
|
||||
})
|
||||
if ($info.description) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'SettingDetails.descriptionName')
|
||||
Value = $info.description
|
||||
EntityKey = 'description'
|
||||
Category = $assignmentsCategory; SubCategory = $sub
|
||||
})
|
||||
}
|
||||
|
||||
$admins = @()
|
||||
foreach ($id in @($info.members)) {
|
||||
$resolved = $idInfo | Where-Object Id -EQ $id | Select-Object -First 1
|
||||
$admins += if ($resolved.displayName) { $resolved.displayName } else { $id }
|
||||
}
|
||||
if ($admins.Count -gt 0) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'RoleAssignment.RoleAssignmentAdmin')
|
||||
Value = ($admins -join $Context.ObjectSeparator)
|
||||
EntityKey = 'members'
|
||||
Category = $assignmentsCategory; SubCategory = $sub
|
||||
})
|
||||
}
|
||||
|
||||
$scopeMembers = @()
|
||||
foreach ($id in @($info.scopeMembers)) {
|
||||
$resolved = $idInfo | Where-Object Id -EQ $id | Select-Object -First 1
|
||||
$scopeMembers += if ($resolved.displayName) { $resolved.displayName } else { $id }
|
||||
}
|
||||
if ($scopeMembers.Count -gt 0) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'RoleAssignment.RoleAssignmentScope')
|
||||
Value = ($scopeMembers -join $Context.ObjectSeparator)
|
||||
EntityKey = 'scopeMembers'
|
||||
Category = $assignmentsCategory; SubCategory = $sub
|
||||
})
|
||||
}
|
||||
|
||||
$scopeTags = @($info.roleScopeTags | ForEach-Object { $_.displayName })
|
||||
if ($scopeTags.Count -gt 0) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'TableHeaders.scopeTags')
|
||||
Value = ($scopeTags -join $Context.ObjectSeparator)
|
||||
EntityKey = 'scopeTags'
|
||||
Category = $assignmentsCategory; SubCategory = $sub
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([RoleDefinitionDocHandler]::new())
|
||||
@@ -0,0 +1,46 @@
|
||||
# Role Scope Tag documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:4882 (Invoke-
|
||||
# CDDocumentScopeTag). Claims @odata.type='#microsoft.graph.roleScopeTag'.
|
||||
#
|
||||
# Old handler accumulated per-tag rows into a cross-batch
|
||||
# $script:ObjectTypeFullTable hashtable that the output providers flushed as a
|
||||
# single consolidated "Scope Tags" table at PostProcess time. The new engine
|
||||
# already has $ctx.ObjectTypeFullTable for the same purpose, but no output
|
||||
# provider consumes it yet — until that's wired up, we just emit per-object
|
||||
# BasicInfo so each tag at least documents independently rather than being
|
||||
# dropped on the floor as NoProvider.
|
||||
#
|
||||
# Assignments are intentionally NOT translated here: Invoke-TranslateAssignments
|
||||
# is a separate ~370-LOC port (DocumentationMigration.md risk #4) that no
|
||||
# handler in the new project calls yet. When that lands, this handler can
|
||||
# trivially call it after the BasicInfo block.
|
||||
|
||||
class ScopeTagDocHandler : DocumentationHandlerBase {
|
||||
ScopeTagDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.roleScopeTag')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
# Plain Name + Description rows. Scope tags don't have Platform supported
|
||||
# or Profile type rows in any old-engine path, so skip the
|
||||
# Add-BasicDefaultValues helper (which would emit blank Platform /
|
||||
# Profile rows from a missing ObjectCategories entry).
|
||||
if ($PolicyObject.Name) {
|
||||
$nameProp = if ($PolicyObject.PolicyType._NameProperty) { $PolicyObject.PolicyType._NameProperty } else { 'displayName' }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $PolicyObject.Name $nameProp
|
||||
}
|
||||
|
||||
$descValue = if ($obj.description) { $obj.description } else { '' }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.descriptionName') $descValue 'description'
|
||||
|
||||
if ($null -ne $obj.isBuiltIn) {
|
||||
$val = if ($obj.isBuiltIn) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
|
||||
Add-BasicPropertyValue (Get-LanguageString 'RoleScopeTag.isBuiltIn') $val 'isBuiltIn'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([ScopeTagDocHandler]::new())
|
||||
@@ -0,0 +1,90 @@
|
||||
# Terms of Use (agreement) documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:2361. Note this handler
|
||||
# does NOT use Add-BasicDefaultValues — the old engine only emits Name +
|
||||
# Profile type for agreements (no Description, no Created/Modified).
|
||||
|
||||
class TermsOfUseDocHandler : DocumentationHandlerBase {
|
||||
TermsOfUseDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.agreement')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
$offLabel = Get-LanguageString 'SettingDetails.offOption'
|
||||
$onLabel = Get-LanguageString 'SettingDetails.onOption'
|
||||
|
||||
# BasicInfo: just Name + Profile type
|
||||
Add-BasicPropertyValue (Get-LanguageString 'SettingDetails.nameName') $obj.displayName 'displayName'
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'AzureCA.menuItemTermsOfUse') '@odata.type'
|
||||
|
||||
$viewingValue = if ($obj.isViewingBeforeAcceptanceRequired) { $onLabel } else { $offLabel }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'TermsOfUse.Wizard.agreementIsViewingBeforeAcceptanceRequiredLabel')
|
||||
Value = $viewingValue; Category = $null; SubCategory = $null
|
||||
EntityKey = 'isViewingBeforeAcceptanceRequired'
|
||||
})
|
||||
|
||||
$perDeviceValue = if ($obj.isPerDeviceAcceptanceRequired) { $onLabel } else { $offLabel }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'TermsOfUse.Wizard.agreementIsPerDeviceAcceptanceRequiredLabel')
|
||||
Value = $perDeviceValue; Category = $null; SubCategory = $null
|
||||
EntityKey = 'isPerDeviceAcceptanceRequired'
|
||||
})
|
||||
|
||||
$expirationValue = if ($obj.termsExpiration) { $onLabel } else { $offLabel }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'TermsOfUse.Wizard.isAcceptanceExpirationEnabledLabel')
|
||||
Value = $expirationValue; Category = $null; SubCategory = $null
|
||||
EntityKey = 'isAcceptanceExpirationEnabledLabel'
|
||||
})
|
||||
|
||||
# Expiration details (only when termsExpiration is set)
|
||||
if ($obj.termsExpiration.startDateTime) {
|
||||
try {
|
||||
if ($obj.termsExpiration.startDateTime -is [datetime]) {
|
||||
$tmp = if ($obj.termsExpiration.startDateTime.Kind -eq 'Utc') { $obj.termsExpiration.startDateTime.ToLocalTime() } else { $obj.termsExpiration.startDateTime }
|
||||
}
|
||||
else {
|
||||
$tmp = ([datetime]::Parse($obj.termsExpiration.startDateTime, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal)).ToLocalTime()
|
||||
}
|
||||
$startStr = $tmp.ToShortDateString()
|
||||
}
|
||||
catch {
|
||||
Write-Log "Failed to parse date from string $($obj.termsExpiration.startDateTime)" 2
|
||||
$startStr = $obj.termsExpiration.startDateTime
|
||||
}
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'TermsOfUse.Wizard.acceptanceExpirationStartDateTimeLabel')
|
||||
Value = $startStr; Category = $null; SubCategory = $null
|
||||
EntityKey = 'startDateTime'
|
||||
})
|
||||
|
||||
$freqValue = switch ($obj.termsExpiration.frequency) {
|
||||
'P365D' { Get-LanguageString 'TermsOfUse.AcceptanceExpirationFrequency.annually' }
|
||||
'P180D' { Get-LanguageString 'TermsOfUse.AcceptanceExpirationFrequency.biannually' }
|
||||
'P30D' { Get-LanguageString 'TermsOfUse.AcceptanceExpirationFrequency.monthly' }
|
||||
'P90D' { Get-LanguageString 'TermsOfUse.AcceptanceExpirationFrequency.quarterly' }
|
||||
default { $null }
|
||||
}
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'TermsOfUse.Wizard.acceptanceExpirationFrequencyLabel')
|
||||
Value = $freqValue; Category = $null; SubCategory = $null
|
||||
EntityKey = 'frequency'
|
||||
})
|
||||
}
|
||||
|
||||
if ($null -ne $obj.userReacceptRequiredFrequency) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = (Get-LanguageString 'TermsOfUse.Wizard.acceptanceDurationLabel')
|
||||
Value = (Get-DurationValue $obj.userReacceptRequiredFrequency)
|
||||
Category = $null; SubCategory = $null
|
||||
EntityKey = 'userReacceptRequiredFrequency'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([TermsOfUseDocHandler]::new())
|
||||
@@ -0,0 +1,400 @@
|
||||
# Windows Kiosk Configuration documentation handler.
|
||||
#
|
||||
# Ported from old Extensions/DocumentationCustom.psm1:3923 (Invoke-
|
||||
# CDDocumentWindowsKioskConfiguration). Claims @odata.type=
|
||||
# '#microsoft.graph.windowsKioskConfiguration'.
|
||||
#
|
||||
# Generic Profile/walker can't handle this type because the actual settings
|
||||
# live under nested $obj.kioskProfiles[0].appConfiguration /
|
||||
# .userAccountsConfiguration with discriminated @odata.type subtypes
|
||||
# (windowsKioskSingleWin32App, windowsKioskSingleUWPApp, windowsKioskMultipleApps,
|
||||
# windowsKioskAutologon, windowsKioskAzureADGroup/User, etc.). A handler with
|
||||
# explicit subtype dispatch is required.
|
||||
#
|
||||
# Preserves an old-engine quirk: when userAccountsConfiguration is an array of
|
||||
# mixed AAD User + AAD Group entries, PS evaluates the switch on the implicit
|
||||
# array of @odata.types and `-eq 'kioskAADUserAndGroup'` on the resulting array
|
||||
# returns the matching items (truthy in if-test). The "User logon type" row
|
||||
# then shows empty because `"SettingDetails.$($logonTypeLngId)"` interpolates
|
||||
# the array as space-joined.
|
||||
|
||||
class WindowsKioskDocHandler : DocumentationHandlerBase {
|
||||
WindowsKioskDocHandler() {
|
||||
$this.ODataTypes = @('#microsoft.graph.windowsKioskConfiguration')
|
||||
}
|
||||
|
||||
[void] Document([object]$PolicyObject, [DocumentationContext]$Context) {
|
||||
$obj = $PolicyObject.JsonObject
|
||||
|
||||
# ---- Basic info ----
|
||||
Add-BasicDefaultValues $PolicyObject
|
||||
Add-BasicAdditionalValues $PolicyObject
|
||||
Add-BasicPropertyValue (Get-LanguageString 'TableHeaders.configurationType') (Get-LanguageString 'Category.kioskConfigurationV2') '@odata.type'
|
||||
# Old engine emits a Platform row from $obj.platform. The raw payload
|
||||
# doesn't carry one for this type, so the lookup resolves to empty —
|
||||
# golden fixtures still contain the empty row, so emit it for parity.
|
||||
Add-BasicPropertyValue (Get-LanguageString 'Inputs.platformLabel') (Get-LanguageString "Platform.$($obj.platform)") 'platform'
|
||||
|
||||
# ---- Settings ----
|
||||
$category = Get-LanguageString 'Category.kiosk'
|
||||
|
||||
$appConfig = $obj.kioskProfiles[0].appConfiguration
|
||||
$userConfig = $obj.kioskProfiles[0].userAccountsConfiguration
|
||||
|
||||
# kioskMode dispatch
|
||||
if ($appConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskSingleWin32App' -or
|
||||
$appConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskSingleUWPApp') {
|
||||
$kioskModeType = 'single'
|
||||
$kioskMode = Get-LanguageString 'SettingDetails.kioskSelectionSingleMode'
|
||||
}
|
||||
else {
|
||||
$kioskModeType = 'multi'
|
||||
$kioskMode = Get-LanguageString 'SettingDetails.kioskSelectionMultiMode'
|
||||
}
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskSelectionName'
|
||||
Value = $kioskMode
|
||||
EntityKey = 'kioskMode'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
# User logon type
|
||||
$logonTypeLngId = switch ($userConfig.'@odata.type') {
|
||||
'#microsoft.graph.windowsKioskAutologon' { 'kioskUserLogonTypeAutologon' }
|
||||
'#microsoft.graph.windowsKioskAzureADUser' { 'kioskAADUserAndGroup' }
|
||||
'#microsoft.graph.windowsKioskAzureADGroup' { 'kioskAADUserAndGroup' }
|
||||
'#microsoft.graph.windowsKioskLocalUser' { 'kioskAppTypeStore' }
|
||||
'#microsoft.graph.windowsKioskVisitor' { 'kioskVisitor' }
|
||||
}
|
||||
$logonType = if ($logonTypeLngId) {
|
||||
Get-LanguageString "SettingDetails.$logonTypeLngId"
|
||||
} else {
|
||||
Write-Log "Unknown kiosk user logon type. $($userConfig.'@odata.type')" 2
|
||||
$null
|
||||
}
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskSelectionUsers'
|
||||
Value = $logonType
|
||||
EntityKey = 'userAccountsConfigurationType'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
# User logon name(s)
|
||||
if ($logonTypeLngId -eq 'kioskAADUserAndGroup') {
|
||||
$aadUser = Get-LanguageString 'SettingDetails.kioskAADUser'
|
||||
$aadGroup = Get-LanguageString 'SettingDetails.kioskAADGroup'
|
||||
$users = @()
|
||||
foreach ($u in $userConfig) {
|
||||
$sep = $Context.PropertySeparator
|
||||
if ($u.'@odata.type' -eq '#microsoft.graph.windowsKioskAzureADUser') {
|
||||
$users += "$($u.userPrincipalName)$sep$aadUser"
|
||||
}
|
||||
else {
|
||||
$users += "$($u.displayName)$sep$aadGroup"
|
||||
}
|
||||
}
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskUserAccountName'
|
||||
Value = $users -join $Context.ObjectSeparator
|
||||
EntityKey = 'userAccounts'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
elseif ($userConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskLocalUser') {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskUserAccountName'
|
||||
Value = $userConfig.userName
|
||||
EntityKey = 'userName'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
|
||||
# Single-app: detect underlying app type and emit type-specific rows
|
||||
if ($kioskModeType -eq 'single') {
|
||||
$uwpAppType = $null
|
||||
$appType = $null
|
||||
if ($appConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskSingleWin32App') {
|
||||
$uwpAppType = 'win32App'
|
||||
$appType = Get-LanguageString 'SettingDetails.selectWin32AppForEdge86'
|
||||
}
|
||||
elseif ($appConfig.'@odata.type' -eq '#microsoft.graph.windowsKioskSingleUWPApp') {
|
||||
if ($appConfig.uwpApp.appUserModelId -like 'Microsoft.MicrosoftEdge*') {
|
||||
$uwpAppType = 'edge'
|
||||
$appType = Get-LanguageString 'SettingDetails.selectMicrosoftEdgeApp'
|
||||
}
|
||||
elseif ($appConfig.uwpApp.appUserModelId -like 'Microsoft.KioskBrowser*') {
|
||||
$uwpAppType = 'kioskBrowser'
|
||||
$appType = Get-LanguageString 'SettingDetails.selectKioskBrowserApp'
|
||||
}
|
||||
else {
|
||||
$uwpAppType = 'storeApp'
|
||||
$appType = Get-LanguageString 'SettingDetails.selectStoreApp'
|
||||
}
|
||||
}
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskApplicationType'
|
||||
Value = $appType
|
||||
EntityKey = 'kioskApplicationType'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
$edgeKioskModeType = if ($appConfig.win32App.edgeKioskType -eq 'publicBrowsing') {
|
||||
Get-LanguageString 'SettingDetails.edgeKioskModeTypePublicBrowsingInPrivate'
|
||||
} else {
|
||||
Get-LanguageString 'SettingDetails.edgeKioskModeTypeDigitalSignage'
|
||||
}
|
||||
|
||||
if ($uwpAppType -eq 'win32App') {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.win32EdgeKioskUrl'
|
||||
Value = $appConfig.win32App.edgeKiosk
|
||||
EntityKey = 'edgeKiosk'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.edgeKioskModeType'
|
||||
Value = $edgeKioskModeType
|
||||
EntityKey = 'edgeKioskType'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.edgeKioskResetAfterIdleTimeInMinutesName'
|
||||
Value = $appConfig.win32App.edgeKioskIdleTimeoutMinutes
|
||||
EntityKey = 'edgeKioskIdleTimeoutMinutes'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
elseif ($uwpAppType -eq 'edge') {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.edgeKioskModeType'
|
||||
Value = $edgeKioskModeType
|
||||
EntityKey = 'edgeKioskType'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
elseif ($uwpAppType -eq 'kioskBrowser') {
|
||||
$show = Get-LanguageString 'BooleanActions.show'
|
||||
$hide = Get-LanguageString 'BooleanActions.hide'
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.win10KioskBrowserDefaultWebsiteName'
|
||||
Value = $obj.kioskBrowserDefaultUrl
|
||||
EntityKey = 'kioskBrowserDefaultUrl'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.win10KioskBrowserHomeButtonName'
|
||||
Value = if ($obj.kioskBrowserEnableHomeButton) { $show } else { $hide }
|
||||
EntityKey = 'kioskBrowserEnableHomeButton'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.win10KioskBrowserNavigationButtonName'
|
||||
Value = if ($obj.kioskBrowserEnableNavigationButtons) { $show } else { $hide }
|
||||
EntityKey = 'kioskBrowserEnableNavigationButtons'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.win10KioskBrowserEnableEndSessionButtonName'
|
||||
Value = if ($obj.kioskBrowserEnableEndSessionButton) { $show } else { $hide }
|
||||
EntityKey = 'kioskBrowserEnableEndSessionButton'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.edgeKioskResetAfterIdleTimeInMinutesName'
|
||||
Value = $obj.kioskBrowserRestartOnIdleTimeInMinutes
|
||||
EntityKey = 'kioskBrowserRestartOnIdleTimeInMinutes'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.win10BlockedWebsitesName'
|
||||
Value = $obj.kioskBrowserBlockedURLs -join $Context.ObjectSeparator
|
||||
EntityKey = 'kioskBrowserBlockedURLs'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.win10AllowedWebsitesName'
|
||||
Value = $obj.kioskBrowserBlockedUrlExceptions -join $Context.ObjectSeparator
|
||||
EntityKey = 'kioskBrowserBlockedUrlExceptions'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
elseif ($uwpAppType -eq 'storeApp') {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskModeAppStoreUrlOrManagedAppIdName'
|
||||
Value = $appConfig.uwpApp.name
|
||||
EntityKey = 'edgeKioskType'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Multi-app: app table + start-layout / taskbar / downloads rows
|
||||
if ($kioskModeType -eq 'multi') {
|
||||
$apps = @()
|
||||
foreach ($app in $appConfig.apps) {
|
||||
$kioskTypeLngId = switch ($app.appType) {
|
||||
'aumId' { 'kioskAppTypeAUMID' }
|
||||
'desktop' { 'kioskAppTypeDesktop' }
|
||||
'store' { 'kioskAppTypeStore' }
|
||||
default { 'kioskAppTypeUnknown' }
|
||||
}
|
||||
$kioskTileLngId = switch ($app.startLayoutTileSize) {
|
||||
'medium' { 'kioskTileMedium' }
|
||||
'small' { 'kioskTileSmall' }
|
||||
'wide' { 'kioskTileWide' }
|
||||
'large' { 'kioskTileLarge' }
|
||||
}
|
||||
$sep = $Context.PropertySeparator
|
||||
$autoLaunchStr = if ($app.autoLaunch -eq $true) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
|
||||
$apps += '{0}{1}{2}{3}{4}{5}{6}' -f $app.Name, $sep, (Get-LanguageString "SettingDetails.$kioskTypeLngId"), $sep, $autoLaunchStr, $sep, (Get-LanguageString "SettingDetails.$kioskTileLngId")
|
||||
}
|
||||
|
||||
if ($apps.Count -gt 0) {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskAppTableName'
|
||||
Value = $apps -join $Context.ObjectSeparator
|
||||
EntityKey = 'kioskApps'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
|
||||
$altLayout = if ($null -ne $appConfig.startMenuLayoutXml) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.alternativeStartLayoutName'
|
||||
Value = $altLayout
|
||||
EntityKey = 'alternativeStartLayout'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
if ($null -ne $appConfig.startMenuLayoutXml) {
|
||||
$xmlStr = try {
|
||||
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($appConfig.startMenuLayoutXml))
|
||||
} catch { $appConfig.startMenuLayoutXml }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskStartMenuLayoutXmlName'
|
||||
Value = $xmlStr
|
||||
EntityKey = 'startMenuLayoutXml'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
|
||||
$taskBar = if ($appConfig.showTaskBar) { Get-LanguageString 'BooleanActions.show' } else { Get-LanguageString 'BooleanActions.hide' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskShowTaskbarName'
|
||||
Value = $taskBar
|
||||
EntityKey = 'showTaskBar'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
$downloads = if ($appConfig.allowAccessToDownloadsFolder) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.win10KioskAccessDownloadsFolderName'
|
||||
Value = $downloads
|
||||
EntityKey = 'allowAccessToDownloadsFolder'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
# disallowDesktopApps blocks classic Win32/desktop apps on the multi-app
|
||||
# kiosk. No scraped label exists for this toggle, so use an ASCII literal
|
||||
# (the Yes/No value is still localized).
|
||||
$disallowDesktopApps = if ($appConfig.disallowDesktopApps) { Get-LanguageString 'SettingDetails.yes' } else { Get-LanguageString 'SettingDetails.no' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = 'Block desktop (Win32) apps'
|
||||
Value = $disallowDesktopApps
|
||||
EntityKey = 'disallowDesktopApps'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
|
||||
# Force-restart maintenance window
|
||||
$forceUpdateLng = if ($obj.windowsKioskForceUpdateSchedule) { 'BooleanActions.require' } else { 'BooleanActions.notConfigured' }
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskForceRestart'
|
||||
Value = Get-LanguageString $forceUpdateLng
|
||||
EntityKey = 'windowsKioskForceUpdateSchedule'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
if ($obj.windowsKioskForceUpdateSchedule) {
|
||||
try {
|
||||
$startDateObj = if ($obj.windowsKioskForceUpdateSchedule.startDateTime -is [DateTime]) {
|
||||
$tmp = $obj.windowsKioskForceUpdateSchedule.startDateTime
|
||||
if ($tmp.Kind -eq [DateTimeKind]::Utc) { $tmp.ToLocalTime() } else { $tmp }
|
||||
} else {
|
||||
Get-Date $obj.windowsKioskForceUpdateSchedule.startDateTime -ErrorAction Stop
|
||||
}
|
||||
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskStartDateTime'
|
||||
Value = ($startDateObj.ToShortDateString() + $Context.ObjectSeparator + $startDateObj.ToShortTimeString())
|
||||
EntityKey = 'startDateTime'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
$recurrenceType = switch ($obj.windowsKioskForceUpdateSchedule.recurrence) {
|
||||
'weekly' { 'kioskWeekly' }
|
||||
'monthly' { 'kioskMonthly' }
|
||||
default { 'kioskDaily' }
|
||||
}
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.kioskRecurrence'
|
||||
Value = Get-LanguageString "SettingDetails.$recurrenceType"
|
||||
EntityKey = 'recurrence'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
|
||||
if ($obj.windowsKioskForceUpdateSchedule.recurrence -eq 'weekly') {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.dayOfWeek'
|
||||
Value = Get-LanguageString "SettingDetails.$($obj.windowsKioskForceUpdateSchedule.dayofWeek)"
|
||||
EntityKey = 'dayofWeek'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
elseif ($obj.windowsKioskForceUpdateSchedule.recurrence -eq 'monthly') {
|
||||
Add-CustomSettingObject ([PSCustomObject]@{
|
||||
Name = Get-LanguageString 'SettingDetails.dayOfMonth'
|
||||
Value = $obj.windowsKioskForceUpdateSchedule.dayofMonth
|
||||
EntityKey = 'dayofMonth'
|
||||
Category = $category
|
||||
SubCategory = $null
|
||||
})
|
||||
}
|
||||
}
|
||||
catch { Write-Log "Failed to format kiosk force-update schedule: $($_.Exception.Message)" 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DocumentationRegistry]::RegisterHandler([WindowsKioskDocHandler]::new())
|
||||
Reference in New Issue
Block a user