mirror of
https://github.com/Micke-K/IntuneManagement.git
synced 2026-09-29 03:15:38 +02:00
IntuneManagement 4.0.0-beta1
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
# Settings
|
||||
|
||||
Settings are the tool's configuration: everything the Settings dialog shows, plus a
|
||||
handful of hidden keys and per-feature state. This page is about how they are stored,
|
||||
how they are addressed, and how to drive them from automation without touching the
|
||||
machine they run on.
|
||||
|
||||
For cache and logging, see [Settings Cache Logging](SettingsCacheLogging.md).
|
||||
|
||||
|
||||
Every registered key, with type, default and description, is in
|
||||
[SettingsReference.md](SettingsReference.md) - generated from the code, so it is always current.
|
||||
|
||||
## Three layers
|
||||
|
||||
Addressing a setting used to mean knowing where it lives. It does not any more:
|
||||
|
||||
| Layer | Where | Addressed by | Functions |
|
||||
| --- | --- | --- | --- |
|
||||
| storage | `Internal/Core.ps1` | **path** (`SubPath` + key) | `Get-SettingStoreValue`, `Save-SettingStoreValue`, `Remove-SettingStoreValue` |
|
||||
| resolver | `Internal/Settings.ps1` | **key** | `Get-SettingValue`, `Set-SettingValue`, `Remove-SettingValue`, `Test-SettingValueConfigured`, `Resolve-SettingValue` |
|
||||
| public | `Public/*Setting*.ps1` | **key** | `Get-IMSetting`, `Set-IMSetting`, `Remove-IMSetting`, `Get-IMSettingDefinition`, plus the store cmdlets below |
|
||||
|
||||
The dependency direction is one-way: public calls the resolver, the resolver calls
|
||||
storage. Nothing goes the other way.
|
||||
|
||||
**Prefer the resolver.** It looks the key up in the registered definitions and derives
|
||||
the `SubPath` from the registration, so a write cannot land somewhere a read never
|
||||
looks. Every SubPath bug the project has had - `GraphPageSize` resolving differently in
|
||||
headless sessions, the bulk-export remember-last-used round trip being silently dead,
|
||||
`DefaultCloud` - was a hand-written path on one side only. `Tools/Audit-Settings.ps1`
|
||||
reports remaining path-addressed access to a registered key as a warning.
|
||||
|
||||
`Get-SettingValue` lives in `Internal/Core.ps1` rather than with the rest of the
|
||||
resolver because `Write-Log` reads `LogFile` through it during preload, long before
|
||||
`Internal/` is dot-sourced.
|
||||
|
||||
### Registration
|
||||
|
||||
A setting exists because some file called `Add-SettingsObject`:
|
||||
|
||||
```powershell
|
||||
Add-SettingsObject -Key "GraphPageSize" -Section "IntuneManager" -SubPath "IntuneManager" `
|
||||
-Title "Page size" -Type "List" -DefaultValue "0" -ItemsSource $sizes
|
||||
```
|
||||
|
||||
`-SubPath` is the storage path. With no `-SubPath` the `-Section` is used, except
|
||||
`General`, which stores at the root of the store. `-Type` is a **UI editor hint**
|
||||
(`Boolean`, `Int`, `File`, `List`, `Text`), not a storage type - everything on disk is
|
||||
a string.
|
||||
|
||||
`Get-IMSettingDefinition` lists the registrations; `Get-IMSettingDefinition -Section
|
||||
IntuneManager` narrows to one section.
|
||||
|
||||
## Three store modes
|
||||
|
||||
The mode is decided at the very top of `Internal/Core.ps1`, before anything can log:
|
||||
|
||||
| Mode | Backing | Default when |
|
||||
| --- | --- | --- |
|
||||
| `Registry` | `HKCU:\Software\IntuneManagement` | on Windows |
|
||||
| `Json` | `IM_SETTINGS_FILE`, else `LocalApplicationData/IntuneManagement/Settings.json` | off Windows, or `IM_SETTINGS_FILE` is set |
|
||||
| `Memory` | a tree with no file behind it - reads and writes work, nothing touches disk | never; opt in explicitly |
|
||||
|
||||
Environment variables, read once at load:
|
||||
|
||||
| Variable | Effect |
|
||||
| --- | --- |
|
||||
| `IM_SETTINGS_STORE` | `Memory`, `Json` or `Registry`. `Registry` off Windows falls back to `Json`. An unrecognized value falls back to the platform default and is warned about once logging works. |
|
||||
| `IM_SETTINGS_FILE` | the `Json` store's file. Created if missing. Implies `Json`. |
|
||||
|
||||
Memory mode is not a fourth code path: the storage primitives take their JSON branch
|
||||
whenever a settings **object** exists, and persist only when a settings **file** exists
|
||||
too. Memory mode is an object with no file.
|
||||
|
||||
`Get-IMSettingsStore` reports what the store actually is - which is not always what was
|
||||
requested, because a `Json` store whose file cannot be read falls back to the registry,
|
||||
and off Windows that leaves no store at all (`Mode = "None"`: reads return registered
|
||||
defaults, writes go nowhere).
|
||||
|
||||
```powershell
|
||||
Get-IMSettingsStore # Mode / Path / Persisted / ValueCount / RequestedMode
|
||||
Get-IMSettingsStore -IncludeValues # ... plus every SubPath/Key/Value row
|
||||
```
|
||||
|
||||
## Public API
|
||||
|
||||
```powershell
|
||||
Get-IMSetting GraphPageSize # the effective value
|
||||
Get-IMSetting GraphPageSize -Detailed # value + Source (Default/Global/Tenant) + Type + Default + ...
|
||||
Get-IMSetting ExportFolder -Scope Global # ignore any tenant override
|
||||
Get-IMSetting ExportFolder -Scope Tenant -TenantID <guid>
|
||||
|
||||
Set-IMSetting GraphPageSize 999
|
||||
Set-IMSetting ExportFolder '\\server\intune\contoso' -Scope Tenant
|
||||
Set-IMSetting UseBatchAPI $false -PassThru # returns the resolved value afterwards
|
||||
|
||||
Remove-IMSetting ExportFolder -Scope Tenant # revert to the global value
|
||||
Remove-IMSetting ExportFolder # revert to the registered default
|
||||
```
|
||||
|
||||
`Set-IMSetting` and `Remove-IMSetting` **persist by default** - to the registry or the
|
||||
settings file, whichever the active store is. There is no opt-in switch. They support
|
||||
`-WhatIf`, they log the store and path they wrote to, and `Get-IMSettingsStore` lets a
|
||||
script assert on the store before writing anything. If a script must not write to the
|
||||
machine it runs on, switch to a memory store first.
|
||||
|
||||
`Get-IMSetting` accepts a key from the pipeline, so `Get-IMSettingDefinition -Section
|
||||
IntuneManager | Get-IMSetting -Detailed` dumps a whole section with provenance.
|
||||
|
||||
### Scope and precedence
|
||||
|
||||
A setting can be written globally or for one tenant. Reads take the tenant value first:
|
||||
|
||||
```text
|
||||
tenant value -> global value -> registered DefaultValue
|
||||
```
|
||||
|
||||
`-Scope Tenant` with no `-TenantID` uses the connected tenant, and **fails loudly** if
|
||||
there is none - writing the global value instead would change every tenant when the
|
||||
caller asked for one.
|
||||
|
||||
`Get-IMSetting -Detailed` reports which level answered in `Source`. That is computed
|
||||
fresh from the store on every call; the `.Value` cached on a definition object by
|
||||
`Get-SettingValue` is a per-session artifact of whoever read it last.
|
||||
|
||||
A **scoped** read (`-Scope Global` / `-Scope Tenant`) reports the absence of a value at
|
||||
that level as `$null` with `Source = 'NotSet'`. It does not fall back to the registered
|
||||
default, because "this tenant does not override the setting" and "this tenant overrides
|
||||
it to the same value as the default" are different facts. Only the default `-Scope
|
||||
Effective` falls back, which is what the application itself resolves.
|
||||
|
||||
### Keys with no registration
|
||||
|
||||
The hidden keys (`ExportReplaceTokens`) and the per-feature state namespaces are not
|
||||
registered with `Add-SettingsObject`, so they have no path to take from a registration.
|
||||
`-SubPath` supplies it, and is accepted on **all three** cmdlets - a key that can be
|
||||
written this way can be read and removed the same way:
|
||||
|
||||
```powershell
|
||||
Set-IMSetting ExportReplaceTokens 'TenantId' -SubPath 'IntuneManager'
|
||||
Get-IMSetting ExportReplaceTokens -SubPath 'IntuneManager'
|
||||
Remove-IMSetting ExportReplaceTokens -SubPath 'IntuneManager'
|
||||
```
|
||||
|
||||
Such a key has no registered default and no declared type, so its `Source` is `Tenant`,
|
||||
`Global` or `NotSet` - never `Default` - and the value comes back as the string the
|
||||
store holds. Tenant precedence works as usual (`-Scope Tenant`, `-TenantID`), which is
|
||||
how a per-tenant `ExportReplaceTokens` is set.
|
||||
|
||||
For a **registered** key `-SubPath` is reported in the log and ignored: the registration
|
||||
decides the path, so a write cannot be aimed somewhere the application never reads.
|
||||
|
||||
## Automation: a run that reads and writes nothing on the worker
|
||||
|
||||
The problem: a runbook on a shared Hybrid Worker has no business reading, let alone
|
||||
writing, that worker's `HKCU` hive or settings file. A memory store solves it, and a
|
||||
checked-in settings file makes the configuration reviewable:
|
||||
|
||||
```powershell
|
||||
Import-Module .\IntuneManagement.psd1
|
||||
|
||||
# Nothing from here on touches the worker's registry or settings file.
|
||||
Use-IMSettingsStore -Memory
|
||||
Import-IMSettingsStore .\config\documentation-run.json
|
||||
|
||||
# Whatever the file did not cover.
|
||||
Set-IMSetting GraphPageSize 999
|
||||
Set-IMSetting UseBatchAPI $true
|
||||
|
||||
Connect-IMIntuneManagement -Provider OAuth -TenantId $tid -AppId $appId -Certificate $cert
|
||||
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup DeviceConfiguration
|
||||
```
|
||||
|
||||
`IM_SETTINGS_STORE=Memory` in the environment does the same thing from the first line
|
||||
of the module load, which matters if anything the module logs during load would
|
||||
otherwise resolve against the real store.
|
||||
|
||||
| Cmdlet | Use |
|
||||
| --- | --- |
|
||||
| `Use-IMSettingsStore -Memory [-Seed]` | switch to memory. `-Seed` copies the persisted values in first, so the session starts from the real configuration and then diverges without writing back. |
|
||||
| `Use-IMSettingsStore <path>` | switch to a `Json` store at that file, creating it if missing. |
|
||||
| `Use-IMSettingsStore -Registry` | switch back to `HKCU`. Windows only. |
|
||||
| `Export-IMSettingsStore <path>` | write the whole current store to a JSON file, whatever mode it is in - including a registry store. |
|
||||
| `Import-IMSettingsStore <path>` | load a settings file into the **active** store, value by value. |
|
||||
|
||||
Two things about `Import-IMSettingsStore` worth knowing:
|
||||
|
||||
- It **merges** into what is already there rather than replacing it. For a clean slate,
|
||||
`Use-IMSettingsStore -Memory` (without `-Seed`) first, then import into that.
|
||||
- It goes through the storage layer, so one implementation is correct for all three
|
||||
modes: values persist in `Json` mode, land in the registry in `Registry` mode, stay in
|
||||
memory in `Memory` mode. Which also means importing into a registry store **writes to
|
||||
the registry**.
|
||||
- Keys with no registration are imported anyway - the hidden keys and per-feature state
|
||||
namespaces are real - but they are listed in a warning, because a typo looks
|
||||
identical and would otherwise silently do nothing.
|
||||
|
||||
## Stored shape
|
||||
|
||||
Everything is a string. Booleans are PascalCase `"True"`/`"False"`, which is what
|
||||
`$true.ToString()` produces and what every existing store contains.
|
||||
`Set-SettingValue` coerces through `Format-SettingStoreValue`, so `$true`, `"true"` and
|
||||
`"TRUE"` all land in the one canonical shape, and a value written through the resolver
|
||||
is indistinguishable from one written by the Settings dialog.
|
||||
|
||||
Watch out for `[bool]"False"` - it is `$true` in PowerShell, as any non-empty string is.
|
||||
Stored Booleans are compared to `"true"`, never cast. Both the reader and the writer do
|
||||
this the same way on purpose.
|
||||
|
||||
An **empty string** counts as "not set" on read, so clearing a text box in the Settings
|
||||
dialog restores the registered default rather than persisting `""`. "Is it configured"
|
||||
is therefore a different question from "does it have a value", and
|
||||
`Test-SettingValueConfigured` (`Get-IMSetting` does not expose it) answers it by asking
|
||||
whether the value exists at that path at all.
|
||||
|
||||
## Legacy layout note
|
||||
|
||||
A `SubPath` with more than one level (`<tenantid>\IntuneManager`) used to be stored by
|
||||
the JSON/memory store as a **single flat property** literally named
|
||||
`"<tenantid>\IntuneManager"`, while the registry stored the same path as nested keys.
|
||||
The cause was `"a\b".Split(@('/','\'))` not splitting at all - PowerShell binds a string
|
||||
array to a different `String.Split` overload and hands back the whole path as one
|
||||
element. It is now `[char[]]`-cast and splits properly.
|
||||
|
||||
Existing settings files keep working: `Get-SettingsTreeNode` resolves a flat property
|
||||
first and writes back to it when it finds one, so no tenant-specific value is orphaned
|
||||
by the fix. New values nest.
|
||||
|
||||
## Testing
|
||||
|
||||
Settings leak from the developer machine into tests, and tests leak into the developer
|
||||
machine. `Tests/TestBootstrap.ps1` has the two helpers:
|
||||
|
||||
```powershell
|
||||
Describe 'x' {
|
||||
BeforeEach { $script:saved = Use-TestSettingsStore }
|
||||
AfterEach { Restore-TestSettingsStore -State $script:saved }
|
||||
|
||||
InModuleScope IntuneManagement { It 'y' { ... } }
|
||||
}
|
||||
```
|
||||
|
||||
`Use-TestSettingsStore` swaps in an empty memory store (so every key resolves to its
|
||||
registered default) and returns the previous state for `Restore-TestSettingsStore`.
|
||||
|
||||
**The hooks must be declared at `Describe` level, wrapping `InModuleScope`.** Pester 4
|
||||
silently never runs a `BeforeEach`/`AfterEach` declared *inside* an `InModuleScope`
|
||||
block - nothing fails, the tests just run against the developer's real store and write
|
||||
to it. `Static.Tests.ps1` has a gate for this with a shrink-only allowlist of the files
|
||||
that still get it wrong.
|
||||
|
||||
`Tests/SettingsStore.Tests.ps1` covers the store modes, path resolution, value shape,
|
||||
provenance, portability and the public surface.
|
||||
|
||||
## Auditing
|
||||
|
||||
`Tools/Audit-Settings.ps1` cross-references every registration against every read and
|
||||
write. It is wired into `Static.Tests.ps1` as a gate; see its help for the full
|
||||
taxonomy. Errors: orphan registrations, engine-consumed-but-UI-registered (breaks
|
||||
headless sessions), SubPath-mismatched access, duplicate registrations. Warnings:
|
||||
UI-only-but-engine-registered, and registered keys addressed by path instead of through
|
||||
the resolver.
|
||||
|
||||
```powershell
|
||||
.\Tools\Audit-Settings.ps1
|
||||
.\Tools\Audit-Settings.ps1 -IncludeUndefined # also keys read but never registered
|
||||
```
|
||||
Reference in New Issue
Block a user