xolo-admin 1.0.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.
@@ -0,0 +1,1032 @@
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
+ # frozen_string_literal: true
8
+
9
+ module Xolo
10
+
11
+ module Admin
12
+
13
+ # A collection of methods to convert and validate values used in Xolo.
14
+ #
15
+ # These methods will convert target values if possible, e.g. if a value
16
+ # should be an Integer, a String containing an integer will be converted
17
+ # before validation.
18
+ #
19
+ # If the converted value is valid, it will be returned, otherwise a
20
+ # Xolo::InvalidDataError will be raised.
21
+ #
22
+ module Validate
23
+
24
+ # Constants
25
+ #########################
26
+ #########################
27
+
28
+ # True can be specified as 'true' or 'y' or 'yes'
29
+ # case insensitively. Anything else is false.
30
+ TRUE_RE = /(\Atrue\z)|(\Ay(es)?\z)/i.freeze
31
+
32
+ # Convenience - constants from classes
33
+
34
+ TITLE_ATTRS = Xolo::Admin::Title::ATTRIBUTES
35
+ VERSION_ATTRS = Xolo::Admin::Version::ATTRIBUTES
36
+
37
+ # Self Service icons must be one of these mime types,
38
+ # as determined by the output of `file -b -I`
39
+ SSVC_ICON_MIME_TYPES = %w[
40
+ image/jpeg
41
+ image/png
42
+ image/gif
43
+ ].freeze
44
+
45
+ # The minimum length of a Titles Description.
46
+ MIN_TITLE_DESC_LENGTH = 25
47
+
48
+ # OS versions (min/max) must match this regex.
49
+ # - Start of string
50
+ # - two digits
51
+ # - optionally
52
+ # - a dot
53
+ # - one or more digits
54
+ # - optionally
55
+ # - a dot
56
+ # - one or more digits
57
+ OS_VERSION_RE = /\A\d\d(\.\d+){0,2}\z/.freeze
58
+
59
+ # Module methods
60
+ ##############################
61
+ ##############################
62
+
63
+ # when this module is included
64
+ def self.included(includer)
65
+ Xolo.verbose_include includer, self
66
+ end
67
+
68
+ # Instance Methods
69
+ ##########################
70
+ ##########################
71
+
72
+ # Thes methods all raise this error
73
+ def raise_invalid_data_error(val, msg)
74
+ raise Xolo::InvalidDataError, "'#{val}': #{msg}"
75
+ end
76
+
77
+ # is the given command valid?
78
+ #########
79
+ def validate_cli_command
80
+ return if Xolo::Admin::Options::COMMANDS.key? cli_cmd.command
81
+
82
+ msg = cli_cmd.command.pix_empty? ? "Usage: #{usage}" : "Unknown command: '#{cli_cmd.command}'"
83
+ raise ArgumentError, msg
84
+ end # validate command
85
+
86
+ # were we given a title?
87
+ #########
88
+ def validate_cli_title
89
+ # this command doesn't need a title arg
90
+ return if Xolo::Admin::Options::COMMANDS[cli_cmd.command][:target] == :none
91
+
92
+ raise ArgumentError, "No title provided!\nUsage: #{usage}" unless cli_cmd.title
93
+
94
+ # this validates the format
95
+ validate_title cli_cmd.title
96
+ end
97
+
98
+ # were we given a version?
99
+ #########
100
+ def validate_cli_version
101
+ # this command doesn't need a version arg
102
+ return unless version_command? || title_or_version_command?
103
+
104
+ # TODO:
105
+ # If this is an 'add-' command, ensure the version
106
+ # doesn't already exist.
107
+ # Otherwise, make sure it does already exist
108
+ #
109
+
110
+ vers = cli_cmd.version
111
+ return unless vers.to_s.empty? || vers.start_with?(Xolo::DASH)
112
+
113
+ raise ArgumentError, "No version provided with '#{cli_cmd.command}' command!\nUsage: #{usage}"
114
+ end
115
+
116
+ # Validate the command options acquired from the command line.
117
+ # Walkthru will validate them individually as they are entered.
118
+ #
119
+ ##########################
120
+ def validate_cli_cmd_opts
121
+ cmd = cli_cmd.command
122
+ opts_defs = Xolo::Admin::Options::COMMANDS[cmd][:opts]
123
+ return if opts_defs.empty?
124
+
125
+ opts_defs.each do |key, deets|
126
+ # skip things not given on the command line
127
+ next unless cli_cmd_opts["#{key}_given"]
128
+
129
+ # skip things that shouldn't be validated
130
+ next unless deets[:validate]
131
+
132
+ # if the item is not required, and 'none' is given, set the
133
+ # value to nil and we're done
134
+ unless deets[:required]
135
+ if cli_cmd_opts[key].is_a?(Array) && cli_cmd_opts[key].include?(Xolo::NONE)
136
+ cli_cmd_opts[key] = []
137
+ next
138
+ elsif cli_cmd_opts[key] == Xolo::NONE
139
+ cli_cmd_opts[key] = nil
140
+ next
141
+ end
142
+ end
143
+
144
+ # If an item is :multi, it is an array.
145
+ # If it has only one item, split it on commas
146
+ # This handles multi items being given multiple times as CLI opts, or
147
+ # as comma-sep values in CLI opts or walkthru.
148
+ #
149
+ if deets[:multi] && cli_cmd_opts[key].size == 1
150
+ cli_cmd_opts[key] = cli_cmd_opts[key].first.split(Xolo::COMMA_SEP_RE)
151
+ end
152
+
153
+ validation_method =
154
+ case deets[:validate]
155
+ when Symbol
156
+ deets[:validate]
157
+ when TrueClass
158
+ "validate_#{key}"
159
+ end
160
+
161
+ # nothing to do if no validation method
162
+ next unless validation_method
163
+
164
+ # run the validation, which raises an error if invalid, or returns
165
+ # the converted value if OK - the converted value replaces the original in the
166
+ # cmd_opts
167
+ # puts "Validating #{key}, value '#{cli_cmd_opts[key]}', with #{validation_method}" if debug?
168
+ cli_cmd_opts[key] = send validation_method, cli_cmd_opts[key]
169
+ # puts "Valid Value for #{key} is now '#{cli_cmd_opts[key]}'" if debug?
170
+ end
171
+
172
+ # if we are here, eveything on the commandline checked out, so now
173
+ # go through the opts_defs keys, and for any that were not given on the command line,
174
+ # add the value for that key from current_opt_values to Xolo::Admin::Options.cli_cmd_opts
175
+ # which will then be passed for internal consistency validation.
176
+ end
177
+
178
+ # Title Attributes
179
+ #
180
+ ##################################################
181
+
182
+ # validate a Xolo title. Must be 2+ chars long, only lowercase
183
+ # alpha-numerics & dashes
184
+ #
185
+ # Must also exist or not exist, depending on the command we are running
186
+ # @param val [Object] The value to validate
187
+ #
188
+ # @return [String] The valid value
189
+ ##########################
190
+ def validate_title(val)
191
+ val = val.to_s.strip
192
+
193
+ title_exists = Xolo::Admin::Title.exist?(val, server_cnx)
194
+
195
+ # adding... cant already exist, and name must be OK
196
+ if cli_cmd.command == Xolo::Admin::Options::ADD_TITLE_CMD
197
+ err =
198
+ if title_exists
199
+ 'already exists in Xolo'
200
+ elsif val !~ /\A[a-z0-9-][a-z0-9-]+\z/
201
+ TITLE_ATTRS[:title][:invalid_msg]
202
+ else
203
+ return val
204
+ end
205
+
206
+ # a command where it must exist
207
+ elsif Xolo::Admin::Options::MUST_EXIST_COMMANDS.include?(cli_cmd.command)
208
+ return val if title_exists
209
+
210
+ err = "doesn't exist in Xolo"
211
+
212
+ # any other command
213
+ else
214
+ return val
215
+ end
216
+
217
+ raise_invalid_data_error val, err
218
+ end
219
+
220
+ # validate a title display-name. Must be 3+ chars long
221
+ #
222
+ # @param val [Object] The value to validate
223
+ #
224
+ #
225
+ # @return [String] The valid value
226
+ ##########################
227
+ def validate_title_display_name(val)
228
+ val = val.to_s.strip
229
+ return val if val =~ /\A\S.+\S\z/
230
+
231
+ raise_invalid_data_error val, TITLE_ATTRS[:display_name][:invalid_msg]
232
+ end
233
+
234
+ # validate a title description. Must be 20+ chars long
235
+ #
236
+ # @param val [Object] The value to validate
237
+ #
238
+ # @return [Boolean, String] The validity, or the valid value
239
+ ##########################
240
+ def validate_title_desc(val)
241
+ val = val.to_s.strip
242
+ return val if val.length >= Xolo::Core::BaseClasses::Title::MIN_TITLE_DESC_LENGTH
243
+
244
+ raise_invalid_data_error val, TITLE_ATTRS[:description][:invalid_msg]
245
+ end
246
+
247
+ # validate a title publisher. Must be 3+ chars long
248
+ #
249
+ # @param val [Object] The value to validate
250
+ #
251
+ # @return [String] The valid value
252
+ ##########################
253
+ def validate_publisher(val)
254
+ val = val.to_s.strip
255
+ return val if val.length >= 3
256
+
257
+ raise_invalid_data_error val, TITLE_ATTRS[:publisher][:invalid_msg]
258
+ end
259
+
260
+ # validate a title app_name. Must end with .app
261
+ #
262
+ # @param val [Object] The value to validate
263
+ #
264
+ # @return [String] The valid value
265
+ ##########################
266
+ def validate_app_name(val)
267
+ val = val.to_s.strip
268
+ return val if val.end_with? Xolo::DOTAPP
269
+
270
+ raise_invalid_data_error val, TITLE_ATTRS[:app_bundle_id][:invalid_msg]
271
+ end
272
+
273
+ # validate a title app_bundle_id. Must include at least one dot
274
+ #
275
+ # @param val [Object] The value to validate
276
+ #
277
+ # @return [String] The valid value
278
+ ##########################
279
+ def validate_app_bundle_id(val)
280
+ val = val.to_s.strip
281
+ return val if val.include? Xolo::DOT
282
+
283
+ raise_invalid_data_error val, TITLE_ATTRS[:app_bundle_id][:invalid_msg]
284
+ end
285
+
286
+ # validate a title version script. Must start with '#!'
287
+ #
288
+ # @param val [Object] The value to validate
289
+ #
290
+ # @return [Pathname] The valid value
291
+ ##########################
292
+ def validate_version_script(val)
293
+ val = Pathname.new val.to_s.strip
294
+ return val if val.file? && val.readable? && val.read.start_with?('#!')
295
+
296
+ raise_invalid_data_error val, TITLE_ATTRS[:version_script][:invalid_msg]
297
+ end
298
+
299
+ # validate a title uninstall script:
300
+ # - a path to a script which must start with '#!'
301
+ # OR
302
+ # - 'none' to unset the value
303
+ #
304
+ # TODO: Consistency with expiration.
305
+ #
306
+ # @param val [Object] The value to validate
307
+ #
308
+ # @return [String] The valid value
309
+ ##########################
310
+ def validate_uninstall_script(val)
311
+ val = val.to_s.strip
312
+ return if val == Xolo::NONE
313
+
314
+ script_file = Pathname.new(val).expand_path
315
+
316
+ if script_file.readable?
317
+ script = script_file.read
318
+ return script_file.to_s if script.start_with? '#!'
319
+ end
320
+
321
+ raise_invalid_data_error val, TITLE_ATTRS[:uninstall_script][:invalid_msg]
322
+ end
323
+
324
+ # validate a title uninstall ids:
325
+ # - an array of package identifiers
326
+ # OR
327
+ # - 'none' to unset the value
328
+ #
329
+ # TODO: Consistency with expiration.
330
+ #
331
+ # @param val [Object] The value to validate
332
+ #
333
+ # @return [Array<String>] The valid value
334
+ ##########################
335
+ def validate_uninstall_ids(val)
336
+ val = [val] unless val.is_a? Array
337
+ return Xolo::X if val == [Xolo::X]
338
+ return [] if val.include? Xolo::NONE
339
+
340
+ return val
341
+
342
+ raise_invalid_data_error val, TITLE_ATTRS[:uninstall_ids][:invalid_msg]
343
+ end
344
+
345
+ # validate an array of jamf group names to use as targets when released.
346
+ # 'all', or 'none' are also acceptable
347
+ #
348
+ # These groups cannot be in the excluded_group - but that validation happens
349
+ # later in the consistency checks via validate_scope_targets_and_exclusions
350
+ #
351
+ # @param val [Array<String>] The value to validate: names of jamf comp.
352
+ # groups, or 'all', or 'none'
353
+ #
354
+ # @return [Array<String>] The valid value
355
+ ##########################
356
+ def validate_release_groups(val)
357
+ val = [val] unless val.is_a? Array
358
+ if val.include? Xolo::NONE
359
+ return []
360
+ elsif val.include? Xolo::TARGET_ALL
361
+ validate_release_to_all_allowed
362
+
363
+ return [Xolo::TARGET_ALL]
364
+ end
365
+
366
+ # non-existant jamf groups
367
+ bad_grps = bad_jamf_groups(val)
368
+ return val if bad_grps.empty?
369
+
370
+ bad_grps = "No Such Groups: #{bad_grps.join(Xolo::COMMA_JOIN)}"
371
+ raise_invalid_data_error bad_grps, TITLE_ATTRS[:release_groups][:invalid_msg]
372
+ end
373
+
374
+ # check if the current admin is allowed to set a title's release groups to 'all'
375
+ #
376
+ # @return [void]
377
+ ##########################
378
+ def validate_release_to_all_allowed
379
+ svr_resp = Xolo::Admin::Title.release_to_all_allowed?(server_cnx)
380
+ return if svr_resp[:allowed]
381
+
382
+ raise Xolo::InvalidDataError, svr_resp[:msg]
383
+ end
384
+
385
+ # validate an array of jamf groups to use as exclusions.
386
+ # 'none' is also acceptable
387
+ #
388
+ # excluded groups cannot be in the release groups or pilot groups
389
+ #
390
+ # @param val [Array<String>] The value to validate: names of jamf comp.
391
+ # groups, or 'none'
392
+ #
393
+ # @return [Array<String>] The valid value
394
+ ##########################
395
+ def validate_excluded_groups(val)
396
+ val = [val] unless val.is_a? Array
397
+ return [] if val.include? Xolo::NONE
398
+
399
+ bad_grps = bad_jamf_groups(val)
400
+ return val if bad_grps.empty?
401
+
402
+ bad_grps = "No Such Groups: #{bad_grps.join(Xolo::COMMA_JOIN)}"
403
+
404
+ raise_invalid_data_error bad_grps, TITLE_ATTRS[:excluded_groups][:invalid_msg]
405
+ end
406
+
407
+ # validate an array of jamf groups to use as MDM deployment targets.
408
+ # 'none' is also acceptable
409
+ #
410
+ # @param val [Array<String>] The value to validate: names of jamf comp.
411
+ # groups, or 'none'
412
+ #
413
+ # @return [Array<String>] The valid value
414
+ ##########################
415
+ def validate_deploy_groups(val)
416
+ val = [val] unless val.is_a? Array
417
+ return [] if val.include? Xolo::NONE
418
+
419
+ bad_grps = bad_jamf_groups(val)
420
+ return val if bad_grps.empty?
421
+
422
+ bad_grps = "No Such Groups: #{bad_grps.join(Xolo::COMMA_JOIN)}"
423
+
424
+ raise_invalid_data_error bad_grps, 'Invalid group(s). Must exist in Jamf Pro.'
425
+ end
426
+
427
+ # @param grp_ary [Array<String>] Jamf groups to validate
428
+ # @return [Array<String>] Jamf groups that do not exist.
429
+ ##########################
430
+ def bad_jamf_groups(group_ary)
431
+ group_ary = [group_ary] unless group_ary.is_a? Array
432
+
433
+ group_ary - jamf_computer_group_names
434
+ end
435
+
436
+ # validate a titles expiration. Must be a non-negative integer
437
+ #
438
+ # @param val [Object] The value to validate
439
+ #
440
+ # @return [Integer] The valid value
441
+ ##########################
442
+ def validate_expiration(val)
443
+ val = val.to_s
444
+ if val.pix_integer?
445
+ val = val.to_i
446
+ return Xolo::NONE if val.zero?
447
+ return val if val.positive?
448
+ elsif val == Xolo::NONE
449
+ return val
450
+ end
451
+
452
+ raise_invalid_data_error val, TITLE_ATTRS[:expiration][:invalid_msg]
453
+ end
454
+
455
+ # validate a title expiration paths. Must one or more full paths
456
+ # starting with a / and containing at least one more.
457
+ #
458
+ # @param val [Object] The value to validate
459
+ #
460
+ # @return [Array<String>] The valid array
461
+ ##########################
462
+ def validate_expire_paths(val)
463
+ val = [val] unless val.is_a? Array
464
+ return [] if val.include? Xolo::NONE
465
+
466
+ val.map!(&:to_s)
467
+ bad_paths = []
468
+
469
+ val.each { |path| bad_paths << path unless path =~ %r{\A/\w.*/.*\z} }
470
+
471
+ return val if bad_paths.empty?
472
+
473
+ raise_invalid_data_error bad_paths.join(Xolo::COMMA_JOIN), TITLE_ATTRS[:expire_paths][:invalid_msg]
474
+ end
475
+
476
+ # validate boolean options
477
+ #
478
+ # Never raises an error, just returns true of false based on the string value
479
+ #
480
+ # @param val [Object] The value to validate
481
+ #
482
+ # @return [Boolean] The valid value
483
+ ##########################
484
+ def validate_boolean(val)
485
+ val.to_s =~ TRUE_RE ? true : false
486
+ end
487
+
488
+ # validate a self_service_category. Must exist in Jamf Pro
489
+ #
490
+ # @param val [Object] The value to validate
491
+ #
492
+ # @return [Boolean, String] The validity, or the valid value
493
+ ##########################
494
+ def validate_self_service_category(val)
495
+ val = val.to_s
496
+
497
+ # TODO: implement the check via the xolo server
498
+ return val # if category exists
499
+
500
+ raise_invalid_data_error val, TITLE_ATTRS[:self_service_category][:invalid_msg]
501
+ end
502
+
503
+ # validate a path to a self_service_icon. Must exist locally and be readable
504
+ #
505
+ # @param val [Object] The value to validate
506
+ #
507
+ # @return [Pathname] The valid value
508
+ ##########################
509
+ def validate_self_service_icon(val)
510
+ val = Pathname.new val.to_s.strip
511
+ if val.file? && val.readable?
512
+ mimetype = `/usr/bin/file -b -I #{Shellwords.escape val.to_s}`.split(';').first
513
+ return val if SSVC_ICON_MIME_TYPES.include? mimetype
514
+ end
515
+
516
+ raise_invalid_data_error val, TITLE_ATTRS[:self_service_icon][:invalid_msg]
517
+ end
518
+
519
+ # validate a path to a jamf_pkg_file. Must exist locally and be readable
520
+ # and have the correct ext.
521
+ #
522
+ # @param val [Object] The value to validate
523
+ #
524
+ # @return [Pathname] The valid value
525
+ ##########################
526
+ def validate_pkg_to_upload(val)
527
+ val = Pathname.new val.to_s.strip
528
+ return val if val.file? && val.readable? && (Xolo::OK_PKG_EXTS.include? val.extname)
529
+
530
+ raise_invalid_data_error val, VERSION_ATTRS[:jamf_pkg_file][:invalid_msg]
531
+ end
532
+
533
+ # Version Attributes
534
+ #
535
+ ##################################################
536
+
537
+ # TODO: validate a Xolo Version. Must be 2+ chars long, only lowercase
538
+ # alpha-numerics & dashes
539
+ #
540
+ # @param val [Object] The value to validate
541
+ #
542
+ # @return [String] The valid value
543
+ ##########################
544
+ def validate_version(val)
545
+ val
546
+ end
547
+
548
+ # @param val [Object] The value to validate
549
+ #
550
+ # @return [Date] The valid value
551
+ ##########################
552
+ def validate_publish_date(val)
553
+ val = Time.now.to_s if val.pix_empty?
554
+ val = Time.parse val.to_s
555
+ # TODO: ? Ensure this date is >= the prev. version and <= the next
556
+ return val
557
+
558
+ raise VERSION_ATTRS[:publish_date][:invalid_msg]
559
+ rescue StandardError => e
560
+ raise_invalid_data_error val, e.to_s
561
+ end
562
+
563
+ # @param val [String] The value to validate
564
+ #
565
+ # @return [String] The valid value
566
+ ##########################
567
+ def validate_min_os(val)
568
+ # inherit if needed
569
+ val = current_opt_values[:min_os] if val.pix_empty?
570
+
571
+ # use the default if still empty or 'none' - we get it from the server
572
+ val = Xolo::Admin::Options.default_min_os if val.pix_empty? || val == Xolo::NONE
573
+
574
+ raise VERSION_ATTRS[:min_os][:invalid_msg] unless val =~ OS_VERSION_RE
575
+
576
+ val
577
+ rescue StandardError => e
578
+ raise_invalid_data_error val, e.to_s
579
+ end
580
+
581
+ # @param val [Object] The value to validate
582
+ #
583
+ # @return [Gem::Version] The valid value
584
+ ##########################
585
+ def validate_max_os(val)
586
+ return if val.pix_empty? || val == Xolo::NONE
587
+
588
+ raise VERSION_ATTRS[:max_os][:invalid_msg] unless val =~ OS_VERSION_RE
589
+
590
+ val
591
+ rescue StandardError => e
592
+ raise_invalid_data_error val, e.to_s
593
+ end
594
+
595
+ # @param val [Object] The value to validate
596
+ #
597
+ # @return [Array<Array<String>>] The valid value
598
+ ##########################
599
+ def validate_killapps(val)
600
+ val = [val] unless val.is_a? Array
601
+ return Xolo::X if val == [Xolo::X]
602
+ return Xolo::NONE if val.include? Xolo::NONE
603
+
604
+ val.map!(&:to_s)
605
+
606
+ # if one of the killapps is Xolo::Admin::Version::USE_TITLE_FOR_KILLAPP
607
+ # convert it into the appropriate string
608
+ val.map! { |ka| expand_use_title(ka) }
609
+
610
+ val.each { |ka| validate_single_killapp(ka) }
611
+
612
+ val
613
+ end
614
+
615
+ # @param val [Object] The value to validate
616
+ #
617
+ # @return [String] The valid value
618
+ ##########################
619
+ def validate_contact_email(val)
620
+ val = val.to_s.strip
621
+ return val if val =~ /\A\S+@\S+\.\S+\z/
622
+
623
+ raise_invalid_data_error val, VERSION_ATTRS[:contact_email][:invalid_msg]
624
+ end
625
+
626
+ # expand Xolo::Admin::Version::USE_TITLE_FOR_KILLAPP into the proper killall string
627
+ #
628
+ # @param ka [String] the string to expand if needed
629
+ # @return [String] the original string, or the expanded version
630
+ ##########################
631
+ def expand_use_title(ka)
632
+ return ka unless ka == Xolo::Admin::Version::USE_TITLE_FOR_KILLAPP
633
+
634
+ title = Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
635
+ unless title.app_name
636
+ raise_invalid_data_error Xolo::Admin::Version::USE_TITLE_FOR_KILLAPP,
637
+ 'Title does not use app_name'
638
+ end
639
+ unless title.app_bundle_id
640
+ raise_invalid_data_error Xolo::Admin::Version::USE_TITLE_FOR_KILLAPP,
641
+ 'Title does not use app_bundle_id'
642
+ end
643
+
644
+ "#{title.app_name};#{title.app_bundle_id}"
645
+ end
646
+
647
+ # Validate an individual killapp string
648
+ # @param ka [String] the string to expand if needed
649
+ # @return [String] the validated value
650
+ ##########################
651
+ def validate_single_killapp(ka)
652
+ name, bundle_id = ka.split(Xolo::SEMICOLON_SEP_RE)
653
+ raise_invalid_data_error name, 'App name required before semicolon' unless name
654
+ raise_invalid_data_error name, 'App Bundle ID required after semicolon' unless bundle_id
655
+
656
+ # app name must end with .app. Use the err messge from Title/app_name
657
+ raise_invalid_data_error name, TITLE_ATTRS[:app_name][:invalid_msg] unless name.end_with?(Xolo::DOTAPP)
658
+
659
+ # app bundle id must contain a dot. Use the err messge from Title/app_bundle_id
660
+ unless bundle_id&.include?(Xolo::DOT)
661
+ raise_invalid_data_error bundle_id,
662
+ TITLE_ATTRS[:app_bundle_id][:invalid_msg]
663
+ end
664
+
665
+ ka
666
+ end
667
+
668
+ # validate an array of jamf groups to use as pilots for testing a version
669
+ # before releasing it.
670
+ # 'none' is also acceptable
671
+ #
672
+ # NOTE: we will not compare targets to exclusions - we'll just verify
673
+ # that the jamf groups exist. If a group (or an individual mac) is both a
674
+ # pilot and an exclusion, the exclusion wins.
675
+ #
676
+ # @param val [Array<String>] The value to validate: names of jamf comp.
677
+ # groups, or 'none'
678
+ #
679
+ # @return [Array<String>] The valid value
680
+ ##########################
681
+ def validate_pilot_groups(val)
682
+ val = [val] unless val.is_a? Array
683
+ return [] if val.include? Xolo::NONE
684
+
685
+ bad_grps = bad_jamf_groups(val)
686
+ return val if bad_grps.empty?
687
+
688
+ bad_grps = "No Such Groups: #{bad_grps.join(Xolo::COMMA_JOIN)}"
689
+
690
+ raise_invalid_data_error bad_grps, VERSION_ATTRS[:pilot_groups][:invalid_msg]
691
+ end
692
+
693
+ # Try to fetch a known route from the given xolo server
694
+ #
695
+ # @param val [String] The hostname to validate.
696
+ #
697
+ # @return [void]
698
+ #######
699
+ def validate_hostname(val)
700
+ if val.downcase == 'x'
701
+ val = nil
702
+ return
703
+ end
704
+
705
+ response = server_cnx(host: val).get Xolo::Admin::Connection::PING_ROUTE
706
+ return val if response.body == Xolo::Admin::Connection::PING_RESPONSE
707
+
708
+ raise_invalid_data_error val, Xolo::Admin::Configuration::KEYS[:hostname][:invalid_msg]
709
+ rescue Faraday::ConnectionFailed
710
+ raise_invalid_data_error val, Xolo::Admin::Configuration::KEYS[:hostname][:invalid_msg]
711
+ rescue Faraday::SSLError
712
+ raise_invalid_data_error val, 'SSL Error. Be sure to use the fully qualified hostname.'
713
+ end
714
+
715
+ # Password (and username) will be validated via the server
716
+ #
717
+ # @param val [String] The passwd to be validated with the stored or given username
718
+ #
719
+ # @return [void]
720
+ #######
721
+ def validate_pw(val)
722
+ if val.downcase == 'x'
723
+ val = Xolo::BLANK
724
+ return
725
+ end
726
+ pw = val
727
+ hostname = walkthru? ? walkthru_cmd_opts[:hostname] : cli_cmd_opts[:hostname]
728
+ admin = walkthru? ? walkthru_cmd_opts[:admin] : cli_cmd_opts[:admin]
729
+ no_gui = walkthru? ? walkthru_cmd_opts[:no_gui] : cli_cmd_opts[:no_gui]
730
+
731
+ raise Xolo::MissingDataError, 'hostname must be set before password' if hostname.pix_empty?
732
+ raise Xolo::MissingDataError, 'admin username must be set before password' if admin.pix_empty?
733
+
734
+ pw = config.data_from_command_file_or_string(pw, enforce_secure_mode: true) if no_gui
735
+
736
+ payload = { admin: admin, password: pw }.to_json
737
+ begin
738
+ server_cnx(host: hostname).post Xolo::Admin::Connection::LOGIN_ROUTE, payload
739
+ rescue Faraday::UnauthorizedError
740
+ raise_invalid_data_error 'User/Password', 'Username or Password is incorrect'
741
+ end
742
+
743
+ # return the value for storage in the config file if no_gui is true
744
+ return val if no_gui
745
+
746
+ store_pw admin, val
747
+
748
+ # return a placeholder value saying that the password is stored in the keychain
749
+ Xolo::Admin::Configuration::CREDENTIALS_IN_KEYCHAIN
750
+ end
751
+
752
+ # Does the chosen editor exist and is it executable?
753
+ #
754
+ # @aram val [String] The path to the editor executable, possibly followed by
755
+ # a space and any command line arguments, e.g. '/usr/bin/vim -c "set ft=markdown"'
756
+ #
757
+ # @return [void]
758
+ #######
759
+ def validate_editor(val)
760
+ tool = val.split(' -')[0].strip
761
+ tool = Pathname.new tool
762
+ raise_invalid_data_error val, Xolo::Admin::Configuration::KEYS[:editor][:invalid_msg] unless tool.executable?
763
+
764
+ val
765
+ end
766
+
767
+ # Internal Consistency Checks!
768
+ #
769
+ ##################################################
770
+
771
+ # These methods all raise this error
772
+ #
773
+ # @param msg [String] an error message
774
+ #
775
+ # @return [void]
776
+ ########
777
+ def raise_consistency_error(msg)
778
+ raise Xolo::InvalidDataError, msg
779
+ end
780
+
781
+ # Given an ostruct of options that have been individually validated, and combined
782
+ # with any current_opt_values as needed, check the data for internal consistency.
783
+ # The unset values in the ostruct should be nil. 'none' is used for unsetting values,
784
+ # in the CLI and walkthru, and is converted to nil during validation if appropriate.
785
+ #
786
+ # @param opts [OpenStruct] the current options
787
+ #
788
+ # @return [void]
789
+ #######
790
+ def validate_internal_consistency(opts)
791
+ return unless add_command? || edit_command?
792
+
793
+ if version_command?
794
+ validate_version_consistency opts
795
+ elsif title_command?
796
+ validate_title_consistency opts
797
+ end
798
+ end
799
+
800
+ # @param opts [OpenStruct] the current options
801
+ #
802
+ # @return [void]
803
+ #######
804
+ def validate_version_consistency(opts)
805
+ validate_scope_targets_and_exclusions(opts)
806
+ validate_min_os_and_max_os(opts)
807
+ end
808
+
809
+ # @param opts [OpenStruct] the current options
810
+ #
811
+ # @return [void]
812
+ #######
813
+ def validate_title_consistency(opts)
814
+ # if we are just listing the versions, nothing to do
815
+ return if cli_cmd.command == Xolo::Admin::Options::LIST_VERSIONS_CMD
816
+
817
+ # order of these matters
818
+ validate_scope_targets_and_exclusions(opts)
819
+ validate_title_consistency_app_and_script(opts)
820
+ validate_title_consistency_app_or_script(opts)
821
+ validate_title_consistency_app_name_and_id(opts)
822
+ validate_title_consistency_uninstall(opts)
823
+ validate_title_consistency_expire_paths(opts)
824
+ validate_title_consistency_no_all_in_ssvc(opts)
825
+ validate_title_consistency_ssvc_needs_category(opts)
826
+ end # title_consistency(opts)
827
+
828
+ # groups that will be scope targets (pilot or release) cannot
829
+ # also be in the exclusions.
830
+ #
831
+ # @param opts [OpenStruct] the current options
832
+ #
833
+ # @return [void]
834
+ #######
835
+ def validate_scope_targets_and_exclusions(opts)
836
+ # require 'pp'
837
+ # puts 'Opts Are:'
838
+ # pp opts.to_h
839
+ # pp caller
840
+
841
+ if title_command?
842
+ excls = opts.excluded_groups
843
+ tgts = opts.release_groups
844
+ tgt_type = :release
845
+ elsif version_command?
846
+ @title_for_version_validation ||= Xolo::Admin::Title.fetch cli_cmd.title, server_cnx
847
+ excls = @title_for_version_validation.excluded_groups
848
+ tgts = opts.pilot_groups
849
+ tgt_type = :pilot
850
+ else
851
+ excls = nil
852
+ tgts = nil
853
+ end
854
+ return unless excls && tgts
855
+
856
+ in_both = excls & tgts
857
+ return if in_both.empty?
858
+
859
+ raise_consistency_error "These groups are in both #{tgt_type}_groups and the title's excluded_groups: '#{in_both.join "', '"}'"
860
+ end
861
+
862
+ # if app_name or app_bundle_id is given, can't use --version-script
863
+ #
864
+ # @param opts [OpenStruct] the current options
865
+ #
866
+ # @return [void]
867
+ #######
868
+ def validate_title_consistency_app_and_script(opts)
869
+ return unless opts[:version_script] && (opts[:app_bundle_id] || opts[:app_name])
870
+
871
+ msg =
872
+ if walkthru?
873
+ 'Version Script cannot be used with App Name & App Bundle ID'
874
+ else
875
+ '--version-script cannot be used with --app-name & --app-bundle-id'
876
+ end
877
+
878
+ raise_consistency_error msg
879
+ end
880
+
881
+ # but either version_script or appname and bundle id must be given
882
+ #
883
+ # @param opts [OpenStruct] the current options
884
+ #
885
+ # @return [void]
886
+ #######
887
+ def validate_title_consistency_app_or_script(opts)
888
+ return if opts[:version_script]
889
+ return if opts[:app_name] || opts[:app_bundle_id]
890
+
891
+ msg =
892
+ if walkthru?
893
+ 'Either App Name & App Bundle ID. or Version Script must be given.'
894
+ else
895
+ 'Must provide either --app-name & --app-bundle-id OR --version-script'
896
+ end
897
+ raise_consistency_error msg
898
+ end
899
+
900
+ # If using app_name and bundle id, both must be given
901
+ #
902
+ # @param opts [OpenStruct] the current options
903
+ #
904
+ # @return [void]
905
+ #######
906
+ def validate_title_consistency_app_name_and_id(opts)
907
+ return if opts[:version_script]
908
+ return if opts[:app_name] && opts[:app_bundle_id]
909
+
910
+ msg =
911
+ if walkthru?
912
+ 'App Name & App Bundle ID must both be given if either is.'
913
+ else
914
+ '--app-name & --app-bundle-id must both be given if either is.'
915
+ end
916
+ raise_consistency_error msg
917
+ end
918
+
919
+ # if expiration is > 0, there must be at least one expiration path
920
+ #
921
+ # @param opts [OpenStruct] the current options
922
+ #
923
+ # @return [void]
924
+ #######
925
+ def validate_title_consistency_expire_paths(opts)
926
+ return unless opts.expiration.to_i.positive?
927
+ return unless opts.expire_paths.pix_empty?
928
+
929
+ msg =
930
+ if walkthru?
931
+ 'At least one Expiration Path must be given if Expiration is > 0.'
932
+ else
933
+ 'At least one --expiration-path must be given if --expiration is > 0'
934
+ end
935
+ raise_consistency_error msg
936
+ end
937
+
938
+ # if uninstall script, cant have uninstall ids
939
+ # and vice versa
940
+ #
941
+ # @param opts [OpenStruct] the current options
942
+ #
943
+ # @return [void]
944
+ #######
945
+ def validate_title_consistency_uninstall(opts)
946
+ # walktrhu will have already validated this
947
+ return if walkthru?
948
+
949
+ if opts.uninstall_script_given && opts.uninstall_ids_given
950
+
951
+ # if uninstall_script is given, uninstall_ids must be unset
952
+ # and vice versa
953
+ # raise an error if both are given
954
+
955
+ raise_consistency_error '--uninstall-script cannot be used with --uninstall-ids'
956
+
957
+ elsif opts.uninstall_script_given
958
+ opts.uninstall_ids = nil
959
+
960
+ elsif opts.uninstall_ids_given
961
+ opts.uninstall_script_given = nil
962
+ end
963
+ end
964
+
965
+ # if target_group is all, can't be in self service
966
+ #
967
+ # @param opts [OpenStruct] the current options
968
+ #
969
+ # @return [void]
970
+ #######
971
+ def validate_title_consistency_no_all_in_ssvc(opts)
972
+ return unless opts[:release_groups].to_a.include?(Xolo::TARGET_ALL) && opts[:self_service]
973
+
974
+ msg =
975
+ if walkthru?
976
+ "Cannot be in Self Service when Target Group is '#{Xolo::TARGET_ALL}'"
977
+ else
978
+ "--self-service cannot be used when --target-groups contains '#{Xolo::TARGET_ALL}'"
979
+ end
980
+ raise_consistency_error msg
981
+ end
982
+
983
+ # if in self service, a category must be assigned
984
+ #
985
+ # @param opts [OpenStruct] the current options
986
+ #
987
+ # @return [void]
988
+ #######
989
+ def validate_title_consistency_ssvc_needs_category(opts)
990
+ return unless opts[:self_service]
991
+ return if opts[:self_service_category]
992
+
993
+ msg =
994
+ if walkthru?
995
+ 'A Self Service Category must be given if Self Service is true.'
996
+ else
997
+ 'A --self-service-category must be provided when using --self-service'
998
+ end
999
+ raise_consistency_error msg
1000
+ end
1001
+
1002
+ # min_os must be <= max_os
1003
+ # max_os must be >= min_os
1004
+ #
1005
+ # @param opts [OpenStruct] the current options
1006
+ #
1007
+ # @return [void]
1008
+ #######
1009
+ def validate_min_os_and_max_os(opts)
1010
+ # if no max_os, nothing to do
1011
+ return if opts[:max_os].pix_empty?
1012
+
1013
+ min_os = Gem::Version.new opts[:min_os]
1014
+ max_os = Gem::Version.new opts[:max_os]
1015
+
1016
+ # if things look OK, we're done
1017
+ return if min_os <= max_os && max_os >= min_os
1018
+
1019
+ msg =
1020
+ if walkthru?
1021
+ 'Minimum OS must be less than or equal to Maximum OS'
1022
+ else
1023
+ '--max-os must be greater than or equal to --min-os'
1024
+ end
1025
+ raise_consistency_error msg
1026
+ end
1027
+
1028
+ end # module validate
1029
+
1030
+ end # module Core
1031
+
1032
+ end # module Xolo