ruby-jss 1.2.4a4 → 1.2.6

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of ruby-jss might be problematic. Click here for more details.

@@ -30,6 +30,8 @@ module Jamf
30
30
  # A token used for a JSS connection
31
31
  class Token
32
32
 
33
+ JAMF_VERSION_RSRC = 'v1/jamf-pro-version'.freeze
34
+
33
35
  AUTH_RSRC = 'auth'.freeze
34
36
 
35
37
  NEW_TOKEN_RSRC = "#{AUTH_RSRC}/tokens".freeze
@@ -56,9 +58,22 @@ module Jamf
56
58
  # @return [URI] The base API url, e.g. https://myjamf.jamfcloud.com/uapi
57
59
  attr_reader :base_url
58
60
 
59
- # when was this token created?
61
+ # @return [Jamf::Timestamp] when was this token created?
60
62
  attr_reader :login_time
61
63
 
64
+ # What happened the last time we tried to refresh?
65
+ # :expired_refreshed - token was expired, a new token was created with the pw
66
+ # :expired_pw_failed - token was expired, pw failed to make a new token
67
+ # :expired_no_pw - token was expired, but no pw was given to make a new one
68
+ # :refreshed - the token refresh worked with no need for the pw
69
+ # :refresh_failed - the token refresh failed, and no pw was given to make a new one
70
+ # :refreshed_with_pw - the token refresh failed, pw worked to make a new token
71
+ # :refresh_failed_no_pw - the token refresh failed, pw also failed to make a new token
72
+ # nil - no refresh has been attempted for this token.
73
+ #
74
+ # @return [Symbol, nil] :refreshed, :pw, :expired,:failed, or nil if never refreshed
75
+ attr_reader :last_refresh_result
76
+
62
77
  def initialize(**params)
63
78
  @valid = false
64
79
  @user = params[:user]
@@ -90,7 +105,7 @@ module Jamf
90
105
  raise Jamf::AuthenticationError, 'Incorrect name or password'
91
106
  else
92
107
  # TODO: better error reporting here
93
- raise 'An error occurred while authenticating'
108
+ raise Jamf::AuthenticationError, 'An error occurred while authenticating'
94
109
  end
95
110
  end # init_from_pw
96
111
 
@@ -118,8 +133,13 @@ module Jamf
118
133
  end
119
134
 
120
135
  # @return [String]
121
- def api_version
122
- token_connection(Jamf::Connection::SLASH, token: @auth_token).get.body[:version]
136
+ def jamf_version
137
+ raw_jamf_version.split('-').first
138
+ end
139
+
140
+ # @return [String]
141
+ def jamf_build
142
+ raw_jamf_version.split('-').last
123
143
  end
124
144
 
125
145
  # @return [Boolean]
@@ -165,18 +185,39 @@ module Jamf
165
185
  @account = Jamf::APIAccount.new resp.body
166
186
  end
167
187
 
168
- # Use this token to get a fresh one
169
- # TODO: better error reporting
170
- def refresh
171
- raise 'Token has expired' if expired?
188
+ # Use this token to get a fresh one. If a pw is provided
189
+ # try to use it to get a new token if a proper refresh fails.
190
+ #
191
+ # @param pw [String] Optional password to use if token refresh fails.
192
+ # Must be the correct passwd or the token's user (obviously)
193
+ #
194
+ # @return [Jamf::Timestamp] the new expiration time
195
+ #
196
+ def refresh(pw = nil)
197
+ # gotta have a pw if expired
198
+ if expired?
199
+ # try the passwd
200
+ return refresh_with_passwd(pw, :expired_refreshed, :expired_pw_failed) if pw
201
+
202
+ # no passwd? no chance!
203
+ @last_refresh_result = :expired_no_pw
204
+ raise Jamf::InvalidTokenError, 'Token has expired'
205
+ end
172
206
 
207
+ # Now try a normal refresh of our non-expired token
173
208
  keep_alive_token_resp = token_connection(KEEP_ALIVE_RSRC, token: @auth_token).post
209
+ if keep_alive_token_resp.success?
210
+ parse_token_from_response keep_alive_token_resp
211
+ @last_refresh_result = :refreshed
212
+ return expires
213
+ end
174
214
 
175
- raise 'An error occurred while authenticating' unless keep_alive_token_resp.success?
215
+ # if we're here, the normal refresh failed, so try the pw
216
+ return refresh_with_passwd(pw, :refreshed_with_pw, :refresh_failed_no_pw) if pw
176
217
 
177
- parse_token_from_response keep_alive_token_resp
178
- # parse_token_from_response keep_alive_rsrc.post('')
179
- expires
218
+ # if we're here, no pw? no chance!
219
+ @last_refresh_result = :refresh_failed
220
+ raise 'An error occurred while refreshing the token' unless pw
180
221
  end
181
222
  alias keep_alive refresh
182
223
 
@@ -190,6 +231,32 @@ module Jamf
190
231
  #################################
191
232
  private
192
233
 
