class Jamf::Scopable::Scope

This class represents a Scope in the JSS, as can be applied to Scopable objects like Policies, Profiles, etc. Instances of this class are generally used as the value of the @scope attribute of those objects.

Scope data comes from the API as a hash within the overall object data. The main keys of the hash define the included targets of the scope. A sub-hash defines limitations on those targets, and another sub-hash defines explicit exclusions.

This class provides methods for adding, removing, or fully replacing the various items in scope’s realms: targets, limitations, and exclusions.

This class also provides a way to see if a machine will be included in this scope.

Discussion: Users & User Groups in Scopes:

The Classic API has bugs, as well as non-obvious/historical oddness, regarding the use of Users, UserGroups, Directory Service/Local Users, and Directory Service User Groups in scopes. Here’s a discussion of those issues, and how ruby-jss handles them.

Historical Oddness

Because the concept of ‘scope’ existed before Jamf Pro had ‘Users’ and ‘User Groups’ (Jamf::User and Jamf::UserGroup classes in ruby-jss) there is non-obvious inconsistency between the labels for API data, and the labels for that data in the web UI:

Users

What appears in the UI as ‘Users’ are User objects in Jamf pro, which in ruby-jss are Jamf::User instances.

These will appear in the API data as <jss_users> element with <user> sub-elements (XML) or the ‘jss_users’ array (JSON). These are available as Targets or Exclusions.

In this class, they are also referred to as ‘jss_users’

Directory Service/Local Users

When editing a scope in the UI, in Limitations and Exclusions, you can add arbitrary strings that will be matched to the users assigned to machines, or that appear in any of the defined LDAP servers. These scope items are called ‘Directory Service/Local Users’ but used to be called ‘LDAP/Local Users’

In the API data for scopes, these items appear in the <users> element with <user> sub-elements (XML) or ‘users’ array (JSON) of the limitations and exclusions data

In this class, these items ultimately use the same names they have in the API data: ‘users’ but when specifying that you are setting that value, you can use any of these synonyms, plural or singular:

ldap_users, jamf_ldap_users, directory_service_local_users

User Groups

What appears in the UI as ‘User Groups’ are User Group objects in Jamf Pro, both static and smart. In ruby-jss, these are Jamf::UserGroup instances.

They will appear in the API data as <jss_user_groups> element with <user_group> sub-elements (XML) or the ‘jss_user_groups’ array (JSON). These are available as Targets or Exclusions.

In this class they are also referred to as ‘jss_user_groups’

Directory Service User Groups

When editing a scope in the UI, in Limitations and Exclusions, you can look up and add groups from any of the defined LDAP servers. These scope items are called ‘Directory Service User Groups’ but used to be called ‘LDAP User Groups’

In the API data for scopes, these items appear in the <user_groups> element with <user_group> sub-elements (XML) or ‘user_groups’ array (JSON) of the limitations and exclusions data

In this class, these items ultimately use the same names they have in the API data: ‘user_groups’ but when specifying that you are setting that value, you can use any of these synonyms, singular or plural:

ldap_user_groups, directory_service_user_groups

IMPORTANT: API BUG IN POLICY AND PATCH POLICY SCOPES - CAN CAUSE DATA LOSS

When you GET the data for policies and patch policies from the Classic API the scope data returned will NOT include the ‘jss_users’ and ‘jss_user_groups’ data in the targets or the exclusions, even if they are defined in the web UI.

More importantly, if you try to include those in the XML when you PUT a policy back to make a change via the API, you’ll get an error because the API endpoint doesn’t know what <jss_users> or <jss_user_groups> elements are.

Even more importanly, since you cannot include those elements in your PUT body, if they actually exist in the scope, THEY WILL BE ERASED from the actual scope, because they weren’t in the PUT data. This will always happen if you include the <scope> element in your PUT data, even if you didn’t change the scope.

Fortunately the Classic API, or at least this part of it, doesn’t fully adhere to the REST standards for PUT, and if you don’t include the <scope> element in the XML, the server will just ignore the scope entirely, and nothing will change.

We make use of that here to allow for editing Policies without fear of erasing those parts of the scope. As long as you don’t change anything about the scope, there will be no <scope> element in the XML sent with a PUT, and the scope is safe from harm.

If you DO change the scope of a policy, this bug cannot be avoided, and you’ll delete any “User”/jss_user and “User Groups/jss_user_groups” defined in the targets or exclusions.

By default, if you try to change the scope of a Policy of PatchPolicy, you’ll get a warning about the possibility of losing data when you save.

You can supress those warnings either by supressing all ruby warnings, or by calling Jamf::Scopable::Scope.do_not_warn_about_policy_scope_bugs

IMPORTANT: API BUG IN OSX CONFIG PROFILE SCOPES - CAN CAUSE DATA LOSS

