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,70 @@
|
|
|
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 Server
|
|
12
|
+
|
|
13
|
+
module Constants
|
|
14
|
+
|
|
15
|
+
# when this module is included
|
|
16
|
+
def self.included(includer)
|
|
17
|
+
Xolo.verbose_include includer, self
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Constants
|
|
21
|
+
#####################################
|
|
22
|
+
|
|
23
|
+
EXECUTABLE_FILENAME = 'xoloserver'
|
|
24
|
+
|
|
25
|
+
# Sinatra App Environments
|
|
26
|
+
|
|
27
|
+
APP_ENV_DEV = 'development'
|
|
28
|
+
APP_ENV_TEST = 'test'
|
|
29
|
+
APP_ENV_PROD = 'production'
|
|
30
|
+
|
|
31
|
+
# Sinatra App Settings
|
|
32
|
+
|
|
33
|
+
SESSION_EXPIRE_AFTER = 3600 # seconds
|
|
34
|
+
|
|
35
|
+
# Paths
|
|
36
|
+
|
|
37
|
+
DATA_DIR = Pathname.new('/Library/Application Support/xoloserver')
|
|
38
|
+
|
|
39
|
+
BACKUPS_DIR = DATA_DIR + 'backups'
|
|
40
|
+
|
|
41
|
+
# streaming progress from the server.
|
|
42
|
+
# When a line containing only this string shows up in a stream file
|
|
43
|
+
# that means the stream is done, and no more lines will be sent.
|
|
44
|
+
PROGRESS_COMPLETE = 'PROGRESS_COMPLETE'
|
|
45
|
+
|
|
46
|
+
# The max time (in seconds) to wait for a the Jamf server to
|
|
47
|
+
# see a change in the Title Editor, e.g.
|
|
48
|
+
# a new version appearing or an EA needing acceptance.
|
|
49
|
+
# Normally the Jamf server will check in with the Title Editor
|
|
50
|
+
# every 5 minutes.
|
|
51
|
+
MAX_JAMF_WAIT_FOR_TITLE_EDITOR = 3600
|
|
52
|
+
|
|
53
|
+
# The max time (in seconds) to wait for a the Jamf server to
|
|
54
|
+
# stop the pkg deletion thread pool. It will wait until the
|
|
55
|
+
# queue is empty, or until this time has passed.
|
|
56
|
+
# Each pkg deletion thread can take up to 5 minutes, and
|
|
57
|
+
# there are 10 threads in the pool.
|
|
58
|
+
MAX_JAMF_WAIT_FOR_PKG_DELETION = 3600
|
|
59
|
+
|
|
60
|
+
# Jamf objects are named with this prefix followed by <title>-<version>
|
|
61
|
+
# See also: Xolo::Server::Version#jamf_obj_name_pfx
|
|
62
|
+
# which holds the full prefix for that version, and is used as the
|
|
63
|
+
# full object name if appropriate (e.g. Package objects)
|
|
64
|
+
JAMF_OBJECT_NAME_PFX = 'xolo-'
|
|
65
|
+
|
|
66
|
+
end # module Constants
|
|
67
|
+
|
|
68
|
+
end # Server
|
|
69
|
+
|
|
70
|
+
end # module
|
|
@@ -0,0 +1,257 @@
|
|
|
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 Helpers
|
|
16
|
+
|
|
17
|
+
module Auth
|
|
18
|
+
|
|
19
|
+
# Constants
|
|
20
|
+
#####################
|
|
21
|
+
#####################
|
|
22
|
+
|
|
23
|
+
# these routes don't need an auth'd session
|
|
24
|
+
NO_AUTH_ROUTES = [
|
|
25
|
+
'/ping',
|
|
26
|
+
'/auth/login',
|
|
27
|
+
'/default_min_os'
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
# these route prefixes don't need an auth'd session
|
|
31
|
+
NO_AUTH_PREFIXES = [
|
|
32
|
+
'/ping/'
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
# these routes are expected to be called by the xolo server itself
|
|
36
|
+
# and will have the internal_auth_token in the headers
|
|
37
|
+
# and will come from IPV4_LOOPBACK
|
|
38
|
+
#
|
|
39
|
+
# We use routes like this for internal tasks that require a
|
|
40
|
+
# server-request context.
|
|
41
|
+
INTERNAL_ROUTES = [
|
|
42
|
+
'/maint/cleanup-internal'
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
# these routes must
|
|
46
|
+
SERVER_ADMIN_ROUTES = [
|
|
47
|
+
'/maint/threads',
|
|
48
|
+
'/maint/state',
|
|
49
|
+
'/maint/cleanup',
|
|
50
|
+
'/maint/update-client-data',
|
|
51
|
+
'/maint/rotate-logs',
|
|
52
|
+
'/maint/set-log-level',
|
|
53
|
+
'/maint/shutdown-server'
|
|
54
|
+
].freeze
|
|
55
|
+
|
|
56
|
+
# The loopback address for IPV4, aka 'localhost'
|
|
57
|
+
IPV4_LOOPBACK = '127.0.0.1'
|
|
58
|
+
|
|
59
|
+
# Module methods
|
|
60
|
+
#####################
|
|
61
|
+
#####################
|
|
62
|
+
|
|
63
|
+
# when this module is included
|
|
64
|
+
#####################
|
|
65
|
+
def self.included(includer)
|
|
66
|
+
Xolo.verbose_include includer, self
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# If a request comes in from one of our known IP addresses
|
|
70
|
+
# with a valid internal_auth_toke in the headers, then the request is allowed.
|
|
71
|
+
#
|
|
72
|
+
# This allows the xolo server to send requests to itself without needing
|
|
73
|
+
# to authenticate, as is needed for some kinds of maintenance tasks
|
|
74
|
+
# such as cleanup.
|
|
75
|
+
#
|
|
76
|
+
# The token value is generated anew at startup and is a long random string, it
|
|
77
|
+
# is only available to the xolo server itself from its memory, and
|
|
78
|
+
# is never stored.
|
|
79
|
+
#
|
|
80
|
+
# @return [String] The internal_auth_token to be used in the Authorization header of requests
|
|
81
|
+
#####################
|
|
82
|
+
def self.internal_auth_token_header
|
|
83
|
+
@internal_auth_token_header ||= "Bearer #{SecureRandom.hex(64)}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Instance methods
|
|
87
|
+
#####################
|
|
88
|
+
#####################
|
|
89
|
+
|
|
90
|
+
# Is the internal_auth_token in the headers of the request?
|
|
91
|
+
# and is the request coming from one of our known IP addresses?
|
|
92
|
+
#
|
|
93
|
+
# @return [Boolean] Is this a valid request from the xolo server itself?
|
|
94
|
+
#####################
|
|
95
|
+
def valid_internal_auth_token?
|
|
96
|
+
log_info "Checking internal auth token from #{request.ip}"
|
|
97
|
+
|
|
98
|
+
if !internal_ip_ok?
|
|
99
|
+
warning = "Invalid IP address for internal request: #{request.ip}"
|
|
100
|
+
elsif !internal_token_ok?
|
|
101
|
+
warning = "Invalid internal auth token '#{request.env['HTTP_AUTHORIZATION']}' from #{request.ip}"
|
|
102
|
+
else
|
|
103
|
+
log_info "Internal request for #{request.path} is valid"
|
|
104
|
+
return true
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
log_warn "WARNING: #{warning}"
|
|
108
|
+
halt 403, { status: 403, error: 'You do not have access to this resource' }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @return [Boolean] Is the internal_auth_token in the headers of the request?
|
|
112
|
+
#####################
|
|
113
|
+
def internal_token_ok?
|
|
114
|
+
request.env['HTTP_AUTHORIZATION'] == Xolo::Server::Helpers::Auth.internal_auth_token_header
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @return [Boolean] Is the request coming from one of our known IP addresses?
|
|
118
|
+
#####################
|
|
119
|
+
def internal_ip_ok?
|
|
120
|
+
# server_ip_addresses.include? request.ip
|
|
121
|
+
# always require the request to come from the loopback address
|
|
122
|
+
request.ip == Xolo::Server::Helpers::Auth::IPV4_LOOPBACK
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @return [Array<String>] The IP addresses of this server
|
|
126
|
+
#####################
|
|
127
|
+
def server_ip_addresses
|
|
128
|
+
Socket.ip_address_list.map(&:ip_address)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# is the given username a member of the admin_jamf_group?
|
|
132
|
+
# or the server_admin_jamf_group?
|
|
133
|
+
# If not, they are not allowed to talk to the xolo server.
|
|
134
|
+
#
|
|
135
|
+
# @param admin_name [String] The jamf acct name of the person seeking access
|
|
136
|
+
#
|
|
137
|
+
# @return [Boolean] Is the admin a member of the admin_jamf_group?
|
|
138
|
+
#####################
|
|
139
|
+
def member_of_admin_jamf_group?(admin_name)
|
|
140
|
+
log_info "Checking if '#{admin_name}' is allowed to access the Xolo server"
|
|
141
|
+
|
|
142
|
+
groupname = Xolo::Server.config.admin_jamf_group
|
|
143
|
+
return true if user_in_jamf_acct_group?(groupname, admin_name)
|
|
144
|
+
|
|
145
|
+
# if they're not in the admin group, check the server_admin group
|
|
146
|
+
return true if member_of_server_admin_jamf_group?(admin_name)
|
|
147
|
+
|
|
148
|
+
log_info "'#{admin_name}' is not a member of the admin_jamf_group or the server_admin_jamf_group"
|
|
149
|
+
false
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# is the given username a member of the server_admin_jamf_group?
|
|
153
|
+
# they must be in order to access the server admin routes
|
|
154
|
+
#
|
|
155
|
+
# @param admin_name [String] The jamf acct name of the person seeking access
|
|
156
|
+
#
|
|
157
|
+
# @return [Boolean] Is the admin a member of the server_admin_jamf_group?
|
|
158
|
+
#####################
|
|
159
|
+
def member_of_server_admin_jamf_group?(admin_name)
|
|
160
|
+
return false unless Xolo::Server.config.server_admin_jamf_group
|
|
161
|
+
|
|
162
|
+
log_info "Checking if '#{admin_name}' is allowed to access server admin routes"
|
|
163
|
+
|
|
164
|
+
groupname = Xolo::Server.config.server_admin_jamf_group
|
|
165
|
+
return true if user_in_jamf_acct_group?(groupname, admin_name)
|
|
166
|
+
|
|
167
|
+
log_info "'#{admin_name}' is not a member of the server_admin_jamf_group '#{groupname}'"
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# is the session[:admin] a member of the server_admin_jamf_group,
|
|
172
|
+
# and has a valid session?
|
|
173
|
+
#
|
|
174
|
+
# @return [Boolean]
|
|
175
|
+
#####################
|
|
176
|
+
def valid_server_admin?
|
|
177
|
+
return true if session[:authenticated] && member_of_server_admin_jamf_group?(session[:admin])
|
|
178
|
+
|
|
179
|
+
halt 403, { status: 403, error: 'You do not have access to that resource.' }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# is the given username a member of the release_to_all_approval_group?
|
|
183
|
+
# If not, they are not allowed to set a title's release_groups to 'all'.
|
|
184
|
+
#
|
|
185
|
+
# @param admin_name [String] The jamf acct name of the person
|
|
186
|
+
#
|
|
187
|
+
# @return [Boolean] Is the admin allowed to set release_groups to all?
|
|
188
|
+
#####################
|
|
189
|
+
def allowed_to_release_to_all?(admin_name)
|
|
190
|
+
log_debug "Checking if '#{admin_name}' is allowed to release to all"
|
|
191
|
+
|
|
192
|
+
groupname = Xolo::Server.config.release_to_all_jamf_group
|
|
193
|
+
if groupname.pix_empty?
|
|
194
|
+
log_debug 'No release_to_all_jamf_group defined, allowing all admins to release to all'
|
|
195
|
+
return true
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
if user_in_jamf_acct_group?(groupname, admin_name)
|
|
199
|
+
log_debug "'#{admin_name}' is allowed to release to all"
|
|
200
|
+
true
|
|
201
|
+
else
|
|
202
|
+
log_debug "'#{admin_name}' is not allowed to release to all"
|
|
203
|
+
false
|
|
204
|
+
end
|
|
205
|
+
ensure
|
|
206
|
+
jamf_cnx&.disconnect
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# check to see if a username is a member of a Jamf AccountGroup either from Jamf or from LDAP
|
|
210
|
+
#
|
|
211
|
+
def user_in_jamf_acct_group?(groupname, username)
|
|
212
|
+
log_debug "Checking if '#{username}' is a member of the Jamf AccountGroup '#{groupname}'"
|
|
213
|
+
|
|
214
|
+
# This isn't well implemented in ruby-jss, so use c_get directly
|
|
215
|
+
jgroup = jamf_cnx.c_get("accounts/groupname/#{groupname}")[:group]
|
|
216
|
+
|
|
217
|
+
if jgroup[:ldap_server]
|
|
218
|
+
Jamf::LdapServer.check_membership jgroup[:ldap_server][:id], username, groupname, cnx: jamf_cnx
|
|
219
|
+
else
|
|
220
|
+
jgroup[:members].any? { |m| m[:name] == username }
|
|
221
|
+
end
|
|
222
|
+
rescue Jamf::NoSuchItemError
|
|
223
|
+
false
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Try to authenticate the jamf user trying to log in to xolo
|
|
227
|
+
#
|
|
228
|
+
# @param admin [String] The jamf acct name of the person seeking access
|
|
229
|
+
#
|
|
230
|
+
# @param pw [String] The password for the jamf acct
|
|
231
|
+
#
|
|
232
|
+
# @return [Boolean] Did the password work for the user?
|
|
233
|
+
#####################
|
|
234
|
+
def authenticated_via_jamf?(admin, pw)
|
|
235
|
+
log_debug "Checking Jamf authentication for admin '#{admin}'"
|
|
236
|
+
login_cnx = Jamf::Connection.new(
|
|
237
|
+
host: Xolo::Server.config.jamf_hostname,
|
|
238
|
+
port: Xolo::Server.config.jamf_port,
|
|
239
|
+
verify_cert: Xolo::Server.config.jamf_verify_cert,
|
|
240
|
+
open_timeout: Xolo::Server.config.jamf_open_timeout,
|
|
241
|
+
timeout: Xolo::Server.config.jamf_timeout,
|
|
242
|
+
user: admin,
|
|
243
|
+
pw: pw
|
|
244
|
+
)
|
|
245
|
+
login_cnx.disconnect
|
|
246
|
+
true
|
|
247
|
+
rescue Jamf::AuthenticationError
|
|
248
|
+
false
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
end # Routes
|
|
254
|
+
|
|
255
|
+
end # Server
|
|
256
|
+
|
|
257
|
+
end # module Xolo
|