222 lines
11 KiB
Markdown
222 lines
11 KiB
Markdown
# Configuration reference
|
||
|
||
Every field in `persona-engine.json`, and every finding code the validator can produce.
|
||
|
||
The authoritative schema is [`config/persona-engine.schema.json`](../config/persona-engine.schema.json)
|
||
(JSON Schema draft-07). A working example is
|
||
[`config/persona-engine.example.json`](../config/persona-engine.example.json), which is validated
|
||
against its own schema before every commit — if the example the documentation points at could not
|
||
pass, every reader's first run would fail.
|
||
|
||
For a task-oriented walkthrough of every condition type and operator with worked JSON examples, see
|
||
[RuleAuthoringGuide.md](RuleAuthoringGuide.md). This document is the terse reference; that one
|
||
teaches by example.
|
||
|
||
## Top level
|
||
|
||
| Field | Required | Notes |
|
||
| --- | --- | --- |
|
||
| `configVersion` | yes | Semantic version, `major.minor.patch`. A downgrade is a safety finding. |
|
||
| `metadata` | no | `owner`, `changeReference`, `description`. Free-form; not read by the engine. |
|
||
| `engine` | yes | Engine behaviour. |
|
||
| `dataSources` | yes | Which directory data may be retrieved. |
|
||
| `logging` | no | Audit output. |
|
||
| `personas` | yes | The declared persona catalogue. |
|
||
| `rules` | yes | Ordered business rules. |
|
||
|
||
## `engine`
|
||
|
||
| Field | Required | Default | Notes |
|
||
| --- | --- | --- | --- |
|
||
| `targetAttribute` | yes | — | The single attribute the engine may write. Must be a directory extension property and must appear in `approvedWritableAttributes`. |
|
||
| `approvedWritableAttributes` | yes | — | The allow-list. Comparison is **ordinal** — extension property names are case-sensitive in Graph. |
|
||
| `maxConditionDepth` | no | `5` | RE-004. Minimum 1, hard ceiling 10. |
|
||
| `summaryInterval` | no | `25` | Interim summary frequency. `0` suppresses interim summaries; a final summary always appears. |
|
||
| `defaultMembershipMode` | no | `direct` | Mode for membership conditions that do not specify one. |
|
||
| `evaluationErrorThreshold` | no | unset | Count of `EvaluationError` results above which the run reports exit code 4. Unset means report, do not fail. |
|
||
|
||
Setting `evaluationErrorThreshold` to `0` makes a single transient lookup failure fail the run. That
|
||
is occasionally what you want; it is rarely what you meant.
|
||
|
||
## `dataSources`
|
||
|
||
| Field | Required | Notes |
|
||
| --- | --- | --- |
|
||
| `groups.enabled` | yes | When false, no membership facet is retrieved. Rules needing it become `EvaluationError`. |
|
||
| `groups.membershipMode` | no | Pins a mode globally. **Leave it out unless you mean to restrict** — absent means "any mode is acceptable", and RE-007 makes mode a per-condition choice. Pinning it makes every per-condition override a `PE-SEM-014` warning. |
|
||
| `roles.enabled` | yes | Directory role assignments. |
|
||
| `roles.includeEligible` | no | PIM-eligible assignments. **Out of scope for v1**; no provider is implemented. |
|
||
|
||
The engine retrieves only the facets enabled rules actually reference. A configuration with no role
|
||
conditions never calls the role endpoint, so a tenant where role reads are unavailable can still run
|
||
property-only rules.
|
||
|
||
## `logging`
|
||
|
||
| Field | Required | Default | Notes |
|
||
| --- | --- | --- | --- |
|
||
| `destination` | no | `both` | `file`, `stream`, `both`, or `none`. `stream` writes records to the PowerShell Information stream. |
|
||
| `path` | no | `<current-directory>/logs/persona-engine-audit.ndjson` | NDJSON output file. One record per line. |
|
||
| `traceConditionValues` | no | `false` | Writes evaluated attribute values into audit records. |
|
||
| `acknowledgeConditionTracing` | no | `false` | **Required whenever `traceConditionValues` is true** (VR-003). |
|
||
|
||
## `personas`
|
||
|
||
The declared catalogue. A rule assigning a persona absent from this list is a `PE-SEM-010` error — the
|
||
catalogue is what stops a typo from writing a new persona value into the directory.
|
||
|
||
`Unclassified` and `EvaluationError` are processing results and may never be declared or assigned.
|
||
|
||
## `rules`
|
||
|
||
| Field | Required | Notes |
|
||
| --- | --- | --- |
|
||
| `id` | yes | Unique. Appears in every audit record; this is how a decision is traced to its rule. |
|
||
| `name` | yes | Human-readable. |
|
||
| `description` | yes | Why the rule exists. Required, because a rule nobody can explain cannot be safely changed. |
|
||
| `enabled` | yes | Disabled rules are excluded from evaluation but still appear in summaries with zero matches. |
|
||
| `priority` | yes | Unique integer. **Lower evaluates first.** |
|
||
| `persona` | yes | Must appear in `personas`. |
|
||
| `match` | yes | The root condition group. |
|
||
| `tags`, `owner`, `changeReference`, `effectiveDate`, `notes`, `testCases` | no | Metadata. `effectiveDate` is **not** evaluated — a date-dependent decision would break determinism. |
|
||
|
||
Priorities must be unique among enabled rules. The engine breaks ties by rule ID so results stay
|
||
deterministic, but the resulting order is an accident rather than a decision, so `PE-SEM-002` blocks it.
|
||
|
||
## Condition groups and conditions
|
||
|
||
A group has `operator` (`all` or `any`) and a `conditions` array. Each entry is either another group
|
||
or a condition.
|
||
|
||
| Field | Applies to | Notes |
|
||
| --- | --- | --- |
|
||
| `type` | all | `property`, `membership`, or `role`. |
|
||
| `property` | `property` | One of the supported names below, or an extension property. |
|
||
| `operator` | all | See the operator table. |
|
||
| `value` | most | Single comparison value. |
|
||
| `values` | `in`, `notIn` | Comparison set. |
|
||
| `groupObjectIds` | `membership` | Group Object IDs. Names are mutable; IDs are not (RE-009). |
|
||
| `roleIds` | `role` | Role **template** IDs, which are stable across tenants. |
|
||
| `membershipMode` | `membership` | `direct` or `transitive`, per condition. |
|
||
| `caseSensitive` | — | Reserved. Not implemented in v1; the schema accepts the key so a later version needs no breaking change. |
|
||
|
||
### Supported properties
|
||
|
||
`AccountObjectId` · `UserPrincipalName` · `DisplayName` · `UserType` · `AccountEnabled` ·
|
||
`CompanyName` · `JobTitle` · `Department`
|
||
|
||
Plus any directory extension property named `extension_<32-hex-app-id>_<name>`. Anything else is
|
||
`PE-SEM-015`: unsupported properties are never retrieved, so the condition would compare against a
|
||
permanently absent value and quietly never match.
|
||
|
||
### Operators (RE-005)
|
||
|
||
| Operator | Applies to | Notes |
|
||
| --- | --- | --- |
|
||
| `equals`, `notEquals` | property | Case-insensitive (RE-006). |
|
||
| `contains`, `notContains` | property | Case-insensitive substring. |
|
||
| `startsWith`, `endsWith` | property | Case-insensitive. |
|
||
| `matchesRegex` | property | Pattern compiled at validation time. An invalid pattern is `PE-SEM-016`, not a runtime failure. |
|
||
| `in`, `notIn` | property | Requires `values`. |
|
||
| `isNull`, `isNotNull` | property | Tests presence. **Must not carry a value** — it would be silently ignored (`PE-SEM-009`). |
|
||
| `memberOf`, `notMemberOf` | membership, role | Requires `groupObjectIds` or `roleIds`. |
|
||
|
||
Null and absent properties are treated as empty for ordinary comparisons and never cause an
|
||
evaluation failure (FR-012). Intentional null matching uses `isNull` / `isNotNull`.
|
||
|
||
## Tri-state evaluation
|
||
|
||
Conditions return `True`, `False`, or `Unknown`. `Unknown` means required data could not be
|
||
retrieved, and it propagates:
|
||
|
||
| Group | Contains | Result |
|
||
| --- | --- | --- |
|
||
| `all` | any `False` | `False` |
|
||
| `all` | only `True` plus at least one `Unknown` | `Unknown` |
|
||
| `any` | any `True` | `True` |
|
||
| `any` | only `False` plus at least one `Unknown` | `Unknown` |
|
||
|
||
An `Unknown` at a rule's root makes the account `EvaluationError`: the stored persona is preserved
|
||
and no write is attempted (FR-013, FR-014).
|
||
|
||
## Validation layers
|
||
|
||
Run in order, stopping at the first that produces `Error` findings. Running semantic checks over a
|
||
structurally invalid document yields noise, not signal.
|
||
|
||
| Layer | Mechanism | Codes |
|
||
| --- | --- | --- |
|
||
| 1 Syntax | `ConvertFrom-Json` | `PE-SYN-nnn` |
|
||
| 2 Schema | `Test-Json -SchemaFile` | `PE-SCH-nnn` |
|
||
| 3 Semantic | PowerShell checks | `PE-SEM-nnn` |
|
||
| 4 Safety | PowerShell checks | `PE-SAF-nnn` |
|
||
|
||
Codes are stable. Pipelines and runbooks match on them, so a code is never reused for a different
|
||
condition and never renumbered.
|
||
|
||
### Syntax — `PE-SYN`
|
||
|
||
| Code | Condition |
|
||
| --- | --- |
|
||
| `PE-SYN-001` | Configuration file not found, or is not a file |
|
||
| `PE-SYN-002` | File exists but could not be read |
|
||
| `PE-SYN-003` | File is not valid JSON |
|
||
|
||
### Schema — `PE-SCH`
|
||
|
||
| Code | Condition |
|
||
| --- | --- |
|
||
| `PE-SCH-001` | Document violates the schema |
|
||
| `PE-SCH-002` | Schema file not found |
|
||
| `PE-SCH-003` | Schema file exists but is not valid JSON Schema |
|
||
|
||
`PE-SCH-003` exists because of V-5a: `Test-Json` returns `$true` when the schema itself cannot be
|
||
parsed. A wrapper trusting the return value would report every configuration as schema-valid against
|
||
a schema that never ran.
|
||
|
||
### Semantic — `PE-SEM` (VR-002)
|
||
|
||
| Code | Condition | Severity |
|
||
| --- | --- | --- |
|
||
| `PE-SEM-001` | Duplicate rule ID | Error |
|
||
| `PE-SEM-002` | Duplicate priority among enabled rules | Error |
|
||
| `PE-SEM-003` | No rules, or no enabled rules | Error |
|
||
| `PE-SEM-004` | Blank target attribute | Error |
|
||
| `PE-SEM-005` | Target attribute absent from the approved list | Error |
|
||
| `PE-SEM-006` | Rule references a disabled data source | Error |
|
||
| `PE-SEM-007` | `memberOf` / `notMemberOf` with no group or role IDs | Error |
|
||
| `PE-SEM-008` | `in` / `notIn` with no `values` | Error |
|
||
| `PE-SEM-009` | `isNull` / `isNotNull` carrying a comparison value | Error |
|
||
| `PE-SEM-010` | Persona not in the declared catalogue | Error |
|
||
| `PE-SEM-011` | `Unclassified` used as a rule persona | Error |
|
||
| `PE-SEM-012` | Nesting deeper than `maxConditionDepth` | Error |
|
||
| `PE-SEM-013` | `maxConditionDepth` outside 1–10 | Error |
|
||
| `PE-SEM-014` | Condition mode differs from an explicitly pinned global mode | Warning |
|
||
| `PE-SEM-015` | Unsupported property name | Error |
|
||
| `PE-SEM-016` | Invalid regular expression | Error |
|
||
|
||
Several of these are also enforced by the schema. The overlap is deliberate: layer 2 can be bypassed
|
||
with `-SchemaPath`, and V-5a showed an unparseable schema passes silently. Anything that can
|
||
misclassify a privileged account is checked twice.
|
||
|
||
### Safety — `PE-SAF` (VR-003)
|
||
|
||
| Code | Condition | Severity |
|
||
| --- | --- | --- |
|
||
| `PE-SAF-001` | Blank target attribute | Error enforcing, Warning in preview |
|
||
| `PE-SAF-002` | Approved list contains a non-extension attribute | Error |
|
||
| `PE-SAF-003` | Enabled rules need data the data sources do not provide | Error |
|
||
| `PE-SAF-004` | `configVersion` lower than the deployed version | Error enforcing, Warning in preview |
|
||
| `PE-SAF-005` | Rules removed or reordered with no version change | Error enforcing, Warning in preview |
|
||
| `PE-SAF-006` | Tracing enabled without acknowledgement | Error |
|
||
| `PE-SAF-007` | Save would overwrite an existing configuration with no backup | Error |
|
||
|
||
`PE-SAF-004` and `PE-SAF-005` need `-PreviousConfigPath`. Without it they are **skipped**, and an
|
||
`Information` finding says so — silence would be read as approval.
|
||
|
||
## Escalation (VR-005)
|
||
|
||
`Error` blocks execution and saving. `Warning` blocks only under `-TreatWarningsAsErrors`.
|
||
`Information` never blocks. Passing `-TreatWarningsAsErrors` does not change a finding's severity;
|
||
it changes the caller's tolerance for it.
|