xolo-server 1.0.0 → 2.0.2

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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +42 -4
  3. data/bin/xoloserver +3 -0
  4. data/data/client/xolo +1233 -0
  5. data/lib/optimist_with_insert_blanks.rb +1216 -0
  6. data/lib/xolo/core/base_classes/configuration.rb +238 -0
  7. data/lib/xolo/core/base_classes/server_object.rb +112 -0
  8. data/lib/xolo/core/base_classes/title.rb +884 -0
  9. data/lib/xolo/core/base_classes/version.rb +641 -0
  10. data/lib/xolo/core/constants.rb +85 -0
  11. data/lib/xolo/core/exceptions.rb +52 -0
  12. data/lib/xolo/core/json_wrappers.rb +43 -0
  13. data/lib/xolo/core/loading.rb +59 -0
  14. data/lib/xolo/core/output.rb +292 -0
  15. data/lib/xolo/core/security_cmd.rb +128 -0
  16. data/lib/xolo/core/version.rb +21 -0
  17. data/lib/xolo/core.rb +47 -0
  18. data/lib/xolo/server/app.rb +7 -0
  19. data/lib/xolo/server/configuration.rb +243 -38
  20. data/lib/xolo/server/constants.rb +10 -0
  21. data/lib/xolo/server/helpers/auth.rb +19 -2
  22. data/lib/xolo/server/helpers/autopkg.rb +157 -0
  23. data/lib/xolo/server/helpers/client_data.rb +90 -60
  24. data/lib/xolo/server/helpers/file_transfers.rb +412 -82
  25. data/lib/xolo/server/helpers/jamf_pro.rb +31 -7
  26. data/lib/xolo/server/helpers/log.rb +2 -0
  27. data/lib/xolo/server/helpers/maintenance.rb +1 -0
  28. data/lib/xolo/server/helpers/notification.rb +4 -3
  29. data/lib/xolo/server/helpers/pkg_signing.rb +16 -12
  30. data/lib/xolo/server/helpers/progress_streaming.rb +9 -12
  31. data/lib/xolo/server/helpers/subscriptions.rb +119 -0
  32. data/lib/xolo/server/helpers/titles.rb +27 -3
  33. data/lib/xolo/server/helpers/versions.rb +23 -11
  34. data/lib/xolo/server/mixins/changelog.rb +9 -16
  35. data/lib/xolo/server/mixins/title_jamf_access.rb +375 -390
  36. data/lib/xolo/server/mixins/title_ted_access.rb +50 -8
  37. data/lib/xolo/server/mixins/version_jamf_access.rb +118 -129
  38. data/lib/xolo/server/mixins/version_ted_access.rb +34 -4
  39. data/lib/xolo/server/object_locks.rb +2 -1
  40. data/lib/xolo/server/routes/auth.rb +2 -2
  41. data/lib/xolo/server/routes/jamf_pro.rb +11 -1
  42. data/lib/xolo/server/routes/maint.rb +2 -1
  43. data/lib/xolo/server/routes/subscriptions.rb +126 -0
  44. data/lib/xolo/server/routes/title_editor.rb +1 -1
  45. data/lib/xolo/server/routes/titles.rb +26 -11
  46. data/lib/xolo/server/routes/uploads.rb +0 -14
  47. data/lib/xolo/server/routes/versions.rb +14 -13
  48. data/lib/xolo/server/routes.rb +15 -23
  49. data/lib/xolo/server/title.rb +100 -77
  50. data/lib/xolo/server/version.rb +178 -18
  51. data/lib/xolo/server.rb +8 -0
  52. metadata +20 -11
