xolo-server 1.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/data/client/xolo +152 -79
  3. data/lib/xolo/core/base_classes/title.rb +254 -18
  4. data/lib/xolo/core/base_classes/version.rb +47 -7
  5. data/lib/xolo/core/constants.rb +7 -3
  6. data/lib/xolo/core/security_cmd.rb +128 -0
  7. data/lib/xolo/core/version.rb +1 -1
  8. data/lib/xolo/core.rb +1 -0
  9. data/lib/xolo/server/app.rb +7 -0
  10. data/lib/xolo/server/configuration.rb +243 -37
  11. data/lib/xolo/server/constants.rb +10 -0
  12. data/lib/xolo/server/helpers/auth.rb +19 -2
  13. data/lib/xolo/server/helpers/autopkg.rb +157 -0
  14. data/lib/xolo/server/helpers/client_data.rb +90 -60
  15. data/lib/xolo/server/helpers/file_transfers.rb +412 -82
  16. data/lib/xolo/server/helpers/jamf_pro.rb +30 -7
  17. data/lib/xolo/server/helpers/log.rb +2 -0
  18. data/lib/xolo/server/helpers/maintenance.rb +1 -0
  19. data/lib/xolo/server/helpers/notification.rb +4 -3
  20. data/lib/xolo/server/helpers/pkg_signing.rb +16 -12
  21. data/lib/xolo/server/helpers/progress_streaming.rb +9 -12
  22. data/lib/xolo/server/helpers/subscriptions.rb +119 -0
  23. data/lib/xolo/server/helpers/titles.rb +27 -3
  24. data/lib/xolo/server/helpers/versions.rb +23 -11
  25. data/lib/xolo/server/mixins/changelog.rb +9 -16
  26. data/lib/xolo/server/mixins/title_jamf_access.rb +375 -385
  27. data/lib/xolo/server/mixins/title_ted_access.rb +29 -3
  28. data/lib/xolo/server/mixins/version_jamf_access.rb +95 -112
  29. data/lib/xolo/server/mixins/version_ted_access.rb +25 -0
  30. data/lib/xolo/server/object_locks.rb +2 -1
  31. data/lib/xolo/server/routes/auth.rb +2 -2
  32. data/lib/xolo/server/routes/jamf_pro.rb +11 -1
  33. data/lib/xolo/server/routes/maint.rb +2 -1
  34. data/lib/xolo/server/routes/subscriptions.rb +126 -0
  35. data/lib/xolo/server/routes/title_editor.rb +1 -1
  36. data/lib/xolo/server/routes/titles.rb +26 -11
  37. data/lib/xolo/server/routes/uploads.rb +0 -14
  38. data/lib/xolo/server/routes/versions.rb +14 -13
  39. data/lib/xolo/server/routes.rb +9 -0
  40. data/lib/xolo/server/title.rb +100 -77
  41. data/lib/xolo/server/version.rb +177 -15
  42. data/lib/xolo/server.rb +8 -0
  43. metadata +7 -9
@@ -27,7 +27,7 @@ module Xolo
27
27
 
28
28
  DFT_EMAIL_FROM = 'xolo-server-do-not-reply'
29
29
 
30
- ALERT_TOOL_EMAIL_PREFIX = 'email:'
30
+ ALERT_TOOL_EMAIL_PREFIX = 'mailto:'
31
31
 
32
32
  # Module Methods
33
33
  #######################
@@ -45,7 +45,8 @@ module Xolo
45
45
  # Send a message thru the alert_tool, if one is defined in the config.
46
46
  #
47
47
  # Messages are prepended with "#{level} ALERT: "
48
- # This should be called by passing alert: true to one of the
48
+ #
49
+ # This should be called by passing 'alert: true' to one of the
49
50
  # logging wrapper methods
50
51
  #
51
52
  # @param msg [String] the message to send
@@ -136,7 +137,7 @@ module Xolo
136
137
  @server_fqdn ||= Addrinfo.getaddrinfo(Socket.gethostname, nil).first.getnameinfo.first
137
138
  end
138
139
 
139
- end # Log
140
+ end # Notification
140
141
 
141
142
  end # Helpers
142
143
 
@@ -44,21 +44,25 @@ module Xolo
44
44
  ##############################
45
45
 
46
46
  # do we need to sign a pkg?
47
- # TODO: sign zipped bundle installers? prob not, they shouldn't be used anymore
48
- # (I'm looking at YOU Adobe)
49
47
  # @param pkg [Pathname] Path to a .pkg to see if it's signed.
