Datum.psm1
Class FileProvider { hidden $Path hidden [hashtable] $DataOptions FileProvider ($Path,$DataOptions) { $this.DataOptions = $DataOptions $this.Path = Get-Item $Path -ErrorAction SilentlyContinue $Result = Get-ChildItem $path | ForEach-Object { if($_.PSisContainer) { $val = [scriptblock]::Create("New-DatumFileProvider -Path `"$($_.FullName)`" -DataOptions `$this.DataOptions") $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val } else { $val = [scriptblock]::Create("Get-FileProviderData -Path `"$($_.FullName)`" -DataOptions `$this.DataOptions") $this | Add-Member -MemberType ScriptProperty -Name $_.BaseName -Value $val } } } } #$ConfigurationData = [fileProvider]::new($PWD.Path,@{}) #($ConfigurationData.AllNodes.psobject.Properties | % { $ConfigurationData.AllNodes.($_.Name) })[1] Class Node : hashtable { Node([hashtable]$NodeData) { $NodeData.keys | % { $This[$_] = $NodeData[$_] } $this | Add-member -MemberType ScriptProperty -Name Roles -Value { $PathArray = $ExecutionContext.InvokeCommand.InvokeScript('Get-PSCallStack')[2].Position.text -split '\.' $PropertyPath = $PathArray[2..($PathArray.count-1)] -join '\' Write-warning "Resolve $PropertyPath" $obj = [PSCustomObject]@{} $currentNode = $obj if($PathArray.Count -gt 3) { foreach ($property in $PathArray[2..($PathArray.count-2)]) { Write-Debug "Adding $Property property" $currentNode | Add-member -MemberType NoteProperty -Name $property -Value ([PSCustomObject]@{}) $currentNode = $currentNode.$property } } Write-Debug "Adding Resolved property to last object's property $($PathArray[-1])" $currentNode | Add-member -MemberType NoteProperty -Name $PathArray[-1] -Value ($PropertyPath) return $obj } } static ResolveDscProperty($Path) { "Resolve-DscProperty $Path" } } Class SecureDatum { [hashtable] hidden $UnprotectParams SecureDatum($Object,[hashtable]$UnprotectParams) { $this.UnprotectParams = $UnprotectParams if($Object -is [hashtable]) { $Object = [PSCustomObject]$Object } if ($Object -is [PSCustomObject]) { foreach ($Property in $Object.PSObject.Properties.name) { $MemberTypeParams = @{ MemberType = 'NoteProperty' Name = $Property Value = ([SecureDatum]::GetObject($Object.$Property,$UnprotectParams)) } if ($MemberTypeParams.Value -is [scriptblock]) { $MemberTypeParams.MemberType = 'ScriptProperty' } $This | Add-Member @MemberTypeParams } } } [string] ToString() { return "{$($this.PSObject.Properties.Name -join ', ')}" } static [object] GetObject($object,$UnprotectParams) { if($null -eq $object) { return $null } elseif($object -is [PSCustomObject] -or $object -is [hashtable]) { return ([SecureDatum]::new($object,$UnprotectParams)) } elseif ($object -is [System.Collections.IEnumerable] -and $object -isnot [string]) { $collection = @() $collection = foreach ($item in $object) { [SecureDatum]::GetObject($item,$UnprotectParams) } return $collection } elseif($object -is [string] -and $object -match "^\[ENC=[\w\W]*\]$") { $UnprotectScriptBlock = " `$Base64Data = `"$object`" `[SecureDatum]::Unprotect(`$Base64Data.Trim(),`$this.UnprotectParams) " return ([scriptblock]::Create($UnprotectScriptBlock)) } else { return $object } } static [object] Unprotect($object,$UnprotectParams) { return (Unprotect-Datum -Base64Data $object @UnprotectParams) } } function ConvertTo-Hashtable { param ( [Parameter(ValueFromPipeline)] $InputObject ) process { if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Collections.Hashtable]) { return $InputObject } elseif ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) { $collection = @( foreach ($object in $InputObject) { ConvertTo-Hashtable $object } ) Write-Output -NoEnumerate $collection } elseif ($InputObject -is [psobject]) { $hash = @{} foreach ($property in $InputObject.PSObject.Properties) { $hash[$property.Name] = ConvertTo-Hashtable $property.Value } $hash } else { $InputObject } } } function ConvertTo-ProtectedDatum {###########ConvertTo-DatumSecureObjectReader param ( [Parameter(ValueFromPipeline)] $InputObject, $UnprotectOptions ) process { if ($UnprotectOptions.ContainsKey('ClearTextPassword')) { $UnprotectOptions['password'] = $UnprotectOptions.ClearTextPassword | ConvertTo-SecureString -AsPlainText -force $null = $UnprotectOptions.remove('ClearTextPassword') } elseif ($UnprotectOptions.ContainsKey('SecureStringPassword')) { $UnprotectOptions['password'] = $UnprotectOptions.SecureStringPassword | ConvertTo-SecureString $null = $UnprotectOptions.remove('SecureStringPassword') } [SecureDatum]::GetObject($InputObject,$UnprotectOptions) } } #Requires -module powershell-yaml #Using Module Datum function Get-FileProviderData { [CmdletBinding()] Param( $Path, [AllowNull()] $DataOptions ) Write-Verbose "Getting File Provider Data for Path: $Path" $File = Get-Item -Path $Path switch ($File.Extension) { '.psd1' { Import-PowerShellDataFile $File } '.json' { Get-Content -Raw $Path | ConvertFrom-Json | ConvertTo-Hashtable } '.yml' { convertfrom-yaml (Get-Content -raw $Path) | ConvertTo-Hashtable } '.ejson'{ Get-Content -Raw $Path | ConvertFrom-Json | ConvertTo-ProtectedDatum -UnprotectOptions $DataOptions} '.eyaml'{ ConvertFrom-Yaml (Get-Content -Raw $Path) | ConvertTo-ProtectedDatum -UnprotectOptions $DataOptions} '.epsd1'{ Import-PowerShellDatafile $File | ConvertTo-ProtectedDatum -UnprotectOptions $DataOptions} Default { Get-Content -Raw $Path } } } function New-DatumFileProvider { Param( [alias('DataDir')] $Path, [AllowNull()] $DataOptions ) [FileProvider]::new($Path,$DataOptions) } <# Datum Structure is a PSCustomObject To that object we add DatumStores as Script Properties/Class instances Those Properties embed the mechanism to call the container hierarchy and the RAW value of the items The format of the item defines its method of conversion from raw to Object #> function New-DatumStructure { [CmdletBinding()] Param ( $Structure ) $root = @{} foreach ($store in $Structure.DatumStructure){ $StoreParams = Convertto-hashtable $Store.StoreOptions $cmd = Get-Command ("{0}\New-Datum{1}Provider" -f ($store.StoreProvider -split '::')) $storeObject = &$cmd @StoreParams $root.Add($store.StoreName,$storeObject) } [PSCustomObject]$root } #Requires -Modules ProtectedData function Protect-Datum { [CmdletBinding()] [OutputType([PSObject])] Param ( # Serialized Protected Data represented on Base64 encoding [Parameter( Mandatory ,Position=0 ,ValueFromPipeline ,ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [PSObject] $InputObject, # By Password only for development / Test purposes [Parameter( ParameterSetName='ByPassword' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [System.Security.SecureString] $Password, # Specify the Certificate to be used by ProtectedData [Parameter( ParameterSetName='ByCertificate' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $Certificate, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Int] $MaxLineLength = 100, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Header = '[ENC=', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Footer = ']', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Switch] $NoEncapsulation ) begin { } process { Write-Verbose "Deserializing the Object from Base64" $ProtectDataParams = @{ InputObject = $InputObject } Write-verbose "Calling Protect-Data $($PSCmdlet.ParameterSetName)" Switch ($PSCmdlet.ParameterSetName) { 'ByCertificae' { $ProtectDataParams.Add('Certificate',$Certificate)} 'ByPassword' { $ProtectDataParams.Add('Password',$Password) } } $securedData = Protect-Data @ProtectDataParams $xml = [System.Management.Automation.PSSerializer]::Serialize($securedData, 5) $bytes = [System.Text.Encoding]::UTF8.GetBytes($xml) $Base64String = [System.Convert]::ToBase64String($bytes) if($MaxLineLength -gt 0) { $Base64DataBlock = [regex]::Replace($Base64String,"(.{$MaxLineLength})","`$1`r`n") } else { $Base64DataBlock = $Base64String } if(!$NoEncapsulation) { $Header,$Base64DataBlock,$Footer -join '' } else { $Base64DataBlock } } } Function Resolve-Datum { [cmdletBinding()] Param( [Parameter( Mandatory )] [string] $PropertyPath, $Node, $Default, [Parameter( Mandatory )] [string[]] $searchPaths, [Parameter( Mandatory )] $DatumStructure = $DatumStructure, $MaxDepth, [ValidateSet('MostSpecific','AllValues')] $SearchBehavior = 'MostSpecific' ) $Pattern = '(?<opening><%=)(?<sb>.*?)(?<closure>%>)' $splitPattern = '\' + [IO.Path]::DirectorySeparatorChar $Depth = 0 $MergeResult = $null # Walk every search path in listed order, and return datum when found at end of path foreach ($SearchPath in $searchPaths) { $ArraySb = [System.Collections.ArrayList]@() $CurrentSearch = Join-Path $searchPath $PropertyPath #extract script block for execution $newSearch = [regex]::Replace($CurrentSearch, $Pattern, { param($match) $expr = $match.groups['sb'].value $index = $ArraySb.Add($expr) "`$({$index})" }, @('IgnoreCase', 'SingleLine', 'MultiLine')) $PathStack = $newSearch -split $splitPattern $DatumFound = Resolve-DatumPath -Node $Node -DatumStructure $DatumStructure -PathStack $PathStack -PathVariables $ArraySb #Stop processing further path when the Max depth is reached # or when you found the first value if (($DatumFound -and ($SearchBehavior -eq 'MostSpecific')) -or ($Depth -eq $MaxDepth)) { Write-Debug "Depth: $depth; Search Behavior: $SearchBehavior" $DatumFound return } elseif($DatumFound -and ($SearchBehavior -eq 'AllValues')) { $DatumFound } #Add Those merge Behaviour: # Unique # Hash # Deep merge # https://docs.puppet.com/puppet/5.0/hiera_merging.html # Configure Merge Behaviour in the Datum structure (as per Puppet hiera) } } function Resolve-DatumPath { [CmdletBinding()] param( $Node, $DatumStructure, [string[]] $PathStack, [System.Collections.ArrayList] $PathVariables ) $currentNode = $DatumStructure foreach ($StackItem in $PathStack) { $RelativePath = $PathStack[0..$PathStack.IndexOf($StackItem)] Write-Verbose "Current relative Path: $($RelativePath -join '\')" $LeftOfStack = $PathStack[$PathStack.IndexOf($StackItem)..($PathStack.Count-1)] Write-Verbose "Left Path to search: $($LeftOfStack -join '\')" if ( $StackItem -match '\{\d+\}') { Write-Debug -Message "Replacing expression $StackItem" $StackItem = [scriptblock]::Create( ($StackItem -f ([string[]]$PathVariables)) ).Invoke() Write-Debug -Message ($StackItem | Format-List * | Out-String) $PathItem = $stackItem } else { $PathItem = $CurrentNode.($ExecutionContext.InvokeCommand.ExpandString($StackItem)) } switch ($PathItem) { $null { Write-Verbose -Message "NULL FOUND AT PATH: $(($RelativePath -join '\') -f [string[]]$PathVariables) before reaching $($LeftOfStack -join '\')" Return $null } {$_.GetType() -eq [hashtable]} { $CurrentNode = $PathItem; Break } default { $CurrentNode = $PathItem; break } } if ($LeftOfStack.Count -eq 1) { Write-Output $CurrentNode } } } #Requires -Modules ProtectedData function Unprotect-Datum { [CmdletBinding()] [OutputType([PSObject])] Param ( # Serialized Protected Data represented on Base64 encoding [Parameter( Mandatory ,Position=0 ,ValueFromPipeline ,ValueFromPipelineByPropertyName )] [ValidateNotNullOrEmpty()] [string] $Base64Data, # By Password only for development / Test purposes [Parameter( ParameterSetName='ByPassword' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [System.Security.SecureString] $Password, # Specify the Certificate to be used by ProtectedData [Parameter( ParameterSetName='ByCertificate' ,Mandatory ,Position=1 ,ValueFromPipelineByPropertyName )] [String] $Certificate, # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Header = '[ENC=', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [String] $Footer = ']', # Number of columns before inserting newline in chunk [Parameter( ValueFromPipelineByPropertyName )] [Switch] $NoEncapsulation ) begin { } process { if (!$NoEncapsulation) { Write-Verbose "Removing $header DATA $footer " $Base64Data = $Base64Data -replace "^$([regex]::Escape($Header))" -replace "$([regex]::Escape($Footer))$" } Write-Verbose "Deserializing the Object from Base64" $bytes = [System.Convert]::FromBase64String($Base64Data) $xml = [System.Text.Encoding]::UTF8.GetString($bytes) $obj = [System.Management.Automation.PSSerializer]::Deserialize($xml) $UnprotectDataParams = @{ InputObject = $obj } Write-verbose "Calling Unprotect-Data $($PSCmdlet.ParameterSetName)" Switch ($PSCmdlet.ParameterSetName) { 'ByCertificae' { $UnprotectDataParams.Add('Certificate',$Certificate)} 'ByPassword' { $UnprotectDataParams.Add('Password',$Password) } } Unprotect-Data @UnprotectDataParams } } |