smart_proxy_container_gateway 3.3.1 → 3.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b85cb8971300559f91cbae183a96bfa821c0bd90151c2b7c765561d9e1f84adc
4
- data.tar.gz: 988f40bafe8e1aaf13042faf2bd5505bfc416e7e4160f94419ba7b271878bdfe
3
+ metadata.gz: 470113167c3182629d62ee7babddf775b3e99e4c21ce0df5a9716894867dad86
4
+ data.tar.gz: 7adbe20e3c0dda9a9a3704501144c5f4134c7d9c266f36b19f24e83df09ad997
5
5
  SHA512:
6
- metadata.gz: 4c249cf8162619a123d179234182ff19b7f724393600738c421acd2b0627b0b370fb01f38cd0b57304c3d50765015df40170eae42617e5c29d56c457a25ec531
7
- data.tar.gz: ff795672ab091823d36af4085ca328775ec8ce2f1f5d954d0760c7ba4540b4842ab0dfcdf9d741d53ec534a7603711f5da78ac65e7b8a423a5b5f44c54d2980c
6
+ metadata.gz: b017c8a19c575d9ddcae254500917089eef21739914b5dc73447ba39af9840679028a4401bf15fd1216ae7299b7da7f55c6200a87830d7553fd8b380bc6c821b
7
+ data.tar.gz: e37b37684d5eaace152ab018067f4fa9292a5f5663543e030ed07eba894e59d6486c6b43a0c465c11dd7de65af14599b2e54a22882ff541dd5cb45e1ae381776
@@ -7,9 +7,11 @@ require 'sinatra'
7
7
  require 'smart_proxy_container_gateway/container_gateway'
8
8
  require 'smart_proxy_container_gateway/container_gateway_main'
9
9
  require 'smart_proxy_container_gateway/foreman_api'
10
+ require 'smart_proxy_container_gateway/rhsm_client'
10
11
 
11
12
  module Proxy
12
13
  module ContainerGateway
14
+ # rubocop:disable Metrics/ClassLength
13
15
  class Api < ::Sinatra::Base
14
16
  include ::Proxy::Log
15
17
  helpers ::Proxy::Helpers
@@ -19,6 +21,35 @@ module Proxy
19
21
  inject_attr :database_impl, :database
20
22
  inject_attr :container_gateway_main_impl, :container_gateway_main
21
23
 
24
+ get '/index/static/?' do
25
+ client_cert = ::Cert::RhsmClient.new(cert_from_request) if valid_cert?
26
+ valid_uuid = client_cert&.uuid&.present?
27
+
28
+ pulp_response = container_gateway_main.flatpak_static_index(translated_headers_for_proxy, params)
29
+
30
+ if pulp_response.code.to_i >= 400
31
+ status pulp_response.code.to_i
32
+ body pulp_response.body
33
+ elsif valid_uuid
34
+ host = database.connection[:hosts][{ uuid: client_cert.uuid }]
35
+ if host.nil?
36
+ repo_response = ForemanApi.new.fetch_host_repositories(client_cert.uuid, request.params)
37
+ halt repo_response.code.to_i, repo_response.body unless repo_response.code.to_i == 200
38
+ container_gateway_main.update_host_repositories(client_cert.uuid,
39
+ JSON.parse(repo_response.body)['repositories'])
40
+ end
41
+ catalog = container_gateway_main.host_catalog(client_cert.uuid).select_map(::Sequel[:repositories][:name])
42
+ pulp_index = JSON.parse(pulp_response.body)
43
+ halt 400, "Error: 'Results' key is missing in pulp_index" unless pulp_index.key?("Results")
44
+ pulp_index["Results"].select! { |result| catalog.include?(result["Name"]) }
45
+ status 200
46
+ body pulp_index.to_json
47
+ else
48
+ status pulp_response.code.to_i
49
+ body pulp_response.body
50
+ end
51
+ end
52
+
22
53
  get '/v1/_ping/?' do
23
54
  pulp_response = container_gateway_main.ping(translated_headers_for_proxy)
24
55
  status pulp_response.code.to_i
@@ -26,7 +57,9 @@ module Proxy
26
57
  end
27
58
 
28
59
  get '/v2/?' do
29
- if auth_header.present? && (auth_header.unauthorized_token? || auth_header.valid_user_token?)
60
+ client_cert = ::Cert::RhsmClient.new(cert_from_request) if valid_cert?
61
+ valid_uuid = client_cert&.uuid&.present?
62
+ if valid_uuid || (auth_header.present? && (auth_header.unauthorized_token? || auth_header.valid_user_token?))
30
63
  response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