When fetching the data for OSX Configuration Profiles using JSON (which ruby-jss does) and the scope of the profile contains more than one ‘jss_user_groups` as a target, then only the last one will be returned. If you have more than one such group as a target, and use ruby-jss to make changes to the scope, all but the last jss_user_groups used as targets will be removed.

This only appears to affect scope targets, not exclusions, and only for OSX Config Profiles. Other scopable objects that use jss_user_groups in their API data seem to be OK.

This is due to a long-standing API bug regarding how Arrays in XML are incorrectly translated into Hashes of a single Hash when returning the data as JSON - they shoud be Arrays of Hashes in JSON - one hash for each item.

Even though this bug was first reported to jamf in 2009, it still appears in many places throughout the Classic API. ruby-jss works around some of the worst instances of the bug, but such workarounds are complex requiring re-fetching the data in XML and parsing it manually. At the moment there are no plans to do that for this specific scope bug.

By default, if you try to change the scope of an object affected by this bug, you’ll get a warning about the possibility of losing data when you save.

You can supress those warnings either by supressing all ruby warnings, or by calling Jamf::Scopable::Scope.do_not_warn_about_array_hash_scope_bugs

@see Jamf::Scopable

Constants

DEFAULT_SCOPE

Here’s a default scope as it might come from the API.

ESS

added to the ends of singular key names if needed, e.g. computer_group => computer_groups

EXCLUSIONS

any of them can be excluded

INCLUSIONS

Backward Compatibility

JAMF_DATA_LOSS_BUG_CLASSES

These classes are affected by the jss_users/jss_user_groups bug.

They do not accept jss_users or jss_user_groups in their targets or exclusions, and editing their scope via the API will always delete those items from the scope if they exist.

See discussion in the Scope class comments.

JAMF_DATA_LOSS_BUG_KEYS

The classes affected by the jss_users/jss_user_groups bug do not include these items in their Target or Exclusion API data, even if the scope has such items defined in the JSS

See discussion in the Scope class comments.

LDAP_BASED_KEYS

In the API data for limitations and exclusions ‘users’ is what appears as Directory Service/Local Users in the web UI and ‘user_groups’ appears as ‘Directory Service User Groups’.

Contrasted with ‘jss_users’ and ‘jss_user_groups’ in the API data for targets and exlcusions, which are Jamf::User and Jamf::UserGroup objects.

LDAP_GROUP_KEYS

These keys always mean :user_groups

LDAP_JAMF_USER_KEYS

These keys always mean :users

LIMITATIONS

These can limit the inclusion list These are the keys that come from the API the :users key from the API is what we call :jamf_ldap_users and the :user_groups key from the API we call :ldap_user_groups See the IMPORTANT discussion above.

SCOPING_CLASSES

These are the classes that Scopes can use for defining a scope, keyed by appropriate symbols.

synonyms, including singular/plural forms, are used to allow for more natural language when specifying these scope entities. The key used in the actual API data is usually the plural.

NOTE: user and user_group in Scope data refer to ‘Directory Service/Local User’ and ‘Directory Service User Group’ as labeled in the web-ui. These were formerly labeled as ‘LDAP/Local User’ and ‘LDAP User Group’.

TARGETS

These can be part of the base target list of the scope, along with the appropriate target and target group keys

TARGETS_AND_GROUPS

This hash maps the availble Scope Target keys from SCOPING_CLASSES to their corresponding target group keys from SCOPING_CLASSES.

Attributes

all_targets[R]

@return [Boolean]

Does this scope cover all targets?

If this is true, the @targets Hash is ignored, and all targets in the JSS form the base scope.

all_targets?[R]

@return [Boolean]

Does this scope cover all targets?

If this is true, the @targets Hash is ignored, and all targets in the JSS form the base scope.

container[RW]

@return [Jamf::APIObject subclass]

A reference to the object that contains this Scope

For telling it when a change is made and an update needed and for accessing its api connection

exclusions[R]

The items in these arrays are the exclusions applied to targets in the @targets .

The arrays of ids are:

  • :computers or :mobile_devices (which are directly excluded)

  • :direct_exclusions - a synonym for :mobile_devices or :computers

  • :computer_groups or :mobile_device_groups (which exclude all of their memebers)

  • :group_exclusions - a synonym for :computer_groups or :mobile_device_groups

  • :departments

  • :buildings

  • :network_segments

  • :jss_users

  • :jss_user_groups

  • :users

  • :user_groups #

@return [Hash{Symbol: Array<Integer, String>}]

group_class[R]

what type of target group is this scope for? ComputerGroups or MobileDeviceGroups?

inclusions[R]

The items which form the base scope of included targets

This is the group of targets to which the limitations and exclusions apply. they keys are:

  • :computers or :mobile_devices (which are directly targeted)

  • :direct_targets - a synonym for :mobile_devices or :computers

  • :computer_groups or :mobile_device_groups (which target all of their memebers)

  • :group_targets - a synonym for :computer_groups or :mobile_device_groups

  • :departments

  • :buildings

  • :jss_users

  • :jss_user_groups

and the values are Arrays of names of those things.

@return [Hash{Symbol: Array<Integer>}]

limitations[R]

The items in these arrays are the limitations applied to targets in the @targets .

The arrays of ids are:

  • :network_segments

  • :users

  • :user_groups

  • :ibeacons

@return [Hash{Symbol: Array<Integer, String>}]

should_update[RW]

@return [Boolean] Have changes been made to the scope, that need

to be sent to the server?
should_update?[RW]

@return [Boolean] Have changes been made to the scope, that need

to be sent to the server?
target_class[R]

what type of target is this scope for? Computers or MobileDevices?

targets[R]

The items which form the base scope of included targets

This is the group of targets to which the limitations and exclusions apply. they keys are:

  • :computers or :mobile_devices (which are directly targeted)

  • :direct_targets - a synonym for :mobile_devices or :computers

  • :computer_groups or :mobile_device_groups (which target all of their memebers)

  • :group_targets - a synonym for :computer_groups or :mobile_device_groups

  • :departments

  • :buildings

  • :jss_users

  • :jss_user_groups

and the values are Arrays of names of those things.

@return [Hash{Symbol: Array<Integer>}]

unable_to_verify_ldap_entries[RW]

@return [Boolean] should we expect a potential 409 Conflict

if we can't connect to LDAP servers for verification?

Public Class Methods

do_not_warn_about_array_hash_scope_bugs() click to toggle source

call this to suppress warnings about data loss bug in OSXConfigurationProfile scopes when there are jss_user_groups used as targets

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
356 def self.do_not_warn_about_array_hash_scope_bugs
357   @do_not_warn_about_array_hash_scope_bugs = true
358 end
do_not_warn_about_array_hash_scope_bugs?() click to toggle source

Has do_not_warn_about_policy_scope_bugs been set?

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
361 def self.do_not_warn_about_array_hash_scope_bugs?
362   @do_not_warn_about_array_hash_scope_bugs
363 end
do_not_warn_about_policy_scope_bugs() click to toggle source

call this to suppress warnings about data loss bug in Policy and Patch Policy scopes

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
344 def self.do_not_warn_about_policy_scope_bugs
345   @do_not_warn_about_policy_scope_bugs = true
346 end
do_not_warn_about_policy_scope_bugs?() click to toggle source

Has do_not_warn_about_policy_scope_bugs been set?

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
349 def self.do_not_warn_about_policy_scope_bugs?
350   @do_not_warn_about_policy_scope_bugs
351 end
new(target_key, raw_scope = nil, container: nil) click to toggle source

If raw_scope is empty, a default scope, scoped to no targets, is created, and can be modified as needed.

@param target_key the kind of thing we’re scoping, a key from {TARGETS_AND_GROUPS}

@param raw_scope the JSON :scope data from an API query that is scopable, e.g. a Policy.

@param container The scopable object to which this scope belongs, e,g, an

instance of Jamf::Policy, Jamf::MobileDeviceApplication, etc.. If not provided, will be
set automatically after initialization
    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
464 def initialize(target_key, raw_scope = nil, container: nil)
465   raw_scope ||= DEFAULT_SCOPE.dup
466   unless TARGETS_AND_GROUPS.key?(target_key)
467     raise Jamf::InvalidDataError, "The target class of a Scope must be one of the symbols :#{TARGETS_AND_GROUPS.keys.join(', :')}"
468   end
469 
470   @should_update = false
471   @container = container
472 
473   @target_key = target_key
474   @target_class = SCOPING_CLASSES[@target_key]
475   @group_key = TARGETS_AND_GROUPS[@target_key]
476   @group_class = SCOPING_CLASSES[@group_key]
477 
478   @target_keys = [@target_key, @group_key] + TARGETS
479   @exclusion_keys = [@target_key, @group_key] + EXCLUSIONS
480 
481   if JAMF_DATA_LOSS_BUG_CLASSES.include?(@container.class)
482     @target_keys -= JAMF_DATA_LOSS_BUG_KEYS
483     @exclusion_keys -= JAMF_DATA_LOSS_BUG_KEYS
484   end
485 
486   parse_targets(raw_scope)
487   parse_limitations(raw_scope)
488   parse_exclusions(raw_scope)
489 end

Public Instance Methods

add_exclusion(key, item) click to toggle source

Add a single item for exclusions of this scope.

The item name will be checked for existence in the JSS, and an exception raised if the item doesn’t exist.

@param key the type of item being added to the exclusions, :computer, :building, etc…

@param item a valid identifier of the item being added

@example

add_exclusion(:network_segments, "foo")

@return [void]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
850 def add_exclusion(key, item)
851   key = pluralize_key(key)
852   item_id = validate_item(:exclusion, key, item)
853   return if @exclusions[key]&.include?(item_id)
854 
855   raise Jamf::AlreadyExistsError, "Can't exclude #{key} scope to '#{item}' because it's already explicitly included." if @targets[key]&.include?(item)
856 
857   raise Jamf::AlreadyExistsError, "Can't exclude #{key} '#{item}' because it's already an explicit limitation." if @limitations[key]&.include?(item)
858 
859   @exclusions[key] << item_id
860   note_pending_changes
861 end
add_inclusion(key, item = nil)
Alias for: add_target
add_limitation(key, item) click to toggle source

Add a single item for limiting this scope.

The item name will be checked for existence in the JSS, and an exception raised if the item doesn’t exist.

@param key the type of item being added, :computer, :building, etc…

@param item a valid identifier of the item being added

@example

add_limitation(:network_segments, "foo")

@return [void]

@todo handle ldap user/group lookups

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
760 def add_limitation(key, item)
761   key = pluralize_key(key)
762   item_id = validate_item(:limitation, key, item)
763   return nil if @limitations[key]&.include?(item_id)
764 
765   if @exclusions[key]&.include?(item_id)
766     raise Jamf::AlreadyExistsError, "Can't set #{key} limitation for '#{name}' because it's already an explicit exclusion."
767   end
768 
769   @limitations[key] << item_id
770   note_pending_changes
771 end
add_target(key, item = nil) click to toggle source

Add a single item as a target in this scope.

The item name will be checked for existence in the JSS, and an exception

raised if the item doesn't exist.

@param key the key from #{SCOPING_CLASSES} for the kind of item being added, :computer, :building, etc…

Use :all to scope to all targets (the same as calling #set_all_targets)

@param item a valid identifier of the item being added

@example

add_target(:computers, "mantis")

@example

add_target(:computer_groups, 2342)

@return [void]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
665 def add_target(key, item = nil)
666   if key == :all
667     set_all_targets
668     return
669   end
670 
671   key = pluralize_key(key)
672   item_id = validate_item(:target, key, item)
673   return if @targets[key]&.include?(item_id)
674 
675   if @exclusions[key]&.include?(item_id)
676     raise Jamf::AlreadyExistsError,
677           "Can't set #{key} target to '#{item}' because it's already an explicit exclusion."
678   end
679 
680   @targets[key] << item_id
681   @all_targets = false
682   note_pending_changes
683 end
Also aliased as: add_inclusion
all_targets=(bool) click to toggle source

to match the all_targets and all_targets? methods Just calls set_all_targets

@param bool [Boolean] shoud this scope include all targets?

@return [void]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
638 def all_targets=(bool)
639   if Jamf::Validate.boolean bool
640     set_all_targets
641   else
642     @all_targets = false
643     note_pending_changes
644   end
645 end
in_scope?(machine) click to toggle source

is a given machine is in this scope?

For a parameter you may pass either an instantiated Jamf::MobileDevice or Jamf::Computer, or an identifier for one. If an identifier is passed, it is not instantiated, but an API request is made for just the required subsets of data, thus speeding things up a bit when calling this method many times.

WARNING: For scopes that include Jamf Users and Jamf User Groups as targets or exclusions, this method may return an incorrect value. See the discussion in the documentation for the Scopable::Scope class under ‘IMPORTANT - Users & User Groups in Targets and Exclusions’

NOTE: currently in-range iBeacons are transient, and are not reported to the JSS as inventory data. As such they are ignored in this result. If a scope contains iBeacon limitations or exclusions, it is up to the user to be aware of that when evaluating the meaning of this result.

@param machine[Integer, String, Jamf::MobileDevice, Jamf::Computer]

Either an identifier for the machine, or an instantiated object

@return [Boolean]

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1017 def in_scope?(machine)
1018   machine_data = fetch_machine_data machine
1019 
1020   a_target?(machine_data) && within_limitations?(machine_data) && !excluded?(machine_data)
1021 end
include_all(clear = false)
Alias for: set_all_targets
pretty_print_instance_variables() click to toggle source

Remove large or redundant data structures from the instance_variables used to create pretty-print (pp) output.

@return [Array] the desired instance_variables

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
963 def pretty_print_instance_variables
964   vars = instance_variables.sort
965   vars.delete :@container
966   vars
967 end
remove_exclusion(key, item) click to toggle source

Remove a single item for exclusions of this scope

@param key the type of item being removed from the excludions, :computer, :building, etc…

@param item a valid identifier of the item being removed

@example

remove_exclusion(:network_segments, "foo")

@return [void]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
874 def remove_exclusion(key, item)
875   key = pluralize_key(key)
876   item_id = validate_item :exclusion, key, item, error_if_not_found: false
877   return unless @exclusions[key]&.include?(item_id)
878 
879   @exclusions[key].delete item_id
880   note_pending_changes
881 end
remove_inclusion(key, item)
Alias for: remove_target
remove_limitation(key, item) click to toggle source

Remove a single item for limiting this scope.

@param key the type of item being removed, :computer, :building, etc…

@param item a valid identifier of the item being removed

@example

remove_limitation(:network_segments, "foo")

@return [void]

@todo handle ldap user/group lookups

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
786 def remove_limitation(key, item)
787   key = pluralize_key(key)
788   item_id = validate_item :limitation, key, item, error_if_not_found: false
789   return unless item_id
790   return unless @limitations[key]&.include?(item_id)
791 
792   @limitations[key].delete item_id
793   note_pending_changes
794 end
remove_target(key, item) click to toggle source

Remove a single item as a target for this scope.

@param key the key from #{SCOPING_CLASSES} for the kind of item being removed, :computer, :building, etc…

@param item a valid identifier of the item being removed

@example

remove_target(:computer, "mantis")

@return [void]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
697 def remove_target(key, item)
698   key = pluralize_key(key)
699   item_id = validate_item :target, key, item, error_if_not_found: false
700   return unless item_id
701   return unless @targets[key]&.include?(item_id)
702 
703   @targets[key].delete item_id
704   note_pending_changes
705 end
Also aliased as: remove_inclusion
scope_xml() click to toggle source

@api private Return a REXML Element containing the current state of the Scope for adding into the XML of the container.

@return [REXML::Element]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
889 def scope_xml
890   scope = REXML::Element.new 'scope'
891   scope.add_element(@all_key.to_s).text = @all_targets
892 
893   @target_keys.each do |klass|
894     list = @targets[klass]
895     list.compact!
896     list.delete 0
897     list_as_hashes = list.map { |i| { id: i } }
898 
899     xml_list = SCOPING_CLASSES[klass].xml_list(list_as_hashes, :id)
900     xml_list.name = 'jss_users' if SCOPING_CLASSES[klass] == Jamf::User
901     xml_list.name = 'jss_user_groups' if SCOPING_CLASSES[klass] == Jamf::UserGroup
902     scope << xml_list
903   end
904 
905   limitations = scope.add_element('limitations')
906   @limitations.each do |klass, list|
907     list.compact!
908     list.delete 0
909     if klass == :users
910       users_xml = limitations.add_element 'users'
911       list.each do |name|
912         user_xml = users_xml.add_element 'user'
913         user_xml.add_element('name').text = name
914       end
915     elsif klass == :user_groups
916       user_groups_xml = limitations.add_element 'user_groups'
917       list.each do |name|
918         user_group_xml = user_groups_xml.add_element 'user_group'
919         user_group_xml.add_element('name').text = name
920       end
921     else
922       list_as_hashes = list.map { |i| { id: i } }
923       limitations << SCOPING_CLASSES[klass].xml_list(list_as_hashes, :id)
924     end
925   end
926 
927   exclusions = scope.add_element('exclusions')
928   @exclusion_keys.each do |klass|
929     list = @exclusions[klass]
930     list.compact!
931     list.delete 0
932     if klass == :users
933       users_xml = exclusions.add_element 'users'
934       list.each do |name|
935         user_xml = users_xml.add_element 'user'
936         user_xml.add_element('name').text = name
937       end
938     elsif klass == :user_groups
939       user_groups_xml = exclusions.add_element 'user_groups'
940       list.each do |name|
941         user_group_xml = user_groups_xml.add_element 'user_group'
942         user_group_xml.add_element('name').text = name
943       end
944     else
945       list_as_hashes = list.map { |i| { id: i } }
946 
947       xml_list = SCOPING_CLASSES[klass].xml_list(list_as_hashes, :id)
948       xml_list.name = 'jss_users' if SCOPING_CLASSES[klass] == Jamf::User
949       xml_list.name = 'jss_user_groups' if SCOPING_CLASSES[klass] == Jamf::UserGroup
950       exclusions << xml_list
951 
952     end
953   end
954   scope
955 end
scoped_machines() click to toggle source

Return a hash of id => name for all machines in the target class that are within this scope.

WARNING: This must instantiate all machines in the target class. It will still be slow, at least the first time for each target class. On the upside, the instantiated machines will be cached, so generating this list for other scopes with the same target class will be much much faster. In tests, 1600 Computers took about 7 minutes the first time, but less than 1 second after caching.

See also the warning for in_scope?

@return [Hash{Integer => String}]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
985 def scoped_machines
986   scoped_machines = {}
987   @target_class.all_objects(:refresh, cnx: container.cnx).each do |machine|
988     scoped_machines[machine.id] = machine.name if in_scope? machine
989   end
990   scoped_machines
991 end
set_all_targets(clear = false) click to toggle source

Set the scope’s targets to all.

By default, the limitations and exclusions remain. If a non-false parameter is provided, they will be removed also.

@param clear Should the limitations and exclusions be removed also?

@return [void]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
568 def set_all_targets(clear = false)
569   @targets = {}
570   @target_keys.each { |k| @targets[k] = [] }
571   @all_targets = true
572   if clear
573     @limitations = {}
574     LIMITATIONS.each { |k| @limitations[k] = [] }
575 
576     @exclusions = {}
577     @exclusion_keys.each { |k| @exclusions[k] = [] }
578   end
579   note_pending_changes
580 end
Also aliased as: include_all
set_exclusion(key, list)
Alias for: set_exclusions
set_exclusions(key, list) click to toggle source

Replace an exclusion list for this scope

The list must be an Array of names of items of the Class being excluded from the scope Each will be checked for existence in the JSS, and an exception raised if the item doesn’t exist.

@param key the type of item being excluded, :computer, :building, etc…

@param list the identifiers of the items being set

@example

set_exclusion(:network_segments, ['foo','bar'])

@return [void]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
810 def set_exclusions(key, list)
811   key = pluralize_key(key)
812   raise Jamf::InvalidDataError, "List must be an Array of #{key} identifiers, it may be empty." unless list.is_a? Array
813 
814   # check the idents
815   list.map! do |ident|
816     item_id = validate_item(:exclusion, key, ident)
817     case key
818     when *@target_keys
819       if @targets[key] && @exclusions[key].include?(item_id)
820         raise Jamf::AlreadyExistsError, "Can't exclude #{key} '#{ident}' because it's already explicitly included."
821       end
822     when *LIMITATIONS
823       if @limitations[key] && @exclusions[key].include?(item_id)
824         raise Jamf::AlreadyExistsError, "Can't exclude #{key} '#{ident}' because it's already an explicit limitation."
825       end
826     end
827     item_id
828   end # each
829 
830   return nil if list.sort == @exclusions[key].sort
831 
832   @exclusions[key] = list
833   note_pending_changes
834 end
Also aliased as: set_exclusion
set_inclusion(key, list = nil)
Alias for: set_targets
set_inclusions(key, list = nil)
Alias for: set_targets
set_limitation(key, list)
Alias for: set_limitations
set_limitations(key, list) click to toggle source

Replace a limitation list for this scope.

The list must be an Array of names of items of the Class represented by the key. Each will be checked for existence in the JSS, and an exception raised if the item doesn’t exist.

@param key the type of items being set as limitations, :network_segments, :users, etc…

@param list the identifiers of the items being set as limitations

@example

set_limitation(:network_segments, ['foo',231])

@return [void]

@todo handle ldap user group lookups

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
724 def set_limitations(key, list)
725   key = pluralize_key(key)
726   raise Jamf::InvalidDataError, "List must be an Array of #{key} identifiers, it may be empty." unless list.is_a? Array
727 
728   # check the idents
729   list.map! do |ident|
730     item_id = validate_item(:limitation, key, ident)
731     if @exclusions[key]&.include?(item_id)
732       raise Jamf::AlreadyExistsError, "Can't set #{key} limitation for '#{name}' because it's already an explicit exclusion."
733     end
734 
735     item_id
736   end # each
737 
738   return nil if list.sort == @limitations[key].sort
739 
740   @limitations[key] = list
741   note_pending_changes
742 end
Also aliased as: set_limitation
set_target(key, list = nil)
Alias for: set_targets
set_targets(key, list = nil) click to toggle source

Replace a list of item names for as targets in this scope.

The list must be an Array of names of items of the Class represented by the key. Each will be checked for existence in the JSS, and an exception raised if the item doesn’t exist.

@param key the key from #{SCOPING_CLASSES} for the kind of items being included, :computer, :building, etc… Use :all to scope to all targets (the same as calling set_all_targets)

@param list identifiers of the items being added

@example

set_targets(:computers, ['kimchi','mantis'])

@return [void]

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
601 def set_targets(key, list = nil)
602   if key == :all
603     set_all_targets
604     return
605   end
606 
607   key = pluralize_key(key)
608   raise Jamf::InvalidDataError, "List must be an Array of #{key} identifiers, it may be empty." unless list.is_a? Array
609 
610   # check the idents
611   list.map! do |ident|
612     item_id = validate_item(:target, key, ident)
613 
614     if @exclusions[key]&.include?(item_id)
615       raise Jamf::AlreadyExistsError, \
616             "Can't set #{key} target to '#{ident}' because it's already an explicit exclusion."
617     end
618 
619     item_id
620   end # each
621 
622   return nil if list.sort == @targets[key].sort
623 
624   @targets[key] = list
625   @all_targets = false
626   note_pending_changes
627 end
to_s() click to toggle source
     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1024 def to_s
1025   "Scope for #{container.class} id #{container.id}"
1026 end

Private Instance Methods

a_target?(machine_data) click to toggle source

@param machine_data See fetch_machine_data @return [Boolean]

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1203 def a_target?(machine_data)
1204   return false unless machine_data[:general][:managed]
1205   return true if \
1206     all_targets? || \
1207     machine_directly_scoped?(machine_data, :target) || \
1208     machine_in_scope_group?(machine_data, :target) || \
1209     machine_in_scope_buildings?(machine_data, :target) || \
1210     machine_in_scope_depts?(machine_data, :target)
1211 
1212   false
1213 end
excluded?(machine_data) click to toggle source

@param machine_data See fetch_machine_data @return [Boolean]

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1230 def excluded?(machine_data)
1231   return true if
1232     machine_directly_scoped?(machine_data, :exclusion) || \
1233     machine_in_scope_group?(machine_data, :exclusion) || \
1234     machine_in_scope_buildings?(machine_data, :exclusion) || \
1235     machine_in_scope_depts?(machine_data, :exclusion) || \
1236     machine_in_scope_netsegs?(machine_data, :exclusion) || \
1237     machine_in_scope_jamf_ldap_users_list?(machine_data, :exclusion) || \
1238     machine_in_scope_ldap_usergroup_list?(machine_data, :exclusion)
1239 
1240   false
1241 end
fetch_machine_data(machine) click to toggle source

The data used by the methods that figure out if a machine is in this scope, a Hash of Hashes. the sub hashes are:

general: the 'general' subset
location: the 'location' subset
group_ids: an Array of the group ids to which the machine belongs.

@param machine[Integer, String, Jamf::MobileDevice, Jamf::Computer]

Either an identifier for the machine, or an instantiated object

@return

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1122 def fetch_machine_data(machine)
1123   case machine
1124   when Jamf::Computer
1125     raise Jamf::InvalidDataError, "Targets of this scope must be #{@target_class}" unless @target_class == Jamf::Computer
1126 
1127     general = machine.init_data[:general]
1128     location = machine.init_data[:location]
1129     group_ids = group_ids machine.computer_groups
1130 
1131     # put in standardize place for easier use
1132     # MDevs already have this at general[:managed]
1133     general[:managed] = general[:remote_management][:managed]
1134 
1135   when Jamf::MobileDevice
1136     raise Jamf::InvalidDataError, "Targets of this scope must be #{@target_class}" unless @target_class == Jamf::MobileDevice
1137 
1138     general = machine.init_data[:general]
1139     location = machine.init_data[:location]
1140     group_ids = group_ids machine.mobile_device_groups
1141 
1142   else
1143     general, location, group_ids = fetch_subsets(machine)
1144   end # case
1145 
1146   {
1147     general: general,
1148     location: location,
1149     group_ids: group_ids
1150   }
1151 end
fetch_subsets(ident) click to toggle source

When we are given an indentifier for a machine, fetch just the subsets of API data we need to determine if the machine is in this scope

@param ident[String, Integer]

@return [Array] the general, locacation, and parsed group IDs

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1161 def fetch_subsets(ident)
1162   id = @target_class.valid_id ident, cnx: container.cnx
1163   raise Jamf::NoSuchItemError, "No #{@target_class} matching #{machine}" unless id
1164 
1165   if @target_class == Jamf::MobileDevice
1166     grp_subset = 'MobileDeviceGroups'
1167     top_key = :mobile_device
1168   else
1169     grp_subset = 'GroupsAccounts'
1170     top_key = :computer
1171   end
1172   subset_rsrc = "#{@target_class::RSRC_BASE}/id/#{id}/subset/General&Location&#{grp_subset}"
1173   data = container.cnx.c_get(subset_rsrc)[top_key]
1174   grp_data =
1175     if @target_class == Jamf::MobileDevice
1176       data[:mobile_device_groups]
1177     else
1178       data[:groups_accounts][:computer_group_memberships]
1179     end
1180 
1181   [data[:general], data[:location], group_ids(grp_data)]
1182 end
group_ids(raw) click to toggle source

Given the raw API data for a machines group membership, return an array of the IDs of the groups.

@param raw The API array of the machine’s group memberships

@return [Array] The ID’s of the groups to which the machine belongs.

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1191 def group_ids(raw)
1192   if @target_class == Jamf::MobileDevice
1193     raw.map { |mdg| mdg[:id] }
1194   else
1195     names_to_ids = @group_class.map_all_ids_to(:name).invert
1196     raw.map { |gn| names_to_ids[gn] }
1197   end
1198 end
machine_directly_scoped?(machine_data, part) click to toggle source

@param machine_data See fetch_machine_data @param part either :target or :exclusion @return [Boolean] Is the machine directly spcified in this part of the scope?

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1247 def machine_directly_scoped?(machine_data, part)
1248   scope_list = part == :target ? @targets[:direct_targets] : @exclusions[:direct_exclusions]
1249   scope_list.include? machine_data[:general][:id]
1250 end
machine_in_scope_buildings?(machine_data, part) click to toggle source

@param machine_data See fetch_machine_data @param part either :target or :exclusion @return [Boolean] Is the machine in any building listed in this part of the scope?

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1270 def machine_in_scope_buildings?(machine_data, part)
1271   scope_list = part == :target ? @targets[:buildings] : @exclusions[:buildings]
1272 
1273   # nil if empty
1274   return if scope_list.empty?
1275   # false if no building for the machine - it isn't in any dept
1276   return false if machine_data[:location][:building].to_s.empty?
1277 
1278   building_id = Jamf::Building.map_all_ids_to(:name).invert[machine_data[:location][:building]]
1279   scope_list.include? building_id
1280 end
machine_in_scope_depts?(machine_data, part) click to toggle source

@param machine_data See fetch_machine_data @param part either :target or :exclusion @return [Boolean] Is the machine in any department listed in this part of the scope?

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1286 def machine_in_scope_depts?(machine_data, part)
1287   scope_list = part == :target ? @targets[:departments] : @exclusions[:departments]
1288 
1289   # nil if empty
1290   return if scope_list.empty?
1291   # false if no dept for the machine - it isn't in any dept
1292   return false if machine_data[:location][:department].to_s.empty?
1293 
1294   dept_id = Jamf::Department.map_all_ids_to(:name).invert[machine_data[:location][:department]]
1295 
1296   scope_list.include? dept_id
1297 end
machine_in_scope_group?(machine_data, part) click to toggle source

@param machine_data See fetch_machine_data @param part either :target or :exclusion @return [Boolean] Is the machine a member of any group listed in this part of the scope?

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1256 def machine_in_scope_group?(machine_data, part)
1257   scope_list = part == :target ? @targets[:group_targets] : @exclusions[:group_exclusions]
1258   # if the list is empty, return nil
1259   return if scope_list.empty?
1260 
1261   # if the intersection of the machine's group ids, and those of the scope part
1262   # is not empty, then the machine is in at least one of the groups
1263   !(machine_data[:group_ids] & scope_list).empty?
1264 end
machine_in_scope_jamf_ldap_users_list?(machine_data, part) click to toggle source

@param machine_data See fetch_machine_data @param part either :limitation or :exclusion @return [Boolean] Is the user of this machine in the list of jamf/ldap users in this part of the scope?

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1323 def machine_in_scope_jamf_ldap_users_list?(machine_data, part)
1324   scope_list = part == :limitation ? @limitations[:jamf_ldap_users] : @exclusions[:jamf_ldap_users]
1325 
1326   # nil if the list is empty
1327   return if scope_list.empty?
1328 
1329   scope_list.include? machine_data[:location][:username]
1330 end
machine_in_scope_ldap_usergroup_list?(machine_data, part) click to toggle source

@param machine_data See fetch_machine_data @param part either :limitation or :exclusion @return [Boolean] Is the user of this machine a member of any of the LDAP groups in in this part of the scope?

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1336 def machine_in_scope_ldap_usergroup_list?(machine_data, part)
1337   scope_list = part == :limitation ? @limitations[:ldap_user_groups] : @exclusions[:ldap_user_groups]
1338 
1339   # nil if the list is empty
1340   return if scope_list.empty?
1341 
1342   # loop thru them checking to see if the user is a member
1343   scope_list.each do |ldapgroup|
1344     server = Jamf::LdapServer.server_for_group ldapgroup
1345     # if the group doesn't exist in any LDAP the user isn't a part of it
1346     next unless server
1347 
1348     # if the user name is in any group, return true
1349     return true if Jamf::LdapServer.check_membership server, machine_data[:location][:username], ldapgroup
1350   end
1351 
1352   # if we're here, not in any group
1353   false
1354 end
machine_in_scope_netsegs?(machine_data, part) click to toggle source

@param machine_data See fetch_machine_data @param part either :limitation or :exclusion @return [Boolean] Is the machine in any NetworkSegment listed in this part of the scope?

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1303 def machine_in_scope_netsegs?(machine_data, part)
1304   scope_list = part == :limitation ? @limitations[:network_segments] : @exclusions[:network_segments]
1305 
1306   # nil if no netsegs in scope part
1307   return if scope_list.empty?
1308 
1309   ip = @target_class == Jamf::Computer ? machine_data[:general][:last_reported_ip] : machine_data[:general][:ip_address]
1310   # false if no ip for machine - it isn't in a any of the segs
1311   return false if ip.to_s.empty?
1312 
1313   mach_segs = Jamf::NetworkSegment.network_segments_for_ip ip
1314 
1315   # if the intersection is not empty, then the machine is in at least one of the net segs
1316   !(mach_segs & scope_list).empty?
1317 end
note_pending_changes() click to toggle source

make a note both in this instance and in our container that a change has been made and an update is needed

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1358 def note_pending_changes
1359   warn_about_data_loss_bug
1360   warn_about_array_hash_bug
1361   @should_update = true
1362   @container&.should_update
1363 end
parse_exclusions(raw_scope) click to toggle source

parse the limitations from the init data

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
542 def parse_exclusions(raw_scope)
543   @exclusions = {}
544   return unless raw_scope[:exclusions]
545 
546   @exclusion_keys.each do |k|
547     api_data = raw_scope[:exclusions][k]
548     api_data ||= []
549     @exclusions[k] = api_data.compact.map do |n|
550       LDAP_BASED_KEYS.include?(k) ? n[:name].to_s : n[:id].to_i
551     end
552 
553     @exclusions[:direct_exclusions] = @exclusions[k] if k == @target_key
554     @exclusions[:group_exclusions] = @exclusions[k] if k == @group_key
555   end # @exclusion_keys.each
556 end
parse_limitations(raw_scope) click to toggle source

parse the limitations from the init data

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
526 def parse_limitations(raw_scope)
527   @limitations = {}
528   return unless raw_scope[:limitations]
529 
530   LIMITATIONS.each do |k|
531     api_data = raw_scope[:limitations][k]
532     api_data ||= []
533     @limitations[k] = api_data.compact.map do |n|
534       LDAP_BASED_KEYS.include?(k) ? n[:name].to_s : n[:id].to_i
535     end
536   end # LIMITATIONS.each do |k|
537 end
parse_targets(raw_scope) click to toggle source

parse the targets from the init data

    # File lib/jamf/api/classic/api_objects/scopable/scope.rb
493 def parse_targets(raw_scope)
494   @all_key = "all_#{@target_key}".to_sym
495   @all_targets = raw_scope[@all_key]
496 
497   # Everything gets mapped from an Array of Hashes to
498   # an Array of ids
499   @targets = {}
500   @target_keys.each do |k|
501     raw_scope[k] ||= []
502     @targets[k] =
503       if raw_scope[k].is_a? Array
504         # the data should be an array of hashes with :id and :name
505         raw_scope[k].compact.map { |n| n[:id].to_i }
506 
507       elsif raw_scope[k].is_a? Hash
508         # its a hash of hashes, it suffers the 2009 XML->JSON Array bug and
509         # there will be data loss of any more than one item.
510         # We know this to be the case for OSXConfigProfiles using
511         # jss_user_groups as targets. When used as exclusions, they are
512         # the correct array of hashes
513         @array_hash_bug_target_key = k unless raw_scope[k].empty?
514         raw_scope[k].values.compact.map { |n| n[:id].to_i }
515       else
516         []
517       end
518     @targets[:direct_targets] = @targets[k] if k == @target_key
519     @targets[:group_targets] = @targets[k] if k == @group_key
520   end # @target_keys.each do |k|
521 end
pluralize_key(key) click to toggle source

the symbols used in the API data are plural, e.g. ‘network_segments’ this will pluralize them, allowing us to use singulars as well. This also handles the synonyms for users and user_groups

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1100 def pluralize_key(key)
1101   if LDAP_JAMF_USER_KEYS.include? key
1102     :users
1103   elsif LDAP_GROUP_KEYS.include? key
1104     :user_groups
1105   else
1106     key.to_s.end_with?(ESS) ? key : "#{key}s".to_sym
1107   end
1108 end
validate_item(realm, key, ident, error_if_not_found: true) click to toggle source

look up a valid id or nil, for use in a scope type Raise an error if not found, unless error_if_not_found is falsey

@param realm [Symbol] How is this key being used in the scope?

:target, :limitation, or :exclusion

@param key [Symbol] What kind of thing are we adding to the scope?

e.g computer, network_segment, etc.

@param ident [String, Integer] A unique identifier for the item being

validated, jss id, name, serial number, etc.

@param error_if_not_found [Boolean] raise an error if no match for the ident

@return [Integer, String, nil] the valid id or string for the item, or nil if not found

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1048 def validate_item(realm, key, ident, error_if_not_found: true)
1049   # which keys allowed depends on how the item is used...
1050   # Classes susceptible to the Data Loss Bug have JAMF_DATA_LOSS_BUG_KEYS
1051   # removed from the possible keys.
1052   possible_keys =
1053     case realm
1054     when :target
1055       JAMF_DATA_LOSS_BUG_CLASSES.include?(@container.class) ? (@target_keys - JAMF_DATA_LOSS_BUG_KEYS) : @target_keys
1056     when :limitation
1057       LIMITATIONS
1058     when :exclusion
1059       JAMF_DATA_LOSS_BUG_CLASSES.include?(@container.class) ? (@exclusion_keys - JAMF_DATA_LOSS_BUG_KEYS) : @exclusion_keys
1060     else
1061       raise ArgumentError, 'Unknown realm, must be :target, :limitation, or :exclusion'
1062     end
1063 
1064   key = pluralize_key(key)
1065 
1066   unless possible_keys.include? key
1067     msg = "#{realm} key must be one of :#{possible_keys.join(', :')}."
1068     if JAMF_DATA_LOSS_BUG_CLASSES.include?(@container.class) && JAMF_DATA_LOSS_BUG_KEYS.include?(key)
1069       msg = "#{msg}\nJAMF BUG WARNING: The API cannot handle :jss_users or :jss_user_groups in scope targets or exclusions of Policies or Patch Policies. If any exist in the scope they will be deleted when you save any changes to the policy via the API."
1070     end
1071     raise Jamf::InvalidDataError, msg
1072   end
1073 
1074   id = nil
1075 
1076   # id will be a string
1077   if key == :users
1078     id = ident
1079   # the web UI doesn't validate this data, it accepts any string, so we do too
1080   # id = ident if Jamf::User.all_names(:refresh, cnx: container.cnx).include?(ident) || Jamf::LdapServer.user_in_ldap?(ident)
1081 
1082   # id will be a string
1083   elsif key == :user_groups
1084     # The web UI does validate that the group exists in LDAP
1085     id = ident if Jamf::LdapServer.group_in_ldap? ident, cnx: container.cnx
1086 
1087   # id will be an integer
1088   else
1089     id = SCOPING_CLASSES[key].valid_id ident, cnx: container.cnx
1090   end
1091 
1092   raise Jamf::NoSuchItemError, "No existing #{key} matching '#{ident}'" if error_if_not_found && id.nil?
1093 
1094   id
1095 end
warn_about_array_hash_bug() click to toggle source

display warning about array-hash bug data loss

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1377 def warn_about_array_hash_bug
1378   return unless @array_hash_bug_target_key
1379   return if Jamf::Scopable::Scope.do_not_warn_about_array_hash_scope_bugs?
1380   return if @warn_about_array_hash_bug_has_run
1381 
1382   warn "WARNING: At least one #{@array_hash_bug_target_key} is used as a scope target.\nDue to a bug in the Classic API, if you save changes to this scope, all but the last #{@array_hash_bug_target_key} will be deleted from the scope targets when you save!"
1383 
1384   @warn_about_array_hash_bug_has_run = true
1385 end
warn_about_data_loss_bug() click to toggle source

display data loss warning.

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1366 def warn_about_data_loss_bug
1367   return unless JAMF_DATA_LOSS_BUG_CLASSES.include?(@container.class)
1368   return if Jamf::Scopable::Scope.do_not_warn_about_policy_scope_bugs?
1369   return if @warn_about_data_loss_bug_has_run
1370 
1371   warn "WARNING: Saving changes to this scope may cause data loss!\nDue to a bug in the Classic API, if the scope uses Jamf Users or User Groups in the Targets or Exclusions, they will be deleted from the scope when you save!"
1372 
1373   @warn_about_data_loss_bug_has_run = true
1374 end
within_limitations?(machine_data) click to toggle source

@param machine_data See fetch_machine_data @return [Boolean]

     # File lib/jamf/api/classic/api_objects/scopable/scope.rb
1218 def within_limitations?(machine_data)
1219   return false if \
1220     machine_in_scope_netsegs?(machine_data, :limitation) == false || \
1221     machine_in_scope_jamf_ldap_users_list?(machine_data, :limitation) == false || \
1222     machine_in_scope_ldap_usergroup_list?(machine_data, :limitation) == false
1223 
1224   true
1225 end