Repository navigation
Add Extensibility to Plaster's Constrained Runspace #333
Description
Plaster is a good platform to scaffold content, and I have used it for building module templates. But recently I have been using it to scaffold application configuration data.
One shortcoming I have found is that Plaster uses constrained runspaces to perform the scaffolding. Correct me if I'm wrong, but this was to provide safety around what is essentially Invoke-Expression. The difficulty comes from trying to write clean code and turn comma separated strings into properly formatted output.
Example template files:
Test.json.txt
(This is the simplest way I could think to join comma separated strings into a JSON array.)
{
"Key": "Value",
"Array": [
""
<%
if ($PLASTER_PARAM_Param1) {
$PLASTER_PARAM_Param1.split(",").trim().foreach({",`"$_`""})
}
%>
]
}The resulting file is not well formatted, and has a blank element in the first position of the array:
Test.json
{
"Key": "Value",
"Array": [
""
,"Test"
,"Testing"
]
}Another option is to use .Replace, and replace commas with newlines(`n) and double quotes ("). But newlines are not interpolated in the constrained runspace. So this doesn't work:
"`"$($PLASTER_PARAM_Param1.replace(",", "`"`n,`""))`""Possible Solutions
One solution that I would like to see, is a configuration file that can be placed in a user directory (like $PROFILE), or in the module folder itself. This file contains a list of allowable cmdlets in the scaffolding process, and Invoke-Plaster adds these cmdlets to the runspace. This would provide the extensibility, without having to rebuild the module with a custom list of cmdlets. It also puts the security onus on the user that adds the commands. Either the installing user can add a config file, or only the current user's config file is used during scaffolding.
Another solution, is adding a parameter to the Invoke-Plaster function, like -AllowedCmdlets to add a list of cmdlets to be used during scaffolding. This makes it a little easier for the user to scaffold with the cmdlets they need, but on the flip-side it makes it easier to pass cmdlets required for command injection. So this provides a little less control when scaffolding content.
Either of these solutions would allow the code to be cleaner, like so:
{
"Key": "Value",
"Array": [
<%
if ($PLASTER_PARAM_Param1) {
$output = $PLASTER_PARAM_Param1 -split ",") -join "`",`n`""
"`"$output`""
} else {
"`"`""
}
%>
]
}
Activity
rkeithhill commentedon Jun 12, 2018
It's a bit more fundamental than that. The idea is that folks can scaffold with any template and not have to worry about templates doing anything other than creating/editing files under the specified destination path. It's a trade-off between trust in Plaster/templates and flexibility. We've err'd on the side of safety/trust.
One option we could consider is a special parameter to Invoke-Plaster along the lines of
-AllowArbitraryScriptExecution, in which case we'd create the runspace as "unconstrained" or just run in the current runspace - whichever is easier to implement alongside the existing constrained runspace.Or we could provide an
-AllowedCmdletsalthough there are likely functions and possibly aliases template authors want to use. Or we could allow template authors to declare additional commands in the template and then during execution, we show these to the user and get approval. The only problem with that is that it puts a heck of a burden on the end user to keep themselves safe. IIRC I once tried to addget-commandto the constrained runspace only to discover that I could then execute any command.Anyway, any changes to "open up" Plaster would likely require the review of the PowerShell Committee.
bwright86 commentedon Jun 12, 2018
I think it would be worth it to add the flexibility into Plaster, so that the user can choose. For first use, Plaster could default to using a safety zone for scaffolding, but also have the ability to extend Plaster to do more useful things.
Maybe some tooling could be done to utilize PSScriptAnalyzer for scanning Plaster Templates to see if potentially hazardous things are being done, or create some Pester tests that can provide a level of safety (i.e. warnings, etc...) to downloaded templates. Even using the zone identifiers on downloaded files could let Plaster know the files came from the "Internet Zone".
I think a lot of people are currently building their own templates (Probably mostly to learn.), so there is less worry about what they put into their own scripts. But if a central repository of templates comes to fruition, it will be good to already have a safety mechanism in place.
I have written a very rough module to extract input parameters and TemplateFile variables for Plaster Templates (It still needs a way to extract script blocks from a TemplateFile.) This was more to validate that the correct input is being collected in a manifest for the matrices of TemplateFiles being utilized. If there is interest to extend this into passing the output into PSScriptAnalyzer or Pester Tests, I could build it towards that.
Link: https://github.com/bwright86/plasterhelper
Thank you for the response.
JustinGrote commentedon Jul 3, 2018
What I've been doing is basically queueing up anything that doesn't operate inside the runspace and writing it out to a separate script file, then printing a message when the template is done that says "For additional functionality, please run {. $pathtofile} after you have reviewed the contents of the script and determined them safe". Things like git setup, account setup tasks/authorization, etc.
For my powershell module constructor, I have a step in the invoke-build that I write called "Setup" that also includes these. So after deploying the plaster you can run invoke-build setup for additional items, but like @rkeithhill says the user can trust that Plaster itself won't trash their system.
I like the idea of keeping Plaster "safe" at the expense of ease of use, because it's pretty easy to just add a second step for the extra stuff, and you can write config items decided during plaster questions to a configuration file that will automate the second-step process.