15 KiB
Persona Engine
A modular, configuration-driven PowerShell 7 identity-classification service for Microsoft Entra ID.
The engine enumerates Entra user accounts, evaluates each one against an ordered, JSON-defined rule set, deterministically assigns exactly one persona, and updates a single approved persona attribute — and only when the calculated value differs from the current value.
Project status: Phase 1 complete — specification drafted. No implementation code exists yet. The approved requirements baseline is
Persona-Engine-Developer-Handoff.txt, now converted intospecs/001-persona-engine/spec.md. Project governance is ratified in.specify/memory/constitution.md(v1.0.0). The next step is Phase 2: produceplan.mdandresearch.md, closing OTD-001 through OTD-005.
Spec-driven development
This project follows the GitHub Spec Kit workflow. Nothing is implemented before it is specified, planned, and decomposed into tasks.
Specify -> Plan -> Tasks -> Implement
| Stage | Artifact | Phase | State |
|---|---|---|---|
| Baseline | Persona-Engine-Developer-Handoff.txt |
0 | Approved |
| Constitution | .specify/memory/constitution.md |
0 | Ratified v1.0.0 |
| Specify | specs/001-persona-engine/spec.md |
1 | Draft complete |
| Plan | specs/001-persona-engine/plan.md, research.md |
2 | In progress |
| Contracts | data-model.md, contracts/, persona-engine.schema.json |
3 | Not started |
| Tasks | specs/001-persona-engine/tasks.md |
4 | Not started |
| Implement | src/, tests/, pipelines/ |
5–12 | Not started |
Any item that is unresolved must be captured as an explicit assumption, risk, or architecture decision. It must never be silently implemented.
Core design principles
| Principle | Meaning |
|---|---|
| Deterministic | The same input and configuration always produce the same persona. |
| Exactly one result | Every evaluated account receives one persona — never zero, never several. |
| First match wins | Rules are ordered; evaluation stops at the first match. |
| Idempotent | Re-running changes nothing unless the calculated value actually changed. |
| Configuration-driven | Business rules live in JSON, never in PowerShell source. |
| Fail safe | If evaluation cannot complete reliably, the existing persona is preserved. |
| Explainable | Every result identifies the matched rule, run ID, UPN, and Account Object ID. |
| Modular | The pure rule engine has no dependency on Graph, Azure Automation, or the console. |
Scope (version 1)
In scope
- Microsoft Entra user objects only
- Ordered, first-match business rules defined in JSON (the only supported configuration format)
- Property, group-membership, and role-based conditions with nested
All/Anycomposition - Native PowerShell
-WhatIfas the approved no-write control - Structured, audit-friendly logging plus immediate per-user output and periodic summaries
- Local PowerShell 7 execution and Azure Automation PowerShell 7 runbook execution
Out of scope for v1 — the architecture must not assume these share user-object properties:
- Service principals, managed identities, workload identities, agentic identities
Candidate persona catalogue
These are 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
Two values are processing results rather than rules:
Unclassified— evaluation succeeded, but no rule matched.EvaluationError— evaluation could not complete; the existing persona is preserved.
Components
1. Invoke-PersonaEngine.ps1
Retrieval, evaluation, reporting, and controlled persistence.
| Parameter | Notes |
|---|---|
-ConfigPath <string> |
Required |
-WhatIf |
Native risk-mitigation parameter; the approved no-write control |
-Verbose / -Debug |
Native common parameters; -Debug must not mean read-only |
-UserObjectId <GUID> |
Optional single-user test execution |
-OutputPath <string> |
Optional override, if permitted |
-CorrelationId <GUID> |
Optional supplied run identifier |
The script uses CmdletBinding with SupportsShouldProcess.
# Read-only evaluation
./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -WhatIf
# Read-only with operational detail
./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -WhatIf -Verbose
# Single-user validation
./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json -UserObjectId <ACCOUNT-OBJECT-ID> -WhatIf
# Production, changed-values-only processing
./Invoke-PersonaEngine.ps1 -ConfigPath ./config/persona-engine.json
Exit codes
| Code | Meaning |
|---|---|
0 |
Successful run; no fatal processing errors |
1 |
Configuration validation failure |
2 |
Authentication / authorization failure |
3 |
User enumeration failure |
4 |
Fatal required data-provider failure |
5 |
Reconciliation failure |
6 |
Unexpected fatal engine error |
A per-user EvaluationError does not necessarily terminate the run, but the final status must report the number of affected accounts and may apply a configurable warning/failure threshold.
2. Edit-PersonaEngineConfig.ps1
Configuration validation, interactive editing, synthetic rule testing, and pipeline enforcement.
| Parameter | Notes |
|---|---|
-ConfigPath <string> |
Required |
-ValidateOnly |
Validate without entering the editor |
-NonInteractive |
Pipeline mode; returns codes instead of prompting |
-SchemaPath <string> |
Optional schema override |
-OutputPath <string> |
Optional Save-As target |
-TreatWarningsAsErrors |
Escalate warnings |
-TestDataPath <string> |
Optional synthetic sample input |
# Validate only
./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -ValidateOnly
# Pipeline validation
./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json -ValidateOnly -NonInteractive
# Interactive editor
./Edit-PersonaEngineConfig.ps1 -ConfigPath ./config/persona-engine.json
Validation runs in four layers: JSON syntax → JSON Schema → semantic → safety.
Architecture
Invoke-PersonaEngine.ps1
|
+-- Configuration Import-PersonaConfiguration, Test-PersonaConfiguration, Resolve-TargetAttribute
+-- Authentication Connect-PersonaGraphInteractive, Connect-PersonaGraphManagedIdentity
+-- Data Providers Get-PersonaUsers, Get-PersonaGroupMembership, Get-PersonaDirectoryRoles
+-- Normalization ConvertTo-PersonaUserRecord, ConvertTo-PersonaMembershipRecord
+-- Rule Engine Test-PersonaCondition, Test-PersonaConditionGroup, Test-PersonaRule, Resolve-UserPersona
+-- Persistence Compare-PersonaValue, Set-UserPersonaAttribute
+-- Presentation Write-UserPersonaResult, Write-PersonaSummary
+-- Audit New-PersonaAuditRecord, Export-PersonaRunReport
Critical flow
Graph acquisition -> normalized identity record -> pure rule engine
-> persona decision result -> comparison -> optional persistence adapter
-> console and structured audit output
The pure rule engine must not depend on Graph authentication, Azure Automation, or console rendering. It has to be testable offline with synthetic data.
Planned repository structure
PersonaEngine/
|-- README.md
|-- Invoke-PersonaEngine.ps1
|-- Edit-PersonaEngineConfig.ps1
|-- PersonaEngine.psd1
|-- PersonaEngine.psm1
|
|-- config/ persona-engine.example.json, persona-engine.schema.json
|-- src/ Authentication/ Configuration/ DataProviders/ Normalization/
| RuleEngine/ Persistence/ Presentation/ Audit/
|-- tests/ Unit/ Integration/ Configuration/ Safety/ TestData/
|-- docs/ Architecture.md BusinessRules.md ConfigurationReference.md
| Logging.md SecurityModel.md OperationsRunbook.md
|-- pipelines/ validate.yml test.yml release.yml
|-- specs/
|-- 001-persona-engine/
|-- spec.md plan.md tasks.md research.md data-model.md quickstart.md
|-- contracts/ checklists/
Security model
- Authentication — Azure Automation uses a managed identity. Local development uses an approved interactive or read-only application identity. No client secret in source control.
- Least privilege — the execution identity gets only what the enabled rules require: in-scope user properties, configured group membership, configured role data, and the existing persona value.
- Write permissions — the production identity is granted the minimum permission needed to update the configured target attribute. Whether Entra can enforce write scope at the individual attribute level must be verified, never assumed (see OTD-003).
- Compensating controls, if the Graph permission proves broader than the single attribute: the persistence module accepts only the approved target attribute; that attribute must appear in
approvedWritableAttributes; validation rejects all others; a dedicated function builds a request body containing only that attribute; unit and integration tests inspect the request body; code owners and branch policies gate persistence changes; directory audit logs are monitored for unexpected property writes. - Data handling — UPN and Account Object ID are approved for logs. Never log access tokens, authorization headers, secrets, or full Graph responses. Detailed condition values are diagnostic-only, behind
-Debug. - Kill switch — disable the Automation schedule, run with
-WhatIf, revoke production write permission, or disable write deployment stages.
Sanitization requirements
Every artifact in this repository — docs, examples, tests, configuration samples — must be free of organization names, real domains, tenant or subscription IDs, automation account names, real UPNs or Object IDs, real group/role identifiers, environment-specific attribute names, Log Analytics details, and any secret, token, certificate, or credential.
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 test data must be obviously fictional and must never reproduce real employee records. Tenant-specific configuration belongs in a protected repository or configuration store, not here.
Open technical decisions
Research items to be resolved in research.md or an ADR. OTD-001 through OTD-005 must be closed before persistence implementation.
| ID | Decision |
|---|---|
| OTD-001 | Select the exact Entra persona attribute mechanism — data type, Graph read/update method, discoverability, Conditional Access compatibility |
| OTD-002 | Confirm exact least-privilege Microsoft Graph permissions |
| OTD-003 | Confirm whether write authorization can be restricted to the individual target attribute |
| OTD-004 | Select the Graph access approach — SDK cmdlets, direct REST, or a controlled combination |
| OTD-005 | Select a JSON Schema validation approach compatible with PS7 locally and in Azure Automation |
| OTD-006 | Select structured-log destination and transport |
| OTD-007 | Define retry policy — retryable status codes, max attempts, backoff, jitter, logging |
| OTD-008 | Define full versus incremental processing roadmap |
| OTD-009 | Define production schedule and concurrency lock |
| OTD-010 | Define rollback implementation |
Getting started
Implementation has not begun. The current work item is Phase 2 — planning.
Initialize the Spec Kit project structure.Done.Convert the handoff baseline intoDone.specs/001-persona-engine/spec.md.- Build a requirements traceability list using FR/NFR identifiers.
- Close OTD-001 through OTD-005 before any persistence work.
- Create
persona-engine.schema.jsonand a placeholder-onlypersona-engine.example.json. - Define normalized PowerShell object contracts.
- Build the pure rule engine first, with offline Pester tests, before any Graph integration.
- Implement configuration validation and non-interactive pipeline mode.
- Implement Graph read adapters, then console and structured logging.
- Implement the persistence adapter last, with
ShouldProcessand tests proving zero writes under-WhatIf.
Requirements: PowerShell 7. Offline unit testing must not require tenant connectivity.
Delivery
- Hosted in Azure DevOps Git — feature branches, pull requests, protected release branch, code owners on persistence, security configuration, and production rules.
- Validation pipeline — repository hygiene checks, PowerShell static analysis, JSON Schema validation, semantic/safety configuration validation, Pester unit tests, Pester safety tests, test result publication, artifact packaging.
- Release pipeline — validate approved branch/tag, repeat validation and tests, package, deploy to Azure Automation, import modules, publish runbook, keep the schedule disabled, execute
-WhatIfvalidation, approval gate, then enable enforcement. - Changes to the target attribute,
approvedWritableAttributes, rule priority, rule enablement, rule conditions, persona outputs, authentication permissions, persistence functions, logging destination, orWhatIf/ShouldProcessbehaviour all require review.
Definition of done (v1)
Version 1 is complete when both PowerShell scripts are implemented; the JSON Schema exists and is documented; validation covers syntax, schema, semantic, and safety layers; ordered first-match evaluation and nested All/Any work within the configured depth; the initial property and membership operators are tested; null behaviour matches the approved decision; required group-lookup failures produce EvaluationError and preserve the existing persona; Unclassified users are reported distinctly; each user result displays immediately; interim and final summaries work — including interval 0 — and reconciliation checks pass; logs include UPN and Account Object ID; -WhatIf produces zero Graph writes; only changed valid values are written in enforcement mode; the write payload contains only the approved target attribute; local offline Pester, read-only production-tenant, Azure Automation PowerShell 7, and Azure DevOps pipeline runs all pass; security review confirms permissions and compensating controls; operational documentation, kill switch, and rollback procedure are complete; and -WhatIf impact evidence is reviewed before enforcement is enabled.