@@ -0,0 +1,157 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # main module
11
+ module Xolo
12
+
13
+ module Server
14
+
15
+ module Helpers
16
+
17
+ # This is mixed in to Xolo::Server as a helper module, so its
18
+ # instance methods are available in sinatra routes and views.
19
+ #
20
+ #
21
+ # - unlock autpkg_user's login keychain if pw is in config
22
+ # - run recipe
23
+ # - move pkg to workspace
24
+ # - sign pkg if needed
25
+ # - wrap and re-sign if needed
26
+ # - rename pkg
27
+ # - upload to Jamf Pro
28
+ #
29
+ module AutoPkg
30
+
31
+ # Constants
32
+ #######################
33
+ #######################
34
+
35
+ AUTOPKG_UPLOADED_BY = 'autopkg'
36
+ FAIL_UNTRUSTED_RECIPES_CLI_OPT = '-k FAIL_RECIPES_WITHOUT_TRUST_INFO=yes'
37
+
38
+ # Module Methods
39
+ #######################
40
+ #######################
41
+
42
+ # when this module is included
43
+ def self.included(includer)
44
+ Xolo.verbose_include includer, self
45
+ end
46
+
47
+ # Instance Methods
48
+ #######################
49
+ ######################
50
+
51
+ # The Etc::Passwd entry for the autopkg_user
52
+ ###############################
53
+ def autopkg_user_entry
54
+ return unless Xolo::Server.config.autopkg_user
55
+
56
+ @autopkg_user_entry ||= Etc.getpwnam(Xolo::Server.config.autopkg_user)
57
+ rescue ArgumentError
58
+ nil
59
+ end
60
+
61
+ # unlock the autopkg_user's login keychain if a password is provided in the config
62
+ # This is necessary if any recipes need to access it for signing identities
63
+ ###############################
64
+ def unlock_autopkg_user_keychain
65
+ return unless autopkg_enabled?
66
+ return unless Xolo::Server.config.autopkg_user_keychain_pw
67
+
68
+ keychain_path = "#{autopkg_user_entry.dir}/Library/Keychains/login.keychain-db"
69
+ cmd = [
70
+ 'unlock-keychain',
71
+ '-p',
72
+ Xolo::Server.config.autopkg_user_keychain_pw,
73
+ keychain_path
74
+ ]
75
+ run_security(cmd.map { |i| security_escape i }.join(' '))
76
+ end
77
+
78
+ # Is AutoPkg integration enabled for the server?
79
+ ###############################
80
+ def autopkg_enabled?
81
+ return @autopkg_enabled if defined?(@autopkg_enabled)
82
+
83
+ @autopkg_enabled =
84
+ Xolo::Server.config.autopkg_executable && \
85
+ Pathname.new(Xolo::Server.config.autopkg_executable).executable? && \
86
+ autopkg_user_entry && \
87
+ true
88
+ rescue ArgumentError
89
+ @autopkg_enabled = false
90
+ end
91
+
92
+ # the autopkg run command for this title
93
+ #####################################
94
+ def autopkg_run_command(title_object)
95
+ return unless autopkg_enabled?
96
+
97
+ [
98
+ '/bin/launchctl',
99
+ 'asuser',
100
+ autopkg_user_entry.uid.to_s,
101
+ 'sudo',
102
+ '-u',
103
+ Xolo::Server.config.autopkg_user,
104
+ Xolo::Server.config.autopkg_executable.shellescape,
105
+ 'run',
106
+ '--verbose',
107
+ title_object.autopkg_recipe.shellescape,
108
+ FAIL_UNTRUSTED_RECIPES_CLI_OPT
109
+ ]
110
+ end
111
+
112
+ # Run the AutoPkg recipe for this title
113
+ # return the Pathname to the latest pkg in the autopkg_dir
114
+ #
115
+ # @param title_object [Xolo::Server::Title] the title object for which to run the recipe.
116
+ # It must have an autopkg_recipe defined.
117
+ # @return [Pathname, nil] the latest pkg file in the autopkg_dir after running the recipe,
118
+ # or nil if the recipe is not enabled for this title
119
+ ##############################
120
+ def run_autopkg_recipe(title_object)
121
+ return unless title_object.autopkg_enabled?
122
+
123
+ recipe = title_object.autopkg_recipe
124
+ pkgdir = Pathname.new title_object.autopkg_dir
125
+
126
+ cmd = autopkg_run_command(title_object)
127
+ progress "Running AutoPkg recipe for #{title_object.title} via command: #{cmd.join(' ')}", log: :info
128
+
129
+ unlock_autopkg_user_keychain
130
+
131
+ souterr, status = Open3.capture2e(*cmd)
132
+ souterr.strip!
133
+
134
+ if status.success?
135
+ progress "AutoPkg recipe #{recipe} for title '#{title_object.title}' completed successfully.", log: :info, alert: true
136
+ log_debug 'AutoPkg output:'
137
+ souterr.lines.each { |l| log_debug "AutoPkg: #{l.chomp}" }
138
+
139
+ pkgs = pkgdir.children.select { |c| c.extname == '.pkg' }
140
+ pkgs.max_by(&:mtime)
141
+
142
+ else
143
+ progress "ERROR: AutoPkg recipe #{autopkg_recipe} failed with status #{status.exitstatus}.", log: :error, alert: true
144
+ log_error 'AutoPkg output:'
145
+ souterr.lines.each { |l| log_error "AutoPkg: #{l.chomp}" }
146
+
147
+ raise "AutoPkg recipe #{autopkg_recipe} failed."
148
+ end
149
+ end
150
+
151
+ end # AutoPkg
152
+
153
+ end # Helpers
154
+
155
+ end # Server
156
+
157
+ end # module Xolo
@@ -46,34 +46,8 @@ module Xolo
46
46
 