234
+ # refresh a token using a password, return a result
235
+ # @param pw[String] the password to use
236
+ # @return [JamfTimestamp] the new expiration
237
+ def refresh_with_passwd(pw, success, failure)
238
+ init_from_pw(pw)
239
+ @last_refresh_result = success
240
+ expires
241
+ rescue => e
242
+ @last_refresh_result = failure
243
+ raise e, "#{e}. Status: :#{failure}"
244
+ end
245
+
246
+ # @return [String]
247
+ def raw_jamf_version
248
+ # TODO: Remove this once we require Jamf Pro 10.19 and up
249
+ # the rsrc for getting the version used to be nothing (the
250
+ # base url itself returnedit) but now its JAMF_VERSION_RSRC
251
+ resp = token_connection(Jamf::BLANK, token: @auth_token).get # .body # [:version]
252
+ return resp.body[:version] if resp.success?
253
+
254
+ resp = token_connection(JAMF_VERSION_RSRC, token: @auth_token).get
255
+ return resp.body[:version] if resp.success?
256
+
257
+ raise Jamf::InvalidConnectionError, 'Unable to read Jamf version from the API'
258
+ end
259
+
193
260
  # a generic, one-time Faraday connection for token
194
261
  # acquision & manipulation
195
262
  #
@@ -224,7 +224,7 @@ module Jamf
224
224
  extensionAttributes: {
225
225
  class: Jamf::InventoryPreloadExtensionAttribute,
226
226
  multi: true,
227
- aliases: %i[ext_attrs eas]
227
+ aliases: %i[eas]
228
228
  }
229
229
 
230
230
  }.freeze
@@ -251,6 +251,13 @@ module Jamf
251
251
  extensionAttributes_delete_at idx if idx
252
252
  end
253
253
 
254
+ # a Hash of ea name => ea_value for all eas currently set.
255
+ def ext_attrs
256
+ eas = {}
257
+ extensionAttributes.each { |ea| eas[ea.name] = ea.value }
258
+ eas
259
+ end
260
+
254
261
  # clear all values for this record except id, serialNumber, and deviceType
255
262
  def clear
256
263
  OBJECT_MODEL.keys.each do |attr|
@@ -82,6 +82,11 @@ module Jamf
82
82
  #
83
83
  class AuthenticationError < RuntimeError; end
84
84
 
85
+ # InvalidTokenError - raise this when a connection token is
86
+ # expired or otherwise invalid
87
+ #
88
+ class InvalidTokenError < RuntimeError; end
89
+
85
90
  # ConflictError - raise this when
86
91
  # attempts to PUT or PUSH to the API
87
92
  # result in a 409 Conflict http error.
@@ -27,6 +27,6 @@
27
27
  module Jamf
28
28
 
29
29
  ### The version of the Jamf module
30
- VERSION = '0.0.0a3'.freeze
30
+ VERSION = '0.0.1'.freeze
31
31
 
32
32
  end # module
data/lib/jss.rb CHANGED
@@ -194,8 +194,9 @@ module JSS
194
194
  class Category < JSS::APIObject; end
195
195
  class Computer < JSS::APIObject; end
196
196
  class Department < JSS::APIObject; end
197
- class EBook < JSS::APIObject; end
198
197
  class DistributionPoint < JSS::APIObject; end
198
+ class EBook < JSS::APIObject; end
199
+ class IBeacon < JSS::APIObject; end
199
200
  class LDAPServer < JSS::APIObject; end
200
201
  class MacApplication < JSS::APIObject; end
201
202
  class MobileDevice < JSS::APIObject; end
@@ -152,21 +152,26 @@ module JSS
152
152
  # @return [void]
153
153
  #
154
154
  def parse_criteria
155
- @criteria = (JSS::Criteriable::Criteria.new @init_data[:criteria].map { |c| JSS::Criteriable::Criterion.new c } if @init_data[:criteria])
156
- @criteria.container = self if @criteria
155
+ @criteria = JSS::Criteriable::Criteria.new
156
+ @criteria.criteria = @init_data[:criteria].map { |c| JSS::Criteriable::Criterion.new c } if @init_data[:criteria]
157
+
158
+ @criteria.container = self
157
159
  end
158
160
 
159
161
  #
160
162
  # Change the criteria, it must be a JSS::Criteriable::Criteria instance
161
163
  #
162
- # @param new_criteria[JSS::Criteriable::Criteria] the new criteria
164
+ # @param new_criteria[JSS::Criteriable::Criteria, nil] the new criteria. An
165
+ # empty criteria object is used if nil is passed.
163
166
  #
164
167
  # @return [void]
165
168
  #
166
169
  def criteria=(new_criteria)
170
+ new_criteria ||= JSS::Criteriable::Criteria.new
167
171
  raise JSS::InvalidDataError, 'JSS::Criteriable::Criteria instance required' unless new_criteria.is_a?(JSS::Criteriable::Criteria)
172
+
168
173
  @criteria = new_criteria
169
- @criteria.container = self
174
+ @criteria.container = self unless new_criteria.nil?
170
175
  @need_to_update = true
171
176
  end
172
177
 
@@ -72,21 +72,18 @@ module JSS
72
72
  attr_reader :criteria
73
73
 
74
74
  ### @return [JSS::APIObject subclass] a reference to the object containing these Criteria
75
- attr_reader :container
75
+ attr_writer :container
76
76
 
77
77
  ###
78
78
  ### @param new_criteria[Array<JSS::Criteriable::Criterion>]
79
79
  ###
80
- def initialize(new_criteria)
80
+ def initialize(new_criteria = [])
81
81
  @criteria = []