48
+ # @param version [Xolo::Server::Version] the version that is being uploaded/re-uploaded
50
49
  # @return [Boolean] should we sign it?
51
- def need_to_sign?(pkg)
52
- log_debug "Checking need to sign uploaded pkg '#{pkg}'"
53
- unless Xolo::Server.config.sign_pkgs
54
- log_debug "No need to sign '#{pkg.basename}': xolo server is not configured to sign pkgs."
50
+ #############################
51
+ def need_to_sign?(pkg, version)
52
+ log_debug "Checking need to sign uploaded pkg '#{pkg}' for version '#{version.version}' of title '#{version.title}'"
53
+
54
+ # if an autopkg pkg and we are not configured to sign autopkg pkgs, then no need to sign
55
+ if version.pkg_is_from_autopkg && !Xolo::Server.config.sign_autopkg_pkgs
56
+ log_debug "No need to sign '#{pkg.basename}': version '#{version.version}' of title '#{version.title}' is from autopkg and server is not configured to sign autopkg pkgs."
55
57
  return false
56
- end
57
- if pkg.extname == Xolo::DOT_ZIP
58
- log_debug "No need to sign '#{pkg.basename}': It is a compressed .pkg bundle. TODO: maybe support signing these?"
58
+
59
+ # if not an autopkg pkg and we are not configured to sign non-autopkg pkgs, then no need to sign
60
+ elsif !version.pkg_is_from_autopkg && !Xolo::Server.config.sign_pkgs
61
+ log_debug "No need to sign '#{pkg.basename}': server is not configured to sign pkgs."
59
62
  return false
60
63
  end
61
64
 
65
+ # is the pkg already signed?
62
66
  !pkg_signed?(pkg)
63
67
  end
64
68
 
@@ -83,7 +87,7 @@ module Xolo
83
87
  #
84
88
  # @return [void]
85
89
  #######################################################
86
- def sign_uploaded_pkg(unsigned_pkg, signed_pkg)
90
+ def sign_pkg(unsigned_pkg, signed_pkg)
87
91
  unlock_signing_keychain
88
92
 
89
93
  sh_unsigned = Shellwords.escape unsigned_pkg.to_s
@@ -99,7 +103,7 @@ module Xolo
99
103
 
100
104
  msg = "Failed to sign uploaded pkg: #{stdouterr}"
101
105
  log_error msg
102
- halt 400, { status: 400, error: msg }
106
+ raise Xolo::Core::Exceptions::ServerError msg
103
107
  end
104
108
 
105
109
  # unlock the pkg signing keychain
@@ -129,7 +133,7 @@ module Xolo
129
133
 
130
134
  msg = "Error unlocking signing keychain: #{outerrs}"
131
135
  log_error msg
132
- halt 400, { status: 400, error: msg }
136
+ raise Xolo::Core::Exceptions::ServerError, msg
133
137
  end
134
138
 
135
139
  end # JamfPro
@@ -72,7 +72,7 @@ module Xolo
72
72
  log_debug 'Thread with_streaming is starting and yielding to block'
73
73
  yield
74
74
  log_debug 'Thread with_streaming is finished'
75
- rescue StandardError => e
75
+ rescue => e
76
76
  progress "ERROR: #{e.class}: #{e}", log: :error
77
77
  e.backtrace.each { |l| log_debug "..#{l}" }
78
78
  ensure
@@ -141,29 +141,26 @@ module Xolo
141
141
  #
142
142
  # @return [void]
143
143
  ###################
144
- def progress(msg, log: :nil)
144
+ def progress(msg, log: :nil, alert: false)
145
145
  # log_debug "Progress method called from #{caller_locations.first}"
146
146
 
147
147
  progress_stream_file.pix_append "#{msg.chomp}\n"
148
148
 
149
- unless log
150
- # log_debug 'Processed unlogged progress message'
151
- return
152
- end
149
+ return unless log
153
150
 
154
151
  case log
155
152
  when :debug
156
- log_debug msg
153
+ log_debug msg, alert: alert
157
154
  when :info
158
- log_info msg
155
+ log_info msg, alert: alert
159
156
  when :warn
160
- log_warn msg
157
+ log_warn msg, alert: alert
161
158
  when :error
162
- log_error msg
159
+ log_error msg, alert: alert
163
160
  when :fatal
164
- log_fatal msg
161
+ log_fatal msg, alert: alert
165
162
  when :unknown
