smart_proxy_container_gateway 1.2.0 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d41b324aaa36809e103fc5a20cf934ed6821825355d6968e140f1ec1135c347
4
- data.tar.gz: 645b65950210122681aed76963da3a95cb48ef75284c139046c992ff49bfa30f
3
+ metadata.gz: bb4e40d814ff330008ad5f1bf4bf667ba9e91415bc997ea4d66a2beb7fb0a00e
4
+ data.tar.gz: 84360d1228198e91460a2b841985b12c5c9f5d5d062d191862a2e9cc5bef10e9
5
5
  SHA512:
6
- metadata.gz: 012b85b6a699b3f3887d3ec69e2995aa230c26be432c4353052946065135cfe5496dac8d27a4c7d3e5533f40dcc6c584fae81e22b695ea7af8e8087b6e80ddc3
7
- data.tar.gz: 0a1ce283eb9b6fbbb9ec3360a16b7b76107b9f294ba3d5a58cac0e9147c94a672643de50027d4086a4701ee01a1c52311f2120b8bbb74f2b870f9a0e7764eba5
6
+ metadata.gz: 37997d8de598e480912ecc0785fc755138aa90163dcb20981210eff4a935ae85a49a157b8c51189a6a13a6a29a1ff391429e477257d466b0b9d0d19d04d33fcd
7
+ data.tar.gz: 5a81a246030ed8d9fcd7b847c30fb241c2974e203ae09b256a017432cffa549073430a203e5d6231f55527db005237477a4903482c88ddad47e4c2ca894f8def
data/README.md CHANGED
@@ -35,11 +35,22 @@ The Container Gateway plugin requires a Pulp 3 instance to connect to. Related
35
35
 
36
36
  # Database information
37
37
 
38
- SQLite database migrations are completely automated. The plugin checks if the database is up-to-date before each query.
38
+ SQLite and PostgreSQL are supported, with SQLite being the default for development and testing.
39
+ Use PostgreSQL in production for improved performance by adding the following settings:
40
+ ```
41
+ # Example PostgreSQL connection settings, using UNIX socket and ident auth
42
+ :db_connection_string: postgres:///container_gateway
43
+ ```
44
+
45
+ When switching from SQLite to PostgreSQL, if the PostgreSQL database is empty, the SQLite database will be automatically migrated to PostgreSQL.
46
+ For the migration to work, the sqlite_db_path setting must point to the old SQLite database file if the default (no setting definition) was not used.
47
+ The SQLite database file will be deleted after the migration to PostgreSQL is complete.
48
+
49
+ Database migrations are completely automated. The plugin checks if the database is up-to-date at initialization time.
39
50
 
40
51
  # Katello interaction
41
52
 
42
- Auth information is retrieved from the Katello server during smart proxy sync time and cached in the SQLite database.
53
+ Auth information is retrieved from the Katello server during smart proxy sync time and cached in the database.
43
54
 
44
55
  Logging in with a container client will cause the Container Gateway to fetch a token from Katello using the login information.
45
56
 
@@ -7,7 +7,6 @@ module Proxy
7
7
 
8
8
  default_settings :pulp_endpoint => "https://#{`hostname`.strip}",
9
9
  :katello_registry_path => '/v2/',
10
- :sqlite_db_path => '/var/lib/foreman-proxy/smart_proxy_container_gateway.db',
11
10
  :sqlite_timeout => 30_000
12
11
 
13
12
  # Load defaults that copy values from SETTINGS. This is done as
@@ -25,6 +24,27 @@ module Proxy
25
24
  validate :pulp_endpoint, url: true
26
25
 
27
26
  rackup_path File.join(__dir__, 'container_gateway_http_config.ru')
27
+
28
+ load_dependency_injection_wirings do |container_instance, settings|
29
+ container_instance.singleton_dependency :database_impl, (lambda do
30
+ connection_string = settings.fetch(:db_connection_string) do
31
+ unless settings[:sqlite_db_path]
32
+ raise ValueError, 'Missing db_connection_string or sqlite_db_path option'
33
+ end
34
+
35
+ # Legacy setup
36
+ "sqlite://#{settings[:sqlite_db_path]}?timeout=#{settings[:sqlite_timeout]}"
37
+ end
38
+
39
+ Proxy::ContainerGateway::Database.new(connection_string, settings[:sqlite_db_path])
40
+ end)
41
+ container_instance.singleton_dependency :container_gateway_main_impl, (lambda do
42
+ Proxy::ContainerGateway::ContainerGatewayMain.new(
43
+ database: container_instance.get_dependency(:database_impl),
44
+ **settings.slice(:pulp_endpoint, :pulp_client_ssl_ca, :pulp_client_ssl_cert, :pulp_client_ssl_key)
45
+ )
46
+ end)
47
+ end
28
48
  end
