ruby-jss 3.1.0b2 → 3.2.0b3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bae3e60bf2b0cb08e2bc6a1ead15c1de6b213e667046c90026a803f3549b67da
4
- data.tar.gz: a08f8ad25dd5ab4bfd614e2e6b508fd2b408154f2d39bcabfc1a693341a9fedf
3
+ metadata.gz: 2f0e6a46c0c834feb898c00c2df2f509756a7f84cd346510c6f2502be433258a
4
+ data.tar.gz: f765f17913f8bf75f8ea9d066845b451150ce893139d089e797fed061a84929d
5
5
  SHA512:
6
- metadata.gz: 54b0cbe591066ae572f1ae3797afddea5f3f44be35273127798473118b96d014090fc09f39de28be41e96cd19ebac1334cd016d9e97a43b4163791341303ce1a
7
- data.tar.gz: 35e189daa832b3a6598725f42d3d0716bb1f95845c70edd86206ab1a35ffb8f0af8680d7c33306d08ad258758c623a3974ba107574dc1dbebd6e6db5c8a65e28
6
+ metadata.gz: fb2c176af326d5d03e7d2b650c5aa8d2bafd3de420e0a3514af9532a9a68619ec5bdc9888e718ebeec1f355061b5c931ae4663dd43310d7d8c3e8edf99fa121c
7
+ data.tar.gz: 12c3b89f22ed8e9a9384e8928d7bd4c5f09122276f793cf49e418ded3293e4a741bdfa2fad9ca96d47c6016d97fed63db5aeb447cf46cb4cf59e9b3582641700
data/CHANGES.md CHANGED
@@ -15,11 +15,44 @@ __Please update all installations of ruby-jss to at least v1.6.0.__
15
15
  Many many thanks to actae0n of Blacksun Hackers Club for reporting this issue and providing examples of how it could be exploited.
16
16
 
17
17
  --------
