ruby-jss 4.2.0b2 → 4.2.0

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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +25 -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 +307 -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 +8 -0
  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/composer.rb +114 -107
  98. data/lib/jamf/oapi_validate.rb +1 -0
  99. data/lib/jamf/version.rb +1 -1
  100. data/test/bin/runtests +2 -2
  101. data/test/lib/jamf_test/collection_tests.rb +10 -2
  102. data/test/tests/computer_group.rb +29 -12
  103. data/test/tests/{jp_building.rb → j_building.rb} +2 -2
  104. data/test/tests/j_package.rb +47 -0
  105. 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.
@@ -333,14 +355,42 @@ module Jamf
333
355
  #####################################
334
356
 
335
357
  # @return [Pathname] the local receipt when this pkg is installed by a policy
358
+ #############################
336
359
  def receipt
337
360
  # the receipt is the filename with any .zip extension removed.
338
361
  fileName ? (Jamf::Client::RECEIPTS_FOLDER + fileName.to_s.sub(/.zip$/, '')) : nil
339
362
  end
340
363
 
364
+ # Change the os_requirements field in the JSS
365
+ # E.g. 10.5, 10.5.3, 10.6.x
366
+ #
367
+ # Extra feature: Minumum OS's can now be specified as a
368
+ # string using the notation ">=10.6.7".
369
+ #
370
+ # @see Jamf.expand_min_os
371
+ #
372
+ # @param new_val [String,Array<String>] comma-separated string, or array of os versions
373
+ #
374
+ # @return [void]
375
+ #############################
376
+ def osRequirements=(new_val)
377
+ # make sure we have an array
378
+ new_val = [new_val].flatten.compact.uniq.map(&:to_s)
379
+ new_val.map! do |vers|
380
+ vers.start_with?('>=') ? Jamf.expand_min_os(vers) : vers
381
+ end
382
+
383
+ orig_osRequirements = osRequirements
384
+ @osRequirements = new_val.join(', ')
385
+ note_unsaved_change :osRequirements, orig_osRequirements
386
+ end
387
+
341
388
  # Recalculate the checksum of the package file from a given filepath, and update the
342
389
  # object's checksum and hashValue attributes.
343
390
  #
391
+ # NOTE: This updates the checksum used when installing via a Policy.
392
+ # The checksum(s) used when deploying via MDM is stored in the manifest.
393
+ #
344
394
  # You will need to call #save on the object to save the new checksum to the server.
345
395
  #
346
396
  # New checksums are always SHA512
@@ -368,38 +418,65 @@ module Jamf
368
418
  self.class.calculate_checksum(filepath, hashType) == checksum
369
419
  end
370
420
 
371
- # @return [String] The manifest file name or a default if none is set
421
+ # @return [String] A default if none is set explicitly
372
422
  ##############################
373
423
  def default_manifestFileName
374
424
  "#{fileName.gsub(' ', '-')}#{MANIFEST_FILENAME_DEFAULT_SUFFIX}"
375
425
  end
376
426
 
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
427
+ # Set the manifest from a local file or a String containing an XML plist.
428
+ # If from a file, the manifestFileName attribute is set to the filename
429
+ #
430
+ # To automatically generate a manifest plist for this package from a
431
+ # locally-readable .pkg file, use #generate_manifest
432
+ #
433
+ # All manifests require a valid URL for downloading the .pkg file when
434
+ # installing on a client.
435
+ #
436
+ # No validation of the manifest is done here.
379
437
  #
380
- # To generate a basic manifest plist for this package, use #generate_manifest
438
+ # DEPLOYING VIA MDM:
381
439
  #
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
440
+ # When using this method, if you want to be able to deploy the package using
441
+ # deploy_via_mdm, the manifest MUST include a metadata dictionary
442
+ # with at least the following keys:
443
+ # - 'kind' = 'software'
444
+ # - 'bundle-identifier' that preferably matches the bundle identifier of the pkg
445
+ # - 'bundle-version' = that preferably matches the version of the pkg
446
+ # - 'title' = the name of the pkg or what it installs
447
+ # - 'sizeInBytes' = the size of the .pkg in bytes
448
+ # as well as one of these non-standard keys:
449
+ # - 'sha256-whole' = the SHA256 digest of the whole file, regardless of chunking data in the 'assets' array
450
+ # OR
451
+ # - 'md5-whole' = the MD5 digest of the whole file, regardless of chunking data in the 'assets' array
391
452
  #