29
49
  end
30
50
  end
@@ -6,7 +6,6 @@ require 'sinatra'
6
6
  require 'smart_proxy_container_gateway/container_gateway'
7
7
  require 'smart_proxy_container_gateway/container_gateway_main'
8
8
  require 'smart_proxy_container_gateway/foreman_api'
9
- require 'sqlite3'
10
9
 
11
10
  module Proxy
12
11
  module ContainerGateway
@@ -14,15 +13,23 @@ module Proxy
14
13
  include ::Proxy::Log
15
14
  helpers ::Proxy::Helpers
16
15
  helpers ::Sinatra::Authorization::Helpers
16
+ extend ::Proxy::ContainerGateway::DependencyInjection
17
+
18
+ inject_attr :database_impl, :database
19
+ inject_attr :container_gateway_main_impl, :container_gateway_main
17
20
 
18
21
  get '/v1/_ping/?' do
19
- Proxy::ContainerGateway.ping
22
+ pulp_response = container_gateway_main.ping(translated_headers_for_proxy)
23
+ status pulp_response.code.to_i
24
+ body pulp_response.body
20
25
  end
21
26
 
22
27
  get '/v2/?' do
23
28
  if auth_header.present? && (auth_header.unauthorized_token? || auth_header.valid_user_token?)
24
29
  response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
25
- Proxy::ContainerGateway.ping
30
+ pulp_response = container_gateway_main.ping(translated_headers_for_proxy)
31
+ status pulp_response.code.to_i
32
+ body pulp_response.body
26
33
  else
27
34
  redirect_authorization_headers
28
35
  halt 401, "unauthorized"
@@ -33,22 +40,34 @@ module Proxy
33
40
  repository = params[:splat][0]
34
41
  tag = params[:splat][1]
35
42
  handle_repo_auth(repository, auth_header, request)
36
- redirection_location = Proxy::ContainerGateway.manifests(repository, tag)
37
- redirect to(redirection_location)
43
+ pulp_response = container_gateway_main.manifests(repository, tag, translated_headers_for_proxy)
44
+ if pulp_response.code.to_i >= 400
45
+ status pulp_response.code.to_i
46
+ body pulp_response.body
47
+ else
48
+ redirection_location = pulp_response['location']
49
+ redirect to(redirection_location)
50
+ end
38
51
  end
39
52
 
40
53
  get '/v2/*/blobs/*/?' do
41
54
  repository = params[:splat][0]
42
55
  digest = params[:splat][1]
43
56
  handle_repo_auth(repository, auth_header, request)
44
- redirection_location = Proxy::ContainerGateway.blobs(repository, digest)
45
- redirect to(redirection_location)
57
+ pulp_response = container_gateway_main.blobs(repository, digest, translated_headers_for_proxy)
58
+ if pulp_response.code.to_i >= 400
59
+ status pulp_response.code.to_i
60
+ body pulp_response.body
61
+ else
62
+ redirection_location = pulp_response['location']
63
+ redirect to(redirection_location)
64
+ end
46
65
  end
47
66
 
48
67
  get '/v2/*/tags/list/?' do
49
68
  repository = params[:splat][0]
50
69
  handle_repo_auth(repository, auth_header, request)
51
- pulp_response = Proxy::ContainerGateway.tags(repository, params)
70
+ pulp_response = container_gateway_main.tags(repository, translated_headers_for_proxy, params)
52
71
  # "link"=>["<http://pulpcore-api/v2/container-image-name/tags/list?n=100&last=last-tag-name>; rel=\"next\""],
53
72
  # https://docs.docker.com/registry/spec/api/#pagination-1
54
73
  if pulp_response['link'].nil?
@@ -56,7 +75,8 @@ module Proxy
56
75
  else
57
76
  headers['link'] = pulp_response['link']
58
77
  end
59
- pulp_response.body
78
+ status pulp_response.code.to_i
79
+ body pulp_response.body
60
80
  end
61
81
 
62
82
  get '/v1/search/?' do
@@ -74,19 +94,23 @@ module Proxy
74
94
  end
75
95
  params[:user] = username
76
96
  end
77
- repositories = Proxy::ContainerGateway.v1_search(params)
97
+ repositories = container_gateway_main.v1_search(params)
78
98
 
79
99
  content_type :json
80
- { num_results: repositories.size, query: params[:q], results: repositories }.to_json
100
+ {
101
+ num_results: repositories.size,
102
+ query: params[:q],
103
+ results: repositories.map { |repo_name| { description: '', name: repo_name } }
104
+ }.to_json
81
105
  end
82
106
 
83
107
  get '/v2/_catalog/?' do
84
108
  catalog = []
85
109
  if auth_header.present?
86
110
  if auth_header.unauthorized_token?
87
- catalog = Proxy::ContainerGateway.catalog
111
+ catalog = container_gateway_main.catalog.select_map(::Sequel[:repositories][:name])
88
112
  elsif auth_header.valid_user_token?
89
- catalog = Proxy::ContainerGateway.catalog(auth_header.user)
113
+ catalog = container_gateway_main.catalog(auth_header.user).select_map(::Sequel[:repositories][:name])
90
114
  else
91
115
  redirect_authorization_headers
92
116
  halt 401, "unauthorized"
@@ -131,7 +155,7 @@ module Proxy
131
155
  expires_in = token_response_body.fetch("expires_in", 60)
132
156
  expires_at = token_issue_time + expires_in.seconds
133
157
 
134
- ContainerGateway.insert_token(
158
+ container_gateway_main.insert_token(
135
159
  request.params['account'],
136
160
  token_response_body['token'],
137
161
  expires_at.rfc3339
@@ -141,8 +165,8 @@ module Proxy
141
165
  if repo_response.code.to_i != 200
142
166
  halt repo_response.code.to_i, repo_response.body
143
167
  else
144
- ContainerGateway.update_user_repositories(request.params['account'],
145
- JSON.parse(repo_response.body)['repositories'])
168
+ container_gateway_main.update_user_repositories(request.params['account'],
169
+ JSON.parse(repo_response.body)['repositories'])
146
170
  end
147
171
 
148
172
  # Return the original token response from Katello
@@ -154,13 +178,13 @@ module Proxy
154
178
  do_authorize_any
155
179
 
156
180
  content_type :json
157
- { users: User.map(:name) }.to_json
181
+ { users: database.connection[:users].map(:name) }.to_json
158
182
  end
159
183
 
160
184
  put '/user_repository_mapping/?' do
161
185
  do_authorize_any
162
186
 
163
- ContainerGateway.update_user_repo_mapping(params)
187
+ container_gateway_main.update_user_repo_mapping(params)
164
188
  {}
165
189
  end
166
190
 
@@ -168,22 +192,32 @@ module Proxy
168
192
  do_authorize_any
169
193
 
170
194
  repositories = params['repositories'].nil? ? [] : params['repositories']
171
- ContainerGateway.update_repository_list(repositories)
195
+ container_gateway_main.update_repository_list(repositories)
172
196
  {}
173
197
  end
174
198
 
175
199
  private
176
200
 
201
+ def translated_headers_for_proxy
202
+ current_headers = {}
203
+ env = request.env.select do |key, _value|
204
+ key.match("^HTTP_.*")
205
+ end
206
+ env.each do |header|
207
+ current_headers[header[0].split('_')[1..].join('-')] = header[1]
208
+ end
209
+ current_headers
210
+ end
211
+
177
212
  def handle_repo_auth(repository, auth_header, request)
178
213
  user_token_is_valid = false
179
- # FIXME: Getting unauthenticated token here...
180
214
  if auth_header.present? && auth_header.valid_user_token?
181
215
  user_token_is_valid = true
182
- username = auth_header.user.name
216
+ username = auth_header.user[:name]
183
217
  end
184
218
  username = request.params['account'] if username.nil?
185
219
 
186
- return if Proxy::ContainerGateway.authorized_for_repo?(repository, user_token_is_valid, username)
220
+ return if container_gateway_main.authorized_for_repo?(repository, user_token_is_valid, username)
187
221
 
188
222
  redirect_authorization_headers
189
223
  halt 401, "unauthorized"
@@ -201,6 +235,10 @@ module Proxy
201
235
  end
202
236
 
203
237
  class AuthorizationHeader
238
+ extend ::Proxy::ContainerGateway::DependencyInjection
239
+
240
+ inject_attr :database_impl, :database
241
+ inject_attr :container_gateway_main_impl, :container_gateway_main
204
242
  UNAUTHORIZED_TOKEN = 'unauthorized'.freeze
205
243
 
206
244
  def initialize(value)
@@ -208,11 +246,11 @@ module Proxy
208
246
  end
209
247
 
210
248
  def user
211
- ContainerGateway.token_user(@value.split(' ')[1])
249
+ container_gateway_main.token_user(@value.split(' ')[1])
212
250
  end
213
251
 
214
252
  def valid_user_token?
215
- token_auth? && ContainerGateway.valid_token?(@value.split(' ')[1])
253
+ token_auth? && container_gateway_main.valid_token?(@value.split(' ')[1])
216
254
  end
217
255
 
218
256
  def raw_header
@@ -1,47 +1,62 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
3
  require 'digest'
4
+ require 'smart_proxy_container_gateway/dependency_injection'
4
5
  require 'sequel'
5
6
  module Proxy
6
7
  module ContainerGateway
7
8
  extend ::Proxy::Util
8
9
  extend ::Proxy::Log
9
10
 
10
- class << self
11
- Sequel.extension :migration, :core_extensions
12
- def pulp_registry_request(uri)
11
+ class ContainerGatewayMain
12
+ attr_reader :database
13
+
14
+ def initialize(database:, pulp_endpoint:, pulp_client_ssl_ca:, pulp_client_ssl_cert:, pulp_client_ssl_key:)
15
+ @database = database
16
+ @pulp_endpoint = pulp_endpoint
17
+ @pulp_client_ssl_ca = pulp_client_ssl_ca
18
+ @pulp_client_ssl_cert = OpenSSL::X509::Certificate.new(File.read(pulp_client_ssl_cert))
19
+ @pulp_client_ssl_key = OpenSSL::PKey::RSA.new(
20
+ File.read(pulp_client_ssl_key)
21
+ )
22
+ end
23
+
24
+ def pulp_registry_request(uri, headers)
13
25
  http_client = Net::HTTP.new(uri.host, uri.port)
14
- http_client.ca_file = pulp_ca
15
- http_client.cert = pulp_cert
16
- http_client.key = pulp_key
26
+ http_client.ca_file = @pulp_client_ssl_ca
27
+ http_client.cert = @pulp_client_ssl_cert
28
+ http_client.key = @pulp_client_ssl_key
17
29
  http_client.use_ssl = true
18
30
 
19
31
  http_client.start do |http|
20
32
  request = Net::HTTP::Get.new uri
33
+ headers.each do |key, value|
34
+ request[key] = value
35
+ end
21
36
  http.request request
22
37
  end
23
38
  end
24
39
 
25
- def ping
26
- uri = URI.parse("#{Proxy::ContainerGateway::Plugin.settings.pulp_endpoint}/pulpcore_registry/v2/")
27
- pulp_registry_request(uri).body
40
+ def ping(headers)
41
+ uri = URI.parse("#{@pulp_endpoint}/pulpcore_registry/v2/")
42
+ pulp_registry_request(uri, headers)
28
43
  end
29
44
 
30
- def manifests(repository, tag)
45
+ def manifests(repository, tag, headers)
31
46
  uri = URI.parse(
32
- "#{Proxy::ContainerGateway::Plugin.settings.pulp_endpoint}/pulpcore_registry/v2/#{repository}/manifests/#{tag}"
47
+ "#{@pulp_endpoint}/pulpcore_registry/v2/#{repository}/manifests/#{tag}"
33
48
  )
34
- pulp_registry_request(uri)['location']
49
+ pulp_registry_request(uri, headers)
35
50
  end
36
51
 
37
- def blobs(repository, digest)
52
+ def blobs(repository, digest, headers)
38
53
  uri = URI.parse(
39
- "#{Proxy::ContainerGateway::Plugin.settings.pulp_endpoint}/pulpcore_registry/v2/#{repository}/blobs/#{digest}"
54
+ "#{@pulp_endpoint}/pulpcore_registry/v2/#{repository}/blobs/#{digest}"
40
55
  )
41
- pulp_registry_request(uri)['location']
56
+ pulp_registry_request(uri, headers)
42
57
  end
43
58
 
44
- def tags(repository, params = {})
59
+ def tags(repository, headers, params = {})
45
60
  query = "?"
46
61
  unless params[:n].nil? || params[:n] == ""
47
62
  query = "#{query}n=#{params[:n]}"
@@ -50,51 +65,54 @@ module Proxy
50
65
  query = "#{query}last=#{params[:last]}" unless params[:last].nil? || params[:last] == ""
51
66
 
52
67
  uri = URI.parse(
53
- "#{Proxy::ContainerGateway::Plugin.settings.pulp_endpoint}/pulpcore_registry/v2/#{repository}/tags/list#{query}"
68
+ "#{@pulp_endpoint}/pulpcore_registry/v2/#{repository}/tags/list#{query}"
54
69
  )
55
- pulp_registry_request(uri)
70
+ pulp_registry_request(uri, headers)
56
71
  end
57
72
 
58
73
  def v1_search(params = {})
59
74
  if params[:n].nil? || params[:n] == ""
60
- params[:n] = 25
75
+ limit = 25
61
76
  else
62
- params[:n] = params[:n].to_i
77
+ limit = params[:n].to_i
63
78
  end
79
+ return [] unless limit.positive?
64
80
 
65
- repo_count = 0
66
- repositories = []
67
- user = params[:user].nil? ? nil : User.find(name: params[:user])
68
- Proxy::ContainerGateway.catalog(user).each do |repo_name|
69
- break if repo_count >= params[:n]
81
+ query = params[:q]
82
+ query = nil if query == ''
70
83
 
71
- if params[:q].nil? || params[:q] == "" || repo_name.include?(params[:q])
72
- repo_count += 1
73
- repositories << { name: repo_name }
74
- end
75
- end
76
- repositories
84
+ user = params[:user].nil? ? nil : database.connection[:users][{ name: params[:user] }]
85
+
86
+ repositories = query ? catalog(user).grep(:name, "%#{query}%") : catalog(user)
87
+ repositories.limit(limit).select_map(::Sequel[:repositories][:name])
77
88
  end
78
89
 
79
90
  def catalog(user = nil)
80
91
  if user.nil?
81
92
  unauthenticated_repos
82
93
  else
83
- (unauthenticated_repos + user.repositories_dataset.map(:name)).sort
94
+ database.connection[:repositories].
95
+ left_join(:repositories_users, repository_id: :id).
96
+ left_join(:users, ::Sequel[:users][:id] => :user_id).where(user_id: user[:id]).
97
+ or(Sequel[:repositories][:auth_required] => false).order(::Sequel[:repositories][:name])
84
98
  end
85
99
  end
86
100
 
87
101
  def unauthenticated_repos
88
- Repository.where(auth_required: false).order(:name).map(:name)
102
+ database.connection[:repositories].where(auth_required: false).order(:name)
89
103
  end
90
104
 
91
105
  # Replaces the entire list of repositories
92
106
  def update_repository_list(repo_list)
93
- RepositoryUser.dataset.delete
94
- Repository.dataset.delete
95
- repo_list.each do |repo|
96
- Repository.find_or_create(name: repo['repository'],
97
- auth_required: repo['auth_required'].to_s.downcase == "true")
107
+ # repositories_users cascades on deleting repositories (or users)
108
+ database.connection.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
109
+ repository = database.connection[:repositories]
110
+ repository.delete
111
+
112
+ repository.import(
113
+ %i[name auth_required],
114
+ repo_list.map { |repo| [repo['repository'], repo['auth_required'].to_s.downcase == "true"] }
115
+ )
98
116
  end
99
117
  end
100
118
 
@@ -103,122 +121,95 @@ module Proxy
103
121
  # Get hash map of all users and their repositories
104
122
  # Ex: {"users"=> [{"admin"=>[{"repository"=>"repo", "auth_required"=>"true"}]}]}
105
123
  # Go through list of repositories and add them to the DB
106
- RepositoryUser.dataset.delete
107
- user_repo_maps['users'].each do |user_repo_map|
108
- user_repo_map.each do |user, repos|
109
- next if repos.nil?
110
-
111
- repos.each do |repo|
112
- found_repo = Repository.find(name: repo['repository'],
113
- auth_required: repo['auth_required'].to_s.downcase == "true")
114
- if found_repo.nil?
115
- logger.warn("#{repo['repository']} does not exist in this smart proxy's environments")
116
- elsif found_repo.auth_required
117
- found_repo.add_user(User.find(name: user))
118
- end
124
+ repositories = database.connection[:repositories]
125
+
126
+ entries = user_repo_maps['users'].flat_map do |user_repo_map|
127
+ user_repo_map.filter_map do |username, repos|
128
+ user_repo_names = repos.filter { |repo| repo['auth_required'].to_s.downcase == "true" }.map do |repo|
129
+ repo['repository']
119
130
  end
131
+ user = database.connection[:users][{ name: username }]
132
+ repositories.where(name: user_repo_names, auth_required: true).select(:id).map { |repo| [repo[:id], user[:id]] }
120
133
  end
121
134
  end
135
+ entries.flatten!(1)
136
+
137
+ repositories_users = database.connection[:repositories_users]
138
+ database.connection.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
139
+ repositories_users.delete
140
+ repositories_users.import(%i[repository_id user_id], entries)
141
+ end
122
142
  end
123
143
 
124
144
  # Replaces the user-repo mapping for a single user
125
145
  def update_user_repositories(username, repositories)
126
- user = User.where(name: username).first
127
- user.remove_all_repositories
128
- repositories.each do |repo_name|
129
- found_repo = Repository.find(name: repo_name)
130
- if found_repo.nil?
131
- logger.warn("#{repo_name} does not exist in this smart proxy's environments")
132
- elsif user.repositories_dataset.where(name: repo_name).first.nil? && found_repo.auth_required
133
- user.add_repository(found_repo)
134
- end
146
+ user = database.connection[:users][{ name: username }]
147
+
148
+ user_repositories = database.connection[:repositories_users]
149
+ database.connection.transaction(isolation: :serializable,
150
+ retry_on: [Sequel::SerializationFailure],
151
+ num_retries: 10) do
152
+ user_repositories.where(user_id: user[:id]).delete
153
+
154
+ user_repositories.import(
155
+ %i[repository_id user_id],
156
+ database.connection[:repositories].where(name: repositories, auth_required: true).select(:id).map do |repo|
157
+ [repo[:id], user[:id]]
158
+ end
159
+ )
135
160
  end
136
161
  end
137
162
 
138
163
  def authorized_for_repo?(repo_name, user_token_is_valid, username = nil)
139
- repository = Repository.where(name: repo_name).first
164
+ repository = database.connection[:repositories][{ name: repo_name }]
140
165
 
141
166
  # Repository doesn't exist
142
167
  return false if repository.nil?
143
168
 
144
169
  # Repository doesn't require auth
145
- return true unless repository.auth_required
170
+ return true unless repository[:auth_required]
146
171
 
147
- if username && user_token_is_valid && repository.auth_required
172
+ if username && user_token_is_valid
148
173
  # User is logged in and has access to the repository
149
- user = User.find(name: username)
150
- return !user.repositories_dataset.where(name: repo_name).first.nil?
174
+ return !database.connection[:repositories_users].where(
175
+ repository_id: repository[:id], user_id: database.connection[:users].first(name: username)[:id]
176
+ ).empty?
151
177
  end
152
178
 
153
179
  false
154
180
  end
155
181
 
156
182
  def token_user(token)
157
- User[AuthenticationToken.find(token_checksum: Digest::SHA256.hexdigest(token)).user_id]
183
+ database.connection[:users][{
184
+ id: database.connection[:authentication_tokens].where(token_checksum: checksum(token)).select(:user_id)
185
+ }]
158
186
  end
159
187
 
160
188
  def valid_token?(token)
161
- AuthenticationToken.where(token_checksum: Digest::SHA256.hexdigest(token)).where do
189
+ !database.connection[:authentication_tokens].where(token_checksum: checksum(token)).where do
162
190
  expire_at > Sequel::CURRENT_TIMESTAMP
163
- end.count.positive?
191
+ end.empty?
164
192
  end
165
193
 
166
194
  def insert_token(username, token, expire_at_string, clear_expired_tokens: true)
167
195
  checksum = Digest::SHA256.hexdigest(token)
168
- user = User.find_or_create(name: username)
169
-
170
- AuthenticationToken.where(:token_checksum => checksum).delete
171
- AuthenticationToken.create(token_checksum: checksum, expire_at: expire_at_string.to_s, user_id: user.id)
172
- AuthenticationToken.where { expire_at < Sequel::CURRENT_TIMESTAMP }.delete if clear_expired_tokens
173
- end
174
-
175
- def initialize_db
176
- file_path = Proxy::ContainerGateway::Plugin.settings.sqlite_db_path
177
- sqlite_timeout = Proxy::ContainerGateway::Plugin.settings.sqlite_timeout
178
- conn = Sequel.connect("sqlite://#{file_path}", timeout: sqlite_timeout)
179
- container_gateway_path = $LOAD_PATH.detect { |path| path.include? 'smart_proxy_container_gateway' }
180
- begin
181
- Sequel::Migrator.check_current(conn, "#{container_gateway_path}/smart_proxy_container_gateway/sequel_migrations")
182
- rescue Sequel::Migrator::NotCurrentError
183
- migrate_db(conn, container_gateway_path)
184
- end
185
- conn
186
- end
196
+ user = Sequel::Model(database.connection[:users]).find_or_create(name: username)
187
197
 
188
- private
189
-
190
- def migrate_db(db_connection, container_gateway_path)
191
- Sequel::Migrator.run(db_connection, "#{container_gateway_path}/smart_proxy_container_gateway/sequel_migrations")
192
- end
198
+ database.connection.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
199
+ database.connection[:authentication_tokens].where(:token_checksum => checksum).delete
200
+ Sequel::Model(database.connection[:authentication_tokens]).
201
+ create(token_checksum: checksum, expire_at: expire_at_string.to_s, user_id: user.id)
202
+ return unless clear_expired_tokens
193
203
 
194
- def pulp_ca
195
- Proxy::ContainerGateway::Plugin.settings.pulp_client_ssl_ca
204
+ database.connection[:authentication_tokens].where { expire_at < Sequel::CURRENT_TIMESTAMP }.delete
205
+ end
196
206
  end
197
207
 
198
- def pulp_cert
199
- OpenSSL::X509::Certificate.new(File.read(Proxy::ContainerGateway::Plugin.settings.pulp_client_ssl_cert))
200
- end
208
+ private
201
209
 
202
- def pulp_key
203
- OpenSSL::PKey::RSA.new(
204
- File.read(Proxy::ContainerGateway::Plugin.settings.pulp_client_ssl_key)
205
- )
210
+ def checksum(token)
211
+ Digest::SHA256.hexdigest(token)
206
212
  end
207
213
  end
208
-
209
- class Repository < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:repositories])
210
- many_to_many :users
211
- end
212
-
213
- class User < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:users])
214
- many_to_many :repositories
215
- one_to_many :authentication_tokens
216
- end
217
-
218
- class RepositoryUser < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:repositories_users]); end
219
-
220
- class AuthenticationToken < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:authentication_tokens])
221
- many_to_one :users
222
- end
223
214
  end
224
215
  end
@@ -0,0 +1,46 @@
1
+ require 'sequel'
2
+ module Proxy
3
+ module ContainerGateway
4
+ class Database
5
+ attr_reader :connection
6
+
7
+ def initialize(connection_string, prior_sqlite_db_path = nil)
8
+ @connection = Sequel.connect(connection_string)
9
+ if connection_string.start_with?('sqlite://')
10
+ @connection.run("PRAGMA foreign_keys = ON;")
11
+ @connection.run("PRAGMA journal_mode = wal;")
12
+ elsif prior_sqlite_db_path && File.exist?(prior_sqlite_db_path) &&
13
+ (!@connection.table_exists?(:repositories) || @connection[:repositories].count.zero?)
14
+ migrate_to_postgres(Sequel.sqlite(prior_sqlite_db_path), @connection)
15
+ File.delete(prior_sqlite_db_path)
16
+ end
17
+ migrate
18
+ end
19
+
20
+ private
21
+
22
+ def migrate
23
+ Sequel.extension :migration, :core_extensions
24
+ migration_path = File.join(__dir__, 'sequel_migrations')
25
+ begin
26
+ Sequel::Migrator.check_current(@connection, migration_path)
27
+ rescue Sequel::Migrator::NotCurrentError
28
+ Sequel::Migrator.run(@connection, migration_path)
29
+ end
30
+ end
31
+
32
+ def migrate_to_postgres(sqlite_db, postgres_db)
33
+ migrate
34
+ sqlite_db.transaction do
35
+ sqlite_db.tables.each do |table|
36
+ next if table == :schema_info
37
+
38
+ sqlite_db[table].each do |row|
39
+ postgres_db[table.to_sym].insert(row)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,10 @@
1
+ module Proxy
2
+ module ContainerGateway
3
+ module DependencyInjection
4
+ include Proxy::DependencyInjection::Accessors
5
+ def container_instance
6
+ @container_instance ||= ::Proxy::Plugins.instance.find { |p| p[:name] == :container_gateway }[:di_container]
7
+ end
8
+ end
9
+ end
10
+ end
@@ -25,8 +25,8 @@ Sequel.migration do
25
25
  end
26
26
 
27
27
  # Populate the new user_id foreign key for all authentication_tokens
28
- from(:authentication_tokens).insert([:user_id],
29
- from(:users).select(:id).where(name: self[:authentication_tokens][:username]))
28
+ from(:authentication_tokens).
29
+ update(user_id: from(:users).select(:id).where(name: Sequel[:authentication_tokens][:username]))
30
30
 
31
31
  alter_table(:authentication_tokens) do
32
32
  drop_column :username
@@ -0,0 +1,28 @@
1
+ Sequel.migration do
2
+ up do
3
+ # SQLite does not support dropping columns until version 3.35, which is not in the EL8 ecosystem as of March, 2024.
4
+ create_table(:repositories_users2) do
5
+ foreign_key :repository_id, :repositories, on_delete: :cascade
6
+ foreign_key :user_id, :users, on_delete: :cascade
7
+ primary_key %i[repository_id user_id]
8
+ index %i[repository_id user_id]
9
+ end
10
+ run "INSERT INTO repositories_users2(repository_id, user_id) SELECT repository_id, user_id from repositories_users"
11
+
12
+ drop_table(:repositories_users)
13
+ rename_table(:repositories_users2, :repositories_users)
14
+ end
15
+
16
+ down do
17
+ create_table(:repositories_users2) do
18
+ foreign_key :repository_id, :repositories
19
+ foreign_key :user_id, :users
20
+ primary_key %i[repository_id user_id]
21
+ index %i[repository_id user_id]
22
+ end
23
+ run "INSERT INTO repositories_users2(repository_id, user_id) SELECT repository_id, user_id from repositories_users"
24
+
25
+ drop_table(:repositories_users)
26
+ rename_table(:repositories_users2, :repositories_users)
27
+ end
28
+ end
@@ -1,5 +1,5 @@
1
1
  module Proxy
2
2
  module ContainerGateway
3
- VERSION = '1.2.0'.freeze
3
+ VERSION = '3.0.0'.freeze
4
4
  end
5
5
  end
@@ -1,2 +1,4 @@
1
1
  require 'smart_proxy_container_gateway/version'
2
+ require 'smart_proxy_container_gateway/database'
2
3
  require 'smart_proxy_container_gateway/container_gateway'
4
+ require 'smart_proxy_container_gateway/container_gateway_main'
@@ -1,10 +1,16 @@
1
1
  ---
2
2
  :enabled: true
3
- :pulp_endpoint: 'https://your_pulp_3_server_here.com'
4
- :pulp_client_ssl_ca: 'CA Cert for authenticating with Pulp'
5
- :pulp_client_ssl_cert: 'X509 certificate for authenticating with Pulp'
6
- :pulp_client_ssl_key: 'RSA private key for the Pulp certificate'
7
- :katello_registry_path: 'Katello container registry suffix, e.g., /v2/'
8
- :sqlite_db_path: '/var/lib/foreman-proxy/smart_proxy_container_gateway.db'
3
+
4
+ #:pulp_endpoint: 'https://pulp3.example.com'
5
+ #:pulp_client_ssl_ca: '/path/to/ca.pem'
6
+ #:pulp_client_ssl_cert: '/path/to/cert.pem'
7
+ #:pulp_client_ssl_key: '/path/to/key.pem'
8
+
9
+ #:katello_registry_path: '/v2/'
10
+
11
+ #:db_connection_string: postgresql:///container_gateway
12
+
13
+ # Legacy options
14
+ #:sqlite_db_path: '/var/lib/foreman-proxy/smart_proxy_container_gateway.db'
9
15
  # Database busy timeout in milliseconds
