smart_proxy_container_gateway 1.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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