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.
-
How ruby-jss handles this bug:
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
andJamf::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 ServiceUser
Group’ as labeled in the web-ui. These were formerly labeled as ‘LDAP/Local User’ and ‘LDAPUser
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 fromSCOPING_CLASSES
to their corresponding target group keys fromSCOPING_CLASSES
.
Attributes
@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
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>}]
what type of target group is this scope for? ComputerGroups or MobileDeviceGroups?
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>}]
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>}]
@return [Boolean] Have changes been made to the scope, that need
to be sent to the server?
@return [Boolean] Have changes been made to the scope, that need
to be sent to the server?
what type of target is this scope for? Computers or MobileDevices?
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>}]
@return [Boolean] should we expect a potential 409 Conflict
if we can't connect to LDAP servers for verification?
Public Class Methods
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
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
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
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 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 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 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
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
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
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 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 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 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
@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
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 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
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
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
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
# 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
@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
@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
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
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
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
@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
@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
@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
@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
@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
@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
@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
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 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 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 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
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
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
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
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
@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