392
- # @param manifest [String, Pathname] the manifest plist data or path to a local file
453
+ # The non-standard keys are because the Jamf Pro API endpoint for deploying via MDM requires
454
+ # a whole-file checksum even if the file is chunked in the manifest.
455
+ #
456
+ # See the MANIFEST_PLIST_TEMPLATE constant for an example of the data structure (as a ruby hash, not a plist)
457
+ #
458
+ # @param new_manifest [String, Pathname] the manifest plist data or path to a local file
393
459
  #
394
460
  # @return [void]
395
461
  ##############################
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
462
+ def manifest=(new_manifest)
463
+ # if its a string but not an xml plist, assume its a path
464
+ new_manifest = Pathname.new(new_manifest) if new_manifest.is_a?(String) && !new_manifest.start_with?('<?xml')
465
+ orig_manifest = manifest
466
+
467
+ new_xml =
468
+ if new_manifest.is_a? Pathname
469
+ new_manifest.read
470
+
471
+ elsif new_manifest.is_a? String
472
+ new_manifest
473
+
474
+ else
475
+ raise ArgumentError, 'Argument must be a Pathname, or a String containing a path or an XML plist'
476
+ end
477
+
478
+ @manifest = new_xml
479
+ note_unsaved_change :manifest, orig_manifest
403
480
  end
404
481
 
405
482
  # return the manifest as a ruby hash converted from the plist
@@ -413,24 +490,34 @@ module Jamf
413
490
  CFPropertyList.native_types(CFPropertyList::List.new(data: manifest).value)
414
491
  end
415
492
 
416
- # Generate a manifest plist (xml) for this package, update the #manifest attribute,
417
- # and assign an appropriate #manifestFileName.
493
+ # Generate a manifest plist (xml) for this package from a local .pkg file,
494
+ # and update the #manifest and #manifestFileName attributes
418
495
  #
419
496
  # Afterwards, you will need to call #save on the object to save the new values to
420
497
  # the server.
421
498
  #
499
+ # See also #manifest= for setting the manifest from a file or string.
500
+ #
422
501
  # 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.
502
+ # (if you have set one) usually with the fileName appended. The
503
+ # class default may come from the ruby-jss config, or be set directly on the class,
504
+ # see JPackage.default_manifest_base_url=
425
505
  #
426
506
  # Unless set explicitly afterward using #manifestFileName= the manifest filename
427
507
  # will be the fileName of the Package object, with spaces converted to dashes,
428
508
  # followed by MANIFEST_FILENAME_DEFAULT_SUFFIX.
429
509
  # e.g. my-app.pkg-manifest.plist
430
510
  #
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.
511
+ # By default, this method is invoked when uploading the pkg file using #upload
512
+ # and the opts will be passed from that method to this one.
513
+ # When invoked from #upload, the new values will be saved to the Jamf Pro server automatically.
514
+ #
515
+ # The manifests generated by this method are suitable for use in MDM deployments.
516
+ #
517
+ # If you don't provide a bundle_identifier, it will be generated from the packageName,
518
+ # prefixed with 'com.pixar.ruby-jss.' and with spaces converted to dashes.
519
+ #
520
+ # If you don't provide a bundle_version, it will be '0'
434
521
  #
435
522
  # @param filepath [String, Pathname] the path to a local copy of the package file for which
436
523
  # this manifest is being generated. This MUST match the one uploaded to the server, as it is
@@ -450,10 +537,12 @@ module Jamf
450
537
  # A common chunk size is 10MB, or 1024 * 1024 * 10.
451
538
  # NOTE: Not all distribution points support chunked downloads.
452
539
  #
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'
540
+ # @option opts bundle_identifier [String, Symbol] The bundle identifier of the package,
541
+ # Should match that in the .pkg itself.
542
+ # Defaults to 'com.pixar.ruby-jss.packageName' where packageName is the
543
+ # packageName with whitespace converted to dashes.
455
544
  #
456
- # @option opts bundle_version [String, Symbol] the version of the package.
545
+ # @option opts bundle_version [String] the version of the package.
457
546
  # Defaults to '0'
458
547
  #
459
548
  # @option opts subtitle [String] a subtitle for the package, optional