31
64
  pulp_response = container_gateway_main.ping(translated_headers_for_proxy)
32
65
  status pulp_response.code.to_i
@@ -117,8 +150,17 @@ module Proxy
117
150
  end
118
151
 
119
152
  get '/v2/_catalog/?' do
153
+ client_cert = ::Cert::RhsmClient.new(cert_from_request) if valid_cert?
120
154
  catalog = []
121
- if auth_header.present?
155
+ if client_cert&.uuid&.present?
156
+ host = database.connection[:hosts][{ uuid: client_cert.uuid }]
157
+ if host.nil?
158
+ repo_response = ForemanApi.new.fetch_host_repositories(client_cert.uuid, request.params)
159
+ container_gateway_main.update_host_repositories(client_cert.uuid,
160
+ JSON.parse(repo_response.body)['repositories'])
161
+ end
162
+ catalog = container_gateway_main.host_catalog(client_cert.uuid).select_map(::Sequel[:repositories][:name])
163
+ elsif auth_header.present?
122
164
  if auth_header.unauthenticated_token? || auth_header.unauthorized_token?
123
165
  catalog = container_gateway_main.catalog.select_map(::Sequel[:repositories][:name])
124
166
  elsif auth_header.valid_user_token?
@@ -213,12 +255,51 @@ module Proxy
213
255
  {}
214
256
  end
215
257
 
258
+ put '/update_hosts/?' do
259
+ do_authorize_any
260
+ hosts = params['hosts'] || []
261
+ # Refresh hosts table
262
+ database.connection.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
263
+ hosts_table = database.connection[:hosts]
264
+ hosts_table.delete
265
+ hosts_table.import(%i[uuid], hosts.map { |host| [host['uuid']] })
266
+ end
267
+ {}
268
+ end
269
+
270
+ put '/host_repository_mapping/?' do
271
+ do_authorize_any
272
+ container_gateway_main.update_host_repo_mapping(params)
273
+ {}
274
+ end
275
+
276
+ put '/update_host_repositories/?' do
277
+ do_authorize_any
278
+ params['hosts'].flat_map do |host_map|
279
+ host_map.filter_map do |host_uuid, repos|
280
+ if repos.nil? || repos.empty?
281
+ repo_names = []
282
+ else
283
+ repo_names = repos
284
+ .select { |repo| repo['auth_required'].to_s.downcase == "true" }
285
+ .map { |repo| repo['repository'] }
286
+ end
287
+ container_gateway_main.update_host_repositories(host_uuid, repo_names)
288
+ end
289
+ end
290
+ {}
291
+ end
292
+
216
293
  private
217
294
 
218
295
  def flatpak_client?
219
296
  request.user_agent&.downcase&.include?('flatpak')
220
297
  end
221
298
 
299
+ def valid_cert?
300
+ cert_from_request.present? && !cert_from_request.empty? && !cert_from_request.include?('null')
301
+ end
302
+
222
303
  def head_or_get_blobs
223
304
  repository = params[:splat][0]
224
305
  digest = params[:splat][1]
@@ -272,6 +353,8 @@ module Proxy
272
353
  end
273
354
 
274
355
  def handle_repo_auth(repository, auth_header, request)
356
+ return if handle_client_cert_auth(repository)
357
+
275
358
  user_token_is_valid = false
276
359
  if auth_header.present? && auth_header.valid_user_token?
277
360
  user_token_is_valid = true
@@ -279,19 +362,33 @@ module Proxy
279
362
  # For flatpak client, header doesn't contain user name. Extract it from token.
280
363
  username ||= container_gateway_main.token_user(@value.split(' ')[1]) if flatpak_client?
281
364
  end
282
- username = request.params['account'] if username.nil?
365
+ username ||= request.params['account']
283
366
 
284
367
  return if container_gateway_main.authorized_for_repo?(repository, user_token_is_valid, username)
285
368
 
369
+ handle_unauthorized_access(username)
370
+ end
371
+
372
+ def handle_unauthorized_access(username)
286
373
  redirect_authorization_headers
374
+ halt 401, "unauthorized" if flatpak_client? && username.nil?
375
+ throw_repo_not_found_error
376
+ end
287
377
 
