Files
personaEngine2/specs/001-persona-engine/spec.md
T
dave cdc6bb33d3 Implement Stage A: rule engine, validation, audit, and safety gates
Completes 109 of 121 tasks. Every remaining task needs a tenant connection
(T055, T056, T101-T103) or an Azure Automation account (T115-T121).

  354 offline Pester tests      PASS
  Engine purity (Principle IV)  PASS
  Sanitization (SC-013)         PASS  (156 files)
  Graph module loaded in tests  none  (SC-008 holds)

What landed
  - Four-layer configuration validation with stable finding codes, covering
    every VR-002 and VR-003 condition, plus a 23-fixture invalid-config corpus
  - Run loop, audit records (NDJSON through a single sink), summaries,
    reconciliation, and exit codes 0-6
  - Persistence behind a single write-body builder whose result always has
    exactly one key
  - Invoke-PersonaEngine.ps1 and Edit-PersonaEngineConfig.ps1
  - Six docs, two pipelines, traceability matrix, V-5a and sanitization records

Three deviations from tasks.md, each recorded in its status block

  T033 is not in Resolve-UserPersona. evaluationErrorThreshold is run-level
  state and the rule engine is pure; a counter there would break Principle IV.
  It lives in New-PersonaRunCounter and is applied in the run loop.

  A new src/Engine/ layer holds Invoke-PersonaEngineRun. The entry script
  imports the manifest, which requires Microsoft.Graph.Authentication, so a
  loop living only inside it could not run on a machine without the Graph SDK
  and SC-004 could not be proven at all. The entry script is now a thin
  wrapper and what ships is what is tested.

  The invalid-config corpus is generated by a committed script, with the
  generated fixtures committed too, so a reviewer sees the fixture in the diff.

Defects found by running the code, not by reading it

  Group and role ID lists were double-wrapped: @(Get-PersonaGroupIdPage ...)
  around a comma-returned array collapsed every membership list into one
  bogus space-joined entry. That is a silent false non-match, exactly what
  FR-013 exists to prevent.

  A 403 whose status appears only in the exception message parsed as $null,
  which the retry policy treats as a transport error - five requests per
  account against a tenant already refusing. Status extraction now falls back
  to the message text, bounded to 400-599.

  The sanitization scan walked tracked files only, so it covered 34 of 156
  files and none of this phase's code. It now scans untracked non-ignored
  files too, and a negative control confirms it catches a planted leak.

  Test-Json reports one error per violating location, not first-failure-only
  as the V-5a draft claimed. Record and pin corrected.

Enforcement remains blocked on the V-4 security sign-off.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 21:48:19 -04:00

35 KiB

Feature Specification: Persona Engine

Feature Branch: 001-persona-engine

Created: 2026-08-20

Status: Draft

Input: Derived from the approved Phase 0 baseline, Persona-Engine-Developer-Handoff.txt (document status: Approved Phase 0 baseline; classification: Generic / Sanitized).

Summary: Deterministic, configuration-driven persona classification for Microsoft Entra ID user objects. The system evaluates every in-scope user account against an ordered, JSON-defined rule set, assigns exactly one persona, and updates a single approved persona attribute only when the calculated value differs from the stored value.


User Scenarios & Testing (mandatory)

User Story 1 - Preview classification without changing the directory (Priority: P1)

An identity administrator runs the engine against the tenant in preview mode. Every in-scope user is retrieved, evaluated against the configured rules, and reported with its current persona, its calculated persona, and the rule that produced it. Nothing in the directory changes.

Why this priority: This is the MVP and the primary safety control. Impact analysis must be possible before any write capability exists, and the project's initial connected development stage is explicitly read-only. Delivered alone, this story provides complete classification visibility with zero risk to the tenant.

Independent Test: Run the engine with -WhatIf against a tenant using a read-only identity. Verify a per-user result is displayed for every account and that the write adapter receives zero calls.

Acceptance Scenarios:

  1. Given a valid configuration and a read-only identity, When the engine runs in preview mode, Then every in-scope user is evaluated and reported, and no directory write is issued.
  2. Given a user whose calculated persona differs from the stored persona, When the engine runs in preview mode, Then the result is reported as WouldUpdate with both values and the matched rule ID.
  3. Given a user whose calculated persona equals the stored persona, When the engine runs in preview mode, Then the result is reported as unchanged and no write is proposed.
  4. Given preview mode is active, When the run completes, Then logs and summaries are produced exactly as in enforcement mode.