166
- log_unknown msg
163
+ log_unknown msg, alert: alert
167
164
  end
168
165
  end
169
166
 
@@ -0,0 +1,119 @@
1
+ # Copyright 2025 Pixar
2
+ #
3
+ # Licensed under the terms set forth in the LICENSE.txt file available at
4
+ # at the root of this project.
5
+ #
6
+ #
7
+
8
+ # frozen_string_literal: true
9
+
10
+ # main module
11
+ module Xolo
12
+
13
+ module Server
14
+
15
+ module Helpers
16
+
17
+ # This is mixed in to Xolo::Server::App (as a helper, available in route processing)
18
+ #
19
+ # This holds methods and constants for working with subscribed titles - those
20
+ # that are managed by other Non-Xolo Patch Sources.
21
+ #
22
+ module Subscriptions
23
+
24
+ # Constants
25
+ #####################
26
+ #####################
27
+
28
+ # Module Methods
29
+ #######################
30
+ #######################
31
+
32
+ # when this module is included
33
+ def self.included(includer)
34
+ Xolo.verbose_include includer, self
35
+ end
36
+
37
+ # Instance Methods
38
+ #######################
39
+ ######################
40
+
41
+ # All available (i.e. not yet subscribed) titles on all patch sources defined in Jamf.
42
+ # @return [Array<Hash>] the available titles and their sources
43
+ #####################################
44
+ def available_titles_for_subscription
45
+ available = []
46
+
47
+ Jamf::PatchSource.all(cnx: jamf_cnx).each do |ps|
48
+ log_debug "Checking Patch Source #{ps} for available titles"
49
+ ps = Jamf::PatchSource.fetch id: ps[:id], cnx: jamf_cnx
50
+ ps.available_titles.each do |t|
51
+ data = t.merge({ source_id: ps.id, source_name: ps.name })
52
+ available << data
53
+ end
54
+ end
55
+
56
+ available
57
+ end
58
+
59
+ # Process an incoming webhook event, possibly for a subscribed title
60
+ # Do this in a thread so that we can return a 200 response to the webhook immediately,
61
+ # and do the processing asynchronously (which may involve time-consuming tasks like autopkg runs)
62
+ #################################
63
+ def process_patch_title_updated_webhook(req_body)
64
+ @process_webhook_thread = Thread.new do
65
+ log_debug "Using a thread for processing PatchSoftwareTitleUpdated webhook event with body: #{req_body}"
66
+
67
+ event_data = parse_json(req_body)[:event]
68
+
69
+ title_name = event_data[:name]
70
+ title_id = event_data[:jssID]
71
+ new_version = event_data[:latestVersion]
72
+
73
+ log_debug "Received PatchSoftwareTitleUpdate webhook event for patch title '#{title_name}' (jamf id #{title_id}), new version '#{new_version}'"
74
+
75
+ subscribed_title = subscribed_title_objects.select { |tobj| tobj.jamf_patch_title_id.to_i == title_id.to_i }.first
76
+
77
+ if subscribed_title
78
+ msg = +"New version '#{new_version}' is available for subscribed title '#{subscribed_title.title}' (#{subscribed_title.display_name})."
79
+
80
+ msg << " Running autopkg recipe '#{subscribed_title.autopkg_recipe}'." if subscribed_title.autopkg_enabled?
81
+
82
+ log_info msg
83
+
84
+ Xolo::Server::Version.add_version_via_subscription(
85
+ title_object: subscribed_title,
86
+ new_version: new_version
87
+ )
88
+ else
89
+ log_debug "Title '#{title_name}' ID #{title_id} is not a subscribed title in Xolo. Ignoring webhook."
90
+ end
91
+ rescue => e
92
+ msg = "Error processing PatchSoftwareTitleUpdated webhook event: #{e.class}: #{e}"
93
+ log_error msg
94
+ raise e, msg
95
+ end # thread
96
+ end
97
+
98
+ # TODO: Delete this method after confirming we don't want it
99
+ # It wasn't in use when it was commented out.
100
+ #
101
+ # Subscribe to a title on a given patch source.
102
+ # @param source_id [Integer] the id or name of the patch source in Jamf
103
+ # @param name_id [Integer] the name_id of the title on that patch source
104
+ # @param display_name [String] an display name for the title
105
+ # @return [Integer] the id of the new subscribed title, or false on failure
106
+ #####################################
107
+ # def subscribe_to_title(source_id:, name_id:, display_name:)
108
+ # new_sub = Jamf::PatchTitle.create name: display_name, source_id: source_id, name_id: name_id, cnx: jamf_cnx
109
+
110
+ # new_sub.save
111
+ # end
112
+
113
+ end # Subscriptions
114
+
115
+ end # Helpers
116
+
117
+ end # Server
118
+
119
+ end # module Xolo
@@ -40,8 +40,32 @@ module Xolo
40
40
  # A an array of all server titles as Title objects