288
- # If username couldn't be determined from the token or auth_headers
289
- # which is case for first flatpak request, halt with 401 instead of 404
290
- if flatpak_client? && username.nil?
291
- halt 401, "unauthorized"
378
+ def handle_client_cert_auth(repository)
379
+ client_cert = ::Cert::RhsmClient.new(cert_from_request) if valid_cert?
380
+ valid_uuid = client_cert&.uuid&.present?
381
+ if valid_uuid
382
+ host = database.connection[:hosts][{ uuid: client_cert.uuid }]
383
+ if host.nil?
384
+ repo_response = ForemanApi.new.fetch_host_repositories(client_cert.uuid, request.params)
385
+ container_gateway_main.update_host_repositories(client_cert.uuid,
386
+ JSON.parse(repo_response.body)['repositories'])
387
+ end
388
+ halt 401, "unauthorized" unless container_gateway_main.cert_authorized_for_repo?(repository, client_cert.uuid)
389
+ return true
292
390
  end
293
-
294
- throw_repo_not_found_error
391
+ false
295
392
  end
296
393
 
297
394
  def redirect_authorization_headers
@@ -301,6 +398,15 @@ module Proxy
301
398
  "scope=\"repository:registry:pull,push\""
302
399
  end
303
400
 
401
+ def cert_from_request
402
+ request.env['HTTP_X_RHSM_SSL_CLIENT_CERT'] ||
403
+ request.env['SSL_CLIENT_CERT'] ||
404
+ request.env['HTTP_SSL_CLIENT_CERT'] ||
405
+ ENV['HTTP_X_RHSM_SSL_CLIENT_CERT'] ||
406
+ ENV['SSL_CLIENT_CERT'] ||
407
+ ENV['HTTP_SSL_CLIENT_CERT']
408
+ end
409
+
304
410
  def auth_header
305
411
  AuthorizationHeader.new(request.env['HTTP_AUTHORIZATION'])
306
412
  end
@@ -363,5 +469,6 @@ module Proxy
363
469
  end
364
470
  end
365
471
  end
472
+ # rubocop:enable Metrics/ClassLength
366
473
  end
367
474
  end
@@ -1,6 +1,7 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
3
  require 'digest'
4
+ require 'erb'
4
5
  require 'smart_proxy_container_gateway/dependency_injection'
5
6
  require 'sequel'
6
7
  module Proxy
@@ -40,6 +41,14 @@ module Proxy
40
41
  end
41
42
  end
42
43
 
44
+ def flatpak_static_index(headers, params = {})
45
+ uri = URI.parse("#{@pulp_endpoint}/pulpcore_registry/index/static")
46
+ unless params.empty?
47
+ uri.query = params.map { |k, v| "#{ERB::Util.url_encode(k.to_s)}=#{ERB::Util.url_encode(v.to_s)}" }.join('&')
48
+ end
49
+ pulp_registry_request(uri, headers)
50
+ end
51
+
43
52
  def ping(headers)
44
53
  uri = URI.parse("#{@pulp_endpoint}/pulpcore_registry/v2/")
45
54
  pulp_registry_request(uri, headers)
@@ -101,6 +110,17 @@ module Proxy
101
110
  end
102
111
  end
103
112
 
113
+ def host_catalog(host_uuid = nil)
114
+ if host_uuid.nil?
115
+ unauthenticated_repos
116
+ else
117
+ database.connection[:repositories].
118
+ left_join(:hosts_repositories, repository_id: :id).
119
+ left_join(:hosts, ::Sequel[:hosts][:id] => :host_id).where(uuid: host_uuid).
120
+ or(Sequel[:repositories][:auth_required] => false).order(::Sequel[:repositories][:name])
121
+ end
122
+ end
123
+
104
124
  def unauthenticated_repos
105
125
  database.connection[:repositories].where(auth_required: false).order(:name)
106
126
  end
@@ -163,6 +183,66 @@ module Proxy
163
183
  end
164
184
  end
165
185
 
