277 lines
14 KiB
Markdown
277 lines
14 KiB
Markdown
# 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 0 — approved requirements baseline.**
|
||
> No implementation code exists yet. The authoritative baseline is
|
||
> [`Persona-Engine-Developer-Handoff.txt`](Persona-Engine-Developer-Handoff.txt).
|
||
> The next step is Phase 1: convert that baseline into `specs/001-persona-engine/spec.md`.
|
||
|
||
---
|
||
|
||
## 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 |
|
||
| Specify | `specs/001-persona-engine/spec.md` | 1 | Not started |
|
||
| Plan | `specs/001-persona-engine/plan.md`, `research.md` | 2 | Not started |
|
||
| 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` / `Any` composition
|
||
- Native PowerShell `-WhatIf` as 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`.
|
||
|
||
```powershell
|
||
# 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 |
|
||
|
||
```powershell
|
||
# 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 1.
|
||
|
||
1. Initialize the Spec Kit project structure.
|
||
2. Convert the handoff baseline into `specs/001-persona-engine/spec.md`.
|
||
3. Build a requirements traceability list using FR/NFR identifiers.
|
||
4. Close OTD-001 through OTD-005 before any persistence work.
|
||
5. Create `persona-engine.schema.json` and a placeholder-only `persona-engine.example.json`.
|
||
6. Define normalized PowerShell object contracts.
|
||
7. **Build the pure rule engine first**, with offline Pester tests, before any Graph integration.
|
||
8. Implement configuration validation and non-interactive pipeline mode.
|
||
9. Implement Graph read adapters, then console and structured logging.
|
||
10. Implement the persistence adapter **last**, with `ShouldProcess` and 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 `-WhatIf` validation, 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, or `WhatIf`/`ShouldProcess` behaviour **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.
|