-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' into get_702_working_after_debian_refactor
- Loading branch information
Showing
30 changed files
with
1,032 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -90,3 +90,4 @@ gem 'terminal-table', '~> 3.0' | |
|
||
# needed by rmt-server-pubcloud | ||
gem 'jwt', '~> 2.1' | ||
gem 'base32' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.bundle/ | ||
log/*.log | ||
pkg/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Registry | ||
Provide an authentication end point for a container registry. | ||
This supports access control based on system credentials to registry paths that are set to have access restrictions. |
4 changes: 4 additions & 0 deletions
4
engines/registry/app/controllers/registry/application_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
module Registry | ||
class ApplicationController < ActionController::Base | ||
end | ||
end |
103 changes: 103 additions & 0 deletions
103
engines/registry/app/controllers/registry/registry_controller.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
module Registry | ||
class RegistryController < Registry::ApplicationController | ||
REGISTRY_SERVICE = 'SUSE Linux OCI Registry'.freeze | ||
REGISTRY_API_VERSION = 'registry/2.0'.freeze | ||
|
||
before_action :set_requested_scopes, except: [ :catalog ] | ||
before_action :basic_auth, except: [ :catalog ] | ||
before_action :catalog_token_auth, only: [ :catalog ] | ||
|
||
# AuthZ handler | ||
# AuthZ will validate which of the requested scope policies are fulfilled | ||
# with the current login access and prepare the token to be sent back to the client | ||
def authorize | ||
token = AccessToken.new(@client&.account, params['service'], @requested_scopes.map { |s| s.granted(client: @client) }).token | ||
render json: { token: token }, status: :ok | ||
end | ||
|
||
# Catalog handler | ||
# Returns a Distribution Registry HTTP API V2 - compatible repository catalog as defined in | ||
# https://distribution.github.io/distribution/spec/api/#listing-repositories | ||
def catalog | ||
access_scope = AccessScope.parse('registry:catalog:*') | ||
origin_url = request.protocol + request.host | ||
repos = access_scope.allowed_paths(System.find_by(login: @client&.account), origin_url) | ||
logger.debug("Returning #{repos.size} repos for client #{@client}") | ||
|
||
response.set_header('Docker-Distribution-Api-Version', REGISTRY_API_VERSION) | ||
render json: { repositories: repos }, status: :ok | ||
end | ||
|
||
private | ||
|
||
# Support multiple scopes. Podman & Docker handle this differently | ||
# - Podman sends multiple querystrings with the same name. | ||
# - Docker sends space-separated values in only one query string. | ||
# This should normalize everything | ||
def set_requested_scopes | ||
raw_scopes = CGI.parse(request.env['QUERY_STRING']).fetch('scope', []) | ||
.join(' ') | ||
.split | ||
.map(&:presence) | ||
.compact | ||
.map(&:downcase) | ||
|
||
@requested_scopes = [] | ||
@requested_scopes = raw_scopes.map { |scope| AccessScope.parse(scope) } unless raw_scopes.empty? | ||
|
||
logger.info("Requested scopes: #{@requested_scopes.map(&:to_s)}") | ||
end | ||
|
||
# AuthN handler | ||
# https://docs.docker.com/registry/spec/auth/jwt/#getting-a-bearer-token | ||
def basic_auth | ||
# skip authentication if this is not a login request | ||
return unless request.authorization | ||
|
||
authenticate_or_request_with_http_basic('SUSE Registry Authentication') do |login, password| | ||
begin | ||
@client = Registry::AuthenticatedClient.new(login, password) | ||
rescue StandardError | ||
logger.info _('Could not find system with login \"%{login}\" and password \"%{password}\"') % | ||
{ login: login, password: password } | ||
error = ActionController::TranslatedError.new(N_('Please, re-authenticate')) | ||
error.status = :unauthorized | ||
render json: { error: error.message }.to_json, status: :unauthorized | ||
end | ||
|
||
true | ||
end | ||
end | ||
|
||
def catalog_token_auth | ||
authenticate_or_request_with_http_token(authorize_url, 'authentication required') do |token| | ||
begin | ||
@client = CatalogClient.new(token) | ||
rescue JWT::DecodeError | ||
logger.info _('Invalid token') | ||
error = ActionController::TranslatedError.new(N_('Please, run cloudguestregistryauth')) | ||
error.status = :unauthorized | ||
render json: { error: error.message }.to_json, status: :unauthorized | ||
return | ||
end | ||
|
||
@client.authorized_for_catalog? | ||
end | ||
end | ||
|
||
# is called by authenticate_or_request_with_http_token when client provides no token | ||
def request_http_token_authentication(realm = authorize_url, message = 'authentication required') | ||
www_authenticate = [ | ||
%(Bearer realm="#{realm.delete('"')}"), | ||
%(service="#{REGISTRY_SERVICE.delete('"')}"), | ||
%(scope="registry:catalog:*") | ||
] | ||
|
||
www_authenticate << %(error="insufficient_scope") if request.authorization | ||
|
||
headers['WWW-Authenticate'] = www_authenticate.join(',') | ||
|
||
render json: { errors: [ code: 'UNAUTHORIZED', details: nil, message: message] }, status: :unauthorized | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
require 'yaml' | ||
|
||
AUTHORIZED_ACTION = ['pull'].freeze | ||
# this is analogous to auth.Access in golang code.' | ||
class AccessScope | ||
attr_accessor :type, | ||
:class, | ||
:namespace, | ||
:image, | ||
:actions | ||
|
||
# Parses a String into an authorization scope. | ||
# | ||
# scope - a String containing an authorization scope in the following format: | ||
# `<type>[(<class>)]:<namespace>/<image>:<actions>` | ||
# `repository(special):name:pull` | ||
# `registry:catalog:*` | ||
# | ||
# Returns a new Scope. | ||
def self.parse(scope) | ||
raise_on_invalid_scope(scope) | ||
type, name, actions = scope.split(':') | ||
_, type, klass = /(\w+)\(?(\w+)?\)?/.match(type).to_a | ||
actions = actions.split(',') | ||
new(type: type, klass: klass, name: name, actions: actions) | ||
end | ||
|
||
def initialize(type:, name:, actions:, klass: nil) | ||
@type = type | ||
@klass = klass | ||
@namespace, _, @image = name.rpartition('/').map(&:strip).map(&:presence) | ||
@actions = actions | ||
end | ||
|
||
def full_type | ||
return "#{@type}(#{@klass})" if @klass.present? | ||
|
||
@type | ||
end | ||
|
||
def to_s | ||
"#{full_type}:#{name}:#{@actions.join(',')}" | ||
end | ||
|
||
def granted(client: nil) | ||
aa = authorized_actions(client) | ||
Rails.logger.info "Granted actions for user '#{client&.account || '<anonymous>'}': #{aa}" | ||
{ | ||
'type' => @type, | ||
'class' => @klass, | ||
'name' => name, | ||
'actions' => aa | ||
} | ||
end | ||
|
||
def name | ||
[namespace, image].map(&:presence).compact.join('/') | ||
end | ||
|
||
def authorized_actions(client = nil) | ||
if @namespace.nil? | ||
@image == 'catalog' ? @actions : AUTHORIZED_ACTION | ||
else | ||
allowed_paths(client.systems.first) | ||
if @allowed_paths.any? { |allowed_path| File.fnmatch(@namespace + '*', allowed_path) } | ||
@actions & AUTHORIZED_ACTION | ||
else | ||
[] | ||
end | ||
end | ||
end | ||
|
||
def self.raise_on_invalid_scope(scope) | ||
# if nothing is passed, return | ||
raise Registry::Exceptions::InvalidScope.new('Empty scope') if scope.blank? | ||
# if scope is malformed, return | ||
raise Registry::Exceptions::InvalidScope.new('Invalid scope format') unless scope.split(':').size == 3 | ||
raise Registry::Exceptions::InvalidScope.new('Invalid scope format') unless %r{^[a-z0-9\-_/:*(),.]+$}i.match?(scope) | ||
end | ||
|
||
def allowed_paths(system = nil, origin_url = nil) | ||
repo_list = RegistryCatalogService.new.repos(reload: false, system: system, origin_url: origin_url) | ||
access_policies_yml = YAML.safe_load( | ||
File.read(Rails.application.config.access_policies) | ||
) | ||
active_products = system.activations.includes(:product).pluck(:product_class) | ||
|
||
allowed_products = (active_products & access_policies_yml.keys) | ||
allowed_glob_paths = access_policies_yml.values_at(*allowed_products).flatten | ||
|
||
@allowed_paths = parse_repos(repo_list, allowed_glob_paths) | ||
end | ||
|
||
def parse_repos(repos, allowed_paths) | ||
filtered_repos = [] | ||
|
||
allowed_paths.each do |allowed_path| | ||
pattern = allowed_path.gsub(/(?<!\*)\*(?!\*)/, '[^/]*').gsub('**', '.*') | ||
repos.each do |repo| | ||
next if filtered_repos.include? repo | ||
|
||
filtered_repos << repo unless (repo =~ /^#{pattern}$/).nil? | ||
end | ||
end | ||
filtered_repos | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
require 'base32' | ||
|
||
# Following Docker distribution token auth specs, | ||
# see docs here: https://github.com/distribution/distribution/blob/main/docs/spec/auth/token.md | ||
class AccessToken | ||
def initialize(account, service, granted_scopes) | ||
@account = account | ||
@service = service # "SUSE Linux OCI Registry" | ||
@granted_scopes = granted_scopes | ||
end | ||
|
||
def token | ||
JWT.encode(claim, private_key, 'RS256', { 'kid' => jwt_kid }) | ||
end | ||
|
||
private | ||
|
||
def claim | ||
{}.tap do |hash| | ||
hash['iss'] = 'RMT' # "matching issuer in registry auth token config" | ||
hash['sub'] = @account | ||
hash['aud'] = @service | ||
hash['exp'] = Time.now.getlocal.to_i + (5 * 60) # expires at | ||
hash['nbf'] = Time.now.getlocal.to_i # not before | ||
hash['iat'] = Time.now.getlocal.to_i # issued at | ||
hash['jti'] = Base64.urlsafe_encode64(SecureRandom.uuid, padding: false) | ||
hash['access'] = @granted_scopes | ||
Rails.logger.debug { "Returning token for claim: #{hash}" } | ||
end | ||
end | ||
|
||
# Returns the ID of the key which was to used to sign the token. | ||
def jwt_kid | ||
sha256 = Digest::SHA256.new | ||
sha256.update(private_key.public_key.to_der) | ||
payload = StringIO.new(sha256.digest).read(30) | ||
Base32.encode(payload).chars.each_slice(4).with_object([]) do |slice, mem| | ||
mem << slice.join | ||
mem | ||
end.join(':') | ||
end | ||
|
||
def private_key | ||
@private_key ||= Rails.application.config.registry_private_key | ||
end | ||
end |
Oops, something went wrong.