186
+ # Replaces the entire host-repo mapping for all hosts.
187
+ # Assumes host is present in the DB.
188
+ def update_host_repo_mapping(host_repo_maps)
189
+ # Get DB tables
190
+ hosts_repositories = database.connection[:hosts_repositories]
191
+
192
+ # Build list of [repository_id, host_id] pairs
193
+ entries = build_host_repository_mapping(host_repo_maps)
194
+
195
+ # Insert all in a single transaction
196
+ database.connection.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
197
+ hosts_repositories.delete
198
+ hosts_repositories.import(%i[repository_id host_id], entries)
199
+ end
200
+ end
201
+
202
+ def build_host_repository_mapping(host_repo_maps)
203
+ hosts = database.connection[:hosts]
204
+ repositories = database.connection[:repositories]
205
+ entries = host_repo_maps['hosts'].flat_map do |host_map|
206
+ host_map.filter_map do |host_uuid, repos|
207
+ host = hosts[{ uuid: host_uuid }]
208
+ next unless host
209
+
210
+ repo_names = repos
211
+ .select { |repo| repo['auth_required'].to_s.downcase == "true" }
212
+ .map { |repo| repo['repository'] }
213
+
214
+ repositories
215
+ .where(name: repo_names, auth_required: true)
216
+ .select(:id)
217
+ .map { |repo| [repo[:id], host[:id]] }
218
+ end
219
+ end
220
+ entries.flatten!(1)
221
+ end
222
+
223
+ def update_host_repositories(uuid, repositories)
224
+ host = find_or_create_host(uuid)
225
+ hosts_repositories = database.connection[:hosts_repositories]
226
+ database.connection.transaction(isolation: :serializable,
227
+ retry_on: [Sequel::SerializationFailure],
228
+ num_retries: 10) do
229
+ hosts_repositories.where(host_id: host[:id]).delete
230
+ return if repositories.nil? || repositories.empty?
231
+
232
+ hosts_repositories.import(
233
+ %i[repository_id host_id],
234
+ database.connection[:repositories].where(name: repositories, auth_required: true).select(:id).map do |repo|
235
+ [repo[:id], host[:id]]
236
+ end
237
+ )
238
+ end
239
+ end
240
+
241
+ def find_or_create_host(uuid)
242
+ database.connection[:hosts].insert_conflict(target: :uuid, action: :ignore).insert(uuid: uuid)
243
+ database.connection[:hosts][{ uuid: uuid }]
244
+ end
245
+
166
246
  # Returns:
167
247
  # true if the user is authorized to access the repo, or
168
248
  # false if the user is not authorized to access the repo or if it does not exist
@@ -185,6 +265,20 @@ module Proxy
185
265
  false
186
266
  end
187
267
 
268
+ def cert_authorized_for_repo?(repo_name, uuid)
269
+ database.connection.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
270
+ repository = database.connection[:repositories][{ name: repo_name }]
271
+ return false if repository.nil?
272
+ return true unless repository[:auth_required]
273
+
274
+ database.connection[:hosts_repositories]
275
+ .where(repository_id: repository[:id])
276
+ .join(:hosts, id: :host_id)
277
+ .where(Sequel[:hosts][:uuid] => uuid)
278
+ .any?
279
+ end
280
+ end
281
+
188
282
  def token_user(token)