41
41
  # @return [Array<Xolo::Server::Title>]
42
42
  ############
43
- def all_title_objects
44
- all_titles.map { |t| instantiate_title t }
43
+ def all_title_objects(refresh: false)
44
+ @all_title_objects = nil if refresh
45
+ return @all_title_objects if @all_title_objects
46
+
47
+ log_debug 'Instantiating all titles...'
48
+ @all_title_objects = all_titles.map { |t| instantiate_title t }
49
+ log_debug "Instantiated #{@all_title_objects.length} titles."
50
+ @all_title_objects
51
+ end
52
+
53
+ # A an array of server titles as Title objects, only for subscribed titles
54
+ # @return [Array<Xolo::Server::Title>]
55
+ def subscribed_title_objects(refresh: false)
56
+ @subscribed_title_objects = nil if refresh
57
+ return @subscribed_title_objects if @subscribed_title_objects
58
+
59
+ @subscribed_title_objects = all_title_objects(refresh: refresh).select { |t| t.subscribed? }
60
+ end
61
+
62
+ # A an array of server titles as Title objects, only for managed titles
63
+ # @return [Array<Xolo::Server::Title>]
64
+ def managed_title_objects(refresh: false)
65
+ @managed_title_objects = nil if refresh
66
+ return @managed_title_objects if @managed_title_objects
67
+
68
+ @managed_title_objects = all_title_objects(refresh: refresh).select { |t| t.managed? }
45
69
  end
46
70
 
47
71
  # Instantiate a Server::Title with access to the Sinatra app instance,
@@ -68,7 +92,7 @@ module Xolo
68
92
  Xolo::Server::Title.load data
69
93
 
70
94
  else
71
- msg = 'Invalid data to instantiate a Xolo::Server::Title'
95
+ msg = "Invalid data to instantiate a Xolo::Server::Title: #{data.class}:#{data} "
72
96
  log_error msg
73
97
 
74
98
  halt 400, { status: 400, error: msg }
@@ -53,7 +53,7 @@ module Xolo
53
53
  # A list of all known versions of a title
54
54
  # @return [Array<Xolo::Server::Version>]
55
55
  ############
56
- def all_version_instances(title)
56
+ def all_version_objects(title)
57
57
  all_versions(title).map { |v| instantiate_version title: title, version: v }
58
58
  end
59
59
 
@@ -69,24 +69,33 @@ module Xolo
69
69
  # to use for access from the version object to the Sinatra App instance
70
70
  # for the session and api connection objects
71
71
  #
72
- # @param data [Hash] hash to use with .new
73
72
  # @param title [String, Xolo::Server::Title] title to use with .load
74
73
  # @param version [String] version to use with .load
74
+ # @param data [Hash] hash to use with .new
75
75
  #
76
76
  # @return [Xolo::Server::Version]
77
77
  #################
78
- def instantiate_version(data = nil, title: nil, version: nil)
79
- title_obj = nil
78
+ def instantiate_version(title: nil, version: nil, **data)
79
+ log_debug "Instantiating version with title: '#{title}' (#{title.class}), version: '#{version},' data: #{data}"
80
80
 
81
- if data
82
- title = data[:title]
83
- elsif title.is_a?(Xolo::Server::Title)
81
+ if title.is_a?(Xolo::Server::Title)
84
82
  title_obj = title
85
83
  title = title_obj.title
84
+ elsif data[:title]
85
+ title = data[:title]
86
+ title_obj = instantiate_title(title)
87
+ elsif title
88
+ title_obj = instantiate_title(title)
89
+ else
90
+ halt 400, { status: 400, error: 'Missing title to instantiate a Xolo::Server::Version' }
91
+ log_error 'Missing title to instantiate a Xolo::Server::Version'
86
92
  end
87
93
 
88
94
  vers =
89
- if data.is_a? Hash
95
+ if !data.empty?
96
+ # ensure the title and version are in the data
97
+ data[:title] ||= title
98
+ data[:version] ||= version
90
99
  Xolo::Server::Version.new data
91
100
 
92
101
  elsif title && version