User Story 2 - Define classification rules without changing code (Priority: P1)

A security engineer expresses business classification logic as ordered rules in a JSON configuration file. Rules combine user properties, group membership, and directory-role assignments using nested all / any groups. No PowerShell source is modified.

Why this priority: Configuration-driven behaviour is a core design principle and a precondition for every other story. Without it the engine cannot classify anything.

Independent Test: Author a configuration containing multiple ordered rules and evaluate it against synthetic user records offline, with no tenant connectivity, confirming the expected persona for each fixture.

Acceptance Scenarios:

  1. Given a rule set with unique numeric priorities, When a user is evaluated, Then rules are evaluated in ascending priority order.
  2. Given a user matching rules at priority 10 and priority 20, When evaluation runs, Then the persona from priority 10 is assigned and evaluation stops.
  3. Given all enabled rules evaluate successfully and none match, When evaluation completes, Then the calculated persona is Unclassified.
  4. Given a rule with a nested any group inside an all group, When evaluation runs, Then the composed logic resolves correctly within the configured depth limit.
  5. Given a disabled rule, When evaluation runs, Then that rule is skipped and excluded from the enabled rule count.

User Story 3 - Preserve existing values when evaluation cannot be trusted (Priority: P1)

When required data for a user cannot be reliably retrieved — for example a group lookup fails — the engine records EvaluationError for that account, preserves the stored persona, and continues processing other users.

Why this priority: Fail-safe behaviour is a core design principle. An unavailable data source must never be silently interpreted as a non-match, because that would misclassify privileged accounts.

Independent Test: Inject a group-lookup failure for a specific synthetic user and verify that user receives EvaluationError, no write is attempted for them, and the run continues.

Acceptance Scenarios:

  1. Given required group data cannot be retrieved for a user, When that user is evaluated, Then the result is EvaluationError and the stored persona is left unchanged.
  2. Given a user has received EvaluationError, When persistence is considered, Then no write is attempted for that user.
  3. Given some users receive EvaluationError, When the run completes, Then the final status reports the number of affected accounts.

User Story 4 - Observe progress and reconcile results (Priority: P2)

An operator watching a run sees each user's result appear immediately, receives a rule-by-rule summary table at a configurable interval, and receives a final summary whose totals reconcile.

Why this priority: Operational visibility for long tenant-wide runs. Valuable but not required for correct classification.

Independent Test: Run against a synthetic set of 60 users with a summary interval of 25 and confirm two interim summaries, one final summary, and a passing reconciliation check.

Acceptance Scenarios:

  1. Given a user has been processed, When processing completes for that user, Then the result is displayed immediately rather than buffered to the end.
  2. Given a summary interval greater than zero, When that many users have been processed, Then a table of all business rules and their counts is displayed.
  3. Given a summary interval of zero, When the run proceeds, Then no interim summaries are displayed but the final summary is still displayed.
  4. Given any summary is produced, When totals are checked, Then Processed = Matched + Unclassified + EvaluationError; a mismatch is logged as an engine defect.

User Story 5 - Validate and edit configuration safely (Priority: P2)

A configuration owner validates a configuration file, edits rules interactively, tests rules against synthetic sample users, compares against another configuration, and saves only after validation passes — with a backup taken first.

Why this priority: Makes the configuration-driven model usable and safe in practice, but the engine can be exercised with a hand-authored file first.

Independent Test: Point the configuration tool at a deliberately malformed file and confirm each validation layer reports actionable findings and blocks the save.

Acceptance Scenarios:

  1. Given a configuration with duplicate rule priorities, When validation runs, Then an Error-severity finding is produced identifying the rule IDs and the save is blocked.
  2. Given an edit session with unsaved valid changes, When the file is saved over an existing configuration, Then a timestamped backup or save-as output is produced first.
  3. Given synthetic sample users are supplied, When rules are tested, Then the resulting persona for each sample is reported without any tenant connection.
  4. Given a valid configuration, When validation runs, Then no Error findings are produced and the file is accepted.

User Story 6 - Block invalid configuration in a pipeline (Priority: P2)

A pipeline owner runs configuration validation as a non-interactive build step that fails the pipeline on invalid configuration.

Why this priority: Prevents an invalid or unsafe configuration reaching the automation environment. Depends on Story 5's validation engine.