@@ -470,10 +559,12 @@ module Jamf
470
559
  validate_local_file(file)
471
560
 
472
561
  filesize = file.size
473
- url = parse_manifest_url opts[:url], append_filename: opts[:append_filename_to_url]
474
562
 
475
563
  # make the manifest
476
564
  new_manifest = MANIFEST_PLIST_TEMPLATE.dup
565
+
566
+ url = build_manifest_url opts[:url], append_filename: opts[:append_filename_to_url]
567
+
477
568
  new_manifest[:items][0][:assets][0]['url'] = url.to_s
478
569
 
479
570
  # get the checksum(s)
@@ -486,25 +577,14 @@ module Jamf
486
577
  new_manifest[:items][0][:metadata]['title'] = packageName
487
578
  new_manifest[:items][0][:metadata]['subtitle'] = opts[:subtitle] if opts[:subtitle]
488
579
  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
580
+ new_manifest[:items][0][:metadata]['bundle-identifier'] =
581
+ (opts[:bundle_identifier] || "#{MANIFEST_BUNDLE_ID_PREFIX}#{packageName.gsub(/\s+/, '-')}")
582
+ new_manifest[:items][0][:metadata]['bundle-version'] = opts[:bundle_version] if opts[:bundle_version]
504
583
 
505
584
  plist = CFPropertyList::List.new
506
585
  plist.value = CFPropertyList.guess(new_manifest)
507
- self.manifest = plist.to_str CFPropertyList::List::FORMAT_XML, formatted: true
586
+ self.manifest = plist.to_str(CFPropertyList::List::FORMAT_XML, formatted: true)
587
+
508
588
  self.manifestFileName = default_manifestFileName
509
589
  end
510
590
 
@@ -524,12 +604,16 @@ module Jamf
524
604
  # the url is not valid.
525
605
  #
526
606
  # @param given_url [String] the URL to use, if provided
607
+ # @param append_filename [Boolean] should the filename be appended to the URL?
527
608
  #
528
609
  # @return [URI] the URI object for the URL
529
610
  ##############################
530
- def parse_manifest_url(given_url = nil, append_filename: true)
611
+ def build_manifest_url(given_url = nil, append_filename: true)
531
612
  url = given_url || self.class.default_manifest_base_url
532
- raise ArgumentError, 'No URL provided for manifest' unless url
613
+ unless url
614
+ raise ArgumentError,
615
+ '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.'
616
+ end
533
617
 
534
618
  # append the filename to the URL if needed
535
619
  url = "#{url.to_s.chomp('/')}/#{CGI.escape fileName}" unless append_filename == false
@@ -537,9 +621,10 @@ module Jamf
537
621
  # check validity and return
538
622
  URI.parse url
539
623
  end
540
- private :parse_manifest_url
624
+ private :build_manifest_url
541
625
 
542
- # calculate the manifest checksum[s] for a given file, and store in the manifest data
626
+ # calculate the manifest checksum[s] for a given file, and store in the manifest data.
627
+ # We only do SHA256, but Apple supports MD5 as well.
543
628
  #
544
629
  # @param file [Pathname] the path to the file to checksum
545
630
  # @param chunk_size [Integer] the size of each chunk in the manifest, in bytes.
@@ -564,6 +649,18 @@ module Jamf
564
649
  new_manifest[:items][0][:assets][0]['sha256-size'] = file.size
565
650
  new_manifest[:items][0][:assets][0]['sha256s'] = [Digest::SHA256.hexdigest(file.read)]
566
651
  end
652
+
653
+ # Store the whole-file checksum in
654
+ # manifest[:items][0][:metadata]['sha256-whole']. taking it from
655
+ # manifest[:items][0][:assets][0]['sha256s'][0], if available, or generate it if needed
656
+ # It is used by the deploy_via_mdm method.
657
+ # This value is required for MDM deployments, even if the file is chunked in the manifest.
658
+ new_manifest[:items][0][:metadata]['sha256-whole'] =
659
+ if new_manifest[:items][0][:assets][0]['sha256s'].size == 1
660
+ new_manifest[:items][0][:assets][0]['sha256s'][0]
661
+ else
662
+ Digest::SHA256.hexdigest(file.read)
663
+ end
567
664
  end
568
665
  private :calculate_manifest_checksums
569
666
 
@@ -583,8 +680,10 @@ module Jamf
583
680
  end
