Skip to content

Commit

Permalink
Merge pull request #174 from danielboth/ScheduledTask-gMSA-support
Browse files Browse the repository at this point in the history
ScheduledTask: Add support for Group Managed Service Accounts
  • Loading branch information
PlagueHO authored Sep 25, 2018
2 parents c666f37 + c8d0aa7 commit cb145a5
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -267,6 +272,10 @@ function Get-TargetResource
[System.Management.Automation.PSCredential]
$ExecuteAsCredential,

[Parameter()]
[System.String]
$ExecuteAsGMSA,

[Parameter()]
[System.UInt32]
$DaysInterval = 1,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -755,6 +769,10 @@ function Set-TargetResource
[System.Management.Automation.PSCredential]
$ExecuteAsCredential,

[Parameter()]
[System.String]
$ExecuteAsGMSA,

[Parameter()]
[System.UInt32]
$DaysInterval = 1,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1520,6 +1554,10 @@ function Test-TargetResource
[System.Management.Automation.PSCredential]
$ExecuteAsCredential,

[Parameter()]
[System.String]
$ExecuteAsGMSA,

[Parameter()]
[System.UInt32]
$DaysInterval = 1,
Expand Down Expand Up @@ -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 ([email protected]) is used.
#>

$PSBoundParameters['ExecuteAsGMSA'] = $PSBoundParameters.ExecuteAsGMSA -replace '^.+\\|@.+', $null
}

$desiredValues = $PSBoundParameters
$desiredValues.TaskPath = $TaskPath

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}'.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 [email protected] (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
}
}
}
146 changes: 142 additions & 4 deletions Tests/Unit/MSFT_ScheduledTask.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1528,7 +1528,7 @@ try
Settings = [pscustomobject] @{
Enabled = $true
}
}
}
}

It 'Should return the correct values from Get-TargetResource' {
Expand Down Expand Up @@ -1577,7 +1577,7 @@ try
Settings = [pscustomobject] @{
Enabled = $true
}
}
}
}

It 'Should return the correct values from Get-TargetResource' {
Expand Down Expand Up @@ -1652,7 +1652,7 @@ try
Settings = [pscustomobject] @{
Enabled = $true
}
}
}
}

It 'Should return the correct values from Get-TargetResource' {
Expand Down Expand Up @@ -1707,7 +1707,7 @@ try
Settings = [pscustomobject] @{
Enabled = $true
}
}
}
}

It 'Should return the correct values from Get-TargetResource' {
Expand All @@ -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 = '[email protected]'

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
Expand Down

0 comments on commit cb145a5

Please sign in to comment.