47
47
  CLIENT_DATA_STR = 'client-data'
48
48
 
49
- # The name of the Jamf Package object that contains the xolo-client-data
50
- # NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY
51
- CLIENT_DATA_PACKAGE_NAME = "#{Xolo::Server::JAMF_OBJECT_NAME_PFX}#{CLIENT_DATA_STR}"
52
-
53
49
  # The name of the package file that installs the xolo-client-data JSON file
54
- CLIENT_DATA_COMPONENT_PACKAGE_FILE = "#{CLIENT_DATA_PACKAGE_NAME}-component.pkg"
55
-
56
- CLIENT_DATA_PACKAGE_FILE = "#{CLIENT_DATA_PACKAGE_NAME}.pkg"
57
-
58
- # The package identifier for the xolo-client-data package
59
- CLIENT_DATA_PACKAGE_IDENTIFIER = "com.pixar.xolo.#{CLIENT_DATA_STR}"
60
-
61
- # The name of the Jamf::Policy object that installs the xolo-client-data package
62
- # automatically on all managed Macs
63
- # NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY
64
- CLIENT_DATA_AUTO_POLICY_NAME = "#{CLIENT_DATA_PACKAGE_NAME}-auto"
65
-
66
- # The name of the Jamf::Policy object that installs the xolo-client-data package
67
- # manually on a managed Mac
68
- CLIENT_DATA_MANUAL_POLICY_NAME = "#{CLIENT_DATA_PACKAGE_NAME}-manual"
69
-
70
- # The name of the client-data JSON file in the xolo-client-data package
71
- # this is the file that is installed onto managed Macs in
72
- # /Library/Application Support/xolo/
73
- CLIENT_DATA_FILE = 'client-data.json'
74
-
75
- # The trgger event for the manual policy to update the client data JSON file
76
- CLIENT_DATA_MANUAL_POLICY_TRIGGER = 'update-xolo-client-data'
50
+ CLIENT_DATA_COMPONENT_PACKAGE_FILE = "#{CLIENT_DATA_STR}-component.pkg"
77
51
 
78
52
  # Module methods
79
53
  #
@@ -111,12 +85,70 @@ module Xolo
111
85
  ##############################
112
86
  ##############################
113
87
 