584
681
  private :append_manifest_image
585
682
 
586
- # Upload a package file to Jamf Pro for this package object.
587
- # After uploading, the upload response is in the @upload_response attribute.
683
+ # Upload a package file to Jamf Pro.
684
+ #
685
+ # WARNING: This will automatically call #save, saving any pending changes to
686
+ # the Jamf Pro server!
588
687
  #
589
688
  # This uses the Jamf Pro API to upload the file via the package/upload endpoint.
590
689
  # If you don't use an appropriate primary distribution point, this may not work.
@@ -594,44 +693,44 @@ module Jamf
594
693
  # If that filename is in use by some other package, you'll get an error:
595
694
  # Field: fileName, Error: DUPLICATE_FIELD duplicate name
596
695
  #
597
- # IMPORTANT: This will automatically call #save at least once, and possibly twice.
696
+ # This will automatically call #save at least once, and possibly twice.
598
697
  # First, in order to ensure the correct fileName in Jamf based on the file being uploaded,
599
698
  # and second, in order to update the checksum and manifest in Jamf Pro, if needed.
600
699
  # *** Any other outstanding changes will also be saved!
601
700
  #
701
+ # After uploading, the response from the server is in the #upload_response attribute,
702
+ # with a timestamp added to the data from the API.
703
+ #
602
704
  # @param filepath [String, Pathname] the path to the package file to upload
603
705
  #
604
706
  # @param opts[Hash] a hash of keyword arguments
605
707
  #
606
708
  # @option opts :update_checksum [Boolean] update the checksum of the package in Jamf Pro.
607
709
  # Defaults to true. All new checksums are SHA_512.
710
+ # WARNING: If you set this to false, the checksum in the object will not be updated
711
+ # and installs may fail. Be sure to set it to the correct value yourself.
608
712
  #
609
713
  # @option opts :update_manifest [Boolean] update the manifest of the package in Jamf Pro
610
- # Defaults to true
714
+ # Defaults to true.
715
+ # WARNING: If you set this to false, the manifest in the object will not be updated
716
+ # and PreStage & MDM deployments may fail. Be sure to set it to the correct value
717
+ # using #generate_manifest or #manifest= yourself.
611
718
  #
612
- # @options opts url [String] the URL where the package will be downloaded from,
613
- # defaults to the class default
719
+ # @options opts url [String] See #generate_manifest
614
720
  #
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.
721
+ # @option opts append_filename_to_url [Boolean] See #generate_manifest
618
722
  #
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.
723
+ # @option opts chunk_size [Integer] See #generate_manifest
623
724
  #
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'
725
+ # @option opts bundle_identifier [String] See #generate_manifest
626
726
  #
627
- # @option opts bundle_version [String, Symbol] the version of the package.
628
- # Defaults to '0'
727
+ # @option opts bundle_version [String] See #generate_manifest
629
728
  #
630
- # @option opts subtitle [String] a subtitle for the package, optional
729
+ # @option opts subtitle [String] See #generate_manifest
631
730
  #
632
- # @option opts full_size_image_url [String] optional, used during MDM deployment
731
+ # @option opts full_size_image_url [String] See #generate_manifest
633
732
  #
634
- # @option opts display_image_url [String] optional, used during MDM deployment
733
+ # @option opts display_image_url [String] See #generate_manifest
635
734
  #
636
735
  # @return [void]
637
736
  ##############################
@@ -640,70 +739,72 @@ module Jamf
640
739
  validate_local_file(file)
641
740
 
642
741
  # 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
742
+ # must happen before the upload so it matches the file being uploaded
743
+ self.fileName = file.basename.to_s
647
744
 
648
- # upload the file
649
- @upload_response = cnx.jp_upload("#{get_path}/#{UPLOAD_ENDPOINT}", file)
650
- @upload_response[:time] = Time.now
745
+ # We must save the checksum and manifest to the server before uploading
746
+ # the file, because otherwise jamf will likely overwrite the manifest
747
+ # after it uploads to the primary distribution point.
651
748
 
652
749
  # recalulate the checksum unless told no to
750
+ # NOTE: It appears that the checksum will always be recaluclated by
751
+ # the Jamf Pro server, as MD5. If you really want our default SHA512,
752
+ # then do this again later, manually.
653
753
  recalculate_checksum(file) unless opts[:update_checksum] == false
654
754
 
655
- # generate a manifest if needed
656
- generate_manifest file, **opts unless opts[:update_manifest] == false
755
+ # generate a manifest using the new file
756
+ generate_manifest(file, **opts) unless opts[:update_manifest] == false
657
757
 
658
- # save the new checksum and manifest
758
+ # save the new fileName, checksum and manifest
659
759
  save
760
+
761
+ # upload the file
762
+ @upload_response = cnx.jp_upload("#{get_path}/#{UPLOAD_ENDPOINT}", file)
763
+ @upload_response[:time] = Time.now
764
+ @upload_response
660
765
  end
661
766
 
662
767
  # Deploy this package to computers or a group via MDM.
663
768
  #
664
769
  # REQUIREMENTS:
665
- # - The package must have a manifest, see #generate_manifest
770
+ # - The package must have a manifest with specific data. See #manifest=
771
+ # and #generate_manifest for details.
666
772
  # - The .pkg file must be a product archive (.pkg) built with Xcode or productbuild.
667
773
  # (it must contain a 'Distribution' file, usually generated by those tools)
668
774
  # Simple 'component' packages built with pkgbuild are not supported.
669
- # - The .pkg file must be signed with a Developer ID Installer certificate
775
+ # - The .pkg file must be signed with a trusted signing certificate
670
776
  #
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.
777
+ # This will send an MDM InstallEnterpriseApplication command to install the package
778
+ # to one or more computers, and/or the members of a single computer group.
673
779
  #
674
780
  # @param computer_ids [Array<Integer>,Integer] The ids of the computers to deploy to
675
781
  #
676
782
  # @param group_id [Integer] The id of the computer group to deploy to
677
783
  #
678
- # @param managed [Boolean] Should the installed package be managed by Jamf Pro, default is false.
784
+ # @param managed [Boolean] Should the installed package be managed by Jamf Pro?
785
+ # Defaults to false. This seems to be for App Store apps only??
679
786
  #
680
- # @return [void]
787
+ # @return [Hash] the response from the server. see #deploy_response
681
788
  ##############################
682
789
  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
790
+ raise ArgumentError, 'No computer_ids or group_id provided' unless computer_ids || group_id
791
+ raise Jamf::MissingDataError, 'No manifest set for this package' if manifest.to_s.empty?
792
+ raise Jamf::NoSuchItemError, 'This package has no id, it must be saved in Jamf Pro before uploading' unless exist?
685
793
 
686
- # convert the manifest to a ruby hash
794
+ # convert the full manifest to a ruby hash
687
795
  parsed_manifest = manifest_hash
688
796
 
689
797
  # manifest data for the MDMDeploy command, which is a hash.
690
798
  # hopefully some day Jamf will just use the manifest for the pkg
691
799
  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
800
+ mdm_manifest['url'] = manifest_url_for_deployment(parsed_manifest)
801
+ mdm_manifest['hash'], mdm_manifest['hashType'] = manifest_checksum_for_deployment(parsed_manifest)
802
+ mdm_manifest['bundleId'] = manifest_bundle_identifier_for_deployment(parsed_manifest)
803
+ mdm_manifest['bundleVersion'] = manifest_bundle_version_for_deployment(parsed_manifest)
804
+ mdm_manifest['title'] = manifest_title_for_deployment(parsed_manifest)
805
+ mdm_manifest['sizeInBytes'] = manifest_size_for_deployment(parsed_manifest)
806
+
807
+ set_optional_mdm_manifest_values(parsed_manifest, mdm_manifest)
707
808
 
708
809
  # make sure the computers are in an array
709
810
  computer_ids = [computer_ids].flatten.compact.uniq
@@ -712,13 +813,100 @@ module Jamf
712
813
  payload = {
713
814
  manifest: mdm_manifest,
714
815
  installAsManaged: managed,
715
- devices: computer_ids,
716
- groupId: group_id.to_s
816
+ devices: computer_ids
717
817
  }
818
+ payload[:groupId] = group_id.to_s if group_id
819
+
718
820
  # send the command
719
821
  @deploy_response = cnx.post(DEPLOYMENT_ENDPOINT, payload)
720
822
  end
721
823
 
