# frozen_string_literal: true module API # Internal access API module Internal class Base < Grape::API before { authenticate_by_gitlab_shell_token! } helpers ::API::Helpers::InternalHelpers helpers ::Gitlab::Identifier UNKNOWN_CHECK_RESULT_ERROR = 'Unknown check result'.freeze helpers do def response_with_status(code: 200, success: true, message: nil, **extra_options) status code { status: success, message: message }.merge(extra_options).compact end def lfs_authentication_url(project) # This is a separate method so that EE can alter its behaviour more # easily. project.http_url_to_repo end def ee_post_receive_response_hook(response) # Hook for EE to add messages end def check_allowed(params) # This is a separate method so that EE can alter its behaviour more # easily. # Stores some Git-specific env thread-safely env = parse_env Gitlab::Git::HookEnv.set(gl_repository, env) if project actor = Support::GitAccessActor.from_params(params) actor.update_last_used_at! access_checker = access_checker_for(actor, params[:protocol]) check_result = begin result = access_checker.check(params[:action], params[:changes]) @project ||= access_checker.project result rescue Gitlab::GitAccess::UnauthorizedError => e return response_with_status(code: 401, success: false, message: e.message) rescue Gitlab::GitAccess::TimeoutError => e return response_with_status(code: 503, success: false, message: e.message) rescue Gitlab::GitAccess::NotFoundError => e return response_with_status(code: 404, success: false, message: e.message) end log_user_activity(actor.user) case check_result when ::Gitlab::GitAccessResult::Success payload = { gl_repository: gl_repository, gl_project_path: gl_project_path, gl_id: Gitlab::GlId.gl_id(actor.user), gl_username: actor.username, git_config_options: [], gitaly: gitaly_payload(params[:action]), gl_console_messages: check_result.console_messages } # Custom option for git-receive-pack command receive_max_input_size = Gitlab::CurrentSettings.receive_max_input_size.to_i if receive_max_input_size > 0 payload[:git_config_options] << "receive.maxInputSize=#{receive_max_input_size.megabytes}" if Feature.enabled?(:gitaly_upload_pack_filter, project) payload[:git_config_options] << "uploadpack.allowFilter=true" << "uploadpack.allowAnySHA1InWant=true" end end response_with_status(**payload) when ::Gitlab::GitAccessResult::CustomAction response_with_status(code: 300, message: check_result.message, payload: check_result.payload) else response_with_status(code: 500, success: false, message: UNKNOWN_CHECK_RESULT_ERROR) end end end namespace 'internal' do # Check if git command is allowed for project # # Params: # key_id - ssh key id for Git over SSH # user_id - user id for Git over HTTP or over SSH in keyless SSH CERT mode # username - user name for Git over SSH in keyless SSH cert mode # protocol - Git access protocol being used, e.g. HTTP or SSH # project - project full_path (not path on disk) # action - git action (git-upload-pack or git-receive-pack) # changes - changes as "oldrev newrev ref", see Gitlab::ChangesList # check_ip - optional, only in EE version, may limit access to # group resources based on its IP restrictions post "/allowed" do # It was moved to a separate method so that EE can alter its behaviour more # easily. check_allowed(params) end # rubocop: disable CodeReuse/ActiveRecord post "/lfs_authenticate" do status 200 if params[:key_id] actor = Key.find(params[:key_id]) actor.update_last_used_at elsif params[:user_id] actor = User.find_by(id: params[:user_id]) raise ActiveRecord::RecordNotFound.new("No such user id!") unless actor else raise ActiveRecord::RecordNotFound.new("No key_id or user_id passed!") end Gitlab::LfsToken .new(actor) .authentication_payload(lfs_authentication_url(project)) end # rubocop: enable CodeReuse/ActiveRecord # # Get a ssh key using the fingerprint # # rubocop: disable CodeReuse/ActiveRecord get "/authorized_keys" do fingerprint = params.fetch(:fingerprint) do Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint end key = Key.find_by(fingerprint: fingerprint) not_found!("Key") if key.nil? present key, with: Entities::SSHKey end # rubocop: enable CodeReuse/ActiveRecord # # Discover user by ssh key, user id or username # get '/discover' do if params[:key_id] user = UserFinder.new(params[:key_id]).find_by_ssh_key_id elsif params[:username] user = UserFinder.new(params[:username]).find_by_username end present user, with: Entities::UserSafe end get "/check" do { api_version: API.version, gitlab_version: Gitlab::VERSION, gitlab_rev: Gitlab.revision, redis: redis_ping } end # rubocop: disable CodeReuse/ActiveRecord post '/two_factor_recovery_codes' do status 200 if params[:key_id] key = Key.find_by(id: params[:key_id]) if key key.update_last_used_at else break { 'success' => false, 'message' => 'Could not find the given key' } end if key.is_a?(DeployKey) break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' } end user = key.user unless user break { success: false, message: 'Could not find a user for the given key' } end elsif params[:user_id] user = User.find_by(id: params[:user_id]) unless user break { success: false, message: 'Could not find the given user' } end end unless user.two_factor_enabled? break { success: false, message: 'Two-factor authentication is not enabled for this user' } end codes = nil ::Users::UpdateService.new(current_user, user: user).execute! do |user| codes = user.generate_otp_backup_codes! end { success: true, recovery_codes: codes } end # rubocop: enable CodeReuse/ActiveRecord post '/pre_receive' do status 200 reference_counter_increased = Gitlab::ReferenceCounter.new(params[:gl_repository]).increase { reference_counter_increased: reference_counter_increased } end post "/notify_post_receive" do status 200 # TODO: Re-enable when Gitaly is processing the post-receive notification # return unless Gitlab::GitalyClient.enabled? # # begin # repository = wiki? ? project.wiki.repository : project.repository # Gitlab::GitalyClient::NotificationService.new(repository.raw_repository).post_receive # rescue GRPC::Unavailable => e # render_api_error!(e, 500) # end end post '/post_receive' do status 200 response = Gitlab::InternalPostReceive::Response.new user = identify(params[:identifier]) project = Gitlab::GlRepository.parse(params[:gl_repository]).first push_options = Gitlab::PushOptions.new(params[:push_options]) response.reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease PostReceive.perform_async(params[:gl_repository], params[:identifier], params[:changes], push_options.as_json) mr_options = push_options.get(:merge_request) if mr_options.present? message = process_mr_push_options(mr_options, project, user, params[:changes]) response.add_alert_message(message) end broadcast_message = BroadcastMessage.current&.last&.message response.add_alert_message(broadcast_message) response.add_merge_request_urls(merge_request_urls) # A user is not guaranteed to be returned; an orphaned write deploy # key could be used if user redirect_message = Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id) project_created_message = Gitlab::Checks::ProjectCreated.fetch_message(user.id, project.id) response.add_basic_message(redirect_message) response.add_basic_message(project_created_message) end ee_post_receive_response_hook(response) present response, with: Entities::InternalPostReceive::Response end end end end end API::Internal::Base.prepend_if_ee('EE::API::Internal::Base')