Independent Test: Execute validation in non-interactive mode against a known-bad configuration and assert a non-zero process exit code with no prompt.

Acceptance Scenarios:

  1. Given an invalid configuration, When validation runs non-interactively, Then the process returns a non-zero exit code and never prompts.
  2. Given a configuration producing only Warning findings and warnings-as-errors is enabled, When validation runs, Then the process returns a non-zero exit code.
  3. Given a valid configuration, When validation runs non-interactively, Then the process returns zero.

User Story 7 - Audit any classification decision (Priority: P2)

An auditor reconstructs why any account received its persona, correlating the result to a UPN, Account Object ID, matched rule, configuration version, and run ID.

Why this priority: Auditability is a stated non-functional requirement and a precondition for the security review that gates production write access.

Independent Test: Complete a run, then select an arbitrary user event from the structured log and confirm every required correlation field is present and internally consistent with the run-level event.

Acceptance Scenarios:

  1. Given a completed run, When a user event is inspected, Then it contains the run ID, UPN, and Account Object ID.
  2. Given a changed or proposed value, When the event is inspected, Then it identifies the rule that produced the persona.
  3. Given a completed run, When the run-level event is inspected, Then it records the configuration version and configuration file hash.

User Story 8 - Enforce changes in production (Priority: P3)

Once security review and approval are complete, the engine runs in enforcement mode and writes the calculated persona to the approved attribute — only for accounts whose value actually changed, and only that attribute.

Why this priority: Deliberately last. The baseline states write permissions are not granted during the initial read-only development stage, and enforcement is gated on preview evidence and security approval.

Independent Test: In a controlled set, change one account's inputs so its persona changes, run in enforcement mode, and verify the request body contains only the approved attribute and only that account was written.

Acceptance Scenarios:

  1. Given evaluation succeeded and the calculated value differs from the stored value, When enforcement mode runs and the operation is confirmed, Then the approved attribute is updated.
  2. Given the calculated value equals the stored value, When enforcement mode runs, Then no write is issued.
  3. Given any write is issued, When the request body is inspected, Then it contains only the approved target attribute.
  4. Given a target attribute absent from the approved writable attribute list, When the configuration is validated, Then execution is blocked before any connection is made.

User Story 9 - Trace the values behind every rule decision (Priority: P3)

A rule author or developer runs the engine with the native diagnostic switch and sees, for each rule evaluated against a user, every value that fed the decision: which property was read and what it actually resolved to, which groups were checked and what the membership lookup returned, the operator and comparison value applied, and the boolean result of each condition, group, and rule.

Why this priority: Rule authoring and misclassification troubleshooting are otherwise guesswork — a normal-mode line reports that PER-800 matched, not why the three conditions before it did not. Valuable across the whole project life, but the engine classifies correctly without it, so it follows the rule engine rather than gating it.

Independent Test: Evaluate a synthetic user with a known rule set under the diagnostic switch and confirm the emitted trace names every property and group the rules reference, shows each resolved value, and produces per-condition results that compose to the reported outcome.

Acceptance Scenarios:

  1. Given the diagnostic switch is active, When a rule is evaluated for a user, Then the trace emits that rule's identifier, name, and priority, and for each condition the property path or membership source, the resolved value, the operator, the comparison value, and the condition result.
  2. Given a user is evaluated against several rules, When the trace is inspected, Then every rule evaluated for that user appears in priority order — not only the matched rule.
  3. Given evaluation stops at a matching rule, When the trace is inspected, Then it records which rule matched and that evaluation stopped there, and rules after it do not appear.
  4. Given a property is null or absent, When the condition is traced, Then the resolved value is shown explicitly as null rather than as an empty string, so null handling is distinguishable from an empty match.
  5. Given a membership lookup failed, When the condition is traced, Then the trace shows the retrieval status as failed rather than showing the membership as false.
  6. Given a nested condition group, When the trace is inspected, Then each group's type, its child results, and the group's own result are shown at their nesting depth.
  7. Given the diagnostic switch is active, When the run completes, Then the persona assigned to every user is identical to the persona assigned by the same run without the switch.
  8. Given the diagnostic switch is active, When write behaviour is examined, Then it is unchanged — the switch carries no read-only meaning.
  9. Given the diagnostic switch is active, When the trace is inspected, Then it contains no access token, authorization header, secret, or full raw directory response.