@@ -95,12 +104,12 @@ module Xolo
95
104
 
96
105
  Xolo::Server::Version.load title, version
97
106
  else
98
- msg = 'Invalid data to instantiate a Xolo::Server::Version'
107
+ msg = "Invalid title, version, or data to instantiate a Xolo::Server::Version: #{data.class}:#{data} "
99
108
  log_error msg
100
109
  halt 400, { status: 400, error: msg }
101
110
  end
102
111
 
103
- vers.title_object = title_obj || instantiate_title(title)
112
+ vers.title_object = title_obj
104
113
  vers.server_app_instance = self
105
114
  vers
106
115
  end
@@ -110,6 +119,7 @@ module Xolo
110
119
  # @return [void]
111
120
  ##################
112
121
  def halt_on_missing_version(title, version)
122
+ log_debug "Checking for missing version '#{version}' of title '#{title}'"
113
123
  return if all_versions(title).include? version
114
124
 
115
125
  msg = "No version '#{version}' for title '#{title}'."
@@ -127,6 +137,8 @@ module Xolo
127
137
  # @return [void]
128
138
  ##################
129
139
  def halt_on_existing_version(title, version)
140
+ log_debug "Checking for existing version '#{version}' of title '#{title}'"
141
+
130
142
  return unless all_versions(title).include? version
131
143
 
132
144
  msg = "Version '#{version}' of title '#{title}' already exists."
@@ -151,7 +163,7 @@ module Xolo
151
163
  halt 409, { status: 409, error: msg }
152
164
  end
153
165
 
154
- end # TitleEditor
166
+ end # Versions
155
167
 
156
168
  end # Helpers
157
169
 
@@ -67,7 +67,11 @@ module Xolo
67
67
  # This is so that the changelog can be accessed after the title is deleted.
68
68
  ################
69
69
  def self.backup_file_dir
70
- @backup_file_dir ||= Xolo::Server::BACKUPS_DIR + 'changelogs'
70
+ return @backup_file_dir if @backup_file_dir&.exist?
71
+
72
+ @backup_file_dir = Xolo::Server::BACKUPS_DIR + 'changelogs'
73
+ @backup_file_dir.mkpath unless @backup_file_dir.exist?
74
+ @backup_file_dir
71
75
  end
72
76
 
73
77
  # A hash of the read-write locks for each title's changelog file
@@ -184,8 +188,10 @@ module Xolo
184
188
  # @return [void]
185
189
  #######################
186
190
  def log_change(attrib: nil, old_val: nil, new_val: nil, msg: nil)
191
+ log_debug "Preparing to log change for #{title}: attrib=#{attrib.inspect}, old_val=#{old_val.inspect}, new_val=#{new_val.inspect}, msg=#{msg.inspect}"
192
+
187
193
  raise ArgumentError, 'Must provide attrib: or action:' if !msg && !attrib
188
- raise ArgumentError, 'Must provide old: or new: or both with attrib:' if attrib && !old_val && !new_val
194
+ raise ArgumentError, 'Must provide old: or new: or both with attrib:' if attrib && old_val.nil? && new_val.nil?
189
195
 
190
196
  # if action, attrib, old, and new are ignored
191
197
  attrib, old_val, new_val = nil if msg
@@ -262,19 +268,6 @@ module Xolo
262
268
  def log_update_changes
263
269
  return unless changes_for_update
264
270
 
265
- # self.class::ATTRIBUTES.each do |attr, deets|
266
- # next unless deets[:changelog]
267
-
268
- # new_val = deets[:type] == :time ? Time.parse(new_data_for_update[attr]) : new_data_for_update[attr]
269
- # old_val = send attr
270
-
271
- # new_val = "'#{new_val.sort.join("', '")}'" if new_val.is_a? Array
272
- # old_val = "'#{old_val.sort.join("', '")}'" if old_val.is_a? Array
273
- # next if new_val == old_val
274
-
275
- # log_change attrib: attr, old_val: old_val, new_val: new_val
276
- # end
277
-
278
271
  changes_for_update.each do |attr, vals|
279
272
  log_change attrib: attr, old_val: vals[:old], new_val: vals[:new]
280
273
  end
@@ -302,7 +295,7 @@ module Xolo
302
295
 
303
296
  # final backup
304
297
  changelog_backup_file.delete if changelog_backup_file.exist?
305
- changelog_file.rename changelog_backup_file
298
+ changelog_file.rename(changelog_backup_file)
306
299
  end
307
300
  end
308
301