82
+
83
+ # validates the param and fills @criteria
82
84
  self.criteria = new_criteria
83
85
  end # init
84
86
 
85
- ### set the object we belong to, so we can set its @should_update value
86
- def container= (a_thing)
87
- @container = a_thing
88
- end
89
-
90
87
  ###
91
88
  ### Provide a whole new array of JSS::Criteriable::Criterion instances for this Criteria
92
89
  ###
@@ -94,7 +91,7 @@ module JSS
94
91
  ###
95
92
  ### @return [void]
96
93
  ###
97
- def criteria= (new_criteria)
94
+ def criteria=(new_criteria)
98
95
  unless new_criteria.is_a?(Array) && new_criteria.reject { |c| c.is_a?(JSS::Criteriable::Criterion) }.empty?
99
96
  raise JSS::InvalidDataError, 'Argument must be an Array of JSS::Criteriable::Criterion instances.'
100
97
  end
@@ -104,6 +101,14 @@ module JSS
104
101
  @container.should_update if @container
105
102
  end
106
103
 
104
+ # Remove all criterion objects
105
+ #
106
+ # @return [void]
107
+ def clear
108
+ @criteria = []
109
+ @container.should_update if @container
110
+ end
111
+
107
112
  ###
108
113
  ### Add a new criterion to the end of the criteria
109
114
  ###
@@ -192,6 +197,18 @@ module JSS
192
197
  @criteria.each_index { |ci| @criteria[ci].priority = ci }
193
198
  end
194
199
 
200
+ # Remove the various cached data
201
+ # from the instance_variables used to create
202
+ # pretty-print (pp) output.
203
+ #
204
+ # @return [Array] the desired instance_variables
205
+ #
206
+ def pretty_print_instance_variables
207
+ vars = instance_variables.sort
208
+ vars.delete :@container
209
+ vars
210
+ end
211
+
195
212
  ###
196
213
  ### @return [REXML::Element] the xml element for the criteria
197
214
  ###
@@ -200,7 +217,6 @@ module JSS
200
217
  ### @api private
201
218
  ###
202
219
  def rest_xml
203
- raise JSS::MissingDataError, "Criteria can't be empty" if @criteria.empty?
204
220
  cr = REXML::Element.new 'criteria'
205
221
  @criteria.each { |c| cr << c.rest_xml }
206
222
  cr
@@ -293,27 +293,6 @@ module JSS
293
293
  end # completed_mdm_commands
294
294
  alias completed_commands completed_mdm_commands
295
295
 
296
- # The time of the most recently completed MDM command.
297
- #
298
- # For Mobile Devices, this seems to be the best indicator of the real
299
- # last-contact time, since the last_inventory_update is changed when
300
- # changes are made via the API.
301
- #
302
- # @param ident [Type] The identifier for the object - id, name, sn, udid, etc.
303
- #
304
- # @param api [JSS::APIConnection] The API connection to use for the query
305
- # defaults to the currently active connection
306
- #
307
- # @return [Time, nil] An array of completed MDMCommands
308
- #
309
- def last_mdm_contact(ident, api: JSS.api)
310
- cmds = completed_mdm_commands(ident, api: api)
311
- return nil if cmds.empty?
312
-
313
- JSS.epoch_to_time cmds.map{|cmd| cmd.completed_epoch }.max
314
- end
315
-
316
-
317
296
  # The history of pending mdm commands for a target
318
297
  #
319
298
  # @param ident [Type] The identifier for the object - id, name, sn, udid, etc.
@@ -342,6 +321,28 @@ module JSS
342
321
  end # completed_mdm_commands
343
322
  alias failed_commands failed_mdm_commands
344
323
 
324
+ # The time of the most recently completed or failed MDM command.
325
+ # (knowledge of a failure means the device communicated with us)
326
+ #
327
+ # For Mobile Devices, this seems to be the best indicator of the real
328
+ # last-contact time, since the last_inventory_update is changed when
329
+ # changes are made via the API.
330
+ #
331
+ # @param ident [Type] The identifier for the object - id, name, sn, udid, etc.
332
+ #
333
+ # @param api [JSS::APIConnection] The API connection to use for the query
334
+ # defaults to the currently active connection
335
+ #
336
+ # @return [Time, nil] An array of completed MDMCommands
337
+ #
338
+ def last_mdm_contact(ident, api: JSS.api)
339
+ epochs = completed_mdm_commands(ident, api: api).map { |cmd| cmd.completed_epoch }
340
+ epochs += failed_mdm_commands(ident, api: api).map { |cmd| cmd.failed_epoch }
341
+ epoch = epochs.max
342
+ epoch ? JSS.epoch_to_time(epoch) : nil
343
+ end
344
+
345
+
345
346
  # The history of app store apps for a computer
346
347
  #
347
348
  # @param ident [Type] The identifier for the object - id, name, sn, udid, etc.
@@ -126,6 +126,10 @@ module JSS
126
126
  }
127
127
  }.freeze
128
128
 
129
+ HW_PREFIX_TV = 'AppleTV'.freeze
130
+ HW_PREFIX_IPAD = 'iPad'.freeze
131
+ HW_PREFIX_IPHONE = 'iPhone'.freeze
132
+
129
133
  NON_UNIQUE_NAMES = true
130
134
 
131
135
  # This class lets us seach for computers
