From f410c375e6f769984f275b53e72eaec4ad610529 Mon Sep 17 00:00:00 2001 From: Markus Spanner-Denzer Date: Fri, 19 Sep 2025 11:06:56 +0200 Subject: [PATCH] adding new property "group" to run the scheduled task in group context (TASK_LOGON_GROUP) --- REFERENCE.md | 13 +++++++-- .../scheduled_task/taskscheduler_api2.rb | 22 ++++++++++++-- .../scheduled_task/win32_taskscheduler.rb | 22 ++++++++++++-- lib/puppet/type/scheduled_task.rb | 17 +++++++++-- .../puppet_labs/scheduled_task/task.rb | 29 ++++++++++++++++++- 5 files changed, 93 insertions(+), 10 deletions(-) diff --git a/REFERENCE.md b/REFERENCE.md index 08c8aca8..920df2a3 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -66,6 +66,14 @@ The basic property that the resource should be in. Default value: `present` +##### `group` + +The group to run the scheduled task as. + +Please also note that Puppet must be running as a privileged user +in order to manage `scheduled_task` resources. Running as an +unprivileged user will result in 'access denied' errors. + ##### `trigger` One or more triggers defining when the task should run. A single trigger is @@ -205,7 +213,8 @@ conditions will fail with a reported error of 'The operation completed successfully'. It is recommended that you either choose another user to run the scheduled task, or alter the security policy to allow v1 scheduled tasks to run as the -'SYSTEM' account. Defaults to 'SYSTEM'. +'SYSTEM' account. +Defaults to 'SYSTEM' unless the group property is set. Please also note that Puppet must be running as a privileged user in order to manage `scheduled_task` resources. Running as an @@ -216,8 +225,6 @@ user does not end with a dollar sign (`$`) signifying a Group Managed Service Account (gMSA), the task will be created with `Run only when user is logged on` specified. -Default value: `system` - ##### `working_dir` The full path of the directory in which to start the command. diff --git a/lib/puppet/provider/scheduled_task/taskscheduler_api2.rb b/lib/puppet/provider/scheduled_task/taskscheduler_api2.rb index 9e8df1ef..0e42040b 100644 --- a/lib/puppet/provider/scheduled_task/taskscheduler_api2.rb +++ b/lib/puppet/provider/scheduled_task/taskscheduler_api2.rb @@ -59,6 +59,10 @@ def user account end + def group + task.group_information + end + def compatibility task.compatibility end @@ -79,6 +83,14 @@ def user_insync?(current, should) Puppet::Util::Windows::SID.name_to_sid(current) == Puppet::Util::Windows::SID.name_to_sid(should[0]) end + def group_insync?(current, should) + return false unless current + + # By comparing account SIDs we don't have to worry about case + # sensitivity, or canonicalization of the account name. + Puppet::Util::Windows::SID.name_to_sid(current) == Puppet::Util::Windows::SID.name_to_sid(should[0]) + end + def trigger_insync?(current, should) should = [should] unless should.is_a?(Array) current = [current] unless current.is_a?(Array) @@ -177,12 +189,18 @@ def user=(value) end end + def group=(value) + raise("Invalid group: #{value}") unless Puppet::Util::Windows::SID.name_to_sid(value) + + task.set_group_information(value) + end + def create @triggers = nil @task = PuppetX::PuppetLabs::ScheduledTask::Task.new(resource[:name]) self.command = resource[:command] - [:arguments, :working_dir, :enabled, :trigger, :user, :compatibility, :description].each do |prop| + [:arguments, :working_dir, :enabled, :trigger, :user, :group, :compatibility, :description].each do |prop| send("#{prop}=", resource[prop]) if resource[prop] end end @@ -202,7 +220,7 @@ def flush # this is a Windows security feature with the v1 COM APIs that prevent # arbitrary reassignment of a task scheduler command to run as SYSTEM # without the authorization to do so - self.user = resource[:user] + self.user = resource[:user] unless resource[:group] task.save @task = nil end diff --git a/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb b/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb index 18578b6a..7f16253e 100644 --- a/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb +++ b/lib/puppet/provider/scheduled_task/win32_taskscheduler.rb @@ -56,6 +56,10 @@ def user account end + def group + task.group_information + end + def trigger @triggers ||= task.triggers.compact # remove nils for unsupported trigger types end @@ -72,6 +76,14 @@ def user_insync?(current, should) Puppet::Util::Windows::SID.name_to_sid(current) == Puppet::Util::Windows::SID.name_to_sid(should[0]) end + def group_insync?(current, should) + return false unless current + + # By comparing account SIDs we don't have to worry about case + # sensitivity, or canonicalization of the account name. + Puppet::Util::Windows::SID.name_to_sid(current) == Puppet::Util::Windows::SID.name_to_sid(should[0]) + end + def trigger_insync?(current, should) should = [should] unless should.is_a?(Array) current = [current] unless current.is_a?(Array) @@ -153,12 +165,18 @@ def user=(value) end end + def group=(value) + raise("Invalid group: #{value}") unless Puppet::Util::Windows::SID.name_to_sid(value) + + task.set_group_information(value) + end + def create @triggers = nil @task = PuppetX::PuppetLabs::ScheduledTask::Task.new(resource[:name], :v1_compatibility) self.command = resource[:command] - [:arguments, :working_dir, :enabled, :trigger, :user, :description].each do |prop| + [:arguments, :working_dir, :enabled, :trigger, :user, :group, :description].each do |prop| send("#{prop}=", resource[prop]) if resource[prop] end end @@ -177,7 +195,7 @@ def flush # this is a Windows security feature with the v1 COM APIs that prevent # arbitrary reassignment of a task scheduler command to run as SYSTEM # without the authorization to do so - self.user = resource[:user] + self.user = resource[:user] unless resource[:group] task.save @task = nil end diff --git a/lib/puppet/type/scheduled_task.rb b/lib/puppet/type/scheduled_task.rb index fadf9685..d9639669 100644 --- a/lib/puppet/type/scheduled_task.rb +++ b/lib/puppet/type/scheduled_task.rb @@ -77,7 +77,8 @@ completed successfully'. It is recommended that you either choose another user to run the scheduled task, or alter the security policy to allow v1 scheduled tasks to run as the - 'SYSTEM' account. Defaults to 'SYSTEM'. + 'SYSTEM' account. + Defaults to 'SYSTEM' unless the group property is set. Please also note that Puppet must be running as a privileged user in order to manage `scheduled_task` resources. Running as an @@ -88,7 +89,7 @@ Managed Service Account (gMSA), the task will be created with `Run only when user is logged on` specified." - defaultto :system + defaultto { :system if @resource[:group].nil? || @resource[:group].empty? } def insync?(current) provider.user_insync?(current, @should) @@ -103,6 +104,18 @@ def insync?(current) to determine if a scheduled task is in sync or not." end + newproperty(:group) do + desc "The group to run the scheduled task as. + + Please also note that Puppet must be running as a privileged user + in order to manage `scheduled_task` resources. Running as an + unprivileged user will result in 'access denied' errors." + + def insync?(current) + provider.group_insync?(current, @should) + end + end + newproperty(:compatibility, required_features: :compatibility) do desc "The compatibility level associated with the task. May currently be set to 1 for compatibility with tasks on a Windows XP or Windows Server diff --git a/lib/puppet_x/puppet_labs/scheduled_task/task.rb b/lib/puppet_x/puppet_labs/scheduled_task/task.rb index e5d39b3f..f76ec103 100644 --- a/lib/puppet_x/puppet_labs/scheduled_task/task.rb +++ b/lib/puppet_x/puppet_labs/scheduled_task/task.rb @@ -218,10 +218,15 @@ def initialize(task_name, compatibility_level = nil) # definition populated when task exists, otherwise new @task, @definition = self.class.task(@full_task_path) task_userid = @definition.Principal.UserId || '' + task_groupid = @definition.Principal.GroupId || '' self.compatibility = TASK_COMPATIBILITY::TASK_COMPATIBILITY_V1 if compatibility_level == :v1_compatibility - set_account_information(task_userid, nil) + if task_groupid == '' + set_group_information(task_groupid) + else + set_account_information(task_userid, nil) + end end # API v1 Compatibility list @@ -320,6 +325,8 @@ def save task_user = nil task_password = nil + # user and password should be nil if task should run in + # service account or group context case @definition.Principal.LogonType when TASK_LOGON_TYPE::TASK_LOGON_PASSWORD, TASK_LOGON_TYPE::TASK_LOGON_INTERACTIVE_TOKEN_OR_PASSWORD @@ -369,6 +376,18 @@ def set_account_information(user, password) true end + # Sets the +group+ for the given task if the task should run in a group context. + # If successfull then true is returned. + # + def set_group_information(group) + @definition.Principal.RunLevel = TASK_RUNLEVEL_TYPE::TASK_RUNLEVEL_HIGHEST + + @definition.Principal.GroupId = group + @definition.Principal.LogonType = TASK_LOGON_TYPE::TASK_LOGON_GROUP + + true + end + # Returns the user associated with the task or nil if no user has yet # been associated with the task. # @@ -377,6 +396,14 @@ def account_information principal&.UserId end + # Returns the group associated with the task or nil if no group has yet + # been associated with the task. + # + def group_information + principal = @definition.Principal + principal&.GroupId + end + # Returns the name of the application associated with the task. # def application_name