Edge Cases

  • Null or absent optional property: treated as empty for ordinary string comparisons. If it does not match the requested value the condition is false — this is not an evaluation failure. Intentional null matching uses dedicated isNull / isNotNull operators (FR-012).
  • Disabled accounts: remain in scope and are evaluated normally; accountEnabled is logged and may be used by configured rules (FR-011).
  • No enabled rules: semantic validation rejects the configuration before processing.
  • Condition nesting beyond the limit: rejected by both the runtime validator and the editor, whether it exceeds the configured maximum or the hard software ceiling.
  • Invalid regular expression in a rule: rejected at validation time, before execution.
  • memberOf with no group Object IDs, or in / notIn with no values, or isNull / isNotNull carrying a comparison value: semantic validation errors.
  • Rule producing an undefined or prohibited persona, or using Unclassified as an ordinary business-rule persona: semantic validation error.
  • Group rules enabled while group retrieval is globally disabled: safety validation error.
  • Reconciliation mismatch (Processed ≠ Matched + Unclassified + EvaluationError): logged as an engine defect, not silently ignored.
  • Overlapping scheduled runs writing conflicting values: must be prevented (see OTD-009).
  • Emergency-access accounts: identified by immutable user Object IDs inside normal rule definitions. There must be no separate hard-coded classification path (§8.8).
  • Service throttling and pagination: bounded retry with backoff for retryable responses; non-retryable authorization or validation failures are not retried indefinitely.

Requirements (mandatory)

Requirement identifiers are carried forward unchanged from the Phase 0 baseline (§7) to preserve traceability.

Functional Requirements

  • FR-001 — Load configuration: the engine MUST load a specified JSON configuration file.
  • FR-002 — Validate before processing: the engine MUST validate JSON syntax, schema, semantics, and safety constraints before connecting or processing users. A configuration with errors MUST NOT be used.
  • FR-003 — Authenticate: the solution MUST support interactive/delegated authentication for approved local development and managed-identity authentication for the automation environment. Authentication logic MUST be isolated behind an adapter.
  • FR-004 — Enumerate all users: production runs MUST support enumeration of all in-scope user accounts, including pagination.
  • FR-005 — Select required properties: the engine MUST retrieve the properties required by enabled rules plus operational fields required for logging and updates, avoiding unused properties where practical. Initial properties: id, userPrincipalName, displayName (diagnostics), userType, accountEnabled, companyName, jobTitle, department, and the configured persona attribute.
  • FR-006 — Retrieve related data: the engine MUST retrieve and cache related data required by enabled rules, including configured group membership and directory-role information.
  • FR-007 — Normalize data: raw directory responses MUST be converted to normalized internal objects before evaluation.
  • FR-008 — Evaluate ordered rules: rules MUST be sorted by priority and evaluated in that order.
  • FR-009 — Stop after first match: authoritative evaluation MUST stop at the first matching rule.
  • FR-010 — Unclassified result: if all enabled rules evaluate successfully and none match, the calculated persona MUST be Unclassified.
  • FR-011 — Disabled accounts: disabled accounts remain in scope and are evaluated normally.
  • FR-012 — Null handling: a null or absent optional property is treated as empty for ordinary string comparisons and MUST NOT cause an evaluation failure. isNull / isNotNull provide intentional null matching.
  • FR-013 — Group-data failure: if required group data cannot be reliably retrieved, the affected user MUST receive EvaluationError. An unknown membership result MUST NOT be assumed false.
  • FR-014 — Preserve current value on evaluation failure: a user with EvaluationError MUST retain the stored persona, with no write performed.
  • FR-015 — Compare current and calculated values: the engine MUST compare stored and calculated personas before persistence.
  • FR-016 — Write changed values only: a production write occurs only if evaluation completed successfully, the calculated value differs from the stored value, the target attribute is valid and approved, preview mode is not active, and the operation passes the confirmation gate.
  • FR-017 — Preview safety: in preview mode, reads continue, rules evaluate normally, values are compared, intended updates are reported as WouldUpdate, no write request is issued, and logs and summaries are still produced.
  • FR-018 — Real-time console output: each user's result MUST be displayed immediately after that user is processed.
  • FR-019 — Periodic summary: after every configured number of processed users, a table of all business rules and counts MUST be displayed.
  • FR-020 — Summary interval semantics: default interval is 25; a value greater than zero displays interim summaries at that interval; zero suppresses interim summaries; a final summary is always displayed.
  • FR-021 — Final reconciliation: at each summary the engine MUST verify Processed = Matched + Unclassified + EvaluationError, and MUST log a failed reconciliation as an engine defect.
  • FR-022 — Structured logs: the engine MUST produce structured audit records suitable for file output and future ingestion into an approved logging platform.
  • FR-023 — Configuration editor: the project MUST include a second tool that validates and interactively edits the JSON configuration.
  • FR-024 — Non-interactive validation: the configuration tool MUST support non-interactive validation and return a non-zero exit code on failure.
  • FR-025 — Configuration test data: the editor MUST support testing rules against synthetic sample users without tenant connectivity.
  • FR-026 — Backup before save: interactive edits MUST be validated before save and SHOULD create a timestamped backup or save-as output before replacing an existing configuration.

