Public/Invoke-IdleStepEnsureAttributes.ps1

function Invoke-IdleStepEnsureAttributes {
    <#
    .SYNOPSIS
    Ensures that multiple identity attributes match their desired values.

    .DESCRIPTION
    This is a provider-agnostic step that can ensure multiple attributes in a single step.
    The host must supply a provider instance via Context.Providers[<ProviderAlias>].

    Provider interaction strategy:
    1. If the provider implements EnsureAttributes(IdentityKey, AttributesHashtable), it is called once (fast path).
    2. Otherwise, the step falls back to calling EnsureAttribute(IdentityKey, Name, Value) for each attribute.

    The step is idempotent by design: it converges state to the desired values.

    Authentication:
    - If With.AuthSessionName is present, the step acquires an auth session via
      Context.AcquireAuthSession(Name, Options) and passes it to the provider method
      if the provider supports an AuthSession parameter.
    - With.AuthSessionOptions (optional, hashtable) is passed to the broker for
      session selection (e.g., @{ Role = 'Tier0' }).
    - ScriptBlocks in AuthSessionOptions are rejected (security boundary).

    .PARAMETER Context
    Execution context created by IdLE.Core.

    .PARAMETER Step
    Normalized step object from the plan. Must contain a 'With' hashtable.

    .OUTPUTS
    PSCustomObject (PSTypeName: IdLE.StepResult)
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [object] $Context,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [object] $Step
    )

    $with = $null
    if ($Step.PSObject.Properties.Name -contains 'With') {
        $with = $Step.With
    }
    
    if ($null -eq $with -or -not ($with -is [hashtable])) {
        throw "EnsureAttributes requires 'With' to be a hashtable."
    }

    if (-not $with.ContainsKey('IdentityKey')) {
        throw "EnsureAttributes requires With.IdentityKey."
    }

    if (-not $with.ContainsKey('Attributes')) {
        throw "EnsureAttributes requires With.Attributes."
    }

    $attributes = $with.Attributes
    if ($null -eq $attributes -or -not ($attributes -is [hashtable])) {
        throw "EnsureAttributes requires With.Attributes to be a hashtable."
    }

    if ($attributes.Count -eq 0) {
        throw "EnsureAttributes requires With.Attributes to contain at least one attribute."
    }

    $providerAlias = if ($with.ContainsKey('Provider')) { [string]$with.Provider } else { 'Identity' }

    if (-not ($Context.PSObject.Properties.Name -contains 'Providers')) {
        throw "Context does not contain a Providers hashtable."
    }
    if ($null -eq $Context.Providers -or -not ($Context.Providers -is [hashtable])) {
        throw "Context.Providers must be a hashtable."
    }
    if (-not $Context.Providers.ContainsKey($providerAlias)) {
        throw "Provider '$providerAlias' was not supplied by the host."
    }

    $provider = $Context.Providers[$providerAlias]
    
    # Check if provider has EnsureAttributes method (fast path)
    $hasEnsureAttributes = $null -ne $provider.PSObject.Methods['EnsureAttributes']
    
    $anyChanged = $false
    $attributeResults = @()
    
    if ($hasEnsureAttributes) {
        # Fast path: call EnsureAttributes once
        $result = Invoke-IdleProviderMethod `
            -Context $Context `
            -With $with `
            -ProviderAlias $providerAlias `
            -MethodName 'EnsureAttributes' `
            -MethodArguments @([string]$with.IdentityKey, $attributes)
        
        if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) {
            $anyChanged = [bool]$result.Changed
        }
        
        # If provider returns per-attribute details, use them
        if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Attributes')) {
            $attributeResults = $result.Attributes
        } else {
            # Provider doesn't return per-attribute details, so we can't determine individual attribute changes
            # Report overall status but mark individual attribute change status as unknown
            foreach ($key in $attributes.Keys) {
                $attributeResults += @{
                    Name    = $key
                    Changed = $anyChanged  # Overall result - individual changes unknown without provider details
                    Error   = $null
                }
            }
        }
    }
    else {
        # Fallback: call EnsureAttribute for each attribute
        foreach ($key in $attributes.Keys) {
            $attrName = [string]$key
            $attrValue = $attributes[$key]
            
            try {
                $result = Invoke-IdleProviderMethod `
                    -Context $Context `
                    -With $with `
                    -ProviderAlias $providerAlias `
                    -MethodName 'EnsureAttribute' `
                    -MethodArguments @([string]$with.IdentityKey, $attrName, $attrValue)
                
                $changed = $false
                if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Changed')) {
                    $changed = [bool]$result.Changed
                }
                
                if ($changed) {
                    $anyChanged = $true
                }
                
                $attributeResults += @{
                    Name    = $attrName
                    Changed = $changed
                    Error   = $null
                }
            }
            catch {

                throw
            }
        }
    }

    return [pscustomobject]@{
        PSTypeName = 'IdLE.StepResult'
        Name       = [string]$Step.Name
        Type       = [string]$Step.Type
        Status     = 'Completed'
        Changed    = $anyChanged
        Error      = $null
        Data       = @{
            Attributes = $attributeResults
        }
    }
}