@@ -489,6 +493,19 @@ module JSS
489
493
  end
490
494
  end # initialize
491
495
 
496
+ def tv?
497
+ model_identifier.start_with? HW_PREFIX_TV
498
+ end
499
+ alias apple_tv? tv?
500
+
501
+ def ipad?
502
+ model_identifier.start_with? HW_PREFIX_IPAD
503
+ end
504
+
505
+ def iphone?
506
+ model_identifier.start_with? HW_PREFIX_IPHONE
507
+ end
508
+
492
509
  def name=(new_name)
493
510
  super
494
511
  @needs_mdm_name_change = true if managed? && supervised?
@@ -41,6 +41,85 @@ module JSS
41
41
  # This class provides methods for adding, removing, or fully replacing the
42
42
  # various items in scope's realms: targets, limitations, and exclusions.
43
43
  #
44
+ # IMPORTANT:
45
+ # The classic API has bugs regarding the use of Users, UserGroups,
46
+ # LDAP/Local Users, & LDAP User Groups in scopes. Here's a discussion
47
+ # of those bugs and how ruby-jss handles them.
48
+ #
49
+ # Targets/Inclusions
50
+ # - 'Users' can only be JSS::Users - No LDAP
51
+ # - BUG: They do not appear in API data (XML or JSON) and are
52
+ # NOT SUPPORTED in ruby-jss.
53
+ # - You must use the Web UI to work with them in a Scope.
54
+ # - 'User Groups' can only be JSS::UserGroups - 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
+ #
59
+ # Limitations
60
+ # - 'LDAP/Local Users' can be any string
61
+ # - The Web UI accepts any string, even if no matching Local or LDAP user.
62
+ # - The data shows up in API data in scope=>limitations=>users
63
+ # by name only (the string provided), no IDs
64
+ # - 'LDAP User Groups' can only be LDAP groups that actually exist
65
+ # - The Web UI won't let you add a group that doesn't exist in ldap
66
+ # - The data shows up in API data in scope=>limitations=>user_groups
67
+ # by name and LDAP ID (which may be empty)
68
+ # - The data ALSO shows up in API data in scope=>limit_to_users=>user_groups
69
+ # by name only, no LDAP IDs. ruby-jss ignores this and looks at
70
+ # scope=>limitations=>user_groups
71
+ #
72
+ # Exclusions, combines the behavior of Inclusions & Limitations
73
+ # - 'Users' can only be JSS::Users - No LDAP
74
+ # - BUG: They do not appear in API data (XML or JSON) and are
75
+ # NOT SUPPORTED in ruby-jss.
76
+ # - You must use the Web UI to work with them in a Scope.
77
+ # - 'User Groups' can only be JSS::UserGroups - 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
+ # - 'LDAP/Local Users' can be any string
82
+ # - The Web UI accepts any string, even if no matching Local or LDAP user.
83
+ # - The data shows up in API data in scope=>exclusions=>users
84
+ # by name only (the string provided), no IDs
85
+ # - 'LDAP User Groups' can only be LDAP groups that actually exist
86
+ # - The Web UI won't let you add a group that doesn't exist in ldap
87
+ # - The data shows up in API data in scope=>exclusions=>user_groups
88
+ # by name and LDAP ID (which may be empty)
89
+ #
90
+ #
91
+ # How ruby-jss handles this:
92
+ #
93
+ # - Methods #set_targets and #add_target will not accept the keys
94
+ # :user, :users, :user_group, :user_groups.
95
+ #
96
+ # - Method #remove_target will ignore them.
97
+ #
98
+ # - Methods #set_limitations, #add_limitation & #remove_limitation will accept:
99
+ # - :user, :ldap_user, or :jamf_ldap_user (and their plurals) for working
100
+ # with 'LDAP/Local Users'. When setting or adding, the provided
101
+ # string(s) must exist as either a JSS::User or an LDAP user
102
+ # - :user_group or :ldap_user_group (and their plurals) for working with
103
+ # 'LDAP User Groups'. When setting or adding, the provided string
104
+ # must exist as a group in LDAP.
105
+ #
106
+ # - Methods #set_exclusions, #add_exclusion & #remove_exclusion will accept:
107
+ # - :user, :ldap_user, or :jamf_ldap_user (and their plurals) for working
108
+ # with 'LDAP/Local Users'. When setting or adding, the provided string(s)
109
+ # must exist as either a JSS::User or an LDAP user.
110
+ # - :user_group or :ldap_user_group (and their plurals) for working with
111
+ # 'LDAP User Groups''. When setting or adding, the provided string
112
+ # must exist as a group in LDAP.
113
+ #
114
+ # Internally in the Scope instance:
115
+ #
116
+ # - The limitations and exclusions that match the WebUI's 'LDAP/Local Users'
117
+ # are in @limitations[:jamf_ldap_users] and @exclusions[:jamf_ldap_users]
118
+ #
119
+ # - The limitations and exclusions that match the WebUI's 'LDAP User Groups'
120
+ # are in @limitations[:ldap_user_groups] and @exclusions[:ldap_user_groups]
121
+ #
122
+ #
44
123
  # @see JSS::Scopable
45
124
  #
46
125
  class Scope
@@ -50,6 +129,8 @@ module JSS
50
129
 
51
130
  # These are the classes that Scopes can use for defining a scope,