Rule Engine Requirements

  • RE-001 — Each rule MUST carry id, name, description, enabled, priority, persona, and match. Optional fields: tags, owner, changeReference, effectiveDate, notes, testCases.
  • RE-002 — Priorities MUST be unique numeric values; lower values evaluate first.
  • RE-003 — Logical composition supports all, any, and nesting of both.
  • RE-004 — Maximum nesting depth is configurable: default 5, minimum 1, hard ceiling 10. Both the runtime validator and the editor MUST reject configurations exceeding the configured limit or the ceiling.
  • RE-005 — Initial operators: equals, notEquals, contains, notContains, startsWith, endsWith, matchesRegex, in, notIn, isNull, isNotNull, memberOf, notMemberOf.
  • RE-006 — String comparison is case-insensitive by default. Regex patterns MUST be validated before execution.
  • RE-007 — Group membership mode is configurable per condition: direct or transitive.
  • RE-008 — The engine MUST support any configured combination of identity sources (security-group membership, directory-role assignments, and further sources added through data-provider modules). First match still wins.
  • RE-009 — Special accounts (for example emergency-access accounts) are identified by immutable user Object IDs within normal rule definitions. No hard-coded classification path is permitted.

Validation Requirements

  • VR-001 — Validation runs in four layers: JSON syntax, JSON Schema, semantic, safety.
  • VR-002 — Semantic validation MUST detect at minimum: duplicate rule IDs; duplicate priorities; no enabled rules; invalid or blank target attribute; target attribute absent from the approved writable list; reference to an unavailable data source; memberOf without group Object IDs; in/notIn without values; isNull/isNotNull carrying a comparison value; undefined or prohibited persona values; Unclassified used as an ordinary rule persona; condition depth over the configured maximum; configured maximum over the hard ceiling; group mode not enabled globally; unsupported property name; invalid regular expression.
  • VR-003 — Safety validation MUST detect at minimum: production-capable configuration with a blank target attribute; unsupported writable attribute; enabled group rules while group retrieval is disabled; prohibited configuration version downgrade; rule deletion or reorder without a version change where enforcement is enabled; condition tracing enabled without explicit acknowledgement; save path overwriting the only valid configuration without a backup.
  • VR-004 — Every finding MUST carry a severity (Error, Warning, Information), a JSON path or rule ID, a finding code, a clear description, and a suggested resolution.
  • VR-005 — Errors block execution and saving. Warnings require acknowledgement in the interactive editor and block only when warnings-as-errors is configured.

Non-Functional Requirements

Carried forward from the baseline (§19):

  • NFR-001 — Runtime targets PowerShell 7 in both the local and automation environments.
  • NFR-002 — Performance: display each result shortly after evaluation; cache reusable group/role data where safe; avoid repeatedly retrieving the same static membership data; record per-user and total duration. No hard performance target is set until representative tenant testing is complete.
  • NFR-003 — Reliability: handle pagination; bounded retry with backoff for retryable responses; respect service-provided retry guidance; do not retry non-retryable failures indefinitely.
  • NFR-004 — Maintainability: public functions carry comment-based help; business rules are never hard-coded; functions have focused responsibilities; rule evaluation remains independently testable.
  • NFR-005 — Auditability: every run has a unique run ID; every user event includes run ID, UPN, and Account Object ID; configuration hash and version are recorded; every changed or proposed value identifies the matched rule.
  • NFR-006 — Security: least privilege; managed identity for automation; no secrets in source control; only the approved target attribute in any write payload; no write path in preview mode.
  • NFR-007 — Portability: the core rule engine and configuration validation run locally without the automation platform; tenant access is isolated behind data-provider and authentication modules.
  • NFR-008 — Compatibility: avoid Windows PowerShell-only dependencies unless explicitly approved; verify all required modules in the automation PowerShell 7 environment.