10
- :sqlite_timeout: 30000
16
+ #:sqlite_timeout: 30000
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_proxy_container_gateway
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Ballou
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-01-09 00:00:00.000000000 Z
11
+ date: 2024-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: sequel
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -68,10 +82,13 @@ files:
68
82
  - lib/smart_proxy_container_gateway/container_gateway_api.rb
69
83
  - lib/smart_proxy_container_gateway/container_gateway_http_config.ru
70
84
  - lib/smart_proxy_container_gateway/container_gateway_main.rb
85
+ - lib/smart_proxy_container_gateway/database.rb
86
+ - lib/smart_proxy_container_gateway/dependency_injection.rb
71
87
  - lib/smart_proxy_container_gateway/foreman_api.rb
72
88
  - lib/smart_proxy_container_gateway/sequel_migrations/001_initial.rb
73
89
  - lib/smart_proxy_container_gateway/sequel_migrations/002_auth_tokens.rb
74
90
  - lib/smart_proxy_container_gateway/sequel_migrations/003_authorization_reorg.rb
91
+ - lib/smart_proxy_container_gateway/sequel_migrations/004_users_repositories_cascade_delete.rb
75
92
  - lib/smart_proxy_container_gateway/version.rb
76
93
  - settings.d/container_gateway.yml.example
77
94
  homepage: https://github.com/Katello/smart_proxy_container_gateway
@@ -93,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
110
  - !ruby/object:Gem::Version
94
111
  version: '0'
95
112
  requirements: []
96
- rubygems_version: 3.4.10
113
+ rubygems_version: 3.4.21
97
114
  signing_key:
98
115
  specification_version: 4
99
116
  summary: Pulp 3 container registry support for Foreman/Katello Smart-Proxy