smart_proxy_container_gateway 1.0.3 → 1.0.4

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: ed0a914e50f04c3851c5ddde3f117ed6ab10d87912c1736cd229d270e44f9043
4
- data.tar.gz: cc6e50ca1142aa6e4a8103092033bafb8c0b29e342bc8edb303b7ecf4e53ddf7
3
+ metadata.gz: 8c0467076082cd7138c8415f2915d72c9b69fea3af5909d9c396ed4b247f5009
4
+ data.tar.gz: 66a1866562b0fb767f1a7c94f40ad3572d89a67966269db711dfd2ca967449e1
5
5
  SHA512:
6
- metadata.gz: 87a98b19c70679c4e1529adf4b080e8b9c5bd78fdde4db87709f76743eb43ad8b6ee0552a42bfc03465d36679da768e4d4f8c5ef8daca58dcd31d07e256bd9d2
7
- data.tar.gz: 470244c840404138894b8f22991cc65cbdb429b0764632ac4e1a87444f7e0d67319a7277c7f9bac22665c3b2808d47e3b0ed05f519aec16bf6081d84878f5ea8
6
+ metadata.gz: d5fb41b0be01a09736defddc0ec23ccf2ef81199700194cf46fbbc4afaaa6b2ffeebb79afa6119822a50f225ae7675ce1ea19946c49ff96882912c924a752b02
7
+ data.tar.gz: 7098744ff44801ee8051e3bdcdb87d2d7379eada25517552683023ef0cdd5932a52bdffd4b874e22b465cc7309754cc09b87a7d2fd36365be020a006c5f732cc
@@ -2,7 +2,6 @@ require 'sinatra'
2
2
  require 'smart_proxy_container_gateway/container_gateway'
3
3
  require 'smart_proxy_container_gateway/container_gateway_main'
4
4
  require 'smart_proxy_container_gateway/foreman_api'
5
- require 'sequel'
6
5
  require 'sqlite3'
7
6
 
8
7
  module Proxy
@@ -10,7 +9,7 @@ module Proxy
10
9
  class Api < ::Sinatra::Base
11
10
  include ::Proxy::Log
12
11
  helpers ::Proxy::Helpers
13
- Sequel.extension :migration, :core_extensions
12
+ helpers ::Sinatra::Authorization::Helpers
14
13
 
15
14
  get '/v1/_ping/?' do
16
15
  Proxy::ContainerGateway.ping
@@ -18,6 +17,7 @@ module Proxy
18
17
 
19
18
  get '/v2/?' do
20
19
  if auth_header.present? && (auth_header.unauthorized_token? || auth_header.valid_user_token?)
20
+ response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
21
21
  Proxy::ContainerGateway.ping
22
22
  else
23
23
  redirect_authorization_headers
@@ -26,31 +26,32 @@ module Proxy
26
26
  end
27
27
 
28
28
  get '/v2/:repository/manifests/:tag/?' do
29
- unless Proxy::ContainerGateway.authorized_for_repo?(params[:repository])
30
- redirect_authorization_headers
31
- halt 401, "unauthorized"
32
- end
29
+ handle_repo_auth(params, auth_header, request)
33
30
  redirection_location = Proxy::ContainerGateway.manifests(params[:repository], params[:tag])
34
31
  redirect to(redirection_location)
35
32
  end
36
33
 
37
34
  get '/v2/:repository/blobs/:digest/?' do
38
- unless Proxy::ContainerGateway.authorized_for_repo?(params[:repository])
39
- redirect_authorization_headers
40
- halt 401, "unauthorized"
41
- end
35
+ handle_repo_auth(params, auth_header, request)
42
36
  redirection_location = Proxy::ContainerGateway.blobs(params[:repository], params[:digest])
43
37
  redirect to(redirection_location)
44
38
  end
45
39
 
46
40
  get '/v1/search/?' do
47
41
  # Checks for podman client and issues a 404 in that case. Podman
48
- # examines the response from a /v1_search request. If the result
42
+ # examines the response from a /v1/search request. If the result
49
43
  # is a 4XX, it will then proceed with a request to /_catalog
50
44
  if !request.env['HTTP_USER_AGENT'].nil? && request.env['HTTP_USER_AGENT'].downcase.include?('libpod')
51
45
  halt 404, "not found"
52
46
  end
53
47
 
