Skip to content

Template resolution coerces scalars to string (breaks booleans like Enabled=$false) #166

@blindzero

Description

@blindzero

Description

IdLE template resolution currently coerces resolved placeholder values to string unconditionally.

This breaks scenarios where a step expects a typed scalar (e.g. bool) and the workflow uses a template placeholder like:

  • Enabled = '{{Request.DesiredState.Enabled}}'

Even if Request.DesiredState.Enabled is a real boolean $false, the template resolver returns "False" (a non-empty string). In PowerShell, casting a non-empty string to [bool] yields $true, so the user ends up enabled unexpectedly.

This is observed with IdLE.Step.CreateIdentity (AD provider), where Enabled is derived from Attributes['Enabled'] via [bool] conversion.

This is a fail-fast correctness bug: it silently flips user intent for boolean parameters.

Step-0 Analysis (Safe)

What happens today

  1. Request contains a real boolean:

    • DesiredState.Enabled = $false
  2. Workflow contains a template placeholder:

    • Enabled = '{{Request.DesiredState.Enabled}}'
  3. The template resolver resolves placeholders and coerces to string (effectively [string]$resolvedValue).

  4. The AD provider adapter reads the resolved value:

    • $enabled = [bool]$Attributes['Enabled']

In PowerShell:

  • [bool]"False" is $true (because it's a non-empty string).

Why this is not a user error

  • Users are doing the correct thing: passing a boolean in the request.
  • The workflow template language suggests typed data flows should work (especially for scalar placeholders).
  • The engine should preserve the type when the value is a pure placeholder, otherwise typed step inputs cannot be safely templated.

Steps to Reproduce

  1. Create a request:

    $joinerReq = New-IdleLifecycleRequestObject -LifecycleEvent 'Joiner' -Actor $env:USERNAME `
      -IdentityKeys @{ SamAccountName = 'foo.bar' } `
      -DesiredState @{ Enabled = $false }
  2. Use a workflow step that templates Enabled:

    @{
      Name = 'Create AD account'
      Type = 'IdLE.Step.CreateIdentity'
      With = @{
        Provider    = 'AD'
        IdentityKey = '{{Request.IdentityKeys.SamAccountName}}'
        Attributes  = @{
          Enabled = '{{Request.DesiredState.Enabled}}'
        }
      }
    }
  3. Execute the plan (any host runner).

Expected Behavior

  • Enabled should remain a boolean False after template resolution.
  • The resulting AD account should be created disabled (or at least the provider should see $enabled -eq $false).

Actual Behavior

  • The template resolver returns "False" (string).
  • The AD provider converts it to boolean and receives $true.
  • The account is created enabled (or Enabled behaves inverted).

Scope / Impact

  • Affects any step/provider relying on typed scalar inputs (bool, int, datetime, guid, etc.) when values are supplied via templates.
  • High risk: produces silent incorrect state and can cause security/process issues (e.g., accounts enabled when they should be disabled).
  • Relevant for 1.0 because template-based workflows are a core feature.

Proposed Fix (Agent-Safe)

Fix rule

When a workflow value is exactly one template placeholder (no prefix/suffix text), the template engine must return the resolved value as-is (typed object) instead of coercing to string.

Examples:

  • Input: '{{Request.DesiredState.Enabled}}'

    • Output type: bool (same as Request.DesiredState.Enabled)
  • Input: 'Enabled={{Request.DesiredState.Enabled}}' (mixed string)

    • Output type: string (string interpolation remains valid)

Implementation outline

  1. In the template resolution function (currently Resolve-IdleTemplateString or wherever the placeholder substitution is performed):

    • Detect "pure placeholder" strings using a strict regex, e.g. ^\s*\{\{.+?\}\}\s*$
    • If it is a pure placeholder:
      • Resolve the placeholder and return the underlying value (object) without converting to string.
    • Else:
      • Perform the current string interpolation behavior.
  2. Ensure this behavior is applied consistently wherever workflow templates are resolved (arrays/hashtables/nested structures).

  3. Add unit tests for:

    • bool $false stays $false (typed)
    • bool $true stays $true
    • int stays int
    • datetime stays datetime
    • mixed string interpolation still returns string
  4. Add a regression test for the AD CreateIdentity scenario:

    • CreateIdentity receives a boolean $false for Enabled when templated.

Backwards compatibility

This is a bug fix. Typed preservation only happens for pure placeholders and should not break existing string interpolation patterns.

Additional Notes / References

  • The AD provider currently interprets Enabled via [bool]$Attributes['Enabled'], which is correct given a boolean input, but becomes wrong after string coercion.
  • After this fix, provider code does not need to change; template resolution will supply correct types.

Environment

  • PowerShell version: 7.x (PowerShell Core)
  • OS: Windows (RSAT/AD tooling scenario)
  • IdLE version / commit: current main / latest release at time of report

Metadata

Metadata

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions