xolo-server 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.
- checksums.yaml +7 -0
- data/LICENSE.txt +177 -0
- data/README.md +7 -0
- data/bin/xoloserver +106 -0
- data/data/com.pixar.xoloserver.plist +29 -0
- data/data/uninstall-pkgs-by-id.zsh +103 -0
- data/lib/xolo/server/app.rb +133 -0
- data/lib/xolo/server/command_line.rb +216 -0
- data/lib/xolo/server/configuration.rb +739 -0
- data/lib/xolo/server/constants.rb +70 -0
- data/lib/xolo/server/helpers/auth.rb +257 -0
- data/lib/xolo/server/helpers/client_data.rb +415 -0
- data/lib/xolo/server/helpers/file_transfers.rb +265 -0
- data/lib/xolo/server/helpers/jamf_pro.rb +156 -0
- data/lib/xolo/server/helpers/log.rb +97 -0
- data/lib/xolo/server/helpers/maintenance.rb +401 -0
- data/lib/xolo/server/helpers/notification.rb +145 -0
- data/lib/xolo/server/helpers/pkg_signing.rb +141 -0
- data/lib/xolo/server/helpers/progress_streaming.rb +252 -0
- data/lib/xolo/server/helpers/title_editor.rb +92 -0
- data/lib/xolo/server/helpers/titles.rb +145 -0
- data/lib/xolo/server/helpers/versions.rb +160 -0
- data/lib/xolo/server/log.rb +286 -0
- data/lib/xolo/server/mixins/changelog.rb +315 -0
- data/lib/xolo/server/mixins/title_jamf_access.rb +1668 -0
- data/lib/xolo/server/mixins/title_ted_access.rb +519 -0
- data/lib/xolo/server/mixins/version_jamf_access.rb +1541 -0
- data/lib/xolo/server/mixins/version_ted_access.rb +373 -0
- data/lib/xolo/server/object_locks.rb +102 -0
- data/lib/xolo/server/routes/auth.rb +89 -0
- data/lib/xolo/server/routes/jamf_pro.rb +89 -0
- data/lib/xolo/server/routes/maint.rb +174 -0
- data/lib/xolo/server/routes/title_editor.rb +71 -0
- data/lib/xolo/server/routes/titles.rb +285 -0
- data/lib/xolo/server/routes/uploads.rb +93 -0
- data/lib/xolo/server/routes/versions.rb +261 -0
- data/lib/xolo/server/routes.rb +168 -0
- data/lib/xolo/server/title.rb +1143 -0
- data/lib/xolo/server/version.rb +902 -0
- data/lib/xolo/server.rb +205 -0
- data/lib/xolo-server.rb +8 -0
- metadata +243 -0
|
@@ -0,0 +1,373 @@
|
|
|
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 Mixins
|
|
16
|
+
|
|
17
|
+
# This is mixed in to Xolo::Server::Version
|
|
18
|
+
# to define Version/Patch-related access to the Title Edit server
|
|
19
|
+
#
|
|
20
|
+
module VersionTedAccess
|
|
21
|
+
|
|
22
|
+
# Module methods
|
|
23
|
+
#
|
|
24
|
+
# These are available as module methods but not as 'helper'
|
|
25
|
+
# methods in sinatra routes & views.
|
|
26
|
+
#
|
|
27
|
+
##############################
|
|
28
|
+
##############################
|
|
29
|
+
|
|
30
|
+
# when this module is included
|
|
31
|
+
##############################
|
|
32
|
+
def self.included(includer)
|
|
33
|
+
Xolo.verbose_include includer, self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Instance methods
|
|
37
|
+
#
|
|
38
|
+
# These are available directly in sinatra routes and views
|
|
39
|
+
#
|
|
40
|
+
##############################
|
|
41
|
+
##############################
|
|
42
|
+
|
|
43
|
+
# Create a new version in the title editor
|
|
44
|
+
#
|
|
45
|
+
# TODO: allow specification of version_order, probably by accepting a value
|
|
46
|
+
# for the 'previous_version'?
|
|
47
|
+
#
|
|
48
|
+
# @return [void]
|
|
49
|
+
##########################
|
|
50
|
+
def create_patch_in_ted
|
|
51
|
+
progress "Title Editor: Creating Patch '#{version}' of SoftwareTitle '#{title}'", log: :info
|
|
52
|
+
ted_title.patches.add_patch(
|
|
53
|
+
version: version,
|
|
54
|
+
minimumOperatingSystem: min_os,
|
|
55
|
+
releaseDate: publish_date,
|
|
56
|
+
reboot: reboot,
|
|
57
|
+
standalone: standalone
|
|
58
|
+
)
|
|
59
|
+
new_patch = ted_title.patches.patch version
|
|
60
|
+
|
|
61
|
+
set_patch_killapps
|
|
62
|
+
set_patch_capabilites
|
|
63
|
+
set_ted_patch_component_criteria ttl_obj: title_object
|
|
64
|
+
|
|
65
|
+
self.ted_id_number = new_patch.patchId
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Update version/patch in the title editor directly.
|
|
69
|
+
# This never called when updating versions via changes to
|
|
70
|
+
# the title - that process calls the sub-methods directly.
|
|
71
|
+
#
|
|
72
|
+
# @return [void]
|
|
73
|
+
##########################
|
|
74
|
+
def update_patch_in_ted
|
|
75
|
+
progress "Title Editor: Updating Patch '#{version}' SoftwareTitle '#{title}'", log: :info
|
|
76
|
+
|
|
77
|
+
Xolo::Server::Version::ATTRIBUTES.each do |attr, deets|
|
|
78
|
+
ted_attribute = deets[:ted_attribute]
|
|
79
|
+
next unless ted_attribute
|
|
80
|
+
|
|
81
|
+
new_val = new_data_for_update[attr]
|
|
82
|
+
old_val = send(attr)
|
|
83
|
+
next if new_val == old_val
|
|
84
|
+
|
|
85
|
+
# These changes happen in real time on the Title Editor server, no need to #save
|
|
86
|
+
log_debug "Title Editor: Updating patch attribute '#{ted_attribute}': #{old_val} -> #{new_val}"
|
|
87
|
+
ted_patch.send "#{ted_attribute}=", new_val
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
set_patch_killapps if changes_for_update[:killapps]
|
|
91
|
+
set_patch_capabilites if changes_for_update[:min_os] || changes_for_update[:max_os]
|
|
92
|
+
|
|
93
|
+
# This should not be needed here - its only affected by changes to the title, and
|
|
94
|
+
# those are handled by the title object calling the same method
|
|
95
|
+
#
|
|
96
|
+
# set_ted_patch_component_criteria ttl_obj: title_object
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Set any killapps for this version in the title editor.
|
|
100
|
+
#
|
|
101
|
+
# @return [void]
|
|
102
|
+
##########################
|
|
103
|
+
def set_patch_killapps
|
|
104
|
+
# creating a new patch
|
|
105
|
+
if creating?
|
|
106
|
+
kapps = killapps
|
|
107
|
+
|
|
108
|
+
# updating - delete and replace with any new ones
|
|
109
|
+
elsif updating?
|
|
110
|
+
return unless changes_for_update.key? :killapps
|
|
111
|
+
|
|
112
|
+
kapps = changes_for_update[:killapps][:new]
|
|
113
|
+
ted_patch.killApps.delete_all_killApps
|
|
114
|
+
|
|
115
|
+
# repairing delete and replace with current, to ensure values are correct.
|
|
116
|
+
elsif repairing?
|
|
117
|
+
kapps = killapps
|
|
118
|
+
ted_patch.killApps.delete_all_killApps
|
|
119
|
+
else
|
|
120
|
+
return
|
|
121
|
+
end
|
|
122
|
+
return if kapps.pix_empty?
|
|
123
|
+
|
|
124
|
+
# set the current values
|
|
125
|
+
kapps.each do |ka_str|
|
|
126
|
+
msg = "Title Editor: Setting killApp '#{ka_str}' for Patch '#{version}' of SoftwareTitle '#{title}'"
|
|
127
|
+
progress msg, log: :info
|
|
128
|
+
|
|
129
|
+
name, bundleid = ka_str.split(Xolo::SEMICOLON_SEP_RE)
|
|
130
|
+
ted_patch.killApps.add_killApp(
|
|
131
|
+
appName: name,
|
|
132
|
+
bundleId: bundleid
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Set the capabilities for this version in the title editor.
|
|
138
|
+
# This is a collection of criteria that define which computers
|
|
139
|
+
# can install this version.
|
|
140
|
+
#
|
|
141
|
+
# At the very least we enforce the required minimum OS.
|
|
142
|
+
# and optional maximim OS.
|
|
143
|
+
#
|
|
144
|
+
# TODO: Allow xadm to specify other capability criteria?
|
|
145
|
+
#
|
|
146
|
+
# @return [void]
|
|
147
|
+
##########################
|
|
148
|
+
def set_patch_capabilites
|
|
149
|
+
# creating a new patch? Just use the current values
|
|
150
|
+
if creating?
|
|
151
|
+
min = min_os
|
|
152
|
+
max = max_os
|
|
153
|
+
|
|
154
|
+
# updating an existing patch? Use the new values if they exist
|
|
155
|
+
# noting that a nil new value for max_os means its being removed
|
|
156
|
+
elsif updating?
|
|
157
|
+
return unless changes_for_update&.key?(:max_os) || changes_for_update&.key?(:min_os)
|
|
158
|
+
|
|
159
|
+
# min gets reset even if it didn't change and it can't be empty.
|
|
160
|
+
min = changes_for_update.dig(:min_os, :new) || min_os
|
|
161
|
+
|
|
162
|
+
# if max is changing and its nil, its being removed, so won't be re-added
|
|
163
|
+
# if its not changing, keep the current value
|
|
164
|
+
max = changes_for_update&.key?(:max_os) ? changes_for_update[:max_os][:new] : max_os
|
|
165
|
+
|
|
166
|
+
# delete the existing criteria
|
|
167
|
+
ted_patch.capabilities.delete_all_criteria
|
|
168
|
+
|
|
169
|
+
elsif repairing?
|
|
170
|
+
min = min_os
|
|
171
|
+
max = max_os
|
|
172
|
+
# delete the existing criteria
|
|
173
|
+
ted_patch.capabilities.delete_all_criteria
|
|
174
|
+
|
|
175
|
+
else
|
|
176
|
+
return
|
|
177
|
+
end
|
|
178
|
+
return unless min || max
|
|
179
|
+
|
|
180
|
+
msg = "Title Editor: Setting min_os capability for Patch '#{version}' of SoftwareTitle '#{title}' to '#{min}'"
|
|
181
|
+
progress msg, log: :info
|
|
182
|
+
|
|
183
|
+
# min os - one is always required
|
|
184
|
+
ted_patch.capabilities.add_criterion(
|
|
185
|
+
name: 'Operating System Version',
|
|
186
|
+
operator: 'greater than or equal',
|
|
187
|
+
value: min
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return unless max
|
|
191
|
+
|
|
192
|
+
msg = "Title Editor: Setting max_os capability for Patch '#{version}' of SoftwareTitle '#{title}' to '#{max}'"
|
|
193
|
+
progress msg, log: :debug
|
|
194
|
+
|
|
195
|
+
ted_patch.capabilities.add_criterion(
|
|
196
|
+
name: 'Operating System Version',
|
|
197
|
+
operator: 'less than or equal',
|
|
198
|
+
value: max
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Set the component criteria for this version in the title editor.
|
|
203
|
+
#
|
|
204
|
+
# @param app_name [String] the name of the app to use in app-based requirements,
|
|
205
|
+
# must be used with app_bundle_id, cannot be used with ea_name
|
|
206
|
+
#
|
|
207
|
+
# @param app_bundle_id [String] the bundle id of the app to use in app-based requirements
|
|
208
|
+
# must be used with app_name, cannot be used with ea_name
|
|
209
|
+
#
|
|
210
|
+
# @param ea_name [String] the name of the EA to use in EA-based requirements (the ted_ea_key)
|
|
211
|
+
# Cannot be used with app_name or app_bundle_id
|
|
212
|
+
#
|
|
213
|
+
# @param ttl_obj [Xolo::Server::Title] the title object with the values
|
|
214
|
+
#
|
|
215
|
+
# This is a collection of criteria that define which computers
|
|
216
|
+
# have this version installed
|
|
217
|
+
#
|
|
218
|
+
# @return [void]
|
|
219
|
+
##########################
|
|
220
|
+
def set_ted_patch_component_criteria(app_name: nil, app_bundle_id: nil, ea_name: nil, ttl_obj: nil)
|
|
221
|
+
if ttl_obj
|
|
222
|
+
app_name, app_bundle_id, ea_name = get_patch_component_criteria_params(ttl_obj)
|
|
223
|
+
|
|
224
|
+
elsif !((app_name && app_bundle_id) || ea_name)
|
|
225
|
+
raise Xolo::MissingDataError, 'Must provide either ea_name or app_name & app_bundle_id, or ttl_obj'
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
type = ea_name ? 'Extension Attribute (version_script)' : 'App'
|
|
229
|
+
msg = "Title Editor: Setting #{type}-based component criteria for Patch '#{version}' of SoftwareTitle '#{title}'"
|
|
230
|
+
progress msg, log: :info
|
|
231
|
+
|
|
232
|
+
# delete any already there and make a new one
|
|
233
|
+
ted_patch.delete_component
|
|
234
|
+
ted_patch.add_component name: title, version: version
|
|
235
|
+
comp = ted_patch.component
|
|
236
|
+
|
|
237
|
+
ea_name ? set_ea_component(comp, ea_name) : set_app_component(comp, app_name, app_bundle_id)
|
|
238
|
+
|
|
239
|
+
# SHouldn't be needed here, is called in both create and update
|
|
240
|
+
# enable_ted_patch
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# get the param values for patch component criteria from the title object,
|
|
244
|
+
# which may be setting them via an update
|
|
245
|
+
#
|
|
246
|
+
# @param ttl_obj [Xolo::Server::Title] the title object with the values
|
|
247
|
+
# @return [Array] the values for the component criteria
|
|
248
|
+
##########################
|
|
249
|
+
def get_patch_component_criteria_params(ttl_obj)
|
|
250
|
+
app_name, app_bundle_id, ea_name = nil
|
|
251
|
+
|
|
252
|
+
if ttl_obj.updating?
|
|
253
|
+
if ttl_obj.changes_for_update.dig :app_name, :new
|
|
254
|
+
app_name = ttl_obj.changes_for_update[:app_name][:new]
|
|
255
|
+
app_bundle_id = ttl_obj.changes_for_update[:app_bundle_id][:new]
|
|
256
|
+
else
|
|
257
|
+
ea_name = ttl_obj.ted_ea_key
|
|
258
|
+
end
|
|
259
|
+
elsif ttl_obj.app_name
|
|
260
|
+
app_name = ttl_obj.app_name
|
|
261
|
+
app_bundle_id = ttl_obj.app_bundle_id
|
|
262
|
+
else
|
|
263
|
+
ea_name = ttl_obj.ted_ea_key
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
[app_name, app_bundle_id, ea_name]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# @return [void]
|
|
270
|
+
##############################
|
|
271
|
+
def set_ea_component(comp, ea_name)
|
|
272
|
+
comp.criteria.add_criterion(
|
|
273
|
+
type: 'extensionAttribute',
|
|
274
|
+
name: ea_name,
|
|
275
|
+
operator: 'is',
|
|
276
|
+
value: version
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# @return [void]
|
|
281
|
+
##############################
|
|
282
|
+
def set_app_component(comp, app_name, app_bundle_id)
|
|
283
|
+
comp.criteria.add_criterion(
|
|
284
|
+
name: 'Application Title',
|
|
285
|
+
operator: 'is',
|
|
286
|
+
value: app_name
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
comp.criteria.add_criterion(
|
|
290
|
+
name: 'Application Bundle ID',
|
|
291
|
+
operator: 'is',
|
|
292
|
+
value: app_bundle_id
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
comp.criteria.add_criterion(
|
|
296
|
+
name: 'Application Version',
|
|
297
|
+
operator: 'is',
|
|
298
|
+
value: version
|
|
299
|
+
)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# For a patch to be enabled in the Title Editor, it needs at least a component criterion
|
|
303
|
+
# and one capability. Xolo enforces those when the patch is created, so from the title
|
|
304
|
+
# editor's view it should be OK from the start.
|
|
305
|
+
#
|
|
306
|
+
# But Xolo can't really do anything with it until there's a Jamf Package object and
|
|
307
|
+
# an uploaded installer.
|
|
308
|
+
# So once we have those, this method is called to enable the patch.
|
|
309
|
+
#
|
|
310
|
+
# @param version [Xolo::Server::Version] the version who's patch to enable
|
|
311
|
+
#
|
|
312
|
+
# @return [void]
|
|
313
|
+
##############################
|
|
314
|
+
def enable_ted_patch
|
|
315
|
+
progress "Title Editor: (Re-)Enabling Patch '#{version}' of SoftwareTitle '#{title}'", log: :info
|
|
316
|
+
ted_patch(refresh: true).enable
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Delete from the title editor
|
|
320
|
+
# @return [Integer] title editor id
|
|
321
|
+
###########################
|
|
322
|
+
def delete_patch_from_ted
|
|
323
|
+
patch_id = ted_title.patches.versions_to_patchIds[version]
|
|
324
|
+
if patch_id
|
|
325
|
+
progress "Title Editor: Deleting Patch '#{version}' of SoftwareTitle '#{title}'", log: :info
|
|
326
|
+
ted_title.patches.delete_patch patch_id
|
|
327
|
+
|
|
328
|
+
else
|
|
329
|
+
log_debug "Title Editor: No id for Patch '#{version}' of SoftwareTitle '#{title}', nothing to delete"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
ted_id_number
|
|
333
|
+
rescue Windoo::NoSuchItemError
|
|
334
|
+
ted_id_number
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# @return [String] the URL for the Title Editor Web App page for this patch
|
|
338
|
+
###################################
|
|
339
|
+
def ted_patch_url
|
|
340
|
+
"https://#{Xolo::Server.config.ted_hostname}/patches/#{ted_id_number}"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Ensure all the TEd items for this version are correct based on the server data.
|
|
344
|
+
#########################
|
|
345
|
+
def repair_ted_patch
|
|
346
|
+
progress "Title Editor: Repairing Patch '#{version}' of SoftwareTitle '#{title}'", log: :info
|
|
347
|
+
|
|
348
|
+
Xolo::Server::Version::ATTRIBUTES.each do |attr, deets|
|
|
349
|
+
ted_attribute = deets[:ted_attribute]
|
|
350
|
+
# if there's no ted_attribute, this isn't a value stored in the Title Editor
|
|
351
|
+
next unless ted_attribute
|
|
352
|
+
|
|
353
|
+
real_val = send(attr)
|
|
354
|
+
ted_val = ted_patch.send(ted_attribute)
|
|
355
|
+
next if ted_val == real_val
|
|
356
|
+
|
|
357
|
+
# These changes happen in real time on the Title Editor server, no need to #save
|
|
358
|
+
progress "Title Editor: Repairing patch attribute '#{ted_attribute}': #{ted_val} -> #{real_val}"
|
|
359
|
+
ted_patch.send "#{ted_attribute}=", real_val
|
|
360
|
+
end
|
|
361
|
+
set_patch_killapps
|
|
362
|
+
set_patch_capabilites
|
|
363
|
+
set_ted_patch_component_criteria ttl_obj: title_object
|
|
364
|
+
enable_ted_patch unless ted_patch(refresh: true).enabled?
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
end # VersionTedAccess
|
|
368
|
+
|
|
369
|
+
end # Mixins
|
|
370
|
+
|
|
371
|
+
end # Server
|
|
372
|
+
|
|
373
|
+
end # module Xolo
|
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
# main module
|
|
10
|
+
module Xolo
|
|
11
|
+
|
|
12
|
+
# Server Module
|
|
13
|
+
module Server
|
|
14
|
+
|
|
15
|
+
# Code for locking titles and versions as they are being modified
|
|
16
|
+
##################################
|
|
17
|
+
module ObjectLocks
|
|
18
|
+
|
|
19
|
+
# when this module is included
|
|
20
|
+
def self.included(includer)
|
|
21
|
+
Xolo.verbose_include includer, self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# when this module is extended
|
|
25
|
+
def self.extended(extender)
|
|
26
|
+
Xolo.verbose_extend extender, self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# How many seconds are update locks valid for?
|
|
30
|
+
OBJECT_LOCK_LIMIT = 60 * 60
|
|
31
|
+
|
|
32
|
+
# The locks for titles and versions - when they are being created or updated
|
|
33
|
+
# they appear in this data structure with an expiration timestamp.
|
|
34
|
+
# During that time, no other updates can be made to the same title or version.
|
|
35
|
+
#
|
|
36
|
+
# Keys are the title names, values are hashes with these sub-keys:
|
|
37
|
+
# - expires: the time the title lock expires, time locked plus OBJECT_LOCK_LIMIT
|
|
38
|
+
# - versions: Hash of versions_that_are_locked => expiration_time
|
|
39
|
+
#
|
|
40
|
+
# @return [Concurrent::Hash] The locks
|
|
41
|
+
################################
|
|
42
|
+
def object_locks
|
|
43
|
+
@object_locks ||= Concurrent::Hash.new
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Remove any expired locks from the object_locks
|
|
47
|
+
################################
|
|
48
|
+
def remove_expired_object_locks
|
|
49
|
+
now = Time.now
|
|
50
|
+
|
|
51
|
+
# first delete any expired version locks
|
|
52
|
+
object_locks.each_value do |locks|
|
|
53
|
+
locks[:versions] ||= {}
|
|
54
|
+
|
|
55
|
+
locks[:versions].delete_if do |vers, exp|
|
|
56
|
+
if exp < now
|
|
57
|
+
log_debug "Removing expired lock on version #{vers} of title #{title}"
|
|
58
|
+
true
|
|
59
|
+
else
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# now delete any expired title locks that have no versions locked
|
|
66
|
+
object_locks.delete_if do |_title, locks|
|
|
67
|
+
# keep it if there are versions locked
|
|
68
|
+
if !locks[:versions].empty?
|
|
69
|
+
false
|
|
70
|
+
|
|
71
|
+
# if there's a title lock expiration time, check it
|
|
72
|
+
elsif locks[:expires]
|
|
73
|
+
if locks[:expires] < now
|
|
74
|
+
log_debug "Removing expired lock on title #{title}"
|
|
75
|
+
true
|
|
76
|
+
else
|
|
77
|
+
false
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# no title-lock expiration time
|
|
81
|
+
else
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Testing Concurrent::ReentrantReadWriteLock for titles and versions
|
|
88
|
+
# to be acquired and released in the route blocks
|
|
89
|
+
#
|
|
90
|
+
def rw_lock(title, version = nil)
|
|
91
|
+
@rw_locks ||= Concurrent::Hash.new
|
|
92
|
+
@rw_locks[title] ||= { lock: Concurrent::ReentrantReadWriteLock.new }
|
|
93
|
+
return @rw_locks[title] unless version
|
|
94
|
+
|
|
95
|
+
@rw_locks[title][version] ||= Concurrent::ReentrantReadWriteLock.new
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
end # ObjectLocks
|
|
99
|
+
|
|
100
|
+
end # Server
|
|
101
|
+
|
|
102
|
+
end # module Xolo
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
# main module
|
|
10
|
+
module Xolo
|
|
11
|
+
|
|
12
|
+
# Server Module
|
|
13
|
+
module Server
|
|
14
|
+
|
|
15
|
+
module Routes
|
|
16
|
+
|
|
17
|
+
module Auth
|
|
18
|
+
|
|
19
|
+
# This is how we 'mix in' modules to Sinatra servers:
|
|
20
|
+
# We make them extentions here with
|
|
21
|
+
# extend Sinatra::Extension (from sinatra-contrib)
|
|
22
|
+
# and then 'register' them in the server with
|
|
23
|
+
# register Xolo::Server::<Module>
|
|
24
|
+
# Doing it this way allows us to split the code into a logical
|
|
25
|
+
# file structure, without re-opening the Sinatra::Base server app,
|
|
26
|
+
extend Sinatra::Extension
|
|
27
|
+
|
|
28
|
+
# when this module is included
|
|
29
|
+
def self.included(includer)
|
|
30
|
+
Xolo.verbose_include includer, self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Auth a Xolo Admin via Jamf API login
|
|
34
|
+
# Must be a member of the Jamf Admin group
|
|
35
|
+
# named in Xolo::Server.config.admin_jamf_group
|
|
36
|
+
# before
|
|
37
|
+
###################
|
|
38
|
+
post '/auth/login' do
|
|
39
|
+
request.body.rewind
|
|
40
|
+
payload = parse_json request.body.read
|
|
41
|
+
admin = payload[:admin]
|
|
42
|
+
pw = payload[:password]
|
|
43
|
+
|
|
44
|
+
err =
|
|
45
|
+
if !member_of_admin_jamf_group?(admin)
|
|
46
|
+
"'#{admin}' is not allowed to use the Xolo server"
|
|
47
|
+
elsif !authenticated_via_jamf?(admin, pw)
|
|
48
|
+
"Incorrect xolo admin username or password for '#{admin}'"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if err
|
|
52
|
+
log_info "Authentication failed: #{err}"
|
|
53
|
+
halt 401, { status: 401, error: err }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Set the session values
|
|
57
|
+
session[:xolo_id] = "#{Time.now.to_i}#{SecureRandom.alphanumeric 3}"
|
|
58
|
+
session[:admin] = payload[:proxy_admin] ? "#{payload[:proxy_admin]}-via-#{admin}" : admin
|
|
59
|
+
session[:authenticated] = true
|
|
60
|
+
log_info "Authenticated admin '#{session[:admin]}' from #{request.ip}"
|
|
61
|
+
|
|
62
|
+
body({ admin: admin, authenticated: true, proxy_admin: payload[:proxy_admin] })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Log out the current admin
|
|
66
|
+
###################
|
|
67
|
+
post '/auth/logout' do
|
|
68
|
+
log_info "Logging out admin '#{session[:admin]}' from #{request.ip}"
|
|
69
|
+
session.clear
|
|
70
|
+
body({ authenticated: false })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# check if the current admin is allowed to set a title's release groups to 'all'
|
|
74
|
+
###################
|
|
75
|
+
get '/auth/release_to_all_allowed' do
|
|
76
|
+
data = {
|
|
77
|
+
allowed: allowed_to_release_to_all?(session[:admin]),
|
|
78
|
+
msg: "Please contact #{Xolo::Server.config.release_to_all_contact} to set release groups to '#{Xolo::TARGET_ALL}'. Let us know why you think the title should be deployed to all computers. Until then you can leave the release groups empty, or use actual Jamf groups."
|
|
79
|
+
}
|
|
80
|
+
body data
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end # module auth
|
|
84
|
+
|
|
85
|
+
end # Routes
|
|
86
|
+
|
|
87
|
+
end # Server
|
|
88
|
+
|
|
89
|
+
end # module Xolo
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
# main module
|
|
10
|
+
module Xolo
|
|
11
|
+
|
|
12
|
+
# Server Module
|
|
13
|
+
module Server
|
|
14
|
+
|
|
15
|
+
module Routes
|
|
16
|
+
|
|
17
|
+
module JamfPro
|
|
18
|
+
|
|
19
|
+
# This is how we 'mix in' modules to Sinatra servers
|
|
20
|
+
# for route definitions and similar things
|
|
21
|
+
#
|
|
22
|
+
# (things to be 'included' for use in route and view processing
|
|
23
|
+
# are mixed in by delcaring them to be helpers)
|
|
24
|
+
#
|
|
25
|
+
# We make them extentions here with
|
|
26
|
+
# extend Sinatra::Extension (from sinatra-contrib)
|
|
27
|
+
# and then 'register' them in the server with
|
|
28
|
+
# register Xolo::Server::<Module>
|
|
29
|
+
# Doing it this way allows us to split the code into a logical
|
|
30
|
+
# file structure, without re-opening the Sinatra::Base server app,
|
|
31
|
+
# and let xeitwork do the requiring of those files
|
|
32
|
+
extend Sinatra::Extension
|
|
33
|
+
|
|
34
|
+
# Module methods
|
|
35
|
+
#
|
|
36
|
+
##############################
|
|
37
|
+
##############################
|
|
38
|
+
|
|
39
|
+
# when this module is included
|
|
40
|
+
def self.included(includer)
|
|
41
|
+
Xolo.verbose_include includer, self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# when this module is extended
|
|
45
|
+
def self.extended(extender)
|
|
46
|
+
Xolo.verbose_extend extender, self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Routes
|
|
50
|
+
#
|
|
51
|
+
##############################
|
|
52
|
+
##############################
|
|
53
|
+
|
|
54
|
+
# We probably don't need this - just for testing for now.
|
|
55
|
+
###############
|
|
56
|
+
get '/jamf/package-names' do
|
|
57
|
+
log_debug "Jamf: Fetching Jamf Package Names for #{session[:admin]}"
|
|
58
|
+
jamf_cnx
|
|
59
|
+
body Jamf::JPackage.all_names(:refresh, cnx: jamf_cnx).sort
|
|
60
|
+
ensure
|
|
61
|
+
jamf_cnx&.disconnect
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# A list of all current computer groups
|
|
65
|
+
###############
|
|
66
|
+
get '/jamf/computer-group-names' do
|
|
67
|
+
log_debug "Jamf: Fetching Jamf ComputerGroup Names for #{session[:admin]}"
|
|
68
|
+
body Jamf::ComputerGroup.all_names(:refresh, cnx: jamf_cnx).sort
|
|
69
|
+
ensure
|
|
70
|
+
jamf_cnx&.disconnect
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# A list of all current categories
|
|
74
|
+
###############
|
|
75
|
+
get '/jamf/category-names' do
|
|
76
|
+
log_debug "Jamf: Fetching Jamf Category Names for #{session[:admin]}"
|
|
77
|
+
jcnx = jamf_cnx
|
|
78
|
+
body Jamf::Category.all_names(:refresh, cnx: jcnx).sort
|
|
79
|
+
ensure
|
|
80
|
+
jcnx&.disconnect
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
end # Module
|
|
84
|
+
|
|
85
|
+
end # Routes
|
|
86
|
+
|
|
87
|
+
end # Server
|
|
88
|
+
|
|
89
|
+
end # module Xolo
|