88
+ # The name of the client-data JSON file in the xolo-client-data package
89
+ # this is the file that is installed onto managed Macs in
90
+ # /Library/Application Support/xolo/
91
+ # @return [String] the name of the client-data JSON file in the package
92
+ ###################
93
+ def client_data_filename
94
+ @client_data_filename ||= Xolo::Server.config.test_server ? "test-#{CLIENT_DATA_STR}.json" : "#{CLIENT_DATA_STR}.json"
95
+ end
96
+
97
+ # The package identifier for the xolo-client-data package
98
+ # @return [String] the package identifier for the xolo-client-data package
99
+ ###################
100
+ def client_data_package_identifier
101
+ @client_data_package_identifier ||= Xolo::Server.config.test_server ? "com.pixar.xolotest.#{CLIENT_DATA_STR}" : "com.pixar.xolo.#{CLIENT_DATA_STR}"
102
+ end
103
+
104
+ # The name of the Jamf Package object that contains the xolo client-data
105
+ # NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY
106
+ # @return [String] the name of the client-data package in Jamf Pro
107
+ ###################
108
+ def client_data_package_name
109
+ @client_data_package_name ||= "#{jamf_obj_name_pfx_base}#{CLIENT_DATA_STR}"
110
+ end
111
+
112
+ # The name of the package file that installs the xolo-client-data JSON file
113
+ # @return [String] the name of the package file for the client-data package
114
+ ###################
115
+ def client_data_package_file
116
+ @client_data_package_file ||= "#{client_data_package_name}.pkg"
117
+ end
118
+
119
+ # The name of the Jamf::Policy object that installs the xolo-client-data package
120
+ # automatically on all managed Macs
121
+ # NOTE: Set the category to Xolo::Server::JAMF_XOLO_CATEGORY
122
+ # @return [String] the name of the automatic policy for the client-data package in Jamf Pro
123
+ ###################
124
+ def client_data_auto_policy_name
125
+ @client_data_auto_policy_name ||= "#{client_data_package_name}-auto"
126
+ end
127
+
128
+ # The name of the Jamf::Policy object that installs the xolo-client-data package
129
+ # manually on a managed Mac
130
+ # @return [String] the name of the manual policy for the client-data package in Jamf Pro
131
+ ###################
132
+ def client_data_manual_policy_name
133
+ @client_data_manual_policy_name ||= "#{client_data_package_name}-manual"
134
+ end
135
+
136
+ # The trgger event for the manual policy to update the client data JSON file
137
+ # @return [String] the trigger event for the manual policy to update the client data JSON file
138
+ ###################
139
+ def client_data_manual_policy_trigger
140
+ @client_data_manual_policy_trigger ||= Xolo::Server.config.test_server ? 'test-update-xolo-client-data' : 'update-xolo-client-data'
141
+ end
142
+
143
+ ####################################
144
+ ####################################
145
+
114
146
  # @return [Jamf::JPackage] the xolo-client-data package object
115
147
  #####################
116
148
  def client_data_jpackage
117
149
  return @client_data_jpackage if @client_data_jpackage
118
150
 
119
- @client_data_jpackage = Jamf::JPackage.fetch packageName: CLIENT_DATA_PACKAGE_NAME, cnx: jamf_cnx
151
+ @client_data_jpackage = Jamf::JPackage.fetch packageName: client_data_package_name, cnx: jamf_cnx
120
152
  rescue Jamf::NoSuchItemError
121
153
  @client_data_jpackage = create_client_data_jamf_package
122
154
  end
@@ -162,15 +194,15 @@ module Xolo
162
194
  # @return [Jamf::JPackage]
163
195
  #####################
164
196
  def create_client_data_jamf_package
165
- progress "Jamf: Creating package object '#{CLIENT_DATA_PACKAGE_NAME}'"
197
+ progress "Jamf: Creating package object '#{client_data_package_name}'"
166
198
 
167
- info = "Installs the xolo client data JSON file into /Library/Application Support/xolo/#{CLIENT_DATA_FILE}"
199
+ info = "Installs the xolo client data JSON file into /Library/Application Support/xolo/#{client_data_filename}"
168
200
 
169
201
  # Create the package
170
202
  pkg = Jamf::JPackage.create(
171
203
  cnx: jamf_cnx,
172
- packageName: CLIENT_DATA_PACKAGE_NAME,
173
- fileName: CLIENT_DATA_PACKAGE_FILE,
204
+ packageName: client_data_package_name,
205
+ fileName: client_data_package_file,
174
206
  categoryId: jamf_xolo_category_id,
175
207
  info: info
176
208
  )
@@ -178,11 +210,11 @@ module Xolo
178
210
  pkg.save
179
211
  # .pkg files are not uploaded here, but in the upload_client_data_package method
180
212
 
181
- log_debug "Jamf: Created package '#{CLIENT_DATA_PACKAGE_NAME}'"
213
+ log_debug "Jamf: Created package '#{client_data_package_name}'"
182
214
 
183
215
  pkg
184
- rescue StandardError => e
185
- raise "Jamf: Error creating Jamf::JPackage '#{CLIENT_DATA_PACKAGE_NAME}': #{e.class}: #{e}"
216
+ rescue => e
217
+ raise "Jamf: Error creating Jamf::JPackage '#{client_data_package_name}': #{e.class}: #{e}"
186
218
  end
187
219
 
188
220
  # Create the xolo-client-data policies in Jamf Pro
@@ -192,13 +224,11 @@ module Xolo
192
224
  def create_client_data_policies_if_needed
193
225
  all_pol_names = Jamf::Policy.all_names(cnx: jamf_cnx)
194
226
 
