ruby-jss 1.2.4a4 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

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