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
2016-06-16 23:09:34 +05:30
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' )