52
131
  # keyed by appropriate symbols.
132
+ # NOTE: All the user and group ones don't actually refer to
133
+ # JSS::User or JSS::UserGroup. See IMPORTANT discussion above.
53
134
  SCOPING_CLASSES = {
54
135
  computers: JSS::Computer,
55
136
  computer: JSS::Computer,
@@ -65,16 +146,37 @@ module JSS
65
146
  department: JSS::Department,
66
147
  network_segments: JSS::NetworkSegment,
67
148
  network_segment: JSS::NetworkSegment,
68
- users: JSS::User,
69
- user: JSS::User,
70
- user_groups: JSS::UserGroup,
71
- user_group: JSS::UserGroup
149
+ ibeacon: JSS::IBeacon,
150
+ ibeacons: JSS::IBeacon,
151
+ user: nil,
152
+ users: nil,
153
+ ldap_user: nil,
154
+ ldap_users: nil,
155
+ jamf_ldap_user: nil,
156
+ jamf_ldap_users: nil,
157
+ user_group: nil,
158
+ user_groups: nil,
159
+ ldap_user_group: nil,
160
+ ldap_user_groups: nil
72
161
  }.freeze
73
162
 
74
- # Some things get checked in LDAP as well as the JSS
75
- LDAP_USER_KEYS = %i[user users].freeze
76
- LDAP_GROUP_KEYS = %i[user_groups user_group].freeze
77
- CHECK_LDAP_KEYS = LDAP_USER_KEYS + LDAP_GROUP_KEYS
163
+ # These keys always mean :jamf_ldap_users
164
+ LDAP_JAMF_USER_KEYS = %i[
165
+ user
166
+ users
167
+ ldap_user
168
+ ldap_users
169
+ jamf_ldap_user
170
+ jamf_ldap_users
171
+ ].freeze
172
+
173
+ # These keys always mean :ldap_user_groups
174
+ LDAP_GROUP_KEYS = %i[
175
+ user_group
176
+ user_groups
177
+ ldap_user_group
178
+ ldap_user_groups
179
+ ].freeze
78
180
 
79
181
  # This hash maps the availble Scope Target keys from SCOPING_CLASSES to
80
182
  # their corresponding target group keys from SCOPING_CLASSES.
@@ -88,7 +190,16 @@ module JSS
88
190
  INCLUSIONS = %i[buildings departments].freeze
89
191
 
90
192
  # These can limit the inclusion list
91
- LIMITATIONS = %i[network_segments users user_groups].freeze
193
+ # These are the keys that come from the API
194
+ # the :users key from the API is what we call :jamf_ldap_users
195
+ # and the :user_groups key from the API we call :ldap_user_groups
196
+ # See the IMPORTANT discussion above.
197
+ LIMITATIONS = %i[
198
+ ibeacons
199
+ network_segments
200
+ jamf_ldap_users
201
+ ldap_user_groups
202
+ ].freeze
92
203
 
93
204
  # any of them can be excluded
94
205
  EXCLUSIONS = INCLUSIONS + LIMITATIONS
@@ -179,7 +290,9 @@ module JSS
179
290
  #
180
291
  def initialize(target_key, raw_scope = nil)
181
292
  raw_scope ||= DEFAULT_SCOPE.dup
182
- raise JSS::InvalidDataError, "The target class of a Scope must be one of the symbols :#{TARGETS_AND_GROUPS.keys.join(', :')}" unless TARGETS_AND_GROUPS.key?(target_key)
293
+ unless TARGETS_AND_GROUPS.key?(target_key)
294
+ raise JSS::InvalidDataError, "The target class of a Scope must be one of the symbols :#{TARGETS_AND_GROUPS.keys.join(', :')}"
295
+ end
183
296
 
184
297
  @target_key = target_key
185
298
  @target_class = SCOPING_CLASSES[@target_key]
@@ -197,24 +310,64 @@ module JSS
197
310
  @inclusions = {}
198
311
  @inclusion_keys.each do |k|
199
312
  raw_scope[k] ||= []
200
- @inclusions[k] = raw_scope[k].compact.map { |n| n[:id].to_i }
313
+ @inclusions[k] = raw_scope[k].compact.map { |n| n[:id].to_i }
201
314
  end # @inclusion_keys.each do |k|
202
315
 
316
+ # the :users key from the API is what we call :jamf_ldap_users
317
+ # and the :user_groups key from the API we call :ldap_user_groups
318
+ # See the IMPORTANT discussion above.
203
319
  @limitations = {}
204
320
  if raw_scope[:limitations]
321
+
205
322
  LIMITATIONS.each do |k|
206
- raw_scope[:limitations][k] ||= []
207
- @limitations[k] = raw_scope[:limitations][k].compact.map { |n| n[:id].to_i }
323
+ # :jamf_ldap_users comes from :users in the API data
324
+ if k == :jamf_ldap_users
325
+ api_data = raw_scope[:limitations][:users]
326
+ api_data ||= []
327
+ @limitations[k] = api_data.compact.map { |n| n[:name].to_s }
328
+
329
+ # :ldap_user_groups comes from :user_groups in the API data
330
+ elsif k == :ldap_user_groups
331
+ api_data = raw_scope[:limitations][:user_groups]
332
+ api_data ||= []
333
+ @limitations[k] = api_data.compact.map { |n| n[:name].to_s }
334
+
335
+ # others handled normally.
336
+ else
337
+ api_data = raw_scope[:limitations][k]
338
+ api_data ||= []
339
+ @limitations[k] = api_data.compact.map { |n| n[:id].to_i }
340
+ end
208
341
  end # LIMITATIONS.each do |k|
