### Copyright 2023 Pixar ### ### Licensed under the Apache License, Version 2.0 (the "Apache License") ### with the following modification; you may not use this file except in ### compliance with the Apache License and the following modification to it: ### Section 6. Trademarks. is deleted and replaced with: ### ### 6. Trademarks. This License does not grant permission to use the trade ### names, trademarks, service marks, or product names of the Licensor ### and its affiliates, except as required to comply with Section 4(c) of ### the License and to reproduce the content of the NOTICE file. ### ### You may obtain a copy of the Apache License at ### ### http://www.apache.org/licenses/LICENSE-2.0 ### ### Unless required by applicable law or agreed to in writing, software ### distributed under the Apache License with the above modification is ### distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ### KIND, either express or implied. See the Apache License for the specific ### language governing permissions and limitations under the Apache License. ### ### ### module Jamf # A Patch Policy in the JSS # # When making new Patch Polices :patch_title and :target_version must be # provided as well as :name. # # :patch_title is the name or id of a currently active patch title # # :target_version is the string identfier of an available version of # the title. The target version MUST have a package assigned to it. # # See {Jamf::PatchTitle} and {Jamf::PatchSource.available_titles} for methods # to acquire such info. # # @see Jamf::APIObject # class PatchPolicy < Jamf::APIObject include Jamf::SelfServable include Jamf::Scopable include Jamf::Creatable include Jamf::Updatable RSRC_BASE = 'patchpolicies'.freeze RSRC_LIST_KEY = :patch_policies RSRC_OBJECT_KEY = :patch_policy RSRC_BY_PATCH_TITLE = 'patchpolicies/softwaretitleconfig/id/'.freeze # TODO: complain to jamf about this - should be the same as RSRC_LIST_KEY RSRC_BY_PATCH_TITLE_LIST_KEY = :"patch policies" SCOPE_TARGET_KEY = :computers AUTO_INSTALL_GRACE_PERIOD_MESSAGE = '$APP_NAMES will quit in $DELAY_MINUTES minutes so that $SOFTWARE_TITLE can be updated. Save anything you are working on and quit the app(s).'.freeze DFT_ENABLED = false # the default dist method - not in ssvc DFT_DISTRIBUTION = 'prompt'.freeze # the value of #deadline when there is no deadline NO_DEADLINE = :none DFT_DEADLINE = 7 # The valud of #grace_period when not defined DFT_GRACE_PERIOD = 15 DFT_GRACE_PERIOD_SUBJECT = 'Important'.freeze DFT_GRACE_PERIOD_MESSAGE = '$APP_NAMES will quit in $DELAY_MINUTES minutes so that $SOFTWARE_TITLE can be updated. Save anything you are working on and quit the app(s).'.freeze # See {Jamf::XMLWorkaround} USE_XML_WORKAROUND = { patch_policy: { general: { id: -1, name: Jamf::BLANK, enabled: nil, target_version: Jamf::BLANK, release_date: 0, incremental_update: nil, reboot: nil, minimum_os: Jamf::BLANK, kill_apps: [ { kill_app_name: Jamf::BLANK, kill_app_bundle_id: Jamf::BLANK } ], distribution_method: Jamf::BLANK, allow_downgrade: nil, patch_unknown: nil }, scope: { all_computers: nil, computers: [ { id: -1, name: Jamf::BLANK, udid: Jamf::BLANK } ], computer_groups: [ { id: -1, name: Jamf::BLANK } ], users: [ { id: -1, username: Jamf::BLANK } ], user_groups: [ { id: -1, name: Jamf::BLANK } ], buildings: [ { id: -1, name: Jamf::BLANK } ], departments: [ { id: -1, name: Jamf::BLANK } ], limitations: { network_segments: [ { id: -1, name: Jamf::BLANK } ], ibeacons: [ { id: -1, name: Jamf::BLANK } ] }, exclusions: { computers: [ { id: -1, name: Jamf::BLANK, udid: Jamf::BLANK } ], computer_groups: [ { id: -1, name: Jamf::BLANK } ], users: [ { id: -1, username: Jamf::BLANK } ], user_groups: [ { id: -1, name: Jamf::BLANK } ], buildings: [ { id: -1, name: Jamf::BLANK } ], departments: [ { id: -1, name: Jamf::BLANK } ], network_segments: [ { id: -1, name: Jamf::BLANK } ], ibeacons: [ { id: -1, name: Jamf::BLANK } ] } }, user_interaction: { install_button_text: Jamf::BLANK, self_service_description: Jamf::BLANK, self_service_icon: { id: -1, filename: Jamf::BLANK, uri: Jamf::BLANK }, notifications: { notification_enabled: nil, notification_type: Jamf::BLANK, notification_subject: Jamf::BLANK, notification_message: Jamf::BLANK, reminders: { notification_reminders_enabled: nil, notification_reminder_frequency: 1 } }, deadlines: { deadline_enabled: nil, deadline_period: 7 }, grace_period: { grace_period_duration: 15, notification_center_subject: 'Important', message: AUTO_INSTALL_GRACE_PERIOD_MESSAGE } }, software_title_configuration_id: 2 } }.freeze # Class Methods ################################ # Fetch name and id of all PatchPolicies tied to a given PatchTitle # # @param title[String,Integer] the name or id of the PatchTitle for which # to retrieve a list of patch policies # # @return [Array] the :id and :name of each policy for the title # def self.all_for_title(title, api: nil, cnx: Jamf.cnx) cnx = api if api title_id = Jamf::PatchTitle.valid_id title raise Jamf::NoSuchItemError, "No PatchTitle matching '#{title}'" unless title_id cnx.c_get("#{RSRC_BY_PATCH_TITLE}#{title_id}")[RSRC_BY_PATCH_TITLE_LIST_KEY] end # Override APIObject.fetch, since there's no .../patchpolicies/name/... endpoint # @see APIObject#fetch # def self.fetch(searchterm = nil, **args) cnx = args[:cnx] cnx ||= Jamf.cnx name_search = args.delete :name if name_search id = valid_id name_search, cnx: cnx raise Jamf::NoSuchItemError, "No #{self::RSRC_OBJECT_KEY} found #{err_detail}" unless id args[:id] = id end super end # fetch # Attributes ################################ # @return [Boolean] is this patch policy enabled? attr_reader :enabled alias enabled? enabled # When setting, the version must exist in the policy's PatchTitle, # and have a package assigned to it. # # @param new_tgt_vers[String] the new version for this Patch Policy. # # @return [String] The version deployed by this policy # attr_reader :target_version alias version target_version # @return [Time] when the target_version was released attr_reader :release_date # @return [Boolean] must this patch be installed only over the prev. version? attr_reader :incremental_update alias incremental_update? incremental_update # @return [Boolean] does this patch require a reboot after installation? attr_reader :reboot alias reboot_required reboot alias reboot? reboot alias reboot_required? reboot # @return [String] The min. OS version require to install this patch attr_reader :minimum_os # @return [Array] The apps that cannot be running when this is installed. # each Hash contains :kill_app_name and :kill_app_bundle_id, both Strings attr_reader :kill_apps # Can this title be downgraded to this version? # @param new_val [Boolean] # @return [Boolean] attr_reader :allow_downgrade alias allow_downgrade? allow_downgrade alias downgradable? allow_downgrade # Can this policy run when we don't know the prev. version? # @param new_val [Boolean] # @return [Boolean] attr_reader :patch_unknown alias patch_unknown? patch_unknown # How many days is the install deadline? # @param days [Integer, Symbol] :none, or a positive integer. Integers < 1 # have the same meaning as :none # # @return [Integer, Symnol] :none, or a positive integer attr_reader :deadline # @param new_period [Integer] Negative integers will be saved as 0 # # @return [Integer] How many minutes does the user have to quit the killapps? # attr_reader :grace_period alias grace_period_duration grace_period # @param subj [String] the new subject # # @return [String] The Subject of the message displayed asking the user to # quit the killapps within @grace_period minutes attr_reader :grace_period_subject alias grace_period_notification_center_subject grace_period_subject # @param subj [String] the new message # # @return [String] The message displayed asking the user to quit the killapps # within @grace_period minutes attr_reader :grace_period_message # @return [Integer] the id of the Jamf::PatchTitle for this policy. # Can be set with the patch_title: param of .make, but is read-only after # that. attr_reader :patch_title_id alias software_title_id patch_title_id alias software_title_configuration_id patch_title_id # When making new Patch Polices :patch_title is required and is # a Jamf::PatchTitle or the name or id of one # # If target_version: is provided, it must exist in the PatchTitle, # and must have a package assigned to it. # def initialize(**data) super # creation... unless in_jss @init_data[:general] ||= {} @init_data[:software_title_configuration_id] = validate_patch_title @init_data[:patch_title] # need this set here for the validations below @patch_title_id = @init_data[:software_title_configuration_id] # were we given target_version in the make params? validate_target_version @init_data[:target_version] if @init_data[:target_version] @init_data[:general][:target_version] = @init_data[:target_version] # other defaults @init_data[:general][:enabled] = false @init_data[:general][:allow_downgrade] = false @init_data[:general][:patch_unknown] = false @init_data[:general][:distribution_method] = DFT_DISTRIBUTION end @patch_title_id = @init_data[:software_title_configuration_id] gen = @init_data[:general] @enabled = gen[:enabled] @target_version = gen[:target_version] @allow_downgrade = gen[:allow_downgrade] @patch_unknown = gen[:patch_unknown] @init_data[:user_interaction] ||= {} deadlines = @init_data[:user_interaction][:deadlines] deadlines ||= {} deadlines[:deadline_period] = DFT_DEADLINE if deadlines[:deadline_period].to_s.empty? @deadline = deadlines[:deadline_enabled] ? deadlines[:deadline_period] : NO_DEADLINE grace = @init_data[:user_interaction][:grace_period] grace ||= {} @grace_period = grace[:grace_period_duration] @grace_period = DFT_GRACE_PERIOD if @grace_period.to_s.empty? @grace_period_subject = grace[:notification_center_subject] @grace_period_subject = DFT_GRACE_PERIOD_SUBJECT if @grace_period_subject.to_s.empty? @grace_period_message = grace[:message] @grace_period_message = DFT_GRACE_PERIOD_MESSAGE if @grace_period_message.to_s.empty? # read-only values, they come from the version. @release_date = JSS.epoch_to_time gen[:release_date] @incremental_update = gen[:incremental_update] @reboot = gen[:reboot] @minimum_os = gen[:minimum_os] @kill_apps = gen[:kill_apps] end # The Jamf::PatchTitle to for this PatchPolicy # # @param refresh [Boolean] Should the Title be re-fetched from the API? # # @return [Jamf::PatchTitle, nil] # def patch_title(refresh = false) @patch_title = nil if refresh @patch_title ||= Jamf::PatchTitle.fetch id: patch_title_id, cnx: @cnx end # @return [String] the name of the PatchTitle for this patch policy # def patch_title_name return @patch_title.name if @patch_title Jamf::PatchTitle.map_all_ids_to(:name)[software_title_configuration_id] end # See attr_reader :target_version # def target_version=(new_tgt_vers) return if new_tgt_vers == target_version @target_version = validate_target_version new_tgt_vers @need_to_update = true @refetch_for_new_version = true end # enable this policy # # @return [void] # def enable return if enabled @enabled = true @need_to_update = true end # disable this policy # # @return [void] # def disable return unless enabled @enabled = false @need_to_update = true end # see attr_reader :allow_downgrade # def allow_downgrade=(new_val) return if new_val == allow_downgrade @allow_downgrade = Jamf::Validate.boolean new_val @need_to_update = true end # see attr_reader :patch_unknown # def patch_unknown=(new_val) return if new_val == patch_unknown @patch_unknown = Jamf::Validate.boolean new_val @need_to_update = true end # see attr_reader :deadline # def deadline=(days) unless days == NO_DEADLINE days = Jamf::Validate.integer(days) days = NO_DEADLINE unless days.positive? end return if days == deadline @deadline = days @need_to_update = true end # see attr_reader :grace_period # def grace_period=(mins) mins = Jamf::Validate.integer(mins) mins = 0 if mins.negative? return if mins == grace_period @grace_period = mins @need_to_update = true end # see attr_reader :grace_period_subject # def grace_period_subject=(subj) return if grace_period_subject == subj.to_s @grace_period_subject = subj.to_s @need_to_update = true end # see attr_reader :grace_period_message # def grace_period_message=(msg) return if grace_period_message == msg @grace_period_message = msg @need_to_update = true end # Create a new PatchPolicy in the JSS # # @return [Integer] the id of the new policy # def create validate_for_saving # TODO: prepare for more cases where the POST rsrc is # different from the PUT/GET/DELETE. orig_rsrc = @rest_rsrc @rest_rsrc = "#{RSRC_BY_PATCH_TITLE}#{CGI.escape patch_title_id.to_s}" super @rest_rsrc = orig_rsrc refetch_version_info id end # Update an existing PatchPolicy with changes from ruby # # @return [Integer] the id of the policy # def update validate_for_saving super refetch_version_info if @refetch_for_new_version @refetch_for_new_version = false id end # Private Instance Methods ##################################### private # raise an error if the patch title we're trying to use isn't available in # the jss. If handed a PatchTitle instance, we assume it came from the JSS # ## @param new_title[String,Integer,Jamf::PatchTitle] the title to validate # # @return [Integer] the id of the valid title # def validate_patch_title(a_title) if a_title.is_a? Jamf::PatchTitle @patch_title = a_title return a_title.id end raise Jamf::MissingDataError, ':patch_title is required' unless a_title title_id = Jamf::PatchTitle.valid_id a_title, cnx: @cnx return title_id if title_id raise Jamf::NoSuchItemError, "No Patch Title matches '#{a_title}'" end # raise an exception if a given target version is not valid for this policy # Otherwise return it # def validate_target_version(tgt_vers) raise Jamf::MissingDataError, "target_version can't be nil" unless tgt_vers Jamf::Validate.non_empty_string tgt_vers unless patch_title(:refresh).versions.key? tgt_vers errmsg = "Version '#{tgt_vers}' does not exist for title: #{patch_title_name}." raise Jamf::NoSuchItemError, errmsg end return tgt_vers if patch_title.versions_with_packages.key? tgt_vers errmsg = "Version '#{tgt_vers}' cannot be used in Patch Policies until a package is assigned to it." raise Jamf::UnsupportedError, errmsg end def validate_for_saving validate_target_version target_version end # Update our local version data after the target_version is changed # def refetch_version_info tmp = self.class.fetch id: id, cnx: @cnx @release_date = tmp.release_date @incremental_update = tmp.incremental_update @reboot = tmp.reboot @minimum_os = tmp.minimum_os @kill_apps = tmp.kill_apps end def rest_xml doc = REXML::Document.new Jamf::Connection::XML_HEADER obj = doc.add_element RSRC_OBJECT_KEY.to_s general = obj.add_element 'general' general.add_element('target_version').text = target_version general.add_element('name').text = name general.add_element('enabled').text = enabled?.to_s general.add_element('allow_downgrade').text = allow_downgrade general.add_element('patch_unknown').text = patch_unknown # always include the scope if we have no id yet - we could be # a clone of an existing object obj << scope.scope_xml if scope.should_update? || @id.nil? add_self_service_xml doc # self svc xml gave us the user_interaction section user_int = obj.elements['user_interaction'] dlines = user_int.add_element 'deadlines' if deadline == NO_DEADLINE dlines.add_element('deadline_enabled').text = 'false' else dlines.add_element('deadline_enabled').text = 'true' dlines.add_element('deadline_period').text = deadline.to_s end grace = user_int.add_element 'grace_period' grace.add_element('grace_period_duration').text = grace_period.to_s grace.add_element('notification_center_subject').text = grace_period_subject.to_s grace.add_element('message').text = grace_period_message.to_s doc.to_s end end # class PatchPolicy end # module Jamf