DSCResources/Library/JeaProxy.psm1
# Copyright © 2014, Microsoft Corporation. All rights reserved. Import-Module $PSScriptRoot\..\Library\Helper.psm1 Import-Module $PSScriptRoot\..\Library\JeaDir.psm1 Add-Type @' namespace Jea { using System.Collections; using System.Collections.Generic; using System.Globalization; public class Parameter { public string ValidatePattern; public string ValidateSet; public string ParameterType; public string Mandatory; } public class Proxy { public string Module; public string Name; public Hashtable Parameter; public Proxy() { Parameter = new Hashtable(System.StringComparer.InvariantCultureIgnoreCase); } } } '@ function ConvertTo-CSpec { [CmdletBinding()] Param ( [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)] $In ) Begin { } Process { if ($In.Module -or $In.Name) { new-object psobject -Property @{ Module = $(if ($In.Module ) {$In.module.Trim() }) Name = $(if ($In.Name ) {$In.Name.Trim() }else {'*'}) Parameter = $(if ($In.Parameter ) {$In.Parameter.Trim() }else {'*'}) ValidateSet = $(if ($In.ValidateSet ) {$In.ValidateSet.Trim() }) ValidatePattern = $(if ($In.ValidatePattern) {$In.ValidatePattern.Trim()}) ParameterType = $(if ($In.ParameterType ) {$In.ParameterType.Trim() }) Mandatory = $In.Mandatory } } } End { } } function Get-JeaProxy { param( [Parameter(Mandatory=$true,Position=0)] [ValidateNotNull()] $Name, [Parameter(Mandatory=$true,Position=1)] [ValidateNotNull()] $Parameter ) #Names may specify specific commands or have wildcards to specify sets of commands if (!$CommandsToGenerate.$Name -and $Parameter) { $CommandsToGenerate.$Name = New-Object Jea.Proxy } return $CommandsToGenerate.$Name } function Add-ParametersToProxy { param( [Parameter(Mandatory=$true,Position=0)] [ValidateNotNull()] $Proxy, [Parameter(Mandatory=$true,Position=1)] [ValidateNotNull()] $CSpec, [Parameter(Mandatory=$true,Position=2)] [ValidateNotNull()] $CmdInfo ) if ($CSpec.Parameter -eq '*') { foreach ($ParameterName in $CmdInfo.Parameters.Keys) { $p = $proxy.parameter.($ParameterName) if (!$p) { $p = new-object Jea.Parameter $proxy.parameter.Add($ParameterName, $p) } } } else { $p = $proxy.parameter.$($CSpec.Parameter) if (!$p) { $p = new-object Jea.Parameter $proxy.parameter.Add($CSpec.Parameter.ToLower(), $p) } if ($CSpec.ValidateSet) { $p.ValidateSet =$CSpec.ValidateSet.Tolower() } if ($CSpec.ValidatePattern) { $p.ValidatePattern = $CSpec.ValidatePattern } if ($CSpec.ParameterType) { $p.ParameterType = $CSpec.ParameterType } if ($CSpec.Mandatory) { $p.Mandatory = $CSpec.Mandatory } } } function ConvertTo-CommandsToGenerate { param( [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)] [ValidateNotNull()] $CSpec ) Begin { $CommandsToGenerate = @{} } Process { $module = $CSpec.Module if ($module -match "\.dll$") { $module = ((split-path $module -Leaf) -split '.dll')[0] } foreach ($CmdInfo in Get-Command -Module $Module -Name $CSpec.Name -CommandType Function,Cmdlet) { $proxy = Get-JeaProxy -Name $CmdInfo.Name -Parameter $CSpec.Parameter Add-ParametersToProxy -Proxy $proxy -CSpec $CSpec -CmdInfo $cmdInfo } } End { return $CommandsToGenerate } } function New-ToolKitPremable { param ( [Parameter(Mandatory)] [String]$Name, [String] $CommandSpecs, [System.String[]] $Applications ) # Now we generate the File @" <# This is a auto-generated module containing proxy cmdlets. Generated At: $(Get-date) Generated On: $(hostname) Generated By: $($env:UserDomain + '\' + $env:UserName) #region OrginalCSVFile ******************** START Original Source file *********************** $CommandSpecs ******************** END Original Source file *********************** #endRegion #> $( $list = @() foreach ($a in $Applications) { $list += """$a""" } if ($list.count) { '$ExportedApplications = ' + ($list -join ',') } ) "@ } # Some of these are dangerous and other get in the way of the runspace working $forbiddenProxy = @( 'Exit-Pssession','Format-Table','Format-List','Format-Custom','Format-Wide', 'Get-Command','Get-Help','Get-Formatdata','Get-Member','Group-Object', 'Import-Module','Measure-Object','New-Object','Out-Default','Select-Object', 'TabExpansion2','Where-Object','Write-Debug','Write-Error','Write-Host', 'Write-Output','Write-Verbose','Write-Warning' ) function ConvertTo-ProxyFunctions { [CmdletBinding()] Param ( # Param1 help description [Parameter(Mandatory=$true, ValueFromPipeline=$true, Position=0)] $CmdName ) Begin { # Proxy Modules are typically going to be used in constrained runspaces where best # practice will be to turn of ModuleAutoloading so the proxy needs to load whatever # modules it will proxy $modulesToImport = @{'Microsoft.PowerShell.Core'=1 } $exportCmdlet = @() } Process { $Cmd = Get-Command -Name $CmdName -CommandType Cmdlet,Function -ErrorAction Stop if (!$cmd) { Throw "No such Object [$CmdName :$CommandType]" } <# TODO: Need to do some flavor of analsys of MANDATORY PARAMETERS in SETs #> foreach ($c in $cmd |where {$_.Name -notIn $forbiddenProxy}) { if ($c.Module) {import-module -Name $c.module -ErrorAction Ignore -Verbose:0} if ($c.CommandType -eq 'function') { rename-item function:$($c.Name) $($c.Name + '-Original') $c = Get-command -name ($cmdName + '-Original') -CommandType Function -ErrorAction Stop } $Parameter = $CommandsToGenerate.$CmdName.Parameter.Keys $MetaData = New-Object System.Management.Automation.CommandMetaData $c $metaData.Name = $CmdName foreach ($p in @($MetaData.Parameters.Keys)) { $p = $p.Tolower() if ($p -notin $Parameter) { $null = $MetaData.Parameters.Remove($p) } else { $v = $CommandsToGenerate.$CmdName.Parameter.$p.ValidateSet if ($v) { $MetaData.Parameters.$p.attributes.Add( $(New-Object System.Management.Automation.ValidateSetAttribute $($v -split ';'))) } $v = $CommandsToGenerate.$CmdName.Parameter.$p.ValidatePattern if ($v) { $MetaData.Parameters.$p.attributes.Add( $(New-Object System.Management.Automation.ValidatePatternAttribute $v)) } $v = $CommandsToGenerate.$CmdName.Parameter.$p.ParameterType if ($v) { $type = [System.AppDomain]::CurrentDomain.GetAssemblies().GetTypes() | where {$_.fullname -match $ParameterType} if ($type) { $MetaData.Parameters.$p.ParameterType = $type[0].FullName } } $v = $CommandsToGenerate.$CmdName.Parameter.$p.Mandatory if ($v) { foreach($ps in $MetaData.Parameters.$p.Parametersets.Keys) { $MetaData.Parameters.$p.Parametersets.$PS.IsMandatory=$true } } }#end }#foreach if ($c.Module) { $RealModule = $c.module if (!$modulesToImport.$RealModule) { $modulesToImport.$RealModule = 'Already imported' @" Import-Module $($RealModule) -Scope Global "@ } } @" #region $cmdname $( if ($c.CommandType -eq 'function') { "rename-item function:$cmdName $($cmdName+ '-Original')" } ) function $cmdName { "@ [System.Management.Automation.ProxyCommand]::create($MetaData) @" } # $cmdName #endregion "@ $exportCmdlet += $CmdName } #foreach $cmd } End { @" Export-ModuleMember -Function $(($exportCmdlet | sort -Unique) -join ',') #EOF "@ } } function Test-Schema { param( [Parameter(Mandatory)] $CSVs ) $allowed = 'Module','Name','Parameter','ValidateSet','ValidatePattern','ParameterType','Mandatory' $mismatch = $CSVs |Get-Member -MemberType Properties | where Name -notIn $Allowed if ($mismatch) { $errorMsg = "Incorrect CommandSpec schema: $($Mismatch.Name -join ',')" Write-Verbose $errorMsg throw $errorMsg } } <# .Synopsis Use a CSV-formated string to drive creation of a JeaProxy module .DESCRIPTION JeaProxy modules provide fine grain control over what a user can invoke. It accomplishes this by manipulating the command parsing information and generating a proxy function. This process is driven off a CommandSpecs which is a CSV formated string using the schema: Module,Name,Parameter,ValidateSet,ValidatePattern,ParameterType If only a name is specified, the cmdlet is surfaced in whole If a Name and a parameter are specified, then only those parameters will be surfaced for that cmdlet. Since it is a CSV format, only one parameter can be specified on a line so we need to process all the lines and consolidate the information before we create the proxies. If a Name, a parameter and a Validate is specified, we add a VALIDATESET attribute with the values of the Validate field. The values need to be seperated with a ';'. Applications can also be specified. Applications are non-PowerShell native executables (e.g. Ping.exe or IPconfig.exe) .EXAMPLE Export-JeaProxy -Name GeneralAdmin -Applications "ping.exe","ipconfig.exe" -CommandSpecs @` Module,Name,Parameter,ValidateSet,ValidatePattern,ParameterType ,Get-Process ,Stop-Process,Name,calc;notepad ,get-service ,Stop-Service,Name,,^SQL `@ .OUTPUTS Two files are created in the ($env:ProgramFiles)\Jea\Toolkit directory 1) $Name-Toolkit.psm1 # The proxy module 2) $Name-CommandSpecs.csv # For diagnostics .NOTES General notes #> function Export-JeaProxy { param ( [Parameter(Mandatory)] [String]$Name, [String] $CommandSpecs, [System.String[]] $Applications ) $CommandSpecs > (Join-Path (Get-JeaToolKitDir) "$($Name)-CommandSpecs.csv") Write-Verbose "New [JeaDirectory.CSV]$($Name)-CommandSpecs.csv" $CSVs = $CommandsToGenerate = $CommandSpecs.ToLower() | ConvertFrom-Csv Test-Schema $CSVs $CommandsToGenerate = $CSVs | ConvertTo-CSPec | ConvertTo-CommandsToGenerate $toolkit = (Join-Path (Get-JeaToolKitDir) "$($Name)-ToolKit.psm1") New-ToolKitPremable @PSBoundParameters > $toolkit $CommandsToGenerate.Keys |Sort {($_ -split '-')[1]},{($_ -split '-')[0]} | ConvertTo-ProxyFunctions >> $toolkit Write-Verbose "New [JeaDirectory.Module]$toolkit" } #Export-JeaProxy |