209
342
  end # if raw_scope[:limitations]
210
343
 
344
+ # the :users key from the API is what we call :jamf_ldap_users
345
+ # and the :user_groups key from the API we call :ldap_user_groups
346
+ # See the IMPORTANT discussion above.
211
347
  @exclusions = {}
212
348
  if raw_scope[:exclusions]
349
+
213
350
  @exclusion_keys.each do |k|
214
- raw_scope[:exclusions][k] ||= []
215
- @exclusions[k] = raw_scope[:exclusions][k].compact.map { |n| n[:id].to_i }
216
- end
217
- end
351
+ # :jamf_ldap_users comes from :users in the API data
352
+ if k == :jamf_ldap_users
353
+ api_data = raw_scope[:exclusions][:users]
354
+ api_data ||= []
355
+ @exclusions[k] = api_data.compact.map { |n| n[:name].to_s }
356
+
357
+ # :ldap_user_groups comes from :user_groups in the API data
358
+ elsif k == :ldap_user_groups
359
+ api_data = raw_scope[:exclusions][:user_groups]
360
+ api_data ||= []
361
+ @exclusions[k] = api_data.compact.map { |n| n[:name].to_s }
362
+
363
+ # others handled normally.
364
+ else
365
+ api_data = raw_scope[:exclusions][k]
366
+ api_data ||= []
367
+ @exclusions[k] = api_data.compact.map { |n| n[:id].to_i }
368
+ end # if ...elsif... else
369
+ end # @exclusion_keys.each
370
+ end # if raw_scope[:exclusions]
218
371
 
219
372
  @container = nil
220
373
  end # init
@@ -239,7 +392,7 @@ module JSS
239
392
  @exclusions = {}
240
393
  @exclusion_keys.each { |k| @exclusions[k] = [] }
241
394
  end
242
- @container.should_update if @container
395
+ @container&.should_update
243
396
  end
244
397
 
245
398
  # Replace a list of item names for as targets in this scope.
@@ -266,10 +419,12 @@ module JSS
266
419
  # check the idents
267
420
  list.map! do |ident|
268
421
  item_id = validate_item(:target, key, ident)
269
- if @exclusions[key] && @exclusions[key].include?(item_id)
422
+
423
+ if @exclusions[key]&.include?(item_id)
270
424
  raise JSS::AlreadyExistsError, \
271
- "Can't set #{key} target to '#{ident}' because it's already an explicit exclusion."
425
+ "Can't set #{key} target to '#{ident}' because it's already an explicit exclusion."
272
426
  end
427
+
273
428
  item_id
274
429
  end # each
275
430
 
@@ -277,7 +432,7 @@ module JSS
277
432
 
278
433
  @inclusions[key] = list
279
434
  @all_targets = false
280
- @container.should_update if @container
435
+ @container&.should_update
281
436
  end # sinclude_in_scope
282
437
  alias set_target set_targets
283
438
  alias set_inclusion set_targets
@@ -303,13 +458,13 @@ module JSS
303
458
  def add_target(key, item)
304
459
  key = pluralize_key(key)
305
460
  item_id = validate_item(:target, key, item)
306
- return if @inclusions[key] && @inclusions[key].include?(item_id)
461
+ return if @inclusions[key]&.include?(item_id)
307
462
 
308
- raise JSS::AlreadyExistsError, "Can't set #{key} target to '#{item}' because it's already an explicit exclusion." if @exclusions[key] && @exclusions[key].include?(item_id)
463
+ raise JSS::AlreadyExistsError, "Can't set #{key} target to '#{item}' because it's already an explicit exclusion." if @exclusions[key]&.include?(item_id)
309
464
 
310
465
  @inclusions[key] << item_id
311
466
  @all_targets = false
312
- @container.should_update if @container
467
+ @container&.should_update
313
468
  end
314
469
  alias add_inclusion add_target
315
470
 
@@ -328,9 +483,10 @@ module JSS
328
483
  key = pluralize_key(key)
329
484
  item_id = validate_item :target, key, item, error_if_not_found: false
330
485
  return unless item_id
331
- return unless @inclusions[key] && @inclusions[key].include?(item_id)
486
+ return unless @inclusions[key]&.include?(item_id)
487
+
332
488
  @inclusions[key].delete item_id
333
- @container.should_update if @container
489
+ @container&.should_update
334
490
  end
335
491
  alias remove_inclusion remove_target
336
492
 
@@ -357,14 +513,17 @@ module JSS
357
513
  # check the idents
358
514
  list.map! do |ident|
359
515
  item_id = validate_item(:limitation, key, ident)
360
- raise JSS::AlreadyExistsError, "Can't set #{key} limitation for '#{name}' because it's already an explicit exclusion." if @exclusions[key] && @exclusions[key].include?(item_id)
516
+ if @exclusions[key]&.include?(item_id)
517
+ raise JSS::AlreadyExistsError, "Can't set #{key} limitation for '#{name}' because it's already an explicit exclusion."
518
+ end
519
+
361
520
  item_id