189
283
  database.connection[:users][{
190
284
  id: database.connection[:authentication_tokens].where(token_checksum: checksum(token)).select(:user_id)
@@ -1,15 +1,17 @@
1
1
  require 'uri'
2
+ require 'openssl'
2
3
 
3
4
  module Proxy
4
5
  module ContainerGateway
5
6
  class ForemanApi
6
- def registry_request(auth_header, params, suffix)
7
+ def registry_request(auth_header, params, suffix, uuid: '', cert: false)
7
8
  uri = URI.join(Proxy::SETTINGS.foreman_url, Proxy::ContainerGateway::Plugin.settings.katello_registry_path, suffix)
8
9
  uri.query = process_params(params)
9
10
 
10
11
  req = Net::HTTP::Get.new(uri)
11
- req.add_field('Authorization', auth_header)
12
+ req.add_field('Authorization', auth_header) unless cert
12
13
  req.add_field('Accept', 'application/json')
14
+ req.add_field('HostUUID', uuid) if cert
13
15
  req.content_type = 'application/json'
14
16
  http = Net::HTTP.new(uri.hostname, uri.port)
15
17
  http.use_ssl = true
@@ -29,6 +31,10 @@ module Proxy
29
31
  def fetch_user_repositories(auth_header, params)
30
32
  registry_request(auth_header, params, '_catalog')
31
33
  end
34
+
35
+ def fetch_host_repositories(uuid, params)
36
+ registry_request(nil, params, '_catalog', uuid: uuid, cert: true)
37
+ end
32
38
  end
33
39
  end
34
40
  end
@@ -0,0 +1,33 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Cert
5
+ class RhsmClient
6
+ attr_accessor :cert
7
+
8
+ def initialize(cert)
9
+ self.cert = extract(cert)
10
+ end
11
+
12
+ def uuid
13
+ @uuid ||= @cert.subject.to_a.find { |entry| entry[0] == 'CN' }&.[](1)
14
+ end
15
+
16
+ private
17
+
18
+ def extract(cert)
19
+ raise('Invalid cert provided. Ensure that the provided cert is not empty.') if cert.empty?
20
+
21
+ cert = strip_cert(cert)
22
+ cert = Base64.decode64(cert)
23
+ OpenSSL::X509::Certificate.new(cert)
24
+ end
25
+
26
+ def strip_cert(cert)
27
+ cert = cert.to_s.gsub("-----BEGIN CERTIFICATE-----", "").gsub("-----END CERTIFICATE-----", "")
28
+ cert.delete!(' ')
29
+ cert.delete!("\n")
30
+ cert
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,24 @@
1
+ Sequel.migration do
2
+ up do
3
+ # Create the hosts table
4
+ create_table(:hosts) do
5
+ primary_key :id
6
+ String :uuid, null: false, unique: true
7
+ end
8
+
9
+ # Create the hosts_repositories join table
10
+ create_table(:hosts_repositories) do
11
+ foreign_key :host_id, :hosts, on_delete: :cascade
12
+ foreign_key :repository_id, :repositories, on_delete: :cascade
13
+ primary_key %i[host_id repository_id]
14
+ end
15
+ end
16
+
17
+ down do
18
+ # Drop the hosts_repositories join table
19
+ drop_table(:hosts_repositories)
20
+
21
+ # Drop the hosts table
22
+ drop_table(:hosts)
23
+ end
24
+ end
@@ -1,5 +1,5 @@
1
1
  module Proxy
2
2
  module ContainerGateway
3
- VERSION = '3.3.1'.freeze
3
+ VERSION = '3.4.0'.freeze
4
4
  end
5
5
  end
@@ -2,3 +2,4 @@ require 'smart_proxy_container_gateway/version'
2
2
  require 'smart_proxy_container_gateway/database'
3
3
  require 'smart_proxy_container_gateway/container_gateway'
4
4
  require 'smart_proxy_container_gateway/container_gateway_main'
5
+ require 'smart_proxy_container_gateway/rhsm_client'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_proxy_container_gateway
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.1
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Ballou
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-04-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -77,8 +76,8 @@ email: ianballou67@gmail.com
77
76
  executables: []
78
77
  extensions: []
79
78
  extra_rdoc_files:
80
- - README.md
81
79
  - LICENSE
80
+ - README.md
82
81
  files:
83
82
  - LICENSE
84
83
  - README.md
@@ -91,17 +90,18 @@ files:
91
90
  - lib/smart_proxy_container_gateway/database.rb
92
91
  - lib/smart_proxy_container_gateway/dependency_injection.rb
93
92
  - lib/smart_proxy_container_gateway/foreman_api.rb
93
+ - lib/smart_proxy_container_gateway/rhsm_client.rb
94
94
  - lib/smart_proxy_container_gateway/sequel_migrations/001_initial.rb
95
95
  - lib/smart_proxy_container_gateway/sequel_migrations/002_auth_tokens.rb
96
96
  - lib/smart_proxy_container_gateway/sequel_migrations/003_authorization_reorg.rb
97
97
  - lib/smart_proxy_container_gateway/sequel_migrations/004_users_repositories_cascade_delete.rb
98
+ - lib/smart_proxy_container_gateway/sequel_migrations/005_host_repositories.rb
98
99
  - lib/smart_proxy_container_gateway/version.rb
99
100
  - settings.d/container_gateway.yml.example
100
101
  homepage: https://github.com/Katello/smart_proxy_container_gateway
101
102
  licenses:
102
103
  - GPL-3.0-only
103
104
  metadata: {}
104
- post_install_message:
105
105
  rdoc_options: []
106
106
  require_paths:
107
107
  - lib
@@ -116,8 +116,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
116
116
  - !ruby/object:Gem::Version
117
117
  version: '0'
118
118
  requirements: []
119
- rubygems_version: 3.5.22
120
- signing_key:
119
+ rubygems_version: 3.6.7
121
120
  specification_version: 4
122
121
  summary: Pulp 3 container registry support for Foreman/Katello Smart-Proxy
123
122
  test_files: []