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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +177 -0
  3. data/README.md +7 -0
  4. data/bin/xoloserver +106 -0
  5. data/data/com.pixar.xoloserver.plist +29 -0
  6. data/data/uninstall-pkgs-by-id.zsh +103 -0
  7. data/lib/xolo/server/app.rb +133 -0
  8. data/lib/xolo/server/command_line.rb +216 -0
  9. data/lib/xolo/server/configuration.rb +739 -0
  10. data/lib/xolo/server/constants.rb +70 -0
  11. data/lib/xolo/server/helpers/auth.rb +257 -0
  12. data/lib/xolo/server/helpers/client_data.rb +415 -0
  13. data/lib/xolo/server/helpers/file_transfers.rb +265 -0
  14. data/lib/xolo/server/helpers/jamf_pro.rb +156 -0
  15. data/lib/xolo/server/helpers/log.rb +97 -0
  16. data/lib/xolo/server/helpers/maintenance.rb +401 -0
  17. data/lib/xolo/server/helpers/notification.rb +145 -0
  18. data/lib/xolo/server/helpers/pkg_signing.rb +141 -0
  19. data/lib/xolo/server/helpers/progress_streaming.rb +252 -0
  20. data/lib/xolo/server/helpers/title_editor.rb +92 -0
  21. data/lib/xolo/server/helpers/titles.rb +145 -0
  22. data/lib/xolo/server/helpers/versions.rb +160 -0
  23. data/lib/xolo/server/log.rb +286 -0
  24. data/lib/xolo/server/mixins/changelog.rb +315 -0
  25. data/lib/xolo/server/mixins/title_jamf_access.rb +1668 -0
  26. data/lib/xolo/server/mixins/title_ted_access.rb +519 -0
  27. data/lib/xolo/server/mixins/version_jamf_access.rb +1541 -0
  28. data/lib/xolo/server/mixins/version_ted_access.rb +373 -0
  29. data/lib/xolo/server/object_locks.rb +102 -0
  30. data/lib/xolo/server/routes/auth.rb +89 -0
  31. data/lib/xolo/server/routes/jamf_pro.rb +89 -0
  32. data/lib/xolo/server/routes/maint.rb +174 -0
  33. data/lib/xolo/server/routes/title_editor.rb +71 -0
  34. data/lib/xolo/server/routes/titles.rb +285 -0
  35. data/lib/xolo/server/routes/uploads.rb +93 -0
  36. data/lib/xolo/server/routes/versions.rb +261 -0
  37. data/lib/xolo/server/routes.rb +168 -0
  38. data/lib/xolo/server/title.rb +1143 -0
  39. data/lib/xolo/server/version.rb +902 -0
  40. data/lib/xolo/server.rb +205 -0
  41. data/lib/xolo-server.rb +8 -0
  42. 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