diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 17:36:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 17:36:54 +0300 |
commit | f61bb2a16a514b71bf33aabbbb999d6732016a24 (patch) | |
tree | 9548caa89e60b4f40b99bbd1dac030420b812aa8 /lib/api | |
parent | 35fc54e5d261f8898e390aea7c2f5ec5fdf0539d (diff) |
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc42
Diffstat (limited to 'lib/api')
65 files changed, 678 insertions, 235 deletions
diff --git a/lib/api/admin/sidekiq.rb b/lib/api/admin/sidekiq.rb index 7e561783685..d91d4a0d4d5 100644 --- a/lib/api/admin/sidekiq.rb +++ b/lib/api/admin/sidekiq.rb @@ -12,11 +12,11 @@ module API namespace 'queues' do desc 'Drop jobs matching the given metadata from the Sidekiq queue' params do - Labkit::Context::KNOWN_KEYS.each do |key| + Gitlab::ApplicationContext::KNOWN_KEYS.each do |key| optional key, type: String, allow_blank: false end - at_least_one_of(*Labkit::Context::KNOWN_KEYS) + at_least_one_of(*Gitlab::ApplicationContext::KNOWN_KEYS) end delete ':queue_name' do result = diff --git a/lib/api/api.rb b/lib/api/api.rb index f83a36068dd..a287ffbfcd8 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -59,7 +59,7 @@ module API project: -> { @project }, namespace: -> { @group }, runner: -> { @current_runner || @runner }, - caller_id: route.origin, + caller_id: api_endpoint.endpoint_id, remote_ip: request.ip, feature_category: feature_category ) @@ -293,6 +293,8 @@ module API mount ::API::Triggers mount ::API::Unleash mount ::API::UsageData + mount ::API::UsageDataQueries + mount ::API::UsageDataNonSqlMetrics mount ::API::UserCounts mount ::API::Users mount ::API::Variables diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 8641271f2df..8822a30d4a1 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -55,7 +55,7 @@ module API user = find_user_from_sources return unless user - if user.is_a?(User) && Feature.enabled?(:user_mode_in_session) + if user.is_a?(User) && Gitlab::CurrentSettings.admin_mode # Sessions are enforced to be unavailable for API calls, so ignore them for admin mode Gitlab::Auth::CurrentUserMode.bypass_session!(user.id) end @@ -236,7 +236,7 @@ module API def after # Use a Grape middleware since the Grape `after` blocks might run # before we are finished rendering the `Grape::Entity` classes - Gitlab::Auth::CurrentUserMode.reset_bypass_session! if Feature.enabled?(:user_mode_in_session) + Gitlab::Auth::CurrentUserMode.reset_bypass_session! if Gitlab::CurrentSettings.admin_mode # Explicit nil is needed or the api call return value will be overwritten nil diff --git a/lib/api/applications.rb b/lib/api/applications.rb index b883f83cc19..be482272b20 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -41,6 +41,8 @@ module API desc 'Delete an application' delete ':id' do application = ApplicationsFinder.new(params).execute + break not_found!('Application') unless application + application.destroy no_content! diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index fa75d012613..339c0e779f9 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -70,7 +70,7 @@ module API optional :variables, Array, desc: 'Array of variables available in the pipeline' end post ':id/pipeline' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42124') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20711') authorize! :create_pipeline, user_project diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 80d5e80e21e..c5249f1377b 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -245,7 +245,7 @@ module API job = authenticate_job! - result = ::Ci::CreateJobArtifactsService.new(job).authorize(artifact_type: params[:artifact_type], filesize: params[:filesize]) + result = ::Ci::JobArtifacts::CreateService.new(job).authorize(artifact_type: params[:artifact_type], filesize: params[:filesize]) if result[:status] == :success content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE @@ -284,7 +284,7 @@ module API artifacts = params[:file] metadata = params[:metadata] - result = ::Ci::CreateJobArtifactsService.new(job).execute(artifacts, params, metadata_file: metadata) + result = ::Ci::JobArtifacts::CreateService.new(job).execute(artifacts, params, metadata_file: metadata) if result[:status] == :success status :created diff --git a/lib/api/commits.rb b/lib/api/commits.rb index a24848082a9..bd9f83ac24c 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -186,16 +186,14 @@ module API use :pagination requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag' end - # rubocop: disable CodeReuse/ActiveRecord get ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do commit = user_project.commit(params[:sha]) not_found! 'Commit' unless commit - notes = commit.notes.order(:created_at) + notes = commit.notes.with_api_entity_associations.fresh present paginate(notes), with: Entities::CommitNote end - # rubocop: enable CodeReuse/ActiveRecord desc 'Cherry pick commit into a branch' do detail 'This feature was introduced in GitLab 8.15' @@ -372,7 +370,7 @@ module API current_user, project_id: user_project.id, commit_sha: commit.sha - ).execute + ).execute.with_api_entity_associations present paginate(commit_merge_requests), with: Entities::MergeRequestBasic end diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb index bd8d9b68858..115a6b8ac4f 100644 --- a/lib/api/composer_packages.rb +++ b/lib/api/composer_packages.rb @@ -161,6 +161,8 @@ module API not_found! unless metadata + track_package_event('pull_package', :composer) + send_git_archive unauthorized_user_project.repository, ref: metadata.target_sha, format: 'zip', append_sha: true end end diff --git a/lib/api/concerns/packages/nuget_endpoints.rb b/lib/api/concerns/packages/nuget_endpoints.rb index 53b778875fc..5364eeb1880 100644 --- a/lib/api/concerns/packages/nuget_endpoints.rb +++ b/lib/api/concerns/packages/nuget_endpoints.rb @@ -95,7 +95,7 @@ module API # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource params do - requires :q, type: String, desc: 'The search term' + optional :q, type: String, desc: 'The search term' optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX optional :prerelease, type: ::Grape::API::Boolean, desc: 'Include prerelease versions', default: true diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 0a541620c3a..9f0f569b711 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -44,7 +44,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord get ":id/deploy_keys" do - keys = user_project.deploy_keys_projects.preload(:deploy_key) + keys = user_project.deploy_keys_projects.preload(deploy_key: :user) present paginate(keys), with: Entities::DeployKeysProject end diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index d0c842bb19d..0a6ecf2919c 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -36,7 +36,9 @@ module API get ':id/deployments' do authorize! :read_deployment, user_project - deployments = DeploymentsFinder.new(params.merge(project: user_project)).execute + deployments = + DeploymentsFinder.new(params.merge(project: user_project)) + .execute.with_api_entity_associations present paginate(deployments), with: Entities::Deployment end diff --git a/lib/api/entities/basic_project_details.rb b/lib/api/entities/basic_project_details.rb index cf0b32bed26..2de49d6ed40 100644 --- a/lib/api/entities/basic_project_details.rb +++ b/lib/api/entities/basic_project_details.rb @@ -8,11 +8,10 @@ module API expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } # Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770 expose :tag_list do |project| - # project.tags.order(:name).pluck(:name) is the most suitable option - # to avoid loading all the ActiveRecord objects but, if we use it here - # it override the preloaded associations and makes a query - # (fixed in https://github.com/rails/rails/pull/25976). - project.tags.map(&:name).sort + # Tags is a preloaded association. If we perform then sorting + # through the database, it will trigger a new query, ending up + # in an N+1 if we have several projects + project.tags.pluck(:name).sort # rubocop:disable CodeReuse/ActiveRecord end expose :ssh_url_to_repo, :http_url_to_repo, :web_url, :readme_url diff --git a/lib/api/entities/clusters/agent.rb b/lib/api/entities/clusters/agent.rb new file mode 100644 index 00000000000..3b4538b81c2 --- /dev/null +++ b/lib/api/entities/clusters/agent.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + module Clusters + class Agent < Grape::Entity + expose :id + expose :project, with: Entities::ProjectIdentity, as: :config_project + end + end + end +end diff --git a/lib/api/entities/email.rb b/lib/api/entities/email.rb index 5ba425def3d..46ebc458bcd 100644 --- a/lib/api/entities/email.rb +++ b/lib/api/entities/email.rb @@ -3,7 +3,7 @@ module API module Entities class Email < Grape::Entity - expose :id, :email + expose :id, :email, :confirmed_at end end end diff --git a/lib/api/entities/job_request/job_info.rb b/lib/api/entities/job_request/job_info.rb index 09c13aa8471..a4bcc9726d0 100644 --- a/lib/api/entities/job_request/job_info.rb +++ b/lib/api/entities/job_request/job_info.rb @@ -4,7 +4,7 @@ module API module Entities module JobRequest class JobInfo < Grape::Entity - expose :name, :stage + expose :id, :name, :stage expose :project_id, :project_name end end diff --git a/lib/api/entities/namespace_existence.rb b/lib/api/entities/namespace_existence.rb new file mode 100644 index 00000000000..d93078ecdac --- /dev/null +++ b/lib/api/entities/namespace_existence.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class NamespaceExistence < Grape::Entity + expose :exists, :suggests + end + end +end diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index e332e5e40fa..690bc5d419d 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -127,15 +127,16 @@ module API # as `:tags` are defined as: `has_many :tags, through: :taggings` # N+1 is solved then by using `subject.tags.map(&:name)` # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/20555 - super(projects_relation).preload(:group) + super(projects_relation).preload(group: :namespace_settings) .preload(:ci_cd_settings) .preload(:project_setting) .preload(:container_expiration_policy) .preload(:auto_devops) + .preload(:service_desk_setting) .preload(project_group_links: { group: :route }, fork_network: :root_project, fork_network_member: :forked_from_project, - forked_from_project: [:route, :forks, :tags, namespace: :route]) + forked_from_project: [:route, :forks, :tags, :group, :project_feature, namespace: [:route, :owner]]) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/entities/project_import_failed_relation.rb b/lib/api/entities/project_import_failed_relation.rb index 16b26ad0efa..b8f842c1646 100644 --- a/lib/api/entities/project_import_failed_relation.rb +++ b/lib/api/entities/project_import_failed_relation.rb @@ -3,7 +3,11 @@ module API module Entities class ProjectImportFailedRelation < Grape::Entity - expose :id, :created_at, :exception_class, :exception_message, :source + expose :id, :created_at, :exception_class, :source + + expose :exception_message do |_| + nil + end expose :relation_key, as: :relation_name end diff --git a/lib/api/entities/user.rb b/lib/api/entities/user.rb index 248a86751d2..3ce6d03e236 100644 --- a/lib/api/entities/user.rb +++ b/lib/api/entities/user.rb @@ -11,10 +11,10 @@ module API work_information(user) end expose :followers, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } do |user| - user.followers.count + user.followers.size end expose :following, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } do |user| - user.followees.count + user.followees.size end end end diff --git a/lib/api/entities/user_preferences.rb b/lib/api/entities/user_preferences.rb new file mode 100644 index 00000000000..7a6df9b6c59 --- /dev/null +++ b/lib/api/entities/user_preferences.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class UserPreferences < Grape::Entity + expose :id, :user_id, :view_diffs_file_by_file + end + end +end diff --git a/lib/api/entities/user_public.rb b/lib/api/entities/user_public.rb index 15e9b905bef..685adb1dd10 100644 --- a/lib/api/entities/user_public.rb +++ b/lib/api/entities/user_public.rb @@ -14,6 +14,7 @@ module API expose :two_factor_enabled?, as: :two_factor_enabled expose :external expose :private_profile + expose :commit_email end end end diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 3e1e430c2f9..b606b2e814d 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -26,7 +26,7 @@ module API get ':id/environments' do authorize! :read_environment, user_project - environments = ::EnvironmentsFinder.new(user_project, current_user, params).find + environments = ::EnvironmentsFinder.new(user_project, current_user, params).execute present paginate(environments), with: Entities::Environment, current_user: current_user end diff --git a/lib/api/files.rb b/lib/api/files.rb index cb73bde73f5..f3de7fbe96b 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -113,7 +113,7 @@ module API desc 'Get raw file metadata from repository' params do requires :file_path, type: String, file_path: true, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb' - requires :ref, type: String, desc: 'The name of branch, tag or commit', allow_blank: false + optional :ref, type: String, desc: 'The name of branch, tag or commit', allow_blank: false end head ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do assign_file_vars! @@ -124,7 +124,7 @@ module API desc 'Get raw file contents from the repository' params do requires :file_path, type: String, file_path: true, desc: 'The url encoded path to the file. Ex. lib%2Fclass%2Erb' - requires :ref, type: String, desc: 'The name of branch, tag commit', allow_blank: false + optional :ref, type: String, desc: 'The name of branch, tag or commit', allow_blank: false end get ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS do assign_file_vars! diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index 3d0ba97b51a..cce55fa92d9 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -62,7 +62,7 @@ module API authorize_upload!(project) bad_request!('File is too large') if max_file_size_exceeded? - track_event('push_package') + ::Gitlab::Tracking.event(self.options[:for].name, 'push_package') create_package_file_params = declared_params.merge(build: current_authenticated_job) ::Packages::Generic::CreatePackageFileService @@ -94,7 +94,7 @@ module API package = ::Packages::Generic::PackageFinder.new(project).execute!(params[:package_name], params[:package_version]) package_file = ::Packages::PackageFileFinder.new(package, params[:file_name]).execute! - track_event('pull_package') + ::Gitlab::Tracking.event(self.options[:for].name, 'pull_package') present_carrierwave_file!(package_file.file) end diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index 09744fbeda2..8d52a0a5b4e 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -8,6 +8,8 @@ module API before { authorize! :admin_group, user_group } feature_category :continuous_integration + helpers Helpers::VariablesHelpers + params do requires :id, type: String, desc: 'The ID of a group' end @@ -30,16 +32,13 @@ module API params do requires :key, type: String, desc: 'The key of the variable' end - # rubocop: disable CodeReuse/ActiveRecord get ':id/variables/:key' do - key = params[:key] - variable = user_group.variables.find_by(key: key) + variable = find_variable(user_group, params) break not_found!('GroupVariable') unless variable present variable, with: Entities::Ci::Variable end - # rubocop: enable CodeReuse/ActiveRecord desc 'Create a new variable in a group' do success Entities::Ci::Variable @@ -50,12 +49,19 @@ module API optional :protected, type: String, desc: 'Whether the variable is protected' optional :masked, type: String, desc: 'Whether the variable is masked' optional :variable_type, type: String, values: ::Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var' + + use :optional_group_variable_params_ee end post ':id/variables' do + filtered_params = filter_variable_parameters( + user_group, + declared_params(include_missing: false) + ) + variable = ::Ci::ChangeVariableService.new( container: user_group, current_user: current_user, - params: { action: :create, variable_params: declared_params(include_missing: false) } + params: { action: :create, variable_params: filtered_params } ).execute if variable.valid? @@ -74,13 +80,19 @@ module API optional :protected, type: String, desc: 'Whether the variable is protected' optional :masked, type: String, desc: 'Whether the variable is masked' optional :variable_type, type: String, values: ::Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file' + + use :optional_group_variable_params_ee end - # rubocop: disable CodeReuse/ActiveRecord put ':id/variables/:key' do + filtered_params = filter_variable_parameters( + user_group, + declared_params(include_missing: false) + ) + variable = ::Ci::ChangeVariableService.new( container: user_group, current_user: current_user, - params: { action: :update, variable_params: declared_params(include_missing: false) } + params: { action: :update, variable_params: filtered_params } ).execute if variable.valid? @@ -91,7 +103,6 @@ module API rescue ::ActiveRecord::RecordNotFound not_found!('GroupVariable') end - # rubocop: enable CodeReuse/ActiveRecord desc 'Delete an existing variable from a group' do success Entities::Ci::Variable @@ -99,21 +110,18 @@ module API params do requires :key, type: String, desc: 'The key of the variable' end - # rubocop: disable CodeReuse/ActiveRecord delete ':id/variables/:key' do - variable = user_group.variables.find_by!(key: params[:key]) + variable = find_variable(user_group, params) + break not_found!('GroupVariable') unless variable destroy_conditionally!(variable) do |target_variable| ::Ci::ChangeVariableService.new( container: user_group, current_user: current_user, - params: { action: :destroy, variable_params: declared_params(include_missing: false) } + params: { action: :destroy, variable: variable } ).execute end - rescue ::ActiveRecord::RecordNotFound - not_found!('GroupVariable') end - # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 26fa00d6186..912813d5bb7 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -52,9 +52,7 @@ module API groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? order_options = { params[:order_by] => params[:sort] } order_options["id"] ||= "asc" - groups = groups.reorder(order_options) - - groups + groups.reorder(order_options) end # rubocop: enable CodeReuse/ActiveRecord @@ -112,7 +110,6 @@ module API end def delete_group(group) - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/46285') destroy_conditionally!(group) do |group| ::Groups::DestroyService.new(group, current_user).async_execute end @@ -141,6 +138,10 @@ module API def authorize_group_creation! authorize! :create_group end + + def check_subscription!(group) + render_api_error!("This group can't be removed because it is linked to a subscription.", :bad_request) if group.paid? + end end resource :groups do @@ -239,6 +240,7 @@ module API delete ":id" do group = find_group!(params[:id]) authorize! :admin_group, group + check_subscription! group delete_group(group) end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 9db4a03c5b9..2d8a4f60e2a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -3,6 +3,7 @@ module API module Helpers include Gitlab::Utils + include Helpers::Caching include Helpers::Pagination include Helpers::PaginationStrategies @@ -48,7 +49,11 @@ module API # Returns the job associated with the token provided for # authentication, if any def current_authenticated_job - @current_authenticated_job + if try(:namespace_inheritable, :authentication) + ci_build_from_namespace_inheritable + else + @current_authenticated_job # rubocop:disable Gitlab/ModuleWithInstanceVariables + end end # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -539,17 +544,6 @@ module API end end - def track_event(action = action_name, **args) - category = args.delete(:category) || self.options[:for].name - raise "invalid category" unless category - - ::Gitlab::Tracking.event(category, action.to_s, **args) - rescue => error - Gitlab::AppLogger.warn( - "Tracking event failed for action: #{action}, category: #{category}, message: #{error.message}" - ) - end - def increment_counter(event_name) feature_name = "usage_data_#{event_name}" return unless Feature.enabled?(feature_name) @@ -564,10 +558,6 @@ module API def increment_unique_values(event_name, values) return unless values.present? - feature_flag = "usage_data_#{event_name}" - - return unless Feature.enabled?(feature_flag, default_enabled: true) - Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name, values: values) rescue => error Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}") diff --git a/lib/api/helpers/authentication.rb b/lib/api/helpers/authentication.rb index a6cfe930190..da11f07485b 100644 --- a/lib/api/helpers/authentication.rb +++ b/lib/api/helpers/authentication.rb @@ -52,6 +52,11 @@ module API token&.user end + def ci_build_from_namespace_inheritable + token = token_from_namespace_inheritable + token if token.is_a?(::Ci::Build) + end + private def find_token_from_raw_credentials(token_types, raw) diff --git a/lib/api/helpers/caching.rb b/lib/api/helpers/caching.rb new file mode 100644 index 00000000000..d0f22109879 --- /dev/null +++ b/lib/api/helpers/caching.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +# Grape helpers for caching. +# +# This module helps introduce standardised caching into the Grape API +# in a similar manner to the standard Grape DSL. + +module API + module Helpers + module Caching + # @return [ActiveSupport::Duration] + DEFAULT_EXPIRY = 1.day + + # @return [ActiveSupport::Cache::Store] + def cache + Rails.cache + end + + # This is functionally equivalent to the standard `#present` used in + # Grape endpoints, but the JSON for the object, or for each object of + # a collection, will be cached. + # + # With a collection all the keys will be fetched in a single call and the + # Entity rendered for those missing from the cache, which are then written + # back into it. + # + # Both the single object, and all objects inside a collection, must respond + # to `#cache_key`. + # + # To override the Grape formatter we return a custom wrapper in + # `Gitlab::Json::PrecompiledJson` which tells the `Gitlab::Json::GrapeFormatter` + # to export the string without conversion. + # + # A cache context can be supplied to add more context to the cache key. This + # defaults to including the `current_user` in every key for safety, unless overridden. + # + # @param obj_or_collection [Object, Enumerable<Object>] the object or objects to render + # @param with [Grape::Entity] the entity to use for rendering + # @param cache_context [Proc] a proc to call for each object to provide more context to the cache key + # @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry + # @param presenter_args [Hash] keyword arguments to be passed to the entity + # @return [Gitlab::Json::PrecompiledJson] + def present_cached(obj_or_collection, with:, cache_context: -> (_) { current_user.cache_key }, expires_in: DEFAULT_EXPIRY, **presenter_args) + json = + if obj_or_collection.is_a?(Enumerable) + cached_collection( + obj_or_collection, + presenter: with, + presenter_args: presenter_args, + context: cache_context, + expires_in: expires_in + ) + else + cached_object( + obj_or_collection, + presenter: with, + presenter_args: presenter_args, + context: cache_context, + expires_in: expires_in + ) + end + + body Gitlab::Json::PrecompiledJson.new(json) + end + + private + + # Optionally uses a `Proc` to add context to a cache key + # + # @param object [Object] must respond to #cache_key + # @param context [Proc] a proc that will be called with the object as an argument, and which should return a + # string or array of strings to be combined into the cache key + # @return [String] + def contextual_cache_key(object, context) + return object.cache_key if context.nil? + + [object.cache_key, context.call(object)].flatten.join(":") + end + + # Used for fetching or rendering a single object + # + # @param object [Object] the object to render + # @param presenter [Grape::Entity] + # @param presenter_args [Hash] keyword arguments to be passed to the entity + # @param context [Proc] + # @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry + # @return [String] + def cached_object(object, presenter:, presenter_args:, context:, expires_in:) + cache.fetch(contextual_cache_key(object, context), expires_in: expires_in) do + Gitlab::Json.dump(presenter.represent(object, **presenter_args).as_json) + end + end + + # Used for fetching or rendering multiple objects + # + # @param objects [Enumerable<Object>] the objects to render + # @param presenter [Grape::Entity] + # @param presenter_args [Hash] keyword arguments to be passed to the entity + # @param context [Proc] + # @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry + # @return [Array<String>] + def cached_collection(collection, presenter:, presenter_args:, context:, expires_in:) + json = fetch_multi(collection, context: context, expires_in: expires_in) do |obj| + Gitlab::Json.dump(presenter.represent(obj, **presenter_args).as_json) + end + + json.values + end + + # An adapted version of ActiveSupport::Cache::Store#fetch_multi. + # + # The original method only provides the missing key to the block, + # not the missing object, so we have to create a map of cache keys + # to the objects to allow us to pass the object to the missing value + # block. + # + # The result is that this is functionally identical to `#fetch`. + def fetch_multi(*objs, context:, **kwargs) + objs.flatten! + map = multi_key_map(objs, context: context) + + cache.fetch_multi(*map.keys, **kwargs) do |key| + yield map[key] + end + end + + # @param objects [Enumerable<Object>] objects which _must_ respond to `#cache_key` + # @param context [Proc] a proc that can be called to help generate each cache key + # @return [Hash] + def multi_key_map(objects, context:) + objects.index_by do |object| + contextual_cache_key(object, context) + end + end + end + end +end diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb index a44fd4b0a5b..8940cf87f82 100644 --- a/lib/api/helpers/common_helpers.rb +++ b/lib/api/helpers/common_helpers.rb @@ -32,6 +32,10 @@ module API end end.compact.to_set end + + def endpoint_id + "#{request.request_method} #{route.origin}" + end end end end diff --git a/lib/api/helpers/graphql_helpers.rb b/lib/api/helpers/graphql_helpers.rb index 3ddef0c16b3..4f7f85bd69d 100644 --- a/lib/api/helpers/graphql_helpers.rb +++ b/lib/api/helpers/graphql_helpers.rb @@ -6,8 +6,8 @@ module API # against the graphql API. Helper code for the graphql server implementation # should be in app/graphql/ or lib/gitlab/graphql/ module GraphqlHelpers - def run_graphql!(query:, context: {}, transform: nil) - result = GitlabSchema.execute(query, context: context) + def run_graphql!(query:, context: {}, variables: nil, transform: nil) + result = GitlabSchema.execute(query, variables: variables, context: context) if transform transform.call(result) diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index 71a18524104..cb938bc8a14 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -116,7 +116,7 @@ module API end def create_note(noteable, opts) - whitelist_query_limiting + disable_query_limiting authorize!(:create_note, noteable) parent = noteable_parent(noteable) @@ -144,8 +144,8 @@ module API present discussion, with: Entities::Discussion end - def whitelist_query_limiting - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/-/issues/211538') + def disable_query_limiting + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/211538') end end end diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb index d5f5448fd42..b18f52b5be6 100644 --- a/lib/api/helpers/packages/conan/api_helpers.rb +++ b/lib/api/helpers/packages/conan/api_helpers.rb @@ -14,7 +14,8 @@ module API package, current_user, project, - conan_package_reference: params[:conan_package_reference] + conan_package_reference: params[:conan_package_reference], + id: params[:id] ) render_api_error!("No recipe manifest found", 404) if yield(presenter).empty? @@ -31,19 +32,15 @@ module API end def recipe_upload_urls - { upload_urls: Hash[ - file_names.select(&method(:recipe_file?)).map do |file_name| - [file_name, build_recipe_file_upload_url(file_name)] - end - ] } + { upload_urls: file_names.select(&method(:recipe_file?)).to_h do |file_name| + [file_name, build_recipe_file_upload_url(file_name)] + end } end def package_upload_urls - { upload_urls: Hash[ - file_names.select(&method(:package_file?)).map do |file_name| - [file_name, build_package_file_upload_url(file_name)] - end - ] } + { upload_urls: file_names.select(&method(:package_file?)).to_h do |file_name| + [file_name, build_package_file_upload_url(file_name)] + end } end def recipe_file?(file_name) @@ -212,10 +209,8 @@ module API end def find_personal_access_token - personal_access_token = find_personal_access_token_from_conan_jwt || + find_personal_access_token_from_conan_jwt || find_personal_access_token_from_http_basic_auth - - personal_access_token end def find_user_from_job_token diff --git a/lib/api/helpers/packages/dependency_proxy_helpers.rb b/lib/api/helpers/packages/dependency_proxy_helpers.rb index 577ba97d68a..989c4e1761b 100644 --- a/lib/api/helpers/packages/dependency_proxy_helpers.rb +++ b/lib/api/helpers/packages/dependency_proxy_helpers.rb @@ -10,7 +10,7 @@ module API def redirect_registry_request(forward_to_registry, package_type, options) if forward_to_registry && redirect_registry_request_available? - track_event("#{package_type}_request_forward") + ::Gitlab::Tracking.event(self.options[:for].name, "#{package_type}_request_forward") redirect(registry_url(package_type, options)) else yield diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index e1898d28ef7..2221eec0f82 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -50,7 +50,8 @@ module API def track_package_event(event_name, scope, **args) ::Packages::CreateEventService.new(nil, current_user, event_name: event_name, scope: scope).execute - track_event(event_name, **args) + category = args.delete(:category) || self.options[:for].name + ::Gitlab::Tracking.event(category, event_name.to_s, **args) end end end diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 39586483990..688cd2da994 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -38,10 +38,17 @@ module API end end + # HTTP status codes to terminate the job on GitLab Runner: + # - 403 def authenticate_job!(require_running: true) job = current_job - not_found! unless job + # 404 is not returned here because we want to terminate the job if it's + # running. A 404 can be returned from anywhere in the networking stack which is why + # we are explicit about a 403, we should improve this in + # https://gitlab.com/gitlab-org/gitlab/-/issues/327703 + forbidden! unless job + forbidden! unless job_token_valid?(job) forbidden!('Project has been deleted!') if job.project.nil? || job.project.pending_delete? diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb index ed3d694f006..2f2ad88c942 100644 --- a/lib/api/helpers/services_helpers.rb +++ b/lib/api/helpers/services_helpers.rb @@ -394,7 +394,7 @@ module API required: true, name: :external_wiki_url, type: String, - desc: 'The URL of the external Wiki' + desc: 'The URL of the external wiki' } ], 'flowdock' => [ @@ -543,9 +543,15 @@ module API }, { required: false, + name: :jira_issue_transition_automatic, + type: Boolean, + desc: 'Enable automatic issue transitions' + }, + { + required: false, name: :jira_issue_transition_id, type: String, - desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the Jira workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`' + desc: 'The ID of one or more transitions for custom issue transitions' }, { required: false, diff --git a/lib/api/helpers/variables_helpers.rb b/lib/api/helpers/variables_helpers.rb new file mode 100644 index 00000000000..e2b3372fc33 --- /dev/null +++ b/lib/api/helpers/variables_helpers.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module API + module Helpers + module VariablesHelpers + extend ActiveSupport::Concern + extend Grape::API::Helpers + + params :optional_group_variable_params_ee do + end + + def filter_variable_parameters(_, params) + params # Overridden in EE + end + + def find_variable(owner, params) + variables = ::Ci::VariablesFinder.new(owner, params).execute.to_a + + return variables.first unless variables.many? # rubocop: disable CodeReuse/ActiveRecord + + conflict!("There are multiple variables with provided parameters. Please use 'filter[environment_scope]'") + end + end + end +end + +API::Helpers::VariablesHelpers.prepend_if_ee('EE::API::Helpers::VariablesHelpers') diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index a3fee49cd8f..4dcfc0cf7eb 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -15,7 +15,7 @@ module API Gitlab::ApplicationContext.push( user: -> { actor&.user }, project: -> { project }, - caller_id: route.origin, + caller_id: api_endpoint.endpoint_id, remote_ip: request.ip, feature_category: feature_category ) @@ -23,7 +23,7 @@ module API helpers ::API::Helpers::InternalHelpers - UNKNOWN_CHECK_RESULT_ERROR = 'Unknown check result'.freeze + UNKNOWN_CHECK_RESULT_ERROR = 'Unknown check result' VALID_PAT_SCOPES = Set.new( Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth::REGISTRY_SCOPES @@ -52,20 +52,20 @@ module API actor.update_last_used_at! check_result = begin - Gitlab::Auth::CurrentUserMode.bypass_session!(actor.user&.id) do - access_check!(actor, params) - end - rescue Gitlab::GitAccess::ForbiddenError => e - # The return code needs to be 401. If we return 403 - # the custom message we return won't be shown to the user - # and, instead, the default message 'GitLab: API is not accessible' - # will be displayed - 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 + with_admin_mode_bypass!(actor.user&.id) do + access_check!(actor, params) + end + rescue Gitlab::GitAccess::ForbiddenError => e + # The return code needs to be 401. If we return 403 + # the custom message we return won't be shown to the user + # and, instead, the default message 'GitLab: API is not accessible' + # will be displayed + 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) @@ -109,9 +109,7 @@ module API end end - def validate_actor_key(actor, key_id) - return 'Could not find a user without a key' unless key_id - + 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 @@ -120,6 +118,19 @@ module API def two_factor_otp_check { success: false, message: 'Feature is not available' } end + + def with_admin_mode_bypass!(actor_id) + return yield unless Gitlab::CurrentSettings.admin_mode + + Gitlab::Auth::CurrentUserMode.bypass_session!(actor_id) do + yield + end + end + + # Overridden in EE + def geo_proxy + {} + end end namespace 'internal' do @@ -193,7 +204,7 @@ module API actor.update_last_used_at! user = actor.user - error_message = validate_actor_key(actor, params[:key_id]) + error_message = validate_actor(actor) if params[:user_id] && user.nil? break { success: false, message: 'Could not find the given user' } @@ -222,7 +233,7 @@ module API actor.update_last_used_at! user = actor.user - error_message = validate_actor_key(actor, params[:key_id]) + 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) @@ -295,7 +306,7 @@ module API actor.update_last_used_at! user = actor.user - error_message = validate_actor_key(actor, params[:key_id]) + error_message = validate_actor(actor) if error_message { success: false, message: error_message } @@ -314,6 +325,12 @@ module API two_factor_otp_check end + + # Workhorse calls this to determine if it is a Geo secondary site + # that should proxy requests. FOSS can quickly return empty data. + get '/geo_proxy', feature_category: :geo_replication do + geo_proxy + end end end end diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index 87ad79d601f..af2c53dd778 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -13,7 +13,7 @@ module API helpers do def authenticate_gitlab_kas_request! - unauthorized! unless Gitlab::Kas.verify_api_request(headers) + render_api_error!('KAS JWT authentication invalid', 401) unless Gitlab::Kas.verify_api_request(headers) end def agent_token @@ -51,9 +51,11 @@ module API end def check_agent_token - forbidden! unless agent_token + unauthorized! unless agent_token forbidden! unless Gitlab::Kas.included_in_gitlab_com_rollout?(agent.project) + + agent_token.track_usage end end diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb index 52c32b4d1cf..0d562cc18f8 100644 --- a/lib/api/invitations.rb +++ b/lib/api/invitations.rb @@ -25,11 +25,11 @@ module API optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' end post ":id/invitations" do - source = find_source(source_type, params[:id]) + params[:source] = find_source(source_type, params[:id]) - authorize_admin_source!(source_type, source) + authorize_admin_source!(source_type, params[:source]) - ::Members::InviteService.new(current_user, params).execute(source) + ::Members::InviteService.new(current_user, params).execute end desc 'Get a list of group or project invitations viewable by the authenticated user' do diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb index e938dbbae87..1cd5bde224b 100644 --- a/lib/api/issue_links.rb +++ b/lib/api/issue_links.rb @@ -18,7 +18,10 @@ module API end get ':id/issues/:issue_iid/links' do source_issue = find_project_issue(params[:issue_iid]) - related_issues = source_issue.related_issues(current_user) + related_issues = source_issue.related_issues(current_user) do |issues| + issues.with_api_entity_associations.preload_awardable + end + related_issues.each { |issue| issue.lazy_subscription(current_user, user_project) } # preload subscriptions present related_issues, with: Entities::RelatedIssue, diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 13dac1c174c..4f2ac73c0d3 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -242,7 +242,7 @@ module API use :issue_params end post ':id/issues' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42320') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/21140') check_rate_limit! :issues_create, [current_user] @@ -288,7 +288,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord put ':id/issues/:issue_iid' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42322') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20775') issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) authorize! :update_issue, issue @@ -346,7 +346,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord post ':id/issues/:issue_iid/move' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42323') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20776') issue = user_project.issues.find_by(iid: params[:issue_iid]) not_found!('Issue') unless issue diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index 7390219b60e..54951f9bd01 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -6,8 +6,6 @@ module API before { authenticate! } - feature_category :continuous_integration - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do params do requires :id, type: String, desc: 'The ID of a project' @@ -40,7 +38,7 @@ module API use :pagination end # rubocop: disable CodeReuse/ActiveRecord - get ':id/jobs' do + get ':id/jobs', feature_category: :continuous_integration do authorize_read_builds! builds = user_project.builds.order('id DESC') @@ -57,7 +55,7 @@ module API params do requires :job_id, type: Integer, desc: 'The ID of a job' end - get ':id/jobs/:job_id' do + get ':id/jobs/:job_id', feature_category: :continuous_integration do authorize_read_builds! build = find_build!(params[:job_id]) @@ -72,7 +70,7 @@ module API params do requires :job_id, type: Integer, desc: 'The ID of a job' end - get ':id/jobs/:job_id/trace' do + get ':id/jobs/:job_id/trace', feature_category: :continuous_integration do authorize_read_builds! build = find_build!(params[:job_id]) @@ -94,7 +92,7 @@ module API params do requires :job_id, type: Integer, desc: 'The ID of a job' end - post ':id/jobs/:job_id/cancel' do + post ':id/jobs/:job_id/cancel', feature_category: :continuous_integration do authorize_update_builds! build = find_build!(params[:job_id]) @@ -111,7 +109,7 @@ module API params do requires :job_id, type: Integer, desc: 'The ID of a build' end - post ':id/jobs/:job_id/retry' do + post ':id/jobs/:job_id/retry', feature_category: :continuous_integration do authorize_update_builds! build = find_build!(params[:job_id]) @@ -129,7 +127,7 @@ module API params do requires :job_id, type: Integer, desc: 'The ID of a build' end - post ':id/jobs/:job_id/erase' do + post ':id/jobs/:job_id/erase', feature_category: :continuous_integration do authorize_update_builds! build = find_build!(params[:job_id]) @@ -148,7 +146,7 @@ module API requires :job_id, type: Integer, desc: 'The ID of a Job' end - post ":id/jobs/:job_id/play" do + post ":id/jobs/:job_id/play", feature_category: :continuous_integration do authorize_read_builds! job = find_job!(params[:job_id]) @@ -174,10 +172,8 @@ module API success Entities::Ci::Job end route_setting :authentication, job_token_allowed: true - get do - # current_authenticated_job will be nil if user is using - # a valid authentication that is not CI_JOB_TOKEN - not_found!('Job') unless current_authenticated_job + get '', feature_category: :continuous_integration do + validate_current_authenticated_job present current_authenticated_job, with: Entities::Ci::Job end @@ -196,6 +192,14 @@ module API builds.where(status: available_statuses && scope) end # rubocop: enable CodeReuse/ActiveRecord + + def validate_current_authenticated_job + # current_authenticated_job will be nil if user is using + # a valid authentication (like PRIVATE-TOKEN) that is not CI_JOB_TOKEN + not_found!('Job') unless current_authenticated_job + end end end end + +API::Jobs.prepend_if_ee('EE::API::Jobs') diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index 4a5b2ead163..bd1d984719e 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -23,6 +23,15 @@ module API helpers ::API::Helpers::PackagesHelpers helpers do + def path_exists?(path) + # return true when FF disabled so that processing the request is not stopped + return true unless Feature.enabled?(:check_maven_path_first) + return false if path.blank? + + Packages::Maven::Metadatum.with_path(path) + .exists? + end + def extract_format(file_name) name, _, format = file_name.rpartition('.') @@ -77,6 +86,22 @@ module API request.head? && file.fog_credentials[:provider] == 'AWS' end + + def fetch_package(file_name:, project: nil, group: nil) + order_by_package_file = false + if Feature.enabled?(:maven_packages_group_level_improvements, default_enabled: :yaml) + order_by_package_file = file_name.include?(::Packages::Maven::Metadata.filename) && + !params[:path].include?(::Packages::Maven::FindOrCreatePackageService::SNAPSHOT_TERM) + end + + ::Packages::Maven::PackageFinder.new( + params[:path], + current_user, + project: project, + group: group, + order_by_package_file: order_by_package_file + ).execute! + end end desc 'Download the maven package file at instance level' do @@ -88,6 +113,9 @@ module API end route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true get 'packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do + # return a similar failure to authorize_read_package!(project) + forbidden! unless path_exists?(params[:path]) + file_name, format = extract_format(params[:file_name]) # To avoid name collision we require project path and project package be the same. @@ -97,8 +125,7 @@ module API authorize_read_package!(project) - package = ::Packages::Maven::PackageFinder - .new(params[:path], current_user, project: project).execute! + package = fetch_package(file_name: file_name, project: project) package_file = ::Packages::PackageFileFinder .new(package, file_name).execute! @@ -127,14 +154,16 @@ module API end route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do + # return a similar failure to group = find_group(params[:id]) + not_found!('Group') unless path_exists?(params[:path]) + file_name, format = extract_format(params[:file_name]) group = find_group(params[:id]) not_found!('Group') unless can?(current_user, :read_group, group) - package = ::Packages::Maven::PackageFinder - .new(params[:path], current_user, group: group).execute! + package = fetch_package(file_name: file_name, group: group) authorize_read_package!(package.project) @@ -167,12 +196,14 @@ module API end route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do + # return a similar failure to user_project + not_found!('Project') unless path_exists?(params[:path]) + authorize_read_package!(user_project) file_name, format = extract_format(params[:file_name]) - package = ::Packages::Maven::PackageFinder - .new(params[:path], current_user, project: user_project).execute! + package = fetch_package(file_name: file_name, project: user_project) package_file = ::Packages::PackageFileFinder .new(package, file_name).execute! diff --git a/lib/api/members.rb b/lib/api/members.rb index 42f608102b3..aaf0e3e1927 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -100,9 +100,9 @@ module API authorize_admin_source!(source_type, source) if params[:user_id].to_s.include?(',') - create_service_params = params.except(:user_id).merge({ user_ids: params[:user_id] }) + create_service_params = params.except(:user_id).merge({ user_ids: params[:user_id], source: source }) - ::Members::CreateService.new(current_user, create_service_params).execute(source) + ::Members::CreateService.new(current_user, create_service_params).execute elsif params[:user_id].present? member = source.members.find_by(user_id: params[:user_id]) conflict!('Member already exists') if member @@ -155,6 +155,8 @@ module API desc 'Removes a user from a group or project.' params do requires :user_id, type: Integer, desc: 'The user ID of the member' + optional :skip_subresources, type: Boolean, default: false, + desc: 'Flag indicating if the deletion of direct memberships of the removed member in subgroups and projects should be skipped' optional :unassign_issuables, type: Boolean, default: false, desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project' end @@ -164,7 +166,7 @@ module API member = source_members(source).find_by!(user_id: params[:user_id]) destroy_conditionally!(member) do - ::Members::DestroyService.new(current_user).execute(member, unassign_issuables: params[:unassign_issuables]) + ::Members::DestroyService.new(current_user).execute(member, skip_subresources: params[:skip_subresources], unassign_issuables: params[:unassign_issuables]) end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index 97a6c7075b3..470f78a7dc2 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -45,7 +45,7 @@ module API merge_request = find_merge_request_with_access(params[:merge_request_iid]) - present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull + present_cached merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull, cache_context: nil end end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 5051c1a5529..613de514ffa 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -8,11 +8,20 @@ module API before { authenticate_non_get! } - feature_category :code_review - helpers Helpers::MergeRequestsHelpers helpers Helpers::SSEHelpers + # These endpoints are defined in `TimeTrackingEndpoints` and is shared by + # API::Issues. In order to be able to define the feature category of these + # endpoints, we need to define them at the top-level by route. + feature_category :code_review, [ + '/projects/:id/merge_requests/:merge_request_iid/time_estimate', + '/projects/:id/merge_requests/:merge_request_iid/reset_time_estimate', + '/projects/:id/merge_requests/:merge_request_iid/add_spent_time', + '/projects/:id/merge_requests/:merge_request_iid/reset_spent_time', + '/projects/:id/merge_requests/:merge_request_iid/time_stats' + ] + # EE::API::MergeRequests would override the following helpers helpers do params :optional_params_ee do @@ -125,7 +134,7 @@ module API use :merge_requests_params use :optional_scope_param end - get do + get feature_category: :code_review do authenticate! unless params[:scope] == 'all' merge_requests = find_merge_requests @@ -145,7 +154,7 @@ module API optional :non_archived, type: Boolean, desc: 'Return merge requests from non archived projects', default: true end - get ":id/merge_requests" do + get ":id/merge_requests", feature_category: :code_review do merge_requests = find_merge_requests(group_id: user_group.id, include_subgroups: true) present merge_requests, serializer_options_for(merge_requests).merge(group: user_group) @@ -184,7 +193,7 @@ module API use :merge_requests_params optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of merge requests' end - get ":id/merge_requests" do + get ":id/merge_requests", feature_category: :code_review do authorize! :read_merge_request, user_project merge_requests = find_merge_requests(project_id: user_project.id) @@ -206,8 +215,8 @@ module API desc: 'The target project of the merge request defaults to the :id of the project' use :optional_params end - post ":id/merge_requests" do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42316') + post ":id/merge_requests", feature_category: :code_review do + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20770') authorize! :create_merge_request_from, user_project @@ -228,7 +237,7 @@ module API params do requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' end - delete ":id/merge_requests/:merge_request_iid" do + delete ":id/merge_requests/:merge_request_iid", feature_category: :code_review do merge_request = find_project_merge_request(params[:merge_request_iid]) authorize!(:destroy_merge_request, merge_request) @@ -247,7 +256,7 @@ module API desc 'Get a single merge request' do success Entities::MergeRequest end - get ':id/merge_requests/:merge_request_iid' do + get ':id/merge_requests/:merge_request_iid', feature_category: :code_review do not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project) merge_request = find_merge_request_with_access(params[:merge_request_iid]) @@ -265,7 +274,7 @@ module API desc 'Get the participants of a merge request' do success Entities::UserBasic end - get ':id/merge_requests/:merge_request_iid/participants' do + get ':id/merge_requests/:merge_request_iid/participants', feature_category: :code_review do not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project) merge_request = find_merge_request_with_access(params[:merge_request_iid]) @@ -278,7 +287,7 @@ module API desc 'Get the commits of a merge request' do success Entities::Commit end - get ':id/merge_requests/:merge_request_iid/commits' do + get ':id/merge_requests/:merge_request_iid/commits', feature_category: :code_review do not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project) merge_request = find_merge_request_with_access(params[:merge_request_iid]) @@ -293,7 +302,7 @@ module API desc 'Get the context commits of a merge request' do success Entities::Commit end - get ':id/merge_requests/:merge_request_iid/context_commits' do + get ':id/merge_requests/:merge_request_iid/context_commits', feature_category: :code_review do merge_request = find_merge_request_with_access(params[:merge_request_iid]) project = merge_request.project @@ -311,7 +320,7 @@ module API desc 'create context commits of merge request' do success Entities::Commit end - post ':id/merge_requests/:merge_request_iid/context_commits' do + post ':id/merge_requests/:merge_request_iid/context_commits', feature_category: :code_review do commit_ids = params[:commits] if commit_ids.size > CONTEXT_COMMITS_POST_LIMIT @@ -339,7 +348,7 @@ module API requires :commits, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, allow_blank: false, desc: 'List of context commits sha' end desc 'remove context commits of merge request' - delete ':id/merge_requests/:merge_request_iid/context_commits' do + delete ':id/merge_requests/:merge_request_iid/context_commits', feature_category: :code_review do commit_ids = params[:commits] merge_request = find_merge_request_with_access(params[:merge_request_iid]) project = merge_request.project @@ -361,7 +370,7 @@ module API desc 'Show the merge request changes' do success Entities::MergeRequestChanges end - get ':id/merge_requests/:merge_request_iid/changes' do + get ':id/merge_requests/:merge_request_iid/changes', feature_category: :code_review do not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project) merge_request = find_merge_request_with_access(params[:merge_request_iid]) @@ -376,7 +385,7 @@ module API desc 'Get the merge request pipelines' do success Entities::Ci::PipelineBasic end - get ':id/merge_requests/:merge_request_iid/pipelines' do + get ':id/merge_requests/:merge_request_iid/pipelines', feature_category: :continuous_integration do pipelines = merge_request_pipelines_with_access not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project) @@ -387,7 +396,7 @@ module API desc 'Create a pipeline for merge request' do success ::API::Entities::Ci::Pipeline end - post ':id/merge_requests/:merge_request_iid/pipelines' do + post ':id/merge_requests/:merge_request_iid/pipelines', feature_category: :continuous_integration do pipeline = ::MergeRequests::CreatePipelineService .new(user_project, current_user, allow_duplicate: true) .execute(find_merge_request_with_access(params[:merge_request_iid])) @@ -415,8 +424,8 @@ module API use :optional_params at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of) end - put ':id/merge_requests/:merge_request_iid' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42318') + put ':id/merge_requests/:merge_request_iid', feature_category: :code_review do + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20772') merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request) @@ -424,7 +433,13 @@ module API mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params.has_key?(:remove_source_branch) mr_params = convert_parameters_from_legacy_format(mr_params) - merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) + service = if mr_params.one? && (mr_params.keys & %i[assignee_id assignee_ids]).one? + ::MergeRequests::UpdateAssigneesService + else + ::MergeRequests::UpdateService + end + + merge_request = service.new(user_project, current_user, mr_params).execute(merge_request) handle_merge_request_errors!(merge_request) @@ -444,8 +459,8 @@ module API optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' end - put ':id/merge_requests/:merge_request_iid/merge' do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42317') + put ':id/merge_requests/:merge_request_iid/merge', feature_category: :code_review do + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/4796') merge_request = find_project_merge_request(params[:merge_request_iid]) @@ -485,7 +500,7 @@ module API end desc 'Returns the up to date merge-ref HEAD commit' - get ':id/merge_requests/:merge_request_iid/merge_ref' do + get ':id/merge_requests/:merge_request_iid/merge_ref', feature_category: :code_review do merge_request = find_project_merge_request(params[:merge_request_iid]) result = ::MergeRequests::MergeabilityCheckService.new(merge_request).execute(recheck: true) @@ -500,7 +515,7 @@ module API desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do success Entities::MergeRequest end - post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do + post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds', feature_category: :code_review do merge_request = find_project_merge_request(params[:merge_request_iid]) unauthorized! unless merge_request.can_cancel_auto_merge?(current_user) @@ -514,7 +529,7 @@ module API params do optional :skip_ci, type: Boolean, desc: 'Do not create CI pipeline' end - put ':id/merge_requests/:merge_request_iid/rebase' do + put ':id/merge_requests/:merge_request_iid/rebase', feature_category: :code_review do merge_request = find_project_merge_request(params[:merge_request_iid]) authorize_push_to_merge_request!(merge_request) @@ -533,7 +548,7 @@ module API params do use :pagination end - get ':id/merge_requests/:merge_request_iid/closes_issues' do + get ':id/merge_requests/:merge_request_iid/closes_issues', feature_category: :code_review do merge_request = find_merge_request_with_access(params[:merge_request_iid]) issues = ::Kaminari.paginate_array(merge_request.visible_closing_issues_for(current_user)) issues = paginate(issues) diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb index b337b992841..d75ed3a48d7 100644 --- a/lib/api/milestone_responses.rb +++ b/lib/api/milestone_responses.rb @@ -80,7 +80,7 @@ module API params = build_finder_params(milestone, parent) - issuables = finder_klass.new(current_user, params).execute + issuables = finder_klass.new(current_user, params).execute.with_api_entity_associations present paginate(issuables), with: entity, current_user: current_user end diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index 25a901c18b6..465d2f23e9d 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -56,6 +56,23 @@ module API present user_namespace, with: Entities::Namespace, current_user: current_user end + + desc 'Get existence of a namespace including alternative suggestions' do + success Entities::NamespaceExistence + end + params do + requires :namespace, type: String, desc: "Namespace's path" + optional :parent_id, type: Integer, desc: "The ID of the parent namespace. If no ID is specified, only top-level namespaces are considered." + end + get ':namespace/exists', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + namespace_path = params[:namespace] + + exists = Namespace.by_parent(params[:parent_id]).filter_by_path(namespace_path).exists? + suggestions = exists ? [Namespace.clean_path(namespace_path)] : [] + + present :exists, exists + present :suggests, suggestions + end end end end diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 15b06cea385..5f3a574eeee 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -67,7 +67,7 @@ module API check_rate_limit! :project_import, [current_user, :project_import] - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42437') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20823') validate_file! diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 19b63c28f89..92f6970e6fc 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -13,6 +13,8 @@ module API feature_category :projects, ['/projects/:id/custom_attributes', '/projects/:id/custom_attributes/:key'] + PROJECT_ATTACHMENT_SIZE_EXEMPT = 1.gigabyte + helpers do # EE::API::Projects would override this method def apply_filters(projects) @@ -52,6 +54,29 @@ module API accepted! end + + def exempt_from_global_attachment_size?(user_project) + list = ::Gitlab::RackAttack::UserAllowlist.new(ENV['GITLAB_UPLOAD_API_ALLOWLIST']) + list.include?(user_project.id) + end + + # Temporarily introduced for upload API: https://gitlab.com/gitlab-org/gitlab/-/issues/325788 + def project_attachment_size(user_project) + return PROJECT_ATTACHMENT_SIZE_EXEMPT if exempt_from_global_attachment_size?(user_project) + return user_project.max_attachment_size if Feature.enabled?(:enforce_max_attachment_size_upload_api, user_project) + + PROJECT_ATTACHMENT_SIZE_EXEMPT + end + + # This is to help determine which projects to use in https://gitlab.com/gitlab-org/gitlab/-/issues/325788 + def log_if_upload_exceed_max_size(user_project, file) + return if file.size <= user_project.max_attachment_size + + if file.size > user_project.max_attachment_size + allowed = exempt_from_global_attachment_size?(user_project) + Gitlab::AppLogger.info({ message: "File exceeds maximum size", file_bytes: file.size, project_id: user_project.id, project_path: user_project.full_path, upload_allowed: allowed }) + end + end end helpers do @@ -215,7 +240,7 @@ module API use :create_params end post do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/21139') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/issues/21139') attrs = declared_params(include_missing: false) attrs = translate_params_for_compatibility(attrs) filter_attributes_using_license!(attrs) @@ -248,7 +273,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord post "user/:user_id", feature_category: :projects do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/21139') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/issues/21139') authenticated_as_admin! user = User.find_by(id: params.delete(:user_id)) not_found!('User') unless user @@ -310,7 +335,7 @@ module API optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the fork' end post ':id/fork', feature_category: :source_code_management do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42284') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20759') not_found! unless can?(current_user, :fork_project, user_project) @@ -460,7 +485,7 @@ module API get ':id/languages', feature_category: :source_code_management do ::Projects::RepositoryLanguagesService .new(user_project, current_user) - .execute.map { |lang| [lang.name, lang.share] }.to_h + .execute.to_h { |lang| [lang.name, lang.share] } end desc 'Delete a project' @@ -545,13 +570,27 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + desc 'Workhorse authorize the file upload' do + detail 'This feature was introduced in GitLab 13.11' + end + post ':id/uploads/authorize', feature_category: :not_owned do + require_gitlab_workhorse! + + status 200 + content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE + FileUploader.workhorse_authorize(has_length: false, maximum_size: project_attachment_size(user_project)) + end + desc 'Upload a file' params do - # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 - requires :file, type: File, desc: 'The file to be uploaded' # rubocop:disable Scalability/FileUploads + requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded' end post ":id/uploads", feature_category: :not_owned do - upload = UploadService.new(user_project, params[:file]).execute + log_if_upload_exceed_max_size(user_project, params[:file]) + + service = UploadService.new(user_project, params[:file]) + service.override_max_attachment_size = project_attachment_size(user_project) + upload = service.execute present upload, with: Entities::ProjectUpload end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index f6ffeeea829..033cc6744b0 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -116,10 +116,23 @@ module API params do requires :from, type: String, desc: 'The commit, branch name, or tag name to start comparison' requires :to, type: String, desc: 'The commit, branch name, or tag name to stop comparison' + optional :from_project_id, type: String, desc: 'The project to compare from' optional :straight, type: Boolean, desc: 'Comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)', default: false end get ':id/repository/compare' do - compare = CompareService.new(user_project, params[:to]).execute(user_project, params[:from], straight: params[:straight]) + if params[:from_project_id].present? + target_project = MergeRequestTargetProjectFinder + .new(current_user: current_user, source_project: user_project, project_feature: :repository) + .execute(include_routes: true).find_by_id(params[:from_project_id]) + + if target_project.blank? + render_api_error!("Target project id:#{params[:from_project_id]} is not a fork of project id:#{params[:id]}", 400) + end + else + target_project = user_project + end + + compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight]) if compare present compare, with: Entities::Compare diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb index 99c278be8e7..705e4778c83 100644 --- a/lib/api/resource_access_tokens.rb +++ b/lib/api/resource_access_tokens.rb @@ -19,7 +19,7 @@ module API get ":id/access_tokens" do resource = find_source(source_type, params[:id]) - next unauthorized! unless has_permission_to_read?(resource) + next unauthorized! unless current_user.can?(:read_resource_access_tokens, resource) tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute @@ -85,10 +85,6 @@ module API def find_token(resource, token_id) PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).find_by_id(token_id) end - - def has_permission_to_read?(resource) - can?(current_user, :project_bot_access, resource) || can?(current_user, :admin_resource_access_tokens, resource) - end end end end diff --git a/lib/api/rubygem_packages.rb b/lib/api/rubygem_packages.rb index 8d2d4586d8d..1d17148e0df 100644 --- a/lib/api/rubygem_packages.rb +++ b/lib/api/rubygem_packages.rb @@ -99,6 +99,8 @@ module API track_package_event('push_package', :rubygems) + package_file = nil + ActiveRecord::Base.transaction do package = ::Packages::CreateTemporaryPackageService.new( user_project, current_user, declared_params.merge(build: current_authenticated_job) @@ -109,12 +111,18 @@ module API file_name: PACKAGE_FILENAME } - ::Packages::CreatePackageFileService.new( + package_file = ::Packages::CreatePackageFileService.new( package, file_params.merge(build: current_authenticated_job) ).execute end - created! + if package_file + ::Packages::Rubygems::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker + + created! + else + bad_request!('Package creation failed') + end rescue ObjectStorage::RemoteStoreError => e Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: user_project.id }) diff --git a/lib/api/search.rb b/lib/api/search.rb index f0ffe6ba443..8fabf379d49 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -22,13 +22,15 @@ module API users: Entities::UserBasic }.freeze - SCOPE_PRELOAD_METHOD = { - merge_requests: :with_api_entity_associations, - projects: :with_api_entity_associations, - issues: :with_api_entity_associations, - milestones: :with_api_entity_associations, - commits: :with_api_commit_entity_associations - }.freeze + def scope_preload_method + { + merge_requests: :with_api_entity_associations, + projects: :with_api_entity_associations, + issues: :with_api_entity_associations, + milestones: :with_api_entity_associations, + commits: :with_api_commit_entity_associations + }.freeze + end def search(additional_params = {}) search_params = { @@ -60,7 +62,7 @@ module API end def preload_method - SCOPE_PRELOAD_METHOD[params[:scope].to_sym] + scope_preload_method[params[:scope].to_sym] end def verify_search_scope!(resource:) diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 64a72b4cb7f..95d0c525ced 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -30,6 +30,7 @@ module API success Entities::ApplicationSetting end params do + optional :admin_mode, type: Boolean, desc: 'Require admin users to re-authenticate for administrative (i.e. potentially dangerous) operations' optional :admin_notification_email, type: String, desc: 'Deprecated: Use :abuse_notification_email instead. Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.' optional :abuse_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.' optional :after_sign_up_text, type: String, desc: 'Text shown after sign up' diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 7636c45bdac..e77d7e34de3 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -28,7 +28,13 @@ module API sort: "#{params[:order_by]}_#{params[:sort]}", search: params[:search]).execute - present paginate(::Kaminari.paginate_array(tags)), with: Entities::Tag, project: user_project + paginated_tags = paginate(::Kaminari.paginate_array(tags)) + + if Feature.enabled?(:api_caching_tags, user_project, type: :development) + present_cached paginated_tags, with: Entities::Tag, project: user_project, cache_context: -> (_tag) { user_project.cache_key } + else + present paginated_tags, with: Entities::Tag, project: user_project + end end desc 'Get a single repository tag' do diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index aebbc95cbea..84c51e5aeac 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -21,7 +21,7 @@ module API optional :variables, type: Hash, desc: 'The list of variables to be injected into build' end post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42283') + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20758') forbidden! if gitlab_pipeline_hook_request? diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb index c7d63f8d6ac..7deec15dcac 100644 --- a/lib/api/usage_data.rb +++ b/lib/api/usage_data.rb @@ -2,24 +2,22 @@ module API class UsageData < ::API::Base - before { authenticate! } + before { authenticate_non_get! } feature_category :usage_ping namespace 'usage_data' do before do - not_found! unless Feature.enabled?(:usage_data_api, default_enabled: true) + not_found! unless Feature.enabled?(:usage_data_api, default_enabled: :yaml, type: :ops) forbidden!('Invalid CSRF token is provided') unless verified_request? end desc 'Track usage data events' do detail 'This feature was introduced in GitLab 13.4.' end - params do requires :event, type: String, desc: 'The event name that should be tracked' end - post 'increment_counter' do event_name = params[:event] @@ -31,7 +29,6 @@ module API params do requires :event, type: String, desc: 'The event name that should be tracked' end - post 'increment_unique_users' do event_name = params[:event] @@ -39,6 +36,16 @@ module API status :ok end + + desc 'Get a list of all metric definitions' do + detail 'This feature was introduced in GitLab 13.11.' + end + get 'metric_definitions' do + content_type 'application/yaml' + env['api.format'] = :binary + + Gitlab::Usage::MetricDefinition.dump_metrics_yaml + end end end end diff --git a/lib/api/usage_data_non_sql_metrics.rb b/lib/api/usage_data_non_sql_metrics.rb new file mode 100644 index 00000000000..63a14a223f5 --- /dev/null +++ b/lib/api/usage_data_non_sql_metrics.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module API + class UsageDataNonSqlMetrics < ::API::Base + before { authenticated_as_admin! } + + feature_category :usage_ping + + namespace 'usage_data' do + before do + not_found! unless Feature.enabled?(:usage_data_non_sql_metrics, default_enabled: :yaml, type: :ops) + end + + desc 'Get Non SQL usage ping metrics' do + detail 'This feature was introduced in GitLab 13.11.' + end + + get 'non_sql_metrics' do + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/325534') + + data = Gitlab::UsageDataNonSqlMetrics.uncached_data + + present data + end + end + end +end diff --git a/lib/api/usage_data_queries.rb b/lib/api/usage_data_queries.rb new file mode 100644 index 00000000000..0ad9ad7650c --- /dev/null +++ b/lib/api/usage_data_queries.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module API + class UsageDataQueries < ::API::Base + before { authenticated_as_admin! } + + feature_category :usage_ping + + namespace 'usage_data' do + before do + not_found! unless Feature.enabled?(:usage_data_queries_api, default_enabled: :yaml, type: :ops) + end + + desc 'Get raw SQL queries for usage data SQL metrics' do + detail 'This feature was introduced in GitLab 13.11.' + end + + get 'queries' do + Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/325534') + + queries = Gitlab::UsageDataQueries.uncached_data + + present queries + end + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index b2f99bb18dc..078ba7542a3 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -231,7 +231,7 @@ module API optional :password, type: String, desc: 'The password of the new user' optional :reset_password, type: Boolean, desc: 'Flag indicating the user will be sent a password reset token' optional :skip_confirmation, type: Boolean, desc: 'Flag indicating the account is confirmed' - at_least_one_of :password, :reset_password + at_least_one_of :password, :reset_password, :force_random_password requires :name, type: String, desc: 'The name of the user' requires :username, type: String, desc: 'The username of the user' optional :force_random_password, type: Boolean, desc: 'Flag indicating a random password will be set' @@ -571,8 +571,6 @@ module API end # rubocop: disable CodeReuse/ActiveRecord delete ":id", feature_category: :users do - Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/20757') - authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -998,6 +996,29 @@ module API present paginate(current_user.emails), with: Entities::Email end + desc "Update the current user's preferences" do + success Entities::UserPreferences + detail 'This feature was introduced in GitLab 13.10.' + end + params do + requires :view_diffs_file_by_file, type: Boolean, desc: 'Flag indicating the user sees only one file diff per page' + end + put "preferences", feature_category: :users do + authenticate! + + preferences = current_user.user_preference + + attrs = declared_params(include_missing: false) + + service = ::UserPreferences::UpdateService.new(current_user, attrs).execute + + if service.success? + present preferences, with: Entities::UserPreferences + else + render_api_error!('400 Bad Request', 400) + end + end + desc 'Get a single email address owned by the currently authenticated user' do success Entities::Email end diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb index 2d25e76626a..29e4a79110f 100644 --- a/lib/api/v3/github.rb +++ b/lib/api/v3/github.rb @@ -18,7 +18,7 @@ module API # Used to differentiate Jira Cloud requests from Jira Server requests # Jira Cloud user agent format: Jira DVCS Connector Vertigo/version # Jira Server user agent format: Jira DVCS Connector/version - JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo'.freeze + JIRA_DVCS_CLOUD_USER_AGENT = 'Jira DVCS Connector Vertigo' include PaginationParams @@ -75,11 +75,14 @@ module API # rubocop: enable CodeReuse/ActiveRecord def authorized_merge_requests - MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?).execute + MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?) + .execute.with_jira_integration_associations end def authorized_merge_requests_for_project(project) - MergeRequestsFinder.new(current_user, authorized_only: !current_user.admin?, project_id: project.id).execute + MergeRequestsFinder + .new(current_user, authorized_only: !current_user.admin?, project_id: project.id) + .execute.with_jira_integration_associations end # rubocop: disable CodeReuse/ActiveRecord @@ -194,16 +197,13 @@ module API # Self-hosted Jira (tested on 7.11.1) requests this endpoint right # after fetching branches. - # rubocop: disable CodeReuse/ActiveRecord get ':namespace/:project/events' do user_project = find_project_with_access(params) merge_requests = authorized_merge_requests_for_project(user_project) - merge_requests = merge_requests.preload(:author, :assignees, :metrics, source_project: :namespace, target_project: :namespace) present paginate(merge_requests), with: ::API::Github::Entities::PullRequestEvent end - # rubocop: enable CodeReuse/ActiveRecord params do use :project_full_path diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 94fa98b7a14..8b0745c6b5b 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -9,21 +9,7 @@ module API feature_category :continuous_integration - helpers do - def filter_variable_parameters(params) - # This method exists so that EE can more easily filter out certain - # parameters, without having to modify the source code directly. - params - end - - def find_variable(params) - variables = ::Ci::VariablesFinder.new(user_project, params).execute.to_a - - return variables.first unless variables.many? # rubocop: disable CodeReuse/ActiveRecord - - conflict!("There are multiple variables with provided parameters. Please use 'filter[environment_scope]'") - end - end + helpers Helpers::VariablesHelpers params do requires :id, type: String, desc: 'The ID of a project' @@ -49,7 +35,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord get ':id/variables/:key' do - variable = find_variable(params) + variable = find_variable(user_project, params) not_found!('Variable') unless variable present variable, with: Entities::Ci::Variable @@ -71,7 +57,7 @@ module API variable = ::Ci::ChangeVariableService.new( container: user_project, current_user: current_user, - params: { action: :create, variable_params: filter_variable_parameters(declared_params(include_missing: false)) } + params: { action: :create, variable_params: declared_params(include_missing: false) } ).execute if variable.valid? @@ -95,17 +81,13 @@ module API end # rubocop: disable CodeReuse/ActiveRecord put ':id/variables/:key' do - variable = find_variable(params) + variable = find_variable(user_project, params) not_found!('Variable') unless variable - variable_params = filter_variable_parameters( - declared_params(include_missing: false) - .except(:key, :filter) - ) variable = ::Ci::ChangeVariableService.new( container: user_project, current_user: current_user, - params: { action: :update, variable: variable, variable_params: variable_params } + params: { action: :update, variable: variable, variable_params: declared_params(include_missing: false).except(:key, :filter) } ).execute if variable.valid? @@ -125,7 +107,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord delete ':id/variables/:key' do - variable = find_variable(params) + variable = find_variable(user_project, params) not_found!('Variable') unless variable ::Ci::ChangeVariableService.new( |