Key Entities

  • User Record — the normalized representation of an in-scope account. Carries the Account Object ID, UPN, display name, user type, enabled state, organizational properties, and the stored persona value. Produced by normalization; consumed by the rule engine. The rule engine MUST NOT see raw directory responses.
  • Membership Record — normalized group and directory-role membership for a user, with the mode (direct or transitive) that produced it, and an explicit indicator of whether retrieval succeeded. The success indicator is what distinguishes "not a member" from "unknown" (FR-013).
  • Business Rule — an ordered, uniquely prioritized, enabled/disabled classification rule producing exactly one persona when its condition tree matches.
  • Condition / Condition Group — a leaf comparison (property or membership, with an operator) or an all/any group containing further conditions, bounded by the configured depth.
  • Persona Decision Result — the outcome for one user: Matched (with the matched rule ID), Unclassified, or EvaluationError; plus stored value, calculated value, and the resulting action (unchanged, would-update, updated, update-failed).
  • Configuration — the complete ordered decision process: engine settings, logging settings, identity data sources, approved writable attributes, target attribute, summary interval, depth limits, and the rule set. Versioned and hashed.
  • Validation Finding — severity, location, code, description, and suggested resolution (VR-004).
  • Run Record — run-level audit event carrying the run ID, timing, versions, configuration hash, execution mode, and all outcome counters (§13.2).

Persona Catalogue

Candidate business classifications — not hard-coded engine behaviour: Guest, BreakGlass-Admin, Tier0-Admin, Tier1-Admin, Tier2-Admin, Restricted-User, Test-Account, Service-Account, Shared-Functional-Account, Meeting-Room-Device, Employee, Contractor, Student. Future modules may add Identity-Workload and Agentic-Account.

Unclassified is a processing result when no rule matches. EvaluationError is an execution result and MUST NOT be treated as a normal persona rule.


Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: Every evaluated account receives exactly one outcome — a matched persona, Unclassified, or EvaluationError. Never zero, never more than one.
  • SC-002: Running the engine twice against an unchanged directory and unchanged configuration produces zero writes on the second run.
  • SC-003: The same user record and configuration produce the same persona on every evaluation, independent of ordering, host, or run.
  • SC-004: A preview-mode run over the full in-scope population issues zero directory write requests, proven by test assertion on the write adapter, not by inspection.
  • SC-005: Every write request body contains exactly one attribute — the approved target attribute.
  • SC-006: 100% of user-level audit events carry run ID, UPN, and Account Object ID; 100% of matched results identify the rule that produced them.
  • SC-007: Reconciliation (Processed = Matched + Unclassified + EvaluationError) passes at every interim and final summary.
  • SC-008: The rule engine and configuration validation execute to completion against synthetic fixtures with no tenant connectivity and no network access.
  • SC-009: Every invalid-configuration condition listed in VR-002 and VR-003 produces a finding with a severity, a location, and a suggested resolution, and blocks execution or saving as specified.
  • SC-010: An invalid configuration in non-interactive mode fails a pipeline run via a non-zero exit code, with no prompt and no hang.
  • SC-011: Every documented exit code is reachable and returned for its documented condition.
  • SC-012: A per-user result is visible to the operator before the next user is processed.
  • SC-013: No artifact in the repository contains an organization name, real domain, tenant or subscription ID, real UPN or Object ID, real group or role identifier, or any secret, token, or credential.

Assumptions

  • Scope: version 1 processes user objects only. Service principals, managed identities, workload identities, and agentic identities are out of scope, and the architecture MUST NOT assume future identity types share user-object properties.
  • Configuration format: JSON is the only supported format for version 1, and the file represents the entire ordered decision process. Tenant-specific parameters are never embedded in source.
  • Environment: the connected environment is the production tenant. Early connected development and testing use read-only permissions; write permissions are not granted during the initial development stage.
  • Automation: one automation account and managed identity. A separate production write-capable identity is not required.
  • Offline testing: unit testing uses synthetic user and membership objects and does not require tenant connectivity.
  • Cadence: a recurring schedule is intended and is deployment-configurable. Hourly is a candidate starting point, subject to tenant size and performance testing.
  • Processing model: version 1 may perform full enumeration. A delta strategy is deferred (OTD-008).
  • Priority convention: lower numeric priority values evaluate first.
  • Sanitization: all artifacts use placeholders only — <ORGANIZATION-NAME>, <PRIMARY-DOMAIN>, <TENANT-ID>, <ACCOUNT-OBJECT-ID>, <GROUP-OBJECT-ID>, <APPROVED-PERSONA-ATTRIBUTE-NAME>, <AUTOMATION-ACCOUNT-NAME>, <LOG-OUTPUT-PATH>. Synthetic data must be obviously fictional.

