smart_proxy_container_gateway 1.2.0 → 2.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 +4 -4
- data/README.md +14 -2
- data/lib/smart_proxy_container_gateway/container_gateway.rb +16 -0
- data/lib/smart_proxy_container_gateway/container_gateway_api.rb +31 -21
- data/lib/smart_proxy_container_gateway/container_gateway_main.rb +93 -107
- data/lib/smart_proxy_container_gateway/database.rb +53 -0
- data/lib/smart_proxy_container_gateway/dependency_injection.rb +10 -0
- data/lib/smart_proxy_container_gateway/sequel_migrations/003_authorization_reorg.rb +2 -2
- data/lib/smart_proxy_container_gateway/sequel_migrations/004_users_repositories_cascade_delete.rb +28 -0
- data/lib/smart_proxy_container_gateway/version.rb +1 -1
- data/lib/smart_proxy_container_gateway.rb +2 -0
- metadata +20 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fee65676d4362e1328671832a7bf3308b193ab1265ea5626eb52d5300ab7d35a
|
|
4
|
+
data.tar.gz: fe57f17f732079b2ac27ce70cc7c04ef3ed141283cba58db75676a36dfb4b678
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6f0716504c021e799a8a7d31390c9d203c248530073211cf24d7659439b1220647306033c4e60bf98b9425d427b822a8f98b6962f3dab788883bc17b3f5e9a8
|
|
7
|
+
data.tar.gz: cfe03eaf3ce5e9ffa23dd355849e81abf249cede80a0f98ac12cf056fc7ec20de81195794e820c44adc9eea9000f176c837b70756670244334b1bbca4690dc54
|
data/README.md
CHANGED
|
@@ -35,11 +35,23 @@ The Container Gateway plugin requires a Pulp 3 instance to connect to. Related
|
|
|
35
35
|
|
|
36
36
|
# Database information
|
|
37
37
|
|
|
38
|
-
SQLite
|
|
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 (default port is 5432)
|
|
42
|
+
:database_backend: postgresql
|
|
43
|
+
:postgresql_connection_string: postgres://foreman-proxy:changeme@localhost:5432/container_gateway
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
When switching from SQLite to PostgreSQL, if the PostgreSQL database is empty, the SQLite database will be automatically migrated to PostgreSQL.
|
|
47
|
+
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.
|
|
48
|
+
The SQLite database file will be deleted after the migration to PostgreSQL is complete.
|
|
49
|
+
|
|
50
|
+
Database migrations are completely automated. The plugin checks if the database is up-to-date at initialization time.
|
|
39
51
|
|
|
40
52
|
# Katello interaction
|
|
41
53
|
|
|
42
|
-
Auth information is retrieved from the Katello server during smart proxy sync time and cached in the
|
|
54
|
+
Auth information is retrieved from the Katello server during smart proxy sync time and cached in the database.
|
|
43
55
|
|
|
44
56
|
Logging in with a container client will cause the Container Gateway to fetch a token from Katello using the login information.
|
|
45
57
|
|
|
@@ -7,6 +7,7 @@ module Proxy
|
|
|
7
7
|
|
|
8
8
|
default_settings :pulp_endpoint => "https://#{`hostname`.strip}",
|
|
9
9
|
:katello_registry_path => '/v2/',
|
|
10
|
+
:database_backend => 'sqlite',
|
|
10
11
|
:sqlite_db_path => '/var/lib/foreman-proxy/smart_proxy_container_gateway.db',
|
|
11
12
|
:sqlite_timeout => 30_000
|
|
12
13
|
|
|
@@ -25,6 +26,21 @@ module Proxy
|
|
|
25
26
|
validate :pulp_endpoint, url: true
|
|
26
27
|
|
|
27
28
|
rackup_path File.join(__dir__, 'container_gateway_http_config.ru')
|
|
29
|
+
|
|
30
|
+
load_dependency_injection_wirings do |container_instance, settings|
|
|
31
|
+
container_instance.singleton_dependency :database_impl, (lambda do
|
|
32
|
+
Proxy::ContainerGateway::Database.new(
|
|
33
|
+
database_backend: settings[:database_backend], sqlite_db_path: settings[:sqlite_db_path],
|
|
34
|
+
sqlite_timeout: settings[:sqlite_timeout], postgresql_connection_string: settings[:postgresql_connection_string]
|
|
35
|
+
)
|
|
36
|
+
end)
|
|
37
|
+
container_instance.singleton_dependency :container_gateway_main_impl, (lambda do
|
|
38
|
+
Proxy::ContainerGateway::ContainerGatewayMain.new(
|
|
39
|
+
database: container_instance.get_dependency(:database_impl),
|
|
40
|
+
**settings.slice(:pulp_endpoint, :pulp_client_ssl_ca, :pulp_client_ssl_cert, :pulp_client_ssl_key)
|
|
41
|
+
)
|
|
42
|
+
end)
|
|
43
|
+
end
|
|
28
44
|
end
|
|
29
45
|
end
|
|
30
46
|
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,19 @@ 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
|
-
|
|
22
|
+
container_gateway_main.ping
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
get '/v2/?' do
|
|
23
26
|
if auth_header.present? && (auth_header.unauthorized_token? || auth_header.valid_user_token?)
|
|
24
27
|
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
|
|
25
|
-
|
|
28
|
+
container_gateway_main.ping
|
|
26
29
|
else
|
|
27
30
|
redirect_authorization_headers
|
|
28
31
|
halt 401, "unauthorized"
|
|
@@ -33,7 +36,7 @@ module Proxy
|
|
|
33
36
|
repository = params[:splat][0]
|
|
34
37
|
tag = params[:splat][1]
|
|
35
38
|
handle_repo_auth(repository, auth_header, request)
|
|
36
|
-
redirection_location =
|
|
39
|
+
redirection_location = container_gateway_main.manifests(repository, tag)
|
|
37
40
|
redirect to(redirection_location)
|
|
38
41
|
end
|
|
39
42
|
|
|
@@ -41,14 +44,14 @@ module Proxy
|
|
|
41
44
|
repository = params[:splat][0]
|
|
42
45
|
digest = params[:splat][1]
|
|
43
46
|
handle_repo_auth(repository, auth_header, request)
|
|
44
|
-
redirection_location =
|
|
47
|
+
redirection_location = container_gateway_main.blobs(repository, digest)
|
|
45
48
|
redirect to(redirection_location)
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
get '/v2/*/tags/list/?' do
|
|
49
52
|
repository = params[:splat][0]
|
|
50
53
|
handle_repo_auth(repository, auth_header, request)
|
|
51
|
-
pulp_response =
|
|
54
|
+
pulp_response = container_gateway_main.tags(repository, params)
|
|
52
55
|
# "link"=>["<http://pulpcore-api/v2/container-image-name/tags/list?n=100&last=last-tag-name>; rel=\"next\""],
|
|
53
56
|
# https://docs.docker.com/registry/spec/api/#pagination-1
|
|
54
57
|
if pulp_response['link'].nil?
|
|
@@ -74,19 +77,23 @@ module Proxy
|
|
|
74
77
|
end
|
|
75
78
|
params[:user] = username
|
|
76
79
|
end
|
|
77
|
-
repositories =
|
|
80
|
+
repositories = container_gateway_main.v1_search(params)
|
|
78
81
|
|
|
79
82
|
content_type :json
|
|
80
|
-
{
|
|
83
|
+
{
|
|
84
|
+
num_results: repositories.size,
|
|
85
|
+
query: params[:q],
|
|
86
|
+
results: repositories.map { |repo_name| { description: '', name: repo_name } }
|
|
87
|
+
}.to_json
|
|
81
88
|
end
|
|
82
89
|
|
|
83
90
|
get '/v2/_catalog/?' do
|
|
84
91
|
catalog = []
|
|
85
92
|
if auth_header.present?
|
|
86
93
|
if auth_header.unauthorized_token?
|
|
87
|
-
catalog =
|
|
94
|
+
catalog = container_gateway_main.catalog.select_map(::Sequel[:repositories][:name])
|
|
88
95
|
elsif auth_header.valid_user_token?
|
|
89
|
-
catalog =
|
|
96
|
+
catalog = container_gateway_main.catalog(auth_header.user).select_map(::Sequel[:repositories][:name])
|
|
90
97
|
else
|
|
91
98
|
redirect_authorization_headers
|
|
92
99
|
halt 401, "unauthorized"
|
|
@@ -131,7 +138,7 @@ module Proxy
|
|
|
131
138
|
expires_in = token_response_body.fetch("expires_in", 60)
|
|
132
139
|
expires_at = token_issue_time + expires_in.seconds
|
|
133
140
|
|
|
134
|
-
|
|
141
|
+
container_gateway_main.insert_token(
|
|
135
142
|
request.params['account'],
|
|
136
143
|
token_response_body['token'],
|
|
137
144
|
expires_at.rfc3339
|
|
@@ -141,8 +148,8 @@ module Proxy
|
|
|
141
148
|
if repo_response.code.to_i != 200
|
|
142
149
|
halt repo_response.code.to_i, repo_response.body
|
|
143
150
|
else
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
container_gateway_main.update_user_repositories(request.params['account'],
|
|
152
|
+
JSON.parse(repo_response.body)['repositories'])
|
|
146
153
|
end
|
|
147
154
|
|
|
148
155
|
# Return the original token response from Katello
|
|
@@ -154,13 +161,13 @@ module Proxy
|
|
|
154
161
|
do_authorize_any
|
|
155
162
|
|
|
156
163
|
content_type :json
|
|
157
|
-
{ users:
|
|
164
|
+
{ users: database.connection[:users].map(:name) }.to_json
|
|
158
165
|
end
|
|
159
166
|
|
|
160
167
|
put '/user_repository_mapping/?' do
|
|
161
168
|
do_authorize_any
|
|
162
169
|
|
|
163
|
-
|
|
170
|
+
container_gateway_main.update_user_repo_mapping(params)
|
|
164
171
|
{}
|
|
165
172
|
end
|
|
166
173
|
|
|
@@ -168,7 +175,7 @@ module Proxy
|
|
|
168
175
|
do_authorize_any
|
|
169
176
|
|
|
170
177
|
repositories = params['repositories'].nil? ? [] : params['repositories']
|
|
171
|
-
|
|
178
|
+
container_gateway_main.update_repository_list(repositories)
|
|
172
179
|
{}
|
|
173
180
|
end
|
|
174
181
|
|
|
@@ -176,14 +183,13 @@ module Proxy
|
|
|
176
183
|
|
|
177
184
|
def handle_repo_auth(repository, auth_header, request)
|
|
178
185
|
user_token_is_valid = false
|
|
179
|
-
# FIXME: Getting unauthenticated token here...
|
|
180
186
|
if auth_header.present? && auth_header.valid_user_token?
|
|
181
187
|
user_token_is_valid = true
|
|
182
|
-
username = auth_header.user
|
|
188
|
+
username = auth_header.user[:name]
|
|
183
189
|
end
|
|
184
190
|
username = request.params['account'] if username.nil?
|
|
185
191
|
|
|
186
|
-
return if
|
|
192
|
+
return if container_gateway_main.authorized_for_repo?(repository, user_token_is_valid, username)
|
|
187
193
|
|
|
188
194
|
redirect_authorization_headers
|
|
189
195
|
halt 401, "unauthorized"
|
|
@@ -201,6 +207,10 @@ module Proxy
|
|
|
201
207
|
end
|
|
202
208
|
|
|
203
209
|
class AuthorizationHeader
|
|
210
|
+
extend ::Proxy::ContainerGateway::DependencyInjection
|
|
211
|
+
|
|
212
|
+
inject_attr :database_impl, :database
|
|
213
|
+
inject_attr :container_gateway_main_impl, :container_gateway_main
|
|
204
214
|
UNAUTHORIZED_TOKEN = 'unauthorized'.freeze
|
|
205
215
|
|
|
206
216
|
def initialize(value)
|
|
@@ -208,11 +218,11 @@ module Proxy
|
|
|
208
218
|
end
|
|
209
219
|
|
|
210
220
|
def user
|
|
211
|
-
|
|
221
|
+
container_gateway_main.token_user(@value.split(' ')[1])
|
|
212
222
|
end
|
|
213
223
|
|
|
214
224
|
def valid_user_token?
|
|
215
|
-
token_auth? &&
|
|
225
|
+
token_auth? && container_gateway_main.valid_token?(@value.split(' ')[1])
|
|
216
226
|
end
|
|
217
227
|
|
|
218
228
|
def raw_header
|
|
@@ -1,19 +1,31 @@
|
|
|
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
|
|
11
|
-
|
|
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
|
+
|
|
12
24
|
def pulp_registry_request(uri)
|
|
13
25
|
http_client = Net::HTTP.new(uri.host, uri.port)
|
|
14
|
-
http_client.ca_file =
|
|
15
|
-
http_client.cert =
|
|
16
|
-
http_client.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|
|
|
@@ -23,20 +35,20 @@ module Proxy
|
|
|
23
35
|
end
|
|
24
36
|
|
|
25
37
|
def ping
|
|
26
|
-
uri = URI.parse("#{
|
|
38
|
+
uri = URI.parse("#{@pulp_endpoint}/pulpcore_registry/v2/")
|
|
27
39
|
pulp_registry_request(uri).body
|
|
28
40
|
end
|
|
29
41
|
|
|
30
42
|
def manifests(repository, tag)
|
|
31
43
|
uri = URI.parse(
|
|
32
|
-
"#{
|
|
44
|
+
"#{@pulp_endpoint}/pulpcore_registry/v2/#{repository}/manifests/#{tag}"
|
|
33
45
|
)
|
|
34
46
|
pulp_registry_request(uri)['location']
|
|
35
47
|
end
|
|
36
48
|
|
|
37
49
|
def blobs(repository, digest)
|
|
38
50
|
uri = URI.parse(
|
|
39
|
-
"#{
|
|
51
|
+
"#{@pulp_endpoint}/pulpcore_registry/v2/#{repository}/blobs/#{digest}"
|
|
40
52
|
)
|
|
41
53
|
pulp_registry_request(uri)['location']
|
|
42
54
|
end
|
|
@@ -50,51 +62,54 @@ module Proxy
|
|
|
50
62
|
query = "#{query}last=#{params[:last]}" unless params[:last].nil? || params[:last] == ""
|
|
51
63
|
|
|
52
64
|
uri = URI.parse(
|
|
53
|
-
"#{
|
|
65
|
+
"#{@pulp_endpoint}/pulpcore_registry/v2/#{repository}/tags/list#{query}"
|
|
54
66
|
)
|
|
55
67
|
pulp_registry_request(uri)
|
|
56
68
|
end
|
|
57
69
|
|
|
58
70
|
def v1_search(params = {})
|
|
59
71
|
if params[:n].nil? || params[:n] == ""
|
|
60
|
-
|
|
72
|
+
limit = 25
|
|
61
73
|
else
|
|
62
|
-
|
|
74
|
+
limit = params[:n].to_i
|
|
63
75
|
end
|
|
76
|
+
return [] unless limit.positive?
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
|
|
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]
|
|
78
|
+
query = params[:q]
|
|
79
|
+
query = nil if query == ''
|
|
70
80
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
76
|
-
repositories
|
|
81
|
+
user = params[:user].nil? ? nil : database.connection[:users][{ name: params[:user] }]
|
|
82
|
+
|
|
83
|
+
repositories = query ? catalog(user).grep(:name, "%#{query}%") : catalog(user)
|
|
84
|
+
repositories.limit(limit).select_map(::Sequel[:repositories][:name])
|
|
77
85
|
end
|
|
78
86
|
|
|
79
87
|
def catalog(user = nil)
|
|
80
88
|
if user.nil?
|
|
81
89
|
unauthenticated_repos
|
|
82
90
|
else
|
|
83
|
-
|
|
91
|
+
database.connection[:repositories].
|
|
92
|
+
left_join(:repositories_users, repository_id: :id).
|
|
93
|
+
left_join(:users, ::Sequel[:users][:id] => :user_id).where(user_id: user[:id]).
|
|
94
|
+
or(Sequel[:repositories][:auth_required] => false).order(::Sequel[:repositories][:name])
|
|
84
95
|
end
|
|
85
96
|
end
|
|
86
97
|
|
|
87
98
|
def unauthenticated_repos
|
|
88
|
-
|
|
99
|
+
database.connection[:repositories].where(auth_required: false).order(:name)
|
|
89
100
|
end
|
|
90
101
|
|
|
91
102
|
# Replaces the entire list of repositories
|
|
92
103
|
def update_repository_list(repo_list)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
# repositories_users cascades on deleting repositories (or users)
|
|
105
|
+
database.connection.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
|
|
106
|
+
repository = database.connection[:repositories]
|
|
107
|
+
repository.delete
|
|
108
|
+
|
|
109
|
+
repository.import(
|
|
110
|
+
%i[name auth_required],
|
|
111
|
+
repo_list.map { |repo| [repo['repository'], repo['auth_required'].to_s.downcase == "true"] }
|
|
112
|
+
)
|
|
98
113
|
end
|
|
99
114
|
end
|
|
100
115
|
|
|
@@ -103,122 +118,93 @@ module Proxy
|
|
|
103
118
|
# Get hash map of all users and their repositories
|
|
104
119
|
# Ex: {"users"=> [{"admin"=>[{"repository"=>"repo", "auth_required"=>"true"}]}]}
|
|
105
120
|
# Go through list of repositories and add them to the DB
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
121
|
+
repositories = database.connection[:repositories]
|
|
122
|
+
|
|
123
|
+
entries = user_repo_maps['users'].flat_map do |user_repo_map|
|
|
124
|
+
user_repo_map.filter_map do |username, repos|
|
|
125
|
+
user_repo_names = repos.filter { |repo| repo['auth_required'].to_s.downcase == "true" }.map do |repo|
|
|
126
|
+
repo['repository']
|
|
119
127
|
end
|
|
128
|
+
user = database.connection[:users][{ name: username }]
|
|
129
|
+
repositories.where(name: user_repo_names, auth_required: true).select(:id).map { |repo| [repo[:id], user[:id]] }
|
|
120
130
|
end
|
|
121
131
|
end
|
|
132
|
+
entries.flatten!(1)
|
|
133
|
+
|
|
134
|
+
repositories_users = database.connection[:repositories_users]
|
|
135
|
+
database.connection.transaction(isolation: :serializable, retry_on: [Sequel::SerializationFailure]) do
|
|
136
|
+
repositories_users.delete
|
|
137
|
+
repositories_users.import(%i[repository_id user_id], entries)
|
|
138
|
+
end
|
|
122
139
|
end
|
|
123
140
|
|
|
124
141
|
# Replaces the user-repo mapping for a single user
|
|
125
142
|
def update_user_repositories(username, repositories)
|
|
126
|
-
user =
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
user = database.connection[:users][{ name: username }]
|
|
144
|
+
|
|
145
|
+
user_repositories = database.connection[:repositories_users]
|
|
146
|
+
database.connection.transaction(isolation: :serializable,
|
|
147
|
+
retry_on: [Sequel::SerializationFailure],
|
|
148
|
+
num_retries: 10) do
|
|
149
|
+
user_repositories.where(user_id: user[:id]).delete
|
|
150
|
+
|
|
151
|
+
user_repositories.import(
|
|
152
|
+
%i[repository_id user_id],
|
|
153
|
+
database.connection[:repositories].where(name: repositories, auth_required: true).select(:id).map do |repo|
|
|
154
|
+
[repo[:id], user[:id]]
|
|
155
|
+
end
|
|
156
|
+
)
|
|
135
157
|
end
|
|
136
158
|
end
|
|
137
159
|
|
|
138
160
|
def authorized_for_repo?(repo_name, user_token_is_valid, username = nil)
|
|
139
|
-
repository =
|
|
161
|
+
repository = database.connection[:repositories][{ name: repo_name }]
|
|
140
162
|
|
|
141
163
|
# Repository doesn't exist
|
|
142
164
|
return false if repository.nil?
|
|
143
165
|
|
|
144
166
|
# Repository doesn't require auth
|
|
145
|
-
return true unless repository
|
|
167
|
+
return true unless repository[:auth_required]
|
|
146
168
|
|
|
147
|
-
if username && user_token_is_valid
|
|
169
|
+
if username && user_token_is_valid
|
|
148
170
|
# User is logged in and has access to the repository
|
|
149
|
-
|
|
150
|
-
|
|
171
|
+
return !database.connection[:repositories_users].where(
|
|
172
|
+
repository_id: repository[:id], user_id: database.connection[:users].first(name: username)[:id]
|
|
173
|
+
).empty?
|
|
151
174
|
end
|
|
152
175
|
|
|
153
176
|
false
|
|
154
177
|
end
|
|
155
178
|
|
|
156
179
|
def token_user(token)
|
|
157
|
-
|
|
180
|
+
database.connection[:users][{
|
|
181
|
+
id: database.connection[:authentication_tokens].where(token_checksum: checksum(token)).select(:user_id)
|
|
182
|
+
}]
|
|
158
183
|
end
|
|
159
184
|
|
|
160
185
|
def valid_token?(token)
|
|
161
|
-
|
|
186
|
+
!database.connection[:authentication_tokens].where(token_checksum: checksum(token)).where do
|
|
162
187
|
expire_at > Sequel::CURRENT_TIMESTAMP
|
|
163
|
-
end.
|
|
188
|
+
end.empty?
|
|
164
189
|
end
|
|
165
190
|
|
|
166
191
|
def insert_token(username, token, expire_at_string, clear_expired_tokens: true)
|
|
167
192
|
checksum = Digest::SHA256.hexdigest(token)
|
|
168
|
-
user =
|
|
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
|
|
193
|
+
user = Sequel::Model(database.connection[:users]).find_or_create(name: username)
|
|
187
194
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
end
|
|
195
|
+
database.connection[:authentication_tokens].where(:token_checksum => checksum).delete
|
|
196
|
+
Sequel::Model(database.connection[:authentication_tokens]).
|
|
197
|
+
create(token_checksum: checksum, expire_at: expire_at_string.to_s, user_id: user.id)
|
|
198
|
+
return unless clear_expired_tokens
|
|
193
199
|
|
|
194
|
-
|
|
195
|
-
Proxy::ContainerGateway::Plugin.settings.pulp_client_ssl_ca
|
|
200
|
+
database.connection[:authentication_tokens].where { expire_at < Sequel::CURRENT_TIMESTAMP }.delete
|
|
196
201
|
end
|
|
197
202
|
|
|
198
|
-
|
|
199
|
-
OpenSSL::X509::Certificate.new(File.read(Proxy::ContainerGateway::Plugin.settings.pulp_client_ssl_cert))
|
|
200
|
-
end
|
|
203
|
+
private
|
|
201
204
|
|
|
202
|
-
def
|
|
203
|
-
|
|
204
|
-
File.read(Proxy::ContainerGateway::Plugin.settings.pulp_client_ssl_key)
|
|
205
|
-
)
|
|
205
|
+
def checksum(token)
|
|
206
|
+
Digest::SHA256.hexdigest(token)
|
|
206
207
|
end
|
|
207
208
|
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
209
|
end
|
|
224
210
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
require 'sequel'
|
|
2
|
+
module Proxy
|
|
3
|
+
module ContainerGateway
|
|
4
|
+
class Database
|
|
5
|
+
attr_reader :connection
|
|
6
|
+
|
|
7
|
+
def initialize(options = {})
|
|
8
|
+
if options[:database_backend] == 'sqlite'
|
|
9
|
+
@connection = Sequel.connect("sqlite://#{options[:sqlite_db_path]}", timeout: options[:sqlite_timeout])
|
|
10
|
+
@connection.run("PRAGMA foreign_keys = ON;")
|
|
11
|
+
@connection.run("PRAGMA journal_mode = wal;")
|
|
12
|
+
else
|
|
13
|
+
unless options[:postgresql_connection_string]
|
|
14
|
+
raise ArgumentError, 'PostgreSQL connection string is required'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@connection = Sequel.connect(options[:postgresql_connection_string])
|
|
18
|
+
if File.exist?(options[:sqlite_db_path]) &&
|
|
19
|
+
(!@connection.table_exists?(:repositories) || @connection[:repositories].count.zero?)
|
|
20
|
+
migrate_to_postgres(Sequel.sqlite(options[:sqlite_db_path]), @connection)
|
|
21
|
+
File.delete(options[:sqlite_db_path])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
migrate
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def migrate
|
|
30
|
+
Sequel.extension :migration, :core_extensions
|
|
31
|
+
migration_path = File.join(__dir__, 'sequel_migrations')
|
|
32
|
+
begin
|
|
33
|
+
Sequel::Migrator.check_current(@connection, migration_path)
|
|
34
|
+
rescue Sequel::Migrator::NotCurrentError
|
|
35
|
+
Sequel::Migrator.run(@connection, migration_path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def migrate_to_postgres(sqlite_db, postgres_db)
|
|
40
|
+
migrate
|
|
41
|
+
sqlite_db.transaction do
|
|
42
|
+
sqlite_db.tables.each do |table|
|
|
43
|
+
next if table == :schema_info
|
|
44
|
+
|
|
45
|
+
sqlite_db[table].each do |row|
|
|
46
|
+
postgres_db[table.to_sym].insert(row)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
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).
|
|
29
|
-
|
|
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
|
data/lib/smart_proxy_container_gateway/sequel_migrations/004_users_repositories_cascade_delete.rb
ADDED
|
@@ -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
|
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:
|
|
4
|
+
version: 2.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-
|
|
11
|
+
date: 2024-04-15 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.
|
|
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
|