diff --git a/docs/advanced/architecture.md b/docs/advanced/architecture.md index 68d9d651..10a44749 100644 --- a/docs/advanced/architecture.md +++ b/docs/advanced/architecture.md @@ -104,3 +104,73 @@ See: [Extensibility](extensibility.md). IdLE treats workflow configuration and lifecycle requests as **untrusted data** and validates that they contain no ScriptBlocks. Host-provided extension points (step registry, providers, external event sinks) are **trusted inputs** and are validated for safe shapes (object contracts). For details, see `advanced/security.md`. + +## v1.0 Public API and Contracts + +### Supported Commands + +The **supported public API** for v1.0 consists of the following commands exported from the IdLE meta-module: + +- `Test-IdleWorkflow` +- `New-IdleLifecycleRequest` +- `New-IdlePlan` +- `Invoke-IdlePlan` +- `Export-IdlePlan` +- `New-IdleAuthSession` + +**Source of truth**: `src/IdLE/IdLE.psd1` → `FunctionsToExport` + +Only these commands are considered **stable contracts**. Internal modules (IdLE.Core, IdLE.Steps.*, IdLE.Provider.*) are unsupported when imported directly. + +### Command Contracts + +For supported commands, the following are **stable contracts** (breaking changes require a major version): + +- Command name +- Parameter names and parameter sets +- Observable semantics (mandatory/optional/default behavior) +- Output type identity at a coarse level (PSTypeName) + +The following are **not contracts** and may change in minor/patch versions: + +- Exact error message strings +- Undocumented internal object properties +- Internal module cmdlets + +### Data Contracts + +**Workflow authoring contract** (PSD1): +- Format: PSD1 workflow definitions validated by `Test-IdleWorkflow` +- Unknown keys: **FAIL** (strict validation) +- Required fields (Name, LifecycleEvent, Steps[].Name, Steps[].Type): **FAIL** if null/empty +- `With` payload values: allow `null` and empty strings (supports "clear attribute" scenarios) + +**Lifecycle request contract**: +- Required fields: `LifecycleEvent`, `CorrelationId` +- Optional fields: `Actor`, `IdentityKeys`, `DesiredState`, `Changes` + +**Plan export contract** (JSON): +- Format: JSON from `Export-IdlePlan` +- Schema and semantics are stable +- See [Plan export specification](../specs/plan-export.md) + +### Capability ID Baseline (v1.0) + +The following capability IDs are frozen as the v1.0 baseline: + +- `IdLE.DirectorySync.Status` - Read directory sync status +- `IdLE.DirectorySync.Trigger` - Trigger directory sync +- `IdLE.Entitlement.Grant` - Grant group membership/entitlement +- `IdLE.Entitlement.List` - List user entitlements +- `IdLE.Entitlement.Revoke` - Revoke group membership/entitlement +- `IdLE.Identity.Attribute.Ensure` - Ensure identity attribute value +- `IdLE.Identity.Create` - Create identity +- `IdLE.Identity.Delete` - Delete identity +- `IdLE.Identity.Disable` - Disable identity +- `IdLE.Identity.Enable` - Enable identity +- `IdLE.Identity.Move` - Move identity (OU/container) +- `IdLE.Mailbox.Info.Read` - Read mailbox metadata/configuration +- `IdLE.Mailbox.OutOfOffice.Ensure` - Ensure Out of Office configuration +- `IdLE.Mailbox.Type.Ensure` - Ensure mailbox type (User/Shared/etc.) + +**Deprecated (pre-1.0)**: `IdLE.Mailbox.Read` → automatically mapped to `IdLE.Mailbox.Info.Read` with deprecation warning during planning. diff --git a/docs/advanced/releases.md b/docs/advanced/releases.md index d545ad04..90949fc2 100644 --- a/docs/advanced/releases.md +++ b/docs/advanced/releases.md @@ -29,6 +29,54 @@ These checks prevent "broken" releases (e.g., tagging the wrong commit or forget ## Versioning policy +IdLE follows [Semantic Versioning](https://semver.org/): + +- **MAJOR** (breaking): Incompatible API changes +- **MINOR** (feature): Backward-compatible functionality additions +- **PATCH** (fix): Backward-compatible bug fixes + +### What Constitutes a Breaking Change + +The following are **breaking changes** and require a new major version: + +- Removing a supported command +- Renaming a supported command +- Removing a parameter +- Renaming a parameter +- Changing a parameter from optional to mandatory +- Changing a parameter's type in an incompatible way +- Removing fields from workflow/request/plan contracts +- Renaming fields in workflow/request/plan contracts + +The following are **non-breaking** (allowed in minor/patch versions): + +- Adding a new command +- Adding a new parameter (must be optional with a sensible default) +- Changing exact error message strings +- Adding new output properties (output types are coarse-grained) +- Internal implementation changes + +### Deprecation Mechanism + +Deprecated supported cmdlets/parameters **MUST** emit a `Write-Warning` on use: + +**Format**: +``` +DEPRECATED: is deprecated in and will be removed in . +Use instead. +``` + +**Example**: +```powershell +Write-Warning "DEPRECATED: Parameter 'OldName' is deprecated in v1.2 and will be removed in v2.0. Use 'NewName' instead." +``` + +**Timeline**: Deprecated features will be supported for **at least one minor version** before removal in the next major version. + +**Example timeline**: +- Deprecated in v1.2 → Removed in v2.0 +- Deprecated in v1.8 → Removed in v2.0 + ### Stable tags Stable releases use tags in the form: diff --git a/docs/reference/cmdlets.md b/docs/reference/cmdlets.md index 046e775c..96558f22 100644 --- a/docs/reference/cmdlets.md +++ b/docs/reference/cmdlets.md @@ -9,6 +9,7 @@ This page links the generated per-cmdlet reference pages and includes their syno | --- | --- | | [Export-IdlePlan](cmdlets/Export-IdlePlan.md) | Exports an IdLE LifecyclePlan as a canonical JSON artifact. | | [Invoke-IdlePlan](cmdlets/Invoke-IdlePlan.md) | Executes an IdLE plan. | +| [New-IdleAuthSession](cmdlets/New-IdleAuthSession.md) | Creates a simple AuthSessionBroker for use with IdLE providers. | | [New-IdleLifecycleRequest](cmdlets/New-IdleLifecycleRequest.md) | Creates a lifecycle request object. | | [New-IdlePlan](cmdlets/New-IdlePlan.md) | Creates a deterministic plan from a lifecycle request and a workflow definition. | | [Test-IdleWorkflow](cmdlets/Test-IdleWorkflow.md) | Validates an IdLE workflow definition file. | diff --git a/docs/reference/cmdlets/New-IdleAuthSession.md b/docs/reference/cmdlets/New-IdleAuthSession.md new file mode 100644 index 00000000..a04183bf --- /dev/null +++ b/docs/reference/cmdlets/New-IdleAuthSession.md @@ -0,0 +1,94 @@ +--- +external help file: IdLE-help.xml +Module Name: IdLE +online version: +schema: 2.0.0 +--- + +# New-IdleAuthSession + +## SYNOPSIS +Creates a simple AuthSessionBroker for use with IdLE providers. + +## SYNTAX + +``` +New-IdleAuthSession [-SessionMap] [[-DefaultCredential] ] + [-ProgressAction ] [] +``` + +## DESCRIPTION +Creates an AuthSessionBroker that routes authentication based on user-defined options. +The broker is used by steps to acquire credentials at runtime without embedding +secrets in workflows or provider construction. + +This is a thin wrapper that delegates to IdLE.Core\New-IdleAuthSessionBroker. + +## EXAMPLES + +### EXAMPLE 1 +``` +$broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Tier0' } = $tier0Credential +} +``` + +## PARAMETERS + +### -SessionMap +A hashtable that maps session configurations to credentials. + +```yaml +Type: Hashtable +Parameter Sets: (All) +Aliases: + +Required: True +Position: 1 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DefaultCredential +Optional default credential to return when no session options are provided. + +```yaml +Type: PSCredential +Parameter Sets: (All) +Aliases: + +Required: False +Position: 2 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ProgressAction +{{ Fill ProgressAction Description }} + +```yaml +Type: ActionPreference +Parameter Sets: (All) +Aliases: proga + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### PSCustomObject with AcquireAuthSession method +## NOTES +For detailed documentation, see: Get-Help IdLE.Core\New-IdleAuthSessionBroker -Full + +## RELATED LINKS diff --git a/docs/reference/providers/provider-ad.md b/docs/reference/providers/provider-ad.md index b3ea84d1..c59bc5c2 100644 --- a/docs/reference/providers/provider-ad.md +++ b/docs/reference/providers/provider-ad.md @@ -101,7 +101,7 @@ $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Provider Use an AuthSessionBroker to manage authentication centrally and enable multi-role scenarios. -**Simple approach with New-IdleAuthSessionBroker:** +**Simple approach with New-IdleAuthSession:** ```powershell # Assuming you have credentials available (e.g., from a secure vault or credential manager) @@ -112,7 +112,7 @@ $adminCredential = Get-Credential -Message "Enter regular admin credentials" $provider = New-IdleADIdentityProvider # Create broker with role-based credential mapping -$broker = New-IdleAuthSessionBroker -SessionMap @{ +$broker = New-IdleAuthSession -SessionMap @{ @{ Role = 'Tier0' } = $tier0Credential @{ Role = 'Admin' } = $adminCredential } -DefaultCredential $adminCredential @@ -195,8 +195,8 @@ $targetCred = Get-Credential -Message "Enter Target AD admin credentials" $sourceAD = New-IdleADIdentityProvider $targetAD = New-IdleADIdentityProvider -AllowDelete -# Use New-IdleAuthSessionBroker for domain-based credential routing -$broker = New-IdleAuthSessionBroker -SessionMap @{ +# Use New-IdleAuthSession for domain-based credential routing +$broker = New-IdleAuthSession -SessionMap @{ @{ Domain = 'Source' } = $sourceCred @{ Domain = 'Target' } = $targetCred } diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index 3095fca3..de561ee1 100644 --- a/docs/reference/providers/provider-entraID.md +++ b/docs/reference/providers/provider-entraID.md @@ -45,7 +45,7 @@ Connect-AzAccount $token = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token # Create broker -$broker = New-IdleAuthSessionBroker -SessionMap @{ +$broker = New-IdleAuthSession -SessionMap @{ @{} = $token } -DefaultCredential $token @@ -71,7 +71,7 @@ $tenantId = "your-tenant-id" $token = Get-GraphAppOnlyToken -ClientId $clientId -ClientSecret $clientSecret -TenantId $tenantId # Create broker -$broker = New-IdleAuthSessionBroker -SessionMap @{ +$broker = New-IdleAuthSession -SessionMap @{ @{} = $token } -DefaultCredential $token @@ -84,7 +84,7 @@ $broker = New-IdleAuthSessionBroker -SessionMap @{ $tier0Token = Get-GraphToken -Role 'Tier0' $adminToken = Get-GraphToken -Role 'Admin' -$broker = New-IdleAuthSessionBroker -SessionMap @{ +$broker = New-IdleAuthSession -SessionMap @{ @{ Role = 'Tier0' } = $tier0Token @{ Role = 'Admin' } = $adminToken } -DefaultCredential $adminToken diff --git a/src/IdLE.Core/IdLE.Core.psm1 b/src/IdLE.Core/IdLE.Core.psm1 index 969fd60f..5631c354 100644 --- a/src/IdLE.Core/IdLE.Core.psm1 +++ b/src/IdLE.Core/IdLE.Core.psm1 @@ -2,6 +2,13 @@ Set-StrictMode -Version Latest +# Internal module warning: discourage direct import unless explicitly allowed +# Suppress warning if IDLE_ALLOW_INTERNAL_IMPORT is set +# (IdLE meta-module sets this automatically; users can also set it for advanced scenarios) +if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { + Write-Warning "IdLE.Core is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass: `$env:IDLE_ALLOW_INTERNAL_IMPORT = '1'" +} + $PublicPath = Join-Path -Path $PSScriptRoot -ChildPath 'Public' $PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' diff --git a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 index 4956ae01..318faa58 100644 --- a/src/IdLE.Core/Public/New-IdlePlanObject.ps1 +++ b/src/IdLE.Core/Public/New-IdlePlanObject.ps1 @@ -169,7 +169,8 @@ function New-IdlePlanObject { ) } - $normalized += $s + # Normalize deprecated capabilities + $normalized += ConvertTo-IdleNormalizedCapability -Capability $s } return @($normalized | Sort-Object -Unique) @@ -217,6 +218,45 @@ function New-IdlePlanObject { return @($Providers) } + function ConvertTo-IdleNormalizedCapability { + <# + .SYNOPSIS + Normalizes capability identifiers and maps deprecated IDs to current ones. + + .DESCRIPTION + Handles capability ID migrations and deprecation warnings during planning. + Pre-1.0 deprecated capability IDs are mapped to their replacements and emit a warning. + + .PARAMETER Capability + The raw capability identifier to normalize. + + .OUTPUTS + Normalized capability identifier (string). + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $Capability + ) + + # Deprecated capability ID mappings (pre-1.0) + # Format: @{ 'OldID' = 'NewID' } + $deprecatedMappings = @{ + 'IdLE.Mailbox.Read' = 'IdLE.Mailbox.Info.Read' + } + + $normalized = $Capability.Trim() + + if ($deprecatedMappings.ContainsKey($normalized)) { + $newId = $deprecatedMappings[$normalized] + Write-Warning "DEPRECATED: Capability '$normalized' is deprecated in v1.0 and will be removed in v2.0. Use '$newId' instead." + return $newId + } + + return $normalized + } + function Get-IdleProviderCapabilities { <# .SYNOPSIS @@ -242,7 +282,18 @@ function New-IdlePlanObject { if ($null -eq $caps) { return @() } - return @($caps | Where-Object { $null -ne $_ } | ForEach-Object { ([string]$_).Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Sort-Object -Unique) + return @( + $caps | + Where-Object { $null -ne $_ } | + ForEach-Object { + $rawCap = ([string]$_).Trim() + if (-not [string]::IsNullOrWhiteSpace($rawCap)) { + ConvertTo-IdleNormalizedCapability -Capability $rawCap + } + } | + Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | + Sort-Object -Unique + ) } return @() diff --git a/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 b/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 index e949bb59..bf57fae4 100644 --- a/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 +++ b/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 @@ -150,7 +150,7 @@ function New-IdleExchangeOnlineProvider { $provider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { $caps = @( - 'IdLE.Mailbox.Read' + 'IdLE.Mailbox.Info.Read' 'IdLE.Mailbox.Type.Ensure' 'IdLE.Mailbox.OutOfOffice.Ensure' ) diff --git a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 index 5211dfbd..3485dbd2 100644 --- a/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 +++ b/src/IdLE.Steps.Common/IdLE.Steps.Common.psm1 @@ -1,6 +1,13 @@ #requires -Version 7.0 Set-StrictMode -Version Latest +# Internal module warning: discourage direct import unless explicitly allowed +# Suppress warning if IDLE_ALLOW_INTERNAL_IMPORT is set +# (IdLE meta-module sets this automatically; users can also set it for advanced scenarios) +if (-not $env:IDLE_ALLOW_INTERNAL_IMPORT) { + Write-Warning "IdLE.Steps.Common is an internal/unsupported module. Import 'IdLE' instead for the supported public API. To bypass: `$env:IDLE_ALLOW_INTERNAL_IMPORT = '1'" +} + $PrivatePath = Join-Path -Path $PSScriptRoot -ChildPath 'Private' if (Test-Path -Path $PrivatePath) { diff --git a/src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 b/src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 index 8d60b21d..efc17e47 100644 --- a/src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 +++ b/src/IdLE.Steps.Mailbox/Public/Get-IdleStepMetadataCatalog.ps1 @@ -19,7 +19,7 @@ function Get-IdleStepMetadataCatalog { .EXAMPLE $metadata = Get-IdleStepMetadataCatalog $metadata['IdLE.Step.Mailbox.GetInfo'].RequiredCapabilities - # Returns: @('IdLE.Mailbox.Read') + # Returns: @('IdLE.Mailbox.Info.Read') #> [CmdletBinding()] param() @@ -28,17 +28,17 @@ function Get-IdleStepMetadataCatalog { # IdLE.Step.Mailbox.GetInfo - read mailbox details $catalog['IdLE.Step.Mailbox.GetInfo'] = @{ - RequiredCapabilities = @('IdLE.Mailbox.Read') + RequiredCapabilities = @('IdLE.Mailbox.Info.Read') } # IdLE.Step.Mailbox.Type.Ensure - idempotent mailbox type conversion $catalog['IdLE.Step.Mailbox.Type.Ensure'] = @{ - RequiredCapabilities = @('IdLE.Mailbox.Read', 'IdLE.Mailbox.Type.Ensure') + RequiredCapabilities = @('IdLE.Mailbox.Info.Read', 'IdLE.Mailbox.Type.Ensure') } # IdLE.Step.Mailbox.OutOfOffice.Ensure - idempotent Out of Office configuration $catalog['IdLE.Step.Mailbox.OutOfOffice.Ensure'] = @{ - RequiredCapabilities = @('IdLE.Mailbox.Read', 'IdLE.Mailbox.OutOfOffice.Ensure') + RequiredCapabilities = @('IdLE.Mailbox.Info.Read', 'IdLE.Mailbox.OutOfOffice.Ensure') } return $catalog diff --git a/src/IdLE/IdLE.Init.ps1 b/src/IdLE/IdLE.Init.ps1 new file mode 100644 index 00000000..9f97d419 --- /dev/null +++ b/src/IdLE/IdLE.Init.ps1 @@ -0,0 +1,4 @@ +# IdLE Module Initialization Script +# This script runs BEFORE nested modules are loaded (via ScriptsToProcess in manifest) +# Set environment variable to suppress internal module warnings during correct nested load +$env:IDLE_ALLOW_INTERNAL_IMPORT = '1' diff --git a/src/IdLE/IdLE.psd1 b/src/IdLE/IdLE.psd1 index 77a3b171..14b107fc 100644 --- a/src/IdLE/IdLE.psd1 +++ b/src/IdLE/IdLE.psd1 @@ -7,6 +7,10 @@ Description = 'IdentityLifecycleEngine (IdLE) meta-module. Imports IdLE.Core and optional packs.' PowerShellVersion = '7.0' + # ScriptsToProcess runs BEFORE NestedModules are loaded + # This allows us to set environment variables to suppress internal module warnings + ScriptsToProcess = @('IdLE.Init.ps1') + NestedModules = @( '..\IdLE.Core\IdLE.Core.psd1', '..\IdLE.Steps.Common\IdLE.Steps.Common.psd1' @@ -18,7 +22,7 @@ 'New-IdlePlan', 'Invoke-IdlePlan', 'Export-IdlePlan', - 'New-IdleAuthSessionBroker' + 'New-IdleAuthSession' ) CmdletsToExport = @() AliasesToExport = @() diff --git a/src/IdLE/IdLE.psm1 b/src/IdLE/IdLE.psm1 index b3c51e68..4d47ef1c 100644 --- a/src/IdLE/IdLE.psm1 +++ b/src/IdLE/IdLE.psm1 @@ -93,5 +93,6 @@ Export-ModuleMember -Function @( 'New-IdleLifecycleRequest', 'New-IdlePlan', 'Invoke-IdlePlan', - 'Export-IdlePlan' + 'Export-IdlePlan', + 'New-IdleAuthSession' ) diff --git a/src/IdLE/Public/New-IdleAuthSession.ps1 b/src/IdLE/Public/New-IdleAuthSession.ps1 new file mode 100644 index 00000000..903e46ae --- /dev/null +++ b/src/IdLE/Public/New-IdleAuthSession.ps1 @@ -0,0 +1,57 @@ +# Re-export authentication session broker functionality from IdLE.Core. +# This wrapper is necessary because PowerShell's Export-ModuleMember can only export +# functions defined in the current module's scope. The wrapper creates the function +# in IdLE's scope, allowing it to be exported. +# +# The function is named New-IdleAuthSession to provide a clean public API name, +# while the Core implementation remains as New-IdleAuthSessionBroker. + +function New-IdleAuthSession { + <# + .SYNOPSIS + Creates a simple AuthSessionBroker for use with IdLE providers. + + .DESCRIPTION + Creates an AuthSessionBroker that routes authentication based on user-defined options. + The broker is used by steps to acquire credentials at runtime without embedding + secrets in workflows or provider construction. + + This is a thin wrapper that delegates to IdLE.Core\New-IdleAuthSessionBroker. + + .PARAMETER SessionMap + A hashtable that maps session configurations to credentials. + + .PARAMETER DefaultCredential + Optional default credential to return when no session options are provided. + + .EXAMPLE + $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Tier0' } = $tier0Credential + } + + .OUTPUTS + PSCustomObject with AcquireAuthSession method + + .NOTES + For detailed documentation, see: Get-Help IdLE.Core\New-IdleAuthSessionBroker -Full + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateNotNull()] + [hashtable] $SessionMap, + + [Parameter()] + [AllowNull()] + [PSCredential] $DefaultCredential + ) + + # Delegate to IdLE.Core implementation. + $params = @{ SessionMap = $SessionMap } + if ($PSBoundParameters.ContainsKey('DefaultCredential')) { + $params['DefaultCredential'] = $DefaultCredential + } + + return IdLE.Core\New-IdleAuthSessionBroker @params +} + diff --git a/tests/CapabilityDeprecation.Tests.ps1 b/tests/CapabilityDeprecation.Tests.ps1 new file mode 100644 index 00000000..263cc27d --- /dev/null +++ b/tests/CapabilityDeprecation.Tests.ps1 @@ -0,0 +1,88 @@ +Set-StrictMode -Version Latest + +BeforeAll { + . (Join-Path $PSScriptRoot '_testHelpers.ps1') + Import-IdleTestModule + + # Import mailbox steps module for capability metadata + $mailboxStepsPath = Join-Path $PSScriptRoot '..' 'src' 'IdLE.Steps.Mailbox' 'IdLE.Steps.Mailbox.psd1' + if (Test-Path $mailboxStepsPath) { + Import-Module $mailboxStepsPath -Force -ErrorAction SilentlyContinue + } +} + +Describe 'Capability Deprecation and Migration' { + Context 'IdLE.Mailbox.Read deprecation' { + It 'Maps deprecated IdLE.Mailbox.Read to IdLE.Mailbox.Info.Read with warning' { + # Create a mock provider that advertises the old capability + $mockProvider = [PSCustomObject]@{ + PSTypeName = 'IdLE.MockProvider' + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { + return @( + 'IdLE.Mailbox.Read' + 'IdLE.Mailbox.Type.Ensure' + 'IdLE.Mailbox.OutOfOffice.Ensure' + ) + } + + # Use a real workflow file that uses mailbox steps + $wfPath = Join-Path $PSScriptRoot '..' 'examples' 'workflows' 'live' 'exo-leaver-mailbox-offboarding.psd1' + + # Verify the workflow file exists + $wfPath | Should -Exist + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Leaver' + $providers = @{ MockProvider = $mockProvider } + + # Planning should succeed and emit a deprecation warning + # Capture warnings by redirecting stream 3 to output + $output = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers 3>&1 + + # Separate plan from warnings + $plan = $output | Where-Object { $_ -isnot [System.Management.Automation.WarningRecord] } + $warnings = $output | Where-Object { $_ -is [System.Management.Automation.WarningRecord] } + + # Assert plan was created successfully + $plan | Should -Not -BeNullOrEmpty + + # Assert deprecation warning was emitted + $warnings | Should -Not -BeNullOrEmpty + ($warnings | Out-String) | Should -Match "DEPRECATED.*IdLE\.Mailbox\.Read.*IdLE\.Mailbox\.Info\.Read" -Because "Should emit deprecation warning for IdLE.Mailbox.Read" + } + + It 'New capability IdLE.Mailbox.Info.Read does not emit warning' { + # Create a mock provider that advertises the new capability + $mockProvider = [PSCustomObject]@{ + PSTypeName = 'IdLE.MockProvider' + } + $mockProvider | Add-Member -MemberType ScriptMethod -Name GetCapabilities -Value { + return @( + 'IdLE.Mailbox.Info.Read' + 'IdLE.Mailbox.Type.Ensure' + 'IdLE.Mailbox.OutOfOffice.Ensure' + ) + } + + # Use a real workflow file + $wfPath = Join-Path $PSScriptRoot '..' 'examples' 'workflows' 'live' 'exo-leaver-mailbox-offboarding.psd1' + + $req = New-IdleLifecycleRequest -LifecycleEvent 'Leaver' + $providers = @{ MockProvider = $mockProvider } + + # Planning should succeed without deprecation warnings + $output = New-IdlePlan -WorkflowPath $wfPath -Request $req -Providers $providers 3>&1 + + # Separate plan from warnings + $plan = $output | Where-Object { $_ -isnot [System.Management.Automation.WarningRecord] } + $warnings = $output | Where-Object { $_ -is [System.Management.Automation.WarningRecord] } + + # Assert plan was created successfully + $plan | Should -Not -BeNullOrEmpty + + # Assert NO deprecation warning was emitted for the new capability + $matchedWarnings = $warnings | Where-Object { $_.Message -match "DEPRECATED.*IdLE\.Mailbox" } + $matchedWarnings | Should -BeNullOrEmpty -Because "New capability should not emit deprecation warning" + } + } +} diff --git a/tests/ModuleSurface.Tests.ps1 b/tests/ModuleSurface.Tests.ps1 index b74009dd..94473ea1 100644 --- a/tests/ModuleSurface.Tests.ps1 +++ b/tests/ModuleSurface.Tests.ps1 @@ -53,6 +53,7 @@ Describe 'Module manifests and public surface' { $expected = @( 'Invoke-IdlePlan' + 'New-IdleAuthSession' 'New-IdleLifecycleRequest' 'New-IdlePlan' 'Test-IdleWorkflow' @@ -204,4 +205,92 @@ Describe 'Module manifests and public surface' { (Get-Command -Module IdLE.Provider.Mock).Name | Should -Contain 'New-IdleMockIdentityProvider' } + + Context 'Internal module import warnings' { + It 'IdLE.Core emits warning when imported directly' { + $existingModule = Get-Module -Name IdLE.Core + if ($existingModule) { + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test direct import warning" + return + } + + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + + # Import and capture warning output + $output = Import-Module $corePsd1 -Force 3>&1 | Out-String + + $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" + $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + $output | Should -Match '\$env:IDLE_ALLOW_INTERNAL_IMPORT' -Because "Warning should show correct PowerShell syntax for bypass" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue + } + } + + It 'IdLE.Core does not emit warning when IDLE_ALLOW_INTERNAL_IMPORT is set' { + $existingModule = Get-Module -Name IdLE.Core + if ($existingModule) { + Set-ItResult -Skipped -Because "IdLE.Core is already loaded; cannot test bypass" + return + } + + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = '1' + + # Import and capture warning output + $output = Import-Module $corePsd1 -Force 3>&1 | Out-String + + $output | Should -BeNullOrEmpty -Because "Internal module should not emit warning when bypass is set" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE.Core -Force -ErrorAction SilentlyContinue + } + } + + It 'IdLE.Steps.Common emits warning when imported directly' { + $existingModule = Get-Module -Name IdLE.Steps.Common + if ($existingModule) { + Set-ItResult -Skipped -Because "IdLE.Steps.Common is already loaded; cannot test direct import warning" + return + } + + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + + # Import and capture warning output + $output = Import-Module $stepsPsd1 -Force 3>&1 | Out-String + + $output | Should -Not -BeNullOrEmpty -Because "Internal module should emit warning on direct import" + $output | Should -Match "internal.*unsupported.*IdLE.*instead" -Because "Warning should indicate module is internal and suggest importing IdLE" + $output | Should -Match '\$env:IDLE_ALLOW_INTERNAL_IMPORT' -Because "Warning should show correct PowerShell syntax for bypass" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE.Steps.Common -Force -ErrorAction SilentlyContinue + } + } + + It 'IdLE meta-module does not emit internal module warnings' { + $originalValue = $env:IDLE_ALLOW_INTERNAL_IMPORT + try { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $null + + # Import and capture warning output + $output = Import-Module $idlePsd1 -Force 3>&1 | Out-String + + $output | Should -BeNullOrEmpty -Because "IdLE meta-module should suppress internal module warnings via ScriptsToProcess" + } + finally { + $env:IDLE_ALLOW_INTERNAL_IMPORT = $originalValue + Remove-Module IdLE -Force -ErrorAction SilentlyContinue + } + } + } } diff --git a/tests/Providers/ExchangeOnlineProvider.Tests.ps1 b/tests/Providers/ExchangeOnlineProvider.Tests.ps1 index a3986e3a..2b2ffd1a 100644 --- a/tests/Providers/ExchangeOnlineProvider.Tests.ps1 +++ b/tests/Providers/ExchangeOnlineProvider.Tests.ps1 @@ -172,7 +172,7 @@ Describe 'ExchangeOnline provider - Unit tests' { Context 'GetCapabilities' { It 'returns mailbox-specific capabilities' { $caps = $provider.GetCapabilities() - $caps | Should -Contain 'IdLE.Mailbox.Read' + $caps | Should -Contain 'IdLE.Mailbox.Info.Read' $caps | Should -Contain 'IdLE.Mailbox.Type.Ensure' $caps | Should -Contain 'IdLE.Mailbox.OutOfOffice.Ensure' }