ruby-jss 4.2.0b2 → 4.2.1

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +44 -7
  3. data/README-2.0.0.md +19 -8
  4. data/README.md +43 -30
  5. data/lib/jamf/api/classic/api_objects/patch_title.rb +0 -2
  6. data/lib/jamf/api/jamf_pro/api_objects/api_client.rb +3 -0
  7. data/lib/jamf/api/jamf_pro/api_objects/api_role.rb +3 -0
  8. data/lib/jamf/api/jamf_pro/api_objects/j_package.rb +333 -119
  9. data/lib/jamf/api/jamf_pro/api_objects/managed_software_updates/plan.rb +237 -0
  10. data/lib/jamf/api/jamf_pro/api_objects/managed_software_updates.rb +291 -0
  11. data/lib/jamf/api/jamf_pro/base_classes/oapi_object.rb +24 -3
  12. data/lib/jamf/api/jamf_pro/mixins/collection_resource.rb +23 -3
  13. data/lib/jamf/api/jamf_pro/mixins/filterable.rb +8 -0
  14. data/lib/jamf/api/jamf_pro/mixins/jpapi_resource.rb +17 -3
  15. data/lib/jamf/api/jamf_pro/mixins/macos_managed_updates.rb +2 -0
  16. data/lib/jamf/api/jamf_pro/mixins/prestage.rb +3 -0
  17. data/lib/jamf/api/jamf_pro/oapi_schemas/account_group.rb +123 -0
  18. data/lib/jamf/api/jamf_pro/oapi_schemas/account_preferences_v1.rb +105 -0
  19. data/lib/jamf/api/jamf_pro/oapi_schemas/assign_remove_profile_response_sync_state.rb +112 -0
  20. data/lib/jamf/api/jamf_pro/oapi_schemas/auth_account_v1.rb +159 -0
  21. data/lib/jamf/api/jamf_pro/oapi_schemas/authentication_type.rb +97 -0
  22. data/lib/jamf/api/jamf_pro/oapi_schemas/{device_enrollment_disown_body.rb → available_updates.rb} +14 -10
  23. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_application.rb +124 -0
  24. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_attachment.rb +102 -0
  25. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_certificate.rb +151 -0
  26. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_configuration_profile.rb +118 -0
  27. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching.rb +372 -0
  28. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching_alert.rb +120 -0
  29. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching_cache_detail.rb +97 -0
  30. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching_data_migration_error.rb +98 -0
  31. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching_data_migration_error_user_info.rb +89 -0
  32. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching_parent.rb +131 -0
  33. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching_parent_alert.rb +105 -0
  34. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching_parent_capabilities.rb +124 -0
  35. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching_parent_details.rb +118 -0
  36. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_content_caching_parent_local_network.rb +97 -0
  37. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_disk.rb +143 -0
  38. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_disk_encryption.rb +120 -0
  39. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_extension_attribute.rb +175 -0
  40. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_font.rb +93 -0
  41. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_general.rb +244 -0
  42. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_hardware.rb +264 -0
  43. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_ibeacon.rb +81 -0
  44. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_inventory.rb +276 -0
  45. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_inventory_file_vault.rb +127 -0
  46. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_licensed_software.rb +88 -0
  47. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_local_user_account.rb +196 -0
  48. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_mdm_capability.rb +88 -0
  49. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_operating_system.rb +148 -0
  50. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_package_receipts.rb +97 -0
  51. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_partition.rb +145 -0
  52. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_partition_encryption.rb +94 -0
  53. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_partition_file_vault2_state.rb +97 -0
  54. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_plugin.rb +93 -0
  55. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_prestage_v3.rb +129 -0
  56. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_printer.rb +99 -0
  57. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_purchase.rb +158 -0
  58. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_remote_management.rb +88 -0
  59. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_section.rb +109 -0
  60. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_security.rb +193 -0
  61. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_service.rb +81 -0
  62. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_software_update.rb +93 -0
  63. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_storage.rb +90 -0
  64. data/lib/jamf/api/jamf_pro/oapi_schemas/computer_user_and_location.rb +131 -0
  65. data/lib/jamf/api/jamf_pro/oapi_schemas/dss_declaration.rb +111 -0
  66. data/lib/jamf/api/jamf_pro/oapi_schemas/dss_declarations.rb +88 -0
  67. data/lib/jamf/api/jamf_pro/oapi_schemas/enrollment_method.rb +97 -0
  68. data/lib/jamf/api/jamf_pro/oapi_schemas/group_membership.rb +94 -0
  69. data/lib/jamf/api/jamf_pro/oapi_schemas/inventory_preload_extension_attribute.rb +89 -0
  70. data/lib/jamf/api/jamf_pro/oapi_schemas/location_information.rb +146 -0
  71. data/lib/jamf/api/jamf_pro/oapi_schemas/{package_manifest.rb → location_information_v2.rb} +35 -37
  72. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_plan.rb +181 -0
  73. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_plan_event_store.rb +85 -0
  74. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_plan_group_post.rb +99 -0
  75. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_plan_post.rb +98 -0
  76. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_plan_post_response.rb +100 -0
  77. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_plan_toggle.rb +115 -0
  78. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_plan_toggle_status.rb +169 -0
  79. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_plan_toggle_status_wrapper.rb +91 -0
  80. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_plans.rb +102 -0
  81. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_status.rb +173 -0
  82. data/lib/jamf/api/jamf_pro/oapi_schemas/managed_software_update_statuses.rb +102 -0
  83. data/lib/jamf/api/jamf_pro/oapi_schemas/mobile_device_prestage_name_v2.rb +94 -0
  84. data/lib/jamf/api/jamf_pro/oapi_schemas/mobile_device_prestage_names_v2.rb +118 -0
  85. data/lib/jamf/api/jamf_pro/oapi_schemas/plan_configuration_post.rb +142 -0
  86. data/lib/jamf/api/jamf_pro/oapi_schemas/plan_device.rb +105 -0
  87. data/lib/jamf/api/jamf_pro/oapi_schemas/plan_device_post.rb +97 -0
  88. data/lib/jamf/api/jamf_pro/oapi_schemas/plan_device_response.rb +96 -0
  89. data/lib/jamf/api/jamf_pro/oapi_schemas/plan_group_post.rb +96 -0
  90. data/lib/jamf/api/jamf_pro/oapi_schemas/plan_search_results.rb +89 -0
  91. data/lib/jamf/api/jamf_pro/oapi_schemas/plan_status.rb +127 -0
  92. data/lib/jamf/api/jamf_pro/oapi_schemas/{device_enrollment_prestage.rb → prestage_purchasing_information.rb} +51 -88
  93. data/lib/jamf/api/jamf_pro/oapi_schemas/prestage_purchasing_information_v2.rb +174 -0
  94. data/lib/jamf/api/jamf_pro/oapi_schemas/prestage_scope_assignment_v2.rb +94 -0
  95. data/lib/jamf/api/jamf_pro/oapi_schemas/v1_site.rb +91 -0
  96. data/lib/jamf/api/jamf_pro/oapi_schemas.rb +24 -1
  97. data/lib/jamf/api/jamf_pro/other_classes/pager.rb +7 -1
  98. data/lib/jamf/composer.rb +114 -107
  99. data/lib/jamf/oapi_validate.rb +1 -0
  100. data/lib/jamf/utility.rb +20 -14
  101. data/lib/jamf/version.rb +1 -1
  102. data/test/bin/runtests +2 -2
  103. data/test/lib/jamf_test/collection_tests.rb +10 -2
  104. data/test/tests/computer_group.rb +29 -12
  105. data/test/tests/{jp_building.rb → j_building.rb} +2 -2
  106. data/test/tests/j_package.rb +47 -0
  107. metadata +87 -8
@@ -25,6 +25,8 @@
25
25
 
26
26
  # frozen_string_literal: true
27
27
 
28
+ require 'cfpropertylist'
29
+
28
30
  # The main Module
29
31
  module Jamf
30
32
 
@@ -102,6 +104,13 @@ module Jamf
102
104
  # which usually only identifies ':id'
103
105
  ALT_IDENTIFIERS = %i[packageName fileName].freeze
104
106
 
107
+ # If the object does not have a 'name' attribute, this is the attribute
108
+ # that holds its name. Used to allow referencing objects by 'name',
109
+ # creates a alias of the attribute called "name" and "name=",
110
+ # and allows the use of "name:" as an identifier in the .fetch, .valid_id and
111
+ # similar methods.
112
+ OBJECT_NAME_ATTR = :packageName
113
+
105
114
  # Must define this when extending Filterable
106
115
  FILTER_KEYS = %i[
107
116
  id fileName packageName categoryId info notes manifestFileName cloudTransferStatus
@@ -126,7 +135,7 @@ module Jamf
126
135
  # The hashType value in the API or manifests for md5
127
136
  CHECKSUM_HASH_TYPE_MD5 = 'MD5'
128
137
 
129
- # The hashType value in the API for sha256 - IF it exists
138
+ # The hashType value in the API for sha256 - IF it exists?
130
139
  CHECKSUM_HASH_TYPE_SHA256 = 'SHA_256'
131
140
 
132
141
  # The hashType value in MDM deploy manifests for sha256
@@ -149,7 +158,15 @@ module Jamf
149
158
  # fileName, with spaces converted to dashes.
150
159
  MANIFEST_FILENAME_DEFAULT_SUFFIX = '-manifest.plist'
151
160
 
152
- # Not doing chunking by default in generated manifests
161
+ # If no manifest bundle identifier is provided, this will be used before
162
+ # the packageName.
163
+ MANIFEST_BUNDLE_ID_PREFIX = 'com.pixar.ruby-jss.'
164
+
165
+ # if no manifest bundle version is provided, this will be used.
166
+ MANIFEST_BUNDLE_VERSION_DEFAULT = '0'
167
+
168
+ # Not doing chunking by default in generated manifests,
169
+ # but if we do, we'll use this
153
170
  MANIFEST_CHUNK_SIZE = 1024 * 1024 * 10 # 10MB
154
171
 
155
172
  MANIFEST_PLIST_TEMPLATE = {
@@ -164,10 +181,11 @@ module Jamf
164
181
  ], # end assets array,
165
182
  metadata: {
166
183
  'kind' => 'software',
167
- 'bundle-identifier' => 'com.example.pkg',
168
- 'bundle-version' => '0',
184
+ 'bundle-identifier' => "#{MANIFEST_BUNDLE_ID_PREFIX}example",
185
+ 'bundle-version' => MANIFEST_BUNDLE_VERSION_DEFAULT,
169
186
  'title' => 'title',
170
- 'sizeInBytes' => 1
187
+ 'sizeInBytes' => 1,
188
+ 'sha256-whole' => 'sha256-goes-here'
171
189
  } # end metadata
172
190
  } # end hash
173
191
  ] # end items array
@@ -210,8 +228,8 @@ module Jamf
210
228
  # for all Jamf::JPackage objects in this ruby session.
211
229
  #
212
230
  # Setting this in the config or here means you don't have to provide a base URL
213
- # each time when calling #generate_manifest, however you can still provide one
214
- # at that time to override any default.
231
+ # each time when calling #generate_manifest or #upload, however you can still provide
232
+ # one at that time to override any default.
215
233
  #
216
234
  # Normally, the package's fileName is appended to this URL to generate the full
217
235
  # download URL. E.g. for a package with a fileName 'my-app.pkg', with the base URL of
@@ -244,18 +262,22 @@ module Jamf
244
262
  #####################################
245
263
 
246
264
  # Checksums
265
+ #
247
266
  # Packages in Jamf Pro can have a checksum in either MD5 or SHA512, or possibly
248
267
  # SHA256 - none of our 1500 pkgs have 256, but given the existence of the sha256
249
268
  # attribute in the API data, I'm assuming it existed at some point, and behaves like
250
269
  # md5 (read on)
251
270
  #
252
- # In all cases, the hashType indicates the type of checksum, as a string, one of
253
- # 'MD5', 'SHA_256', or 'SHA_512'.
271
+ # In all cases, the hashType attribute indicates the type of checksum, as a string,
272
+ # one of 'MD5', 'SHA_256', or 'SHA_512'.
254
273
  #
255
- # In the case of md5 and sha256, the actually digest value (the checksum) is in the
274
+ # In the case of md5 and sha256, the actual digest value (the checksum) is in the
256
275
  # 'md5' or 'sha256' attribute. In the case of sha512, the digest is in the 'hashValue'
257
276
  # attribute.
258
- # In anycase, the digest value will be stored in the checksum attribute of this class
277
+ # In anycase, the digest value will also be stored in the checksum attribute
278
+ #
279
+ # NOTE: This is the checksum used when installing via a Policy.
280
+ # The checksum(s) used when deploying via MDM is stored in the manifest.
259
281
  #
260
282
  # @return [String] the checksum of the package, either an MD5, SHA256, or SHA512
261
283
  # digest of the package file.
@@ -318,6 +340,12 @@ module Jamf
318
340
  self.suppressEula = DEFAULT_SUPPRESS_EULA if suppressEula.nil?
319
341
  self.suppressRegistration = DEFAULT_SUPPRESS_REGISTRATION if suppressRegistration.nil?
320
342
 
343
+ if creating_from_create && osRequirements
344
+ # if we're creating a new object, and osRequirements is set,
345
+ # convert it to a comma-separated string
346
+ @osRequirements = osRequirements_as_comma_sep_string(osRequirements)
347
+ end
348
+
321
349
  @checksum =
322
350
  case hashType
323
351
  when CHECKSUM_HASH_TYPE_MD5
@@ -333,14 +361,62 @@ module Jamf
333
361
  #####################################
334
362
 
335
363
  # @return [Pathname] the local receipt when this pkg is installed by a policy
364
+ #############################
336
365
  def receipt
337
366
  # the receipt is the filename with any .zip extension removed.
338
367
  fileName ? (Jamf::Client::RECEIPTS_FOLDER + fileName.to_s.sub(/.zip$/, '')) : nil
339
368
  end
340
369
 
370
+ # Change the os_requirements field in the JSS
371
+ # E.g. 10.5, 10.5.3, 10.6.x
372
+ #
373
+ # Extra feature: Minumum OS's can now be specified as a
374
+ # string using the notation ">=10.6.7".
375
+ #
376
+ # @see Jamf.expand_min_os
377
+ #
378
+ # @param new_val [String,Array<String>] comma-separated string, or array of os versions
379
+ #
380
+ # @return [void]
381
+ #############################
382
+ def osRequirements=(new_val)
383
+ orig_osRequirements = osRequirements
384
+
385
+ new_val = osRequirements_as_comma_sep_string(new_val)
386
+ @osRequirements = new_val
387
+
388
+ note_unsaved_change :osRequirements, orig_osRequirements
389
+ end
390
+
391
+ # Take a value for osRequirements, and return it as a comma-separated string,
392
+ # possibly expanded if any of them starts with '>='.
393
+ #
394
+ # @see Jamf.expand_min_os
395
+ #
396
+ # @param val [String,Array<String>] comma-separated string, or array of os versions
397
+ #
398
+ # @return [Array<String>] the osRequirements as an array of strings
399
+ #############################
400
+ def osRequirements_as_comma_sep_string(val)
401
+ # make sure we have a flat array of strings
402
+ val = val.split(',').map(&:strip) if val.is_a? String
403
+ val = [val].flatten.compact.uniq.map(&:to_s)
404
+
405
+ # expand any minimum OS versions
406
+ val.map! do |vers|
407
+ vers.start_with?('>=') ? Jamf.expand_min_os(vers) : vers
408
+ end
409
+
410
+ val.flatten.join(', ')
411
+ end
412
+ private :osRequirements_as_comma_sep_string
413
+
341
414
  # Recalculate the checksum of the package file from a given filepath, and update the
342
415
  # object's checksum and hashValue attributes.
343
416
  #
417
+ # NOTE: This updates the checksum used when installing via a Policy.
418
+ # The checksum(s) used when deploying via MDM is stored in the manifest.
419
+ #
344
420
  # You will need to call #save on the object to save the new checksum to the server.
345
421
  #
346
422
  # New checksums are always SHA512
@@ -368,38 +444,65 @@ module Jamf
368
444
  self.class.calculate_checksum(filepath, hashType) == checksum
369
445
  end
370
446
 
371
- # @return [String] The manifest file name or a default if none is set
447
+ # @return [String] A default if none is set explicitly
372
448
  ##############################
373
449
  def default_manifestFileName
374
450
  "#{fileName.gsub(' ', '-')}#{MANIFEST_FILENAME_DEFAULT_SUFFIX}"
375
451
  end
376
452
 
377
- # Set the manifest from a local file or XML plist string.
378
- # If from a file, update the manifestFileName attribute to use the filename
453
+ # Set the manifest from a local file or a String containing an XML plist.
454
+ # If from a file, the manifestFileName attribute is set to the filename
455
+ #
456
+ # To automatically generate a manifest plist for this package from a
457
+ # locally-readable .pkg file, use #generate_manifest
458
+ #
459
+ # All manifests require a valid URL for downloading the .pkg file when
460
+ # installing on a client.
379
461
  #
380
- # To generate a basic manifest plist for this package, use #generate_manifest
462
+ # No validation of the manifest is done here.
381
463
  #
382
- # When using this method, if you want to be able to deploy the pkg using
383
- # #deploy_via_mdm, the manifest MUST include a metadata dictionary
384
- # with at least the following keys
385
- # - 'bundle-identifier' that matches the bundle identifier of the pkg
386
- # - 'bundle-version' = the version of the pkg
387
- # - 'kind' = 'software'
388
- # - 'title' = the name of the pkg or what it installs
389
- # - 'sizeInBytes' = the size of the pkg in bytes
390
- # optional keys include bundle-version, and subtitle
464
+ # DEPLOYING VIA MDM:
391
465
  #
392
- # @param manifest [String, Pathname] the manifest plist data or path to a local file
466
+ # When using this method, if you want to be able to deploy the package using
467
+ # deploy_via_mdm, the manifest MUST include a metadata dictionary
468
+ # with at least the following keys:
469
+ # - 'kind' = 'software'
470
+ # - 'bundle-identifier' that preferably matches the bundle identifier of the pkg
471
+ # - 'bundle-version' = that preferably matches the version of the pkg
472
+ # - 'title' = the name of the pkg or what it installs
473
+ # - 'sizeInBytes' = the size of the .pkg in bytes
474
+ # as well as one of these non-standard keys:
475
+ # - 'sha256-whole' = the SHA256 digest of the whole file, regardless of chunking data in the 'assets' array
476
+ # OR
477
+ # - 'md5-whole' = the MD5 digest of the whole file, regardless of chunking data in the 'assets' array
478
+ #
479
+ # The non-standard keys are because the Jamf Pro API endpoint for deploying via MDM requires
480
+ # a whole-file checksum even if the file is chunked in the manifest.
481
+ #
482
+ # See the MANIFEST_PLIST_TEMPLATE constant for an example of the data structure (as a ruby hash, not a plist)
483
+ #
484
+ # @param new_manifest [String, Pathname] the manifest plist data or path to a local file
393
485
  #
394
486
  # @return [void]
395
487
  ##############################
396
- def manifest=(manifest)
397
- if manifest.is_a? Pathname
398
- @manifest = manifest.read
399
- self.manifestFileName = manifest.basename.to_s
400
- else
401
- @manifest = manifest
402
- end
488
+ def manifest=(new_manifest)
489
+ # if its a string but not an xml plist, assume its a path
490
+ new_manifest = Pathname.new(new_manifest) if new_manifest.is_a?(String) && !new_manifest.start_with?('<?xml')
491
+ orig_manifest = manifest
492
+
493
+ new_xml =
494
+ if new_manifest.is_a? Pathname
495
+ new_manifest.read
496
+
497
+ elsif new_manifest.is_a? String
498
+ new_manifest
499
+
500
+ else
501
+ raise ArgumentError, 'Argument must be a Pathname, or a String containing a path or an XML plist'
502
+ end
503
+
504
+ @manifest = new_xml
505
+ note_unsaved_change :manifest, orig_manifest
403
506
  end
404
507
 
405
508
  # return the manifest as a ruby hash converted from the plist
@@ -413,24 +516,34 @@ module Jamf
413
516
  CFPropertyList.native_types(CFPropertyList::List.new(data: manifest).value)
414
517
  end
415
518
 
416
- # Generate a manifest plist (xml) for this package, update the #manifest attribute,
417
- # and assign an appropriate #manifestFileName.
519
+ # Generate a manifest plist (xml) for this package from a local .pkg file,
520
+ # and update the #manifest and #manifestFileName attributes
418
521
  #
419
522
  # Afterwards, you will need to call #save on the object to save the new values to
420
523
  # the server.
421
524
  #
525
+ # See also #manifest= for setting the manifest from a file or string.
526
+ #
422
527
  # The download URL used in the manifest will be the default for the class
423
- # (if you have set one) or the URL passed in, with the fileName appended. The
424
- # class default may come from the ruby-jss config, or be set directly on the class.
528
+ # (if you have set one) usually with the fileName appended. The
529
+ # class default may come from the ruby-jss config, or be set directly on the class,
530
+ # see JPackage.default_manifest_base_url=
425
531
  #
426
532
  # Unless set explicitly afterward using #manifestFileName= the manifest filename
427
533
  # will be the fileName of the Package object, with spaces converted to dashes,
428
534
  # followed by MANIFEST_FILENAME_DEFAULT_SUFFIX.
429
535
  # e.g. my-app.pkg-manifest.plist
430
536
  #
431
- # You can also generate the manifest when uploading the pkg file by providing
432
- # appropriate options to #upload,
433
- # in which case the new values will be saved to the Jamf Pro server automatically.
537
+ # By default, this method is invoked when uploading the pkg file using #upload
538
+ # and the opts will be passed from that method to this one.
539
+ # When invoked from #upload, the new values will be saved to the Jamf Pro server automatically.
540
+ #
541
+ # The manifests generated by this method are suitable for use in MDM deployments.
542
+ #
543
+ # If you don't provide a bundle_identifier, it will be generated from the packageName,
544
+ # prefixed with 'com.pixar.ruby-jss.' and with spaces converted to dashes.
545
+ #
546
+ # If you don't provide a bundle_version, it will be '0'
434
547
  #
435
548
  # @param filepath [String, Pathname] the path to a local copy of the package file for which
436
549
  # this manifest is being generated. This MUST match the one uploaded to the server, as it is
@@ -450,10 +563,12 @@ module Jamf
450
563
  # A common chunk size is 10MB, or 1024 * 1024 * 10.
451
564
  # NOTE: Not all distribution points support chunked downloads.
452
565
  #
453
- # @option opts bundle_identifier [String, Symbol] the bundle identifier of the package,
454
- # Should match that in the .pkg itself, but defaults to 'xolo.fileName'
566
+ # @option opts bundle_identifier [String, Symbol] The bundle identifier of the package,
567
+ # Should match that in the .pkg itself.
568
+ # Defaults to 'com.pixar.ruby-jss.packageName' where packageName is the
569
+ # packageName with whitespace converted to dashes.
455
570
  #
456
- # @option opts bundle_version [String, Symbol] the version of the package.
571
+ # @option opts bundle_version [String] the version of the package.
457
572
  # Defaults to '0'
458
573
  #
459
574
  # @option opts subtitle [String] a subtitle for the package, optional
@@ -470,10 +585,12 @@ module Jamf
470
585
  validate_local_file(file)
471
586
 
472
587
  filesize = file.size
473
- url = parse_manifest_url opts[:url], append_filename: opts[:append_filename_to_url]
474
588
 
475
589
  # make the manifest
476
590
  new_manifest = MANIFEST_PLIST_TEMPLATE.dup
591
+
592
+ url = build_manifest_url opts[:url], append_filename: opts[:append_filename_to_url]
593
+
477
594
  new_manifest[:items][0][:assets][0]['url'] = url.to_s
478
595
 
479
596
  # get the checksum(s)
@@ -486,25 +603,14 @@ module Jamf
486
603
  new_manifest[:items][0][:metadata]['title'] = packageName
487
604
  new_manifest[:items][0][:metadata]['subtitle'] = opts[:subtitle] if opts[:subtitle]
488
605
  new_manifest[:items][0][:metadata]['sizeInBytes'] = filesize
489
- new_manifest[:items][0][:metadata]['bundle-identifier'] = opts[:bundle_identifier] || "xolo.#{fileName}"
490
- new_manifest[:items][0][:metadata]['bundle-version'] = opts[:bundle_version] || '0'
491
-
492
- # TESTING - store the whole-file checksum in
493
- # manifest[:items][0][:metadata]['sha256-whole']. taking it from
494
- # manifest[:items][0][:assets][0]['sha256s'][0], if available, or generate it if needed
495
- # It is used by the deploy_via_mdm method.
496
- # The test will be to deploy this pkg via MDM in a prestage, and see
497
- # if the 'unknown' hash key 'sha256-whole' causes a problem installing.
498
- new_manifest[:items][0][:metadata]['sha256-whole'] =
499
- if new_manifest[:items][0][:assets][0]['sha256s'].size == 1
500
- new_manifest[:items][0][:assets][0]['sha256s'][0]
501
- else
502
- new_manifest[:items][0][:assets][0]['sha256s'] = [Digest::SHA256.hexdigest(file.read)]
503
- end
606
+ new_manifest[:items][0][:metadata]['bundle-identifier'] =
607
+ (opts[:bundle_identifier] || "#{MANIFEST_BUNDLE_ID_PREFIX}#{packageName.gsub(/\s+/, '-')}")
608
+ new_manifest[:items][0][:metadata]['bundle-version'] = opts[:bundle_version] if opts[:bundle_version]
504
609
 
505
610
  plist = CFPropertyList::List.new
506
611
  plist.value = CFPropertyList.guess(new_manifest)
507
- self.manifest = plist.to_str CFPropertyList::List::FORMAT_XML, formatted: true
612
+ self.manifest = plist.to_str(CFPropertyList::List::FORMAT_XML, formatted: true)
613
+
508
614
  self.manifestFileName = default_manifestFileName
509
615
  end
510
616
 
@@ -524,12 +630,16 @@ module Jamf
524
630
  # the url is not valid.
525
631
  #
526
632
  # @param given_url [String] the URL to use, if provided
633
+ # @param append_filename [Boolean] should the filename be appended to the URL?
527
634
  #
528
635
  # @return [URI] the URI object for the URL
529
636
  ##############################
530
- def parse_manifest_url(given_url = nil, append_filename: true)
637
+ def build_manifest_url(given_url = nil, append_filename: true)
531
638
  url = given_url || self.class.default_manifest_base_url
532
- raise ArgumentError, 'No URL provided for manifest' unless url
639
+ unless url
640
+ raise ArgumentError,
641
+ 'No URL for manifest. Pass one with url: or set one with Jamf::JPackage.default_manifest_base_url=, or set package_manifest_base_url in the ruby-jss.conf file.'
642
+ end
533
643
 
534
644
  # append the filename to the URL if needed
535
645
  url = "#{url.to_s.chomp('/')}/#{CGI.escape fileName}" unless append_filename == false
@@ -537,9 +647,10 @@ module Jamf
537
647
  # check validity and return
538
648
  URI.parse url
539
649
  end
540
- private :parse_manifest_url
650
+ private :build_manifest_url
541
651
 
542
- # calculate the manifest checksum[s] for a given file, and store in the manifest data
652
+ # calculate the manifest checksum[s] for a given file, and store in the manifest data.
653
+ # We only do SHA256, but Apple supports MD5 as well.
543
654
  #
544
655
  # @param file [Pathname] the path to the file to checksum
545
656
  # @param chunk_size [Integer] the size of each chunk in the manifest, in bytes.
@@ -564,6 +675,18 @@ module Jamf
564
675
  new_manifest[:items][0][:assets][0]['sha256-size'] = file.size
565
676
  new_manifest[:items][0][:assets][0]['sha256s'] = [Digest::SHA256.hexdigest(file.read)]
566
677
  end
678
+
679
+ # Store the whole-file checksum in
680
+ # manifest[:items][0][:metadata]['sha256-whole']. taking it from
681
+ # manifest[:items][0][:assets][0]['sha256s'][0], if available, or generate it if needed
682
+ # It is used by the deploy_via_mdm method.
683
+ # This value is required for MDM deployments, even if the file is chunked in the manifest.
684
+ new_manifest[:items][0][:metadata]['sha256-whole'] =
685
+ if new_manifest[:items][0][:assets][0]['sha256s'].size == 1
686
+ new_manifest[:items][0][:assets][0]['sha256s'][0]
687
+ else
688
+ Digest::SHA256.hexdigest(file.read)
689
+ end
567
690
  end
568
691
  private :calculate_manifest_checksums
569
692
 
@@ -583,8 +706,10 @@ module Jamf
583
706
  end
584
707
  private :append_manifest_image
585
708
 
586
- # Upload a package file to Jamf Pro for this package object.
587
- # After uploading, the upload response is in the @upload_response attribute.
709
+ # Upload a package file to Jamf Pro.
710
+ #
711
+ # WARNING: This will automatically call #save, saving any pending changes to
712
+ # the Jamf Pro server!
588
713
  #
589
714
  # This uses the Jamf Pro API to upload the file via the package/upload endpoint.
590
715
  # If you don't use an appropriate primary distribution point, this may not work.
@@ -594,44 +719,44 @@ module Jamf
594
719
  # If that filename is in use by some other package, you'll get an error:
595
720
  # Field: fileName, Error: DUPLICATE_FIELD duplicate name
596
721
  #
597
- # IMPORTANT: This will automatically call #save at least once, and possibly twice.
722
+ # This will automatically call #save at least once, and possibly twice.
598
723
  # First, in order to ensure the correct fileName in Jamf based on the file being uploaded,
599
724
  # and second, in order to update the checksum and manifest in Jamf Pro, if needed.
600
725
  # *** Any other outstanding changes will also be saved!
601
726
  #
727
+ # After uploading, the response from the server is in the #upload_response attribute,
728
+ # with a timestamp added to the data from the API.
729
+ #
602
730
  # @param filepath [String, Pathname] the path to the package file to upload
603
731
  #
604
732
  # @param opts[Hash] a hash of keyword arguments
605
733
  #
606
734
  # @option opts :update_checksum [Boolean] update the checksum of the package in Jamf Pro.
607
735
  # Defaults to true. All new checksums are SHA_512.
736
+ # WARNING: If you set this to false, the checksum in the object will not be updated
737
+ # and installs may fail. Be sure to set it to the correct value yourself.
608
738
  #
609
739
  # @option opts :update_manifest [Boolean] update the manifest of the package in Jamf Pro
610
- # Defaults to true
740
+ # Defaults to true.
741
+ # WARNING: If you set this to false, the manifest in the object will not be updated
742
+ # and PreStage & MDM deployments may fail. Be sure to set it to the correct value
743
+ # using #generate_manifest or #manifest= yourself.
611
744
  #
612
- # @options opts url [String] the URL where the package will be downloaded from,
613
- # defaults to the class default
745
+ # @options opts url [String] See #generate_manifest
614
746
  #
615
- # @option opts append_filename_to_url [Boolean] should the fileName be appended to the URL,
616
- # defaults to true.
617
- # If false, the url given must be the full URL to download the individual package file.
747
+ # @option opts append_filename_to_url [Boolean] See #generate_manifest
618
748
  #
619
- # @option opts chunk_size [Integer] the size of each chunk in the manifest, in bytes.
620
- # If omitted, the whole file will be checksummed at once and downloads will not be chunked.
621
- # A common chunk size is 10MB, or 1024 * 1024 * 10.
622
- # NOTE: Not all distribution points support chunked downloads.
749
+ # @option opts chunk_size [Integer] See #generate_manifest
623
750
  #
624
- # @option opts bundle_identifier [String, Symbol] the bundle identifier of the package,
625
- # Should match that in the .pkg itself, but defaults to 'xolo.fileName'
751
+ # @option opts bundle_identifier [String] See #generate_manifest
626
752
  #
627
- # @option opts bundle_version [String, Symbol] the version of the package.
628
- # Defaults to '0'
753
+ # @option opts bundle_version [String] See #generate_manifest
629
754
  #
630
- # @option opts subtitle [String] a subtitle for the package, optional
755
+ # @option opts subtitle [String] See #generate_manifest
631
756
  #
632
- # @option opts full_size_image_url [String] optional, used during MDM deployment
757
+ # @option opts full_size_image_url [String] See #generate_manifest
633
758
  #
634
- # @option opts display_image_url [String] optional, used during MDM deployment
759
+ # @option opts display_image_url [String] See #generate_manifest
635
760
  #
636
761
  # @return [void]
637
762
  ##############################
@@ -640,70 +765,72 @@ module Jamf
640
765
  validate_local_file(file)
641
766
 
642
767
  # update the filename if needed
643
- # must happen before the upload
644
- real_filename = file.basename.to_s
645
- self.fileName = real_filename unless fileName == real_filename
646
- save
768
+ # must happen before the upload so it matches the file being uploaded
769
+ self.fileName = file.basename.to_s
647
770
 
648
- # upload the file
649
- @upload_response = cnx.jp_upload("#{get_path}/#{UPLOAD_ENDPOINT}", file)
650
- @upload_response[:time] = Time.now
771
+ # We must save the checksum and manifest to the server before uploading
772
+ # the file, because otherwise jamf will likely overwrite the manifest
773
+ # after it uploads to the primary distribution point.
651
774
 
652
775
  # recalulate the checksum unless told no to
776
+ # NOTE: It appears that the checksum will always be recaluclated by
777
+ # the Jamf Pro server, as MD5. If you really want our default SHA512,
778
+ # then do this again later, manually.
653
779
  recalculate_checksum(file) unless opts[:update_checksum] == false
654
780
 
655
- # generate a manifest if needed
656
- generate_manifest file, **opts unless opts[:update_manifest] == false
781
+ # generate a manifest using the new file
782
+ generate_manifest(file, **opts) unless opts[:update_manifest] == false
657
783
 
658
- # save the new checksum and manifest
784
+ # save the new fileName, checksum and manifest
659
785
  save
786
+
787
+ # upload the file
788
+ @upload_response = cnx.jp_upload("#{get_path}/#{UPLOAD_ENDPOINT}", file)
789
+ @upload_response[:time] = Time.now
790
+ @upload_response
660
791
  end
661
792
 
662
793
  # Deploy this package to computers or a group via MDM.
663
794
  #
664
795
  # REQUIREMENTS:
665
- # - The package must have a manifest, see #generate_manifest
796
+ # - The package must have a manifest with specific data. See #manifest=
797
+ # and #generate_manifest for details.
666
798
  # - The .pkg file must be a product archive (.pkg) built with Xcode or productbuild.
667
799
  # (it must contain a 'Distribution' file, usually generated by those tools)
668
800
  # Simple 'component' packages built with pkgbuild are not supported.
669
- # - The .pkg file must be signed with a Developer ID Installer certificate
801
+ # - The .pkg file must be signed with a trusted signing certificate
670
802
  #
671
- # This will send a command to install the package to one or more
672
- # computers, and/or the members of a single computer group.
803
+ # This will send an MDM InstallEnterpriseApplication command to install the package
804
+ # to one or more computers, and/or the members of a single computer group.
673
805
  #
674
806
  # @param computer_ids [Array<Integer>,Integer] The ids of the computers to deploy to
675
807
  #
676
808
  # @param group_id [Integer] The id of the computer group to deploy to
677
809
  #
678
- # @param managed [Boolean] Should the installed package be managed by Jamf Pro, default is false.
810
+ # @param managed [Boolean] Should the installed package be managed by Jamf Pro?
811
+ # Defaults to false. This seems to be for App Store apps only??
679
812
  #
680
- # @return [void]
813
+ # @return [Hash] the response from the server. see #deploy_response
681
814
  ##############################
682
815
  def deploy_via_mdm(computer_ids: nil, group_id: nil, managed: false)
683
- raise Jamf::MissingDataError, 'No manifest set for this package' unless manifest
684
- raise Jamf::NoSuchItemError, 'This package has no id, it must be saved in Jamf Pro before uploading' unless id
816
+ raise ArgumentError, 'No computer_ids or group_id provided' unless computer_ids || group_id
817
+ raise Jamf::MissingDataError, 'No manifest set for this package' if manifest.to_s.empty?
818
+ raise Jamf::NoSuchItemError, 'This package has no id, it must be saved in Jamf Pro before uploading' unless exist?
685
819
 
686
- # convert the manifest to a ruby hash
820
+ # convert the full manifest to a ruby hash
687
821
  parsed_manifest = manifest_hash
688
822
 
689
823
  # manifest data for the MDMDeploy command, which is a hash.
690
824
  # hopefully some day Jamf will just use the manifest for the pkg
691
825
  mdm_manifest = {}
692
- mdm_manifest['url'] = parsed_manifest['items'][0]['assets'][0]['url']
693
- # See the TESTING note in #generate_manifest
694
- mdm_manifest['hash'] = parsed_manifest['items'][0]['metadata']['sha256-whole']
695
- mdm_manifest['hashType'] = CHECKSUM_HASH_TYPE_SHA256_MDM_DEPLOY
696
- mdm_manifest['sizeInBytes'] = parsed_manifest['items'][0]['metadata']['sizeInBytes']
697
- mdm_manifest['bundleId'] = parsed_manifest['items'][0]['metadata']['bundle-identifier']
698
- mdm_manifest['bundleVersion'] = parsed_manifest['items'][0]['metadata']['bundle-version']
699
- mdm_manifest['title'] = parsed_manifest['items'][0]['metadata']['title']
700
-
701
- mdm_manifest['subtitle'] = parsed_manifest['items'][0]['metadata']['subtitle'] if parsed_manifest['items'][0]['metadata']['subtitle']
702
-
703
- parsed_manifest['items'][0]['assets'].each do |asset|
704
- mdm_manifest['fullSizeImageURL'] = asset['url'] if asset['kind'] == 'full-size-image'
705
- mdm_manifest['displayImageURL'] = asset['url'] if asset['kind'] == 'display-image'
706
- end
826
+ mdm_manifest['url'] = manifest_url_for_deployment(parsed_manifest)
827
+ mdm_manifest['hash'], mdm_manifest['hashType'] = manifest_checksum_for_deployment(parsed_manifest)
828
+ mdm_manifest['bundleId'] = manifest_bundle_identifier_for_deployment(parsed_manifest)
829
+ mdm_manifest['bundleVersion'] = manifest_bundle_version_for_deployment(parsed_manifest)
830
+ mdm_manifest['title'] = manifest_title_for_deployment(parsed_manifest)
831
+ mdm_manifest['sizeInBytes'] = manifest_size_for_deployment(parsed_manifest)
832
+
833
+ set_optional_mdm_manifest_values(parsed_manifest, mdm_manifest)
707
834
 
708
835
  # make sure the computers are in an array
709
836
  computer_ids = [computer_ids].flatten.compact.uniq
@@ -712,13 +839,100 @@ module Jamf
712
839
  payload = {
713
840
  manifest: mdm_manifest,
714
841
  installAsManaged: managed,
715
- devices: computer_ids,
716
- groupId: group_id.to_s
842
+ devices: computer_ids
717
843
  }
844
+ payload[:groupId] = group_id.to_s if group_id
845
+
718
846
  # send the command
719
847
  @deploy_response = cnx.post(DEPLOYMENT_ENDPOINT, payload)
720
848
  end
721
849
 
850
+ # the URL is required for MDM deployments
851
+ # @param parsed_manifest [Hash] the parsed manifest data as a ruby hash
852
+ # @return [String] the URL in the manifest for MDM deployments
853
+ #####################################
854
+ def manifest_url_for_deployment(parsed_manifest)
855
+ url = parsed_manifest.dig 'items', 0, 'assets', 0, 'url'
856
+ raise Jamf::MissingDataError, 'No URL in the manifest' unless url
857
+
858
+ url
859
+ end
860
+ private :manifest_url_for_deployment
861
+
862
+ # whole-file checksums are required for MDM deployments
863
+ # @return [Array<String>] the checksum and checksum type in the manifest for MDM deployments
864
+ #####################################
865
+ def manifest_checksum_for_deployment(parsed_manifest)
866
+ if whole = parsed_manifest.dig('items', 0, 'metadata', 'sha256-whole')
867
+ [whole, CHECKSUM_HASH_TYPE_SHA256_MDM_DEPLOY]
868
+ elsif whole = parsed_manifest.dig('items', 0, 'metadata', 'md5-whole')
869
+ [whole, CHECKSUM_HASH_TYPE_MD5]
870
+ else
871
+ raise Jamf::MissingDataError, 'No whole-file checksum in the manifest. Must have either sha256-whole or md5-whole in the metadata'
872
+ end
873
+ end
874
+ private :manifest_checksum_for_deployment
875
+
876
+ # size in bytes is required for MDM deployments
877
+ # @return [Integer] the size in bytes in the manifest for MDM deployments
878
+ #####################################
879
+ def manifest_size_for_deployment(parsed_manifest)
880
+ size = parsed_manifest.dig 'items', 0, 'metadata', 'sizeInBytes'
881
+ raise Jamf::MissingDataError, 'No sizeInBytes in the manifest metadata' unless size
882
+
883
+ size
884
+ end
885
+ private :manifest_size_for_deployment
886
+
887
+ # bundle identifier is required for MDM deployments
888
+ # @return [String] the bundle identifier in the manifest for MDM deployments
889
+ #####################################
890
+ def manifest_bundle_identifier_for_deployment(parsed_manifest)
891
+ bid = parsed_manifest.dig 'items', 0, 'metadata', 'bundle-identifier'
892
+ raise Jamf::MissingDataError, 'No bundle-identifier in the manifest metadata' unless bid
893
+
894
+ bid
895
+ end
896
+ private :manifest_bundle_identifier_for_deployment
897
+
898
+ # bundle version is required for MDM deployments
899
+ # @return [String] the bundle version in the manifest for MDM deployments
900
+ #####################################
901
+ def manifest_bundle_version_for_deployment(parsed_manifest)
902
+ bv = parsed_manifest.dig 'items', 0, 'metadata', 'bundle-version'
903
+ raise Jamf::MissingDataError, 'No bundle-version in the manifest metadata' unless bv
904
+
905
+ bv
906
+ end
907
+ private :manifest_bundle_version_for_deployment
908
+
909
+ # title is required for MDM deployments
910
+ # @return [String] the title in the manifest for MDM deployments
911
+ #####################################
912
+ def manifest_title_for_deployment(parsed_manifest)
913
+ ttl = parsed_manifest.dig 'items', 0, 'metadata', 'title'
914
+ raise Jamf::MissingDataError, 'No title in the manifest metadata' unless ttl
915
+
916
+ ttl
917
+ end
918
+ private :manifest_title_for_deployment
919
+
920
+ # set the optional values for the MDM deployment manifest
921
+ # @return [void]
922
+ #####################################
923
+ def set_optional_mdm_manifest_values(parsed_manifest, mdm_manifest)
924
+ # subtitle is optional for MDM deployments
925
+ sttl = parsed_manifest.dig 'items', 0, 'metadata', 'subtitle'
926
+ mdm_manifest['subtitle'] = sttl if sttl
927
+
928
+ # Images are optional for MDM deployments
929
+ parsed_manifest['items'][0]['assets']&.each do |asset|
930
+ mdm_manifest['fullSizeImageURL'] = asset['url'] if asset['kind'] == 'full-size-image'
931
+ mdm_manifest['displayImageURL'] = asset['url'] if asset['kind'] == 'display-image'
932
+ end
933
+ end
934
+ private :set_optional_mdm_manifest_values
935
+
722
936
  end # class
723
937
 
724
938
  end # module