Clarification Register

Open items carried from the baseline (§24). These are implementation research items to be resolved in plan.md / research.md or an ADR — not unanswered business requirements. OTD-001 through OTD-005 must be closed before persistence implementation.

Updated 2026-08-20 (T114): OTD-001 through OTD-007 and OTD-010 are resolved in research.md. The persistence gate is lifted for Stage A3 test accounts, and remains closed for the general population until V-4 security sign-off is recorded.

ID Item Status
OTD-001 Exact persona attribute mechanism — data type, read/update method, discoverability, Conditional Access compatibility Resolved — directory (schema) extension property on an app registration, addressable as user.extension_<appId>_<name> in dynamic group rules. extensionAttributeN rejected (unavailable for cloud writes on ever-synced and Exchange-originated objects); custom security attributes rejected (not exposed to the dynamic group engine). See research.md OTD-001.
OTD-002 Exact least-privilege directory permissions for users, groups, roles, and the selected attribute ResolvedUser.Read.All, GroupMember.Read.All, RoleManagement.Read.Directory; User.ReadWrite.All for enforcement only. Scopes are requested per enabled-rule need, not unconditionally. See research.md OTD-002.
OTD-003 Whether write authorization can be restricted to the individual target attribute; if not, compensating controls plus security approval Resolved: it cannot. Graph application permissions have no per-property write scope. Six compensating controls are mandatory and implemented; see docs/SecurityModel.md. Security approval (V-4) is still outstanding and gates enforcement.
OTD-004 Directory access approach — SDK cmdlets, direct REST, or a controlled combination Resolved — direct REST via Invoke-MgGraphRequest, so request bodies are explicit values that tests can assert on. This is what makes SC-005 provable. Only Microsoft.Graph.Authentication is a runtime dependency.
OTD-005 JSON Schema validation approach compatible with PowerShell 7 locally and in automation Resolved locallyTest-Json -SchemaFile, draft-07 only. Failure behaviour observed and pinned in V-5a; note that an unparseable schema returns $true. V-5b (Automation runtime) remains open.
OTD-006 Structured-log destination and transport Resolved — NDJSON through a single sink, Write-PersonaAuditRecord. Additional transports are added behind that function, not by widening call sites. See docs/Logging.md.
OTD-007 Retry policy — retryable status codes, max attempts, backoff, jitter, logging Resolved — retry 429/500/502/503/504 and status-less transport failures; never 400/401/403/404/409; honour Retry-After; max 5 attempts; exponential backoff with full jitter capped at 60s. See research.md OTD-007.
OTD-008 Full versus incremental processing roadmap Open — full enumeration only in v1.
OTD-009 Production schedule and concurrency lock to prevent overlapping runs Open — deferred with Stage B (T120). Until then the schedule is the lock; see docs/OperationsRunbook.md.
OTD-010 Rollback implementation — pre-change audit values or another approved mechanism Data captured; tool not built. previousValue is recorded at write time on every Updated record, which is the part that cannot be reconstructed retroactively. The rollback tool itself is out of scope for v1.

Mandatory research item: determine the selected persona attribute mechanism and document the exact authorization boundary for updating it. If individual-attribute authorization is unavailable, document compensating controls before implementation approval. — Done. The mechanism is a directory extension property; the authorization boundary is the whole user object, because Graph offers nothing narrower; the compensating controls are documented in docs/SecurityModel.md and each one is test-enforced. Implementation approval for enforcement still requires the V-4 sign-off.


Out of Scope for Version 1

  • Classification of service principals, managed identities, workload identities, or agentic identities.
  • Configuration formats other than JSON.
  • Delta or incremental processing.
  • Condition-level case-sensitivity control (noted as a possible future addition).
  • Reusable rule fragments and the circular-reference validation they would require.
  • Eligible-role information, unless authorization is confirmed and the data provider is implemented.