Skip to content

IdLE.Provider.EntraID MVP (delegated + app-only) #45

@blindzero

Description

@blindzero

Goal

Provide a production-usable Microsoft Entra ID provider module so IdLE can run real Joiner/Mover/Leaver workflows out of the box.

Scope

New module

  • Create a new provider module: IdLE.Provider.EntraID
  • Backend: Microsoft Graph
    • Recommended baseline: direct REST calls via an internal adapter (portable, testable).
    • Alternative acceptable implementation: Microsoft Graph PowerShell SDK, but it must be documented and wrapped behind the same adapter contract for unit testing.

Capabilities (MVP)

The provider MUST publish capabilities via GetCapabilities().

  • IdLE.Identity.Read
  • IdLE.Identity.List (provider API only, no built-in step)
  • IdLE.Identity.Create
  • IdLE.Identity.Attribute.Ensure
  • IdLE.Identity.Disable
  • IdLE.Identity.Enable
  • IdLE.Entitlement.List (Groups)
  • IdLE.Entitlement.Grant (Groups)
  • IdLE.Entitlement.Revoke (Groups)
  • IdLE.Identity.Delete (opt-in gated, see safety section)

Identity addressing

  • Support lookup by:
    • objectId (GUID string)
    • UPN
    • mail
  • Document resolution rules and canonical identity key.
  • Canonical identity key for outputs MUST be the user objectId (GUID string).

Group entitlement model

  • Entitlement object format follows IdLE convention:
    • Entitlement.Kind = 'Group'
    • Entitlement.Id = <group objectId GUID string> (canonical)
    • Entitlement.DisplayName optional
    • Entitlement.Mail optional
  • Provider MAY accept Entitlement.Id as displayName for convenience, but MUST resolve to objectId deterministically:
    • no match -> throw
    • multiple matches -> throw (no best-effort)

Paging, throttling, transient failures

  • List/Search operations MUST handle Graph paging (nextLink).
  • Provider/adapter MUST classify transient failures for retry policies:
    • 429, 5xx, network timeouts -> throw an exception marked transient via:
      • Exception.Data['Idle.IsTransient'] = $true
    • Include relevant, non-secret metadata in the exception message (HTTP status, request id if available).

Idempotency guarantees (required for retries and re-runs)

  • Create:
    • if the user already exists (by objectId or UPN), treat as success and return Changed = $false.
  • Delete:
    • if the user is already gone, treat as success and return Changed = $false.
  • Disable/Enable:
    • if already in desired state, return Changed = $false.
  • EnsureAttribute:
    • if attribute already matches desired value, return Changed = $false.
  • Group Grant/Revoke:
    • membership already in desired state must be a no-op success (Changed = $false).