362
521
  end # each
363
522
 
364
523
  return nil if list.sort == @limitations[key].sort
365
524
 
366
525
  @limitations[key] = list
367
- @container.should_update if @container
526
+ @container&.should_update
368
527
  end # set_limitation
369
528
  alias set_limitations set_limitation
370
529
 
@@ -386,12 +545,14 @@ module JSS
386
545
  def add_limitation(key, item)
387
546
  key = pluralize_key(key)
388
547
  item_id = validate_item(:limitation, key, item)
389
- return nil if @limitations[key] && @limitations[key].include?(item_id)
548
+ return nil if @limitations[key]&.include?(item_id)
390
549
 
391
- raise JSS::AlreadyExistsError, "Can't set #{key} limitation for '#{name}' because it's already an explicit exclusion." if @exclusions[key] && @exclusions[key].include?(item_id)
550
+ if @exclusions[key]&.include?(item_id)
551
+ raise JSS::AlreadyExistsError, "Can't set #{key} limitation for '#{name}' because it's already an explicit exclusion."
552
+ end
392
553
 
393
554
  @limitations[key] << item_id
394
- @container.should_update if @container
555
+ @container&.should_update
395
556
  end
396
557
 
397
558
  # Remove a single item for limiting this scope.
@@ -411,9 +572,10 @@ module JSS
411
572
  key = pluralize_key(key)
412
573
  item_id = validate_item :limitation, key, item, error_if_not_found: false
413
574
  return unless item_id
414
- return unless @limitations[key] && @limitations[key].include?(item_id)
575
+ return unless @limitations[key]&.include?(item_id)
576
+
415
577
  @limitations[key].delete item_id
416
- @container.should_update if @container
578
+ @container&.should_update
417
579
  end ###
418
580
 
419
581
  # Replace an exclusion list for this scope
@@ -439,9 +601,11 @@ module JSS
439
601
  item_id = validate_item(:exclusion, key, ident)
440
602
  case key
441
603
  when *@inclusion_keys
442
- raise JSS::AlreadyExistsError, "Can't exclude #{key} '#{ident}' because it's already explicitly included." if @inclusions[key] && @inclusions[key].include?(item_id)
604
+ raise JSS::AlreadyExistsError, "Can't exclude #{key} '#{ident}' because it's already explicitly included." if @inclusions[key]&.include?(item_id)
443
605
  when *LIMITATIONS
444
- raise JSS::AlreadyExistsError, "Can't exclude #{key} '#{ident}' because it's already an explicit limitation." if @limitations[key] && @limitations[key].include?(item_id)
606
+ if @limitations[key]&.include?(item_id)
607
+ raise JSS::AlreadyExistsError, "Can't exclude #{key} '#{ident}' because it's already an explicit limitation."
608
+ end
445
609
  end
446
610
  item_id
447
611
  end # each
@@ -449,7 +613,7 @@ module JSS
449
613
  return nil if list.sort == @exclusions[key].sort
450
614
 
451
615
  @exclusions[key] = list
452
- @container.should_update if @container
616
+ @container&.should_update
453
617
  end # limit scope
454
618
 
455
619
  # Add a single item for exclusions of this scope.
@@ -468,12 +632,14 @@ module JSS
468
632
  def add_exclusion(key, item)
469
633
  key = pluralize_key(key)
470
634
  item_id = validate_item(:exclusion, key, item)
471
- return if @exclusions[key] && @exclusions[key].include?(item_id)
472
- raise JSS::AlreadyExistsError, "Can't exclude #{key} scope to '#{item}' because it's already explicitly included." if @inclusions[key] && @inclusions[key].include?(item)
473
- raise JSS::AlreadyExistsError, "Can't exclude #{key} '#{item}' because it's already an explicit limitation." if @limitations[key] && @limitations[key].include?(item)
635
+ return if @exclusions[key]&.include?(item_id)
636
+
637
+ raise JSS::AlreadyExistsError, "Can't exclude #{key} scope to '#{item}' because it's already explicitly included." if @inclusions[key]&.include?(item)
638
+
639
+ raise JSS::AlreadyExistsError, "Can't exclude #{key} '#{item}' because it's already an explicit limitation." if @limitations[key]&.include?(item)
474
640
 
475
641
  @exclusions[key] << item_id
476
- @container.should_update if @container
642
+ @container&.should_update
477
643
  end
478
644
 
479
645
  # Remove a single item for exclusions of this scope
@@ -490,9 +656,10 @@ module JSS
490
656
  def remove_exclusion(key, item)
491
657
  key = pluralize_key(key)
492
658
  item_id = validate_item :exclusion, key, item, error_if_not_found: false
493
- return unless @exclusions[key] && @exclusions[key].include?(item_id)
659
+ return unless @exclusions[key]&.include?(item_id)
660
+
494
661
  @exclusions[key].delete item_id
495
- @container.should_update if @container
662
+ @container&.should_update
496
663
  end
497
664
 
498
665
  # @api private
@@ -508,24 +675,52 @@ module JSS
508
675
  @inclusions.each do |klass, list|
509
676
  list.compact!
510
677
  list.delete 0
511
- list_as_hash = list.map { |i| { id: i } }
512
- scope << SCOPING_CLASSES[klass].xml_list(list_as_hash, :id)
678
+ list_as_hashes = list.map { |i| { id: i } }
679
+ scope << SCOPING_CLASSES[klass].xml_list(list_as_hashes, :id)
513
680
  end
