# Copyright 2019 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. # # # The module module Jamf # Classes ##################################### # @see_also Jamf::JSONObject # # Jamf::Resource represents a thing directly accessible in the API. It # will contain one or more API endpoints. # # A resource has a base URI path in the API used for # interacting with the resource and directly-related sub-resources. # # For example, the device-reenrollment settings are a resource at the url path # # > .../uapi/v1/reenrollment # # and the related sub-resource for the change history of the reenrollment # settings is at # # > .../uapi/v1/reenrollment/history # # All resources based at .../uapi/v1/reenrollment are encapsulated # in the class {Jamf::ReEnrollmentSettings}, a descendent of Jamf::Resource # # There are two types of resources: Singletons and Collections. # # **Singleton resources** have only one instance available in the API, and they # cannot be create or deleted, only fetched and usually updated, tho some cant # be updated either, e.g. Jamf::AppStoreCountryCodes. The device-reenrollment # settings mentioned above are an example of a Singleton resource. # When the resource is fetched from the API, it is cached, and (usually) future # fetching will return the same instance. See {Jamf::SingletonResource} for # details. # # **Collection resources** have more than one resource within them, and those # can (usually) be created and deleted as well as fetched and updated. # The entire collection (or a part of it) can also be fetched as an Array. # When the whole collection is fetched, the result is cached for future use. # See {Jamf::CollectionResource} for details. # # # Instantiating Resources # # For all subclasses of Jamf::Resource, using the ruby standard .new class # method to instantiate an object will raise an exception. We do this to avoid # the ambiguity of the word 'new' in this context. # # Normally in ruby, .new means 'make a new instance of this class in memory'. # But with Jamf Resoureces, when making a new instance in memory, we might be # making an instance of a resource that already exists in Jamf Pro, or perhaps # making an instance of a 'new' thing that we want to create in Jamf Pro, # but doesn't exist there at the moment. # # While we could look at the parameters passed to decide which of those two # things we're doing, (and require specific parameters for each action), that # doesn't change the fact that a human _reading_ the line: # # a_building = Jamf::Building.new name: 'Main Building' # # sounds like we want to create a new building in the JSS, when in fact we're # just retrieving one that's already there. # # To make the code more readable and totally clear, .new is not allowed for # making instances of Jamf::Resources. Instead, use the class method .fetch # to retrieve existing resources, like so: # # a_building = Jamf::Building.fetch name: 'Main Building' # # This makes it clear what the code is doing, and when you get the error that # there's no building with that name, the error makes sense, which it # wouldn't if you were creating a new building in the JSS. # # Likewise, to make a new one in Jamf Pro, use .create, as in: # # a_building = Jamf::Building.create name: 'Main Building' # # This makes it obvious that we're creating a new building in the JSS # # In both cases, the instance method #save is used to send your changes to the # API. If the resource already exists, the changes will be applied to the # server with #save. If it doesn't yet exist, it will be created by #save. # # # # Subclassing # # # ### Required Constant: RSRC_VERSION # # The version of the resource model supported by ruby-jss for this class. # # Every resource in the Jamf Pro API has a version as part of its URL path. # For example, in the full resource URL: # # https://your.jamf.server:port/uapi/v1/reenrollment # # the resource version is `v1`. At any given time, the API may have many # versions of a resource available - v2 might be released with new values # available or deprecated values removed, but v1 remains and is unchanged. # # Each subclass of Jamf::Resource must define RSRC_VERSION as a # String, e.g. 'v1', which defines the version supported by the subclass. # # As new versions are released by Jamf, when the changes are implemented # in ruby-jss, the RSRC_VERSION is updated. # # ## Required Constant: OBJECT_MODEL # # This is required of all {Jamf::JSONObject} subclasses. Refer to that # documentation for full details about implementing the OBJECT_MODEL constant. # # ## Required Constant: RSRC_PATH # # This is the URI path to the resource, relative to the API base and version # ('uapi/vX/'). # # Examples: # # 1. For SingletonResource class {Jamf::Settings::ReEnrollment}, the URL to # the resource is: # # https://your.jamf.server:port/uapi/v1/reenrollment # # and that URL is used to GET and PUT data, and as a base for the change # log data. # # The constant {Jamf::Settings::ReEnrollment::RSRC_PATH} must be # `'reenrollment'` # # 2. For CollectionResource class {Jamf::MobileDevice}, the URL to the # collection Array is: # # https://your.jamf.server:port/uapi/v1/mobile-devices # # and that URL is used to GET lists of mobileDevice data and POST data # to create a new mobile device. # It is also the base URL for GET, PATCH, PUT and DELETE for individual # mobileDevices, and their details and change log data. # # The constant {Jamf::MobileDevice::RSRC_PATH} must be # `'mobile-devices'` # # ## Required Constant: RSRC_VERSION # # As shown in the examples above, the URL paths for resources have a # version number between 'uapi' and the RSRC_PATH # # Both SingletonResources and CollectionResources must defing the constant # RSRC_VERSION as a string containing that version, e.g. 'v1' # # ruby-jss doesn't support the older resource paths that don't have a version # in their path. # # @abstract # class Resource < Jamf::JSONObject extend Jamf::Abstract # Constants ##################################### # These methods are allowed to call .new NEW_CALLERS = ['fetch', 'create', 'all', 'block in all'].freeze # The resource version for previewing new features RSRC_PREVIEW_VERSION = 'preview'.freeze # Public Class Methods ##################################### # the resource path for this resource # @return [String] def self.rsrc_path "#{self::RSRC_VERSION}/#{self::RSRC_PATH}" end def self.preview_path "#{RSRC_PREVIEW_VERSION}/#{self::RSRC_PATH}" end # Disallow direct use of ruby's .new class method for creating instances. # Require use of .fetch or .create. # def self.new(data, cnx: Jamf.cnx) calling_method = caller_locations(1..1).first.label raise Jamf::UnsupportedError, "Use .fetch or .create to instantiate Jamf::Resource's" unless NEW_CALLERS.include? calling_method super end # Attributes ##################################### # @return [String] the resouce path for this object attr_reader :rsrc_path # Instance Methods ##################################### # TODO: error handling def save raise Jamf::UnsupportedError, "#{self.class} objects cannot be changed" unless self.class.mutable? return unless unsaved_changes? exist? ? update_in_jamf : create_in_jamf clear_unsaved_changes @id || :saved end # Private Instance Methods ##################################### private # TODO: handle PATCH when it becomes a thing def update_in_jamf @cnx.put(rsrc_path, to_jamf) rescue Jamf::Connection::APIError => e if e.status == 409 && self.class.included_modules.include?(Jamf::Lockable) raise Jamf::VersionLockError, "The #{self.class} has been modified since it was fetched. Please refetch and try again." end raise e end end # class APIObject end # module JAMF