# Contract: Directory Data Provider The only component permitted to talk to Microsoft Graph. Implements OTD-002, OTD-004, and OTD-007. Everything below the normalization boundary is invisible to the rule engine (Principle IV). **Transport**: `Invoke-MgGraphRequest` from `Microsoft.Graph.Authentication` (OTD-004). No resource-specific SDK modules. ## Authentication (FR-003) | Function | Environment | Mechanism | | --- | --- | --- | | `Connect-PersonaGraphManagedIdentity` | Azure Automation | `Connect-MgGraph -Identity` | | `Connect-PersonaGraphInteractive` | Local development | `Connect-MgGraph -Scopes ` | Both return an opaque connection handle. No token, header, or secret is ever returned to a caller, logged, or written to an audit record. ## Permissions (OTD-002) | Function | Application permission | | --- | --- | | `Get-PersonaUsers` | `User.Read.All` (or `User.ReadWrite.All` for the enforcement identity) | | `Get-PersonaGroupMembership` | `GroupMember.Read.All` | | `Get-PersonaDirectoryRoles` | `RoleManagement.Read.Directory` | | `Set-UserPersonaAttribute` | `User.ReadWrite.All` | `Directory.Read.All` is prohibited — materially broader than the three read permissions combined. ## Read operations ### `Get-PersonaUsers` ```text GET /v1.0/users?$select=&$top=999 ``` - `$select` carries the FR-005 baseline (`id`, `userPrincipalName`, `displayName`, `userType`, `accountEnabled`, `companyName`, `jobTitle`, `department`) plus the configured target attribute and any property referenced by an enabled rule. Unused properties are not requested. - The persona directory extension is selected by its full name, `extension__`. - Pagination follows `@odata.nextLink` until absent (FR-004). A truncated enumeration MUST raise — never return a partial population as if complete. - `-UserObjectId` switches to `GET /v1.0/users/{id}?$select=...`. ### `Get-PersonaGroupMembership` ```text GET /v1.0/users/{id}/memberOf # direct GET /v1.0/users/{id}/transitiveMemberOf # transitive ``` Mode comes from the condition, falling back to `engine.defaultMembershipMode` (RE-007). **Return contract**: always a `MembershipRecord`. On failure it returns a record with `RetrievalSucceeded = $false` and a `FailureReason` — it MUST NOT return an empty list, and MUST NOT throw past the per-user boundary. This single behaviour is what makes FR-013 work: unknown membership becomes `EvaluationError`, never a false non-match. ### `Get-PersonaDirectoryRoles` ```text GET /v1.0/roleManagement/directory/roleAssignments?$filter=principalId eq '' ``` Eligible (PIM) assignments are out of scope for v1 unless authorization is confirmed. ### Caching Group and role data reusable across users is cached for the run's lifetime (NFR-002). The cache is keyed by group or role Object ID and is **never** persisted between runs — a stale cache would make results depend on run history, breaking Principle I. ## Write operation ### `New-PersonaWriteBody` The **only** function permitted to construct a write body (OTD-003 control 3). ```powershell # Returns exactly one key. @{ "" = "" } ``` Contract: - Throws if the attribute name is not `engine.targetAttribute`. - Throws if the attribute is absent from `approvedWritableAttributes`. - Returns a hashtable whose `Count` is exactly `1`. Tests assert on this directly (SC-005). ### `Set-UserPersonaAttribute` ```text PATCH /v1.0/users/{id} Content-Type: application/json ``` - Callable **only** when `ShouldProcess` returned `$true`. Under `-WhatIf` this function is not reached — the caller does not construct a request at all (SC-004). Preview mode is an absence of a call, not a suppressed call. - A failure returns `UpdateFailed` for that user and does not terminate the run. ## Retry policy (OTD-007) | Aspect | Value | | --- | --- | | Retryable | 429, 500, 502, 503, 504, transport timeout | | Never retried | 400, 401, 403, 404, 409 | | `Retry-After` | Honoured when present; overrides computed backoff | | Attempts | Max 5 | | Backoff | Exponential from 1s, full jitter, per-delay cap 60s | | Logging | Attempt number, status code, and delay on every retry | Exhausted retries on **required** data produce `EvaluationError` for the affected user (FR-013). Exhausted retries during enumeration are fatal (exit code `3`). ## Prohibited in this layer - Logging tokens, `Authorization` headers, or full response bodies (Principle V). - Returning raw Graph objects past `ConvertTo-Persona*Record`. - Any reference to rule, persona, or condition concepts — this layer moves data, it does not decide.