195
- unless all_pol_names.include? CLIENT_DATA_AUTO_POLICY_NAME
196
- create_client_data_policy CLIENT_DATA_AUTO_POLICY_NAME
197
- end
227
+ create_client_data_policy client_data_auto_policy_name unless all_pol_names.include? client_data_auto_policy_name
198
228
 
199
- return if all_pol_names.include? CLIENT_DATA_MANUAL_POLICY_NAME
229
+ return if all_pol_names.include? client_data_manual_policy_name
200
230
 
201
- create_client_data_policy CLIENT_DATA_MANUAL_POLICY_NAME
231
+ create_client_data_policy client_data_manual_policy_name
202
232
  end
203
233
 
204
234
  # Create a xolo-client-data install policy in Jamf Pro
@@ -213,7 +243,7 @@ module Xolo
213
243
  # Create the policy and set common attributes
214
244
  pol = Jamf::Policy.create name: pol_name, cnx: jamf_cnx
215
245
  pol.category = Xolo::Server::JAMF_XOLO_CATEGORY
216
- pol.add_package CLIENT_DATA_PACKAGE_NAME
246
+ pol.add_package client_data_package_name
217
247
 
218
248
  # scope to all computers
219
249
  pol.scope.set_all_targets
@@ -225,16 +255,16 @@ module Xolo
225
255
  end
226
256
 
227
257
  # Set the trigger event and frequency
228
- if pol_name == CLIENT_DATA_AUTO_POLICY_NAME
258
+ if pol_name == client_data_auto_policy_name
229
259
  pol.set_trigger_event :checkin, true
230
260
  pol.set_trigger_event :custom, Xolo::BLANK
231
261
  pol.frequency = :daily
232
- elsif pol_name == CLIENT_DATA_MANUAL_POLICY_NAME
262
+ elsif pol_name == client_data_manual_policy_name
233
263
  pol.set_trigger_event :checkin, false
234
- pol.set_trigger_event :custom, CLIENT_DATA_MANUAL_POLICY_TRIGGER
264
+ pol.set_trigger_event :custom, client_data_manual_policy_name
235
265
  pol.frequency = :ongoing
236
266
  else
237
- err_msg = "Jamf: Invalid policy name '#{pol_name}' must be #{CLIENT_DATA_AUTO_POLICY_NAME} or #{CLIENT_DATA_MANUAL_POLICY_NAME}"
267
+ err_msg = "Jamf: Invalid policy name '#{pol_name}' must be #{client_data_auto_policy_name} or #{client_data_manual_policy_name}"
238
268
  log_err err_msg, alert: true
239
269
  return
240
270
  end
@@ -249,8 +279,8 @@ module Xolo
249
279
  # @return [void]
250
280
  #####################
251
281
  def flush_client_data_policy_logs
252
- progress "Jamf: Flushing logs for policy #{CLIENT_DATA_AUTO_POLICY_NAME}", log: :info
253
- pol = Jamf::Policy.fetch name: CLIENT_DATA_AUTO_POLICY_NAME, cnx: jamf_cnx
282
+ progress "Jamf: Flushing logs for policy #{client_data_auto_policy_name}", log: :info
283
+ pol = Jamf::Policy.fetch name: client_data_auto_policy_name, cnx: jamf_cnx
254
284
  pol.flush_logs
255
285
  end
256
286
 
@@ -261,7 +291,7 @@ module Xolo
261
291
  #####################
262
292
  def create_new_client_data_pkg_file
263
293
  pkg_version = Time.now.strftime '%Y%m%d.%H%M%S.%6N'
264
- work_dir_prefix = "#{CLIENT_DATA_PACKAGE_NAME}-#{pkg_version}"
294
+ work_dir_prefix = "#{client_data_package_name}-#{pkg_version}"
265
295
 
266
296
  pkg_work_dir = Pathname.new(Dir.mktmpdir(work_dir_prefix))
267
297
 
@@ -269,11 +299,11 @@ module Xolo
269
299
  root_dir = pkg_work_dir + 'pkgroot'
270
300
  xolo_client_dir = root_dir + 'Library' + 'Application Support' + 'xolo'
271
301
  xolo_client_dir.mkpath
272
- client_data_file = xolo_client_dir + CLIENT_DATA_FILE
302
+ client_data_file = xolo_client_dir + client_data_filename
273
303
  client_data_file.pix_save JSON.pretty_generate(client_data_hash)
