debian-mirror-gitlab/app/services/auth/container_registry_authentication_service.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

320 lines
9.9 KiB
Ruby
Raw Normal View History

2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2016-06-02 11:05:42 +05:30
module Auth
class ContainerRegistryAuthenticationService < BaseService
2019-12-04 20:38:33 +05:30
AUDIENCE = 'container_registry'
2020-03-07 23:17:34 +05:30
REGISTRY_LOGIN_ABILITIES = [
:read_container_image,
:create_container_image,
:destroy_container_image,
:update_container_image,
:admin_container_image,
:build_read_container_image,
:build_create_container_image,
:build_destroy_container_image
].freeze
2016-06-02 11:05:42 +05:30
2022-04-04 11:22:00 +05:30
FORBIDDEN_IMPORTING_SCOPES = %w[push delete *].freeze
ActiveImportError = Class.new(StandardError)
2016-09-29 09:46:39 +05:30
def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities
2016-10-01 15:18:49 +05:30
return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
2016-06-02 11:05:42 +05:30
2020-03-07 23:17:34 +05:30
return error('DENIED', status: 403, message: 'access forbidden') unless has_registry_ability?
2021-10-27 15:23:28 +05:30
unless scopes.any? || current_user || deploy_token || project
2016-11-24 13:41:30 +05:30
return error('DENIED', status: 403, message: 'access forbidden')
2016-06-02 11:05:42 +05:30
end
2018-11-18 11:00:15 +05:30
{ token: authorized_token(*scopes).encoded }
2022-04-04 11:22:00 +05:30
rescue ActiveImportError
error(
'DENIED',
status: 403,
message: 'Your repository is currently being migrated to a new platform and writes are temporarily disabled. Go to https://gitlab.com/groups/gitlab-org/-/epics/5523 to learn more.'
)
2016-06-02 11:05:42 +05:30
end
def self.full_access_token(*names)
2019-10-12 21:52:04 +05:30
access_token(%w(*), names)
end
2022-04-04 11:22:00 +05:30
def self.import_access_token
access_token(%w(*), ['import'], 'registry')
end
2019-10-12 21:52:04 +05:30
def self.pull_access_token(*names)
access_token(['pull'], names)
end
2022-06-21 17:19:12 +05:30
def self.pull_nested_repositories_access_token(name)
name = name.chomp('/') if name.end_with?('/')
paths = [name, "#{name}/*"]
access_token(['pull'], paths)
end
2022-04-04 11:22:00 +05:30
def self.access_token(actions, names, type = 'repository')
2017-08-17 22:00:37 +05:30
names = names.flatten
2016-06-02 11:05:42 +05:30
registry = Gitlab.config.registry
token = JSONWebToken::RSAToken.new(registry.key)
token.issuer = registry.issuer
token.audience = AUDIENCE
token.expire_time = token_expire_at
2016-08-24 12:49:21 +05:30
2016-06-02 11:05:42 +05:30
token[:access] = names.map do |name|
2023-07-09 08:55:56 +05:30
{
type: type,
name: name,
actions: actions,
meta: access_metadata(path: name)
}.compact
2016-06-02 11:05:42 +05:30
end
2016-09-13 17:45:13 +05:30
2016-06-02 11:05:42 +05:30
token.encoded
end
2016-09-13 17:45:13 +05:30
def self.token_expire_at
2020-05-24 23:13:21 +05:30
Time.current + Gitlab::CurrentSettings.container_registry_token_expire_delay.minutes
2016-09-13 17:45:13 +05:30
end
2023-07-09 08:55:56 +05:30
def self.access_metadata(project: nil, path: nil)
# If the project is not given, try to infer it from the provided path
if project.nil?
return if path.nil? # If no path is given, return early
return if path == 'import' # Ignore the special 'import' path
# If the path ends with '/*', remove it so we can parse the actual repository path
path = path.chomp('/*')
# Parse the repository project from the path
begin
project = ContainerRegistry::Path.new(path).repository_project
rescue ContainerRegistry::Path::InvalidRegistryPathError
# If the path is invalid, gracefully handle the error
return
end
end
# Return the project path (lowercase) as metadata
{ project_path: project&.full_path&.downcase }
end
2016-06-02 11:05:42 +05:30
private
def authorized_token(*accesses)
2017-08-17 22:00:37 +05:30
JSONWebToken::RSAToken.new(registry.key).tap do |token|
token.issuer = registry.issuer
token.audience = params[:service]
token.subject = current_user.try(:username)
token.expire_time = self.class.token_expire_at
2022-10-11 01:57:18 +05:30
token[:auth_type] = params[:auth_type]
2017-08-17 22:00:37 +05:30
token[:access] = accesses.compact
end
2016-06-02 11:05:42 +05:30
end
2018-11-18 11:00:15 +05:30
def scopes
return [] unless params[:scopes]
2016-06-02 11:05:42 +05:30
2018-11-18 11:00:15 +05:30
@scopes ||= params[:scopes].map do |scope|
process_scope(scope)
end.compact
2016-06-02 11:05:42 +05:30
end
def process_scope(scope)
type, name, actions = scope.split(':', 3)
actions = actions.split(',')
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
case type
when 'registry'
process_registry_access(type, name, actions)
when 'repository'
path = ContainerRegistry::Path.new(name)
process_repository_access(type, path, actions)
end
end
def process_registry_access(type, name, actions)
return unless current_user&.admin?
return unless name == 'catalog'
return unless actions == ['*']
2016-06-02 11:05:42 +05:30
2018-03-17 18:26:18 +05:30
{ type: type, name: name, actions: ['*'] }
2016-06-02 11:05:42 +05:30
end
2017-08-17 22:00:37 +05:30
def process_repository_access(type, path, actions)
return unless path.valid?
2022-04-04 11:22:00 +05:30
raise ActiveImportError if actively_importing?(actions, path)
2017-08-17 22:00:37 +05:30
requested_project = path.repository_project
2016-06-02 11:05:42 +05:30
return unless requested_project
2020-05-24 23:13:21 +05:30
authorized_actions = actions.select do |action|
2016-06-02 11:05:42 +05:30
can_access?(requested_project, action)
end
2020-05-24 23:13:21 +05:30
log_if_actions_denied(type, requested_project, actions, authorized_actions)
return unless authorized_actions.present?
2017-08-17 22:00:37 +05:30
# At this point user/build is already authenticated.
#
2020-05-24 23:13:21 +05:30
ensure_container_repository!(path, authorized_actions)
2017-08-17 22:00:37 +05:30
2023-07-09 08:55:56 +05:30
{
type: type,
name: path.to_s,
actions: authorized_actions,
meta: self.class.access_metadata(project: requested_project)
}
2021-09-30 23:02:18 +05:30
end
2022-04-04 11:22:00 +05:30
def actively_importing?(actions, path)
return false if FORBIDDEN_IMPORTING_SCOPES.intersection(actions).empty?
container_repository = ContainerRepository.find_by_path(path)
return false unless container_repository
container_repository.migration_importing?
end
2017-08-17 22:00:37 +05:30
##
# Because we do not have two way communication with registry yet,
# we create a container repository image resource when push to the
2018-12-13 13:39:08 +05:30
# registry is successfully authorized.
2017-08-17 22:00:37 +05:30
#
def ensure_container_repository!(path, actions)
return if path.has_repository?
return unless actions.include?('push')
2022-01-26 12:08:38 +05:30
ContainerRepository.find_or_create_from_path(path)
2016-06-02 11:05:42 +05:30
end
2021-02-22 17:27:13 +05:30
# Overridden in EE
2016-06-02 11:05:42 +05:30
def can_access?(requested_project, requested_action)
return false unless requested_project.container_registry_enabled?
2020-09-03 11:15:55 +05:30
return false if requested_project.repository_access_level == ::ProjectFeature::DISABLED
2016-06-02 11:05:42 +05:30
case requested_action
when 'pull'
2018-05-09 12:01:36 +05:30
build_can_pull?(requested_project) || user_can_pull?(requested_project) || deploy_token_can_pull?(requested_project)
2016-06-02 11:05:42 +05:30
when 'push'
2020-04-22 19:07:51 +05:30
build_can_push?(requested_project) || user_can_push?(requested_project) || deploy_token_can_push?(requested_project)
2019-12-04 20:38:33 +05:30
when 'delete'
build_can_delete?(requested_project) || user_can_admin?(requested_project)
when '*'
2017-09-10 17:25:29 +05:30
user_can_admin?(requested_project)
2016-06-02 11:05:42 +05:30
else
false
end
end
2019-12-04 20:38:33 +05:30
def build_can_delete?(requested_project)
# Build can delete only from the project from which it originates
has_authentication_ability?(:build_destroy_container_image) &&
requested_project == project
end
2016-06-02 11:05:42 +05:30
def registry
Gitlab.config.registry
end
2016-09-29 09:46:39 +05:30
2018-05-09 12:01:36 +05:30
def can_user?(ability, project)
2021-10-27 15:23:28 +05:30
can?(current_user, ability, project)
2018-05-09 12:01:36 +05:30
end
2016-09-29 09:46:39 +05:30
def build_can_pull?(requested_project)
# Build can:
# 1. pull from its own project (for ex. a build)
# 2. read images from dependent projects if creator of build is a team member
2016-11-24 13:41:30 +05:30
has_authentication_ability?(:build_read_container_image) &&
2018-05-09 12:01:36 +05:30
(requested_project == project || can_user?(:build_read_container_image, requested_project))
2016-09-29 09:46:39 +05:30
end
2017-09-10 17:25:29 +05:30
def user_can_admin?(requested_project)
has_authentication_ability?(:admin_container_image) &&
2018-05-09 12:01:36 +05:30
can_user?(:admin_container_image, requested_project)
2017-09-10 17:25:29 +05:30
end
2016-09-29 09:46:39 +05:30
def user_can_pull?(requested_project)
2016-11-24 13:41:30 +05:30
has_authentication_ability?(:read_container_image) &&
2018-05-09 12:01:36 +05:30
can_user?(:read_container_image, requested_project)
end
def deploy_token_can_pull?(requested_project)
has_authentication_ability?(:read_container_image) &&
2021-10-27 15:23:28 +05:30
deploy_token.present? &&
2022-07-01 11:34:44 +05:30
can?(deploy_token, :read_container_image, requested_project)
2016-09-29 09:46:39 +05:30
end
2020-04-22 19:07:51 +05:30
def deploy_token_can_push?(requested_project)
has_authentication_ability?(:create_container_image) &&
2021-10-27 15:23:28 +05:30
deploy_token.present? &&
2022-07-01 11:34:44 +05:30
can?(deploy_token, :create_container_image, requested_project)
2020-04-22 19:07:51 +05:30
end
2017-08-17 22:00:37 +05:30
##
# We still support legacy pipeline triggers which do not have associated
# actor. New permissions model and new triggers are always associated with
2019-03-02 22:35:43 +05:30
# an actor. So this should be improved once
2019-12-04 20:38:33 +05:30
# https://gitlab.com/gitlab-org/gitlab-foss/issues/37452 is resolved.
2017-08-17 22:00:37 +05:30
#
2016-09-29 09:46:39 +05:30
def build_can_push?(requested_project)
# Build can push only to the project from which it originates
2016-11-24 13:41:30 +05:30
has_authentication_ability?(:build_create_container_image) &&
2016-09-29 09:46:39 +05:30
requested_project == project
end
def user_can_push?(requested_project)
2016-11-24 13:41:30 +05:30
has_authentication_ability?(:create_container_image) &&
2018-05-09 12:01:36 +05:30
can_user?(:create_container_image, requested_project)
2016-09-29 09:46:39 +05:30
end
2016-10-01 15:18:49 +05:30
def error(code, status:, message: '')
2017-08-17 22:00:37 +05:30
{ errors: [{ code: code, message: message }], http_status: status }
2016-10-01 15:18:49 +05:30
end
2016-11-24 13:41:30 +05:30
def has_authentication_ability?(capability)
2017-08-17 22:00:37 +05:30
@authentication_abilities.to_a.include?(capability)
2016-11-24 13:41:30 +05:30
end
2020-03-07 23:17:34 +05:30
def has_registry_ability?
@authentication_abilities.any? do |ability|
REGISTRY_LOGIN_ABILITIES.include?(ability)
end
end
2020-05-24 23:13:21 +05:30
2021-02-22 17:27:13 +05:30
# Overridden in EE
def extra_info
{}
end
2021-10-27 15:23:28 +05:30
def deploy_token
params[:deploy_token]
end
2020-05-24 23:13:21 +05:30
def log_if_actions_denied(type, requested_project, requested_actions, authorized_actions)
return if requested_actions == authorized_actions
log_info = {
2021-02-22 17:27:13 +05:30
message: 'Denied container registry permissions',
2020-05-24 23:13:21 +05:30
scope_type: type,
requested_project_path: requested_project.full_path,
requested_actions: requested_actions,
authorized_actions: authorized_actions,
username: current_user&.username,
user_id: current_user&.id,
project_path: project&.full_path
2021-02-22 17:27:13 +05:30
}.merge!(extra_info).compact
2020-05-24 23:13:21 +05:30
Gitlab::AuthLogger.warn(log_info)
end
2016-06-02 11:05:42 +05:30
end
end
2021-02-22 17:27:13 +05:30
2021-06-08 01:23:25 +05:30
Auth::ContainerRegistryAuthenticationService.prepend_mod_with('Auth::ContainerRegistryAuthenticationService')