# frozen_string_literal: true module API # Internal access API module Internal class Base < ::API::Base include Gitlab::RackLoadBalancingHelpers WORKHORSE_CIRCUIT_BREAKER_HEADER = 'Enable-Workhorse-Circuit-Breaker' before { authenticate_by_gitlab_shell_token! } before do api_endpoint = env['api.endpoint'] feature_category = api_endpoint.options[:for].try(:feature_category_for_app, api_endpoint).to_s add_gitaly_context(params) if actor.user load_balancer_stick_request(::User, :user, actor.user.id) set_current_organization(user: actor.user) link_scoped_user(params) end Gitlab::ApplicationContext.push( user: -> { actor&.user }, project: -> { project }, caller_id: api_endpoint.endpoint_id, remote_ip: request.ip, feature_category: feature_category ) end helpers ::API::Helpers::InternalHelpers VALID_PAT_SCOPES = Set.new( Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth::REGISTRY_SCOPES ).freeze helpers do def lfs_authentication_url(container) # This is a separate method so that EE can alter its behaviour more # easily. container.lfs_http_url_to_repo end def add_gitaly_context(params) params[:gitaly_context] = gitaly_context(params) end def link_scoped_user(params) context = params[:gitaly_context] return unless context scoped_user_id = context['scoped-user-id'] return unless scoped_user_id.present? scoped_user_id = scoped_user_id.to_i identity = ::Gitlab::Auth::Identity.link_from_scoped_user_id(actor.user, scoped_user_id) not_found!("User ID #{scoped_user_id} not found") unless identity end # rubocop: disable Metrics/AbcSize def check_allowed(params) # This is a separate method so that EE can alter its behaviour more # easily. check_rate_limit!(:gitlab_shell_operation, scope: [params[:action], params[:project], actor.key_or_user]) rate_limiter = Gitlab::Auth::IpRateLimiter.new(request.ip) unless rate_limiter.trusted_ip? check_rate_limit!(:gitlab_shell_operation, scope: [params[:action], params[:project], rate_limiter.ip]) end # Stores some Git-specific env thread-safely # # Snapshot repositories have different relative path than the main repository. For access # checks that need quarantined objects the relative path in also sent with Gitaly RPCs # calls as a header. Gitlab::Git::HookEnv.set(gl_repository, params[:relative_path], parse_env) if container actor.update_last_used_at! check_result = access_check_result return check_result if unsuccessful_response?(check_result) log_user_activity(actor.user) case check_result when ::Gitlab::GitAccessResult::Success payload = { gl_repository: gl_repository, gl_project_path: gl_repository_path, gl_project_id: project&.id, gl_root_namespace_id: project&.root_namespace&.id, gl_id: Gitlab::GlId.gl_id(actor.user), gl_username: actor.username, git_config_options: %w[uploadpack.allowFilter=true uploadpack.allowAnySHA1InWant=true], gitaly: gitaly_payload(params[:action]), gl_console_messages: check_result.console_messages, need_audit: need_git_audit_event? }.merge!(actor.key_details) # 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}" end unless Feature.enabled?(:log_git_streaming_audit_events, project) audit_message = { protocol: params[:protocol], action: params[:action] } # If the protocol is SSH, we need to send the original IP from the PROXY # protocol to the audit streaming event. The original IP from gitlab-shell # is set through the `check_ip` parameter. if include_ip_address_in_audit_event?(Gitlab::IpAddressState.current) audit_message[:ip_address] = Gitlab::IpAddressState.current end send_git_audit_streaming_event(audit_message) end response_with_status(**payload) when ::Gitlab::GitAccessResult::CustomAction response_with_status(code: 300, payload: check_result.payload, gl_console_messages: check_result.console_messages) else response_with_status(code: 500, success: false, message: ::API::Helpers::InternalHelpers::UNKNOWN_CHECK_RESULT_ERROR) end end # rubocop: enable Metrics/AbcSize def validate_actor(actor) return 'Could not find the given key' unless actor.key 'Could not find a user for the given key' unless actor.user end def two_factor_manual_otp_check { success: false, message: 'Feature is not available' } end def two_factor_push_otp_check { success: false, message: 'Feature is not available' } end def add_workhorse_circuit_breaker_header(params) return unless params[:protocol] == 'ssh' && request.headers['Gitlab-Shell-Api-Request'].present? header(WORKHORSE_CIRCUIT_BREAKER_HEADER, "true") 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) # relative_path - relative path of repository having access checks performed. # action - git action (git-upload-pack or git-receive-pack) # changes - changes as "oldrev newrev ref", see Gitlab::ChangesList # gitaly_client_context_bin - context provided by Gitaly client (base64 encoded JSON string) # check_ip - optional, only in EE version, may limit access to # group resources based on its IP restrictions # # /internal/allowed # post "/allowed", feature_category: :source_code_management do # It was moved to a separate method so that EE can alter its behaviour more # easily. add_workhorse_circuit_breaker_header(params) if Feature.enabled?(:workhorse_circuit_breaker, project) check_allowed(params) end # Validate LFS authentication request # # /internal/lfs_authenticate # post "/lfs_authenticate", feature_category: :source_code_management, urgency: :high do not_found! unless container&.lfs_enabled? status 200 unless actor.key_or_user raise ActiveRecord::RecordNotFound, 'User not found!' end actor.update_last_used_at! Gitlab::LfsToken .new(actor.key_or_user, container) .authentication_payload(lfs_authentication_url(container)) end # Check whether an SSH key is known to GitLab # # /internal/authorized_keys # get '/authorized_keys', feature_category: :source_code_management, urgency: :high do fingerprint = Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint_sha256 key = Key.auth.find_by_fingerprint_sha256(fingerprint) not_found!('Key') if key.nil? present key, with: Entities::SSHKey end # Discover user by ssh key, user id or username # # /internal/discover # get '/discover', feature_category: :system_access do present actor.user, with: Entities::UserSafe end # /internal/check # get '/check', feature_category: :not_owned do # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned { api_version: API.version, gitlab_version: Gitlab::VERSION, gitlab_rev: Gitlab.revision, redis: redis_ping } end # /internal/two_factor_recovery_codes # post '/two_factor_recovery_codes', feature_category: :system_access do status 200 actor.update_last_used_at! user = actor.user error_message = validate_actor(actor) if params[:user_id] && user.nil? break { success: false, message: 'Could not find the given user' } elsif error_message break { success: false, message: error_message } end break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' } if actor.key.is_a?(DeployKey) 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 # /internal/personal_access_token # post '/personal_access_token', feature_category: :system_access do status 200 actor.update_last_used_at! user = actor.user error_message = validate_actor(actor) break { success: false, message: 'Deploy keys cannot be used to create personal access tokens' } if actor.key.is_a?(DeployKey) if params[:user_id] && user.nil? break { success: false, message: 'Could not find the given user' } elsif error_message break { success: false, message: error_message } end if params[:name].blank? break { success: false, message: "No token name specified" } end if params[:scopes].blank? break { success: false, message: "No token scopes specified" } end invalid_scope = params[:scopes].find { |scope| VALID_PAT_SCOPES.exclude?(scope.to_sym) } if invalid_scope valid_scopes = VALID_PAT_SCOPES.map(&:to_s).sort break { success: false, message: "Invalid scope: '#{invalid_scope}'. Valid scopes are: #{valid_scopes}" } end begin expires_at = params[:expires_at].presence && Date.parse(params[:expires_at]) rescue ArgumentError break { success: false, message: "Invalid token expiry date: '#{params[:expires_at]}'" } end result = ::PersonalAccessTokens::CreateService.new( current_user: user, target_user: user, organization_id: Current.organization.id, params: { name: params[:name], scopes: params[:scopes], expires_at: expires_at } ).execute unless result.status == :success break { success: false, message: "Failed to create token: #{result.message}" } end access_token = result.payload[:personal_access_token] { success: true, token: access_token.token, scopes: access_token.scopes, expires_at: access_token.expires_at } end # /internal/pre_receive # post '/pre_receive', feature_category: :source_code_management do status 200 reference_counter_increased = Gitlab::ReferenceCounter.new(params[:gl_repository]).increase { reference_counter_increased: reference_counter_increased } end # /internal/post_receive # post '/post_receive', feature_category: :source_code_management do status 200 response = PostReceiveService.new(actor.user, repository, project, params).execute present response, with: Entities::InternalPostReceive::Response end # This endpoint was added in https://gitlab.com/gitlab-org/gitlab/-/issues/212308 # It was added with the plan to be used by GitLab PAM module but we # decided to pursue a different approach, so it's currently not used. # We might revive the PAM module though as it provides better user # flow. # # /internal/two_factor_config # post '/two_factor_config', feature_category: :system_access do status 200 break { success: false } unless Feature.enabled?(:two_factor_for_cli) actor.update_last_used_at! user = actor.user error_message = validate_actor(actor) if error_message { success: false, message: error_message } elsif actor.key.is_a?(DeployKey) { success: true, two_factor_required: false } else { success: true, two_factor_required: user.two_factor_enabled? } end end # /internal/two_factor_push_otp_check # post '/two_factor_push_otp_check', feature_category: :system_access do status 200 two_factor_push_otp_check end # /internal/two_factor_manual_otp_check # post '/two_factor_manual_otp_check', feature_category: :system_access do status 200 two_factor_manual_otp_check end end end end end API::Internal::Base.prepend_mod_with('API::Internal::Base')