274
304
 
275
305
  # build the component package
276
- progress "Jamf: Creating new client-data pkg file '#{CLIENT_DATA_PACKAGE_FILE}'", log: :info
306
+ progress "Jamf: Creating new client-data pkg file '#{client_data_package_file}'", log: :info
277
307
 
278
308
  unlock_signing_keychain
279
309
 
@@ -295,7 +325,7 @@ module Xolo
295
325
  cmd << '--root'
296
326
  cmd << root_dir.to_s
297
327
  cmd << '--identifier'
298
- cmd << CLIENT_DATA_PACKAGE_IDENTIFIER
328
+ cmd << client_data_package_identifier
299
329
  cmd << '--version'
300
330
  cmd << pkg_version
301
331
  cmd << '--install-location'
@@ -309,7 +339,7 @@ module Xolo
309
339
  log_debug "Command to build component pkg '#{CLIENT_DATA_COMPONENT_PACKAGE_FILE}': #{cmd.join(' ')}"
310
340
 
311
341
  stdouterr, exit_status = Open3.capture2e(*cmd)
312
- raise "Error creating #{CLIENT_DATA_PACKAGE_FILE}: #{stdouterr}" unless exit_status.success?
342
+ raise "Error creating #{client_data_package_file}: #{stdouterr}" unless exit_status.success?
313
343
 
314
344
  outfile
315
345
  end
@@ -321,13 +351,13 @@ module Xolo
321
351
  # @return [Pathname] the path to the new package file
322
352
  #####################
323
353
  def build_dist_client_data_pkg_file(component_pkg_file, pkg_version, pkg_work_dir)
324
- pkg_file = pkg_work_dir + CLIENT_DATA_PACKAGE_FILE
354
+ pkg_file = pkg_work_dir + client_data_package_file
325
355
 
326
356
  cmd = ['/usr/bin/productbuild']
327
357
  cmd << '--package'
328
358
  cmd << component_pkg_file.to_s
329
359
  cmd << '--identifier'
330
- cmd << CLIENT_DATA_PACKAGE_IDENTIFIER
360
+ cmd << client_data_package_identifier
331
361
  cmd << '--version'
332
362
  cmd << pkg_version
333
363
  cmd << '--sign'
@@ -336,10 +366,10 @@ module Xolo
336
366
  cmd << Xolo::Server::Configuration::PKG_SIGNING_KEYCHAIN.to_s
337
367
  cmd << pkg_file.to_s
338
368
 
339
- log_debug "Command to build distribution pkg '#{CLIENT_DATA_PACKAGE_FILE}': #{cmd.join(' ')}"
369
+ log_debug "Command to build distribution pkg '#{client_data_package_file}': #{cmd.join(' ')}"
340
370
 
341
371
  stdouterr, exit_status = Open3.capture2e(*cmd)
342
- raise "Error creating #{CLIENT_DATA_PACKAGE_FILE}: #{stdouterr}" unless exit_status.success?
372
+ raise "Error creating #{client_data_package_file}: #{stdouterr}" unless exit_status.success?
343
373
 
344
374
  pkg_file
345
375
  end
@@ -350,7 +380,9 @@ module Xolo
350
380
  cdh = {
351
381
  titles: {}
352
382
  }
353
- all_title_objects.each do |title|
383
+ return cdh if all_titles.empty?
384
+
385
+ all_title_objects(refresh: true).each do |title|
354
386
  cdh[:titles][title.title] = title.to_h
355
387
  cdh[:titles][title.title][:versions] = title.version_objects.map(&:to_h)
356
388
 
@@ -358,9 +390,7 @@ module Xolo
358
390
  cdh[:titles][title.title][:version_script] = title.version_script_contents if title.version_script
359
391
 
360
392
  # add the forced_exclusion_group_name if any
361
- if Xolo::Server.config.forced_exclusion
362
- cdh[:titles][title.title][:excluded_groups] << Xolo::Server.config.forced_exclusion
363
- end
393
+ cdh[:titles][title.title][:excluded_groups] << Xolo::Server.config.forced_exclusion if Xolo::Server.config.forced_exclusion
364
394
 
365
395
  # add the frozen group name to the excluded_groups array
366
396
  cdh[:titles][title.title][:excluded_groups] << title.jamf_frozen_group_name if title.jamf_frozen_group_name