18
- ## \[3.1.0] 2023-06-05
18
+ ## \[UNRELEASED]
19
+
20
+ ### Changed
21
+ - Improved handling of known API bug in Jamf::Scopable::Scope.
22
+
23
+ There is a long-standing bug in working with 'scope' via the Classic API, which can cause data-loss when you have 'Users' and 'User Groups' (known in the api data as `jss_users` and `jss_user_groups`) defined in the targets or exclusions. Thanks to @yanniks on GitHub, I recently learned that the bug only affects a few API objects, namely Policies and PatchPolicies.
24
+
25
+ This release of ruby-jss will properly handle jss_users and jss_user_groups in all scopes where the API can handle them. In Policy and PatchPolicy objects, those values aren't allowed, since the API can't deal with them. If you edit any other aspect of the scope of one of those obects in ruby-jss, you'll get a warning that saving your changes may cause data-loss - deleting any defined Users and User Groups in the targets or exclusions. If the scope really doesn't use any Users or User Groups, you should be OK saving your changes. To prevent the warnings, call `Jamf::Scopable::Scope.do_not_warn_about_policy_scope_bugs` before changing any scopes.
26
+
27
+ For more details, see the discussion in the comments/docs for the Jamf::Scopeable::Scope class in lib/jamf/api/classic/api_objects/scopable/scope.rb or in the [rubydocs page for the Scope class](https://www.rubydoc.info/gems/ruby-jss/Jamf/Scopable/Scope).
28
+
29
+ Many thanks to @yanniks for bringing to my attention that the bug doesn't occur in all scopes.
30
+
31
+ - Warn of API bug when using jss_user_groups as scope targets of OSXConfigurationProfiles
32
+
33
+ We discovered a new (to us) isolated occurrance of the long-standing XML Array => JSON Hash bug
34
+ (which can cause data loss). If you have more that one jss_user_groups defined as scope targets
35
+ of a OSXConfigurationProfile, the API will only return the last of those groups in the JSON data,
36
+ and saving changes to the profile via ruby-jss will remove the other groups from the Profile in
37
+ Jamf.
38
+
39
+ This seems to only affect scope targets of OSXConfigurationProfiles - groups used in exclusions
40
+ seem to be fine, as do other scopable objects that uses jss_user_groups anywhere in their scope.
41
+
42
+ When you edit the scope of a scopable object and ruby-jss notices this API bug applies, you'll see a warning that saving changes to the scope may cause data loss. To disable these warnings, call `Jamf::Scopable::Scope.do_not_warn_about_array_hash_scope_bugs` before changing any scopes.
43
+
44
+ For more details, see the discussion in the comments/docs for the Jamf::Scopeable::Scope class in lib/jamf/api/classic/api_objects/scopable/scope.rb or in the [rubydocs page for the Scope class](https://www.rubydoc.info/gems/ruby-jss/Jamf/Scopable/Scope).
45
+
46
+
47
+ ### Fixed
48
+ - Jamf::DeviceEnrollment.device no longer uses String#upcase!, which fails on frozen strings. Instead just use String#casecmp?
49
+
50
+ ## \[3.1.0] 2023-06-06
19
51
 
20
52
  ### Added
21
53
  - Jamf::Computer.filevault_info and Jamf::Computer#filevault_info can retrieve FileVault info from v1/computer-inventory/filevault and related endpoints
22
54
  - Jamf::Computer.recovery_lock_password and Jamf::Computer#recovery_lock_password can retrieve stored recovery lock passwords
55
+ - Jamf::Pager#last_fetched_page - Integer, the last page returned by #fetch_next_page
23
56
  - There are now several ways to set scopes to all targets.
24
57
  - The original #include_all has been renamed #set_all_targets, and #include_all is an alias to it
25
58
  - The symbol :all can be passed to the #set_targets, and #add_target methods as they 'key' parameter, and they will just call #set_all_targets
@@ -35,7 +68,7 @@ Many many thanks to actae0n of Blacksun Hackers Club for reporting this issue an
35
68
  - Fixed a bug in Jamf::Pager#initialize when constructing the query-path of the paged resource URL
36
69
  - Fixed a bug in Jamf::Pager#initialize: The instantiate: parameter takes a class, not a boolean
37
70
  - Fixed a bug in Jamf::CollectionResource.pager: The instantiate: parameter takes a boolean, but must pass a class to Jamf::Pager#initialize
38
- - Jamf::OAPIObject base-class: can now instantiate objects that hold a single value
71
+ - Jamf::OAPIObject (base-class) can now instantiate objects that hold a single value
39
72
 
40
73
  ### Changed
41
74
  - Auto-generated OAPISchemas have been refreshed from Jamf Pro 10.46.0
@@ -96,13 +129,38 @@ Version 2.0.0 is a major refactoring of ruby-jss. While attempting to provide as
96
129
 
97
130
  Here are the high-level changes and there are many many others. For more details, see [CHANGES-2.0.0.md](CHANGES-2.0.0.md)
98
131
 
99
- - Support for Ruby 3.x
100
- - tested in 3.0 and 3.1
101
- - Combined access to both the Classic and Jamf Pro APIs
102
- - A single namespace module
132
+ ### Added
133
+
134
+ - Support for Ruby 3.x
135
+ - tested in 3.0 and 3.1
136
+ - Combined access to both the Classic and Jamf Pro APIs
137
+ - A single namespace module
103
138
  - Connection objects talk to both APIs & automatically handle details like bearer tokens
104
- - Auto-generated code for Jamf Pro API objects
105
- - Autoloading of code using [Zeitwerk](https://github.com/fxn/zeitwerk)
139
+ - Auto-generated code for Jamf Pro API objects
140
+ - Autoloading of code using [Zeitwerk](https://github.com/fxn/zeitwerk)
141
+
142
+ ### Changed
143
+
144
+ These things are notably different in v2.0.0
145
+ - Paged queries to the Jamf Pro API
146
+ - API data are no longer cached for the JP API, possibly eventually for the classic
147
+ - No Attribute aliases for Jamf Pro API objects
148
+ - Class/Mixin hierarchy for Jamf Pro API objects
149
+ - Support for 'Sticky Sessions' in Jamf Cloud
150
+ - The valid_id method for Classic API collection classes
151
+
152
+ ### Deprecated
153
+
154
+ These things will go away in some future version of ruby-jss, please update your code sooner than later.
155
+
156
+ - Use of the term 'api'
157
+ - .map_all_ids_to method for Classic API collection classes
158
+ - Using .make, #create, and #update for Classic API objects
159
+ - JSS::CONFIG
160
+ - Jamf::Connection instance methods #next_refresh, #secs_to_refresh, & #time_to_refresh
161
+ - Cross-object validation in setters
162
+ - fetch :random
163
+
106
164
 
107
165
  ## \[1.6.7] - 2022-02-22
108
166
 
@@ -170,10 +228,6 @@ Here are the high-level changes and there are many many others. For more details
170
228
 
171
229
  ### Changed
172
230
 
173
- - ruby-jss no longer uses the 'plist' gem due to a remote code execution security issue when using `Plist.parse_xml`. Plists are now handled by the CFPropertyList gem. The existing wrapper method `JSS.parse_plist` bas been updated to use the new gem, and a new wrapper method has been added to convert ruby data to XML plist: `JSS.xml_plist_from(data)`. All internal references to methods from the insecure 'plist' gem have been replaced with calls to those wrapper methods.
174
-
175
- Many many thanks to actae0n of Blacksun Hackers Club for reporting this security issue and providing examples of how it could be exploited.
176
-
177
231
  - In preparation for the removal of the 'runScript' command in the jamf binary, JSS::Script no longer uses it within the 'run' instance method. Instead, it just does what the jamf binary did: It creates a private temp folder, writes the script to disk in that temp folder, executes the script with any given params, then deletes the folder, returning the exit status and output from the script.
178
232
 
179
233
  - Jamf::Script#run now takes parameters in the named params `p4:` through `p11:` for consistency with other parts of ruby-jss.
@@ -184,6 +238,11 @@ Here are the high-level changes and there are many many others. For more details
184
238
 
185
239
  - JSS.expand_min_os has been updated to handle Apple's new version numbers for macOS. This method takes a string like '>=10.14.3' and expands it into a large array of greater OS versions and is used by the 'os_limitations' method of Packages and Scripts. For any range of versions that includes Big Sur, both '11.x.x' and '10.16' as included in the output, to catch machines that may have SYSTEM_VERSION_COMPAT set in their env.
186
240
 
241
+ ### Security
242
+
243
+ - ruby-jss no longer uses the 'plist' gem due to a remote code execution security issue when using `Plist.parse_xml`. Plists are now handled by the CFPropertyList gem. The existing wrapper method `JSS.parse_plist` bas been updated to use the new gem, and a new wrapper method has been added to convert ruby data to XML plist: `JSS.xml_plist_from(data)`. All internal references to methods from the insecure 'plist' gem have been replaced with calls to those wrapper methods.
244
+
245
+ Many many thanks to actae0n of Blacksun Hackers Club for reporting this security issue and providing examples of how it could be exploited.
187
246
 
188
247
  ## \[1.5.3] - 2020-12-28
189
248
 
@@ -408,7 +408,6 @@ module Jamf
408
408
  @grace_period_message = grace[:message]
409
409
  @grace_period_message = DFT_GRACE_PERIOD_MESSAGE if @grace_period_message.to_s.empty?
410
410
 
411
-
412
411
  # read-only values, they come from the version.
413
412
  @release_date = JSS.epoch_to_time gen[:release_date]
414
413
  @incremental_update = gen[:incremental_update]
@@ -432,6 +431,7 @@ module Jamf
432
431
  #
433
432
  def patch_title_name
434
433
  return @patch_title.name if @patch_title
434
+
435
435
  Jamf::PatchTitle.map_all_ids_to(:name)[software_title_configuration_id]
436
436
  end
437
437
 
@@ -439,6 +439,7 @@ module Jamf
439
439
  #
440
440
  def target_version=(new_tgt_vers)
441
441
  return if new_tgt_vers == target_version
442
+
442
443
  @target_version = validate_target_version new_tgt_vers
443
444
  @need_to_update = true
444
445
  @refetch_for_new_version = true
@@ -450,6 +451,7 @@ module Jamf
450
451
  #
451
452
  def enable
452
453
  return if enabled
454
+
453
455
  @enabled = true
454
456
  @need_to_update = true
455
457
  end
@@ -460,6 +462,7 @@ module Jamf
460
462
  #
461
463
  def disable
462
464
  return unless enabled
465
+
463
466
  @enabled = false
464
467
  @need_to_update = true
465
468
  end
@@ -468,6 +471,7 @@ module Jamf
468
471
  #
469
472
  def allow_downgrade=(new_val)
470
473
  return if new_val == allow_downgrade
474
+
471
475
  @allow_downgrade = Jamf::Validate.boolean new_val
472
476
  @need_to_update = true
473
477
  end
@@ -476,6 +480,7 @@ module Jamf
476
480
  #
477
481
  def patch_unknown=(new_val)
478
482
  return if new_val == patch_unknown
483
+
479
484
  @patch_unknown = Jamf::Validate.boolean new_val
480
485
  @need_to_update = true
481
486
  end
@@ -488,6 +493,7 @@ module Jamf
488
493
  days = NO_DEADLINE unless days.positive?
489
494
  end
490
495
  return if days == deadline
496
+
491
497
  @deadline = days
492
498
  @need_to_update = true
493
499
  end
@@ -498,6 +504,7 @@ module Jamf
498
504
  mins = Jamf::Validate.integer(mins)
499
505
  mins = 0 if mins.negative?
500
506
  return if mins == grace_period
507
+
501
508
  @grace_period = mins
502
509
  @need_to_update = true
503
510
  end
@@ -506,6 +513,7 @@ module Jamf
506
513
  #
507
514
  def grace_period_subject=(subj)
508
515
  return if grace_period_subject == subj.to_s
516
+
509
517
  @grace_period_subject = subj.to_s
510
518
  @need_to_update = true
511
519
  end
@@ -514,6 +522,7 @@ module Jamf
514
522
  #
515
523
  def grace_period_message=(msg)
516
524
  return if grace_period_message == msg
525
+
517
526
  @grace_period_message = msg
518
527
  @need_to_update = true
519
528
  end
@@ -563,8 +572,10 @@ module Jamf
563
572
  return a_title.id
564
573
  end
565
574
  raise Jamf::MissingDataError, ':patch_title is required' unless a_title
575
+
566
576
  title_id = Jamf::PatchTitle.valid_id a_title
567
577
  return title_id if title_id
578
+
568
579
  raise Jamf::NoSuchItemError, "No Patch Title matches '#{a_title}'"
569
580
  end
570
581
 
@@ -613,7 +624,7 @@ module Jamf
613
624
  general.add_element('allow_downgrade').text = allow_downgrade
614
625
  general.add_element('patch_unknown').text = patch_unknown
615
626
 
616
- obj << scope.scope_xml
627
+ obj << scope.scope_xml if scope.should_update?
617
628
 
618
629
  add_self_service_xml doc
619
630
 
@@ -2111,7 +2111,7 @@ module Jamf
2111
2111
  activation = @server_side_limitations[:activation]
2112
2112
  date_time_limitations.add_element('activation_date_epoch').text = activation.to_jss_epoch if activation
2113
2113
 
2114
- obj << @scope.scope_xml
2114
+ obj << @scope.scope_xml if @scope.should_update?
2115
2115
 
2116
2116
  reboot = obj.add_element 'reboot'
2117
2117
  JSS.hash_to_rexml_array(@reboot_options).each { |elem| reboot << elem }
@@ -35,7 +35,7 @@ module Jamf
35
35
  #
36
36
  # Scope data comes from the API as a hash within the overall object data.
37
37
  # The main keys of the hash define the included targets of the scope. A
38
- # sub-hash defines limitations on those inclusions, and another sub-hash
38
+ # sub-hash defines limitations on those targets, and another sub-hash
39
39
  # defines explicit exclusions.
40
40
  #
41
41
  # This class provides methods for adding, removing, or fully replacing the
@@ -44,85 +44,143 @@ module Jamf
44
44
  # This class also provides a way to see if a machine will be included in
45
45
  # this scope.
46
46
  #
47
- # IMPORTANT - Users & User Groups in Targets and Exclusions:
47
+ # = Discussion: Users & User Groups in Scopes:
48
+ #######################################
49
+ # The Classic API has bugs, as well as non-obvious/historical oddness,
50
+ # regarding the use of Users, UserGroups, Directory Service/Local Users,
51
+ # and Directory Service User Groups in scopes.
52
+ # Here's a discussion of those issues, and how ruby-jss handles them.
48
53
  #
49
- # The classic API has bugs regarding the use of Users, UserGroups,
50
- # LDAP/Local Users, & LDAP User Groups in scopes. Here's a discussion
51
- # of those bugs and how ruby-jss handles them.
54
+ # == Historical Oddness
52
55
  #
53
- # Targets/Inclusions
54
- # - 'Users' in the Scope UI can only be Jamf::Users - No LDAP
55
- # - BUG: They do not appear in API data (XML or JSON) and are
56
- # NOT SUPPORTED in ruby-jss.
57
- # - You must use the Web UI to work with them in a Scope.
58
- # - 'User Groups' in the Scope UI can only be Jamf::UserGroups - No LDAP
59
- # - BUG: They do not appear in API data (XML or JSON) and are
60
- # NOT SUPPORTED in ruby-jss.
61
- # - You must use the Web UI to work with them in a Scope.
56
+ # Because the concept of 'scope' existed before Jamf Pro had 'Users'
57
+ # and 'User Groups' (Jamf::User and Jamf::UserGroup classes in ruby-jss)
58
+ # there is non-obvious inconsistency between the labels for API data, and
59
+ # the labels for that data in the web UI:
62
60
  #
63
- # Limitations
64
- # - 'LDAP/Local Users' can be any string
65
- # - The Web UI accepts any string, even if no matching Local or LDAP user.
66
- # - The data shows up in API data in scope=>limitations=>users
67
- # by name only (the string provided), no IDs
68
- # - 'LDAP User Groups' can only be LDAP groups that actually exist
69
- # - The Web UI won't let you add a group that doesn't exist in ldap
70
- # - The data shows up in API data in scope=>limitations=>user_groups
71
- # by name and LDAP ID (which may be empty)
72
- # - The data ALSO shows up in API data in scope=>limit_to_users=>user_groups
73
- # by name only, no LDAP IDs. ruby-jss ignores this and looks at
74
- # scope=>limitations=>user_groups
61
+ # === Users
75
62
  #
76
- # Exclusions, combines the behavior of Inclusions & Limitations
77
- # - 'Users' in the Scope UI can only be Jamf::Users - No LDAP
78
- # - BUG: They do not appear in API data (XML or JSON) and are
79
- # NOT SUPPORTED in ruby-jss.
80
- # - You must use the Web UI to work with them in a Scope.
81
- # - 'User Groups' in the Scope UI can only be Jamf::UserGroups - No LDAP
82
- # - BUG: They do not appear in API data (XML or JSON) and are
83
- # NOT SUPPORTED in ruby-jss.
84
- # - You must use the Web UI to work with them in a Scope.
85
- # - 'LDAP/Local Users' can be any string
86
- # - The Web UI accepts any string, even if no matching Local or LDAP user.
87
- # - The data shows up in API data in scope=>exclusions=>users
88
- # by name only (the string provided), no IDs
89
- # - 'LDAP User Groups' can only be LDAP groups that actually exist
90
- # - The Web UI won't let you add a group that doesn't exist in ldap
91
- # - The data shows up in API data in scope=>exclusions=>user_groups
92
- # by name and LDAP ID (which may be empty)
63
+ # What appears in the UI as 'Users' are User objects in Jamf pro, which in ruby-jss
64
+ # are Jamf::User instances.
93
65
  #
66
+ # These will appear in the API data as \<jss_users> element with \<user> sub-elements (XML)
67
+ # or the 'jss_users' array (JSON). These are available as Targets or Exclusions.
94
68
  #
95
- # How ruby-jss handles this:
69
+ # In this class, they are also referred to as 'jss_users'
96
70
  #
97
- # - Methods #set_targets and #add_target will not accept the keys
98
- # :user, :users, :user_group, :user_groups.
71
+ # === Directory Service/Local Users
99
72
  #
100
- # - Method #remove_target will ignore them.
73
+ # When editing a scope in the UI, in Limitations and Exclusions, you can
74
+ # add arbitrary strings that will be matched to the users assigned to machines,
75
+ # or that appear in any of the defined LDAP servers.
76
+ # These scope items are called 'Directory Service/Local Users' but
77
+ # used to be called 'LDAP/Local Users'
101
78
  #
102
- # - Methods #set_limitations, #add_limitation & #remove_limitation will accept:
103
- # - :user, :ldap_user, or :jamf_ldap_user (and their plurals) for working
104
- # with 'LDAP/Local Users'. When setting or adding, the provided
105
- # string(s) must exist as either a Jamf::User or an LDAP user
106
- # - :user_group or :ldap_user_group (and their plurals) for working with
107
- # 'LDAP User Groups'. When setting or adding, the provided string
108
- # must exist as a group in LDAP.
79
+ # In the API data for scopes, these items appear in the \<users> element
80
+ # with \<user> sub-elements (XML) or 'users' array (JSON) of the limitations
81
+ # and exclusions data
109
82
  #
110
- # - Methods #set_exclusions, #add_exclusion & #remove_exclusion will accept:
111
- # - :user, :ldap_user, or :jamf_ldap_user (and their plurals) for working
112
- # with 'LDAP/Local Users'. When setting or adding, the provided string(s)
113
- # must exist as either a Jamf::User or an LDAP user.
114
- # - :user_group or :ldap_user_group (and their plurals) for working with
115
- # 'LDAP User Groups''. When setting or adding, the provided string
116
- # must exist as a group in LDAP.
83
+ # In this class, these items ultimately use the same names they have in the API data:
84
+ # 'users' but when specifying that you are setting that value, you can use any of
85
+ # these synonyms, plural or singular:
86
+ # ldap_users, jamf_ldap_users, directory_service_local_users
117
87
  #
118
- # Internally in the Scope instance:
88
+ # === User Groups
119
89
  #
120
- # - The limitations and exclusions that match the WebUI's 'LDAP/Local Users'
121
- # are in @limitations[:jamf_ldap_users] and @exclusions[:jamf_ldap_users]
90
+ # What appears in the UI as 'User Groups' are User Group objects in Jamf Pro, both
91
+ # static and smart. In ruby-jss, these are Jamf::UserGroup instances.
122
92
  #
123
- # - The limitations and exclusions that match the WebUI's 'LDAP User Groups'
124
- # are in @limitations[:ldap_user_groups] and @exclusions[:ldap_user_groups]
93
+ # They will appear in the API data as \<jss_user_groups> element with \<user_group>
94
+ # sub-elements (XML) or the 'jss_user_groups' array (JSON). These are available as
95
+ # Targets or Exclusions.
125
96
  #
97
+ # In this class they are also referred to as 'jss_user_groups'
98
+ #
99
+ # === Directory Service User Groups
100
+ #
101
+ # When editing a scope in the UI, in Limitations and Exclusions, you can
102
+ # look up and add groups from any of the defined LDAP servers.
103
+ # These scope items are called 'Directory Service User Groups' but
104
+ # used to be called 'LDAP User Groups'
105
+ #
106
+ # In the API data for scopes, these items appear in the \<user_groups> element
107
+ # with \<user_group> sub-elements (XML) or 'user_groups' array (JSON) of the limitations
108
+ # and exclusions data
109
+ #
110
+ # In this class, these items ultimately use the same names they have in the API data:
111
+ # 'user_groups' but when specifying that you are setting that value, you can use any of
112
+ # these synonyms, singular or plural:
113
+ # ldap_user_groups, directory_service_user_groups
114
+ #
115
+ ###################################
116
+ # = IMPORTANT: API BUG IN POLICY AND PATCH POLICY SCOPES - CAN CAUSE DATA LOSS
117
+ ###################################
118
+ #
119
+ # When you GET the data for policies and patch policies from the Classic API
120
+ # the scope data returned will NOT include the 'jss_users' and 'jss_user_groups'
121
+ # data in the targets or the exclusions, even if they are defined in the web UI.
122
+ #
123
+ # More importantly, if you try to include those in the XML when you PUT a policy
124
+ # back to make a change via the API, you'll get an error because the API endpoint
125
+ # doesn't know what <jss_users> or <jss_user_groups> elements are.
126
+ #
127
+ # Even more importanly, since you cannot include those elements in your PUT body,
128
+ # if they actually exist in the scope, THEY WILL BE ERASED from the actual scope,
129
+ # because they weren't in the PUT data.
130
+ # This will always happen if you include the <scope> element in your
131
+ # PUT data, even if you didn't change the scope.
132
+ #
133
+ # - How ruby-jss handles this bug:
134
+ #
135
+ # Fortunately the Classic API, or at least this part of it, doesn't fully adhere
136
+ # to the REST standards for PUT, and if you don't include the <scope> element in
137
+ # the XML, the server will just ignore the scope entirely, and nothing will change.
138
+ #
139
+ # We make use of that here to allow for editing Policies without fear of erasing
140
+ # those parts of the scope. As long as you don't change anything about the scope,
141
+ # there will be no <scope> element in the XML sent with a PUT, and the scope is
142
+ # safe from harm.
143
+ #
144
+ # If you DO change the scope of a policy, this bug cannot be avoided, and you'll
145
+ # delete any "User"/jss_user and "User Groups/jss_user_groups" defined in the
146
+ # targets or exclusions.
147
+ #
148
+ # By default, if you try to change the scope of a Policy of PatchPolicy, you'll
149
+ # get a warning about the possibility of losing data when you save.
150
+ #
151
+ # You can supress those warnings either by supressing all ruby warnings, or
152
+ # by calling Jamf::Scopable::Scope.do_not_warn_about_policy_scope_bugs
153
+ #
154
+ ###################################
155
+ # = IMPORTANT: API BUG IN OSX CONFIG PROFILE SCOPES - CAN CAUSE DATA LOSS
156
+ ###################################
157
+ #
158
+ # When fetching the data for OSX Configuration Profiles using JSON (which ruby-jss does)
159
+ # and the scope of the profile contains more than one `jss_user_groups` as a target, then
160
+ # only the last one will be returned. If you have more than one such group as a target, and
161
+ # use ruby-jss to make changes to the scope, all but the last jss_user_groups used as targets will
162
+ # be removed.
163
+ #
164
+ # This only appears to affect scope targets, not exclusions, and only for
165
+ # OSX Config Profiles. Other scopable objects that use jss_user_groups in their API data
166
+ # seem to be OK.
167
+ #
168
+ # This is due to a long-standing API bug regarding how Arrays in XML are incorrectly
169
+ # translated into Hashes of a single Hash when returning the data as JSON - they shoud be
170
+ # Arrays of Hashes in JSON - one hash for each item.
171
+ #
172
+ # Even though this bug was first reported to jamf in 2009, it still appears in many places throughout
173
+ # the Classic API. ruby-jss works around some of the worst instances of the bug, but such workarounds
174
+ # are complex requiring re-fetching the data in XML and parsing it manually. At the moment there are no
175
+ # plans to do that for this specific scope bug.
176
+ #
177
+ # By default, if you try to change the scope of an object affected by this bug, you'll
178
+ # get a warning about the possibility of losing data when you save.
179
+ #
180
+ # You can supress those warnings either by supressing all ruby warnings, or
181
+ # by calling Jamf::Scopable::Scope.do_not_warn_about_array_hash_scope_bugs
182
+ #
183
+ ########################
126
184
  #
127
185
  # @see Jamf::Scopable
128
186
  #
@@ -133,38 +191,93 @@ module Jamf
133
191
 
134
192
  # These are the classes that Scopes can use for defining a scope,
135
193
  # keyed by appropriate symbols.
136
- # NOTE: All the user and group ones don't actually refer to
137
- # Jamf::User or Jamf::UserGroup. See IMPORTANT discussion above.
194
+ #
195
+ # synonyms, including singular/plural forms, are used to allow for
196
+ # more natural language when specifying these scope entities. The
197
+ # key used in the actual API data is usually the plural.
198
+ #
199
+ # NOTE: user[s] and user_group[s] in Scope data refer to
200
+ # 'Directory Service/Local User' and 'Directory Service User Group'
201
+ # as labeled in the web-ui. These were formerly labeled
202
+ # as 'LDAP/Local User' and 'LDAP User Group'.
203
+ #
138
204
  SCOPING_CLASSES = {
139
205
  computers: Jamf::Computer,
140
206
  computer: Jamf::Computer,
207
+
141
208
  computer_groups: Jamf::ComputerGroup,
142
209
  computer_group: Jamf::ComputerGroup,
210
+
143
211
  mobile_devices: Jamf::MobileDevice,
144
212
  mobile_device: Jamf::MobileDevice,
213
+
145
214
  mobile_device_groups: Jamf::MobileDeviceGroup,
146
215
  mobile_device_group: Jamf::MobileDeviceGroup,
216
+
147
217
  buildings: Jamf::Building,
148
218
  building: Jamf::Building,
219
+
149
220
  departments: Jamf::Department,
150
221
  department: Jamf::Department,
222
+
151
223
  network_segments: Jamf::NetworkSegment,
152
224
  network_segment: Jamf::NetworkSegment,
153
- ibeacon: Jamf::IBeacon,
225
+
154
226
  ibeacons: Jamf::IBeacon,
155
- user: nil,
227
+ ibeacon: Jamf::IBeacon,
228
+
229
+ jss_users: Jamf::User,
230
+ jss_user: Jamf::User,
231
+
232
+ jss_user_groups: Jamf::UserGroup,
233
+ jss_user_group: Jamf::UserGroup,
234
+
156
235
  users: nil,
157
- ldap_user: nil,
236
+ user: nil,
158
237
  ldap_users: nil,
159
- jamf_ldap_user: nil,
238
+ ldap_user: nil,
160
239
  jamf_ldap_users: nil,
161
- user_group: nil,
240
+ jamf_ldap_user: nil,
241
+ directory_service_local_users: nil,
242
+ directory_service_local_user: nil,
243
+
162
244
  user_groups: nil,
245
+ user_group: nil,
246
+ ldap_user_groups: nil,
163
247
  ldap_user_group: nil,
164
- ldap_user_groups: nil
248
+ directory_service_user_groups: nil,
249
+ directory_service_user_group: nil
165
250
  }.freeze
166
251
 
167
- # These keys always mean :jamf_ldap_users
252
+ # These classes are affected by the jss_users/jss_user_groups bug.
253
+ #
254
+ # They do not accept jss_users or jss_user_groups in their targets or
255
+ # exclusions, and editing their scope via the API will always delete those
256
+ # items from the scope if they exist.
257
+ #
258
+ # See discussion in the Scope class comments.
259
+ JAMF_DATA_LOSS_BUG_CLASSES = [
260
+ Jamf::Policy,
261
+ Jamf::PatchPolicy
262
+ ].freeze
263
+
264
+ # The classes affected by the jss_users/jss_user_groups bug do not
265
+ # include these items in their Target or Exclusion API data, even if
266
+ # the scope has such items defined in the JSS
267
+ #
268
+ # See discussion in the Scope class comments.
269
+ JAMF_DATA_LOSS_BUG_KEYS = %i[jss_users jss_user_groups].freeze
270
+
271
+ # In the API data for limitations and exclusions
272
+ # 'users' is what appears as Directory Service/Local Users in the web UI
273
+ # and 'user_groups' appears as 'Directory Service User Groups'.
274
+ #
275
+ # Contrasted with 'jss_users' and 'jss_user_groups' in the API data for
276
+ # targets and exlcusions, which are Jamf::User and Jamf::UserGroup objects.
277
+ #
278
+ LDAP_BASED_KEYS = %i[users user_groups].freeze
279
+
280
+ # These keys always mean :users
168
281
  LDAP_JAMF_USER_KEYS = %i[
169
282
  user
170
283
  users
@@ -172,14 +285,18 @@ module Jamf
172
285
  ldap_users
173
286
  jamf_ldap_user
174
287
  jamf_ldap_users
288
+ directory_service_local_user
289
+ directory_service_local_users
175
290
  ].freeze
176
291
 
177
- # These keys always mean :ldap_user_groups
292
+ # These keys always mean :user_groups
178
293
  LDAP_GROUP_KEYS = %i[
179
294
  user_group
180
295
  user_groups
181
296
  ldap_user_group
182
297
  ldap_user_groups
298
+ directory_service_user_group
299
+ directory_service_user_groups
183
300
  ].freeze
184
301
 
185
302
  # This hash maps the availble Scope Target keys from SCOPING_CLASSES to
@@ -189,9 +306,12 @@ module Jamf
189
306
  # added to the ends of singular key names if needed, e.g. computer_group => computer_groups
190
307
  ESS = 's'.freeze
191
308
 
192
- # These can be part of the base inclusion list of the scope,
309
+ # These can be part of the base target list of the scope,
193
310
  # along with the appropriate target and target group keys
194
- INCLUSIONS = %i[buildings departments].freeze
311
+ TARGETS = %i[buildings departments jss_users jss_user_groups].freeze
312
+
313
+ # Backward Compatibility
314
+ INCLUSIONS = TARGETS
195
315
 
196
316
  # These can limit the inclusion list
197
317
  # These are the keys that come from the API
@@ -201,21 +321,47 @@ module Jamf
201
321
  LIMITATIONS = %i[
202
322
  ibeacons
203
323
  network_segments
204
- jamf_ldap_users
205
- ldap_user_groups
324
+ users
325
+ user_groups
206
326
  ].freeze
207
327
 
208
328
  # any of them can be excluded
209
- EXCLUSIONS = INCLUSIONS + LIMITATIONS
329
+ EXCLUSIONS = TARGETS + LIMITATIONS
210
330
 
211
331
  # Here's a default scope as it might come from the API.
212
332
  DEFAULT_SCOPE = {
213
- all_computers: true,
214
- all_mobile_devices: true,
333
+ all_computers: false,
334
+ all_mobile_devices: false,
215
335
  limitations: {},
216
336
  exclusions: {}
217
337
  }.freeze
218
338
 
339
+ # Class Methods
340
+ ######################
341
+
342
+ # call this to suppress warnings about data loss bug in
343
+ # Policy and Patch Policy scopes
344
+ def self.do_not_warn_about_policy_scope_bugs
345
+ @do_not_warn_about_policy_scope_bugs = true
346
+ end
347
+
348
+ # Has do_not_warn_about_policy_scope_bugs been set?
349
+ def self.do_not_warn_about_policy_scope_bugs?
350
+ @do_not_warn_about_policy_scope_bugs
351
+ end
352
+
353
+ # call this to suppress warnings about data loss bug in
354
+ # OSXConfigurationProfile scopes when there are jss_user_groups
355
+ # used as targets
356
+ def self.do_not_warn_about_array_hash_scope_bugs
357
+ @do_not_warn_about_array_hash_scope_bugs = true
358
+ end
359
+
360
+ # Has do_not_warn_about_policy_scope_bugs been set?
361
+ def self.do_not_warn_about_array_hash_scope_bugs?
362
+ @do_not_warn_about_array_hash_scope_bugs
363
+ end
364
+
219
365
  # Attributes
220
366
  ######################
221
367
 
@@ -257,6 +403,9 @@ module Jamf
257
403
  # - :group_targets - a synonym for :computer_groups or :mobile_device_groups
258
404
  # - :departments
259
405
  # - :buildings
406
+ # - :jss_users
407
+ # - :jss_user_groups
408
+ #
260
409
  # and the values are Arrays of names of those things.
261
410
  #
262
411
  # @return [Hash{Symbol: Array<Integer>}]
@@ -268,8 +417,9 @@ module Jamf
268
417
  #
269
418
  # The arrays of ids are:
270
419
  # - :network_segments
271
- # - :jamf_ldap_users
420
+ # - :users
272
421
  # - :user_groups
422
+ # - :ibeacons
273
423
  #
274
424
  # @return [Hash{Symbol: Array<Integer, String>}]
275
425
  attr_reader :limitations
@@ -284,37 +434,64 @@ module Jamf
284
434
  # - :departments
285
435
  # - :buildings
286
436
  # - :network_segments
437
+ # - :jss_users
438
+ # - :jss_user_groups
287
439
  # - :users
288
- # - :user_groups
289
- #
440
+ # - :user_groups #
290
441
  # @return [Hash{Symbol: Array<Integer, String>}]
291
442
  attr_reader :exclusions
292
443
 
444
+ # @return [Boolean] Have changes been made to the scope, that need
445
+ # to be sent to the server?
446
+ attr_accessor :should_update
447
+ alias should_update? should_update
448
+
293
449
  # Public Instance Methods
294
450
  #####################################
295
451
 
296
- # If raw_scope is empty, a default scope, scoped to all targets, is created, and can be modified
452
+ # If raw_scope is empty, a default scope, scoped to no targets, is created, and can be modified
297
453
  # as needed.
298
454
  #
299
- # @param target_key[Symbol] the kind of thing we're scoping, one of {TARGETS_AND_GROUPS}
455
+ # @param target_key[Symbol] the kind of thing we're scoping, a key from {TARGETS_AND_GROUPS}
300
456
  #
301
457
  # @param raw_scope[Hash] the JSON :scope data from an API query that is scopable, e.g. a Policy.
302
458
  #
303
- def initialize(target_key, raw_scope = nil)
459
+ # @param container[Jamf::APIObject] The scopable object to which this scope belongs, e,g, an
460
+ # instance of Jamf::Policy, Jamf::MobileDeviceApplication, etc.. If not provided, will be
461
+ # set automatically after initialization
462
+ #
463
+ ###########################
464
+ def initialize(target_key, raw_scope = nil, container: nil)
304
465
  raw_scope ||= DEFAULT_SCOPE.dup
305
466
  unless TARGETS_AND_GROUPS.key?(target_key)
306
467
  raise Jamf::InvalidDataError, "The target class of a Scope must be one of the symbols :#{TARGETS_AND_GROUPS.keys.join(', :')}"
307
468
  end
308
469
 
470
+ @should_update = false
471
+ @container = container
472
+
309
473
  @target_key = target_key
310
474
  @target_class = SCOPING_CLASSES[@target_key]
311
475
  @group_key = TARGETS_AND_GROUPS[@target_key]
312
476
  @group_class = SCOPING_CLASSES[@group_key]
313
477
 
314
- @target_keys = [@target_key, @group_key] + INCLUSIONS
478
+ @target_keys = [@target_key, @group_key] + TARGETS
315
479
  @exclusion_keys = [@target_key, @group_key] + EXCLUSIONS
316
480
 
317
- @all_key = "all_#{target_key}".to_sym
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 # init
490
+
491
+ # parse the targets from the init data
492
+ ###########################
493
+ def parse_targets(raw_scope)
494
+ @all_key = "all_#{@target_key}".to_sym
318
495
  @all_targets = raw_scope[@all_key]
319
496
 
320
497
  # Everything gets mapped from an Array of Hashes to
@@ -322,73 +499,64 @@ module Jamf
322
499
  @targets = {}
323
500
  @target_keys.each do |k|
324
501
  raw_scope[k] ||= []
325
- @targets[k] = raw_scope[k].compact.map { |n| n[:id].to_i }
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
326
518
  @targets[:direct_targets] = @targets[k] if k == @target_key
327
519
  @targets[:group_targets] = @targets[k] if k == @group_key
328
520
  end # @target_keys.each do |k|
521
+ end
522
+ private :parse_targets
329
523
 
330
- # the :users key from the API is what we call :jamf_ldap_users
331
- # and the :user_groups key from the API we call :ldap_user_groups
332
- # See the IMPORTANT discussion above.
524
+ # parse the limitations from the init data
525
+ ###########################
526
+ def parse_limitations(raw_scope)
333
527
  @limitations = {}
334
- if raw_scope[:limitations]
335
-
336
- LIMITATIONS.each do |k|
337
- # :jamf_ldap_users comes from :users in the API data
338
- if k == :jamf_ldap_users
339
- api_data = raw_scope[:limitations][:users]
340
- api_data ||= []
341
- @limitations[k] = api_data.compact.map { |n| n[:name].to_s }
342
-
343
- # :ldap_user_groups comes from :user_groups in the API data
344
- elsif k == :ldap_user_groups
345
- api_data = raw_scope[:limitations][:user_groups]
346
- api_data ||= []
347
- @limitations[k] = api_data.compact.map { |n| n[:name].to_s }
348
-
349
- # others handled normally.
350
- else
351
- api_data = raw_scope[:limitations][k]
352
- api_data ||= []
353
- @limitations[k] = api_data.compact.map { |n| n[:id].to_i }
354
- end
355
- end # LIMITATIONS.each do |k|
356
- end # if raw_scope[:limitations]
528
+ return unless raw_scope[:limitations]
357
529
 
358
- # the :users key from the API is what we call :jamf_ldap_users
359
- # and the :user_groups key from the API we call :ldap_user_groups
360
- # See the IMPORTANT discussion above.
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
538
+ private :parse_limitations
539
+
540
+ # parse the limitations from the init data
541
+ ###########################
542
+ def parse_exclusions(raw_scope)
361
543
  @exclusions = {}
362
- if raw_scope[:exclusions]
363
-
364
- @exclusion_keys.each do |k|
365
- # :jamf_ldap_users comes from :users in the API data
366
- if k == :jamf_ldap_users
367
- api_data = raw_scope[:exclusions][:users]
368
- api_data ||= []
369
- @exclusions[k] = api_data.compact.map { |n| n[:name].to_s }
370
-
371
- # :ldap_user_groups comes from :user_groups in the API data
372
- elsif k == :ldap_user_groups
373
- api_data = raw_scope[:exclusions][:user_groups]
374
- api_data ||= []
375
- @exclusions[k] = api_data.compact.map { |n| n[:name].to_s }
376
-
377
- # others handled normally.
378
- else
379
- api_data = raw_scope[:exclusions][k]
380
- api_data ||= []
381
- @exclusions[k] = api_data.compact.map { |n| n[:id].to_i }
382
- @exclusions[:direct_exclusions] = @exclusions[k] if k == @target_key
383
- @exclusions[:group_exclusions] = @exclusions[k] if k == @group_key
384
- end # if ...elsif... else
385
- end # @exclusion_keys.each
386
- end # if raw_scope[:exclusions]
387
-
388
- @container = nil
389
- end # init
544
+ return unless raw_scope[:exclusions]
390
545
 
391
- # Set the scope's inclusions to all targets.
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
557
+ private :parse_exclusions
558
+
559
+ # Set the scope's targets to all.
392
560
  #
393
561
  # By default, the limitations and exclusions remain.
394
562
  # If a non-false parameter is provided, they will be removed also.
@@ -408,7 +576,7 @@ module Jamf
408
576
  @exclusions = {}
409
577
  @exclusion_keys.each { |k| @exclusions[k] = [] }
410
578
  end
411
- @container&.should_update
579
+ note_pending_changes
412
580
  end
413
581
  alias include_all set_all_targets
414
582
 
@@ -455,7 +623,7 @@ module Jamf
455
623
 
456
624
  @targets[key] = list
457
625
  @all_targets = false
458
- @container&.should_update
626
+ note_pending_changes
459
627
  end # sinclude_in_scope
460
628
  alias set_target set_targets
461
629
  alias set_inclusion set_targets
@@ -472,7 +640,7 @@ module Jamf
472
640
  set_all_targets
473
641
  else
474
642
  @all_targets = false
475
- @container&.should_update
643
+ note_pending_changes
476
644
  end
477
645
  end
478
646
 
@@ -511,7 +679,7 @@ module Jamf
511
679
 
512
680
  @targets[key] << item_id
513
681
  @all_targets = false
514
- @container&.should_update
682
+ note_pending_changes
515
683
  end
516
684
  alias add_inclusion add_target
517
685
 
@@ -533,7 +701,7 @@ module Jamf
533
701
  return unless @targets[key]&.include?(item_id)
534
702
 
535
703
  @targets[key].delete item_id
536
- @container&.should_update
704
+ note_pending_changes
537
705
  end
538
706
  alias remove_inclusion remove_target
539
707
 
@@ -553,7 +721,7 @@ module Jamf
553
721
  #
554
722
  # @todo handle ldap user group lookups
555
723
  #
556
- def set_limitation(key, list)
724
+ def set_limitations(key, list)
557
725
  key = pluralize_key(key)
558
726
  raise Jamf::InvalidDataError, "List must be an Array of #{key} identifiers, it may be empty." unless list.is_a? Array
559
727
 
@@ -570,9 +738,9 @@ module Jamf
570
738
  return nil if list.sort == @limitations[key].sort
571
739
 
572
740
  @limitations[key] = list
573
- @container&.should_update
741
+ note_pending_changes
574
742
  end # set_limitation
575
- alias set_limitations set_limitation
743
+ alias set_limitation set_limitations
576
744
 
577
745
  # Add a single item for limiting this scope.
578
746
  #
@@ -599,7 +767,7 @@ module Jamf
599
767
  end
600
768
 
601
769
  @limitations[key] << item_id
602
- @container&.should_update
770
+ note_pending_changes
603
771
  end
604
772
 
605
773
  # Remove a single item for limiting this scope.
@@ -622,7 +790,7 @@ module Jamf
622
790
  return unless @limitations[key]&.include?(item_id)
623
791
 
624
792
  @limitations[key].delete item_id
625
- @container&.should_update
793
+ note_pending_changes
626
794
  end ###
627
795
 
628
796
  # Replace an exclusion list for this scope
@@ -639,7 +807,7 @@ module Jamf
639
807
  #
640
808
  # @return [void]
641
809
  #
642
- def set_exclusion(key, list)
810
+ def set_exclusions(key, list)
643
811
  key = pluralize_key(key)
644
812
  raise Jamf::InvalidDataError, "List must be an Array of #{key} identifiers, it may be empty." unless list.is_a? Array
645
813
 
@@ -662,8 +830,9 @@ module Jamf
662
830
  return nil if list.sort == @exclusions[key].sort
663
831
 
664
832
  @exclusions[key] = list
665
- @container&.should_update
666
- end # limit scope
833
+ note_pending_changes
834
+ end # set_exclusion
835
+ alias set_exclusion set_exclusions
667
836
 
668
837
  # Add a single item for exclusions of this scope.
669
838
  #
@@ -688,7 +857,7 @@ module Jamf
688
857
  raise Jamf::AlreadyExistsError, "Can't exclude #{key} '#{item}' because it's already an explicit limitation." if @limitations[key]&.include?(item)
689
858
 
690
859
  @exclusions[key] << item_id
691
- @container&.should_update
860
+ note_pending_changes
692
861
  end
693
862
 
694
863
  # Remove a single item for exclusions of this scope
@@ -708,7 +877,7 @@ module Jamf
708
877
  return unless @exclusions[key]&.include?(item_id)
709
878
 
710
879
  @exclusions[key].delete item_id
711
- @container&.should_update
880
+ note_pending_changes
712
881
  end
713
882
 
714
883
  # @api private
@@ -726,20 +895,24 @@ module Jamf
726
895
  list.compact!
727
896
  list.delete 0
728
897
  list_as_hashes = list.map { |i| { id: i } }
729
- scope << SCOPING_CLASSES[klass].xml_list(list_as_hashes, :id)
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
730
903
  end
731
904
 
732
905
  limitations = scope.add_element('limitations')
733
906
  @limitations.each do |klass, list|
734
907
  list.compact!
735
908
  list.delete 0
736
- if klass == :jamf_ldap_users
909
+ if klass == :users
737
910
  users_xml = limitations.add_element 'users'
738
911
  list.each do |name|
739
912
  user_xml = users_xml.add_element 'user'
740
913
  user_xml.add_element('name').text = name
741
914
  end
742
- elsif klass == :ldap_user_groups
915
+ elsif klass == :user_groups
743
916
  user_groups_xml = limitations.add_element 'user_groups'
744
917
  list.each do |name|
745
918
  user_group_xml = user_groups_xml.add_element 'user_group'
@@ -756,13 +929,13 @@ module Jamf
756
929
  list = @exclusions[klass]
757
930
  list.compact!
758
931
  list.delete 0
759
- if klass == :jamf_ldap_users
932
+ if klass == :users
760
933
  users_xml = exclusions.add_element 'users'
761
934
  list.each do |name|
762
935
  user_xml = users_xml.add_element 'user'
763
936
  user_xml.add_element('name').text = name
764
937
  end
765
- elsif klass == :ldap_user_groups
938
+ elsif klass == :user_groups
766
939
  user_groups_xml = exclusions.add_element 'user_groups'
767
940
  list.each do |name|
768
941
  user_group_xml = user_groups_xml.add_element 'user_group'
@@ -770,13 +943,18 @@ module Jamf
770
943
  end
771
944
  else
772
945
  list_as_hashes = list.map { |i| { id: i } }
773
- exclusions << SCOPING_CLASSES[klass].xml_list(list_as_hashes, :id)
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
+
774
952
  end
775
953
  end
776
954
  scope
777
955
  end # scope_xml
778
956
 
779
- # Remove the init_data and api object from
957
+ # Remove large or redundant data structures from
780
958
  # the instance_variables used to create
781
959
  # pretty-print (pp) output.
782
960
  #
@@ -869,28 +1047,41 @@ module Jamf
869
1047
  #
870
1048
  def validate_item(realm, key, ident, error_if_not_found: true)
871
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.
872
1052
  possible_keys =
873
1053
  case realm
874
- when :target then @target_keys
875
- when :limitation then LIMITATIONS
876
- when :exclusion then @exclusion_keys
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
877
1060
  else
878
1061
  raise ArgumentError, 'Unknown realm, must be :target, :limitation, or :exclusion'
879
1062
  end
880
1063
 
881
1064
  key = pluralize_key(key)
882
1065
 
883
- raise Jamf::InvalidDataError, "#{realm} key must be one of :#{possible_keys.join(', :')}" \
884
- unless possible_keys.include? key
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
885
1073
 
886
1074
  id = nil
887
1075
 
888
1076
  # id will be a string
889
- if key == :jamf_ldap_users
890
- id = ident if Jamf::User.all_names(:refresh, cnx: container.cnx).include?(ident) || Jamf::LdapServer.user_in_ldap?(ident)
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)
891
1081
 
892
1082
  # id will be a string
893
- elsif key == :ldap_user_groups
1083
+ elsif key == :user_groups
1084
+ # The web UI does validate that the group exists in LDAP
894
1085
  id = ident if Jamf::LdapServer.group_in_ldap? ident, cnx: container.cnx
895
1086
 
896
1087
  # id will be an integer
@@ -905,11 +1096,12 @@ module Jamf
905
1096
 
906
1097
  # the symbols used in the API data are plural, e.g. 'network_segments'
907
1098
  # this will pluralize them, allowing us to use singulars as well.
1099
+ # This also handles the synonyms for users and user_groups
908
1100
  def pluralize_key(key)
909
1101
  if LDAP_JAMF_USER_KEYS.include? key
910
- :jamf_ldap_users
1102
+ :users
911
1103
  elsif LDAP_GROUP_KEYS.include? key
912
- :ldap_user_groups
1104
+ :user_groups
913
1105
  else
914
1106
  key.to_s.end_with?(ESS) ? key : "#{key}s".to_sym
915
1107
  end
@@ -1161,8 +1353,39 @@ module Jamf
1161
1353
  false
1162
1354
  end
1163
1355
 
1356
+ # make a note both in this instance and in our container
1357
+ # that a change has been made and an update is needed
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
1364
+
1365
+ # display data loss warning.
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
1375
+
1376
+ # display warning about array-hash bug data loss
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
1386
+
1164
1387
  end # class Scope
1165
1388
 
1166
1389
  end # module Scopable
1167
1390
 
1168
- end # module
1391
+ end # moduleß
@@ -75,8 +75,8 @@ module Jamf
75
75
  ### @return [void]
76
76
  ###
77
77
  def parse_scope
78
- @scope = Jamf::Scopable::Scope.new self.class::SCOPE_TARGET_KEY, @init_data[:scope]
79
- @scope.container = self
78
+ @scope = Jamf::Scopable::Scope.new self.class::SCOPE_TARGET_KEY, @init_data[:scope], container: self
79
+ @scope.container ||= self
80
80
  end
81
81
 
82
82
  ### Change the scope
@@ -85,9 +85,14 @@ module Jamf
85
85
  ###
86
86
  ### @return [void]
87
87
  ###
88
- def scope= (new_scope)
89
- raise Jamf::InvalidDataError, "Jamf::Scopable::Scope instance required" unless new_criteria.kind_of?(Jamf::Scopable::Scope)
90
- raise Jamf::InvalidDataError, "Scope object must have target_key of :#{self.class::SCOPE_TARGET_KEY}" unless self.class::SCOPE_TARGET_KEY == new_scope.target_key
88
+ def scope=(new_scope)
89
+ raise Jamf::InvalidDataError, 'Jamf::Scopable::Scope instance required' unless new_criteria.is_a?(Jamf::Scopable::Scope)
90
+
91
+ unless self.class::SCOPE_TARGET_KEY == new_scope.target_key
92
+ raise Jamf::InvalidDataError,
93
+ "Scope object must have target_key of :#{self.class::SCOPE_TARGET_KEY}"
94
+ end
95
+
91
96
  @scope = new_scope
92
97
  @need_to_update = true
93
98
  end
@@ -105,13 +110,13 @@ module Jamf
105
110
  #
106
111
  def update
107
112
  super
108
- rescue Jamf::ConflictError => conflict
109
- if scope.unable_to_verify_ldap_entries == true
110
- raise Jamf::InvalidDataError, "Potentially non-existant LDAP user or group in new scope values."
111
- else
112
- raise conflict
113
- end
113
+ @scope.should_update = false
114
+ rescue Jamf::ConflictError => e
115
+ raise Jamf::InvalidDataError, 'Potentially non-existant LDAP user or group in new scope values.' if scope.unable_to_verify_ldap_entries == true
116
+
117
+ raise e
114
118
  end # update
115
119
 
116
120
  end # module Scopable
121
+
117
122
  end # module Jamf
@@ -222,9 +222,8 @@ module Jamf
222
222
  # @return [Jamf::DeviceEnrollmentDevice, nil] the device as known to DEP
223
223
  #
224
224
  def self.device(sn, instance = nil, refresh: false, cnx: Jamf.cnx)
225
- sn.upcase! # SNs from apple are always uppercase
226
225
  devs = devices(instance, refresh: refresh, cnx: cnx)
227
- devs.select { |d| d.serialNumber == sn }.first
226
+ devs.select { |d| d.serialNumber.casecmp? sn }.first
228
227
  end
229
228
 
230
229
  # The history of sync operations between Apple and a given DeviceEnrollment
data/lib/jamf/version.rb CHANGED
@@ -27,6 +27,6 @@
27
27
  module Jamf
28
28
 
29
29
  ### The version of ruby-jss
30
- VERSION = '3.1.0b2'.freeze
30
+ VERSION = '3.2.0b3'.freeze
31
31
 
32
32
  end # module
data/test/tests/policy.rb CHANGED
@@ -38,20 +38,6 @@ module JamfTest
38
38
  def run_class_tests
39
39
  # policies are special so we define all the object tests here
40
40
  run_collection_tests do_object_tests: false
41
-
42
- create_new
43
-
44
- add_data_to_new
45
-
46
- # save_new
47
- # fetch_new
48
- # validate_fetched
49
- # modify_fetched
50
- # re_save_fetched
51
- # re_fetch
52
- # validate_changes
53
- # delete
54
- # confirm_deleted
55
41
  end
56
42
 
57
43
  #################
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-jss
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0b2
4
+ version: 3.2.0b3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Lasell
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2023-06-02 00:00:00.000000000 Z
13
+ date: 2023-08-08 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: CFPropertyList