824
+ # the URL is required for MDM deployments
825
+ # @param parsed_manifest [Hash] the parsed manifest data as a ruby hash
826
+ # @return [String] the URL in the manifest for MDM deployments
827
+ #####################################
828
+ def manifest_url_for_deployment(parsed_manifest)
829
+ url = parsed_manifest.dig 'items', 0, 'assets', 0, 'url'
830
+ raise Jamf::MissingDataError, 'No URL in the manifest' unless url
831
+
832
+ url
833
+ end
834
+ private :manifest_url_for_deployment
835
+
836
+ # whole-file checksums are required for MDM deployments
837
+ # @return [Array<String>] the checksum and checksum type in the manifest for MDM deployments
838
+ #####################################
839
+ def manifest_checksum_for_deployment(parsed_manifest)
840
+ if whole = parsed_manifest.dig('items', 0, 'metadata', 'sha256-whole')
841
+ [whole, CHECKSUM_HASH_TYPE_SHA256_MDM_DEPLOY]
842
+ elsif whole = parsed_manifest.dig('items', 0, 'metadata', 'md5-whole')
843
+ [whole, CHECKSUM_HASH_TYPE_MD5]
844
+ else
845
+ raise Jamf::MissingDataError, 'No whole-file checksum in the manifest. Must have either sha256-whole or md5-whole in the metadata'
846
+ end
847
+ end
848
+ private :manifest_checksum_for_deployment
849
+
850
+ # size in bytes is required for MDM deployments
851
+ # @return [Integer] the size in bytes in the manifest for MDM deployments
852
+ #####################################
853
+ def manifest_size_for_deployment(parsed_manifest)
854
+ size = parsed_manifest.dig 'items', 0, 'metadata', 'sizeInBytes'
855
+ raise Jamf::MissingDataError, 'No sizeInBytes in the manifest metadata' unless size
856
+
857
+ size
858
+ end
859
+ private :manifest_size_for_deployment
860
+
861
+ # bundle identifier is required for MDM deployments
862
+ # @return [String] the bundle identifier in the manifest for MDM deployments
863
+ #####################################
864
+ def manifest_bundle_identifier_for_deployment(parsed_manifest)
865
+ bid = parsed_manifest.dig 'items', 0, 'metadata', 'bundle-identifier'
866
+ raise Jamf::MissingDataError, 'No bundle-identifier in the manifest metadata' unless bid
867
+
868
+ bid
869
+ end
870
+ private :manifest_bundle_identifier_for_deployment
871
+
872
+ # bundle version is required for MDM deployments
873
+ # @return [String] the bundle version in the manifest for MDM deployments
874
+ #####################################
875
+ def manifest_bundle_version_for_deployment(parsed_manifest)
876
+ bv = parsed_manifest.dig 'items', 0, 'metadata', 'bundle-version'
877
+ raise Jamf::MissingDataError, 'No bundle-version in the manifest metadata' unless bv
878
+
879
+ bv
880
+ end
881
+ private :manifest_bundle_version_for_deployment
882
+
883
+ # title is required for MDM deployments
884
+ # @return [String] the title in the manifest for MDM deployments
885
+ #####################################
886
+ def manifest_title_for_deployment(parsed_manifest)
887
+ ttl = parsed_manifest.dig 'items', 0, 'metadata', 'title'
888
+ raise Jamf::MissingDataError, 'No title in the manifest metadata' unless ttl
889
+
890
+ ttl
891
+ end
892
+ private :manifest_title_for_deployment
893
+
894
+ # set the optional values for the MDM deployment manifest
895
+ # @return [void]
896
+ #####################################
897
+ def set_optional_mdm_manifest_values(parsed_manifest, mdm_manifest)
898
+ # subtitle is optional for MDM deployments
899
+ sttl = parsed_manifest.dig 'items', 0, 'metadata', 'subtitle'
900
+ mdm_manifest['subtitle'] = sttl if sttl
901
+
902
+ # Images are optional for MDM deployments
903
+ parsed_manifest['items'][0]['assets']&.each do |asset|
904
+ mdm_manifest['fullSizeImageURL'] = asset['url'] if asset['kind'] == 'full-size-image'
905
+ mdm_manifest['displayImageURL'] = asset['url'] if asset['kind'] == 'display-image'
906
+ end
907
+ end
908
+ private :set_optional_mdm_manifest_values
909
+
722
910
  end # class
723
911
 
724
912
  end # module