### Copyright 2022 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 source. The abstract parent class of {Jamf::PatchInternalSource} and # {Jamf::PatchExternalSource} # # @see Jamf::APIObject # class PatchSource < Jamf::APIObject include Jamf::Updatable HTTP = 'http'.freeze HTTPS = 'https'.freeze DFT_ENABLED = false DFT_SSL = true DFT_SSL_PORT = 443 DFT_NO_SSL_PORT = 80 AVAILABLE_TITLES_RSRC = 'patchavailabletitles/sourceid/'.freeze # TODO: remove this and adjust parsing when jamf fixes the JSON # Data map for PatchReport XML data parsing cuz Borked JSON # @see {Jamf::XMLWorkaround} for details AVAILABLE_TITLES_DATA_MAP = { patch_available_titles: { available_titles: [ { name_id: Jamf::BLANK, current_version: Jamf::BLANK, publisher: Jamf::BLANK, last_modified: Jamf::BLANK, app_name: Jamf::BLANK } ] } }.freeze # Class Methods # # These work from this metaclass, as well as from the # subclasses. In the metaclass, both subclasses are searched # and a :type value is available. ############################################ # Get names, ids and types for all patch sources # # @param refresh[Boolean] should the data be re-queried from the API? # # @param cnx [Jamf::Connection] an API connection to use for the query. # Defaults to the corrently active API. See {Jamf::Connection} # # @return [ArrayString, :id=> Integer, :type => Symbol}>] # def self.all(refresh = false, api: nil, cnx: Jamf.cnx) cnx = api if api if self == Jamf::PatchSource int = Jamf::PatchInternalSource.all(refresh, cnx: cnx).each { |s| s[:type] = :internal } ext = Jamf::PatchExternalSource.all(refresh, cnx: cnx).each { |s| s[:type] = :external } return (int + ext).sort! { |s1, s2| s1[:id] <=> s2[:id] } end super end # Get names, ids for all patch internal sources # # the same as Jamf::PatchInternalSource.all refresh, cnx: cnx # # @see Jamf::PatchInternalSource.all # def self.all_internal(refresh = false, api: nil, cnx: Jamf.cnx) cnx = api if api Jamf::PatchInternalSource.all refresh, cnx: cnx end # Get names, ids for all patch internal sources # # the same as Jamf::PatchExternalSource.all refresh, cnx: cnx # # @see Jamf::PatchExternalSource.all # def self.all_external(refresh = false, api: nil, cnx: Jamf.cnx) cnx = api if api Jamf::PatchExternalSource.all refresh, cnx: cnx end # @see Jamf::APIObject.all_objects # def self.all_objects(refresh = false, api: nil, cnx: Jamf.cnx) cnx = api if api if self == Jamf::PatchSource int = Jamf::PatchInternalSource.all_objects refresh, cnx: cnx ext = Jamf::PatchExternalSource.all_objects refresh, cnx: cnx return (int + ext).sort! { |s1, s2| s1.id <=> s2.id } end super end # Fetch either an internal or external patch source # # BUG: there's an API bug when fetching a non-existent patch source # which is why we rescue 500 internal server errors and report them # as 'no matching patch source' # # @see APIObject.fetch # def self.fetch(searchterm = nil, **args) if self == Jamf::PatchSource begin fetched = Jamf::PatchInternalSource.fetch searchterm, **args rescue fetched = nil end unless fetched begin fetched = Jamf::PatchExternalSource.fetch searchterm, **args rescue raise Jamf::NoSuchItemError, 'No matching PatchSource found' end end return fetched end # if self == Jamf::PatchSource begin super searchterm, **args rescue Jamf::NoSuchItemError raise Jamf::NoSuchItemError, "No matching #{self::RSRC_OBJECT_KEY} found" end end # Only Jamf::PatchExternalSources can be created # # @see APIObject.make # def self.create(**args) case name when 'Jamf::PatchSource' Jamf::PatchExternalSource.make args when 'Jamf::PatchExternalSource' super when 'Jamf::PatchInternalSource' raise Jamf::UnsupportedError, 'PatchInteralSources cannot be created.' end end # bacward compatibility def self.make(**args) create(**args) end # Only Jamf::PatchExternalSources can be deleted # # @see APIObject.delete # def self.delete(victims, api: nil, cnx: Jamf.cnx) cnx = api if api case name when 'Jamf::PatchSource' Jamf::PatchExternalSource victims, cnx: cnx when 'Jamf::PatchExternalSource' super when 'Jamf::PatchInternalSource' raise Jamf::UnsupportedError, 'PatchInteralSources cannot be deleted.' end end # Get a list of patch titles available from a Patch Source (either # internal or external, since they have unique ids ) # # @param source[String,Integer] name or id of the Patch Source for which to # get the available titles # # @param cnx [Jamf::Connection] The api connection to use for the query # Defaults to the currently active connection # # @return [Array] One hash for each available title, with # these keys: # :name_id String # :current_version String # :publisher String # :last_modified Time # :app_name String # def self.available_titles(source, api: nil, cnx: Jamf.cnx) cnx = api if api src_id = valid_patch_source_id source, cnx: cnx raise Jamf::NoSuchItemError, "No Patch Source found matching: #{source}" unless src_id rsrc_base = if valid_patch_source_type(src_id, cnx: cnx) == :internal Jamf::PatchInternalSource::AVAILABLE_TITLES_RSRC else Jamf::PatchExternalSource::AVAILABLE_TITLES_RSRC end rsrc = "#{rsrc_base}#{src_id}" begin # TODO: remove this and adjust parsing when jamf fixes the JSON raw = Jamf::XMLWorkaround.data_via_xml(rsrc, AVAILABLE_TITLES_DATA_MAP, cnx) rescue Jamf::NoSuchItemError return [] end titles = raw[:patch_available_titles][:available_titles] titles.each { |t| t[:last_modified] = Jamf.parse_time t[:last_modified] } titles end # FOr a given patch source, an array of available 'name_id's # which are uniq identifiers for titles available on that source. # # @see available_titles # # @return [Array] the name_ids available on the source # def self.available_name_ids(source, api: nil, cnx: Jamf.cnx) cnx = api if api available_titles(source, cnx: cnx).map { |t| t[:name_id] } end # Given a name or id for a Patch Source (internal or external) # return the id if it exists, or nil if it doesn't. # # NOTE: does not indicate which kind of source it is, just that it exists # and can be used as a source_id for a patch title. # @see .valid_patch_source_type # # @param ident[String,Integer] the name or id to validate # # @param refresh [Boolean] Should the data be re-read from the server # # @param cnx [Jamf::Connection] an API connection to use for the query. # Defaults to the corrently active API. See {Jamf::Connection} # # @return [Integer, nil] the valid id or nil if it doesn't exist. # def self.valid_patch_source_id(ident, refresh = false, api: nil, cnx: Jamf.cnx) cnx = api if api id = Jamf::PatchInternalSource.valid_id ident, refresh, cnx: cnx id ||= Jamf::PatchExternalSource.valid_id ident, refresh, cnx: cnx id end # Given a name or id for a Patch Source # return :internal or :external if it exists, or nil if it doesnt. # # @param ident[String,Integer] the name or id to validate # # @param refresh [Boolean] Should the data be re-read from the server # # @param cnx [Jamf::Connection] an API connection to use for the query. # Defaults to the corrently active API. See {Jamf::Connection} # # @return [Symbol, nil] :internal, :external, or nil if it doesn't exist. # def self.valid_patch_source_type(ident, refresh = false, api: nil, cnx: Jamf.cnx) cnx = api if api return :internel if Jamf::PatchInternalSource.valid_id ident, refresh, cnx: cnx return :external if Jamf::PatchExternalSource.valid_id ident, refresh, cnx: cnx nil end # Attributes ##################################### # @return [Boolean] Is this source enabled? attr_reader :enabled alias enabled? enabled # @return [String] The URL from which patch info is retrieved attr_reader :endpoint alias url endpoint # @param newname [String] The new host name (external sources only) # # @return [String] The host name of the patch source attr_reader :host_name alias hostname host_name alias host host_name # @param new_port [Integer] The new port (external sources only) # # @return [Integer] the TCP port of the patch source attr_reader :port # @return [Boolean] Is SSL enabled for the patch source? attr_reader :ssl_enabled alias ssl_enabled? ssl_enabled # Init def initialize(**args) if instance_of?(Jamf::PatchSource) raise Jamf::UnsupportedError, 'PatchSource is an abstract metaclass. Please use PatchInternalSource or PatchExternalSource' end super @enabled = @init_data[:enabled].to_s.jss_to_bool @enabled ||= false # derive the data not provided for this source type if @init_data[:endpoint] @endpoint = @init_data[:endpoint] url = URI.parse endpoint @host_name = url.host @port = url.port @ssl_enabled = url.scheme == HTTPS else @host_name = @init_data[:host_name] @port = @init_data[:port].to_i @port ||= ssl_enabled? ? DFT_SSL_PORT : DFT_NO_SSL_PORT @ssl_enabled = @init_data[:ssl_enabled].to_s.jss_to_bool @ssl_enabled ||= false @endpoint = "#{ssl_enabled ? HTTPS : HTTP}://#{host_name}:#{port}/" end end # init # Get a list of patch titles available from this Patch Source # @see Jamf::PatchSource.available_titles # def available_titles self.class.available_titles id, cnx: cnx end # Get a list of available name_id's for this patch source # @see Jamf::PatchSource.available_name_ids # def available_name_ids self.class.available_name_ids id, cnx: cnx end # Delete this instance # This method is needed to override APIObject#delete def delete case self.class.name when 'Jamf::PatchExternalSource' super when 'Jamf::PatchInternalSource' raise Jamf::UnsupportedError, 'PatchInteralSources cannot be deleted.' end end end # class PatchSource end # module Jamf