PowerShellConfigurationSetProcessor.cs (14438B)
1 // ----------------------------------------------------------------------------- 2 // <copyright file="PowerShellConfigurationSetProcessor.cs" company="Microsoft Corporation"> 3 // Copyright (c) Microsoft Corporation. Licensed under the MIT License. 4 // </copyright> 5 // ----------------------------------------------------------------------------- 6 7 namespace Microsoft.Management.Configuration.Processor.PowerShell.Set 8 { 9 using System; 10 using System.Collections.Generic; 11 using System.IO; 12 using System.Management.Automation; 13 using Microsoft.Management.Configuration.Processor.Exceptions; 14 using Microsoft.Management.Configuration.Processor.PowerShell.DscResourcesInfo; 15 using Microsoft.Management.Configuration.Processor.PowerShell.Helpers; 16 using Microsoft.Management.Configuration.Processor.PowerShell.ProcessorEnvironments; 17 using Microsoft.Management.Configuration.Processor.PowerShell.Unit; 18 using Microsoft.Management.Configuration.Processor.Set; 19 using Microsoft.Management.Configuration.Processor.Unit; 20 using Windows.Security.Cryptography.Certificates; 21 22 /// <summary> 23 /// IConfigurationSetProcessor implementation using PowerShell DSC v2. 24 /// </summary> 25 internal sealed partial class PowerShellConfigurationSetProcessor : ConfigurationSetProcessorBase, IConfigurationSetProcessor 26 { 27 /// <summary> 28 /// Initializes a new instance of the <see cref="PowerShellConfigurationSetProcessor"/> class. 29 /// </summary> 30 /// <param name="processorEnvironment">The processor environment.</param> 31 /// <param name="configurationSet">Configuration set.</param> 32 /// <param name="isLimitMode">Whether the set processor should work in limitation mode.</param> 33 public PowerShellConfigurationSetProcessor(IProcessorEnvironment processorEnvironment, ConfigurationSet? configurationSet, bool isLimitMode = false) 34 : base(configurationSet, isLimitMode) 35 { 36 this.ProcessorEnvironment = processorEnvironment; 37 } 38 39 /// <summary> 40 /// Gets the processor environment. 41 /// </summary> 42 internal IProcessorEnvironment ProcessorEnvironment { get; } 43 44 /// <inheritdoc /> 45 protected override IConfigurationUnitProcessor CreateUnitProcessorInternal(ConfigurationUnit unit) 46 { 47 var configurationUnitInternal = new ConfigurationUnitAndModule(unit, this.ConfigurationSet?.Path) { UnitTypeIsResourceName = IsUnitTypeResourceName(this.ConfigurationSet?.SchemaVersion) }; 48 this.OnDiagnostics(DiagnosticLevel.Verbose, $"Creating unit processor for: {configurationUnitInternal.QualifiedName}..."); 49 50 var dscResourceInfo = this.PrepareUnitForProcessing(configurationUnitInternal); 51 52 this.OnDiagnostics(DiagnosticLevel.Verbose, $"Using unit from location: {dscResourceInfo.Path}"); 53 return new PowerShellConfigurationUnitProcessor( 54 this.ProcessorEnvironment, 55 new ConfigurationUnitAndResource(configurationUnitInternal, dscResourceInfo), 56 this.IsLimitMode) 57 { SetProcessorFactory = this.SetProcessorFactory }; 58 } 59 60 /// <inheritdoc /> 61 protected override IConfigurationUnitProcessorDetails? GetUnitProcessorDetailsInternal(ConfigurationUnit unit, ConfigurationUnitDetailFlags detailFlags) 62 { 63 var unitInternal = new ConfigurationUnitAndModule(unit, this.ConfigurationSet?.Path); 64 this.OnDiagnostics(DiagnosticLevel.Verbose, $"Getting unit details [{detailFlags}] for: {unitInternal.QualifiedName}"); 65 66 // (Local | Download | Load) will all work off of local files, so if any one is an option just use the local module info if found. 67 DscResourceInfoInternal? dscResourceInfo = null; 68 if (detailFlags.HasFlag(ConfigurationUnitDetailFlags.Local) || detailFlags.HasFlag(ConfigurationUnitDetailFlags.Download) || detailFlags.HasFlag(ConfigurationUnitDetailFlags.Load)) 69 { 70 dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); 71 } 72 73 if (dscResourceInfo is not null) 74 { 75 return this.GetUnitProcessorDetailsLocal( 76 dscResourceInfo.Name, 77 dscResourceInfo, 78 detailFlags.HasFlag(ConfigurationUnitDetailFlags.Load)); 79 } 80 81 if (!(detailFlags.HasFlag(ConfigurationUnitDetailFlags.Catalog) || detailFlags.HasFlag(ConfigurationUnitDetailFlags.Download) || detailFlags.HasFlag(ConfigurationUnitDetailFlags.Load))) 82 { 83 // Not found locally. 84 return null; 85 } 86 87 var unitModuleInfo = this.FindUnitModule(unitInternal); 88 if (unitModuleInfo is null) 89 { 90 // Not found in catalog. 91 return null; 92 } 93 94 PSObject foundModule = unitModuleInfo.Value.Module; 95 string resourceName = unitModuleInfo.Value.ResourceName; 96 97 dynamic foundModuleInfo = foundModule; 98 99 if (detailFlags.HasFlag(ConfigurationUnitDetailFlags.Catalog)) 100 { 101 return Factory.CreateUnitProcessorDetails( 102 resourceName, 103 null, 104 null, 105 foundModule, 106 null); 107 } 108 109 if (detailFlags.HasFlag(ConfigurationUnitDetailFlags.Download)) 110 { 111 var tempSavePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); 112 Directory.CreateDirectory(tempSavePath); 113 this.ProcessorEnvironment.SaveModule(foundModule, tempSavePath); 114 115 var moduleInfo = this.ProcessorEnvironment.GetAvailableModule( 116 Path.Combine(tempSavePath, foundModuleInfo.Name)); 117 118 return Factory.CreateUnitProcessorDetails( 119 resourceName, 120 null, 121 moduleInfo, 122 foundModule, 123 this.GetCertificates(moduleInfo)); 124 } 125 126 if (detailFlags.HasFlag(ConfigurationUnitDetailFlags.Load)) 127 { 128 this.ProcessorEnvironment.InstallModule(foundModule); 129 130 dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); 131 132 if (dscResourceInfo is null) 133 { 134 // Well, this is awkward. 135 throw new InstallDscResourceException( 136 unitInternal.ResourceName, 137 PowerShellHelpers.CreateModuleSpecification(foundModuleInfo.Name, foundModuleInfo.Version)); 138 } 139 140 return this.GetUnitProcessorDetailsLocal(dscResourceInfo.Name, dscResourceInfo, true); 141 } 142 143 return null; 144 } 145 146 private static bool IsUnitTypeResourceName(string? schemaVersion) 147 { 148 return schemaVersion != null && schemaVersion == "0.1"; 149 } 150 151 /// <summary> 152 /// Finds the module and preferred resource name for processing the configuration unit. 153 /// </summary> 154 /// <param name="unitInternal">The internal configuration unit.</param> 155 /// <returns>A tuple containing the module info and preferred resource name, or null if not found.</returns> 156 private (PSObject Module, string ResourceName)? FindUnitModule(ConfigurationUnitAndModule unitInternal) 157 { 158 PSObject? foundModule = null; 159 string resourceName = string.Empty; 160 161 // If module has been specified, find it and assume that the resource will be within it. 162 // Do this first as we do not currently gain much from FindDscResource; if that changes then it can be the primary. 163 if (unitInternal.Module != null) 164 { 165 foundModule = this.ProcessorEnvironment.FindModule(unitInternal); 166 if (foundModule != null) 167 { 168 resourceName = unitInternal.ResourceName; 169 } 170 } 171 else 172 { 173 dynamic? foundResource = this.ProcessorEnvironment.FindDscResource(unitInternal); 174 if (foundResource != null) 175 { 176 foundModule = foundResource.PSGetModuleInfo; 177 178 // Hopefully they will never change the properties name. If someone can explain to me 179 // why assign it Name to $_ in Find-DscResource turns into a string in PowerShell but 180 // into a PSObject here that would be nice... 181 resourceName = foundResource.Name.ToString(); 182 } 183 } 184 185 if (foundModule != null) 186 { 187 return (foundModule, resourceName); 188 } 189 190 return null; 191 } 192 193 private DscResourceInfoInternal PrepareUnitForProcessing(ConfigurationUnitAndModule unitInternal) 194 { 195 // Invoke-DscResource makes a call to Get-DscResource which looks at the entire PSModulePath 196 // to see if a resource exists. DscResourcesMap is an attempt to try to optimize Get-DscResource 197 // by making just one call and get all of them, but it doesn't support minVersion and maxVersion. 198 // For now, lets make PowerShell fully figure out which module to use and try to optimize it later. 199 // This class will have a private member Lazy<DscResourcesMap> which will be initialized by calling 200 // this.ProcessorEnvironment.GetAllDscResources() 201 // To improve the performance even more, we will still need Invoke-DscResource to be update to 202 // get a DSC resource info object instead of calling Get-DscResource every time. 203 var dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); 204 205 if (dscResourceInfo is null) 206 { 207 var findUnitModuleResult = this.FindUnitModule(unitInternal); 208 209 if (findUnitModuleResult is null) 210 { 211 throw new FindDscResourceNotFoundException(unitInternal.ResourceName, unitInternal.Module); 212 } 213 214 this.ProcessorEnvironment.InstallModule(findUnitModuleResult.Value.Module); 215 216 // Now we should find it. 217 dscResourceInfo = this.ProcessorEnvironment.GetDscResource(unitInternal); 218 if (dscResourceInfo is null) 219 { 220 throw new InstallDscResourceException(unitInternal.ResourceName, unitInternal.Module); 221 } 222 } 223 224 // PowerShell will prompt the user when a module that is downloaded from the internet is imported. 225 // For a hosted environment, this will throw an exception because it doesn't support user interaction. 226 // In the case we don't import the module here, eventually Invoke-DscResource will fail for class 227 // resources because they will call a method on a null obj. It is easier to just fail here. 228 // The exception being thrown will have the correct details (user needs to call Unblock-File) 229 // instead of the cryptic Invoke with 0 arguments. 230 if (!string.IsNullOrEmpty(dscResourceInfo.Path)) 231 { 232 try 233 { 234 this.ProcessorEnvironment.ImportModule(dscResourceInfo.Path); 235 } 236 catch (Exception e) 237 { 238 throw new ImportModuleException(dscResourceInfo.ModuleName, e); 239 } 240 } 241 242 return dscResourceInfo; 243 } 244 245 private ConfigurationUnitProcessorDetails GetUnitProcessorDetailsLocal( 246 string unitName, 247 DscResourceInfoInternal dscResourceInfo, 248 bool importModule) 249 { 250 // I'm looking at you resources under C:\WINDOWS\system32\WindowsPowershell 251 if (dscResourceInfo.ModuleName is null || 252 dscResourceInfo.Version is null) 253 { 254 return Factory.CreateUnitProcessorDetails( 255 dscResourceInfo.Name, 256 dscResourceInfo, 257 null, 258 null, 259 null); 260 } 261 262 var module = PowerShellHelpers.CreateModuleSpecification( 263 dscResourceInfo.ModuleName, 264 dscResourceInfo.Version.ToString()); 265 266 // Get-InstalledModule only works for modules installed via PowerShell-Get. 267 // There are some properties that can only be obtain by that it so is better to take both. 268 var moduleInfo = this.ProcessorEnvironment.GetAvailableModule(module); 269 var installedModule = this.ProcessorEnvironment.GetInstalledModule(module); 270 271 if (importModule) 272 { 273 this.ProcessorEnvironment.ImportModule(module); 274 } 275 276 return Factory.CreateUnitProcessorDetails( 277 dscResourceInfo.Name, 278 dscResourceInfo, 279 moduleInfo, 280 installedModule, 281 this.GetCertificates(moduleInfo)); 282 } 283 284 private List<Certificate>? GetCertificates(PSModuleInfo? moduleInfo) 285 { 286 if (moduleInfo is null) 287 { 288 return null; 289 } 290 291 // TODO: we still need to investigate more here, but lets start with something. 292 var paths = new List<string>(); 293 294 var psdPath = Path.Combine(moduleInfo.ModuleBase, $"{moduleInfo.Name}.psd1"); 295 if (File.Exists(psdPath)) 296 { 297 paths.Add(psdPath); 298 } 299 300 var psmPath = Path.Combine(moduleInfo.ModuleBase, $"{moduleInfo.Name}.psm1"); 301 if (File.Exists(psmPath)) 302 { 303 paths.Add(psmPath); 304 } 305 306 return this.ProcessorEnvironment.GetCertsOfValidSignedFiles(paths.ToArray()); 307 } 308 } 309 }