48
+ if auth_header.present? && !auth_header.blank?
49
+ username = auth_header.v1_foreman_authorized_username
50
+ if username.nil?
51
+ halt 401, "unauthorized"
52
+ end
53
+ params[:user] = username
54
+ end
54
55
  repositories = Proxy::ContainerGateway.v1_search(params)
55
56
 
56
57
  content_type :json
@@ -58,13 +59,23 @@ module Proxy
58
59
  end
59
60
 
60
61
  get '/v2/_catalog/?' do
61
- content_type :json
62
- { repositories: Proxy::ContainerGateway.catalog }.to_json
63
- end
62
+ catalog = []
63
+ if auth_header.present?
64
+ if auth_header.unauthorized_token?
65
+ catalog = Proxy::ContainerGateway.catalog
66
+ elsif auth_header.valid_user_token?
67
+ catalog = Proxy::ContainerGateway.catalog(auth_header.user)
68
+ else
69
+ redirect_authorization_headers
70
+ halt 401, "unauthorized"
71
+ end
72
+ else
73
+ redirect_authorization_headers
74
+ halt 401, "unauthorized"
75
+ end
64
76
 
65
- get '/v2/unauthenticated_repository_list/?' do
66
77
  content_type :json
67
- { repositories: Proxy::ContainerGateway.unauthenticated_repos }.to_json
78
+ { repositories: catalog }.to_json
68
79
  end
69
80
 
70
81
  get '/v2/token' do
@@ -83,28 +94,56 @@ module Proxy
83
94
  token_response_body = JSON.parse(token_response.body)
84
95
  ContainerGateway.insert_token(request.params['account'], token_response_body['token'],
85
96
  token_response_body['expires_at'])
97
+
98
+ repo_response = ForemanApi.new.fetch_user_repositories(auth_header.raw_header, request.params)
99
+ if repo_response.code.to_i != 200
100
+ halt repo_response.code.to_i, repo_response.body
101
+ else
102
+ ContainerGateway.update_user_repositories(request.params['account'],
103
+ JSON.parse(repo_response.body)['repositories'])
104
+ end
86
105
  return token_response_body.to_json
87
106
  end
88
107
  end
89
108
 
90
- put '/v2/unauthenticated_repository_list/?' do
91
- if params.key? :repositories
92
- repo_names = params[:repositories]
93
- else
94
- repo_names = JSON.parse(request.body.read)["repositories"]
95
- end
96
- rescue JSON::ParserError
97
- halt 400, "malformed repositories json"
98
- else
99
- if repo_names.nil?
100
- Proxy::ContainerGateway.update_unauthenticated_repos([])
101
- else
102
- Proxy::ContainerGateway.update_unauthenticated_repos(repo_names)
103
- end
109
+ get '/users/?' do
110
+ do_authorize_any
111
+
112
+ content_type :json
113
+ { users: User.map(:name) }.to_json
114
+ end
115
+
116
+ put '/user_repository_mapping/?' do
117
+ do_authorize_any
118
+
119
+ ContainerGateway.update_user_repo_mapping(params)
120
+ {}
121
+ end
122
+
123
+ put '/repository_list/?' do
124
+ do_authorize_any
125
+
126
+ ContainerGateway.update_repository_list(params['repositories'])
127
+ {}
104
128
  end
105
129
 
106
130
  private
107
131
 
132
+ def handle_repo_auth(params, auth_header, request)
133
+ user_token_is_valid = false
134
+ # FIXME: Getting unauthenticated token here...
135
+ if auth_header.present? && auth_header.valid_user_token?
136
+ user_token_is_valid = true
137
+ username = auth_header.user.name
138
+ end
139
+ username = request.params['account'] if username.nil?
140
+
141
+ return if Proxy::ContainerGateway.authorized_for_repo?(params[:repository], user_token_is_valid, username)
142
+
143
+ redirect_authorization_headers
144
+ halt 401, "unauthorized"
145
+ end
146
+
108
147
  def redirect_authorization_headers
109
148
  response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
110
149
  response.headers['Www-Authenticate'] = "Bearer realm=\"https://#{request.host}/v2/token\"," \
@@ -123,6 +162,10 @@ module Proxy
123
162
  @value = value || ''
124
163
  end
125
164
 
165
+ def user
166
+ ContainerGateway.token_user(@value.split(' ')[1])
167
+ end
168
+
126
169
  def valid_user_token?
127
170
  token_auth? && ContainerGateway.valid_token?(@value.split(' ')[1])
128
171
  end
@@ -146,6 +189,19 @@ module Proxy
146
189
  def basic_auth?
147
190
  @value.split(' ')[0] == 'Basic'
148
191
  end
192
+
193
+ def blank?
194
+ Base64.decode64(@value.split(' ')[1]) == ':'
195
+ end
196
+
197
+ # A special case for the V1 API. Defer authentication to Foreman and return the username. `nil` if not authorized.
198
+ def v1_foreman_authorized_username
199
+ username = Base64.decode64(@value.split(' ')[1]).split(':')[0]
200
+ auth_response = ForemanApi.new.fetch_token(raw_header, { 'account' => username })
201
+ return username if auth_response.code.to_i == 200 && (JSON.parse(auth_response.body)['token'] != 'unauthenticated')
202
+
203
+ nil
204
+ end
149
205
  end
150
206
  end
151
207
  end
@@ -1,13 +1,14 @@
1
1
  require 'net/http'
2
2
  require 'uri'
3
3
  require 'digest'
4
-
4
+ require 'sequel'
5
5
  module Proxy
6
6
  module ContainerGateway
7
7
  extend ::Proxy::Util
8
8
  extend ::Proxy::Log
9
9
 
10
10
  class << self
11
+ Sequel.extension :migration, :core_extensions
11
12
  def pulp_registry_request(uri)
12
13
  http_client = Net::HTTP.new(uri.host, uri.port)
13
14
  http_client.ca_file = pulp_ca
@@ -49,7 +50,8 @@ module Proxy
49
50
 
50
51
  repo_count = 0
51
52
  repositories = []
52
- Proxy::ContainerGateway.catalog.each do |repo_name|
53
+ user = params[:user].nil? ? nil : User.find(name: params[:user])
54
+ Proxy::ContainerGateway.catalog(user).each do |repo_name|
53
55
  break if repo_count >= params[:n]
54
56
 
55
57
  if params[:q].nil? || params[:q] == "" || repo_name.include?(params[:q])
@@ -60,48 +62,103 @@ module Proxy
60
62
  repositories
61
63
  end
62
64
 
63
- def catalog
64
- unauthenticated_repos
65
+ def catalog(user = nil)
66
+ if user.nil?
67
+ unauthenticated_repos
68
+ else
69
+ (unauthenticated_repos + user.repositories_dataset.map(:name)).sort
70
+ end
65
71
  end
66
72
 
67
73
  def unauthenticated_repos
68
- conn = initialize_db
69
- conn[:unauthenticated_repositories].order(:name).map(:name)
74
+ Repository.where(auth_required: false).order(:name).map(:name)
70
75
  end
71
76
 
72
- def update_unauthenticated_repos(repo_names)
73
- conn = initialize_db
74
- unauthenticated_repos = conn[:unauthenticated_repositories]
75
- unauthenticated_repos.delete
76
- repo_names.each do |repo_name|
77
- unauthenticated_repos.insert(:name => repo_name)
77
+ # Replaces the entire list of repositories
78
+ def update_repository_list(repo_list)
79
+ RepositoryUser.dataset.delete
80
+ Repository.dataset.delete
81
+ repo_list.each do |repo|
82
+ Repository.find_or_create(name: repo['repository'],
83
+ auth_required: repo['auth_required'].to_s.downcase == "true")
78
84
  end
79
85
  end
80
86
 
81
- def authorized_for_repo?(repo_name)
82
- conn = initialize_db
83
- unauthenticated_repo = conn[:unauthenticated_repositories].where(name: repo_name).first
84
- !unauthenticated_repo.nil?
87
+ # Replaces the entire user-repo mapping for all logged-in users
88
+ def update_user_repo_mapping(user_repo_maps)
89
+ # Get hash map of all users and their repositories
90
+ # Ex: {"users"=> [{"admin"=>[{"repository"=>"repo", "auth_required"=>"true"}]}]}
91
+ # Go through list of repositories and add them to the DB
92
+ RepositoryUser.dataset.delete
93
+ user_repo_maps['users'].each do |user_repo_map|
94
+ user_repo_map.each do |user, repos|
95
+ repos.each do |repo|
96
+ found_repo = Repository.find(name: repo['repository'],
97
+ auth_required: repo['auth_required'].to_s.downcase == "true")
98
+ if found_repo.nil?
99
+ logger.warn("#{repo['repository']} does not exist in this smart proxy's environments")
100
+ elsif found_repo.auth_required
101
+ found_repo.add_user(User.find(name: user))
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ # Replaces the user-repo mapping for a single user
109
+ def update_user_repositories(username, repositories)
110
+ user = User.where(name: username).first
111
+ user.remove_all_repositories
112
+ repositories.each do |repo_name|
113
+ found_repo = Repository.find(name: repo_name)
114
+ if found_repo.nil?
115
+ logger.warn("#{repo_name} does not exist in this smart proxy's environments")
116
+ elsif user.repositories_dataset.where(name: repo_name).first.nil? && found_repo.auth_required
117
+ user.add_repository(found_repo)
118
+ end
119
+ end
120
+ end
121
+
122
+ def authorized_for_repo?(repo_name, user_token_is_valid, username = nil)
123
+ repository = Repository.where(name: repo_name).first
124
+
125
+ # Repository doesn't exist
126
+ return false if repository.nil?
127
+
128
+ # Repository doesn't require auth
129
+ return true unless repository.auth_required
130
+
131
+ if username && user_token_is_valid && repository.auth_required
132
+ # User is logged in and has access to the repository
133
+ user = User.find(name: username)
134
+ return !user.repositories_dataset.where(name: repo_name).first.nil?
135
+ end
136
+
137
+ false
138
+ end
139
+
140
+ def token_user(token)
141
+ User[AuthenticationToken.find(token_checksum: Digest::SHA256.hexdigest(token)).user_id]
85
142
  end
86
143
 
87
144
  def valid_token?(token)
88
- tokens = initialize_db[:authentication_tokens]
89
- tokens.where(token_checksum: Digest::SHA256.hexdigest(token)).where do
145
+ AuthenticationToken.where(token_checksum: Digest::SHA256.hexdigest(token)).where do
90
146
  expire_at > Sequel::CURRENT_TIMESTAMP
91
147
  end.count.positive?
92
148
  end
93
149
 
94
150
  def insert_token(username, token, expire_at_string, clear_expired_tokens: true)
95
- tokens = initialize_db[:authentication_tokens]
96
151
  checksum = Digest::SHA256.hexdigest(token)
152
+ user = User.find_or_create(name: username)
97
153
 
98
- tokens.where(:token_checksum => checksum).delete
99
- tokens.insert(username: username, token_checksum: checksum, expire_at: expire_at_string.to_s)
100
- tokens.where { expire_at < Sequel::CURRENT_TIMESTAMP }.delete if clear_expired_tokens
154
+ AuthenticationToken.where(:token_checksum => checksum).delete
155
+ AuthenticationToken.create(token_checksum: checksum, expire_at: expire_at_string.to_s, user_id: user.id)
156
+ AuthenticationToken.where { expire_at < Sequel::CURRENT_TIMESTAMP }.delete if clear_expired_tokens
101
157
  end
102
158
 
103
159
  def initialize_db
104
- conn = Sequel.connect("sqlite://#{Proxy::ContainerGateway::Plugin.settings.sqlite_db_path}")
160
+ file_path = Proxy::ContainerGateway::Plugin.settings.sqlite_db_path
161
+ conn = Sequel.connect("sqlite://#{file_path}")
105
162
  container_gateway_path = $LOAD_PATH.detect { |path| path.include? 'smart_proxy_container_gateway' }
106
163
  begin
107
164
  Sequel::Migrator.check_current(conn, "#{container_gateway_path}/smart_proxy_container_gateway/sequel_migrations")
@@ -131,5 +188,20 @@ module Proxy
131
188
  )
132
189
  end
133
190
  end
191
+
192
+ class Repository < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:repositories])
193
+ many_to_many :users
194
+ end
195
+
196
+ class User < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:users])
197
+ many_to_many :repositories
198
+ one_to_many :authentication_tokens
199
+ end
200
+
201
+ class RepositoryUser < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:repositories_users]); end
202
+
203
+ class AuthenticationToken < ::Sequel::Model(Proxy::ContainerGateway.initialize_db[:authentication_tokens])
204
+ many_to_one :users
205
+ end
134
206
  end
135
207
  end
@@ -3,15 +3,15 @@ require 'uri'
3
3
  module Proxy
4
4
  module ContainerGateway
5
5
  class ForemanApi
