diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 12:55:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-20 12:55:51 +0300 |
commit | e8d2c2579383897a1dd7f9debd359abe8ae8373d (patch) | |
tree | c42be41678c2586d49a75cabce89322082698334 /lib/api | |
parent | fc845b37ec3a90aaa719975f607740c22ba6a113 (diff) |
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'lib/api')
87 files changed, 1034 insertions, 580 deletions
diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb index 654d3a48162..0462878c90c 100644 --- a/lib/api/admin/ci/variables.rb +++ b/lib/api/admin/ci/variables.rb @@ -8,7 +8,7 @@ module API before { authenticated_as_admin! } - feature_category :continuous_integration + feature_category :pipeline_authoring namespace 'admin' do namespace 'ci' do diff --git a/lib/api/admin/plan_limits.rb b/lib/api/admin/plan_limits.rb index 92f7d3dce0d..ab6a4e4a04a 100644 --- a/lib/api/admin/plan_limits.rb +++ b/lib/api/admin/plan_limits.rb @@ -41,6 +41,7 @@ module API optional :npm_max_file_size, type: Integer, desc: 'Maximum NPM package file size in bytes' optional :nuget_max_file_size, type: Integer, desc: 'Maximum NuGet package file size in bytes' optional :pypi_max_file_size, type: Integer, desc: 'Maximum PyPI package file size in bytes' + optional :terraform_module_max_file_size, type: Integer, desc: 'Maximum Terraform Module package file size in bytes' end put "application/plan_limits" do params = declared_params(include_missing: false) diff --git a/lib/api/api.rb b/lib/api/api.rb index 2a3033753f7..f9e89191a36 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -152,6 +152,7 @@ module API mount ::API::Boards mount ::API::Branches mount ::API::BroadcastMessages + mount ::API::BulkImports mount ::API::Ci::Pipelines mount ::API::Ci::PipelineSchedules mount ::API::Ci::Runner @@ -166,12 +167,14 @@ module API mount ::API::Deployments mount ::API::Environments mount ::API::ErrorTracking + mount ::API::ErrorTrackingCollector mount ::API::Events mount ::API::FeatureFlags mount ::API::FeatureFlagsUserLists mount ::API::Features mount ::API::Files mount ::API::FreezePeriods + mount ::API::Geo mount ::API::GroupAvatar mount ::API::GroupBoards mount ::API::GroupClusters diff --git a/lib/api/bulk_imports.rb b/lib/api/bulk_imports.rb new file mode 100644 index 00000000000..189851cee65 --- /dev/null +++ b/lib/api/bulk_imports.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +module API + class BulkImports < ::API::Base + include PaginationParams + + feature_category :importers + + helpers do + def bulk_imports + @bulk_imports ||= ::BulkImports::ImportsFinder.new(user: current_user, status: params[:status]).execute + end + + def bulk_import + @bulk_import ||= bulk_imports.find(params[:import_id]) + end + + def bulk_import_entities + @bulk_import_entities ||= ::BulkImports::EntitiesFinder.new(user: current_user, bulk_import: bulk_import, status: params[:status]).execute + end + + def bulk_import_entity + @bulk_import_entity ||= bulk_import_entities.find(params[:entity_id]) + end + end + + before { authenticate! } + + resource :bulk_imports do + desc 'List all GitLab Migrations' do + detail 'This feature was introduced in GitLab 14.1.' + end + params do + use :pagination + optional :status, type: String, values: BulkImport.all_human_statuses, + desc: 'Return GitLab Migrations with specified status' + end + get do + present paginate(bulk_imports), with: Entities::BulkImport + end + + desc "List all GitLab Migrations' entities" do + detail 'This feature was introduced in GitLab 14.1.' + end + params do + use :pagination + optional :status, type: String, values: ::BulkImports::Entity.all_human_statuses, + desc: "Return all GitLab Migrations' entities with specified status" + end + get :entities do + entities = ::BulkImports::EntitiesFinder.new(user: current_user, status: params[:status]).execute + + present paginate(entities), with: Entities::BulkImports::Entity + end + + desc 'Get GitLab Migration details' do + detail 'This feature was introduced in GitLab 14.1.' + end + params do + requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration" + end + get ':import_id' do + present bulk_import, with: Entities::BulkImport + end + + desc "List GitLab Migration entities" do + detail 'This feature was introduced in GitLab 14.1.' + end + params do + requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration" + optional :status, type: String, values: ::BulkImports::Entity.all_human_statuses, + desc: 'Return import entities with specified status' + use :pagination + end + get ':import_id/entities' do + present paginate(bulk_import_entities), with: Entities::BulkImports::Entity + end + + desc 'Get GitLab Migration entity details' do + detail 'This feature was introduced in GitLab 14.1.' + end + params do + requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration" + requires :entity_id, type: Integer, desc: "The ID of GitLab Migration entity" + end + get ':import_id/entities/:entity_id' do + present bulk_import_entity, with: Entities::BulkImports::Entity + end + end + end +end diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index c4e0b699524..0bac6fe2054 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -7,11 +7,11 @@ module API content_type :txt, 'text/plain' - feature_category :continuous_integration + feature_category :runner resource :runners do desc 'Registers a new Runner' do - success Entities::RunnerRegistrationDetails + success Entities::Ci::RunnerRegistrationDetails http_codes [[201, 'Runner was created'], [403, 'Forbidden']] end params do @@ -34,10 +34,10 @@ module API if runner_registration_token_valid? # Create shared runner. Requires admin access attributes.merge(runner_type: :instance_type) - elsif @project = Project.find_by_runners_token(params[:token]) + elsif runner_registrar_valid?('project') && @project = Project.find_by_runners_token(params[:token]) # Create a specific runner for the project attributes.merge(runner_type: :project_type, projects: [@project]) - elsif @group = Group.find_by_runners_token(params[:token]) + elsif runner_registrar_valid?('group') && @group = Group.find_by_runners_token(params[:token]) # Create a specific runner for the group attributes.merge(runner_type: :group_type, groups: [@group]) else @@ -47,7 +47,7 @@ module API @runner = ::Ci::Runner.create(attributes) if @runner.persisted? - present @runner, with: Entities::RunnerRegistrationDetails + present @runner, with: Entities::Ci::RunnerRegistrationDetails else render_validation_error!(@runner) end @@ -82,7 +82,7 @@ module API before { set_application_context } desc 'Request a job' do - success Entities::JobRequest::Response + success Entities::Ci::JobRequest::Response http_codes [[201, 'Job was scheduled'], [204, 'No job for Runner'], [403, 'Forbidden']] @@ -214,6 +214,10 @@ module API .new(job, content_range: content_range) .execute(request.body.read) + if result.status == 403 + break error!('403 Forbidden', 403) + end + if result.status == 416 break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{result.stream_size}" }) end @@ -263,7 +267,7 @@ module API end desc 'Upload artifacts for job' do - success Entities::JobRequest::Response + success Entities::Ci::JobRequest::Response http_codes [[201, 'Artifact uploaded'], [400, 'Bad request'], [403, 'Forbidden'], diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index 44ffc941cfa..7f755b1a4d4 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -7,11 +7,11 @@ module API before { authenticate! } - feature_category :continuous_integration + feature_category :runner resource :runners do desc 'Get runners available for user' do - success Entities::Runner + success Entities::Ci::Runner end params do optional :scope, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES, @@ -30,11 +30,11 @@ module API runners = filter_runners(runners, params[:status], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES) runners = runners.tagged_with(params[:tag_list]) if params[:tag_list] - present paginate(runners), with: Entities::Runner + present paginate(runners), with: Entities::Ci::Runner end desc 'Get all runners - shared and specific' do - success Entities::Runner + success Entities::Ci::Runner end params do optional :scope, type: String, values: ::Ci::Runner::AVAILABLE_SCOPES, @@ -55,11 +55,11 @@ module API runners = filter_runners(runners, params[:status], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES) runners = runners.tagged_with(params[:tag_list]) if params[:tag_list] - present paginate(runners), with: Entities::Runner + present paginate(runners), with: Entities::Ci::Runner end desc "Get runner's details" do - success Entities::RunnerDetails + success Entities::Ci::RunnerDetails end params do requires :id, type: Integer, desc: 'The ID of the runner' @@ -68,11 +68,11 @@ module API runner = get_runner(params[:id]) authenticate_show_runner!(runner) - present runner, with: Entities::RunnerDetails, current_user: current_user + present runner, with: Entities::Ci::RunnerDetails, current_user: current_user end desc "Update runner's details" do - success Entities::RunnerDetails + success Entities::Ci::RunnerDetails end params do requires :id, type: Integer, desc: 'The ID of the runner' @@ -92,14 +92,14 @@ module API update_service = ::Ci::UpdateRunnerService.new(runner) if update_service.update(declared_params(include_missing: false)) - present runner, with: Entities::RunnerDetails, current_user: current_user + present runner, with: Entities::Ci::RunnerDetails, current_user: current_user else render_validation_error!(runner) end end desc 'Remove a runner' do - success Entities::Runner + success Entities::Ci::Runner end params do requires :id, type: Integer, desc: 'The ID of the runner' @@ -139,7 +139,7 @@ module API before { authorize_admin_project } desc 'Get runners available for project' do - success Entities::Runner + success Entities::Ci::Runner end params do optional :scope, type: String, values: ::Ci::Runner::AVAILABLE_SCOPES, @@ -158,11 +158,11 @@ module API runners = filter_runners(runners, params[:scope]) runners = apply_filter(runners, params) - present paginate(runners), with: Entities::Runner + present paginate(runners), with: Entities::Ci::Runner end desc 'Enable a runner for a project' do - success Entities::Runner + success Entities::Ci::Runner end params do requires :runner_id, type: Integer, desc: 'The ID of the runner' @@ -172,14 +172,14 @@ module API authenticate_enable_runner!(runner) if runner.assign_to(user_project) - present runner, with: Entities::Runner + present runner, with: Entities::Ci::Runner else render_validation_error!(runner) end end desc "Disable project's runner" do - success Entities::Runner + success Entities::Ci::Runner end params do requires :runner_id, type: Integer, desc: 'The ID of the runner' @@ -204,7 +204,7 @@ module API before { authorize_admin_group } desc 'Get runners available for group' do - success Entities::Runner + success Entities::Ci::Runner end params do optional :type, type: String, values: ::Ci::Runner::AVAILABLE_TYPES, @@ -218,7 +218,7 @@ module API runners = ::Ci::Runner.belonging_to_group(user_group.id, include_ancestors: true) runners = apply_filter(runners, params) - present paginate(runners), with: Entities::Runner + present paginate(runners), with: Entities::Ci::Runner end end diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 27fee7fdea2..1785362656e 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -99,40 +99,26 @@ module API updatable_optional_attributes = %w[target_url description coverage] status.assign_attributes(attributes_for_keys(updatable_optional_attributes)) - if status.valid? - status.update_older_statuses_retried! if Feature.enabled?(:ci_fix_commit_status_retried, user_project, default_enabled: :yaml) - else - render_validation_error!(status) - end + render_validation_error!(status) unless status.valid? - begin - case params[:state] - when 'pending' - status.enqueue! - when 'running' - status.enqueue - status.run! - when 'success' - status.success! - when 'failed' - status.drop!(:api_failure) - when 'canceled' - status.cancel! - else - render_api_error!('invalid state', 400) - end + response = ::Ci::Pipelines::AddJobService.new(pipeline).execute!(status) do |job| + apply_job_state!(job) + rescue ::StateMachines::InvalidTransition => e + render_api_error!(e.message, 400) + end - if pipeline.latest? - MergeRequest.where(source_project: user_project, source_branch: ref) - .update_all(head_pipeline_id: pipeline.id) - end + render_validation_error!(response.payload[:job]) unless response.success? - present status, with: Entities::CommitStatus - rescue StateMachines::InvalidTransition => e - render_api_error!(e.message, 400) + if pipeline.latest? + MergeRequest + .where(source_project: user_project, source_branch: ref) + .update_all(head_pipeline_id: pipeline.id) end + + present response.payload[:job], with: Entities::CommitStatus end # rubocop: enable CodeReuse/ActiveRecord + helpers do def commit strong_memoize(:commit) do @@ -146,6 +132,24 @@ module API pipelines = pipelines.for_id(params[:pipeline_id]) if params[:pipeline_id] pipelines end + + def apply_job_state!(job) + case params[:state] + when 'pending' + job.enqueue! + when 'running' + job.enqueue + job.run! + when 'success' + job.success! + when 'failed' + job.drop!(:api_failure) + when 'canceled' + job.cancel! + else + render_api_error!('invalid state', 400) + end + end end end end diff --git a/lib/api/concerns/packages/debian_package_endpoints.rb b/lib/api/concerns/packages/debian_package_endpoints.rb index c79ae3068b4..7740ba6bfa6 100644 --- a/lib/api/concerns/packages/debian_package_endpoints.rb +++ b/lib/api/concerns/packages/debian_package_endpoints.rb @@ -6,20 +6,17 @@ module API module DebianPackageEndpoints extend ActiveSupport::Concern - DISTRIBUTION_REGEX = %r{[a-zA-Z0-9][a-zA-Z0-9.-]*}.freeze - COMPONENT_REGEX = %r{[a-z-]+}.freeze - ARCHITECTURE_REGEX = %r{[a-z][a-z0-9]*}.freeze LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze PACKAGE_REGEX = API::NO_SLASH_URL_PART_REGEX DISTRIBUTION_REQUIREMENTS = { - distribution: DISTRIBUTION_REGEX + distribution: ::Packages::Debian::DISTRIBUTION_REGEX }.freeze COMPONENT_ARCHITECTURE_REQUIREMENTS = { - component: COMPONENT_REGEX, - architecture: ARCHITECTURE_REGEX + component: ::Packages::Debian::COMPONENT_REGEX, + architecture: ::Packages::Debian::ARCHITECTURE_REGEX }.freeze COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS = { - component: COMPONENT_REGEX, + component: ::Packages::Debian::COMPONENT_REGEX, letter: LETTER_REGEX, source_package: PACKAGE_REGEX }.freeze @@ -40,6 +37,14 @@ module API .sent_through(:http_basic_auth) end + helpers do + def present_release_file + distribution = ::Packages::Debian::DistributionsFinder.new(project_or_group, codename_or_suite: params[:distribution]).execute.last! + + present_carrierwave_file!(distribution.file) + end + end + format :txt content_type :txt, 'text/plain' @@ -65,8 +70,7 @@ module API route_setting :authentication, authenticate_non_public: true get 'Release' do - # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286 - 'TODO Release' + present_release_file end # GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease @@ -76,7 +80,8 @@ module API route_setting :authentication, authenticate_non_public: true get 'InRelease' do - not_found! + # Signature to be added in 7.3 of https://gitlab.com/groups/gitlab-org/-/epics/6057#note_582697034 + present_release_file end params do @@ -92,8 +97,20 @@ module API route_setting :authentication, authenticate_non_public: true get 'Packages' do - # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286 - 'TODO Packages' + relation = "::Packages::Debian::#{project_or_group.class.name}ComponentFile".constantize + + component_file = relation + .preload_distribution + .with_container(project_or_group) + .with_codename_or_suite(params[:distribution]) + .with_component_name(params[:component]) + .with_file_type(:packages) + .with_architecture_name(params[:architecture]) + .with_compression_type(nil) + .order_created_asc + .last! + + present_carrierwave_file!(component_file.file) end end end diff --git a/lib/api/debian_group_packages.rb b/lib/api/debian_group_packages.rb index c6116a8b28f..191ed42a5b8 100644 --- a/lib/api/debian_group_packages.rb +++ b/lib/api/debian_group_packages.rb @@ -24,6 +24,12 @@ module API end namespace ':id/-' do + helpers do + def project_or_group + user_group + end + end + include ::API::Concerns::Packages::DebianPackageEndpoints end end diff --git a/lib/api/entities/basic_project_details.rb b/lib/api/entities/basic_project_details.rb index c75b74b4368..0b231906ccd 100644 --- a/lib/api/entities/basic_project_details.rb +++ b/lib/api/entities/basic_project_details.rb @@ -6,7 +6,7 @@ module API include ::API::ProjectsRelationBuilder include Gitlab::Utils::StrongMemoize - expose :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } + expose :default_branch_or_main, as: :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 :topic_names, as: :tag_list diff --git a/lib/api/entities/bulk_import.rb b/lib/api/entities/bulk_import.rb new file mode 100644 index 00000000000..373ae486dcf --- /dev/null +++ b/lib/api/entities/bulk_import.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + class BulkImport < Grape::Entity + expose :id + expose :status_name, as: :status + expose :source_type + expose :created_at + expose :updated_at + end + end +end diff --git a/lib/api/entities/bulk_imports/entity.rb b/lib/api/entities/bulk_imports/entity.rb new file mode 100644 index 00000000000..e8c31256b17 --- /dev/null +++ b/lib/api/entities/bulk_imports/entity.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module Entities + module BulkImports + class Entity < Grape::Entity + expose :id + expose :bulk_import_id + expose :status_name, as: :status + expose :source_full_path + expose :destination_name + expose :destination_namespace + expose :parent_id + expose :namespace_id + expose :project_id + expose :created_at + expose :updated_at + expose :failures, using: EntityFailure + end + end + end +end diff --git a/lib/api/entities/bulk_imports/entity_failure.rb b/lib/api/entities/bulk_imports/entity_failure.rb new file mode 100644 index 00000000000..a3dbe3280ee --- /dev/null +++ b/lib/api/entities/bulk_imports/entity_failure.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + module BulkImports + class EntityFailure < Grape::Entity + expose :pipeline_class + expose :pipeline_step + expose :exception_class + expose :correlation_id_value + expose :created_at + end + end + end +end diff --git a/lib/api/entities/ci/job.rb b/lib/api/entities/ci/job.rb index 76487ed01dc..cf87684ce55 100644 --- a/lib/api/entities/ci/job.rb +++ b/lib/api/entities/ci/job.rb @@ -7,7 +7,7 @@ module API # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5) expose :artifacts_file, using: ::API::Entities::Ci::JobArtifactFile, if: -> (job, opts) { job.artifacts? } expose :job_artifacts, as: :artifacts, using: ::API::Entities::Ci::JobArtifact - expose :runner, with: ::API::Entities::Runner + expose :runner, with: ::API::Entities::Ci::Runner expose :artifacts_expire_at expose :tag_list do |job| job.tags.map(&:name).sort diff --git a/lib/api/entities/ci/job_request/artifacts.rb b/lib/api/entities/ci/job_request/artifacts.rb new file mode 100644 index 00000000000..4b09db40504 --- /dev/null +++ b/lib/api/entities/ci/job_request/artifacts.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class Artifacts < Grape::Entity + expose :name + expose :untracked + expose :paths + expose :exclude, expose_nil: false + expose :when + expose :expire_in + expose :artifact_type + expose :artifact_format + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/cache.rb b/lib/api/entities/ci/job_request/cache.rb new file mode 100644 index 00000000000..9820719b4f0 --- /dev/null +++ b/lib/api/entities/ci/job_request/cache.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class Cache < Grape::Entity + expose :key, :untracked, :paths, :policy, :when + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/credentials.rb b/lib/api/entities/ci/job_request/credentials.rb new file mode 100644 index 00000000000..57cdd9c9b19 --- /dev/null +++ b/lib/api/entities/ci/job_request/credentials.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class Credentials < Grape::Entity + expose :type, :url, :username, :password + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/dependency.rb b/lib/api/entities/ci/job_request/dependency.rb new file mode 100644 index 00000000000..2c6ed417714 --- /dev/null +++ b/lib/api/entities/ci/job_request/dependency.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class Dependency < Grape::Entity + expose :id, :name, :token + expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? } + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/git_info.rb b/lib/api/entities/ci/job_request/git_info.rb new file mode 100644 index 00000000000..872c896b870 --- /dev/null +++ b/lib/api/entities/ci/job_request/git_info.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class GitInfo < Grape::Entity + expose :repo_url, :ref, :sha, :before_sha + expose :ref_type + expose :refspecs + expose :git_depth, as: :depth + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/image.rb b/lib/api/entities/ci/job_request/image.rb new file mode 100644 index 00000000000..8e404a8fa02 --- /dev/null +++ b/lib/api/entities/ci/job_request/image.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class Image < Grape::Entity + expose :name, :entrypoint + expose :ports, using: Entities::Ci::JobRequest::Port + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/job_info.rb b/lib/api/entities/ci/job_request/job_info.rb new file mode 100644 index 00000000000..5c3f4b08af2 --- /dev/null +++ b/lib/api/entities/ci/job_request/job_info.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class JobInfo < Grape::Entity + expose :id, :name, :stage + expose :project_id, :project_name + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/port.rb b/lib/api/entities/ci/job_request/port.rb new file mode 100644 index 00000000000..31aa06ff843 --- /dev/null +++ b/lib/api/entities/ci/job_request/port.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class Port < Grape::Entity + expose :number, :protocol, :name + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/response.rb b/lib/api/entities/ci/job_request/response.rb new file mode 100644 index 00000000000..86c945cb236 --- /dev/null +++ b/lib/api/entities/ci/job_request/response.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class Response < Grape::Entity + expose :id + expose :token + expose :allow_git_fetch + + expose :job_info, using: Entities::Ci::JobRequest::JobInfo do |model| + model + end + + expose :git_info, using: Entities::Ci::JobRequest::GitInfo do |model| + model + end + + expose :runner_info, using: Entities::Ci::JobRequest::RunnerInfo do |model| + model + end + + expose :runner_variables, as: :variables + expose :steps, using: Entities::Ci::JobRequest::Step + expose :image, using: Entities::Ci::JobRequest::Image + expose :services, using: Entities::Ci::JobRequest::Service + expose :artifacts, using: Entities::Ci::JobRequest::Artifacts + expose :cache, using: Entities::Ci::JobRequest::Cache + expose :credentials, using: Entities::Ci::JobRequest::Credentials + expose :all_dependencies, as: :dependencies, using: Entities::Ci::JobRequest::Dependency + expose :features + end + end + end + end +end + +API::Entities::Ci::JobRequest::Response.prepend_mod_with('API::Entities::Ci::JobRequest::Response') diff --git a/lib/api/entities/ci/job_request/runner_info.rb b/lib/api/entities/ci/job_request/runner_info.rb new file mode 100644 index 00000000000..96336a1080e --- /dev/null +++ b/lib/api/entities/ci/job_request/runner_info.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class RunnerInfo < Grape::Entity + expose :metadata_timeout, as: :timeout + expose :runner_session_url + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/service.rb b/lib/api/entities/ci/job_request/service.rb new file mode 100644 index 00000000000..f89b95c1d5c --- /dev/null +++ b/lib/api/entities/ci/job_request/service.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class Service < Entities::Ci::JobRequest::Image + expose :alias, :command + end + end + end + end +end diff --git a/lib/api/entities/ci/job_request/step.rb b/lib/api/entities/ci/job_request/step.rb new file mode 100644 index 00000000000..2a0c4cd032e --- /dev/null +++ b/lib/api/entities/ci/job_request/step.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module JobRequest + class Step < Grape::Entity + expose :name, :script, :timeout, :when, :allow_failure + end + end + end + end +end diff --git a/lib/api/entities/ci/runner.rb b/lib/api/entities/ci/runner.rb new file mode 100644 index 00000000000..ede698696de --- /dev/null +++ b/lib/api/entities/ci/runner.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class Runner < Grape::Entity + expose :id + expose :description + expose :ip_address + expose :active + expose :instance_type?, as: :is_shared + expose :runner_type + expose :name + expose :online?, as: :online + expose :status + end + end + end +end diff --git a/lib/api/entities/ci/runner_details.rb b/lib/api/entities/ci/runner_details.rb new file mode 100644 index 00000000000..9d44da7e5b3 --- /dev/null +++ b/lib/api/entities/ci/runner_details.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class RunnerDetails < Runner + expose :tag_list + expose :run_untagged + expose :locked + expose :maximum_timeout + expose :access_level + expose :version, :revision, :platform, :architecture + expose :contacted_at + + # rubocop: disable CodeReuse/ActiveRecord + expose :projects, with: Entities::BasicProjectDetails do |runner, options| + if options[:current_user].admin? # rubocop: disable Cop/UserAdmin + runner.projects + else + options[:current_user].authorized_projects.where(id: runner.projects) + end + end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord + expose :groups, with: Entities::BasicGroupDetails do |runner, options| + if options[:current_user].admin? # rubocop: disable Cop/UserAdmin + runner.groups + else + options[:current_user].authorized_groups.where(id: runner.groups) + end + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/lib/api/entities/ci/runner_registration_details.rb b/lib/api/entities/ci/runner_registration_details.rb new file mode 100644 index 00000000000..fa7e44c9e40 --- /dev/null +++ b/lib/api/entities/ci/runner_registration_details.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + class RunnerRegistrationDetails < Grape::Entity + expose :id, :token + end + end + end +end diff --git a/lib/api/entities/group_detail.rb b/lib/api/entities/group_detail.rb index 408254a89be..61f35d0f784 100644 --- a/lib/api/entities/group_detail.rb +++ b/lib/api/entities/group_detail.rb @@ -7,6 +7,7 @@ module API SharedGroupWithGroup.represent(group.shared_with_group_links.public_or_visible_to_user(group, options[:current_user])) end expose :runners_token, if: lambda { |group, options| options[:user_can_admin_group] } + expose :prevent_sharing_groups_outside_hierarchy, if: ->(group) { group.root? } expose :projects, using: Entities::Project do |group, options| projects = GroupProjectsFinder.new( diff --git a/lib/api/entities/helm/index.rb b/lib/api/entities/helm/index.rb new file mode 100644 index 00000000000..168298f24b6 --- /dev/null +++ b/lib/api/entities/helm/index.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Helm + class Index < Grape::Entity + expose :api_version, as: :apiVersion + expose :entries + expose :generated + expose :server_info, as: :serverInfo + end + end + end +end diff --git a/lib/api/entities/job_request/artifacts.rb b/lib/api/entities/job_request/artifacts.rb deleted file mode 100644 index 0d27f5a9189..00000000000 --- a/lib/api/entities/job_request/artifacts.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class Artifacts < Grape::Entity - expose :name - expose :untracked - expose :paths - expose :exclude, expose_nil: false - expose :when - expose :expire_in - expose :artifact_type - expose :artifact_format - end - end - end -end diff --git a/lib/api/entities/job_request/cache.rb b/lib/api/entities/job_request/cache.rb deleted file mode 100644 index cd533d7e5b3..00000000000 --- a/lib/api/entities/job_request/cache.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class Cache < Grape::Entity - expose :key, :untracked, :paths, :policy, :when - end - end - end -end diff --git a/lib/api/entities/job_request/credentials.rb b/lib/api/entities/job_request/credentials.rb deleted file mode 100644 index cdac5566cbd..00000000000 --- a/lib/api/entities/job_request/credentials.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class Credentials < Grape::Entity - expose :type, :url, :username, :password - end - end - end -end diff --git a/lib/api/entities/job_request/dependency.rb b/lib/api/entities/job_request/dependency.rb deleted file mode 100644 index 7d6ec832ba1..00000000000 --- a/lib/api/entities/job_request/dependency.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class Dependency < Grape::Entity - expose :id, :name, :token - expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? } - end - end - end -end diff --git a/lib/api/entities/job_request/git_info.rb b/lib/api/entities/job_request/git_info.rb deleted file mode 100644 index e07099263b5..00000000000 --- a/lib/api/entities/job_request/git_info.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class GitInfo < Grape::Entity - expose :repo_url, :ref, :sha, :before_sha - expose :ref_type - expose :refspecs - expose :git_depth, as: :depth - end - end - end -end diff --git a/lib/api/entities/job_request/image.rb b/lib/api/entities/job_request/image.rb deleted file mode 100644 index 47f4542d2d5..00000000000 --- a/lib/api/entities/job_request/image.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class Image < Grape::Entity - expose :name, :entrypoint - expose :ports, using: Entities::JobRequest::Port - end - end - end -end diff --git a/lib/api/entities/job_request/job_info.rb b/lib/api/entities/job_request/job_info.rb deleted file mode 100644 index a4bcc9726d0..00000000000 --- a/lib/api/entities/job_request/job_info.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class JobInfo < Grape::Entity - expose :id, :name, :stage - expose :project_id, :project_name - end - end - end -end diff --git a/lib/api/entities/job_request/port.rb b/lib/api/entities/job_request/port.rb deleted file mode 100644 index ee427da8657..00000000000 --- a/lib/api/entities/job_request/port.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class Port < Grape::Entity - expose :number, :protocol, :name - end - end - end -end diff --git a/lib/api/entities/job_request/response.rb b/lib/api/entities/job_request/response.rb deleted file mode 100644 index 2e8dfc5bde0..00000000000 --- a/lib/api/entities/job_request/response.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class Response < Grape::Entity - expose :id - expose :token - expose :allow_git_fetch - - expose :job_info, using: Entities::JobRequest::JobInfo do |model| - model - end - - expose :git_info, using: Entities::JobRequest::GitInfo do |model| - model - end - - expose :runner_info, using: Entities::JobRequest::RunnerInfo do |model| - model - end - - expose :runner_variables, as: :variables - expose :steps, using: Entities::JobRequest::Step - expose :image, using: Entities::JobRequest::Image - expose :services, using: Entities::JobRequest::Service - expose :artifacts, using: Entities::JobRequest::Artifacts - expose :cache, using: Entities::JobRequest::Cache - expose :credentials, using: Entities::JobRequest::Credentials - expose :all_dependencies, as: :dependencies, using: Entities::JobRequest::Dependency - expose :features - end - end - end -end - -API::Entities::JobRequest::Response.prepend_mod_with('API::Entities::JobRequest::Response') diff --git a/lib/api/entities/job_request/runner_info.rb b/lib/api/entities/job_request/runner_info.rb deleted file mode 100644 index e6d2e8d9e85..00000000000 --- a/lib/api/entities/job_request/runner_info.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class RunnerInfo < Grape::Entity - expose :metadata_timeout, as: :timeout - expose :runner_session_url - end - end - end -end diff --git a/lib/api/entities/job_request/service.rb b/lib/api/entities/job_request/service.rb deleted file mode 100644 index 9ad5abf4e9e..00000000000 --- a/lib/api/entities/job_request/service.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class Service < Entities::JobRequest::Image - expose :alias, :command - end - end - end -end diff --git a/lib/api/entities/job_request/step.rb b/lib/api/entities/job_request/step.rb deleted file mode 100644 index 498dd017fb4..00000000000 --- a/lib/api/entities/job_request/step.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - module JobRequest - class Step < Grape::Entity - expose :name, :script, :timeout, :when, :allow_failure - end - end - end -end diff --git a/lib/api/entities/label.rb b/lib/api/entities/label.rb index ca9a0912331..dc147f33671 100644 --- a/lib/api/entities/label.rb +++ b/lib/api/entities/label.rb @@ -18,7 +18,9 @@ module API end expose :subscribed do |label, options| - label.subscribed?(options[:current_user], options[:parent]) + label.subscribed?(options[:current_user]) || ( + options[:parent].is_a?(::Project) && label.subscribed?(options[:current_user], options[:parent]) + ) end end end diff --git a/lib/api/entities/plan_limit.rb b/lib/api/entities/plan_limit.rb index 40e8b348c18..04ec44b5167 100644 --- a/lib/api/entities/plan_limit.rb +++ b/lib/api/entities/plan_limit.rb @@ -9,6 +9,7 @@ module API expose :npm_max_file_size expose :nuget_max_file_size expose :pypi_max_file_size + expose :terraform_module_max_file_size end end end diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index 68d91fc6970..f5f565e5b07 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -53,13 +53,7 @@ module API expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } - expose(:container_registry_enabled) do |project, options| - if ::Feature.enabled?(:read_container_registry_access_level, project.namespace, default_enabled: :yaml) - project.feature_available?(:container_registry, options[:current_user]) - else - project.read_attribute(:container_registry_enabled) - end - end + expose(:container_registry_enabled) { |project, options| project.feature_available?(:container_registry, options[:current_user]) } expose :service_desk_enabled expose :service_desk_address diff --git a/lib/api/entities/project_integration.rb b/lib/api/entities/project_integration.rb new file mode 100644 index 00000000000..649e4d015b8 --- /dev/null +++ b/lib/api/entities/project_integration.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module API + module Entities + class ProjectIntegration < Entities::ProjectIntegrationBasic + # Expose serialized properties + expose :properties do |integration, options| + # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 + + attributes = + if integration.data_fields_present? + integration.data_fields.as_json.keys + else + integration.properties.keys + end + + attributes &= integration.api_field_names + + attributes.each_with_object({}) do |attribute, hash| + hash[attribute] = integration.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend + end + end + end + end +end diff --git a/lib/api/entities/project_service_basic.rb b/lib/api/entities/project_integration_basic.rb index eb97ca69a82..2870123b83d 100644 --- a/lib/api/entities/project_service_basic.rb +++ b/lib/api/entities/project_integration_basic.rb @@ -2,10 +2,10 @@ module API module Entities - class ProjectServiceBasic < Grape::Entity + class ProjectIntegrationBasic < Grape::Entity expose :id, :title - expose :slug do |service| - service.to_param.dasherize + expose :slug do |integration| + integration.to_param.dasherize end expose :created_at, :updated_at, :active expose :commit_events, :push_events, :issues_events, :confidential_issues_events diff --git a/lib/api/entities/project_service.rb b/lib/api/entities/project_service.rb deleted file mode 100644 index 947cec1e3cd..00000000000 --- a/lib/api/entities/project_service.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class ProjectService < Entities::ProjectServiceBasic - # Expose serialized properties - expose :properties do |service, options| - # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab/issues/29404 - if service.data_fields_present? - service.data_fields.as_json.slice(*service.api_field_names) - else - service.properties.slice(*service.api_field_names) - end - end - end - end -end diff --git a/lib/api/entities/resource_access_token.rb b/lib/api/entities/resource_access_token.rb new file mode 100644 index 00000000000..a1c7b28af45 --- /dev/null +++ b/lib/api/entities/resource_access_token.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class ResourceAccessToken < Entities::PersonalAccessToken + expose :access_level do |token, options| + options[:project].project_member(token.user).access_level + end + end + end +end diff --git a/lib/api/entities/resource_access_token_with_token.rb b/lib/api/entities/resource_access_token_with_token.rb new file mode 100644 index 00000000000..edbd9b285de --- /dev/null +++ b/lib/api/entities/resource_access_token_with_token.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class ResourceAccessTokenWithToken < Entities::ResourceAccessToken + expose :token + end + end +end diff --git a/lib/api/entities/runner.rb b/lib/api/entities/runner.rb deleted file mode 100644 index e78f14cf920..00000000000 --- a/lib/api/entities/runner.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class Runner < Grape::Entity - expose :id - expose :description - expose :ip_address - expose :active - expose :instance_type?, as: :is_shared - expose :runner_type - expose :name - expose :online?, as: :online - expose :status - end - end -end diff --git a/lib/api/entities/runner_details.rb b/lib/api/entities/runner_details.rb deleted file mode 100644 index 0afe298ef64..00000000000 --- a/lib/api/entities/runner_details.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class RunnerDetails < Runner - expose :tag_list - expose :run_untagged - expose :locked - expose :maximum_timeout - expose :access_level - expose :version, :revision, :platform, :architecture - expose :contacted_at - - # rubocop: disable CodeReuse/ActiveRecord - expose :projects, with: Entities::BasicProjectDetails do |runner, options| - if options[:current_user].admin? - runner.projects - else - options[:current_user].authorized_projects.where(id: runner.projects) - end - end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord - expose :groups, with: Entities::BasicGroupDetails do |runner, options| - if options[:current_user].admin? - runner.groups - else - options[:current_user].authorized_groups.where(id: runner.groups) - end - end - # rubocop: enable CodeReuse/ActiveRecord - end - end -end diff --git a/lib/api/entities/runner_registration_details.rb b/lib/api/entities/runner_registration_details.rb deleted file mode 100644 index c8ed88ba10a..00000000000 --- a/lib/api/entities/runner_registration_details.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class RunnerRegistrationDetails < Grape::Entity - expose :id, :token - end - end -end diff --git a/lib/api/entities/user.rb b/lib/api/entities/user.rb index 3ce6d03e236..973e80dd5ef 100644 --- a/lib/api/entities/user.rb +++ b/lib/api/entities/user.rb @@ -5,7 +5,7 @@ module API class User < UserBasic include UsersHelper expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) } - expose :bio, :bio_html, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title + expose :bio, :bio_html, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title, :pronouns expose :bot?, as: :bot expose :work_information do |user| work_information(user) diff --git a/lib/api/error_tracking_collector.rb b/lib/api/error_tracking_collector.rb new file mode 100644 index 00000000000..08ff8d2e4d1 --- /dev/null +++ b/lib/api/error_tracking_collector.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module API + # This API is responsible for collecting error tracking information + # from sentry client. It allows us to use GitLab as an alternative to + # sentry backend. For more details see https://gitlab.com/gitlab-org/gitlab/-/issues/329596. + class ErrorTrackingCollector < ::API::Base + feature_category :error_tracking + + content_type :envelope, 'application/x-sentry-envelope' + default_format :envelope + + before do + not_found!('Project') unless project + not_found! unless feature_enabled? + end + + helpers do + def project + @project ||= find_project(params[:id]) + end + + def feature_enabled? + ::Feature.enabled?(:integrated_error_tracking, project) && + project.error_tracking_setting&.enabled? + end + end + + desc 'Submit error tracking event to the project' do + detail 'This feature was introduced in GitLab 14.1.' + end + params do + requires :id, type: String, desc: 'The ID of a project' + end + post 'error_tracking/collector/api/:id/envelope' do + # There is a reason why we have such uncommon path. + # We depend on a client side error tracking software which + # modifies URL for its own reasons. + # + # When we give user a URL like this + # HOST/api/v4/error_tracking/collector/123 + # + # Then error tracking software will convert it like this: + # HOST/api/v4/error_tracking/collector/api/123/envelope/ + + begin + parsed_request = ::ErrorTracking::Collector::SentryRequestParser.parse(request) + rescue StandardError + render_api_error!('Failed to parse sentry request', 400) + end + + type = parsed_request[:request_type] + + # Sentry sends 2 requests on each exception: transaction and event. + # Everything else is not a desired behavior. + unless type == 'transaction' || type == 'event' + render_api_error!('400 Bad Request', 400) + + break + end + + # We don't have use for transaction request yet, + # so we record only event one. + if type == 'event' + ::ErrorTracking::CollectErrorService + .new(project, nil, event: parsed_request[:event]) + .execute + end + + no_content! + end + end +end diff --git a/lib/api/geo.rb b/lib/api/geo.rb new file mode 100644 index 00000000000..9fc610c9b32 --- /dev/null +++ b/lib/api/geo.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module API + class Geo < ::API::Base + feature_category :geo_replication + + helpers do + # Overridden in EE + def geo_proxy_response + {} + end + end + + resource :geo do + # Workhorse calls this to determine if it is a Geo site that should proxy + # requests. Workhorse doesn't know if it's in a FOSS/EE context. + get '/proxy' do + require_gitlab_workhorse! + + status :ok + content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE + + geo_proxy_response + end + end + end +end + +API::Geo.prepend_mod diff --git a/lib/api/group_avatar.rb b/lib/api/group_avatar.rb index ddf6787f913..9063040c763 100644 --- a/lib/api/group_avatar.rb +++ b/lib/api/group_avatar.rb @@ -6,15 +6,27 @@ module API feature_category :subgroups - resource :groups do + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc 'Download the group avatar' do detail 'This feature was introduced in GitLab 14.0' end - params do - requires :id, type: String, desc: 'The group id' - end get ':id/avatar' do - present_carrierwave_file!(user_group.avatar) + avatar = user_group.avatar + + not_found!('Avatar') if avatar.blank? + + header( + 'Content-Disposition', + ActionDispatch::Http::ContentDisposition.format( + disposition: 'attachment', + filename: avatar.filename + ) + ) + + present_carrierwave_file!(avatar) end end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 0efb8b57885..9b6b28733ff 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -22,7 +22,7 @@ module API optional :all_available, type: Boolean, desc: 'Show all group that you have access to' optional :search, type: String, desc: 'Search for a specific group' optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user' - optional :order_by, type: String, values: %w[name path id], default: 'name', desc: 'Order by name, path or id' + optional :order_by, type: String, values: %w[name path id similarity], default: 'name', desc: 'Order by name, path, id or similarity if searching' optional :sort, type: String, values: %w[asc desc], default: 'asc', desc: 'Sort by asc (ascending) or desc (descending)' optional :min_access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'Minimum access level of authenticated user' optional :top_level_only, type: Boolean, desc: 'Only include top level groups' @@ -50,9 +50,8 @@ module API groups = GroupsFinder.new(current_user, find_params).execute groups = groups.search(params[:search], include_parents: true) if params[:search].present? 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.reorder(order_options) + + order_groups(groups) end # rubocop: enable CodeReuse/ActiveRecord @@ -123,6 +122,23 @@ module API reorder_projects(projects) end + def order_groups(groups) + return groups.sorted_by_similarity_and_parent_id_desc(params[:search]) if order_by_similarity? + + groups.reorder(group_without_similarity_options) # rubocop: disable CodeReuse/ActiveRecord + end + + def order_by_similarity? + params[:order_by] == 'similarity' && params[:search].present? + end + + def group_without_similarity_options + order_options = { params[:order_by] => params[:sort] } + order_options['name'] = order_options.delete('similarity') if order_options.has_key?('similarity') + order_options["id"] ||= "asc" + order_options + end + # rubocop: disable CodeReuse/ActiveRecord def handle_similarity_order(group, projects) if params[:search].present? && Feature.enabled?(:similarity_search, group, default_enabled: true) @@ -199,6 +215,7 @@ module API optional :name, type: String, desc: 'The name of the group' optional :path, type: String, desc: 'The path of the group' use :optional_params + use :optional_update_params use :optional_update_params_ee end put ':id' do diff --git a/lib/api/helm_packages.rb b/lib/api/helm_packages.rb index dc5630a1395..4280744d8b4 100644 --- a/lib/api/helm_packages.rb +++ b/lib/api/helm_packages.rb @@ -10,11 +10,15 @@ module API feature_category :package_registry + PACKAGE_FILENAME = 'package.tgz' FILE_NAME_REQUIREMENTS = { file_name: API::NO_SLASH_URL_PART_REGEX }.freeze content_type :binary, 'application/octet-stream' + content_type :yaml, 'text/yaml' + + formatter :yaml, -> (object, _) { object.serializable_hash.stringify_keys.to_yaml } authenticate_with do |accept| accept.token_types(:personal_access_token, :deploy_token, :job_token) @@ -25,15 +29,33 @@ module API require_packages_enabled! end - after_validation do - not_found! unless Feature.enabled?(:helm_packages, authorized_user_project) - end - params do requires :id, type: String, desc: 'The ID or full path of a project' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do namespace ':id/packages/helm' do + desc 'Download a chart index' do + detail 'This feature was introduced in GitLab 14.0' + end + params do + requires :channel, type: String, desc: 'Helm channel', regexp: Gitlab::Regex.helm_channel_regex + end + + get ":channel/index.yaml" do + authorize_read_package!(authorized_user_project) + + package_files = Packages::Helm::PackageFilesFinder.new( + authorized_user_project, + params[:channel], + order_by: 'created_at', + sort: 'desc' + ).execute + + env['api.format'] = :yaml + present ::Packages::Helm::IndexPresenter.new(authorized_user_project, params[:id], package_files), + with: ::API::Entities::Helm::Index + end + desc 'Download a chart' do detail 'This feature was introduced in GitLab 14.0' end @@ -50,6 +72,55 @@ module API present_carrierwave_file!(package_file.file) end + + desc 'Authorize a chart upload from workhorse' do + detail 'This feature was introduced in GitLab 14.0' + end + params do + requires :channel, type: String, desc: 'Helm channel', regexp: Gitlab::Regex.helm_channel_regex + end + post "api/:channel/charts/authorize" do + authorize_workhorse!( + subject: authorized_user_project, + has_length: false, + maximum_size: authorized_user_project.actual_limits.helm_max_file_size + ) + end + + desc 'Upload a chart' do + detail 'This feature was introduced in GitLab 14.0' + end + params do + requires :channel, type: String, desc: 'Helm channel', regexp: Gitlab::Regex.helm_channel_regex + requires :chart, type: ::API::Validations::Types::WorkhorseFile, desc: 'The chart file to be published (generated by Multipart middleware)' + end + post "api/:channel/charts" do + authorize_upload!(authorized_user_project) + bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:helm_max_file_size, params[:chart].size) + + package = ::Packages::CreateTemporaryPackageService.new( + authorized_user_project, current_user, declared_params.merge(build: current_authenticated_job) + ).execute(:helm, name: ::Packages::Helm::TEMPORARY_PACKAGE_NAME) + + chart_params = { + file: params[:chart], + file_name: PACKAGE_FILENAME + } + + chart_package_file = ::Packages::CreatePackageFileService.new( + package, chart_params.merge(build: current_authenticated_job) + ).execute + + track_package_event('push_package', :helm, project: authorized_user_project, namespace: authorized_user_project.namespace) + + ::Packages::Helm::ExtractionWorker.perform_async(params[:channel], chart_package_file.id) # rubocop:disable CodeReuse/Worker + + created! + rescue ObjectStorage::RemoteStoreError => e + Gitlab::ErrorTracking.track_exception(e, extra: { channel: params[:channel], project_id: authorized_user_project.id }) + + forbidden! + end end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 6ce04be373f..3398d5da7f5 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -577,10 +577,6 @@ module API Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}") end - def with_api_params(&block) - yield({ api: true, request: request }) - end - protected def project_finder_params_visibility_ce diff --git a/lib/api/helpers/caching.rb b/lib/api/helpers/caching.rb index f24ac7302c1..f567d85443f 100644 --- a/lib/api/helpers/caching.rb +++ b/lib/api/helpers/caching.rb @@ -8,18 +8,15 @@ module API module Helpers module Caching - # @return [ActiveSupport::Duration] - DEFAULT_EXPIRY = 1.day - + include Gitlab::Cache::Helpers # @return [Hash] DEFAULT_CACHE_OPTIONS = { - race_condition_ttl: 5.seconds + race_condition_ttl: 5.seconds, + version: 1 }.freeze - # @return [ActiveSupport::Cache::Store] - def cache - Rails.cache - end + # @return [Array] + PAGINATION_HEADERS = %w[X-Per-Page X-Page X-Next-Page X-Prev-Page Link X-Total X-Total-Pages].freeze # This is functionally equivalent to the standard `#present` used in # Grape endpoints, but the JSON for the object, or for each object of @@ -45,7 +42,7 @@ module API # @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) + def present_cached(obj_or_collection, with:, cache_context: -> (_) { current_user&.cache_key }, expires_in: Gitlab::Cache::Helpers::DEFAULT_EXPIRY, **presenter_args) json = if obj_or_collection.is_a?(Enumerable) cached_collection( @@ -79,15 +76,22 @@ module API # @param key [Object] any object that can be converted into a cache key # @param expires_in [ActiveSupport::Duration, Integer] an expiry time for the cache entry # @return [Gitlab::Json::PrecompiledJson] - def cache_action(key, **cache_opts) - json = cache.fetch(key, **apply_default_cache_options(cache_opts)) do + def cache_action(key, **custom_cache_opts) + cache_opts = apply_default_cache_options(custom_cache_opts) + + json, cached_headers = cache.fetch(key, **cache_opts) do response = yield - if response.is_a?(Gitlab::Json::PrecompiledJson) - response.to_s - else - Gitlab::Json.dump(response.as_json) - end + cached_body = response.is_a?(Gitlab::Json::PrecompiledJson) ? response.to_s : Gitlab::Json.dump(response.as_json) + cached_headers = header.slice(*PAGINATION_HEADERS) + + [cached_body, cached_headers] + end + + cached_headers.each do |key, value| + next if header.key?(key) + + header key, value end body Gitlab::Json::PrecompiledJson.new(json) @@ -120,77 +124,6 @@ module API def apply_default_cache_options(opts = {}) DEFAULT_CACHE_OPTIONS.merge(opts) end - - # 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) - - # TODO: `contextual_cache_key` should be constructed based on the guideline https://docs.gitlab.com/ee/development/redis.html#multi-key-commands. - Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do - cache.fetch_multi(*map.keys, **kwargs) do |key| - yield map[key] - end - 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/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb index 5c5109f3d21..e38213532ba 100644 --- a/lib/api/helpers/groups_helpers.rb +++ b/lib/api/helpers/groups_helpers.rb @@ -30,6 +30,10 @@ module API params :optional_params_ee do end + params :optional_update_params do + optional :prevent_sharing_groups_outside_hierarchy, type: Boolean, desc: 'Prevent sharing groups within this namespace with any groups outside the namespace. Only available on top-level groups.' + end + params :optional_update_params_ee do end diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/integrations_helpers.rb index ca13ea0789a..06539772568 100644 --- a/lib/api/helpers/services_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -6,7 +6,7 @@ module API # # The data structures inside this model are returned using class methods, # allowing EE to extend them where necessary. - module ServicesHelpers + module IntegrationsHelpers def self.chat_notification_settings [ { @@ -159,7 +159,7 @@ module API ].freeze end - def self.services + def self.integrations { 'asana' => [ { @@ -772,7 +772,7 @@ module API } end - def self.service_classes + def self.integration_classes [ ::Integrations::Asana, ::Integrations::Assembla, @@ -799,24 +799,24 @@ module API ::Integrations::Packagist, ::Integrations::PipelinesEmail, ::Integrations::Pivotaltracker, + ::Integrations::Prometheus, ::Integrations::Pushover, ::Integrations::Redmine, ::Integrations::Slack, ::Integrations::SlackSlashCommands, ::Integrations::Teamcity, - ::Integrations::Youtrack, - ::PrometheusService + ::Integrations::Youtrack ] end - def self.development_service_classes + def self.development_integration_classes [ ::Integrations::MockCi, - ::MockMonitoringService + ::Integrations::MockMonitoring ] end end end end -API::Helpers::ServicesHelpers.prepend_mod_with('API::Helpers::ServicesHelpers') +API::Helpers::IntegrationsHelpers.prepend_mod_with('API::Helpers::IntegrationsHelpers') diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 69a83043617..272452bd8db 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -66,6 +66,7 @@ module API optional :autoclose_referenced_issues, type: Boolean, desc: 'Flag indication if referenced issues auto-closing is enabled' optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins' optional :packages_enabled, type: Boolean, desc: 'Enable project packages feature' + optional :squash_option, type: String, values: %w(never always default_on default_off), desc: 'Squash default for project. One of `never`, `always`, `default_on`, or `default_off`.' end params :optional_project_params_ee do @@ -145,6 +146,7 @@ module API :request_access_enabled, :resolve_outdated_diff_discussions, :restrict_user_defined_variables, + :squash_option, :shared_runners_enabled, :snippets_access_level, :tag_list, diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 9ec9b5e1e35..a022d1a56ac 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -14,6 +14,10 @@ module API ActiveSupport::SecurityUtils.secure_compare(params[:token], Gitlab::CurrentSettings.runners_registration_token) end + def runner_registrar_valid?(type) + Feature.disabled?(:runner_registration_control) || Gitlab::CurrentSettings.valid_runner_registrars.include?(type) + end + def authenticate_runner! forbidden! unless current_runner diff --git a/lib/api/helpers/snippets_helpers.rb b/lib/api/helpers/snippets_helpers.rb index 42f56680ded..2d8c761101a 100644 --- a/lib/api/helpers/snippets_helpers.rb +++ b/lib/api/helpers/snippets_helpers.rb @@ -72,22 +72,18 @@ module API end def process_create_params(args) - with_api_params do |api_params| - args[:snippet_actions] = args.delete(:files)&.map do |file| - file[:action] = :create - file.symbolize_keys - end - - args.merge(api_params) + args[:snippet_actions] = args.delete(:files)&.map do |file| + file[:action] = :create + file.symbolize_keys end + + args end def process_update_params(args) - with_api_params do |api_params| - args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys) + args[:snippet_actions] = args.delete(:files)&.map(&:symbolize_keys) - args.merge(api_params) - end + args end def validate_params_for_multiple_files(snippet) diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index ee0ddccc8d4..a06b052847d 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -124,11 +124,6 @@ module API yield end end - - # Overridden in EE - def geo_proxy - {} - end end namespace 'internal' do @@ -320,12 +315,6 @@ 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 c28e2181873..7af5c2ad2ee 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -53,8 +53,6 @@ module API def check_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/issues.rb b/lib/api/issues.rb index 355b5ed3a1f..54013d0e7b4 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -255,9 +255,11 @@ module API issue_params = convert_parameters_from_legacy_format(issue_params) begin + spam_params = ::Spam::SpamParams.new_from_request(request: request) issue = ::Issues::CreateService.new(project: user_project, current_user: current_user, - params: issue_params.merge(request: request, api: true)).execute + params: issue_params, + spam_params: spam_params).execute if issue.spam? render_api_error!({ error: 'Spam detected' }, 400) @@ -294,13 +296,15 @@ module API issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) authorize! :update_issue, issue - update_params = declared_params(include_missing: false).merge(request: request, api: true) + update_params = declared_params(include_missing: false) update_params = convert_parameters_from_legacy_format(update_params) + spam_params = ::Spam::SpamParams.new_from_request(request: request) issue = ::Issues::UpdateService.new(project: user_project, current_user: current_user, - params: update_params).execute(issue) + params: update_params, + spam_params: spam_params).execute(issue) render_spam_error! if issue.spam? diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index 37199279205..beda4433e4f 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -4,7 +4,7 @@ module API class JobArtifacts < ::API::Base before { authenticate_non_get! } - feature_category :continuous_integration + feature_category :build_artifacts # EE::API::JobArtifacts would override the following helpers helpers do diff --git a/lib/api/lint.rb b/lib/api/lint.rb index 3580a7b5e24..945cdf3edb2 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -11,11 +11,7 @@ module API optional :include_merged_yaml, type: Boolean, desc: 'Whether or not to include merged CI config yaml in the response' end post '/lint' do - if Feature.enabled?(:security_ci_lint_authorization) - unauthorized! if (Gitlab::CurrentSettings.signup_disabled? || Gitlab::CurrentSettings.signup_limited?) && current_user.nil? - else - unauthorized! if Gitlab::CurrentSettings.signup_disabled? && current_user.nil? - end + unauthorized! if (Gitlab::CurrentSettings.signup_disabled? || Gitlab::CurrentSettings.signup_limited?) && current_user.nil? result = Gitlab::Ci::YamlProcessor.new(params[:content], user: current_user).execute diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb index 5bae08d4dae..03d1492908d 100644 --- a/lib/api/nuget_project_packages.rb +++ b/lib/api/nuget_project_packages.rb @@ -16,6 +16,7 @@ module API feature_category :package_registry PACKAGE_FILENAME = 'package.nupkg' + SYMBOL_PACKAGE_FILENAME = 'package.snupkg' default_format :json @@ -33,6 +34,10 @@ module API end helpers do + params :file_params do + requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' + end + def project_or_group authorized_user_project end @@ -40,6 +45,49 @@ module API def snowplow_gitlab_standard_context { project: authorized_user_project, namespace: authorized_user_project.namespace } end + + def authorize_nuget_upload + authorize_workhorse!( + subject: project_or_group, + has_length: false, + maximum_size: project_or_group.actual_limits.nuget_max_file_size + ) + end + + def temp_file_name(symbol_package) + return ::Packages::Nuget::TEMPORARY_SYMBOL_PACKAGE_NAME if symbol_package + + ::Packages::Nuget::TEMPORARY_PACKAGE_NAME + end + + def file_name(symbol_package) + return SYMBOL_PACKAGE_FILENAME if symbol_package + + PACKAGE_FILENAME + end + + def upload_nuget_package_file(symbol_package: false) + authorize_upload!(project_or_group) + bad_request!('File is too large') if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size) + + file_params = params.merge( + file: params[:package], + file_name: file_name(symbol_package) + ) + + package = ::Packages::CreateTemporaryPackageService.new( + project_or_group, current_user, declared_params.merge(build: current_authenticated_job) + ).execute(:nuget, name: temp_file_name(symbol_package)) + + package_file = ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job)) + .execute + + yield(package) if block_given? + + ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker + + created! + end end params do @@ -55,40 +103,54 @@ module API end params do - requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' + use :file_params end put do - authorize_upload!(project_or_group) - bad_request!('File is too large') if project_or_group.actual_limits.exceeded?(:nuget_max_file_size, params[:package].size) - - file_params = params.merge( - file: params[:package], - file_name: PACKAGE_FILENAME - ) - - package = ::Packages::CreateTemporaryPackageService.new( - project_or_group, current_user, declared_params.merge(build: current_authenticated_job) - ).execute(:nuget, name: ::Packages::Nuget::TEMPORARY_PACKAGE_NAME) - - package_file = ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job)) - .execute + upload_nuget_package_file do |package| + track_package_event( + 'push_package', + :nuget, + category: 'API::NugetPackages', + user: current_user, + project: package.project, + namespace: package.project.namespace + ) + end + rescue ObjectStorage::RemoteStoreError => e + Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id }) - track_package_event('push_package', :nuget, category: 'API::NugetPackages', user: current_user, project: package.project, namespace: package.project.namespace) + forbidden! + end + put 'authorize' do + authorize_nuget_upload + end - ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker + # https://docs.microsoft.com/en-us/nuget/api/symbol-package-publish-resource + desc 'The NuGet Symbol Package Publish endpoint' do + detail 'This feature was introduced in GitLab 14.1' + end - created! + params do + use :file_params + end + put 'symbolpackage' do + upload_nuget_package_file(symbol_package: true) do |package| + track_package_event( + 'push_symbol_package', + :nuget, + category: 'API::NugetPackages', + user: current_user, + project: package.project, + namespace: package.project.namespace + ) + end rescue ObjectStorage::RemoteStoreError => e Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project_or_group.id }) forbidden! end - put 'authorize' do - authorize_workhorse!( - subject: project_or_group, - has_length: false, - maximum_size: project_or_group.actual_limits.nuget_max_file_size - ) + put 'symbolpackage/authorize' do + authorize_nuget_upload end # https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource @@ -115,14 +177,20 @@ module API requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX end - get '*package_version/*package_filename', format: :nupkg do + get '*package_version/*package_filename', format: [:nupkg, :snupkg] do filename = "#{params[:package_filename]}.#{params[:format]}" package_file = ::Packages::PackageFileFinder.new(find_package(params[:package_name], params[:package_version]), filename, with_file_name_like: true) .execute not_found!('Package') unless package_file - track_package_event('pull_package', :nuget, category: 'API::NugetPackages', project: package_file.project, namespace: package_file.project.namespace) + track_package_event( + params[:format] == 'snupkg' ? 'pull_symbol_package' : 'pull_package', + :nuget, + category: 'API::NugetPackages', + project: package_file.project, + namespace: package_file.project.namespace + ) # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false present_carrierwave_file!(package_file.file, supports_direct_download: false) diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb index 35a68ec4e18..54c0a0628a7 100644 --- a/lib/api/project_packages.rb +++ b/lib/api/project_packages.rb @@ -71,9 +71,7 @@ module API .new(user_project, params[:package_id]).execute destroy_conditionally!(package) do |package| - if package.destroy - package.sync_maven_metadata(current_user) - end + ::Packages::DestroyPackageService.new(container: package, current_user: current_user).execute end end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 084492fd503..fdbfdf1f7a9 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -75,7 +75,8 @@ module API snippet_params = process_create_params(declared_params(include_missing: false)) - service_response = ::Snippets::CreateService.new(project: user_project, current_user: current_user, params: snippet_params).execute + spam_params = ::Spam::SpamParams.new_from_request(request: request) + service_response = ::Snippets::CreateService.new(project: user_project, current_user: current_user, params: snippet_params, spam_params: spam_params).execute snippet = service_response.payload[:snippet] if service_response.success? @@ -116,7 +117,8 @@ module API snippet_params = process_update_params(declared_params(include_missing: false)) - service_response = ::Snippets::UpdateService.new(project: user_project, current_user: current_user, params: snippet_params).execute(snippet) + spam_params = ::Spam::SpamParams.new_from_request(request: request) + service_response = ::Snippets::UpdateService.new(project: user_project, current_user: current_user, params: snippet_params, spam_params: spam_params).execute(snippet) snippet = service_response.payload[:snippet] if service_response.success? diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 83c335a3248..3b1d239398f 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -157,6 +157,8 @@ module API [options[:with].prepare_relation(projects, options), options] end + Preloaders::UserMaxAccessLevelInProjectsPreloader.new(records, current_user).execute if current_user + present records, options end @@ -608,6 +610,10 @@ module API users = users.search(params[:search]) if params[:search].present? users = users.where_not_in(params[:skip_users]) if params[:skip_users].present? + if Feature.enabled?(:sort_by_project_users_by_project_authorizations_user_id, user_project, default_enabled: :yaml) + users = users.order('project_authorizations.user_id' => :asc) # rubocop: disable CodeReuse/ActiveRecord + end + present paginate(users), with: Entities::UserBasic end diff --git a/lib/api/releases.rb b/lib/api/releases.rb index 7cd8b442706..3b7e2b4bd27 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -62,6 +62,8 @@ module API get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do authorize_download_code! + not_found! unless release + present release, with: Entities::Release, current_user: current_user, include_html_description: params[:include_html_description] end @@ -177,7 +179,7 @@ module API end def authorize_download_code! - authorize! :download_code, release + authorize! :download_code, user_project end def authorize_create_evidence! diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index a5234828de3..f274406e225 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -120,24 +120,28 @@ module API 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 - 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) + ff_enabled = Feature.enabled?(:api_caching_rate_limit_repository_compare, user_project, default_enabled: :yaml) + + cache_action_if(ff_enabled, [user_project, :repository_compare, current_user, declared_params], expires_in: 1.minute) do + 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 - else - target_project = user_project - end - compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight]) + compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight]) - if compare - present compare, with: Entities::Compare - else - not_found!("Ref") + if compare + present compare, with: Entities::Compare + else + not_found!("Ref") + end end end diff --git a/lib/api/resource_access_tokens.rb b/lib/api/resource_access_tokens.rb index 705e4778c83..f42acc6b2eb 100644 --- a/lib/api/resource_access_tokens.rb +++ b/lib/api/resource_access_tokens.rb @@ -21,9 +21,10 @@ module API next unauthorized! unless current_user.can?(:read_resource_access_tokens, resource) - tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute + tokens = PersonalAccessTokensFinder.new({ user: resource.bots, impersonation: false }).execute.preload_users - present paginate(tokens), with: Entities::PersonalAccessToken + resource.project_members.load + present paginate(tokens), with: Entities::ResourceAccessToken, project: resource end desc 'Revoke a resource access token' do @@ -57,6 +58,7 @@ module API requires :id, type: String, desc: "The #{source_type} ID" requires :name, type: String, desc: "Resource access token name" requires :scopes, type: Array[String], desc: "The permissions of the token" + optional :access_level, type: Integer, desc: "The access level of the token in the project" optional :expires_at, type: Date, desc: "The expiration date of the token" end post ':id/access_tokens' do @@ -69,7 +71,7 @@ module API ).execute if token_response.success? - present token_response.payload[:access_token], with: Entities::PersonalAccessTokenWithToken + present token_response.payload[:access_token], with: Entities::ResourceAccessTokenWithToken, project: resource else bad_request!(token_response.message) end diff --git a/lib/api/services.rb b/lib/api/services.rb index 8a7abe721dd..a37b6f4626a 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -3,11 +3,11 @@ module API class Services < ::API::Base feature_category :integrations - services = Helpers::ServicesHelpers.services - service_classes = Helpers::ServicesHelpers.service_classes + integrations = Helpers::IntegrationsHelpers.integrations + integration_classes = Helpers::IntegrationsHelpers.integration_classes if Rails.env.development? - services['mock-ci'] = [ + integrations['mock-ci'] = [ { required: true, name: :mock_service_url, @@ -15,28 +15,27 @@ module API desc: 'URL to the mock service' } ] - services['mock-deployment'] = [] - services['mock-monitoring'] = [] + integrations['mock-deployment'] = [] + integrations['mock-monitoring'] = [] - service_classes += Helpers::ServicesHelpers.development_service_classes + integration_classes += Helpers::IntegrationsHelpers.development_integration_classes end - SERVICES = services.freeze - SERVICE_CLASSES = service_classes.freeze + INTEGRATIONS = integrations.freeze - SERVICE_CLASSES.each do |service| - event_names = service.try(:event_names) || next + integration_classes.each do |integration| + event_names = integration.try(:event_names) || next event_names.each do |event_name| - SERVICES[service.to_param.tr("_", "-")] << { + INTEGRATIONS[integration.to_param.tr("_", "-")] << { required: false, name: event_name.to_sym, type: String, - desc: service.event_description(event_name) + desc: IntegrationsHelper.integration_event_description(integration, event_name) } end end - TRIGGER_SERVICES = { + TRIGGER_INTEGRATIONS = { 'mattermost-slash-commands' => [ { name: :token, @@ -61,24 +60,24 @@ module API before { authorize_admin_project } helpers do - def service_attributes(service) - service.fields.inject([]) do |arr, hash| + def integration_attributes(integration) + integration.fields.inject([]) do |arr, hash| arr << hash[:name].to_sym end end end - desc 'Get all active project services' do - success Entities::ProjectServiceBasic + desc 'Get all active project integrations' do + success Entities::ProjectIntegrationBasic end get ":id/services" do - services = user_project.integrations.active + integrations = user_project.integrations.active - present services, with: Entities::ProjectServiceBasic + present integrations, with: Entities::ProjectIntegrationBasic end - SERVICES.each do |service_slug, settings| - desc "Set #{service_slug} service for project" + INTEGRATIONS.each do |slug, settings| + desc "Set #{slug} integration for project" params do settings.each do |setting| if setting[:required] @@ -88,56 +87,52 @@ module API end end end - put ":id/services/#{service_slug}" do - service = user_project.find_or_initialize_service(service_slug.underscore) - service_params = declared_params(include_missing: false).merge(active: true) + put ":id/services/#{slug}" do + integration = user_project.find_or_initialize_integration(slug.underscore) + params = declared_params(include_missing: false).merge(active: true) - if service.update(service_params) - present service, with: Entities::ProjectService + if integration.update(params) + present integration, with: Entities::ProjectIntegration else render_api_error!('400 Bad Request', 400) end end end - desc "Delete a service for project" + desc "Delete an integration from a project" params do - requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service' + requires :slug, type: String, values: INTEGRATIONS.keys, desc: 'The name of the service' end - delete ":id/services/:service_slug" do - service = user_project.find_or_initialize_service(params[:service_slug].underscore) + delete ":id/services/:slug" do + integration = user_project.find_or_initialize_integration(params[:slug].underscore) - destroy_conditionally!(service) do - attrs = service_attributes(service).inject({}) do |hash, key| - hash.merge!(key => nil) - end + destroy_conditionally!(integration) do + attrs = integration_attributes(integration).index_with { nil }.merge(active: false) - unless service.update(attrs.merge(active: false)) - render_api_error!('400 Bad Request', 400) - end + render_api_error!('400 Bad Request', 400) unless integration.update(attrs) end end - desc 'Get the service settings for project' do - success Entities::ProjectService + desc 'Get the integration settings for a project' do + success Entities::ProjectIntegration end params do - requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service' + requires :slug, type: String, values: INTEGRATIONS.keys, desc: 'The name of the service' end - get ":id/services/:service_slug" do - integration = user_project.find_or_initialize_service(params[:service_slug].underscore) + get ":id/services/:slug" do + integration = user_project.find_or_initialize_integration(params[:slug].underscore) not_found!('Service') unless integration&.persisted? - present integration, with: Entities::ProjectService + present integration, with: Entities::ProjectIntegration end end - TRIGGER_SERVICES.each do |service_slug, settings| + TRIGGER_INTEGRATIONS.each do |integration_slug, settings| helpers do - def slash_command_service(project, service_slug, params) - project.integrations.active.find do |service| - service.try(:token) == params[:token] && service.to_param == service_slug.underscore + def slash_command_integration(project, integration_slug, params) + project.integrations.active.find do |integration| + integration.try(:token) == params[:token] && integration.to_param == integration_slug.underscore end end end @@ -146,7 +141,7 @@ module API requires :id, type: String, desc: 'The ID of a project' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc "Trigger a slash command for #{service_slug}" do + desc "Trigger a slash command for #{integration_slug}" do detail 'Added in GitLab 8.13' end params do @@ -154,14 +149,14 @@ module API requires setting[:name], type: setting[:type], desc: setting[:desc] end end - post ":id/services/#{service_slug.underscore}/trigger" do + post ":id/services/#{integration_slug.underscore}/trigger" do project = find_project(params[:id]) # This is not accurate, but done to prevent leakage of the project names not_found!('Service') unless project - service = slash_command_service(project, service_slug, params) - result = service.try(:trigger, params) + integration = slash_command_integration(project, integration_slug, params) + result = integration.try(:trigger, params) if result status result[:status] || 200 diff --git a/lib/api/settings.rb b/lib/api/settings.rb index b4f8320cb74..952bf09b1b1 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -160,6 +160,10 @@ module API optional :usage_ping_enabled, type: Boolean, desc: 'Every week GitLab will report license usage back to GitLab, Inc.' optional :local_markdown_version, type: Integer, desc: 'Local markdown version, increase this value when any cached markdown should be invalidated' optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5 + optional :mailgun_events_enabled, type: Grape::API::Boolean, desc: 'Enable Mailgun event receiver' + given mailgun_events_enabled: ->(val) { val } do + requires :mailgun_signing_key, type: String, desc: 'The Mailgun HTTP webhook signing key for receiving events from webhook' + end optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking' given snowplow_enabled: ->(val) { val } do requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname' diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index b506192fe1c..f1ec1024492 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -84,7 +84,8 @@ module API attrs = process_create_params(declared_params(include_missing: false)) - service_response = ::Snippets::CreateService.new(project: nil, current_user: current_user, params: attrs).execute + spam_params = ::Spam::SpamParams.new_from_request(request: request) + service_response = ::Snippets::CreateService.new(project: nil, current_user: current_user, params: attrs, spam_params: spam_params).execute snippet = service_response.payload[:snippet] if service_response.success? @@ -126,7 +127,8 @@ module API attrs = process_update_params(declared_params(include_missing: false)) - service_response = ::Snippets::UpdateService.new(project: nil, current_user: current_user, params: attrs).execute(snippet) + spam_params = ::Spam::SpamParams.new_from_request(request: request) + service_response = ::Snippets::UpdateService.new(project: nil, current_user: current_user, params: attrs, spam_params: spam_params).execute(snippet) snippet = service_response.payload[:snippet] diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index fe23a111b7f..e4133713c1f 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -31,6 +31,7 @@ module API optional :push_events, type: Boolean, desc: "Trigger hook on push events" optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events" optional :merge_requests_events, type: Boolean, desc: "Trigger hook on tag push events" + optional :repository_update_events, type: Boolean, desc: "Trigger hook on repository update events" optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" end post do diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb index 7deec15dcac..43c75206b88 100644 --- a/lib/api/usage_data.rb +++ b/lib/api/usage_data.rb @@ -4,7 +4,7 @@ module API class UsageData < ::API::Base before { authenticate_non_get! } - feature_category :usage_ping + feature_category :service_ping namespace 'usage_data' do before do diff --git a/lib/api/usage_data_non_sql_metrics.rb b/lib/api/usage_data_non_sql_metrics.rb index 63a14a223f5..d9e0d153e58 100644 --- a/lib/api/usage_data_non_sql_metrics.rb +++ b/lib/api/usage_data_non_sql_metrics.rb @@ -4,7 +4,7 @@ module API class UsageDataNonSqlMetrics < ::API::Base before { authenticated_as_admin! } - feature_category :usage_ping + feature_category :service_ping namespace 'usage_data' do before do diff --git a/lib/api/usage_data_queries.rb b/lib/api/usage_data_queries.rb index 0ad9ad7650c..22e83fe0294 100644 --- a/lib/api/usage_data_queries.rb +++ b/lib/api/usage_data_queries.rb @@ -4,7 +4,7 @@ module API class UsageDataQueries < ::API::Base before { authenticated_as_admin! } - feature_category :usage_ping + feature_category :service_ping namespace 'usage_data' do before do diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 8b0745c6b5b..75df0e050a6 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -7,7 +7,7 @@ module API before { authenticate! } before { authorize! :admin_build, user_project } - feature_category :continuous_integration + feature_category :pipeline_authoring helpers Helpers::VariablesHelpers |