diff --git a/.gitignore b/.gitignore index 1fc6e90..ae79d00 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ cache/* .vscode/ .vs/ *.log +*.pfx # JEA RoleCapabilities/IcingaForWindows.psrc diff --git a/doc/100-General/10-Changelog.md b/doc/100-General/10-Changelog.md index 7fff1b1..5c195a2 100644 --- a/doc/100-General/10-Changelog.md +++ b/doc/100-General/10-Changelog.md @@ -35,6 +35,7 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic * [#449](https://github.com/Icinga/icinga-powershell-framework/pull/449) Fixes unhandled exception while importing modules during `Install-IcingaComponent` process, because of possible missing dependencies * [#451](https://github.com/Icinga/icinga-powershell-framework/pull/451) Fixes PowerShell being unable to enter JEA context if only the Framework is installed and removes the `|` from plugin output, in case a JEA error is thrown that check commands are not present * [#452](https://github.com/Icinga/icinga-powershell-framework/pull/452) Fixes unhandled `true` output on the console while running the installer +* [#454](https://github.com/Icinga/icinga-powershell-framework/pull/454) Fixes JEA catalog compiler and background daemon execution in JEA context ### Enhancements diff --git a/lib/core/dev/New-IcingaForWindowsComponent.psm1 b/lib/core/dev/New-IcingaForWindowsComponent.psm1 index 6d58e5d..f70f6db 100644 --- a/lib/core/dev/New-IcingaForWindowsComponent.psm1 +++ b/lib/core/dev/New-IcingaForWindowsComponent.psm1 @@ -172,8 +172,6 @@ function New-IcingaForWindowsComponent() Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' # This is your main daemon function. Add your code inside the WHILE() loop which is executed once the daemon is loaded.'; Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' # Also check the developer guide for further details: https://icinga.com/docs/icinga-for-windows/latest/doc/900-Developer-Guide/10-Custom-Daemons/'; Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ''; - Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' Use-Icinga -LibOnly -Daemon;'; - Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ''; Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' while ($TRUE) {'; Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' # Add your daemon code within this loop'; Add-Content -Path (Join-Path -Path $ModuleDir -ChildPath ([string]::Format('daemon\{0}.psm1', $DaemonEntry))) -Value ' Start-Sleep -Seconds 1;'; diff --git a/lib/core/jea/Get-IcingaCommandDependency.psm1 b/lib/core/jea/Get-IcingaCommandDependency.psm1 index 3883505..fdcd6da 100644 --- a/lib/core/jea/Get-IcingaCommandDependency.psm1 +++ b/lib/core/jea/Get-IcingaCommandDependency.psm1 @@ -7,43 +7,57 @@ function Get-IcingaCommandDependency() [string]$CmdType = '' ); + # Function, Cmdlet, Alias, Modules, Application if ([string]::IsNullOrEmpty($CmdType)) { return $CompiledList; } + # Create the list container for our object type if not existing + # => Function, Cmdlet, Alias, Modules, Application if ($CompiledList.ContainsKey($CmdType) -eq $FALSE) { $CompiledList.Add($CmdType, @{ }); } + # e.g. Invoke-IcingaCheckCPU if ($CompiledList[$CmdType].ContainsKey($CmdName)) { $CompiledList[$CmdType][$CmdName] += 1; + return $CompiledList; } + # Add the command this function is called with $CompiledList[$CmdType].Add($CmdName, 0); + # The command is not known in our Framework dependency list -> could be a native Windows command if ((Test-PSCustomObjectMember -PSObject $DependencyList -Name $CmdName) -eq $FALSE) { return $CompiledList; } + # Loop our entire dependency list for every single command foreach ($CmdList in $DependencyList.$CmdName.PSObject.Properties.Name) { + # $Cmd => The list of child commands + # $CmdList => Function, Cmdlet, Alias, Modules, Application $Cmd = $DependencyList.$CmdName.$CmdList; + # Create the list container for our object type if not existing + # => Function, Cmdlet, Alias, Modules, Application if ($CompiledList.ContainsKey($CmdList) -eq $FALSE) { $CompiledList.Add($CmdList, @{ }); } + # Loop all commands within our child list for this command foreach ($entry in $Cmd.PSObject.Properties.Name) { - if ($CompiledList[$CmdList].ContainsKey($entry) -eq $FALSE) { - $CompiledList[$CmdList].Add($entry, 0); + # $entry => The command name e.g. Write-IcingaConsolePlain + if ($CompiledList[$CmdList].ContainsKey($entry) -eq $FALSE) { $CompiledList = Get-IcingaCommandDependency ` -DependencyList $DependencyList ` -CompiledList $CompiledList ` - -CmdName $entry; - } else { - $CompiledList[$CmdList][$entry] += 1; - } + -CmdName $entry ` + -CmdType $CmdList; + } else { + $CompiledList[$CmdList][$entry] += 1; + } } } diff --git a/lib/core/jea/Get-IcingaJEAConfiguration.psm1 b/lib/core/jea/Get-IcingaJEAConfiguration.psm1 index d4fae8f..f5759b8 100644 --- a/lib/core/jea/Get-IcingaJEAConfiguration.psm1 +++ b/lib/core/jea/Get-IcingaJEAConfiguration.psm1 @@ -147,11 +147,17 @@ function Get-IcingaJEAConfiguration() -CmdType 'Function'; # We need to add this function for our background daemon we start with 'Start-IcingaForWindowsDaemon', - # as this function is called outside the JEA context + # as these functions are called outside the JEA context $UsedCmdlets = Get-IcingaCommandDependency ` -DependencyList $DependencyList ` -CompiledList $UsedCmdlets ` - -CmdName 'Add-IcingaForWindowsDaemon' ` + -CmdName 'Start-IcingaPowerShellDaemon' ` + -CmdType 'Function'; + + $UsedCmdlets = Get-IcingaCommandDependency ` + -DependencyList $DependencyList ` + -CompiledList $UsedCmdlets ` + -CmdName 'Start-IcingaForWindowsDaemon' ` -CmdType 'Function'; # Fixes error if only the Icinga PowerShell Framework is installed, which then causes JEA to fail entirely because of this missing Cmdlet diff --git a/lib/core/jea/Read-IcingaPowerShellModuleFile.psm1 b/lib/core/jea/Read-IcingaPowerShellModuleFile.psm1 index a33bed8..cd65ee3 100644 --- a/lib/core/jea/Read-IcingaPowerShellModuleFile.psm1 +++ b/lib/core/jea/Read-IcingaPowerShellModuleFile.psm1 @@ -13,15 +13,19 @@ function Read-IcingaPowerShellModuleFile() $FileContent = Read-IcingaFileSecure -File $File; } - $PSParser = [System.Management.Automation.PSParser]::Tokenize($FileContent, [ref]$null); - [array]$Comments = @(); - [array]$RegexFilter = @(); - [string]$RegexPattern = ''; - [array]$CommandList = @(); - [array]$FunctionList = @(); - [hashtable]$CmdCache = @{ }; - [hashtable]$FncCache = @{ }; - [int]$Index = 0; + $PSParser = [System.Management.Automation.PSParser]::Tokenize($FileContent, [ref]$null); + [array]$Comments = @(); + [array]$RegexFilter = @(); + [string]$RegexPattern = ''; + [array]$CommandList = @(); + [array]$FunctionList = @(); + [hashtable]$CmdCache = @{ }; + [hashtable]$FncCache = @{ }; + [int]$Index = 0; + [bool]$ThreadCommand = $FALSE; + [bool]$ThreadFetchNext = $FALSE; + [bool]$ShellCommand = $FALSE; + [bool]$ShellGroupStart = $FALSE; foreach ($entry in $PSParser) { if ($entry.Type -eq 'Comment') { @@ -31,6 +35,12 @@ function Read-IcingaPowerShellModuleFile() $CommandList += [string]$entry.Content; $CmdCache.Add($entry.Content, 0); } + + # We need to include commands we call with New-IcingaThreadInstance e.g. + # => New-IcingaThreadInstance -Name "Main" -ThreadPool (Get-IcingaThreadPool -Name 'MainPool') -Command 'Add-IcingaForWindowsDaemon' -Start; + if ($entry.Content.ToLower() -eq 'new-icingathreadinstance') { + $ThreadCommand = $TRUE; + } } elseif ($entry.Type -eq 'CommandArgument') { if ($PSParser[$index - 1].Type -eq 'Keyword' -And $PSParser[$index - 1].Content.ToLower() -eq 'function') { if ($FncCache.ContainsKey($entry.Content) -eq $FALSE) { @@ -38,6 +48,55 @@ function Read-IcingaPowerShellModuleFile() $FncCache.Add($entry.Content, 0); } } + } elseif ($entry.Type -eq 'Member' -And $entry.Content.ToLower() -eq 'addcommand') { + # In case we have objects that use .AddCommand() we should add these to our function list e.g. + # => [void]$Shell.AddCommand('Set-IcingaEnvironmentGlobal'); + $ShellCommand = $TRUE; + } + + # If we reached -Command for New-IcingaThreadInstance, check for the String element and add its value to our function list e.g. + # => Add-IcingaForWindowsDaemon + if ($ThreadFetchNext) { + if ($entry.Type -eq 'String') { + if (Test-IcingaFunction $entry.Content) { + if ($FncCache.ContainsKey($entry.Content) -eq $FALSE) { + $FunctionList += [string]$entry.Content; + $FncCache.Add($entry.Content, 0); + } + } + } + $ThreadFetchNext = $FALSE; + } + + # If we found the command New-IcingaThreadInstance inside ths script, loop until we reach -Command + if ($ThreadCommand) { + if ($entry.Type -eq 'CommandParameter' -And $entry.Content.ToLower() -eq '-command') { + $ThreadFetchNext = $TRUE; + $ThreadCommand = $FALSE; + } + } + + # If we reached the string content of our .AddCommand() object. add its value to our function list e.g. + # => Set-IcingaEnvironmentGlobal + if ($ShellGroupStart) { + if ($entry.Type -eq 'String') { + if (Test-IcingaFunction $entry.Content) { + if ($FncCache.ContainsKey($entry.Content) -eq $FALSE) { + $FunctionList += [string]$entry.Content; + $FncCache.Add($entry.Content, 0); + } + } + + $ShellGroupStart = $FALSE; + } + } + + # If we found an .AddArgument() member, continue until our group starts with ( + if ($ShellCommand) { + if ($entry.Type -eq 'GroupStart' -And $entry.Content.ToLower() -eq '(') { + $ShellCommand = $FALSE; + $ShellGroupStart = $TRUE; + } } $Index += 1; diff --git a/lib/core/jea/Set-IcingaForWindowsServiceJEAProfile.psm1 b/lib/core/jea/Set-IcingaForWindowsServiceJEAProfile.psm1 new file mode 100644 index 0000000..28387a8 --- /dev/null +++ b/lib/core/jea/Set-IcingaForWindowsServiceJEAProfile.psm1 @@ -0,0 +1,24 @@ +function Set-IcingaForWindowsServiceJEAProfile() +{ + [string]$JeaProfile = Get-IcingaJEAContext; + $IcingaForWindowsService = Get-IcingaForWindowsServiceData; + + if ([string]::IsNullOrEmpty($IcingaForWindowsService.FullPath) -Or (Test-Path $IcingaForWindowsService.FullPath) -eq $FALSE) { + return; + } + + [string]$PreparedServicePath = [string]::Format( + '\"{0}\" \"{1}\" \"{2}\"', + $IcingaForWindowsService.FullPath, + (Get-IcingaPowerShellModuleFile), + $JeaProfile + ); + + $Result = Start-IcingaProcess -Executable 'sc.exe' -Arguments ([string]::Format('config icingapowershell binPath= "{0}"', $PreparedServicePath)); + + if ($Result.ExitCode -ne 0) { + Write-IcingaConsoleError 'Failed to update Icinga for Windows service for JEA profile "{0}": {1}{2}' -Objects $JeaProfile, $ResolveStatus.Message, $ResolveStatus.Error; + } else { + Write-IcingaConsoleNotice 'Icinga for Windows service JEA handling has been configured successfully to profile "{0}"' -Objects $JeaProfile; + } +} diff --git a/lib/core/thread/New-IcingaThreadInstance.psm1 b/lib/core/thread/New-IcingaThreadInstance.psm1 index e9ee6db..13c3c3f 100644 --- a/lib/core/thread/New-IcingaThreadInstance.psm1 +++ b/lib/core/thread/New-IcingaThreadInstance.psm1 @@ -45,6 +45,12 @@ function New-IcingaThreadInstance() [void]$Shell.AddParameter('GlobalEnvironment', $Global:Icinga.Public); } + # Set the JEA context for all threads + if ($null -ne $Global:Icinga -And $Global:Icinga.ContainsKey('Protected') -And $Global:Icinga.Protected.ContainsKey('JEAContext')) { + [void]$Shell.AddCommand('Set-IcingaEnvironmentJEA'); + [void]$Shell.AddParameter('JeaEnabled', $Global:Icinga.Protected.JEAContext); + } + [void]$Shell.AddCommand($Command); $CodeHash = $Command; diff --git a/lib/core/thread/New-IcingaThreadPool.psm1 b/lib/core/thread/New-IcingaThreadPool.psm1 index b9f8703..4663207 100644 --- a/lib/core/thread/New-IcingaThreadPool.psm1 +++ b/lib/core/thread/New-IcingaThreadPool.psm1 @@ -9,7 +9,7 @@ function New-IcingaThreadPool() $SessionFile = Get-IcingaJEASessionFile; if ([string]::IsNullOrEmpty((Get-IcingaJEAContext))) { - $SessionConfiguration = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault(); + $SessionConfiguration = [System.Management.Automation.RunSpaces.InitialSessionState]::CreateDefault(); } else { if ([string]::IsNullOrEmpty($SessionFile)) { Write-IcingaEventMessage -EventId 1502 -Namespace 'Framework'; @@ -18,14 +18,14 @@ function New-IcingaThreadPool() $SessionConfiguration = [System.Management.Automation.Runspaces.InitialSessionState]::CreateFromSessionConfigurationFile($SessionFile); } - $Runspaces = [RunspaceFactory]::CreateRunspacePool( + $RunSpaces = [RunSpaceFactory]::CreateRunSpacePool( $MinInstances, $MaxInstances, $SessionConfiguration, $host ) - $Runspaces.Open(); + $RunSpaces.Open(); - return $Runspaces; + return $RunSpaces; } diff --git a/lib/core/thread/Set-IcingaEnvironmentJEA.psm1 b/lib/core/thread/Set-IcingaEnvironmentJEA.psm1 new file mode 100644 index 0000000..f218760 --- /dev/null +++ b/lib/core/thread/Set-IcingaEnvironmentJEA.psm1 @@ -0,0 +1,10 @@ +function Set-IcingaEnvironmentJEA() +{ + param ( + [bool]$JeaEnabled = $FALSE + ); + + if ($null -ne $Global:Icinga -And $Global:Icinga.ContainsKey('Protected') -And $Global:Icinga.Protected.ContainsKey('JEAContext')) { + $Global:Icinga.Protected.JEAContext = $JeaEnabled; + } +} diff --git a/lib/daemon/Start-IcingaPowerShellDaemon.psm1 b/lib/daemon/Start-IcingaPowerShellDaemon.psm1 index 565f65c..2e9b666 100644 --- a/lib/daemon/Start-IcingaPowerShellDaemon.psm1 +++ b/lib/daemon/Start-IcingaPowerShellDaemon.psm1 @@ -2,20 +2,23 @@ function Start-IcingaPowerShellDaemon() { param ( [switch]$RunAsService = $FALSE, + [switch]$JEAContext = $FALSE, [switch]$JEARestart = $FALSE ); - Start-IcingaForWindowsDaemon -RunAsService:$RunAsService -JEARestart:$JEARestart; + Start-IcingaForWindowsDaemon -RunAsService:$RunAsService -JEARestart:$JEARestart -JEAContext:$JEAContext; } function Start-IcingaForWindowsDaemon() { param ( [switch]$RunAsService = $FALSE, + [switch]$JEAContext = $FALSE, [switch]$JEARestart = $FALSE ); - $Global:Icinga.Protected.RunAsDaemon = $TRUE; + $Global:Icinga.Protected.RunAsDaemon = [bool]$RunAsService; + $Global:Icinga.Protected.JEAContext = [bool]$JEAContext; [string]$MainServicePidFile = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'service.pid'); [string]$JeaPidFile = (Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'jea.pid'); [string]$JeaProfile = Get-IcingaPowerShellConfig -Path 'Framework.JEAProfile'; @@ -32,7 +35,6 @@ function Start-IcingaForWindowsDaemon() if ([string]::IsNullOrEmpty($JeaProfile)) { Write-IcingaDebugMessage -Message 'Starting Icinga for Windows service without JEA context' -Objects $RunAsService, $JEARestart, $JeaProfile; - $Global:Icinga.Protected.RunAsDaemon = $TRUE; # Todo: Add config for active background tasks. Set it to 20 for the moment Add-IcingaThreadPool -Name 'MainPool' -MaxInstances 20; $Global:Icinga.Public.Add('SSLCertificate', $Certificate); @@ -41,6 +43,7 @@ function Start-IcingaForWindowsDaemon() New-IcingaThreadInstance -Name "Main" -ThreadPool (Get-IcingaThreadPool -Name 'MainPool') -Command 'Add-IcingaForWindowsDaemon' -Start; } else { Write-IcingaDebugMessage -Message 'Starting Icinga for Windows service inside JEA context' -Objects $RunAsService, $JEARestart, $JeaProfile; + & powershell.exe -NoProfile -NoLogo -ConfigurationName $JeaProfile -Command { try { Use-Icinga -Daemon; @@ -84,7 +87,7 @@ function Start-IcingaForWindowsDaemon() Write-IcingaFileSecure -File $JeaPidFile -Value ''; Write-IcingaEventMessage -EventId 1505 -Namespace Framework -Objects ([string]::Format('{0}/5', $JeaRestartCounter)); - Start-IcingaForWindowsDaemon -RunAsService:$RunAsService -JEARestart; + Start-IcingaForWindowsDaemon -RunAsService:$RunAsService -JEAContext:$JEAContext -JEARestart; $JeaRestartCounter += 1; $JeaPid = ''; diff --git a/lib/daemons/RestAPI/daemon/New-IcingaForWindowsRESTApi.psm1 b/lib/daemons/RestAPI/daemon/New-IcingaForWindowsRESTApi.psm1 index 1bb9b70..5825d5a 100644 --- a/lib/daemons/RestAPI/daemon/New-IcingaForWindowsRESTApi.psm1 +++ b/lib/daemons/RestAPI/daemon/New-IcingaForWindowsRESTApi.psm1 @@ -9,10 +9,6 @@ function New-IcingaForWindowsRESTApi() $RequireAuth ); - # Import the framework library components and initialise it - # as daemon - Use-Icinga -LibOnly -Daemon; - $RESTEndpoints = Invoke-IcingaNamespaceCmdlets -Command 'Register-IcingaRESTAPIEndpoint*'; Write-IcingaDebugMessage -Message ( [string]::Format( diff --git a/lib/daemons/RestAPI/threads/New-IcingaForWindowsRESTThread.psm1 b/lib/daemons/RestAPI/threads/New-IcingaForWindowsRESTThread.psm1 index 0775cf6..67572a5 100644 --- a/lib/daemons/RestAPI/threads/New-IcingaForWindowsRESTThread.psm1 +++ b/lib/daemons/RestAPI/threads/New-IcingaForWindowsRESTThread.psm1 @@ -5,10 +5,6 @@ function New-IcingaForWindowsRESTThread() $ThreadId ); - # Import the framework library components and initialise it - # as daemon - Use-Icinga -LibOnly -Daemon; - # Initialise our performance counter categories Show-IcingaPerformanceCounterCategories | Out-Null;