lib/jss/api_object/self_servable.rb in ruby-jss-0.7.0 vs lib/jss/api_object/self_servable.rb in ruby-jss-0.8.1

- old
+ new

@@ -21,247 +21,271 @@ ### KIND, either express or implied. See the Apache License for the specific ### language governing permissions and limitations under the Apache License. ### ### +require 'jss/api_object/self_servable/icon' + ### module JSS + # Module Variables ##################################### - ### Module Variables - ##################################### + # Module Methods ##################################### - ### Module Methods - ##################################### + # Sub-Modules ##################################### - ### Sub-Modules - ##################################### - ### A mix-in module for handling Self Service data for objects in the JSS. - ### - ### The JSS objects that have Self Service data return it in a :self_service subset, - ### which have somewhat similar data, i.e. a hash with at least these keys: - ### - :self_service_description - ### - :self_service_icon - ### - :feature_on_main_page - ### - :self_service_categories - ### - ### Config Profiles in self service have this key: - ### - :security - ### - ### Additionally, items that apper in macOS Slf Svc have these keys: - ### - :install_button_text - ### - :force_users_to_view_description - ### - ### See the attribute definitions for details of these values and structures. - ### - ### Including this module in an {APIObject} subclass and calling {#parse_self_service} in the - ### subclass's constructor will give it matching attributes with 'self_service_' - ### appended if needed, e.g. {#self_service_feature_on_main_page} - ### - ### - ### Classes including this module *must*: - ### - ### - Define the constant SELF_SERVICE_TARGET which contains either :macos or :ios - ### - Define the constant SELF_SERVICE_PAYLOAD which contains one of :policy, :profile, - ### :app, or :ebook - ### - Call {#parse_self_service} in the subclass's constructor, after calling super - ### - Define the method #in_self_service? which returns a Boolean indicating that the item is - ### available in self service. Different API objects indicate this in different ways (and - ### macOS app store apps don't provide it at all via the API, - ### so self service can't be implemented fully) - ### - Define the methods #add_to_self_service and #remove_from_self_service, which is handled - ### differently for policies, apps/ebooks, and profiles - ### - Define the method #user_removable? which returns Boolean indicating that the item - ### can be removed by the user in SSvc. macOS profiles store this in the :user_removable - ### key of the :general subset as a boolean, whereas iOS profiles store it in - ### the :security hash of the :self_service data as one of 3 strings - ### - Define the method #user_removable= which sets the appropriate values depending - ### on the class. (see above) - ### - Define a #set_icon_to method which takes the id of a previously uploaded - ### icon, or a local path (String or Pathname) to an image file to use. - ### - Include the result of {#self_service_xml} in their #rest_xml output - ### - ### - ### Generating XML for PUT or POST: - ### - ### If the class including this module is creatable or updatable, calling {#self_service_xml} - ### returns a REXML element representing the Self Service subset, to be included with the - ### #rest_xml output of the subclass. - ### - ### Because some self-service-related data is located outside of the Self Service - ### data subset (e.g. the deployment method for profiles is in the general subset), - ### classes including this module *must* also account for that and add an appropriate - ### XML element as needed. - ### + # A mix-in module for handling Self Service data for objects in the JSS. + # + # The JSS objects that have Self Service data return it in a :self_service subset, + # which have somewhat similar data, i.e. a hash with at least these keys: + # - :self_service_description + # - :self_service_icon + # - :feature_on_main_page + # - :self_service_categories + # + # Config Profiles in self service have this key: + # - :security + # + # Additionally, items that apper in macOS Slf Svc have these keys: + # - :install_button_text + # - :force_users_to_view_description + # + # See the attribute definitions for details of these values and structures. + # + # Including this module in an {APIObject} subclass will give it matching + # attributes with 'self_service_' appended if needed, + # e.g. {#self_service_feature_on_main_page} + # + # + # Classes including this module *MUST*: + # - call {#add_self_service_xml()} in their #rest_xml method + # + # IMPORTANT: Since SelfServable also includes #{JSS::Updatable}, for uploading icons, + # see that module for its requirements. + # + # module SelfServable - ### Constants + include Uploadable + + # Constants ##################################### SELF_SERVABLE = true - IOS_PROFILE_REMOVAL_OPTIONS = ['Always', 'With Authorization', 'Never'].freeze + PROFILE_REMOVAL_BY_USER = { + always: 'Always', + never: 'Never', + with_auth: 'With Authorization' + }.freeze - ### Variables + MAKE_AVAILABLE = 'Make Available in Self Service'.freeze + AUTO_INSTALL = 'Install Automatically'.freeze + AUTO_INSTALL_OR_PROMPT = 'Install Automatically/Prompt Users to Install'.freeze + + DEFAULT_INSTALL_BUTTON_TEXT = 'Install'.freeze + + SELF_SERVICE_CLASSES = { + JSS::Policy => { + in_self_service_data_path: [:self_service, :use_for_self_service], + in_self_service: true, + not_in_self_service: false, + targets: [:macos], + payload: :policy, + can_display_in_categories: true, + can_feature_in_categories: true + }, + JSS::MacApplication => { # TODO: add the correct values when Jamf fixes this bug + in_self_service_data_path: nil, # [:general, :distribution_method], + in_self_service: nil, # MAKE_AVAILABLE, + not_in_self_service: nil, # AUTO_INSTALL_OR_PROMPT, + targets: [:macos], + payload: :app, + can_display_in_categories: true, + can_feature_in_categories: true + }, + JSS::OSXConfigurationProfile => { + in_self_service_data_path: [:general, :distribution_method], + in_self_service: MAKE_AVAILABLE, + not_in_self_service: AUTO_INSTALL, + targets: [:macos], + payload: :profile, + can_display_in_categories: true, + can_feature_in_categories: true + }, + JSS::EBook => { + in_self_service_data_path: [:general, :deployment_type], + in_self_service: MAKE_AVAILABLE, + not_in_self_service: AUTO_INSTALL_OR_PROMPT, + targets: [:macos, :ios], + payload: :app, # ebooks are handled the same way as apps, it seems, + can_display_in_categories: true, + can_feature_in_categories: true + }, + JSS::MobileDeviceApplication => { + in_self_service_data_path: [:general, :deployment_type], + in_self_service: MAKE_AVAILABLE, + not_in_self_service: AUTO_INSTALL_OR_PROMPT, + targets: [:ios], + payload: :app, + can_display_in_categories: true, + can_feature_in_categories: false + }, + JSS::MobileDeviceConfigurationProfile => { + in_self_service_data_path: [:general, :deployment_method], + in_self_service: MAKE_AVAILABLE, + not_in_self_service: AUTO_INSTALL, + targets: [:ios], + payload: :profile, + can_display_in_categories: false, + can_feature_in_categories: false + } + }.freeze + + # Variables ##################################### - ### Attribtues + # Mixed-in Attributes ##################################### - ### @return [String] The verbage that appears in SelfSvc for this item + # @return [Boolean] Is this thing available in Self Service? + attr_reader :in_self_service + alias in_self_service? in_self_service + + # @return [JSS::Icon, nil] The icon used in self-service + attr_reader :icon + alias self_service_icon icon + + # @return [String] The verbage that appears in SelfSvc for this item attr_reader :self_service_description - ### @return [Boolean] Should this item feature on the main page of SSvc? + # @return [Boolean] Should this item feature on the main page of SSvc? + # Only applicable to macOS targets attr_reader :self_service_feature_on_main_page - ### @return [Array<Hash>] The categories in which this item should appear in SSvc - ### - ### Each Hash has these keys about the category - ### - :id => [Integer] the JSS id of the category - ### - :name => [String] the name of the category - ### - ### Most objects also include one or both of these keys: - ### - :display_in => [Boolean] should the item be displayed in this category in SSvc? (OSX SSvc only) - ### - :feature_in => [Boolean] should the item be featured in this category in SSVC? (OSX SSvc only) - ### - ### NOTE: as of Casper 9.61 there's a bug in the JSON output from the API, and only the last - ### category is returned, if more than one are set. - ### + # @return [Array<Hash>] The categories in which this item should appear in SSvc + # + # Each Hash has these keys about the category + # - :id => [Integer] the JSS id of the category + # - :name => [String] the name of the category + # + # Most objects also include one or both of these keys: + # - :display_in => [Boolean] should the item be displayed in this category in SSvc? (not MobDevConfProfiles) + # - :feature_in => [Boolean] should the item be featured in this category in SSVC? (macOS targets only) + # attr_reader :self_service_categories - ### @return [Hash] The icon that appears in SelfSvc for this item - ### - ### The Hash contains these keys with info about the icon: - ### - :uri => [String] the URI for retriving the icon - ### - :id => [Integer] the JSS id number for the icon (not all SSvc items have this) - ### - :data => [String] the icon image encoded as Base64 (not all SSvc items have this) - ### - :filename => [String] The name of the image file uploaded to the JSS, if applicable - ### - attr_reader :self_service_icon + # Profiles in Self Service have an option to allow the user to remove them + # and for iOS profiles, if authentication is required to do so, and if so, + # the password needed for removal. + # + # This data is held in the :security Hash of the selfsvc data. + # The keys are: + # - :removal_disallowed, which should be "removal allowed" + # - :password => [String] if :removal_disallowed is "With Authorization", + # this contains the passwd (in plaintext) needed to remove the profile. + # + # NOTE that the key should be called :removal_allowed, since 'Never' means it can't be removed. + # + # These values are stored in the next two attributes. - ### @return [Hash] The security settings for profiles in SSvc - ### - ### The keys are - ### - :removal_disallowed => [String] one of the items in PROFILE_REMOVAL_OPTIONS - ### - :password => [String] if :removal_disallowed is "With Authorization", this contains the passwd (in plaintext) - ### needed to remove the profile. - ### - ### NOTE that the key should be called :removal_allowed, since 'Never' means it can't be removed. - ### + # @return [Symbol] one of the keys in PROFILE_REMOVAL_BY_USER attr_reader :self_service_user_removable - ### @return [String] The text label on the install button in SSvc (OSX SSvc only) + # @return [String] The password needed for removal, in plain text. + attr_reader :self_service_removal_password + + # @return [String] The text label on the install button in SSvc (OSX SSvc only) attr_reader :self_service_install_button_text - ### @return [Boolean] Should an extra window appear before the user can install the item? (OSX SSvc only) + # @return [Boolean] Should an extra window appear before the user can install the item? (OSX SSvc only) attr_reader :self_service_force_users_to_view_description + # Mixed-in Public Instance Methods ##################################### - ### Mixed-in Instance Methods - ##################################### - ### Call this during initialization of - ### objects that have a self_service subset - ### and the self_service attributes will be populated - ### (as primary attributes) from @init_data - ### - ### @return [void] - ### - def parse_self_service - @init_data[:self_service] ||= {} - @ss_data = @init_data[:self_service] + # Setters + # - @self_service_description = @ss_data[:self_service_description] - @self_service_icon = @ss_data[:self_service_icon] - @self_service_icon ||= {} - - @self_service_feature_on_main_page = @ss_data[:feature_on_main_page] - @self_service_feature_on_main_page ||= false - - @self_service_categories = @ss_data[:self_service_categories] - @self_service_categories ||= [] - - # make this an empty hash if needed - @self_service_security = @ss_data[:security] - @self_service_security ||= {} - - @self_service_install_button_text = @ss_data[:install_button_text] - @self_service_install_button_text ||= 'Install' - - @self_service_force_users_to_view_description = @ss_data[:force_users_to_view_description] - @self_service_force_users_to_view_description ||= false - end # parse - - ### Setters - ### - - ### @param new_val[String] the new discription - ### - ### @return [void] - ### + # @param new_val[String] the new discription + # + # @return [void] + # def self_service_description=(new_val) new_val.strip! return if @self_service_description == new_val @self_service_description = new_val @need_to_update = true end - ### @param new_val[String] the new install button text - ### - ### @return [void] - ### + # @param new_val[String] the new install button text + # + # @return [void] + # def self_service_install_button_text=(new_val) return nil if @self_service_install_button_text == new_val - raise JSS::InvalidDataError, 'Only OS X Self Service Items can have custom button text' unless self.class::SELF_SERVICE_TARGET == :macos + raise JSS::InvalidDataError, 'Only OS X Self Service Items can have custom button text' unless self_service_targets.include? :macos @self_service_install_button_text = new_val.strip @need_to_update = true end - ### @param new_val[Boolean] should this appear on the main SelfSvc page? - ### - ### @return [void] - ### + # @param new_val[Boolean] should this appear on the main SelfSvc page? + # + # @return [void] + # def self_service_feature_on_main_page=(new_val) return nil if @self_service_feature_on_main_page == new_val - raise JSS::InvalidDataError, 'New value must be true or false' unless JSS::TRUE_FALSE.include? new_val + return nil unless @self_service_data_config[:can_feature_in_categories] + raise JSS::InvalidDataError, 'New value must be true or false' unless new_val.jss_boolean? @self_service_feature_on_main_page = new_val @need_to_update = true end - ### @param new_val[Boolean] should this appear on the main SelfSvc page? - ### - ### @return [void] - ### + # @param new_val[Boolean] Should the description be shown to users in a new + # window before executing the payload? + # + # @return [void] + # def self_service_force_users_to_view_description=(new_val) return nil if @self_service_force_users_to_view_description == new_val - raise JSS::InvalidDataError, 'Only OS X Self Service Items can force users to view description' unless self.class::SELF_SERVICE_TARGET == :macos - raise JSS::InvalidDataError, 'New value must be true or false' unless JSS::TRUE_FALSE.include? new_val + raise JSS::InvalidDataError, 'Only OS X Self Service Items can force users to view description' unless self_service_targets.include? :macos + raise JSS::InvalidDataError, 'New value must be true or false' unless new_val.jss_boolean? @self_service_force_users_to_view_description = new_val @need_to_update = true end - ### Add or change one of the categories for this item in SSvc. - ### - ### @param new_cat[String] the name of a category for this item in SelfSvc - ### - ### @param display_in[Boolean] should this item appear in the SelfSvc page for the new category? - ### - ### @param feature_in[Boolean] should this item be featured in the SelfSvc page for the new category? - ### - ### @return [void] - ### + # Add or change one of the categories for this item in self service + # + # @param new_cat[String, Integer] the name or id of a category where this + # object should appear in SelfSvc + # + # @param display_in[Boolean] should this item appear in the SelfSvc page for + # the category? Only meaningful in applicable classes + # + # @param feature_in[Boolean] should this item be featured in the SelfSvc page + # for the category? Only meaningful in applicable classes. + # NOTE: this will always be false if display_in is false. + # + # @return [void] + # def add_self_service_category(new_cat, display_in: true, feature_in: false) - new_cat.strip! + new_cat = JSS::Category.map_all_ids_to(:name)[new_cat] if new_cat.is_a? Integer + feature_in = false if display_in == false raise JSS::NoSuchItemError, "No category '#{new_cat}' in the JSS" unless JSS::Category.all_names(:refresh).include? new_cat - raise JSS::InvalidDataError, 'display_in must be true or false' unless JSS::TRUE_FALSE.include? display_in - raise JSS::InvalidDataError, 'feature_in must be true or false' unless JSS::TRUE_FALSE.include? feature_in - new_data = { name: new_cat, display_in: display_in, feature_in: feature_in } + raise JSS::InvalidDataError, 'display_in must be true or false' unless display_in.jss_boolean? + raise JSS::InvalidDataError, 'feature_in must be true or false' unless feature_in.jss_boolean? + + new_data = { name: new_cat } + new_data[:display_in] = display_in if @self_service_data_config[:can_display_in_categories] + new_data[:feature_in] = feature_in if @self_service_data_config[:can_feature_in_categories] + # see if this category is already among our categories. idx = @self_service_categories.index { |c| c[:name] == new_cat } if idx @self_service_categories[idx] = new_data @@ -269,60 +293,250 @@ @self_service_categories << new_data end @need_to_update = true end + alias set_self_service_category add_self_service_category - ### Remove a category from those for this item in SSvc - ### - ### @param cat [String] the name of the category to remove - ### - ### @return [void] - ### + # Remove a category from those for this item in SSvc + # + # @param cat [String, Integer] the name or id of the category to remove + # + # @return [void] + # def remove_self_service_category(cat) - @self_service_categories.reject! { |c| c[:name] == cat } + @self_service_categories.reject! { |c| c[:name] == cat || c[:id] == cat } @need_to_update = true end - ### @api private - ### - ### Return a REXML <location> element to be - ### included in the rest_xml of - ### objects that have a Location subset - ### - ### @return [REXML::Element] - ### - def self_service_xml - ssvc = REXML::Element.new('self_service') + # Set the value for user-removability of profiles, optionally + # providing a password for removal, on iOS targets. + # + # @param new_val[Symbol] One of the keys of PROFILE_REMOVAL_BY_USER, + # :always, :never, or :with_auth + # + # @param pw[String] A new password to use if removable :with_auth + # + # @return [void] + # + def self_service_user_removable=(new_val, pw = @self_service_removal_password) + return nil if new_val == @self_service_user_removable && pw == @self_service_removal_password + validate_user_removable new_val + @self_service_user_removable = new_val + @self_service_removal_password = pw + @need_to_update = true + end + + # Set a new Self Service icon for this object. + # + # Since JSS::Icon objects are read-only, + # the icon can only be changed by supplying the id number + # of an icon already existing in the JSS, or a path to + # a local file, which will be uploaded to the JSS and added + # to this instance. Uploads + # take effect immediately, but if an integer is supplied, the change + # must be sent to the JSS via {#update} or {#create} + # + # @param new_icon[Integer, String, Pathname] The id or path to the new icon. + # + # @return [false, Integer, Pathname] false means no change was made. + # + def icon=(new_icon) + if new_icon.is_a? Integer + return if new_icon == @icon.id + validate_icon new_icon + @new_icon_id = new_icon + @need_to_update = true + else + unless uploadable? && self.class::UPLOAD_TYPES.keys.include?(:icon) + raise JSS::UnsupportedError, "Class #{self.class} does not support icon uploads." + end + new_icon = Pathname.new new_icon + upload(:icon, new_icon) + refresh_icon + end # new_icon.is_a? Integer + new_icon + end # icon = + alias self_service_icon= icon= + alias assign_icon icon= + + # Add this object to self service if not already there. + # + # @return [void] + # + def add_to_self_service + return nil unless @self_service_data_config[:in_self_service_data_path] + return nil if in_self_service? + @in_self_service = true + @need_to_update = true + end + + # Remove this object from self service if it's there. + # + # @return [void] + # + def remove_from_self_service + return nil unless @self_service_data_config[:in_self_service_data_path] + return nil unless in_self_service? + @in_self_service = false + @need_to_update = true + end + + # Can this thing be removed by the user? + # + # @return [Boolean, nil] nil means 'not applicable' + # + def user_removable? + return nil unless self_service_payload == :profile + @self_service_user_removable != :never + end + + # What devices types can get this thing in Self Service + # + # @return [Array<Symbol>] An array of :macos, :ios, or both. + # + def self_service_targets + @self_service_data_config[:targets] + end + + # What does this object deploy to the device + # via self service? + # + # @return [Symbol] :profile, :app, or :policy + # + def self_service_payload + @self_service_data_config[:payload] + end + + # Mixed-in Private Instance Methods + ##################################### + private + + # Call this during initialization of + # objects that have a self_service subset + # and the self_service attributes will be populated + # (as primary attributes) from @init_data + # + # @return [void] + # + def parse_self_service + ss_data = @init_data[:self_service] + ss_data ||= {} + @self_service_data_config = SELF_SERVICE_CLASSES[self.class] + + @in_self_service = in_self_service_at_init? + + if ss_data[:security] + removable_value = ss_data[:security][:removal_disallowed] + @self_service_user_removable = PROFILE_REMOVAL_BY_USER.invert[removable_value] + @self_service_removal_password = ss_data[:security][:password] + end + + @self_service_description = ss_data[:self_service_description] + + @icon = JSS::Icon.new(ss_data[:self_service_icon]) if ss_data[:self_service_icon] + + @self_service_feature_on_main_page = ss_data[:feature_on_main_page] + @self_service_feature_on_main_page ||= false + + @self_service_categories = ss_data[:self_service_categories] + @self_service_categories ||= [] + + @self_service_install_button_text = ss_data[:install_button_text] + @self_service_install_button_text ||= DEFAULT_INSTALL_BUTTON_TEXT + + @self_service_force_users_to_view_description = ss_data[:force_users_to_view_description] + @self_service_force_users_to_view_description ||= false + end # parse + + # Figure out if this object is in Self Service, from the API + # initialization data. + # Alas, how to do it is far from consistent + # + # @return [Boolean] + # + def in_self_service_at_init? + return nil unless @self_service_data_config[:in_self_service_data_path] + subsection, key = @self_service_data_config[:in_self_service_data_path] + @init_data[subsection][key] == @self_service_data_config[:in_self_service] + end + + def validate_user_removable=(new_val) + raise JSS::UnsupportedError, 'User removal settings not applicable to this class' unless self_service_payload == :profile + + raise JSS::UnsupportedError, 'Removal :with_auth not applicable to this class' if new_val == :with_auth && !self_service_targets.include?(:ios) + + raise JSS::InvalidDataError, "Value must be one of: :#{PROFILE_REMOVAL_BY_USER.keys.join(', :')}" unless PROFILE_REMOVAL_BY_USER.keys.include? new_val + end + + def validate_icon(id) + if JSS::DB_CNX.connected? + raise JSS::NoSuchItemError, "No icon with id #{new_icon}" unless JSS::Icon.all_ids.include? id + end + end + # Re-read the icon data for this object from the API + # Generally done after uploading a new icon via {#icon=} + # + # @return [void] + # + def refresh_icon + return nil unless @in_jss + fresh_data = JSS::API.get_rsrc(@rest_rsrc)[self.class::RSRC_OBJECT_KEY] + icon_data = fresh_data[:self_service][:self_service_icon] + @icon = JSS::Icon.new icon_data + end # refresh icon + + # Add a REXML <self_service> element to the root of the provided REXML::Document + # + # @param xdoc[REXML::Document] The XML Document to which we're adding a Self + # Service subsection + # + # @return [void] + # + def add_self_service_xml(xdoc) + doc_root = xdoc.root + ssvc = doc_root.add_element 'self_service' + ssvc.add_element('self_service_description').text = @self_service_description ssvc.add_element('feature_on_main_page').text = @self_service_feature_on_main_page + if @new_icon_id + icon = ssvc.add_element('self_service_icon') + icon.add_element('id').text = @new_icon_id + end + cats = ssvc.add_element('self_service_categories') @self_service_categories.each do |cat| catelem = cats.add_element('category') catelem.add_element('name').text = cat[:name] - catelem.add_element('display_in').text = cat[:display_in] if cat.keys.include? :display_in - catelem.add_element('feature_in').text = cat[:feature_in] if cat.keys.include? :feature_in + catelem.add_element('display_in').text = cat[:display_in] if @self_service_data_config[:can_display_in_categories] + catelem.add_element('feature_in').text = cat[:feature_in] if @self_service_data_config[:can_feature_in_categories] end - icon = ssvc.add_element('self_service_icon') - @self_service_icon.each { |key, val| icon.add_element(key.to_s).text = val } + if self_service_targets.include? :macos + ssvc.add_element('install_button_text').text = @self_service_install_button_text + ssvc.add_element('force_users_to_view_description').text = @self_service_force_users_to_view_description + end - unless @self_service_security.empty? + if self_service_payload == :profile sec = ssvc.add_element('security') - sec.add_element('removal_disallowed').text = @self_service_security[:removal_disallowed] if @self_service_security[:removal_disallowed] - sec.add_element('password').text = @self_service_security[:password] if @self_service_security[:password] + sec.add_element('removal_disallowed').text = PROFILE_REMOVAL_BY_USER[@self_service_user_removable] + sec.add_element('password').text = @self_service_removal_password if @self_service_removal_password end - ssvc.add_element('install_button_text').text = @self_service_install_button_text if @self_service_install_button_text - ssvc.add_element('force_users_to_view_description').text = @self_service_force_users_to_view_description \ - unless @self_service_force_users_to_view_description.nil? + return unless @self_service_data_config[:in_self_service_data_path] - ssvc - end + in_ss_section, in_ss_elem = @self_service_data_config[:in_self_service_data_path] - ### aliases + in_ss_value = @in_self_service ? @self_service_data_config[:in_self_service] : @self_service_data_config[:not_in_self_service] + + in_ss_section_xml = doc_root.elements[in_ss_section.to_s] + in_ss_section_xml ||= doc_root.add_element(in_ss_section.to_s) + in_ss_section_xml.add_element(in_ss_elem.to_s).text = in_ss_value.to_s + end # add_self_service_xml + + # aliases alias change_self_service_category add_self_service_category end # module SelfServable end # module JSS