Files
personaEngine2/docs/SecurityModel.md
T

8.8 KiB

Security model

What this engine is permitted to do, what it is not, and where the gap between those two is held open by testing rather than by the platform.

The central trade-off (OTD-003)

Microsoft Graph application permissions have no per-property write scope. An identity granted User.ReadWrite.All can write any writable property on any user object. It cannot be narrowed to one extension attribute.

This is not a limitation to be worked around. It is a fact about the platform, recorded here so that nobody later assumes the directory is enforcing something it is not.

The consequence: the only thing standing between this engine and every writable user property is the code in this repository, and the tests that hold it to that. Every control below exists because the directory will not refuse a malformed request on our behalf.

The six compensating controls

All six are mandatory. Each is testable, and each is tested.

# Control Where it lives Proof
1 The persistence layer accepts only the configured target attribute New-PersonaWriteBody throws for any other name WriteBodyRejection.Tests.ps1
2 The target must appear in approvedWritableAttributes Resolve-TargetAttribute and New-PersonaWriteBody, checked twice WriteBodyRejection.Tests.ps1
3 Validation rejects every other attribute PE-SAF-002, layer 4 Safety.Tests.ps1
4 One dedicated function builds the request body, and it is the only one New-PersonaWriteBody returns a hashtable whose Count is exactly 1 WriteBody.Tests.ps1
5 Tests inspect the captured request body Every body issued during a full enforcing run is asserted to have one key WriteBody.Tests.ps1
6 Code owners and branch policies gate persistence changes Repository configuration, outside this codebase Branch protection on src/Persistence/

Control 4 is the load-bearing one. A single construction site makes SC-005 a property of one testable function rather than a convention every future call site has to remember. WriteBody.Tests.ps1 includes a scan asserting that no other file under src/ builds a PATCH body.

Control 2 is deliberately redundant. Validation runs once at startup against the file; the write builder checks again on every write against the values actually in hand — so a configuration object mutated mid-run still cannot widen the blast radius.

Why comparison is ordinal here and case-insensitive elsewhere

Rule matching is case-insensitive (RE-006), because a rule author should not have to match directory casing. Attribute approval is ordinal and case-sensitive, because extension property names are case-sensitive in Graph: extension_<id>_Persona and extension_<id>_persona are two different attributes, and approving one does not approve the other.

Change detection is also ordinal (FR-015). A stored employee against a calculated Employee is a real difference worth correcting, not a formatting quirk.

Permissions

Stage A — local, delegated (current)

Connect-MgGraph -Scopes 'User.Read.All','GroupMember.Read.All','RoleManagement.Read.Directory'

Read-only. Sufficient for every preview run and for closing V-1 (read) and V-3.

The engine requests only the scopes the enabled rules actually need: a configuration with no role conditions never asks for RoleManagement.Read.Directory, and a configuration with no membership conditions never asks for GroupMember.Read.All. Least privilege applies to data as well as to permissions — properties nothing references are not even added to $select.

Stage A3 (delegated write) adds User.ReadWrite.All and targets purpose-created test accounts only. Under delegated authentication the write runs as the operator, which makes the compensating controls more important rather than less: the directory sees the operator's own permissions, not a narrowed service identity.

Never sign in with a standing privileged account for a write run. A Global Administrator session invalidates V-3 as evidence and removes every practical limit on what a defect could reach.

Stage B — Azure Automation, application permissions (deferred)

User.Read.All, GroupMember.Read.All, RoleManagement.Read.Directory, and — for enforcement — User.ReadWrite.All, granted to a managed identity. No client secret, ever, in source control or in a runbook parameter.

Deferred, not waived: no Automation account is available. V-3b and V-5b remain open.

The persona attribute (OTD-001)

A directory (schema) extension property, registered on an application registration and addressable as extension_<appId>_<name>.

Two alternatives were rejected for concrete reasons:

  • extensionAttribute1..15 — unavailable for cloud writes on objects that are, or ever were, synchronized from on-premises, and on Exchange-originated objects. A classification engine that silently cannot write to a subset of the population is worse than one that cannot write at all.
  • Custom security attributes — not exposed to the dynamic group membership engine, which defeats the purpose: the persona exists so that Conditional Access can be targeted through dynamic groups.

PE-SAF-002 rejects any approved attribute that is not shaped like a directory extension property. Built-in attributes such as department or jobTitle are excluded even when the operator holds permission to write them — they are authoritative in the sync source or in HR, and this engine does not own them.

Data handling

Approved for logs: user principal name, account object ID, matched rule ID, stored and calculated persona values, run ID, configuration version and hash.

Never logged, under any setting: access tokens, Authorization headers, client secrets, certificates, credentials, or full Graph responses.

The guarantee is structural rather than filtered. New-PersonaAuditRecord accepts only named, typed values from the decision result and the counters — there is no pass-through of an arbitrary object, so there is nothing for a secret to ride in on. AuditRedaction.Tests.ps1 asserts this holds even when a caller actively attaches a token to the decision result.

Condition tracing

logging.traceConditionValues writes evaluated attribute values into audit records, widening what the log contains beyond the approved set. It requires logging.acknowledgeConditionTracing in the same configuration, or validation fails with PE-SAF-006.

The acknowledgement lives in the configuration rather than in a command-line switch on purpose: a flag passed at a console is invisible to review, while a field in the configuration appears in the diff of the change that enables tracing, next to the person who approved it.

The no-write control

-WhatIf is the only approved no-write control.

-Debug does not imply read-only. A -Debug run without -WhatIf writes, and ShouldProcessGate.Tests.ps1 asserts that it does — because an operator who believed otherwise would reach for -Debug as a safety measure and get an enforcing run. The same holds for -Verbose.

ShouldProcessGate.Tests.ps1 also asserts that the entry script declares no -Preview, -NoWrite, -ReadOnly, or -DryRun parameter, and never reads $WhatIfPreference.

Verification gates

Item Status Blocks
V-1 read half Open — needs a tenant Confidence in the read path across origin types
V-1 write half Open — needs test accounts Enforcement
V-2 dynamic group + CA Open Declaring the persona useful
V-3 non-privileged -WhatIf run Open — needs a tenant Stage A2 sign-off
V-3b managed-identity scopes Deferred Stage B
V-4 security sign-off on these controls Open All enforcement (T101)
V-5a Test-Json behaviour Closed — see V-5a.md Layer 2 implementation
V-5b Test-Json in Automation Deferred Stage B

V-4 gates enforcement. No write run against anything other than purpose-created test accounts until it is recorded in specs/001-persona-engine/verification/V-4.md.

Kill switch

In increasing order of severity — see OperationsRunbook.md for the procedure:

  1. Run with -WhatIf.
  2. Set every rule to enabled: false and deploy.
  3. Disable the Automation schedule (Stage B).
  4. Revoke User.ReadWrite.All from the execution identity.
  5. Remove the write deployment stage.

Steps 4 and 5 are the ones that hold if the code itself is the problem.

Sanitization (SC-013)

No organization name, real domain, tenant or subscription ID, real UPN or Object ID, real group or role identifier, environment-specific attribute name, or any secret may appear in any tracked file. Placeholders only.

Runtime records naturally contain real UPNs and Object IDs — approved for logs — but no such value is ever committed.