6
- def fetch_token(auth_header, params)
7
- url = URI.join(Proxy::SETTINGS.foreman_url, Proxy::ContainerGateway::Plugin.settings.katello_registry_path, 'token')
8
- url.query = process_params(params)
6
+ def registry_request(auth_header, params, suffix)
7
+ uri = URI.join(Proxy::SETTINGS.foreman_url, Proxy::ContainerGateway::Plugin.settings.katello_registry_path, suffix)
8
+ uri.query = process_params(params)
9
9
 
10
- req = Net::HTTP::Get.new(url)
10
+ req = Net::HTTP::Get.new(uri)
11
11
  req.add_field('Authorization', auth_header)
12
12
  req.add_field('Accept', 'application/json')
13
13
  req.content_type = 'application/json'
14
- http = Net::HTTP.new(url.hostname, url.port)
14
+ http = Net::HTTP.new(uri.hostname, uri.port)
15
15
  http.use_ssl = true
16
16
 
17
17
  http.request(req)
@@ -21,6 +21,14 @@ module Proxy
21
21
  params = params_in.slice('scope', 'account').compact
22
22
  URI.encode_www_form(params)
23
23
  end
24
+
25
+ def fetch_token(auth_header, params)
26
+ registry_request(auth_header, params, 'token')
27
+ end
28
+
29
+ def fetch_user_repositories(auth_header, params)
30
+ registry_request(auth_header, params, '_catalog')
31
+ end
24
32
  end
25
33
  end
26
34
  end
@@ -0,0 +1,65 @@
1
+ Sequel.migration do
2
+ up do
3
+ # TODO: Should I be migrating the existing data?
4
+
5
+ create_table(:repositories) do
6
+ primary_key :id
7
+ String :name, null: false
8
+ Boolean :auth_required, null: false
9
+ end
10
+
11
+ create_table(:users) do
12
+ primary_key :id
13
+ String :name, null: false
14
+ end
15
+
16
+ # Migrate unauthenticated_repositories to the new repositories table (TODO: can I select `false` like that?)
17
+ from(:repositories).insert(%i[name auth_required],
18
+ from(:unauthenticated_repositories).select(:name, false))
19
+
20
+ # Migrate names from authentication_tokens to the new users table
21
+ from(:users).insert([:name], from(:authentication_tokens).select(:username))
22
+
23
+ alter_table(:authentication_tokens) do
24
+ add_foreign_key :user_id, :users
25
+ end
26
+
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]))
30
+
31
+ alter_table(:authentication_tokens) do
32
+ drop_column :username
33
+ end
34
+
35
+ create_join_table(repository_id: :repositories, user_id: :users)
36
+ drop_table :unauthenticated_repositories
37
+ end
38
+
39
+ down do
40
+ alter_table(:authentication_tokens) do
41
+ add_column :username, String
42
+ end
43
+
44
+ # Repopulate the name column with usernames
45
+ from(:authentication_tokens).update(username:
46
+ from(:users).select(:name).where(id: self[:authentication_tokens][:user_id]))
47
+
48
+ alter_table(:authentication_tokens) do
49
+ drop_foreign_key :user_id
50
+ end
51
+
52
+ create_table(:unauthenticated_repositories) do
53
+ primary_key :id
54
+ String :name, null: false
55
+ end
56
+
57
+ # Repopulate the unauthenticated_repositories table
58
+ from(:unauthenticated_repositories).insert([:username],
59
+ from(:repositories).select(:name).where(auth_required: true))
60
+
61
+ drop_table :users
62
+ drop_table :repositories
63
+ drop_join_table(repository_id: :repositories, user_id: :users)
64
+ end
65
+ end
@@ -1,5 +1,5 @@
1
1
  module Proxy
2
2
  module ContainerGateway
3
- VERSION = '1.0.3'.freeze
3
+ VERSION = '1.0.4'.freeze
4
4
  end
5
5
  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: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ian Ballou
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-04-15 00:00:00.000000000 Z
11
+ date: 2021-04-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -57,6 +57,7 @@ files:
57
57
  - lib/smart_proxy_container_gateway/foreman_api.rb
58
58
  - lib/smart_proxy_container_gateway/sequel_migrations/001_initial.rb
59
59
  - lib/smart_proxy_container_gateway/sequel_migrations/002_auth_tokens.rb
60
+ - lib/smart_proxy_container_gateway/sequel_migrations/003_authorization_reorg.rb
60
61
  - lib/smart_proxy_container_gateway/version.rb
61
62
  - settings.d/container_gateway.yml.example
62
63
  homepage: http://github.com/ianballou/smart_proxy_container_gateway