Hard constraints (from #77 and #91)

Authentication is host-owned (AuthSessionBroker)

  • The host injects an AuthSessionBroker via Providers.AuthSessionBroker.
  • Steps and providers acquire sessions only via Context.AcquireAuthSession(Name, Options).
  • Workflows/plans/step metadata MUST remain data-only:
    • no secrets
    • no ScriptBlocks (including nested values in AuthSessionOptions)
  • Providers MUST NOT start authentication flows (no interactive login, no device code prompts, no Connect-* patterns that trigger auth).
    • Delegated vs. app-only authentication is a host concern.
  • use latest New-IdleAuthSessionBroker CmdLet for simple auth cases

Multi-auth-context per step (data-only routing)

The provider MUST support selecting different auth contexts per step using existing step metadata keys:

  • With.AuthSessionName (string)
    • For this provider, workflows SHOULD use: MicrosoftGraph
  • With.AuthSessionOptions (hashtable, data-only)
    • Example: @{ Role = 'Tier0' } vs. @{ Role = 'Admin' }

The provider MUST accept an optional AuthSession parameter for all callable methods that may need auth.

Safety: Delete must be explicit opt-in (align with #46)

  • IdLE.Identity.Delete MUST be gated by provider configuration.
  • Provider constructor MUST default to AllowDelete = $false.
  • Provider advertises IdLE.Identity.Delete only when AllowDelete = $true.
  • Example workflows MUST demonstrate this gate by requiring explicit opt-in for delete.

Provider contract (methods)

The provider object returned by the factory function MUST implement these script methods.
All methods MUST be idempotent and MUST accept an optional AuthSession parameter.

Required

  • GetCapabilities() -> string[]

Identity

  • GetIdentity(IdentityKey, [AuthSession]) -> object { IdentityKey, Enabled, Attributes }
  • ListIdentities([hashtable] Filter, [AuthSession]) -> string[]
    • Filter handling is optional; if supported, document supported keys (e.g. Filter.Search).
  • CreateIdentity(IdentityKey, Attributes, [AuthSession]) -> object { IdentityKey, Changed }
  • EnsureAttribute(IdentityKey, Name, Value, [AuthSession]) -> object { IdentityKey, Changed }
  • DisableIdentity(IdentityKey, [AuthSession]) -> object { IdentityKey, Changed }
  • EnableIdentity(IdentityKey, [AuthSession]) -> object { IdentityKey, Changed }
  • DeleteIdentity(IdentityKey, [AuthSession]) -> object { IdentityKey, Changed }
    • Only required when AllowDelete = $true and only then advertised.

Entitlements (Groups)

  • ListEntitlements(IdentityKey, [AuthSession]) -> IdLE.Entitlement[]
  • GrantEntitlement(IdentityKey, Entitlement, [AuthSession]) -> object { IdentityKey, Changed }
  • RevokeEntitlement(IdentityKey, Entitlement, [AuthSession]) -> object { IdentityKey, Changed }

Factory function (public API)

Create a public factory function similar to New-IdleADIdentityProvider:

  • New-IdleEntraIDIdentityProvider
    • Parameters:
      • -AllowDelete (switch, default: off)
      • -Adapter (object, internal/test injection; default constructed)

Notes:

  • The provider instance MAY implement both identity and entitlement contracts (as the AD provider does) so hosts can pass a single provider object under Providers.Identity.
  • PSTypeName SHOULD follow the module naming pattern, e.g. IdLE.Provider.EntraIDIdentityProvider.

AuthSession expectations (provider-side)

The provider MUST treat the auth session as opaque input. To remain flexible for hosts, the provider SHOULD support at least these shapes:

  • string access token (Bearer token)
  • object with property AccessToken
  • object with ScriptMethod GetAccessToken() returning a string
  • (optional) object with property HttpClient (host-managed) if the host wants to fully own HTTP setup

The provider MUST NOT log secrets or tokens and MUST NOT emit them into events or plan exports.

Implementation approach (testable, portable)

Adapter layer (required)

Implement a small internal adapter that performs the actual Graph operations.
The provider MUST call the adapter only (no direct REST/SDK calls outside the adapter), enabling unit tests to inject a fake adapter.

Recommended internal adapter function:

  • New-IdleEntraIDAdapter (internal)

Minimal adapter methods (MVP):

  • GetUserById(objectId, authContext)
  • GetUserByUpn(upn, authContext)
  • CreateUser(payload, authContext)
  • PatchUser(objectId, patchPayload, authContext)
  • DeleteUser(objectId, authContext)
  • ListUsers(filter, authContext) (handles paging)
  • GetGroupById(groupId, authContext)
  • GetGroupByDisplayName(displayName, authContext) (must detect ambiguity)
  • ListUserGroups(objectId, authContext) (handles paging)
  • AddGroupMember(groupObjectId, userObjectId, authContext)
  • RemoveGroupMember(groupObjectId, userObjectId, authContext)

authContext is whatever normalized structure the provider derives from AuthSession (e.g., access token string).

REST baseline

If using REST:

  • Use https://graph.microsoft.com/v1.0 endpoints (document if beta endpoints are used; beta should be avoided for MVP).
  • Implement paging via @odata.nextLink.
  • For transient HTTP failures (429/5xx), throw exceptions marked transient via Exception.Data['Idle.IsTransient'] = $true.
  • Respect Retry-After if present when mapping to transient errors (include it in error metadata; host controls retry timing).

Built-in steps coverage

The provider MUST be usable with built-in steps in IdLE.Steps.Common:

  • IdLE.Step.CreateIdentity -> CreateIdentity
  • IdLE.Step.EnsureAttribute -> EnsureAttribute
  • IdLE.Step.DisableIdentity -> DisableIdentity
  • IdLE.Step.EnableIdentity -> EnableIdentity
  • IdLE.Step.DeleteIdentity -> DeleteIdentity (opt-in gated)
  • IdLE.Step.EnsureEntitlement -> ListEntitlements, GrantEntitlement, RevokeEntitlement

Workflows MAY specify With.Provider to select provider alias; default alias is Identity.

Docs / Examples (part of DoD)

Documentation

Add a provider reference page:

  • docs/reference/provider-entraID.md

It MUST document:

  • Required host prerequisites for both auth modes (delegated vs. app-only) at a conceptual level.
    • Because auth is host-owned via AuthSessionBroker, the doc must focus on what the broker must return for this provider.
  • Required Graph permissions/scopes (high-level guidance).
  • Identity resolution rules (UPN vs. objectId) and canonical keys.
  • Entitlement model (groups; canonical id = group objectId).
  • Safety gate for delete (-AllowDelete).

Example workflows

Provide at least one workflow per LifecycleEvent under examples/workflows/:

  • Joiner
    • CreateIdentity + EnsureAttribute + EnsureEntitlement (group grants)
  • Mover
    • EnsureAttribute changes + group delta
  • Leaver
    • DisableIdentity + optional DeleteIdentity
    • Delete MUST require explicit opt-in and capability requirement

Demo runner integration

Integrate with the single demo runner:

  • Update examples/Invoke-IdleDemo.ps1 to support -Provider EntraID (and keep Mock as default).
  • The demo must show how to supply:
    • Providers.Identity = New-IdleEntraIDIdentityProvider
    • Providers.AuthSessionBroker = <host-created broker>

The demo MUST NOT prompt for credentials inside the provider module. Any interactive auth must be isolated to the demo runner (host role).

Tests (part of DoD)

Unit tests (no live Entra ID)

  • Add Pester unit tests for IdLE.Provider.EntraID using a fake adapter injected via -Adapter.
  • Tests MUST cover:
    • Capability list (including Delete gating)
    • Idempotency rules for all methods
    • Group resolution ambiguity handling (if displayName resolution is supported)
    • Transient error marking behavior (adapter throws with Idle.IsTransient = $true for 429/5xx/timeouts)

Contract tests

  • Add provider contract tests similar in spirit to existing provider tests:
    • Validate required method presence when capability is advertised.
    • Validate AuthSession parameter is supported where applicable.

Acceptance criteria

  • Provider module IdLE.Provider.EntraID loads and advertises capabilities.
  • MVP capabilities are implemented and covered by unit tests (mocked; no live Entra ID required).
  • Idempotency rules are enforced and test-covered.
  • Example workflows run via the demo runner using -Provider EntraID (documented commands).

Definition of done

  • Pester: green
  • PSScriptAnalyzer: green
  • Documentation added/updated (provider reference + example usage)
  • Examples added and demo runner updated
  • No secrets/tokens introduced into workflow files, plan exports, or event streams

References

#46
#77
#91

Metadata

Metadata

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions