smart_proxy_container_gateway 1.0.3 → 1.0.4

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: 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