diff --git a/CHANGELOG.md b/CHANGELOG.md index 95311714..d66effcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- ScheduledTask: + - Added support for Group Managed Service Accounts, implemented using the ExecuteAsGMSA + parameter. Fixes [Issue #111](https://github.com/PowerShell/ComputerManagementDsc/issues/111) + ## 5.2.0.0 - PowershellExecutionPolicy: diff --git a/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/MSFT_ScheduledTask.psm1 b/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/MSFT_ScheduledTask.psm1 index 4210a65e..c503ed96 100644 --- a/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/MSFT_ScheduledTask.psm1 +++ b/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/MSFT_ScheduledTask.psm1 @@ -73,7 +73,12 @@ $script:localizedData = Get-LocalizedData ` .PARAMETER ExecuteAsCredential The credential this task should execute as. If not specified defaults to running - as the local system account. + as the local system account. Cannot be used in combination with ExecuteAsGMSA. + Not used in Get-TargetResource. + + .PARAMETER ExecuteAsGMSA + The gMSA (Group Managed Service Account) this task should execute as. Cannot be + used in combination with ExecuteAsCredential. Not used in Get-TargetResource. .PARAMETER DaysInterval @@ -267,6 +272,10 @@ function Get-TargetResource [System.Management.Automation.PSCredential] $ExecuteAsCredential, + [Parameter()] + [System.String] + $ExecuteAsGMSA, + [Parameter()] [System.UInt32] $DaysInterval = 1, @@ -503,6 +512,7 @@ function Get-TargetResource ScheduleType = $returnScheduleType RepeatInterval = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $trigger.Repetition.Interval ExecuteAsCredential = $task.Principal.UserId + ExecuteAsGMSA = $task.Principal.UserId -replace '^.+\\|@.+', $null Enable = $settings.Enabled DaysInterval = $trigger.DaysInterval RandomDelay = ConvertTo-TimeSpanStringFromScheduledTaskString -TimeSpan $trigger.RandomDelay @@ -579,7 +589,11 @@ function Get-TargetResource .PARAMETER ExecuteAsCredential The credential this task should execute as. If not specified defaults to running - as the local system account. + as the local system account. Cannot be used in combination with ExecuteAsGMSA. + + .PARAMETER ExecuteAsGMSA + The gMSA (Group Managed Service Account) this task should execute as. Cannot be + used in combination with ExecuteAsCredential. .PARAMETER DaysInterval Specifies the interval between the days in the schedule. An interval of 1 produces @@ -755,6 +769,10 @@ function Set-TargetResource [System.Management.Automation.PSCredential] $ExecuteAsCredential, + [Parameter()] + [System.String] + $ExecuteAsGMSA, + [Parameter()] [System.UInt32] $DaysInterval = 1, @@ -951,6 +969,13 @@ function Set-TargetResource -ArgumentName EventSubscription } + if ($ExecuteAsCredential -and $ExecuteAsGMSA) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.gMSAandCredentialError) ` + -ArgumentName ExecuteAsGMSA + } + # Configure the action $actionParameters = @{ Execute = $ActionExecutable @@ -1193,7 +1218,12 @@ function Set-TargetResource # Prepare the register arguments $registerArguments = @{} - if ($PSBoundParameters.ContainsKey('ExecuteAsCredential')) + if ($PSBoundParameters.ContainsKey('ExecuteAsGMSA')) + { + $username = $ExecuteAsGMSA + $LogonType = 'Password' + } + elseif ($PSBoundParameters.ContainsKey('ExecuteAsCredential')) { $username = $ExecuteAsCredential.UserName $registerArguments.Add('User', $username) @@ -1343,7 +1373,11 @@ function Set-TargetResource .PARAMETER ExecuteAsCredential The credential this task should execute as. If not specified defaults to running - as the local system account. + as the local system account. Cannot be used in combination with ExecuteAsGMSA. + + .PARAMETER ExecuteAsGMSA + The gMSA (Group Managed Service Account) this task should execute as. Cannot be + used in combination with ExecuteAsCredential. .PARAMETER DaysInterval Specifies the interval between the days in the schedule. An interval of 1 produces @@ -1520,6 +1554,10 @@ function Test-TargetResource [System.Management.Automation.PSCredential] $ExecuteAsCredential, + [Parameter()] + [System.String] + $ExecuteAsGMSA, + [Parameter()] [System.UInt32] $DaysInterval = 1, @@ -1729,6 +1767,21 @@ function Test-TargetResource $PSBoundParameters['ExecuteAsCredential'] = $username } + if ($PSBoundParameters.ContainsKey('ExecuteAsGMSA')) + { + <# + There is a difference in W2012R2 and W2016 behaviour, + W2012R2 returns the gMSA including the DOMAIN prefix, + W2016 returns this without. So to be sure strip off the + domain part in Get & Test. This means we either need to + remove everything before \ in the case of the DOMAIN\User + format, or we need to remove everything after @ in case + when the UPN format (User@domain.fqdn) is used. + #> + + $PSBoundParameters['ExecuteAsGMSA'] = $PSBoundParameters.ExecuteAsGMSA -replace '^.+\\|@.+', $null + } + $desiredValues = $PSBoundParameters $desiredValues.TaskPath = $TaskPath diff --git a/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/MSFT_ScheduledTask.schema.mof b/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/MSFT_ScheduledTask.schema.mof index 4d40f88a..c1c84bd4 100644 --- a/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/MSFT_ScheduledTask.schema.mof +++ b/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/MSFT_ScheduledTask.schema.mof @@ -13,6 +13,7 @@ class MSFT_ScheduledTask : OMI_BaseResource [Write, Description("Present if the task should exist, Absent if it should be removed"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; [Write, Description("True if the task should be enabled, false if it should be disabled")] boolean Enable; [Write, Description("The credential this task should execute as. If not specified defaults to running as the local system account"), EmbeddedInstance("MSFT_Credential")] string ExecuteAsCredential; + [Write, Description("The gMSA (Group Managed Service Account) this task should execute as. Cannot be used in combination with ExecuteAsCredential.")] string ExecuteAsGMSA; [Write, Description("Specifies the interval between the days in the schedule. An interval of 1 produces a daily schedule. An interval of 2 produces an every-other day schedule.")] Uint32 DaysInterval; [Write, Description("Specifies a random amount of time to delay the start time of the trigger. The delay time is a random time between the time the task triggers and the time that you specify in this setting.")] String RandomDelay; [Write, Description("Specifies how long the repetition pattern repeats after the task starts. May be set to `Indefinitely` to specify an indefinite duration.")] String RepetitionDuration; diff --git a/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/en-US/MSFT_ScheduledTask.strings.psd1 b/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/en-US/MSFT_ScheduledTask.strings.psd1 index d811266f..15b009fe 100644 --- a/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/en-US/MSFT_ScheduledTask.strings.psd1 +++ b/Modules/ComputerManagementDsc/DSCResources/MSFT_ScheduledTask/en-US/MSFT_ScheduledTask.strings.psd1 @@ -11,6 +11,7 @@ ConvertFrom-StringData @' WeeksIntervalError = WeeksInterval must be greater than zero (0) for Weekly schedules. WeeksInterval specified is '{0}'. WeekDayMissingError = At least one weekday must be selected for Weekly schedule. OnEventSubscriptionError = No (valid) XML Event Subscription was provided. This is required when the scheduletype is OnEvent. + gMSAandCredentialError = Both ExecuteAsGMSA and ExecuteAsCredential parameters have been specified. A task can either run as a gMSA (Group Managed Service Account) or as a custom credential, not both. Please modify your configuration to include just one of the two. TriggerCreationError = Error creating new scheduled task trigger. ConfigureTriggerRepetitionMessage = Configuring trigger repetition. RepetitionIntervalError = Repetition interval is set to '{0}' but repetition duration is '{1}'. diff --git a/Modules/ComputerManagementDsc/Examples/Resources/ScheduledTask/14-RunPowerShellTaskOnceAsGroupManagedServiceAccount.ps1 b/Modules/ComputerManagementDsc/Examples/Resources/ScheduledTask/14-RunPowerShellTaskOnceAsGroupManagedServiceAccount.ps1 new file mode 100644 index 00000000..33061ff0 --- /dev/null +++ b/Modules/ComputerManagementDsc/Examples/Resources/ScheduledTask/14-RunPowerShellTaskOnceAsGroupManagedServiceAccount.ps1 @@ -0,0 +1,37 @@ +<# + .EXAMPLE + This example creates a scheduled task called 'Test task Run As gMSA' + in the folder task folder 'MyTasks' that starts a new powershell process once. + The task will run as the user passed into the ExecuteAsGMSA parameter. +#> +Configuration Example +{ + param + ( + [Parameter()] + [System.String[]] + $NodeName = 'localhost', + + # Group Managed Service Account must be in the form of DOMAIN\gMSA$ or user@domain.fqdn (UPN) + [Parameter()] + [ValidatePattern('^\w+\\\w+\$$|\w+@\w+\.\w+')] + [System.String] + $GroupManagedServiceAccount = 'DOMAIN\gMSA$' + ) + + Import-DscResource -ModuleName ComputerManagementDsc + + Node $NodeName + { + ScheduledTask MaintenanceScriptExample + { + TaskName = 'Test task Run As gMSA' + TaskPath = '\MyTasks' + ActionExecutable = 'C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe' + ScheduleType = 'Once' + ActionWorkingPath = (Get-Location).Path + Enable = $true + ExecuteAsGMSA = $GroupManagedServiceAccount + } + } +} diff --git a/Tests/Unit/MSFT_ScheduledTask.Tests.ps1 b/Tests/Unit/MSFT_ScheduledTask.Tests.ps1 index e3943a7a..b9071ee5 100644 --- a/Tests/Unit/MSFT_ScheduledTask.Tests.ps1 +++ b/Tests/Unit/MSFT_ScheduledTask.Tests.ps1 @@ -1528,7 +1528,7 @@ try Settings = [pscustomobject] @{ Enabled = $true } - } + } } It 'Should return the correct values from Get-TargetResource' { @@ -1577,7 +1577,7 @@ try Settings = [pscustomobject] @{ Enabled = $true } - } + } } It 'Should return the correct values from Get-TargetResource' { @@ -1652,7 +1652,7 @@ try Settings = [pscustomobject] @{ Enabled = $true } - } + } } It 'Should return the correct values from Get-TargetResource' { @@ -1707,7 +1707,7 @@ try Settings = [pscustomobject] @{ Enabled = $true } - } + } } It 'Should return the correct values from Get-TargetResource' { @@ -1728,6 +1728,144 @@ try { Set-TargetResource @testParameters } | Should throw } } + + Context 'When a scheduled task is created using a Group Managed Service Account' { + $testParameters = @{ + TaskName = 'Test task' + TaskPath = '\Test\' + ActionExecutable = 'C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe' + ScheduleType = 'Once' + RepeatInterval = (New-TimeSpan -Minutes 15).ToString() + RepetitionDuration = (New-TimeSpan -Hours 8).ToString() + ExecuteAsGMSA = 'DOMAIN\gMSA$' + ExecuteAsCredential = [pscredential]::new('DEMO\RightUser', (ConvertTo-SecureString 'ExamplePassword' -AsPlainText -Force)) + Verbose = $true + } + + It 'Should return an error when both the ExecuteAsGMSA an ExecuteAsCredential ar specified' { + try + { + Set-TargetResource @testParameters -ErrorVariable duplicateCredential + } + catch + { + # Error from Set-TargetResource expected + } + finally + { + $duplicateCredential.Message | Should -Be "Both ExecuteAsGMSA and ExecuteAsCredential parameters have been specified. A task can either run as a gMSA (Group Managed Service Account) or as a custom credential, not both. Please modify your configuration to include just one of the two.`r`nParameter name: ExecuteAsGMSA" + } + } + + $testParameters.Remove('ExecuteAsCredential') + + It 'Should call Register-ScheduledTask with the name of the Group Managed Service Account' { + Set-TargetResource @testParameters + Assert-MockCalled -CommandName Register-ScheduledTask -Times 1 -Scope It -ParameterFilter { + $User -eq $null -and $Inputobject.Principal.UserId -eq $testParameters.ExecuteAsGMSA + } + } + + It 'Should set the LogonType to Password when a Group Managed Service Account is used' { + Set-TargetResource @testParameters + Assert-MockCalled -CommandName Register-ScheduledTask -Times 1 -Scope It -ParameterFilter { + $Inputobject.Principal.Logontype -eq 'Password' + } + } + + Mock -CommandName Get-ScheduledTask -MockWith { + @{ + TaskName = $testParameters.TaskName + TaskPath = $testParameters.TaskPath + Actions = @( + [pscustomobject] @{ + Execute = $testParameters.ActionExecutable + } + ) + Triggers = @( + [pscustomobject] @{ + Repetition = @{ + Duration = "PT$([System.TimeSpan]::Parse($testParameters.RepetitionDuration).TotalHours)H" + Interval = "PT$([System.TimeSpan]::Parse($testParameters.RepeatInterval).TotalMinutes)M" + } + CimClass = @{ + CimClassName = 'MSFT_TaskTimeTrigger' + } + } + ) + Principal = [pscustomobject] @{ + UserId = 'gMSA$' + } + } + } + + It 'Should return true if the task is in desired state and given gMSA user in DOMAIN\User$ format' { + Test-TargetResource @testParameters | Should -Be $true + } + + $testParameters.ExecuteAsGMSA = 'gMSA$@domain.fqdn' + + It 'Should return true if the task is in desired state and given gMSA user in UPN format' { + Test-TargetResource @testParameters | Should -Be $true + } + } + + Context 'When a scheduled task Group Managed Service Account is changed' { + $testParameters = @{ + TaskName = 'Test task' + TaskPath = '\Test\' + ActionExecutable = 'C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe' + ScheduleType = 'Once' + RepeatInterval = (New-TimeSpan -Minutes 15).ToString() + RepetitionDuration = (New-TimeSpan -Hours 8).ToString() + ExecuteAsGMSA = 'DOMAIN\gMSA$' + Verbose = $true + } + + Mock -CommandName Get-ScheduledTask -MockWith { + @{ + TaskName = $testParameters.TaskName + TaskPath = $testParameters.TaskPath + Actions = @( + [pscustomobject] @{ + Execute = $testParameters.ActionExecutable + } + ) + Triggers = @( + [pscustomobject] @{ + Repetition = @{ + Duration = "PT$([System.TimeSpan]::Parse($testParameters.RepetitionDuration).TotalHours)H" + Interval = "PT$([System.TimeSpan]::Parse($testParameters.RepeatInterval).TotalMinutes)M" + } + CimClass = @{ + CimClassName = 'MSFT_TaskTimeTrigger' + } + } + ) + Principal = [pscustomobject] @{ + UserId = 'update_gMSA$' + } + } + } + + It 'Should return false on Test-TargetResource if the task is not in desired state and given gMSA user in DOMAIN\User$ format' { + Test-TargetResource @testParameters | Should -Be $false + } + + It 'Should call Set-ScheduledTask using the new Group Managed Service Account' { + Set-TargetResource @testParameters + Assert-MockCalled -CommandName Set-ScheduledTask -Times 1 -Scope It -ParameterFilter { + $Inputobject.Principal.UserId -eq $testParameters.ExecuteAsGMSA + } + } + + It 'Should set the LogonType to Password when a Group Managed Service Account is used' { + Set-TargetResource @testParameters + Assert-MockCalled -CommandName Set-ScheduledTask -Times 1 -Scope It -ParameterFilter { + $Inputobject.Principal.Logontype -eq 'Password' + } + } + } } } #endregion