diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 09:08:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 09:08:42 +0000 |
commit | b76ae638462ab0f673e5915986070518dd3f9ad3 (patch) | |
tree | bdab0533383b52873be0ec0eb4d3c66598ff8b91 /lib/api | |
parent | 434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff) | |
download | gitlab-ce-b76ae638462ab0f673e5915986070518dd3f9ad3.tar.gz |
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'lib/api')
57 files changed, 1217 insertions, 982 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb index f9e89191a36..40f1b2fa9d3 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -153,10 +153,14 @@ module API mount ::API::Branches mount ::API::BroadcastMessages mount ::API::BulkImports + mount ::API::Ci::JobArtifacts + mount ::API::Ci::Jobs mount ::API::Ci::Pipelines mount ::API::Ci::PipelineSchedules mount ::API::Ci::Runner mount ::API::Ci::Runners + mount ::API::Ci::Triggers + mount ::API::Ci::Variables mount ::API::Commits mount ::API::CommitStatuses mount ::API::ContainerRegistryEvent @@ -184,14 +188,13 @@ module API mount ::API::GroupMilestones mount ::API::Groups mount ::API::GroupContainerRepositories + mount ::API::GroupDebianDistributions mount ::API::GroupVariables mount ::API::ImportBitbucketServer mount ::API::ImportGithub mount ::API::IssueLinks mount ::API::Invitations mount ::API::Issues - mount ::API::JobArtifacts - mount ::API::Jobs mount ::API::Keys mount ::API::Labels mount ::API::Lint @@ -268,14 +271,12 @@ module API mount ::API::Tags mount ::API::Templates mount ::API::Todos - mount ::API::Triggers mount ::API::Unleash mount ::API::UsageData mount ::API::UsageDataQueries mount ::API::UsageDataNonSqlMetrics mount ::API::UserCounts mount ::API::Users - mount ::API::Variables mount ::API::Version mount ::API::Wikis end diff --git a/lib/api/appearance.rb b/lib/api/appearance.rb index fe498bf611b..1eaa4167a7d 100644 --- a/lib/api/appearance.rb +++ b/lib/api/appearance.rb @@ -48,3 +48,5 @@ module API end end end + +API::Appearance.prepend_mod diff --git a/lib/api/badges.rb b/lib/api/badges.rb index 04f155be4e1..d7c850c2f40 100644 --- a/lib/api/badges.rb +++ b/lib/api/badges.rb @@ -8,7 +8,7 @@ module API helpers ::API::Helpers::BadgesHelpers - feature_category :continuous_integration + feature_category :projects helpers do def find_source_if_admin(source_type) diff --git a/lib/api/bulk_imports.rb b/lib/api/bulk_imports.rb index 189851cee65..0705a8285c1 100644 --- a/lib/api/bulk_imports.rb +++ b/lib/api/bulk_imports.rb @@ -8,7 +8,10 @@ module API helpers do def bulk_imports - @bulk_imports ||= ::BulkImports::ImportsFinder.new(user: current_user, status: params[:status]).execute + @bulk_imports ||= ::BulkImports::ImportsFinder.new( + user: current_user, + status: params[:status] + ).execute end def bulk_import @@ -16,7 +19,11 @@ module API end def bulk_import_entities - @bulk_import_entities ||= ::BulkImports::EntitiesFinder.new(user: current_user, bulk_import: bulk_import, status: params[:status]).execute + @bulk_import_entities ||= ::BulkImports::EntitiesFinder.new( + user: current_user, + bulk_import: bulk_import, + status: params[:status] + ).execute end def bulk_import_entity @@ -27,13 +34,44 @@ module API before { authenticate! } resource :bulk_imports do + desc 'Start a new GitLab Migration' do + detail 'This feature was introduced in GitLab 14.2.' + end + params do + requires :configuration, type: Hash, desc: 'The source GitLab instance configuration' do + requires :url, type: String, desc: 'Source GitLab instance URL' + requires :access_token, type: String, desc: 'Access token to the source GitLab instance' + end + requires :entities, type: Array, desc: 'List of entities to import' do + requires :source_type, type: String, desc: 'Source entity type (only `group_entity` is supported)', + values: %w[group_entity] + requires :source_full_path, type: String, desc: 'Source full path of the entity to import' + requires :destination_name, type: String, desc: 'Destination name for the entity' + requires :destination_namespace, type: String, desc: 'Destination namespace for the entity' + end + end + post do + response = BulkImportService.new( + current_user, + params[:entities], + url: params[:configuration][:url], + access_token: params[:configuration][:access_token] + ).execute + + if response.success? + present response.payload, with: Entities::BulkImport + else + render_api_error!(response.message, response.http_status) + end + end + 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' + desc: 'Return GitLab Migrations with specified status' end get do present paginate(bulk_imports), with: Entities::BulkImport @@ -45,10 +83,13 @@ module API params do use :pagination optional :status, type: String, values: ::BulkImports::Entity.all_human_statuses, - desc: "Return all GitLab Migrations' entities with specified status" + desc: "Return all GitLab Migrations' entities with specified status" end get :entities do - entities = ::BulkImports::EntitiesFinder.new(user: current_user, status: params[:status]).execute + entities = ::BulkImports::EntitiesFinder.new( + user: current_user, + status: params[:status] + ).execute present paginate(entities), with: Entities::BulkImports::Entity end @@ -69,7 +110,7 @@ module API 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' + desc: 'Return import entities with specified status' use :pagination end get ':import_id/entities' do diff --git a/lib/api/ci/helpers/runner.rb b/lib/api/ci/helpers/runner.rb new file mode 100644 index 00000000000..b9662b822fb --- /dev/null +++ b/lib/api/ci/helpers/runner.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +module API + module Ci + module Helpers + module Runner + include Gitlab::Utils::StrongMemoize + + prepend_mod_with('API::Ci::Helpers::Runner') # rubocop: disable Cop/InjectEnterpriseEditionModule + + JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN' + JOB_TOKEN_PARAM = :token + + def runner_registration_token_valid? + 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 + + current_runner + .heartbeat(get_runner_details_from_request) + end + + def get_runner_details_from_request + return get_runner_ip unless params['info'].present? + + attributes_for_keys(%w(name version revision platform architecture), params['info']) + .merge(get_runner_config_from_request) + .merge(get_runner_ip) + end + + def get_runner_ip + { ip_address: ip_address } + end + + def current_runner + token = params[:token] + + if token + ::Gitlab::Database::LoadBalancing::RackMiddleware + .stick_or_unstick(env, :runner, token) + end + + strong_memoize(:current_runner) do + ::Ci::Runner.find_by_token(token.to_s) + end + end + + # HTTP status codes to terminate the job on GitLab Runner: + # - 403 + def authenticate_job!(require_running: true) + job = current_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? + forbidden!('Job has been erased!') if job.erased? + + if require_running + job_forbidden!(job, 'Job is not running') unless job.running? + end + + job.runner&.heartbeat(get_runner_ip) + + job + end + + def current_job + id = params[:id] + + if id + ::Gitlab::Database::LoadBalancing::RackMiddleware + .stick_or_unstick(env, :build, id) + end + + strong_memoize(:current_job) do + ::Ci::Build.find_by_id(id) + end + end + + def job_token_valid?(job) + token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s + token && job.valid_token?(token) + end + + def job_forbidden!(job, reason) + header 'Job-Status', job.status + forbidden!(reason) + end + + def set_application_context + return unless current_job + + Gitlab::ApplicationContext.push( + user: -> { current_job.user }, + project: -> { current_job.project } + ) + end + + def track_ci_minutes_usage!(_build, _runner) + # noop: overridden in EE + end + + private + + def get_runner_config_from_request + { config: attributes_for_keys(%w(gpus), params.dig('info', 'config')) } + end + end + end + end +end diff --git a/lib/api/ci/job_artifacts.rb b/lib/api/ci/job_artifacts.rb new file mode 100644 index 00000000000..6431436b50d --- /dev/null +++ b/lib/api/ci/job_artifacts.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +module API + module Ci + class JobArtifacts < ::API::Base + before { authenticate_non_get! } + + feature_category :build_artifacts + + # EE::API::Ci::JobArtifacts would override the following helpers + helpers do + def authorize_download_artifacts! + authorize_read_builds! + end + end + + prepend_mod_with('API::Ci::JobArtifacts') # rubocop: disable Cop/InjectEnterpriseEditionModule + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Download the artifacts archive from a job' do + detail 'This feature was introduced in GitLab 8.10' + end + params do + requires :ref_name, type: String, desc: 'The ref from repository' + requires :job, type: String, desc: 'The name for the job' + end + route_setting :authentication, job_token_allowed: true + get ':id/jobs/artifacts/:ref_name/download', + requirements: { ref_name: /.+/ } do + authorize_download_artifacts! + + latest_build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name]) + authorize_read_job_artifacts!(latest_build) + + present_carrierwave_file!(latest_build.artifacts_file) + end + + desc 'Download a specific file from artifacts archive from a ref' do + detail 'This feature was introduced in GitLab 11.5' + end + params do + requires :ref_name, type: String, desc: 'The ref from repository' + requires :job, type: String, desc: 'The name for the job' + requires :artifact_path, type: String, desc: 'Artifact path' + end + route_setting :authentication, job_token_allowed: true + get ':id/jobs/artifacts/:ref_name/raw/*artifact_path', + format: false, + requirements: { ref_name: /.+/ } do + authorize_download_artifacts! + + build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name]) + authorize_read_job_artifacts!(build) + + path = Gitlab::Ci::Build::Artifacts::Path + .new(params[:artifact_path]) + + bad_request! unless path.valid? + + send_artifacts_entry(build.artifacts_file, path) + end + + desc 'Download the artifacts archive from a job' do + detail 'This feature was introduced in GitLab 8.5' + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + route_setting :authentication, job_token_allowed: true + get ':id/jobs/:job_id/artifacts' do + authorize_download_artifacts! + + build = find_build!(params[:job_id]) + authorize_read_job_artifacts!(build) + + present_carrierwave_file!(build.artifacts_file) + end + + desc 'Download a specific file from artifacts archive' do + detail 'This feature was introduced in GitLab 10.0' + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + requires :artifact_path, type: String, desc: 'Artifact path' + end + route_setting :authentication, job_token_allowed: true + get ':id/jobs/:job_id/artifacts/*artifact_path', format: false do + authorize_download_artifacts! + + build = find_build!(params[:job_id]) + authorize_read_job_artifacts!(build) + + not_found! unless build.available_artifacts? + + path = Gitlab::Ci::Build::Artifacts::Path + .new(params[:artifact_path]) + + bad_request! unless path.valid? + + send_artifacts_entry(build.artifacts_file, path) + end + + desc 'Keep the artifacts to prevent them from being deleted' do + success ::API::Entities::Ci::Job + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + post ':id/jobs/:job_id/artifacts/keep' do + authorize_update_builds! + + build = find_build!(params[:job_id]) + authorize!(:update_build, build) + break not_found!(build) unless build.artifacts? + + build.keep_artifacts! + + status 200 + present build, with: ::API::Entities::Ci::Job + end + + desc 'Delete the artifacts files from a job' do + detail 'This feature was introduced in GitLab 11.9' + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + delete ':id/jobs/:job_id/artifacts' do + authorize_destroy_artifacts! + build = find_build!(params[:job_id]) + authorize!(:destroy_artifacts, build) + + build.erase_erasable_artifacts! + + status :no_content + end + end + end + end +end diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb new file mode 100644 index 00000000000..eea1637c32a --- /dev/null +++ b/lib/api/ci/jobs.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true + +module API + module Ci + class Jobs < ::API::Base + include PaginationParams + before { authenticate! } + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + params do + requires :id, type: String, desc: 'The ID of a project' + end + + helpers do + params :optional_scope do + optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', + values: ::CommitStatus::AVAILABLE_STATUSES, + coerce_with: ->(scope) { + case scope + when String + [scope] + when ::Hash + scope.values + when ::Array + scope + else + ['unknown'] + end + } + end + end + + desc 'Get a projects jobs' do + success Entities::Ci::Job + end + params do + use :optional_scope + use :pagination + end + # rubocop: disable CodeReuse/ActiveRecord + get ':id/jobs', feature_category: :continuous_integration do + authorize_read_builds! + + builds = user_project.builds.order('id DESC') + builds = filter_builds(builds, params[:scope]) + + builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, :tags, pipeline: :project) + present paginate(builds), with: Entities::Ci::Job + end + # rubocop: enable CodeReuse/ActiveRecord + + desc 'Get a specific job of a project' do + success Entities::Ci::Job + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + get ':id/jobs/:job_id', feature_category: :continuous_integration do + authorize_read_builds! + + build = find_build!(params[:job_id]) + + present build, with: Entities::Ci::Job + end + + # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace + # is saved in the DB instead of file). But before that, we need to consider how to replace the value of + # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. + desc 'Get a trace of a specific job of a project' + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + get ':id/jobs/:job_id/trace', feature_category: :continuous_integration do + authorize_read_builds! + + build = find_build!(params[:job_id]) + + authorize_read_build_trace!(build) if build + + header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" + content_type 'text/plain' + env['api.format'] = :binary + + # The trace can be nil bu body method expects a string as an argument. + trace = build.trace.raw || '' + body trace + end + + desc 'Cancel a specific job of a project' do + success Entities::Ci::Job + end + params do + requires :job_id, type: Integer, desc: 'The ID of a job' + end + post ':id/jobs/:job_id/cancel', feature_category: :continuous_integration do + authorize_update_builds! + + build = find_build!(params[:job_id]) + authorize!(:update_build, build) + + build.cancel + + present build, with: Entities::Ci::Job + end + + desc 'Retry a specific build of a project' do + success Entities::Ci::Job + end + params do + requires :job_id, type: Integer, desc: 'The ID of a build' + end + post ':id/jobs/:job_id/retry', feature_category: :continuous_integration do + authorize_update_builds! + + build = find_build!(params[:job_id]) + authorize!(:update_build, build) + break forbidden!('Job is not retryable') unless build.retryable? + + build = ::Ci::Build.retry(build, current_user) + + present build, with: Entities::Ci::Job + end + + desc 'Erase job (remove artifacts and the trace)' do + success Entities::Ci::Job + end + params do + requires :job_id, type: Integer, desc: 'The ID of a build' + end + post ':id/jobs/:job_id/erase', feature_category: :continuous_integration do + authorize_update_builds! + + build = find_build!(params[:job_id]) + authorize!(:erase_build, build) + break forbidden!('Job is not erasable!') unless build.erasable? + + build.erase(erased_by: current_user) + present build, with: Entities::Ci::Job + end + + desc 'Trigger an actionable job (manual, delayed, etc)' do + success Entities::Ci::JobBasic + detail 'This feature was added in GitLab 8.11' + end + params do + requires :job_id, type: Integer, desc: 'The ID of a Job' + end + + post ":id/jobs/:job_id/play", feature_category: :continuous_integration do + authorize_read_builds! + + job = find_job!(params[:job_id]) + + authorize!(:play_job, job) + + bad_request!("Unplayable Job") unless job.playable? + + job.play(current_user) + + status 200 + + if job.is_a?(::Ci::Build) + present job, with: Entities::Ci::Job + else + present job, with: Entities::Ci::Bridge + end + end + end + + resource :job do + desc 'Get current project using job token' do + success Entities::Ci::Job + end + route_setting :authentication, job_token_allowed: true + get '', feature_category: :continuous_integration do + validate_current_authenticated_job + + present current_authenticated_job, with: Entities::Ci::Job + end + end + + helpers do + # rubocop: disable CodeReuse/ActiveRecord + def filter_builds(builds, scope) + return builds if scope.nil? || scope.empty? + + available_statuses = ::CommitStatus::AVAILABLE_STATUSES + + unknown = scope - available_statuses + render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? + + 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 +end + +API::Ci::Jobs.prepend_mod_with('API::Ci::Jobs') diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index 339c0e779f9..4d6d38f2dce 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -44,7 +44,7 @@ module API optional :ref, type: String, desc: 'The ref of pipelines' optional :sha, type: String, desc: 'The sha of pipelines' optional :yaml_errors, type: Boolean, desc: 'Returns pipelines with invalid configurations' - optional :name, type: String, desc: 'The name of the user who triggered pipelines' + optional :name, type: String, desc: '(deprecated) The name of the user who triggered pipelines' optional :username, type: String, desc: 'The username of the user who triggered pipelines' optional :updated_before, type: DateTime, desc: 'Return pipelines updated before the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ' optional :updated_after, type: DateTime, desc: 'Return pipelines updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ' @@ -52,13 +52,14 @@ module API desc: 'Order pipelines' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Sort pipelines' + optional :source, type: String, values: ::Ci::Pipeline.sources.keys end get ':id/pipelines' do authorize! :read_pipeline, user_project authorize! :read_build, user_project pipelines = ::Ci::PipelinesFinder.new(user_project, current_user, params).execute - present paginate(pipelines), with: Entities::Ci::PipelineBasic + present paginate(pipelines), with: Entities::Ci::PipelineBasic, project: user_project end desc 'Create a new pipeline' do @@ -78,12 +79,11 @@ module API .merge(variables_attributes: params[:variables]) .except(:variables) - new_pipeline = ::Ci::CreatePipelineService.new(user_project, - current_user, - pipeline_params) - .execute(:api, ignore_skip_ci: true, save_on_errors: false) + response = ::Ci::CreatePipelineService.new(user_project, current_user, pipeline_params) + .execute(:api, ignore_skip_ci: true, save_on_errors: false) + new_pipeline = response.payload - if new_pipeline.persisted? + if response.success? present new_pipeline, with: Entities::Ci::Pipeline else render_validation_error!(new_pipeline) @@ -188,6 +188,19 @@ module API present pipeline.test_reports, with: TestReportEntity, details: true end + desc 'Gets the test report summary for a given pipeline' do + detail 'This feature was introduced in GitLab 14.2' + success TestReportSummaryEntity + end + params do + requires :pipeline_id, type: Integer, desc: 'The pipeline ID' + end + get ':id/pipelines/:pipeline_id/test_report_summary' do + authorize! :read_build, pipeline + + present pipeline.test_report_summary, with: TestReportSummaryEntity + end + desc 'Deletes a pipeline' do detail 'This feature was introduced in GitLab 11.6' http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']] diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 0bac6fe2054..aabcf34952c 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -3,12 +3,10 @@ module API module Ci class Runner < ::API::Base - helpers ::API::Helpers::Runner + helpers ::API::Ci::Helpers::Runner content_type :txt, 'text/plain' - feature_category :runner - resource :runners do desc 'Registers a new Runner' do success Entities::Ci::RunnerRegistrationDetails @@ -26,7 +24,7 @@ module API optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: %q(List of Runner's tags) optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job' end - post '/' do + post '/', feature_category: :runner do attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :access_level, :maximum_timeout]) .merge(get_runner_details_from_request) @@ -59,7 +57,7 @@ module API params do requires :token, type: String, desc: %q(Runner's authentication token) end - delete '/' do + delete '/', feature_category: :runner do authenticate_runner! destroy_conditionally!(current_runner) @@ -71,7 +69,7 @@ module API params do requires :token, type: String, desc: %q(Runner's authentication token) end - post '/verify' do + post '/verify', feature_category: :runner do authenticate_runner! status 200 body "200" @@ -123,7 +121,7 @@ module API formatter :build_json, ->(object, _) { object } parser :build_json, ::Grape::Parser::Json - post '/request' do + post '/request', feature_category: :continuous_integration do authenticate_runner! unless current_runner.active? @@ -177,7 +175,7 @@ module API end optional :exit_code, type: Integer, desc: %q(Job's exit code) end - put '/:id' do + put '/:id', feature_category: :continuous_integration do job = authenticate_job! Gitlab::Metrics.add_event(:update_build) @@ -204,7 +202,7 @@ module API requires :id, type: Integer, desc: %q(Job's ID) optional :token, type: String, desc: %q(Job's authentication token) end - patch '/:id/trace' do + patch '/:id/trace', feature_category: :continuous_integration do job = authenticate_job! error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range') @@ -249,7 +247,7 @@ module API optional :artifact_type, type: String, desc: %q(The type of artifact), default: 'archive', values: ::Ci::JobArtifact.file_types.keys end - post '/:id/artifacts/authorize' do + post '/:id/artifacts/authorize', feature_category: :build_artifacts do not_allowed! unless Gitlab.config.artifacts.enabled require_gitlab_workhorse! @@ -285,7 +283,7 @@ module API default: 'zip', values: ::Ci::JobArtifact.file_formats.keys optional :metadata, type: ::API::Validations::Types::WorkhorseFile, desc: %(The artifact metadata to store (generated by Multipart middleware)) end - post '/:id/artifacts' do + post '/:id/artifacts', feature_category: :build_artifacts do not_allowed! unless Gitlab.config.artifacts.enabled require_gitlab_workhorse! @@ -314,7 +312,7 @@ module API optional :token, type: String, desc: %q(Job's authentication token) optional :direct_download, default: false, type: Boolean, desc: %q(Perform direct download from remote storage instead of proxying artifacts) end - get '/:id/artifacts' do + get '/:id/artifacts', feature_category: :build_artifacts do job = authenticate_job!(require_running: false) present_carrierwave_file!(job.artifacts_file, supports_direct_download: params[:direct_download]) diff --git a/lib/api/ci/triggers.rb b/lib/api/ci/triggers.rb new file mode 100644 index 00000000000..6a2b16e1568 --- /dev/null +++ b/lib/api/ci/triggers.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +module API + module Ci + class Triggers < ::API::Base + include PaginationParams + + HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase + + feature_category :continuous_integration + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Trigger a GitLab project pipeline' do + success Entities::Ci::Pipeline + end + params do + requires :ref, type: String, desc: 'The commit sha or name of a branch or tag', allow_blank: false + requires :token, type: String, desc: 'The unique token of trigger or job token' + 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.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20758') + + forbidden! if gitlab_pipeline_hook_request? + + # validate variables + params[:variables] = params[:variables].to_h + unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) } + render_api_error!('variables needs to be a map of key-valued strings', 400) + end + + project = find_project(params[:id]) + not_found! unless project + + result = ::Ci::PipelineTriggerService.new(project, nil, params).execute + not_found! unless result + + if result.error? + render_api_error!(result[:message], result[:http_status]) + else + present result[:pipeline], with: Entities::Ci::Pipeline + end + end + + desc 'Get triggers list' do + success Entities::Trigger + end + params do + use :pagination + end + # rubocop: disable CodeReuse/ActiveRecord + get ':id/triggers' do + authenticate! + authorize! :admin_build, user_project + + triggers = user_project.triggers.includes(:trigger_requests) + + present paginate(triggers), with: Entities::Trigger, current_user: current_user + end + # rubocop: enable CodeReuse/ActiveRecord + + desc 'Get specific trigger of a project' do + success Entities::Trigger + end + params do + requires :trigger_id, type: Integer, desc: 'The trigger ID' + end + get ':id/triggers/:trigger_id' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find(params.delete(:trigger_id)) + break not_found!('Trigger') unless trigger + + present trigger, with: Entities::Trigger, current_user: current_user + end + + desc 'Create a trigger' do + success Entities::Trigger + end + params do + requires :description, type: String, desc: 'The trigger description' + end + post ':id/triggers' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.create( + declared_params(include_missing: false).merge(owner: current_user)) + + if trigger.valid? + present trigger, with: Entities::Trigger, current_user: current_user + else + render_validation_error!(trigger) + end + end + + desc 'Update a trigger' do + success Entities::Trigger + end + params do + requires :trigger_id, type: Integer, desc: 'The trigger ID' + optional :description, type: String, desc: 'The trigger description' + end + put ':id/triggers/:trigger_id' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find(params.delete(:trigger_id)) + break not_found!('Trigger') unless trigger + + authorize! :admin_trigger, trigger + + if trigger.update(declared_params(include_missing: false)) + present trigger, with: Entities::Trigger, current_user: current_user + else + render_validation_error!(trigger) + end + end + + desc 'Delete a trigger' do + success Entities::Trigger + end + params do + requires :trigger_id, type: Integer, desc: 'The trigger ID' + end + delete ':id/triggers/:trigger_id' do + authenticate! + authorize! :admin_build, user_project + + trigger = user_project.triggers.find(params.delete(:trigger_id)) + break not_found!('Trigger') unless trigger + + destroy_conditionally!(trigger) + end + end + + helpers do + def gitlab_pipeline_hook_request? + request.get_header(HTTP_GITLAB_EVENT_HEADER) == WebHookService.hook_to_event(:pipeline_hooks) + end + end + end + end +end diff --git a/lib/api/ci/variables.rb b/lib/api/ci/variables.rb new file mode 100644 index 00000000000..9c04d5e9923 --- /dev/null +++ b/lib/api/ci/variables.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +module API + module Ci + class Variables < ::API::Base + include PaginationParams + + before { authenticate! } + before { authorize! :admin_build, user_project } + + feature_category :pipeline_authoring + + helpers ::API::Helpers::VariablesHelpers + + params do + requires :id, type: String, desc: 'The ID of a project' + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Get project variables' do + success Entities::Ci::Variable + end + params do + use :pagination + end + get ':id/variables' do + variables = user_project.variables + present paginate(variables), with: Entities::Ci::Variable + end + + desc 'Get a specific variable from a project' do + success Entities::Ci::Variable + end + params do + requires :key, type: String, desc: 'The key of the variable' + end + # rubocop: disable CodeReuse/ActiveRecord + get ':id/variables/:key' do + variable = find_variable(user_project, params) + not_found!('Variable') unless variable + + present variable, with: Entities::Ci::Variable + end + # rubocop: enable CodeReuse/ActiveRecord + + desc 'Create a new variable in a project' do + success Entities::Ci::Variable + end + params do + requires :key, type: String, desc: 'The key of the variable' + requires :value, type: String, desc: 'The value of the variable' + optional :protected, type: Boolean, desc: 'Whether the variable is protected' + optional :masked, type: Boolean, desc: 'Whether the variable is masked' + optional :variable_type, type: String, values: ::Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var' + optional :environment_scope, type: String, desc: 'The environment_scope of the variable' + end + post ':id/variables' do + variable = ::Ci::ChangeVariableService.new( + container: user_project, + current_user: current_user, + params: { action: :create, variable_params: declared_params(include_missing: false) } + ).execute + + if variable.valid? + present variable, with: Entities::Ci::Variable + else + render_validation_error!(variable) + end + end + + desc 'Update an existing variable from a project' do + success Entities::Ci::Variable + end + params do + optional :key, type: String, desc: 'The key of the variable' + optional :value, type: String, desc: 'The value of the variable' + optional :protected, type: Boolean, desc: 'Whether the variable is protected' + optional :masked, type: Boolean, desc: 'Whether the variable is masked' + optional :variable_type, type: String, values: ::Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file' + optional :environment_scope, type: String, desc: 'The environment_scope of the variable' + optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production' + end + # rubocop: disable CodeReuse/ActiveRecord + put ':id/variables/:key' do + variable = find_variable(user_project, params) + not_found!('Variable') unless variable + + variable = ::Ci::ChangeVariableService.new( + container: user_project, + current_user: current_user, + params: { action: :update, variable: variable, variable_params: declared_params(include_missing: false).except(:key, :filter) } + ).execute + + if variable.valid? + present variable, with: Entities::Ci::Variable + else + render_validation_error!(variable) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + desc 'Delete an existing variable from a project' do + success Entities::Ci::Variable + end + params do + requires :key, type: String, desc: 'The key of the variable' + optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production' + end + # rubocop: disable CodeReuse/ActiveRecord + delete ':id/variables/:key' do + variable = find_variable(user_project, params) + not_found!('Variable') unless variable + + ::Ci::ChangeVariableService.new( + container: user_project, + current_user: current_user, + params: { action: :destroy, variable: variable } + ).execute + + no_content! + end + # rubocop: enable CodeReuse/ActiveRecord + end + end + end +end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 541a37b0abe..5d8985455ad 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -47,7 +47,7 @@ module API path = params[:path] before = params[:until] after = params[:since] - ref = params[:ref_name].presence || user_project.try(:default_branch) || 'master' unless params[:all] + ref = params[:ref_name].presence || user_project.default_branch unless params[:all] offset = (params[:page] - 1) * params[:per_page] all = params[:all] with_stats = params[:with_stats] diff --git a/lib/api/concerns/packages/debian_distribution_endpoints.rb b/lib/api/concerns/packages/debian_distribution_endpoints.rb index 4670c3e3521..798e583b87a 100644 --- a/lib/api/concerns/packages/debian_distribution_endpoints.rb +++ b/lib/api/concerns/packages/debian_distribution_endpoints.rb @@ -80,6 +80,8 @@ module API use :optional_distribution_params end get '/' do + authorize_read_package!(project_or_group) + distribution_params = declared_params(include_missing: false) distributions = ::Packages::Debian::DistributionsFinder.new(project_or_group, distribution_params).execute @@ -96,6 +98,8 @@ module API requires :codename, type: String, regexp: Gitlab::Regex.debian_distribution_regex, desc: 'The Debian Codename' end get '/:codename' do + authorize_read_package!(project_or_group) + distribution = ::Packages::Debian::DistributionsFinder.new(project_or_group, codename: params[:codename]).execute.last! present distribution, with: ::API::Entities::Packages::Debian::Distribution diff --git a/lib/api/concerns/packages/debian_package_endpoints.rb b/lib/api/concerns/packages/debian_package_endpoints.rb index 7740ba6bfa6..0acc015f366 100644 --- a/lib/api/concerns/packages/debian_package_endpoints.rb +++ b/lib/api/concerns/packages/debian_package_endpoints.rb @@ -6,8 +6,6 @@ module API module DebianPackageEndpoints extend ActiveSupport::Concern - LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze - PACKAGE_REGEX = API::NO_SLASH_URL_PART_REGEX DISTRIBUTION_REQUIREMENTS = { distribution: ::Packages::Debian::DISTRIBUTION_REGEX }.freeze @@ -15,14 +13,6 @@ module API component: ::Packages::Debian::COMPONENT_REGEX, architecture: ::Packages::Debian::ARCHITECTURE_REGEX }.freeze - COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS = { - component: ::Packages::Debian::COMPONENT_REGEX, - letter: LETTER_REGEX, - source_package: PACKAGE_REGEX - }.freeze - FILE_NAME_REQUIREMENTS = { - file_name: API::NO_SLASH_URL_PART_REGEX - }.freeze included do feature_category :package_registry @@ -31,109 +21,106 @@ module API helpers ::API::Helpers::Packages::BasicAuthHelpers include ::API::Helpers::Authentication - namespace 'packages/debian' do - authenticate_with do |accept| - accept.token_types(:personal_access_token, :deploy_token, :job_token) - .sent_through(:http_basic_auth) + helpers do + params :shared_package_file_params do + requires :distribution, type: String, desc: 'The Debian Codename or Suite', regexp: Gitlab::Regex.debian_distribution_regex + requires :letter, type: String, desc: 'The Debian Classification (first-letter or lib-first-letter)' + requires :package_name, type: String, desc: 'The Debian Source Package Name', regexp: Gitlab::Regex.debian_package_name_regex + requires :package_version, type: String, desc: 'The Debian Source Package Version', regexp: Gitlab::Regex.debian_version_regex + requires :file_name, type: String, desc: 'The Debian File Name' 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 + def distribution_from!(container) + ::Packages::Debian::DistributionsFinder.new(container, codename_or_suite: params[:distribution]).execute.last! end - format :txt - content_type :txt, 'text/plain' + def present_package_file! + not_found! unless params[:package_name].start_with?(params[:letter]) - params do - requires :distribution, type: String, desc: 'The Debian Codename', regexp: Gitlab::Regex.debian_distribution_regex + package_file = distribution_from!(user_project).package_files.with_file_name(params[:file_name]).last! + + present_carrierwave_file!(package_file.file) end + end - namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do - # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release.gpg - desc 'The Release file signature' do - detail 'This feature was introduced in GitLab 13.5' - end + authenticate_with do |accept| + accept.token_types(:personal_access_token, :deploy_token, :job_token) + .sent_through(:http_basic_auth) + end - route_setting :authentication, authenticate_non_public: true - get 'Release.gpg' do - not_found! - end + rescue_from ArgumentError do |e| + render_api_error!(e.message, 400) + end - # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release - desc 'The unsigned Release file' do - detail 'This feature was introduced in GitLab 13.5' - end + rescue_from ActiveRecord::RecordInvalid do |e| + render_api_error!(e.message, 400) + end - route_setting :authentication, authenticate_non_public: true - get 'Release' do - present_release_file - end + format :txt + content_type :txt, 'text/plain' - # GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease - desc 'The signed Release file' do - detail 'This feature was introduced in GitLab 13.5' - end + params do + requires :distribution, type: String, desc: 'The Debian Codename or Suite', regexp: Gitlab::Regex.debian_distribution_regex + end - route_setting :authentication, authenticate_non_public: true - get 'InRelease' do - # Signature to be added in 7.3 of https://gitlab.com/groups/gitlab-org/-/epics/6057#note_582697034 - present_release_file - end + namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do + # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release.gpg + desc 'The Release file signature' do + detail 'This feature was introduced in GitLab 13.5' + end - params do - requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex - requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex - end + route_setting :authentication, authenticate_non_public: true + get 'Release.gpg' do + distribution_from!(project_or_group).file_signature + end - namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do - # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages - desc 'The binary files index' do - detail 'This feature was introduced in GitLab 13.5' - end - - route_setting :authentication, authenticate_non_public: true - get 'Packages' do - 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 + # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release + desc 'The unsigned Release file' do + detail 'This feature was introduced in GitLab 13.5' + end + + route_setting :authentication, authenticate_non_public: true + get 'Release' do + present_carrierwave_file!(distribution_from!(project_or_group).file) + end + + # GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease + desc 'The signed Release file' do + detail 'This feature was introduced in GitLab 13.5' + end + + route_setting :authentication, authenticate_non_public: true + get 'InRelease' do + present_carrierwave_file!(distribution_from!(project_or_group).signed_file) end params do requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex - requires :letter, type: String, desc: 'The Debian Classification (first-letter or lib-first-letter)' - requires :source_package, type: String, desc: 'The Debian Source Package Name', regexp: Gitlab::Regex.debian_package_name_regex + requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex end - namespace 'pool/:component/:letter/:source_package', requirements: COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS do - # GET {projects|groups}/:id/packages/debian/pool/:component/:letter/:source_package/:file_name - params do - requires :file_name, type: String, desc: 'The Debian File Name' - end - desc 'The package' do + namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do + # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages + desc 'The binary files index' do detail 'This feature was introduced in GitLab 13.5' end route_setting :authentication, authenticate_non_public: true - get ':file_name', requirements: FILE_NAME_REQUIREMENTS do - # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286 - 'TODO File' + get 'Packages' do + 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 191ed42a5b8..29f5047230a 100644 --- a/lib/api/debian_group_packages.rb +++ b/lib/api/debian_group_packages.rb @@ -2,35 +2,50 @@ module API class DebianGroupPackages < ::API::Base - params do - requires :id, type: String, desc: 'The ID of a group' - end + PACKAGE_FILE_REQUIREMENTS = ::API::DebianProjectPackages::PACKAGE_FILE_REQUIREMENTS.merge( + project_id: %r{[0-9]+}.freeze + ).freeze resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - rescue_from ArgumentError do |e| - render_api_error!(e.message, 400) - end + helpers do + def user_project + @project ||= find_project!(params[:project_id]) + end - rescue_from ActiveRecord::RecordInvalid do |e| - render_api_error!(e.message, 400) + def project_or_group + user_group + end end - before do + after_validation do require_packages_enabled! - not_found! unless ::Feature.enabled?(:debian_packages, user_group) + not_found! unless ::Feature.enabled?(:debian_group_packages, user_group) authorize_read_package!(user_group) end - namespace ':id/-' do - helpers do - def project_or_group - user_group - end - end + params do + requires :id, type: String, desc: 'The ID of a group' + end + namespace ':id/-/packages/debian' do include ::API::Concerns::Packages::DebianPackageEndpoints + + # GET groups/:id/packages/debian/pool/:distribution/:project_id/:letter/:package_name/:package_version/:file_name + params do + requires :project_id, type: Integer, desc: 'The Project Id' + use :shared_package_file_params + end + + desc 'The package' do + detail 'This feature was introduced in GitLab 14.2' + end + + route_setting :authentication, authenticate_non_public: true + get 'pool/:distribution/:project_id/:letter/:package_name/:package_version/:file_name', requirements: PACKAGE_FILE_REQUIREMENTS do + present_package_file! + end end end end diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb index 70ddf9dea37..497ce2f4356 100644 --- a/lib/api/debian_project_packages.rb +++ b/lib/api/debian_project_packages.rb @@ -2,17 +2,23 @@ module API class DebianProjectPackages < ::API::Base - params do - requires :id, type: String, desc: 'The ID of a project' - end + PACKAGE_FILE_REQUIREMENTS = { + id: API::NO_SLASH_URL_PART_REGEX, + distribution: ::Packages::Debian::DISTRIBUTION_REGEX, + letter: ::Packages::Debian::LETTER_REGEX, + package_name: API::NO_SLASH_URL_PART_REGEX, + package_version: API::NO_SLASH_URL_PART_REGEX, + file_name: API::NO_SLASH_URL_PART_REGEX + }.freeze + FILE_NAME_REQUIREMENTS = { + file_name: API::NO_SLASH_URL_PART_REGEX + }.freeze resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - rescue_from ArgumentError do |e| - render_api_error!(e.message, 400) - end - - rescue_from ActiveRecord::RecordInvalid do |e| - render_api_error!(e.message, 400) + helpers do + def project_or_group + user_project + end end after_validation do @@ -23,20 +29,32 @@ module API authorize_read_package! end - namespace ':id' do - helpers do - def project_or_group - user_project - end - end + params do + requires :id, type: String, desc: 'The ID of a project' + end + namespace ':id/packages/debian' do include ::API::Concerns::Packages::DebianPackageEndpoints + # GET projects/:id/packages/debian/pool/:distribution/:letter/:package_name/:package_version/:file_name + params do + use :shared_package_file_params + end + + desc 'The package' do + detail 'This feature was introduced in GitLab 14.2' + end + + route_setting :authentication, authenticate_non_public: true + get 'pool/:distribution/:letter/:package_name/:package_version/:file_name', requirements: PACKAGE_FILE_REQUIREMENTS do + present_package_file! + end + params do requires :file_name, type: String, desc: 'The file name' end - namespace 'packages/debian/:file_name', requirements: FILE_NAME_REQUIREMENTS do + namespace ':file_name', requirements: FILE_NAME_REQUIREMENTS do format :txt content_type :json, Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE diff --git a/lib/api/entities/ci/job_request/dependency.rb b/lib/api/entities/ci/job_request/dependency.rb index 2c6ed417714..2672a4a245b 100644 --- a/lib/api/entities/ci/job_request/dependency.rb +++ b/lib/api/entities/ci/job_request/dependency.rb @@ -6,7 +6,7 @@ module API module JobRequest class Dependency < Grape::Entity expose :id, :name, :token - expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.artifacts? } + expose :artifacts_file, using: Entities::Ci::JobArtifactFile, if: ->(job, _) { job.available_artifacts? } end end end diff --git a/lib/api/entities/ci/pipeline_basic.rb b/lib/api/entities/ci/pipeline_basic.rb index f4f2356c812..8086062dc9b 100644 --- a/lib/api/entities/ci/pipeline_basic.rb +++ b/lib/api/entities/ci/pipeline_basic.rb @@ -7,6 +7,8 @@ module API expose :id, :project_id, :sha, :ref, :status expose :created_at, :updated_at + expose :source, if: ->(pipeline, options) { ::Feature.enabled?(:pipeline_source_filter, options[:project], default_enabled: :yaml) } + expose :web_url do |pipeline, _options| Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline) end diff --git a/lib/api/entities/error_tracking.rb b/lib/api/entities/error_tracking.rb index c762c274486..a38e00ca295 100644 --- a/lib/api/entities/error_tracking.rb +++ b/lib/api/entities/error_tracking.rb @@ -8,6 +8,7 @@ module API expose :project_name expose :sentry_external_url expose :api_url + expose :integrated end end end diff --git a/lib/api/entities/issue_basic.rb b/lib/api/entities/issue_basic.rb index 6c332870228..ab248523028 100644 --- a/lib/api/entities/issue_basic.rb +++ b/lib/api/entities/issue_basic.rb @@ -23,7 +23,7 @@ module API expose :issue_type, as: :type, format_with: :upcase, - documentation: { type: "String", desc: "One of #{::Issue.issue_types.keys.map(&:upcase)}" } + documentation: { type: "String", desc: "One of #{::WorkItem::Type.base_types.keys.map(&:upcase)}" } expose :assignee, using: ::API::Entities::UserBasic do |issue| issue.assignees.first diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index f5f565e5b07..890b42ed8c8 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -71,6 +71,7 @@ module API expose(:pages_access_level) { |project, options| project.project_feature.string_access_level(:pages) } expose(:operations_access_level) { |project, options| project.project_feature.string_access_level(:operations) } expose(:analytics_access_level) { |project, options| project.project_feature.string_access_level(:analytics) } + expose(:container_registry_access_level) { |project, options| project.project_feature.string_access_level(:container_registry) } expose :emails_disabled expose :shared_runners_enabled diff --git a/lib/api/entities/project_with_access.rb b/lib/api/entities/project_with_access.rb index c53a712a879..ac89cb52e43 100644 --- a/lib/api/entities/project_with_access.rb +++ b/lib/api/entities/project_with_access.rb @@ -26,8 +26,10 @@ module API # rubocop: disable CodeReuse/ActiveRecord def self.preload_relation(projects_relation, options = {}) relation = super(projects_relation, options) - project_ids = relation.select('projects.id') - namespace_ids = relation.select(:namespace_id) + # use reselect to override the existing select and + # prevent an error `subquery has too many columns` + project_ids = relation.reselect('projects.id') + namespace_ids = relation.reselect(:namespace_id) options[:project_members] = options[:current_user] .project_members diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 57e548183b0..e50da4264b5 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -77,7 +77,7 @@ module API desc "Delete multiple stopped review apps" do detail "Remove multiple stopped review environments older than a specific age" - success Entities::Environment + success Entities::EnvironmentBasic end params do optional :before, type: Time, desc: "The timestamp before which environments can be deleted. Defaults to 30 days ago.", default: -> { 30.days.ago } @@ -90,8 +90,8 @@ module API result = ::Environments::ScheduleToDeleteReviewAppsService.new(user_project, current_user, params).execute response = { - scheduled_entries: Entities::Environment.represent(result.scheduled_entries), - unprocessable_entries: Entities::Environment.represent(result.unprocessable_entries) + scheduled_entries: Entities::EnvironmentBasic.represent(result.scheduled_entries), + unprocessable_entries: Entities::EnvironmentBasic.represent(result.unprocessable_entries) } if result.success? diff --git a/lib/api/error_tracking.rb b/lib/api/error_tracking.rb index 0e44c8b1081..3abf2831bd3 100644 --- a/lib/api/error_tracking.rb +++ b/lib/api/error_tracking.rb @@ -32,6 +32,7 @@ module API end params do requires :active, type: Boolean, desc: 'Specifying whether to enable or disable error tracking settings', allow_blank: false + optional :integrated, type: Boolean, desc: 'Specifying whether to enable or disable integrated error tracking' end patch ':id/error_tracking/settings/' do @@ -45,6 +46,10 @@ module API error_tracking_setting_attributes: { enabled: params[:active] } } + unless params[:integrated].nil? + update_params[:error_tracking_setting_attributes][:integrated] = params[:integrated] + end + result = ::Projects::Operations::UpdateService.new(user_project, current_user, update_params).execute if result[:status] == :success diff --git a/lib/api/error_tracking_collector.rb b/lib/api/error_tracking_collector.rb index 08ff8d2e4d1..13e8e476808 100644 --- a/lib/api/error_tracking_collector.rb +++ b/lib/api/error_tracking_collector.rb @@ -13,6 +13,7 @@ module API before do not_found!('Project') unless project not_found! unless feature_enabled? + not_found! unless active_client_key? end helpers do @@ -21,8 +22,24 @@ module API end def feature_enabled? - ::Feature.enabled?(:integrated_error_tracking, project) && - project.error_tracking_setting&.enabled? + project.error_tracking_setting&.enabled? && + project.error_tracking_setting&.integrated_client? + end + + def find_client_key(public_key) + return unless public_key.present? + + project.error_tracking_client_keys.active.find_by_public_key(public_key) + end + + def active_client_key? + begin + public_key = ::ErrorTracking::Collector::SentryAuthParser.parse(request)[:public_key] + rescue StandardError + bad_request!('Failed to parse sentry request') + end + + find_client_key(public_key) end end @@ -46,7 +63,7 @@ module API begin parsed_request = ::ErrorTracking::Collector::SentryRequestParser.parse(request) rescue StandardError - render_api_error!('Failed to parse sentry request', 400) + bad_request!('Failed to parse sentry request') end type = parsed_request[:request_type] @@ -67,6 +84,9 @@ module API .execute end + # Collector should never return any information back. + # Because DSN and public key are designed for public use, + # it is safe only for submission of new events. no_content! end end diff --git a/lib/api/group_debian_distributions.rb b/lib/api/group_debian_distributions.rb new file mode 100644 index 00000000000..01a8774bd97 --- /dev/null +++ b/lib/api/group_debian_distributions.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module API + class GroupDebianDistributions < ::API::Base + params do + requires :id, type: String, desc: 'The ID of a group' + end + + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + rescue_from ArgumentError do |e| + render_api_error!(e.message, 400) + end + + rescue_from ActiveRecord::RecordInvalid do |e| + render_api_error!(e.message, 400) + end + + after_validation do + require_packages_enabled! + + not_found! unless ::Feature.enabled?(:debian_group_packages, user_group) + end + + namespace ':id/-' do + helpers do + def project_or_group + user_group + end + end + + include ::API::Concerns::Packages::DebianDistributionEndpoints + end + end + end +end diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index 8d52a0a5b4e..13daf05fc78 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -8,7 +8,7 @@ module API before { authorize! :admin_group, user_group } feature_category :continuous_integration - helpers Helpers::VariablesHelpers + helpers ::API::Helpers::VariablesHelpers params do requires :id, type: String, desc: 'The ID of a group' diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 9b6b28733ff..0896357cc73 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -35,7 +35,8 @@ module API :all_available, :custom_attributes, :owned, :min_access_level, - :include_parent_descendants + :include_parent_descendants, + :search ) find_params[:parent] = if params[:top_level_only] @@ -48,7 +49,6 @@ module API find_params.fetch(:all_available, current_user&.can_read_all_resources?) 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_groups(groups) @@ -128,10 +128,6 @@ module API 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') @@ -141,7 +137,7 @@ module API # rubocop: disable CodeReuse/ActiveRecord def handle_similarity_order(group, projects) - if params[:search].present? && Feature.enabled?(:similarity_search, group, default_enabled: true) + if params[:search].present? projects.sorted_by_similarity_desc(params[:search]) else order_options = { name: :asc } diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 3398d5da7f5..9c347148fd0 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -577,6 +577,10 @@ module API Gitlab::AppLogger.warn("Redis tracking event failed for event: #{event_name}, message: #{error.message}") end + def order_by_similarity?(allow_unauthorized: true) + params[:order_by] == 'similarity' && params[:search].present? && (allow_unauthorized || current_user.present?) + end + protected def project_finder_params_visibility_ce diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb index e38213532ba..72bdb32d38c 100644 --- a/lib/api/helpers/groups_helpers.rb +++ b/lib/api/helpers/groups_helpers.rb @@ -23,7 +23,7 @@ module API optional :mentions_disabled, type: Boolean, desc: 'Disable a group from getting mentioned' optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group' optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' - optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' + optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to default branch' optional :shared_runners_setting, type: String, values: ::Namespace::SHARED_RUNNERS_SETTINGS, desc: 'Enable/disable shared runners for the group and its subgroups and projects' end diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index bd0c2501220..e72bbb931f0 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -54,6 +54,14 @@ module API source.add_user(user, params[:access_level], current_user: current_user, expires_at: params[:expires_at]) end + def track_areas_of_focus(member, areas_of_focus) + return unless areas_of_focus + + areas_of_focus.each do |area_of_focus| + Gitlab::Tracking.event(::Members::CreateService.name, 'area_of_focus', label: area_of_focus, property: member.id.to_s) + end + end + def present_members(members) present members, with: Entities::Member, current_user: current_user, show_seat_info: params[:show_seat_info] end diff --git a/lib/api/helpers/packages/dependency_proxy_helpers.rb b/lib/api/helpers/packages/dependency_proxy_helpers.rb index 989c4e1761b..b8ae1dddd7e 100644 --- a/lib/api/helpers/packages/dependency_proxy_helpers.rb +++ b/lib/api/helpers/packages/dependency_proxy_helpers.rb @@ -5,11 +5,17 @@ module API module Packages module DependencyProxyHelpers REGISTRY_BASE_URLS = { - npm: 'https://registry.npmjs.org/' + npm: 'https://registry.npmjs.org/', + pypi: 'https://pypi.org/simple/' + }.freeze + + APPLICATION_SETTING_NAMES = { + npm: 'npm_package_requests_forwarding', + pypi: 'pypi_package_requests_forwarding' }.freeze def redirect_registry_request(forward_to_registry, package_type, options) - if forward_to_registry && redirect_registry_request_available? + if forward_to_registry && redirect_registry_request_available?(package_type) ::Gitlab::Tracking.event(self.options[:for].name, "#{package_type}_request_forward") redirect(registry_url(package_type, options)) else @@ -25,11 +31,20 @@ module API case package_type when :npm "#{base_url}#{options[:package_name]}" + when :pypi + "#{base_url}#{options[:package_name]}/" end end - def redirect_registry_request_available? - ::Gitlab::CurrentSettings.current_application_settings.npm_package_requests_forwarding + def redirect_registry_request_available?(package_type) + application_setting_name = APPLICATION_SETTING_NAMES[package_type] + + raise ArgumentError, "Can't find application setting for package_type #{package_type}" unless application_setting_name + + ::Gitlab::CurrentSettings + .current_application_settings + .attributes + .fetch(application_setting_name, false) end end end diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb index 2d556f889bf..ce5db52fdbc 100644 --- a/lib/api/helpers/packages/npm.rb +++ b/lib/api/helpers/packages/npm.rb @@ -49,28 +49,20 @@ module API when :project params[:id] when :instance - namespace_path = namespace_path_from_package_name + package_name = params[:package_name] + namespace_path = ::Packages::Npm.scope_of(package_name) next unless namespace_path namespace = Namespace.top_most .by_path(namespace_path) next unless namespace - finder = ::Packages::Npm::PackageFinder.new(params[:package_name], namespace: namespace) + finder = ::Packages::Npm::PackageFinder.new(package_name, namespace: namespace) finder.last&.project_id end end end - - # from "@scope/package-name" return "scope" or nil - def namespace_path_from_package_name - package_name = params[:package_name] - return unless package_name.starts_with?('@') - return unless package_name.include?('/') - - package_name.match(Gitlab::Regex.npm_package_name_regex)&.captures&.first - end end end end diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 272452bd8db..becd25595a6 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -35,13 +35,14 @@ module API optional :pages_access_level, type: String, values: %w(disabled private enabled public), desc: 'Pages access level. One of `disabled`, `private`, `enabled` or `public`' optional :operations_access_level, type: String, values: %w(disabled private enabled), desc: 'Operations access level. One of `disabled`, `private` or `enabled`' optional :analytics_access_level, type: String, values: %w(disabled private enabled), desc: 'Analytics access level. One of `disabled`, `private` or `enabled`' + optional :container_registry_access_level, type: String, values: %w(disabled private enabled), desc: 'Controls visibility of the container registry. One of `disabled`, `private` or `enabled`. `private` will make the container registry accessible only to project members (reporter role and above). `enabled` will make the container registry accessible to everyone who has access to the project. `disabled` will disable the container registry' optional :emails_disabled, type: Boolean, desc: 'Disable email notifications' optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis' optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push' optional :remove_source_branch_after_merge, type: Boolean, desc: 'Remove the source branch by default after merge' - optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' + optional :container_registry_enabled, type: Boolean, desc: 'Deprecated: Use :container_registry_access_level instead. Flag indication if the container registry is enabled for that project' optional :container_expiration_policy_attributes, type: Hash do use :optional_container_expiration_policy_params end @@ -124,7 +125,7 @@ module API :ci_config_path, :ci_default_git_depth, :ci_forward_deployment_enabled, - :container_registry_enabled, + :container_registry_access_level, :container_expiration_policy_attributes, :default_branch, :description, @@ -132,7 +133,10 @@ module API :forking_access_level, :issues_access_level, :lfs_enabled, + :merge_pipelines_enabled, :merge_requests_access_level, + :merge_requests_template, + :merge_trains_enabled, :merge_method, :name, :only_allow_merge_if_all_discussions_are_resolved, @@ -166,7 +170,8 @@ module API :jobs_enabled, :merge_requests_enabled, :wiki_enabled, - :snippets_enabled + :snippets_enabled, + :container_registry_enabled ] end diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb deleted file mode 100644 index a022d1a56ac..00000000000 --- a/lib/api/helpers/runner.rb +++ /dev/null @@ -1,121 +0,0 @@ -# frozen_string_literal: true - -module API - module Helpers - module Runner - include Gitlab::Utils::StrongMemoize - - prepend_mod_with('API::Helpers::Runner') # rubocop: disable Cop/InjectEnterpriseEditionModule - - JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN' - JOB_TOKEN_PARAM = :token - - def runner_registration_token_valid? - 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 - - current_runner - .heartbeat(get_runner_details_from_request) - end - - def get_runner_details_from_request - return get_runner_ip unless params['info'].present? - - attributes_for_keys(%w(name version revision platform architecture), params['info']) - .merge(get_runner_config_from_request) - .merge(get_runner_ip) - end - - def get_runner_ip - { ip_address: ip_address } - end - - def current_runner - token = params[:token] - - if token - ::Gitlab::Database::LoadBalancing::RackMiddleware - .stick_or_unstick(env, :runner, token) - end - - strong_memoize(:current_runner) do - ::Ci::Runner.find_by_token(token.to_s) - end - end - - # HTTP status codes to terminate the job on GitLab Runner: - # - 403 - def authenticate_job!(require_running: true) - job = current_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? - forbidden!('Job has been erased!') if job.erased? - - if require_running - job_forbidden!(job, 'Job is not running') unless job.running? - end - - job.runner&.heartbeat(get_runner_ip) - - job - end - - def current_job - id = params[:id] - - if id - ::Gitlab::Database::LoadBalancing::RackMiddleware - .stick_or_unstick(env, :build, id) - end - - strong_memoize(:current_job) do - ::Ci::Build.find_by_id(id) - end - end - - def job_token_valid?(job) - token = (params[JOB_TOKEN_PARAM] || env[JOB_TOKEN_HEADER]).to_s - token && job.valid_token?(token) - end - - def job_forbidden!(job, reason) - header 'Job-Status', job.status - forbidden!(reason) - end - - def set_application_context - return unless current_job - - Gitlab::ApplicationContext.push( - user: -> { current_job.user }, - project: -> { current_job.project } - ) - end - - def track_ci_minutes_usage!(_build, _runner) - # noop: overridden in EE - end - - private - - def get_runner_config_from_request - { config: attributes_for_keys(%w(gpus), params.dig('info', 'config')) } - end - end - end -end diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index a06b052847d..d740c626557 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -165,9 +165,9 @@ module API # Check whether an SSH key is known to GitLab # get '/authorized_keys', feature_category: :source_code_management do - fingerprint = Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint + fingerprint = Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint_sha256 - key = Key.find_by_fingerprint(fingerprint) + key = Key.find_by_fingerprint_sha256(fingerprint) not_found!('Key') if key.nil? present key, with: Entities::SSHKey end diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb index 46d8c0c958d..1f437ad5bd3 100644 --- a/lib/api/invitations.rb +++ b/lib/api/invitations.rb @@ -24,6 +24,7 @@ module API requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)' optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api' + optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon' end post ":id/invitations" do params[:source] = find_source(source_type, params[:id]) @@ -54,11 +55,11 @@ module API success Entities::Member end params do - requires :email, type: String, desc: 'The email address of the invitation.' - optional :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level).' - optional :expires_at, type: DateTime, desc: 'Date string in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`).' + requires :email, type: String, desc: 'The email address of the invitation' + optional :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)' + optional :expires_at, type: DateTime, desc: 'Date string in ISO 8601 format (`YYYY-MM-DDTHH:MM:SSZ`)' end - put ":id/invitations/:email", requirements: { email: /[^\/]+/ } do + put ":id/invitations/:email", requirements: { email: %r{[^/]+} } do source = find_source(source_type, params.delete(:id)) invite_email = params[:email] authorize_admin_source!(source_type, source) @@ -87,7 +88,7 @@ module API params do requires :email, type: String, desc: 'The email address of the invitation' end - delete ":id/invitations/:email", requirements: { email: /[^\/]+/ } do + delete ":id/invitations/:email", requirements: { email: %r{[^/]+} } do source = find_source(source_type, params[:id]) invite_email = params[:email] authorize_admin_source!(source_type, source) diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 54013d0e7b4..a6565f913e3 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -74,7 +74,7 @@ module API desc: 'Return issues sorted in `asc` or `desc` order.' optional :due_date, type: String, values: %w[0 overdue week month next_month_and_previous_two_weeks] << '', desc: 'Return issues that have no due date (`0`), or whose due date is this week, this month, between two weeks ago and next month, or which are overdue. Accepts: `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`, `0`' - optional :issue_type, type: String, values: Issue.issue_types.keys, desc: "The type of the issue. Accepts: #{Issue.issue_types.keys.join(', ')}" + optional :issue_type, type: String, values: WorkItem::Type.base_types.keys, desc: "The type of the issue. Accepts: #{WorkItem::Type.base_types.keys.join(', ')}" use :issues_stats_params use :pagination @@ -91,7 +91,7 @@ module API optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY' optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked" - optional :issue_type, type: String, values: Issue.issue_types.keys, desc: "The type of the issue. Accepts: #{Issue.issue_types.keys.join(', ')}" + optional :issue_type, type: String, values: WorkItem::Type.base_types.keys, desc: "The type of the issue. Accepts: #{WorkItem::Type.base_types.keys.join(', ')}" use :optional_issue_params_ee end diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb deleted file mode 100644 index beda4433e4f..00000000000 --- a/lib/api/job_artifacts.rb +++ /dev/null @@ -1,141 +0,0 @@ -# frozen_string_literal: true - -module API - class JobArtifacts < ::API::Base - before { authenticate_non_get! } - - feature_category :build_artifacts - - # EE::API::JobArtifacts would override the following helpers - helpers do - def authorize_download_artifacts! - authorize_read_builds! - end - end - - prepend_mod_with('API::JobArtifacts') # rubocop: disable Cop/InjectEnterpriseEditionModule - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Download the artifacts archive from a job' do - detail 'This feature was introduced in GitLab 8.10' - end - params do - requires :ref_name, type: String, desc: 'The ref from repository' - requires :job, type: String, desc: 'The name for the job' - end - route_setting :authentication, job_token_allowed: true - get ':id/jobs/artifacts/:ref_name/download', - requirements: { ref_name: /.+/ } do - authorize_download_artifacts! - - latest_build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name]) - authorize_read_job_artifacts!(latest_build) - - present_carrierwave_file!(latest_build.artifacts_file) - end - - desc 'Download a specific file from artifacts archive from a ref' do - detail 'This feature was introduced in GitLab 11.5' - end - params do - requires :ref_name, type: String, desc: 'The ref from repository' - requires :job, type: String, desc: 'The name for the job' - requires :artifact_path, type: String, desc: 'Artifact path' - end - route_setting :authentication, job_token_allowed: true - get ':id/jobs/artifacts/:ref_name/raw/*artifact_path', - format: false, - requirements: { ref_name: /.+/ } do - authorize_download_artifacts! - - build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name]) - authorize_read_job_artifacts!(build) - - path = Gitlab::Ci::Build::Artifacts::Path - .new(params[:artifact_path]) - - bad_request! unless path.valid? - - send_artifacts_entry(build.artifacts_file, path) - end - - desc 'Download the artifacts archive from a job' do - detail 'This feature was introduced in GitLab 8.5' - end - params do - requires :job_id, type: Integer, desc: 'The ID of a job' - end - route_setting :authentication, job_token_allowed: true - get ':id/jobs/:job_id/artifacts' do - authorize_download_artifacts! - - build = find_build!(params[:job_id]) - authorize_read_job_artifacts!(build) - - present_carrierwave_file!(build.artifacts_file) - end - - desc 'Download a specific file from artifacts archive' do - detail 'This feature was introduced in GitLab 10.0' - end - params do - requires :job_id, type: Integer, desc: 'The ID of a job' - requires :artifact_path, type: String, desc: 'Artifact path' - end - route_setting :authentication, job_token_allowed: true - get ':id/jobs/:job_id/artifacts/*artifact_path', format: false do - authorize_download_artifacts! - - build = find_build!(params[:job_id]) - authorize_read_job_artifacts!(build) - - not_found! unless build.available_artifacts? - - path = Gitlab::Ci::Build::Artifacts::Path - .new(params[:artifact_path]) - - bad_request! unless path.valid? - - send_artifacts_entry(build.artifacts_file, path) - end - - desc 'Keep the artifacts to prevent them from being deleted' do - success ::API::Entities::Ci::Job - end - params do - requires :job_id, type: Integer, desc: 'The ID of a job' - end - post ':id/jobs/:job_id/artifacts/keep' do - authorize_update_builds! - - build = find_build!(params[:job_id]) - authorize!(:update_build, build) - break not_found!(build) unless build.artifacts? - - build.keep_artifacts! - - status 200 - present build, with: ::API::Entities::Ci::Job - end - - desc 'Delete the artifacts files from a job' do - detail 'This feature was introduced in GitLab 11.9' - end - params do - requires :job_id, type: Integer, desc: 'The ID of a job' - end - delete ':id/jobs/:job_id/artifacts' do - authorize_destroy_artifacts! - build = find_build!(params[:job_id]) - authorize!(:destroy_artifacts, build) - - build.erase_erasable_artifacts! - - status :no_content - end - end - end -end diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb deleted file mode 100644 index 723a5b0fa3a..00000000000 --- a/lib/api/jobs.rb +++ /dev/null @@ -1,204 +0,0 @@ -# frozen_string_literal: true - -module API - class Jobs < ::API::Base - include PaginationParams - before { authenticate! } - - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - params do - requires :id, type: String, desc: 'The ID of a project' - end - - helpers do - params :optional_scope do - optional :scope, types: [String, Array[String]], desc: 'The scope of builds to show', - values: ::CommitStatus::AVAILABLE_STATUSES, - coerce_with: ->(scope) { - case scope - when String - [scope] - when ::Hash - scope.values - when ::Array - scope - else - ['unknown'] - end - } - end - end - - desc 'Get a projects jobs' do - success Entities::Ci::Job - end - params do - use :optional_scope - use :pagination - end - # rubocop: disable CodeReuse/ActiveRecord - get ':id/jobs', feature_category: :continuous_integration do - authorize_read_builds! - - builds = user_project.builds.order('id DESC') - builds = filter_builds(builds, params[:scope]) - - builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, :tags, pipeline: :project) - present paginate(builds), with: Entities::Ci::Job - end - # rubocop: enable CodeReuse/ActiveRecord - - desc 'Get a specific job of a project' do - success Entities::Ci::Job - end - params do - requires :job_id, type: Integer, desc: 'The ID of a job' - end - get ':id/jobs/:job_id', feature_category: :continuous_integration do - authorize_read_builds! - - build = find_build!(params[:job_id]) - - present build, with: Entities::Ci::Job - end - - # TODO: We should use `present_disk_file!` and leave this implementation for backward compatibility (when build trace - # is saved in the DB instead of file). But before that, we need to consider how to replace the value of - # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse. - desc 'Get a trace of a specific job of a project' - params do - requires :job_id, type: Integer, desc: 'The ID of a job' - end - get ':id/jobs/:job_id/trace', feature_category: :continuous_integration do - authorize_read_builds! - - build = find_build!(params[:job_id]) - - authorize_read_build_trace!(build) if build - - header 'Content-Disposition', "infile; filename=\"#{build.id}.log\"" - content_type 'text/plain' - env['api.format'] = :binary - - # The trace can be nil bu body method expects a string as an argument. - trace = build.trace.raw || '' - body trace - end - - desc 'Cancel a specific job of a project' do - success Entities::Ci::Job - end - params do - requires :job_id, type: Integer, desc: 'The ID of a job' - end - post ':id/jobs/:job_id/cancel', feature_category: :continuous_integration do - authorize_update_builds! - - build = find_build!(params[:job_id]) - authorize!(:update_build, build) - - build.cancel - - present build, with: Entities::Ci::Job - end - - desc 'Retry a specific build of a project' do - success Entities::Ci::Job - end - params do - requires :job_id, type: Integer, desc: 'The ID of a build' - end - post ':id/jobs/:job_id/retry', feature_category: :continuous_integration do - authorize_update_builds! - - build = find_build!(params[:job_id]) - authorize!(:update_build, build) - break forbidden!('Job is not retryable') unless build.retryable? - - build = ::Ci::Build.retry(build, current_user) - - present build, with: Entities::Ci::Job - end - - desc 'Erase job (remove artifacts and the trace)' do - success Entities::Ci::Job - end - params do - requires :job_id, type: Integer, desc: 'The ID of a build' - end - post ':id/jobs/:job_id/erase', feature_category: :continuous_integration do - authorize_update_builds! - - build = find_build!(params[:job_id]) - authorize!(:erase_build, build) - break forbidden!('Job is not erasable!') unless build.erasable? - - build.erase(erased_by: current_user) - present build, with: Entities::Ci::Job - end - - desc 'Trigger an actionable job (manual, delayed, etc)' do - success Entities::Ci::JobBasic - detail 'This feature was added in GitLab 8.11' - end - params do - requires :job_id, type: Integer, desc: 'The ID of a Job' - end - - post ":id/jobs/:job_id/play", feature_category: :continuous_integration do - authorize_read_builds! - - job = find_job!(params[:job_id]) - - authorize!(:play_job, job) - - bad_request!("Unplayable Job") unless job.playable? - - job.play(current_user) - - status 200 - - if job.is_a?(::Ci::Build) - present job, with: Entities::Ci::Job - else - present job, with: Entities::Ci::Bridge - end - end - end - - resource :job do - desc 'Get current project using job token' do - success Entities::Ci::Job - end - route_setting :authentication, job_token_allowed: true - get '', feature_category: :continuous_integration do - validate_current_authenticated_job - - present current_authenticated_job, with: Entities::Ci::Job - end - end - - helpers do - # rubocop: disable CodeReuse/ActiveRecord - def filter_builds(builds, scope) - return builds if scope.nil? || scope.empty? - - available_statuses = ::CommitStatus::AVAILABLE_STATUSES - - unknown = scope - available_statuses - render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? - - 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_mod_with('API::Jobs') diff --git a/lib/api/members.rb b/lib/api/members.rb index 70e13e8d4ae..7130635281a 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -94,6 +94,7 @@ module API requires :user_id, types: [Integer, String], desc: 'The user ID of the new member or multiple IDs separated by commas.' optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'members-api' + optional :areas_of_focus, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Areas the inviter wants the member to focus upon' end # rubocop: disable CodeReuse/ActiveRecord post ":id/members" do @@ -119,7 +120,12 @@ module API not_allowed! # This currently can only be reached in EE elsif member.valid? && member.persisted? present_members(member) - Gitlab::Tracking.event(::Members::CreateService.name, 'create_member', label: params[:invite_source], property: 'existing_user', user: current_user) + Gitlab::Tracking.event(::Members::CreateService.name, + 'create_member', + label: params[:invite_source], + property: 'existing_user', + user: current_user) + track_areas_of_focus(member, params[:areas_of_focus]) else render_validation_error!(member) end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index a9617482557..7ab57982907 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -404,6 +404,7 @@ module API pipeline = ::MergeRequests::CreatePipelineService .new(project: user_project, current_user: current_user, params: { allow_duplicate: true }) .execute(find_merge_request_with_access(params[:merge_request_iid])) + .payload if pipeline.nil? not_allowed! diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index 9d41c2f148f..c2d839571a6 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -27,12 +27,15 @@ module API end params do optional :search, type: String, desc: "Search query for namespaces" + optional :owned_only, type: Boolean, desc: "Owned namespaces only" use :pagination use :optional_list_params_ee end get do - namespaces = current_user.admin ? Namespace.all : current_user.namespaces + owned_only = params[:owned_only] == true + + namespaces = current_user.admin ? Namespace.all : current_user.namespaces(owned_only: owned_only) namespaces = namespaces.include_route diff --git a/lib/api/project_debian_distributions.rb b/lib/api/project_debian_distributions.rb index 58edf51f4f7..f057251fb6b 100644 --- a/lib/api/project_debian_distributions.rb +++ b/lib/api/project_debian_distributions.rb @@ -19,8 +19,6 @@ module API require_packages_enabled! not_found! unless ::Feature.enabled?(:debian_packages, user_project) - - authorize_read_package! end namespace ':id' do diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb index acf9bfece65..fe0e837c596 100644 --- a/lib/api/project_templates.rb +++ b/lib/api/project_templates.rb @@ -12,7 +12,7 @@ module API before { authenticate_non_get! } - feature_category :templates + feature_category :source_code_management params do requires :id, type: String, desc: 'The ID of a project' diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 3b1d239398f..28bcb382ecf 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -45,6 +45,20 @@ module API end end + def support_order_by_similarity!(attrs) + return unless params[:order_by] == 'similarity' + + if order_by_similarity?(allow_unauthorized: false) + # Limit to projects the current user is a member of. + # Do not include all public projects because it + # could cause long running queries + attrs[:non_public] = true + attrs[:sort] = params['order_by'] + else + params[:order_by] = route.params['order_by'][:default] + end + end + def delete_project(user_project) destroy_conditionally!(user_project) do ::Projects::DestroyService.new(user_project, current_user, {}).async_execute @@ -93,8 +107,8 @@ module API params :sort_params do optional :order_by, type: String, - values: %w[id name path created_at updated_at last_activity_at] + Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS, - default: 'created_at', desc: "Return projects ordered by field. #{Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS.join(', ')} are only available to admins." + values: %w[id name path created_at updated_at last_activity_at similarity] + Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS, + default: 'created_at', desc: "Return projects ordered by field. #{Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS.join(', ')} are only available to admins. Similarity is available when searching and is limited to projects the user has access to." optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return projects sorted in ascending and descending order' end @@ -131,16 +145,17 @@ module API end def load_projects - params = project_finder_params - verify_project_filters!(params) + project_params = project_finder_params + support_order_by_similarity!(project_params) + verify_project_filters!(project_params) - ProjectsFinder.new(current_user: current_user, params: params).execute + ProjectsFinder.new(current_user: current_user, params: project_params).execute end def present_projects(projects, options = {}) verify_statistics_order_by_projects! - projects = reorder_projects(projects) + projects = reorder_projects(projects) unless order_by_similarity?(allow_unauthorized: false) projects = apply_filters(projects) records, options = paginate_with_strategies(projects, options[:request_scope]) do |projects| @@ -572,6 +587,27 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + desc 'Import members from another project' do + detail 'This feature was introduced in GitLab 14.2' + end + params do + requires :project_id, type: Integer, desc: 'The ID of the source project to import the members from.' + end + post ":id/import_project_members/:project_id", feature_category: :experimentation_expansion do + authorize! :admin_project, user_project + + source_project = Project.find_by_id(params[:project_id]) + not_found!('Project') unless source_project && can?(current_user, :read_project, source_project) + + result = ::Members::ImportProjectTeamService.new(current_user, params).execute + + if result + { status: result, message: 'Successfully imported' } + else + render_api_error!('Import failed', :unprocessable_entity) + end + end + desc 'Workhorse authorize the file upload' do detail 'This feature was introduced in GitLab 13.11' end diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index 7c5f8bb4d99..706c0702fce 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -10,6 +10,7 @@ module API helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::RelatedResourcesHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers + helpers ::API::Helpers::Packages::DependencyProxyHelpers include ::API::Helpers::Packages::BasicAuthHelpers::Constants feature_category :package_registry @@ -40,7 +41,7 @@ module API end params do - requires :id, type: Integer, desc: 'The ID of a group' + requires :id, type: String, desc: 'The ID of a group' end resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do after_validation do @@ -82,21 +83,26 @@ module API track_package_event('list_package', :pypi) - packages = Packages::Pypi::PackagesFinder.new(current_user, group, { package_name: params[:package_name] }).execute! - presenter = ::Packages::Pypi::PackagePresenter.new(packages, group) + packages = Packages::Pypi::PackagesFinder.new(current_user, group, { package_name: params[:package_name] }).execute + empty_packages = packages.empty? - # Adjusts grape output format - # to be HTML - content_type "text/html; charset=utf-8" - env['api.format'] = :binary + redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do + not_found!('Package') if empty_packages + presenter = ::Packages::Pypi::PackagePresenter.new(packages, group) - body presenter.body + # Adjusts grape output format + # to be HTML + content_type "text/html; charset=utf-8" + env['api.format'] = :binary + + body presenter.body + end end end end params do - requires :id, type: Integer, desc: 'The ID of a project' + requires :id, type: String, desc: 'The ID of a project' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do @@ -142,15 +148,20 @@ module API track_package_event('list_package', :pypi, project: authorized_user_project, namespace: authorized_user_project.namespace) - packages = Packages::Pypi::PackagesFinder.new(current_user, authorized_user_project, { package_name: params[:package_name] }).execute! - presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project) + packages = Packages::Pypi::PackagesFinder.new(current_user, authorized_user_project, { package_name: params[:package_name] }).execute + empty_packages = packages.empty? + + redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do + not_found!('Package') if empty_packages + presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project) - # Adjusts grape output format - # to be HTML - content_type "text/html; charset=utf-8" - env['api.format'] = :binary + # Adjusts grape output format + # to be HTML + content_type "text/html; charset=utf-8" + env['api.format'] = :binary - body presenter.body + body presenter.body + end end desc 'The PyPi Package upload endpoint' do diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index f274406e225..20320d1b7ae 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -29,14 +29,13 @@ module API not_found! end - def assign_blob_vars! + def assign_blob_vars!(limit:) authorize! :download_code, user_project @repo = user_project.repository begin - @blob = Gitlab::Git::Blob.raw(@repo, params[:sha]) - @blob.load_all_data!(@repo) + @blob = Gitlab::Git::Blob.raw(@repo, params[:sha], limit: limit) rescue StandardError not_found! 'Blob' end @@ -55,7 +54,7 @@ module API use :pagination end get ':id/repository/tree' do - ref = params[:ref] || user_project.try(:default_branch) || 'master' + ref = params[:ref] || user_project.default_branch path = params[:path] || nil commit = user_project.commit(ref) @@ -71,7 +70,8 @@ module API requires :sha, type: String, desc: 'The commit hash' end get ':id/repository/blobs/:sha/raw' do - assign_blob_vars! + # Load metadata enough to ask Workhorse to load the whole blob + assign_blob_vars!(limit: 0) no_cache_headers @@ -83,7 +83,7 @@ module API requires :sha, type: String, desc: 'The commit hash' end get ':id/repository/blobs/:sha' do - assign_blob_vars! + assign_blob_vars!(limit: -1) { size: @blob.size, diff --git a/lib/api/rubygem_packages.rb b/lib/api/rubygem_packages.rb index d7f9c584c67..9ef6ec03a41 100644 --- a/lib/api/rubygem_packages.rb +++ b/lib/api/rubygem_packages.rb @@ -101,7 +101,7 @@ module API package_file = nil - ActiveRecord::Base.transaction do + ApplicationRecord.transaction do package = ::Packages::CreateTemporaryPackageService.new( user_project, current_user, declared_params.merge(build: current_authenticated_job) ).execute(:rubygems, name: ::Packages::Rubygems::TEMPORARY_PACKAGE_NAME) diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 952bf09b1b1..aac195f0668 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -48,7 +48,7 @@ module API optional :default_artifacts_expire_in, type: String, desc: "Set the default expiration time for each job's artifacts" optional :default_ci_config_path, type: String, desc: 'The instance default CI/CD configuration file and path for new projects' optional :default_project_creation, type: Integer, values: ::Gitlab::Access.project_creation_values, desc: 'Determine if developers can create projects in the group' - optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' + optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to default branch' optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility' optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility' optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 6c8e2c69a6d..395aacced78 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -59,8 +59,6 @@ module API optional :message, type: String, desc: 'Specifying a message creates an annotated tag' end post ':id/repository/tags', :release_orchestration do - deprecate_release_notes unless params[:release_description].blank? - authorize_admin_tag result = ::Tags::CreateService.new(user_project, current_user) diff --git a/lib/api/templates.rb b/lib/api/templates.rb index b7fb35eac03..a595129fd6a 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -4,17 +4,18 @@ module API class Templates < ::API::Base include PaginationParams - feature_category :templates - GLOBAL_TEMPLATE_TYPES = { gitignores: { - gitlab_version: 8.8 + gitlab_version: 8.8, + feature_category: :source_code_management }, gitlab_ci_ymls: { - gitlab_version: 8.9 + gitlab_version: 8.9, + feature_category: :continuous_integration }, dockerfiles: { - gitlab_version: 8.15 + gitlab_version: 8.15, + feature_category: :source_code_management } }.freeze @@ -33,7 +34,7 @@ module API optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses' use :pagination end - get "templates/licenses" do + get "templates/licenses", feature_category: :source_code_management do popular = declared(params)[:popular] popular = to_boolean(popular) if popular.present? @@ -49,7 +50,7 @@ module API params do requires :name, type: String, desc: 'The name of the template' end - get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do + get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ }, feature_category: :source_code_management do template = TemplateFinder.build(:licenses, nil, name: params[:name]).execute not_found!('License') unless template.present? @@ -72,7 +73,7 @@ module API params do use :pagination end - get "templates/#{template_type}" do + get "templates/#{template_type}", feature_category: properties[:feature_category] do templates = ::Kaminari.paginate_array(TemplateFinder.build(template_type, nil).execute) present paginate(templates), with: Entities::TemplatesList end @@ -84,7 +85,7 @@ module API params do requires :name, type: String, desc: 'The name of the template' end - get "templates/#{template_type}/:name", requirements: { name: /[\w\.-]+/ } do + get "templates/#{template_type}/:name", requirements: { name: /[\w\.-]+/ }, feature_category: properties[:feature_category] do finder = TemplateFinder.build(template_type, nil, name: declared(params)[:name]) new_template = finder.execute diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb index 969122d7906..b8323304957 100644 --- a/lib/api/time_tracking_endpoints.rb +++ b/lib/api/time_tracking_endpoints.rb @@ -88,6 +88,7 @@ module API update_params = { spend_time: { duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)), + summary: params.delete(:summary), user_id: current_user.id } } diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb deleted file mode 100644 index a359083a9d2..00000000000 --- a/lib/api/triggers.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -module API - class Triggers < ::API::Base - include PaginationParams - - HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase - - feature_category :continuous_integration - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Trigger a GitLab project pipeline' do - success Entities::Ci::Pipeline - end - params do - requires :ref, type: String, desc: 'The commit sha or name of a branch or tag', allow_blank: false - requires :token, type: String, desc: 'The unique token of trigger or job token' - 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.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20758') - - forbidden! if gitlab_pipeline_hook_request? - - # validate variables - params[:variables] = params[:variables].to_h - unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) } - render_api_error!('variables needs to be a map of key-valued strings', 400) - end - - project = find_project(params[:id]) - not_found! unless project - - result = ::Ci::PipelineTriggerService.new(project, nil, params).execute - not_found! unless result - - if result.error? - render_api_error!(result[:message], result[:http_status]) - else - present result[:pipeline], with: Entities::Ci::Pipeline - end - end - - desc 'Get triggers list' do - success Entities::Trigger - end - params do - use :pagination - end - # rubocop: disable CodeReuse/ActiveRecord - get ':id/triggers' do - authenticate! - authorize! :admin_build, user_project - - triggers = user_project.triggers.includes(:trigger_requests) - - present paginate(triggers), with: Entities::Trigger, current_user: current_user - end - # rubocop: enable CodeReuse/ActiveRecord - - desc 'Get specific trigger of a project' do - success Entities::Trigger - end - params do - requires :trigger_id, type: Integer, desc: 'The trigger ID' - end - get ':id/triggers/:trigger_id' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.find(params.delete(:trigger_id)) - break not_found!('Trigger') unless trigger - - present trigger, with: Entities::Trigger, current_user: current_user - end - - desc 'Create a trigger' do - success Entities::Trigger - end - params do - requires :description, type: String, desc: 'The trigger description' - end - post ':id/triggers' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.create( - declared_params(include_missing: false).merge(owner: current_user)) - - if trigger.valid? - present trigger, with: Entities::Trigger, current_user: current_user - else - render_validation_error!(trigger) - end - end - - desc 'Update a trigger' do - success Entities::Trigger - end - params do - requires :trigger_id, type: Integer, desc: 'The trigger ID' - optional :description, type: String, desc: 'The trigger description' - end - put ':id/triggers/:trigger_id' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.find(params.delete(:trigger_id)) - break not_found!('Trigger') unless trigger - - authorize! :admin_trigger, trigger - - if trigger.update(declared_params(include_missing: false)) - present trigger, with: Entities::Trigger, current_user: current_user - else - render_validation_error!(trigger) - end - end - - desc 'Delete a trigger' do - success Entities::Trigger - end - params do - requires :trigger_id, type: Integer, desc: 'The trigger ID' - end - delete ':id/triggers/:trigger_id' do - authenticate! - authorize! :admin_build, user_project - - trigger = user_project.triggers.find(params.delete(:trigger_id)) - break not_found!('Trigger') unless trigger - - destroy_conditionally!(trigger) - end - end - - helpers do - def gitlab_pipeline_hook_request? - request.get_header(HTTP_GITLAB_EVENT_HEADER) == WebHookService.hook_to_event(:pipeline_hooks) - end - end - end -end diff --git a/lib/api/user_counts.rb b/lib/api/user_counts.rb index 31c923a219a..634dd0f2179 100644 --- a/lib/api/user_counts.rb +++ b/lib/api/user_counts.rb @@ -6,15 +6,17 @@ module API resource :user_counts do desc 'Return the user specific counts' do - detail 'Open MR Count' + detail 'Assigned open issues, assigned MRs and pending todos count' end get do unauthorized! unless current_user { merge_requests: current_user.assigned_open_merge_requests_count, # @deprecated + assigned_issues: current_user.assigned_open_issues_count, assigned_merge_requests: current_user.assigned_open_merge_requests_count, - review_requested_merge_requests: current_user.review_requested_open_merge_requests_count + review_requested_merge_requests: current_user.review_requested_open_merge_requests_count, + todos: current_user.todos_pending_count } end end diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb index 29e4a79110f..310054c298a 100644 --- a/lib/api/v3/github.rb +++ b/lib/api/v3/github.rb @@ -214,6 +214,8 @@ module API update_project_feature_usage_for(user_project) + next [] unless user_project.repo_exists? + branches = ::Kaminari.paginate_array(user_project.repository.branches.sort_by(&:name)) present paginate(branches), with: ::API::Github::Entities::Branch, project: user_project diff --git a/lib/api/variables.rb b/lib/api/variables.rb deleted file mode 100644 index 75df0e050a6..00000000000 --- a/lib/api/variables.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -module API - class Variables < ::API::Base - include PaginationParams - - before { authenticate! } - before { authorize! :admin_build, user_project } - - feature_category :pipeline_authoring - - helpers Helpers::VariablesHelpers - - params do - requires :id, type: String, desc: 'The ID of a project' - end - - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Get project variables' do - success Entities::Ci::Variable - end - params do - use :pagination - end - get ':id/variables' do - variables = user_project.variables - present paginate(variables), with: Entities::Ci::Variable - end - - desc 'Get a specific variable from a project' do - success Entities::Ci::Variable - end - params do - requires :key, type: String, desc: 'The key of the variable' - end - # rubocop: disable CodeReuse/ActiveRecord - get ':id/variables/:key' do - variable = find_variable(user_project, params) - not_found!('Variable') unless variable - - present variable, with: Entities::Ci::Variable - end - # rubocop: enable CodeReuse/ActiveRecord - - desc 'Create a new variable in a project' do - success Entities::Ci::Variable - end - params do - requires :key, type: String, desc: 'The key of the variable' - requires :value, type: String, desc: 'The value of the variable' - optional :protected, type: Boolean, desc: 'Whether the variable is protected' - optional :masked, type: Boolean, desc: 'Whether the variable is masked' - optional :variable_type, type: String, values: ::Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var' - optional :environment_scope, type: String, desc: 'The environment_scope of the variable' - end - post ':id/variables' do - variable = ::Ci::ChangeVariableService.new( - container: user_project, - current_user: current_user, - params: { action: :create, variable_params: declared_params(include_missing: false) } - ).execute - - if variable.valid? - present variable, with: Entities::Ci::Variable - else - render_validation_error!(variable) - end - end - - desc 'Update an existing variable from a project' do - success Entities::Ci::Variable - end - params do - optional :key, type: String, desc: 'The key of the variable' - optional :value, type: String, desc: 'The value of the variable' - optional :protected, type: Boolean, desc: 'Whether the variable is protected' - optional :masked, type: Boolean, desc: 'Whether the variable is masked' - optional :variable_type, type: String, values: ::Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file' - optional :environment_scope, type: String, desc: 'The environment_scope of the variable' - optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production' - end - # rubocop: disable CodeReuse/ActiveRecord - put ':id/variables/:key' do - variable = find_variable(user_project, params) - not_found!('Variable') unless variable - - variable = ::Ci::ChangeVariableService.new( - container: user_project, - current_user: current_user, - params: { action: :update, variable: variable, variable_params: declared_params(include_missing: false).except(:key, :filter) } - ).execute - - if variable.valid? - present variable, with: Entities::Ci::Variable - else - render_validation_error!(variable) - end - end - # rubocop: enable CodeReuse/ActiveRecord - - desc 'Delete an existing variable from a project' do - success Entities::Ci::Variable - end - params do - requires :key, type: String, desc: 'The key of the variable' - optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production' - end - # rubocop: disable CodeReuse/ActiveRecord - delete ':id/variables/:key' do - variable = find_variable(user_project, params) - not_found!('Variable') unless variable - - ::Ci::ChangeVariableService.new( - container: user_project, - current_user: current_user, - params: { action: :destroy, variable: variable } - ).execute - - no_content! - end - # rubocop: enable CodeReuse/ActiveRecord - end - end -end |