Project

General

Profile

Download (8.99 KB) Statistics
| Branch: | Tag: | Revision:
require 'active_support'
require 'active_support/core_ext/integer'
require 'active_support/core_ext/string'
require 'active_support/time_with_zone'
require 'sinatra'
require 'smart_proxy_container_gateway/container_gateway'
require 'smart_proxy_container_gateway/container_gateway_main'
require 'smart_proxy_container_gateway/foreman_api'

module Proxy
module ContainerGateway
class Api < ::Sinatra::Base
include ::Proxy::Log
helpers ::Proxy::Helpers
helpers ::Sinatra::Authorization::Helpers
extend ::Proxy::ContainerGateway::DependencyInjection

inject_attr :database_impl, :database
inject_attr :container_gateway_main_impl, :container_gateway_main

get '/v1/_ping/?' do
container_gateway_main.ping
end

get '/v2/?' do
if auth_header.present? && (auth_header.unauthorized_token? || auth_header.valid_user_token?)
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
container_gateway_main.ping
else
redirect_authorization_headers
halt 401, "unauthorized"
end
end

get '/v2/*/manifests/*/?' do
repository = params[:splat][0]
tag = params[:splat][1]
handle_repo_auth(repository, auth_header, request)
redirection_location = container_gateway_main.manifests(repository, tag)
redirect to(redirection_location)
end

get '/v2/*/blobs/*/?' do
repository = params[:splat][0]
digest = params[:splat][1]
handle_repo_auth(repository, auth_header, request)
redirection_location = container_gateway_main.blobs(repository, digest)
redirect to(redirection_location)
end

get '/v2/*/tags/list/?' do
repository = params[:splat][0]
handle_repo_auth(repository, auth_header, request)
pulp_response = container_gateway_main.tags(repository, params)
# "link"=>["<http://pulpcore-api/v2/container-image-name/tags/list?n=100&last=last-tag-name>; rel=\"next\""],
# https://docs.docker.com/registry/spec/api/#pagination-1
if pulp_response['link'].nil?
headers['link'] = ""
else
headers['link'] = pulp_response['link']
end
pulp_response.body
end

get '/v1/search/?' do
# Checks for podman client and issues a 404 in that case. Podman
# examines the response from a /v1/search request. If the result
# is a 4XX, it will then proceed with a request to /_catalog
if !request.env['HTTP_USER_AGENT'].nil? && request.env['HTTP_USER_AGENT'].downcase.include?('libpod')
halt 404, "not found"
end

if auth_header.present? && !auth_header.blank?
username = auth_header.v1_foreman_authorized_username
if username.nil?
halt 401, "unauthorized"
end
params[:user] = username
end
repositories = container_gateway_main.v1_search(params)

content_type :json
{
num_results: repositories.size,
query: params[:q],
results: repositories.map { |repo_name| { description: '', name: repo_name } }
}.to_json
end

get '/v2/_catalog/?' do
catalog = []
if auth_header.present?
if auth_header.unauthorized_token?
catalog = container_gateway_main.catalog.select_map(::Sequel[:repositories][:name])
elsif auth_header.valid_user_token?
catalog = container_gateway_main.catalog(auth_header.user).select_map(::Sequel[:repositories][:name])
else
redirect_authorization_headers
halt 401, "unauthorized"
end
else
redirect_authorization_headers
halt 401, "unauthorized"
end

content_type :json
{ repositories: catalog }.to_json
end

get '/v2/token' do
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'

unless auth_header.present? && auth_header.basic_auth?
return { token: AuthorizationHeader::UNAUTHORIZED_TOKEN, issued_at: Time.now.rfc3339,
expires_in: 1.year.seconds.to_i }.to_json
end

token_response = ForemanApi.new.fetch_token(auth_header.raw_header, request.params)
if token_response.code.to_i != 200
halt token_response.code.to_i, token_response.body
else
# This returned token should follow OAuth2 spec. We need some minor conversion
# to store the token with the expires_at time (using rfc3339).
token_response_body = JSON.parse(token_response.body)

if token_response_body['token'].nil?
halt 502, "Recieved malformed token response"
end

# "issued_at" is an optional field. Per OAuth2 we assume time of token response as
# the issue time if the field is ommitted.
token_issue_time = (token_response_body["issued_at"] || token_response["Date"])&.to_time
if token_issue_time.nil?
halt 502, "Recieved malformed token response"
end

# 'expires_in' is an optional field. If not provided, assume 60 seconds per OAuth2 spec
expires_in = token_response_body.fetch("expires_in", 60)
expires_at = token_issue_time + expires_in.seconds

container_gateway_main.insert_token(
request.params['account'],
token_response_body['token'],
expires_at.rfc3339
)

repo_response = ForemanApi.new.fetch_user_repositories(auth_header.raw_header, request.params)
if repo_response.code.to_i != 200
halt repo_response.code.to_i, repo_response.body
else
container_gateway_main.update_user_repositories(request.params['account'],
JSON.parse(repo_response.body)['repositories'])
end

# Return the original token response from Katello
return token_response.body
end
end

get '/users/?' do
do_authorize_any

content_type :json
{ users: database.connection[:users].map(:name) }.to_json
end

put '/user_repository_mapping/?' do
do_authorize_any

container_gateway_main.update_user_repo_mapping(params)
{}
end

put '/repository_list/?' do
do_authorize_any

repositories = params['repositories'].nil? ? [] : params['repositories']
container_gateway_main.update_repository_list(repositories)
{}
end

private

def handle_repo_auth(repository, auth_header, request)
user_token_is_valid = false
if auth_header.present? && auth_header.valid_user_token?
user_token_is_valid = true
username = auth_header.user[:name]
end
username = request.params['account'] if username.nil?

return if container_gateway_main.authorized_for_repo?(repository, user_token_is_valid, username)

redirect_authorization_headers
halt 401, "unauthorized"
end

def redirect_authorization_headers
response.headers['Docker-Distribution-API-Version'] = 'registry/2.0'
response.headers['Www-Authenticate'] = "Bearer realm=\"https://#{request.host}/v2/token\"," \
"service=\"#{request.host}\"," \
"scope=\"repository:registry:pull,push\""
end

def auth_header
AuthorizationHeader.new(request.env['HTTP_AUTHORIZATION'])
end

class AuthorizationHeader
extend ::Proxy::ContainerGateway::DependencyInjection

inject_attr :database_impl, :database
inject_attr :container_gateway_main_impl, :container_gateway_main
UNAUTHORIZED_TOKEN = 'unauthorized'.freeze

def initialize(value)
@value = value || ''
end

def user
container_gateway_main.token_user(@value.split(' ')[1])
end

def valid_user_token?
token_auth? && container_gateway_main.valid_token?(@value.split(' ')[1])
end

def raw_header
@value
end

def present?
!@value.nil? && @value != ""
end

def unauthorized_token?
@value.split(' ')[1] == UNAUTHORIZED_TOKEN
end

def token_auth?
@value.split(' ')[0] == 'Bearer'
end

def basic_auth?
@value.split(' ')[0] == 'Basic'
end

def blank?
Base64.decode64(@value.split(' ')[1]) == ':'
end

# A special case for the V1 API. Defer authentication to Foreman and return the username. `nil` if not authorized.
def v1_foreman_authorized_username
username = Base64.decode64(@value.split(' ')[1]).split(':')[0]
auth_response = ForemanApi.new.fetch_token(raw_header, { 'account' => username })
return username if auth_response.code.to_i == 200 && (JSON.parse(auth_response.body)['token'] != 'unauthenticated')

nil
end
end
end
end
end
(2-2/8)