514
681
 
515
682
  limitations = scope.add_element('limitations')
516
683
  @limitations.each do |klass, list|
517
684
  list.compact!
518
685
  list.delete 0
519
- list_as_hash = list.map { |i| { id: i } }
520
- limitations << SCOPING_CLASSES[klass].xml_list(list_as_hash, :id)
686
+ if klass == :jamf_ldap_users
687
+ users_xml = limitations.add_element 'users'
688
+ list.each do |name|
689
+ user_xml = users_xml.add_element 'user'
690
+ user_xml.add_element('name').text = name
691
+ end
692
+ elsif klass == :ldap_user_groups
693
+ user_groups_xml = limitations.add_element 'user_groups'
694
+ list.each do |name|
695
+ user_group_xml = user_groups_xml.add_element 'user_group'
696
+ user_group_xml.add_element('name').text = name
697
+ end
698
+ else
699
+ list_as_hashes = list.map { |i| { id: i } }
700
+ limitations << SCOPING_CLASSES[klass].xml_list(list_as_hashes, :id)
701
+ end
521
702
  end
522
703
 
523
704
  exclusions = scope.add_element('exclusions')
524
705
  @exclusions.each do |klass, list|
525
706
  list.compact!
526
707
  list.delete 0
527
- list_as_hash = list.map { |i| { id: i } }
528
- exclusions << SCOPING_CLASSES[klass].xml_list(list_as_hash, :id)
708
+ if klass == :jamf_ldap_users
709
+ users_xml = exclusions.add_element 'users'
710
+ list.each do |name|
711
+ user_xml = users_xml.add_element 'user'
712
+ user_xml.add_element('name').text = name
713
+ end
714
+ elsif klass == :ldap_user_groups
715
+ user_groups_xml = exclusions.add_element 'user_groups'
716
+ list.each do |name|
717
+ user_group_xml = user_groups_xml.add_element 'user_group'
718
+ user_group_xml.add_element('name').text = name
719
+ end
720
+ else
721
+ list_as_hashes = list.map { |i| { id: i } }
722
+ exclusions << SCOPING_CLASSES[klass].xml_list(list_as_hashes, :id)
723
+ end
529
724
  end
530
725
  scope
531
726
  end # scope_xml
@@ -551,6 +746,7 @@ module JSS
551
746
  private
552
747
 
553
748
  # look up a valid id or nil, for use in a scope type
749
+ # Raise an error if not found, unless error_if_not_found is falsey
554
750
  #
555
751
  # @param realm [Symbol] How is this key being used in the scope?
556
752
  # :target, :limitation, or :exclusion
@@ -561,7 +757,9 @@ module JSS
561
757
  # @param ident [String, Integer] A unique identifier for the item being
562
758
  # validated, jss id, name, serial number, etc.
563
759
  #
564
- # @return [Integer, nil] the valid id for the item, or nil if not found
760
+ # @param error_if_not_found [Boolean] raise an error if no match for the ident
761
+ #
762
+ # @return [Integer, String, nil] the valid id or string for the item, or nil if not found
565
763
  #
566
764
  def validate_item(realm, key, ident, error_if_not_found: true)
567
765
  # which keys allowed depends on how the item is used...
@@ -573,20 +771,42 @@ module JSS
573
771
  else
574
772
  raise ArgumentError, 'Unknown realm, must be :target, :limitation, or :exclusion'
575
773
  end
774
+
576
775
  key = pluralize_key(key)
776
+
577
777
  raise JSS::InvalidDataError, "#{realm} key must be one of :#{possible_keys.join(', :')}" \
578
778
  unless possible_keys.include? key
579
779
 
580
- # return nil or a valid id
581
- id = SCOPING_CLASSES[key].valid_id ident
780
+ id = nil
781
+
782
+ # id will be a string
783
+ if key == :jamf_ldap_users
784
+ id = ident if JSS::User.all_names(:refresh).include?(ident) || JSS::LDAPServer.user_in_ldap?(ident)
785
+
786
+ # id will be a string
787
+ elsif key == :ldap_user_groups
788
+ id = ident if JSS::LDAPServer.group_in_ldap? ident
789
+
790
+ # id will be an integer
791
+ else
792
+ id = SCOPING_CLASSES[key].valid_id ident
793
+ end
794
+
582
795
  raise JSS::NoSuchItemError, "No existing #{key} matching '#{ident}'" if error_if_not_found && id.nil?
796
+
583
797
  id
584
798
  end # validate_item(type, key, ident)
585
799
 
586
800
  # the symbols used in the API data are plural, e.g. 'network_segments'
587
801
  # this will pluralize them, allowing us to use singulars as well.
588
802
  def pluralize_key(key)
589
- key.to_s.end_with?(ESS) ? key : "#{key}s".to_sym
803
+ if LDAP_JAMF_USER_KEYS.include? key
804
+ :jamf_ldap_users
805
+ elsif LDAP_GROUP_KEYS.include? key
806
+ :ldap_user_groups
807
+ else
808
+ key.to_s.end_with?(ESS) ? key : "#{key}s".to_sym
809
+ end
590
810
  end
591
811
 
592
812
  end # class Scope