diff options
Diffstat (limited to 'lib/api')
61 files changed, 1007 insertions, 337 deletions
diff --git a/lib/api/admin/batched_background_migrations.rb b/lib/api/admin/batched_background_migrations.rb new file mode 100644 index 00000000000..675f3365bd3 --- /dev/null +++ b/lib/api/admin/batched_background_migrations.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +module API + module Admin + class BatchedBackgroundMigrations < ::API::Base + feature_category :database + urgency :low + + before do + authenticated_as_admin! + end + + namespace 'admin' do + resources 'batched_background_migrations/:id' do + desc 'Retrieve a batched background migration' + params do + optional :database, + type: String, + values: Gitlab::Database.all_database_names, + desc: 'The name of the database', + default: 'main' + requires :id, + type: Integer, + desc: 'The batched background migration id' + end + get do + Gitlab::Database::SharedModel.using_connection(base_model.connection) do + present_entity(batched_background_migration) + end + end + end + + resources 'batched_background_migrations' do + desc 'Get the list of the batched background migrations' + params do + optional :database, + type: String, + values: Gitlab::Database.all_database_names, + desc: 'The name of the database, the default `main`', + default: 'main' + end + get do + Gitlab::Database::SharedModel.using_connection(base_model.connection) do + migrations = Database::BatchedBackgroundMigrationsFinder.new(connection: base_model.connection).execute + present_entity(migrations) + end + end + end + + resources 'batched_background_migrations/:id/resume' do + desc 'Resume a batched background migration' + params do + optional :database, + type: String, + values: Gitlab::Database.all_database_names, + desc: 'The name of the database', + default: 'main' + requires :id, + type: Integer, + desc: 'The batched background migration id' + end + put do + Gitlab::Database::SharedModel.using_connection(base_model.connection) do + batched_background_migration.execute! + present_entity(batched_background_migration) + end + end + end + + resources 'batched_background_migrations/:id/pause' do + desc 'Pause a batched background migration' + params do + optional :database, + type: String, + values: Gitlab::Database.all_database_names, + desc: 'The name of the database', + default: 'main' + requires :id, + type: Integer, + desc: 'The batched background migration id' + end + put do + Gitlab::Database::SharedModel.using_connection(base_model.connection) do + batched_background_migration.pause! + present_entity(batched_background_migration) + end + end + end + end + + helpers do + def batched_background_migration + @batched_background_migration ||= Gitlab::Database::BackgroundMigration::BatchedMigration.find(params[:id]) + end + + def base_model + database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME + @base_model ||= Gitlab::Database.database_base_models[database] + end + + def present_entity(result) + present result, + with: ::API::Entities::BatchedBackgroundMigration + end + end + end + end +end diff --git a/lib/api/api.rb b/lib/api/api.rb index e4158eee37f..443bf1d649a 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -167,6 +167,7 @@ module API # Keep in alphabetical order mount ::API::AccessRequests + mount ::API::Admin::BatchedBackgroundMigrations mount ::API::Admin::Ci::Variables mount ::API::Admin::InstanceClusters mount ::API::Admin::PlanLimits @@ -237,7 +238,6 @@ module API mount ::API::ImportGithub mount ::API::Integrations mount ::API::Integrations::JiraConnect::Subscriptions - mount ::API::Integrations::Slack::Events mount ::API::Invitations mount ::API::IssueLinks mount ::API::Issues @@ -263,6 +263,7 @@ module API mount ::API::PackageFiles mount ::API::Pages mount ::API::PagesDomains + mount ::API::PersonalAccessTokens::SelfRevocation mount ::API::PersonalAccessTokens mount ::API::ProjectClusters mount ::API::ProjectContainerRepositories @@ -290,6 +291,7 @@ module API mount ::API::ResourceLabelEvents mount ::API::ResourceMilestoneEvents mount ::API::ResourceStateEvents + mount ::API::RpmProjectPackages mount ::API::RubygemPackages mount ::API::Search mount ::API::Settings @@ -316,6 +318,7 @@ module API mount ::API::Users mount ::API::Version mount ::API::Wikis + mount ::API::Ml::Mlflow end mount ::API::Internal::Base diff --git a/lib/api/branches.rb b/lib/api/branches.rb index b8444351029..5588818cbaf 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -52,25 +52,21 @@ module API merged_branch_names = repository.merged_branch_names(branches.map(&:name)) - if Feature.enabled?(:api_caching_branches, user_project, type: :development) - present_cached( - branches, - with: Entities::Branch, - current_user: current_user, - project: user_project, - merged_branch_names: merged_branch_names, - expires_in: 10.minutes, - cache_context: -> (branch) { [current_user&.cache_key, merged_branch_names.include?(branch.name)] } - ) - else - present( - branches, - with: Entities::Branch, - current_user: current_user, - project: user_project, - merged_branch_names: merged_branch_names - ) - end + expiry_time = if Feature.enabled?(:increase_branch_cache_expiry, type: :ops) + 60.minutes + else + 10.minutes + end + + present_cached( + branches, + with: Entities::Branch, + current_user: current_user, + project: user_project, + merged_branch_names: merged_branch_names, + expires_in: expiry_time, + cache_context: -> (branch) { [current_user&.cache_key, merged_branch_names.include?(branch.name)] } + ) end end @@ -146,7 +142,8 @@ module API branch = find_branch!(params[:branch]) protected_branch = user_project.protected_branches.find_by(name: branch.name) - protected_branch&.destroy + + ::ProtectedBranches::DestroyService.new(user_project, current_user).execute(protected_branch) if protected_branch present branch, with: Entities::Branch, current_user: current_user, project: user_project end diff --git a/lib/api/ci/job_artifacts.rb b/lib/api/ci/job_artifacts.rb index b843404e9d7..b3a0a9ef54a 100644 --- a/lib/api/ci/job_artifacts.rb +++ b/lib/api/ci/job_artifacts.rb @@ -143,7 +143,7 @@ module API reject_if_build_artifacts_size_refreshing!(build.project) - build.erase_erasable_artifacts! + ::Ci::JobArtifacts::DeleteService.new(build).execute status :no_content end diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb index cd5f1f77ced..6049993bf6f 100644 --- a/lib/api/ci/jobs.rb +++ b/lib/api/ci/jobs.rb @@ -142,7 +142,8 @@ module API reject_if_build_artifacts_size_refreshing!(build.project) - build.erase(erased_by: current_user) + ::Ci::BuildEraseService.new(build, current_user).execute + present build, with: Entities::Ci::Job end @@ -209,8 +210,8 @@ module API .select { |_role, role_access_level| role_access_level <= user_access_level } .map(&:first) - environment = if environment_slug = current_authenticated_job.persisted_environment&.slug - { slug: environment_slug } + environment = if persisted_environment = current_authenticated_job.persisted_environment + { tier: persisted_environment.tier, slug: persisted_environment.slug } end # See https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/kubernetes_ci_access.md#apiv4joballowed_agents-api diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index ec9b09a3419..4b578f8b7e5 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -93,7 +93,7 @@ module API params[:active] = !params.delete(:paused) if params.include?(:paused) update_service = ::Ci::Runners::UpdateRunnerService.new(runner) - if update_service.update(declared_params(include_missing: false)) + if update_service.execute(declared_params(include_missing: false)).success? present runner, with: Entities::Ci::RunnerDetails, current_user: current_user else render_validation_error!(runner) @@ -129,8 +129,17 @@ module API authenticate_list_runners_jobs!(runner) jobs = ::Ci::RunnerJobsFinder.new(runner, current_user, params).execute + jobs = jobs.preload( # rubocop: disable CodeReuse/ActiveRecord + [ + :user, + { pipeline: { project: [:route, { namespace: :route }] } }, + { project: [:route, { namespace: :route }] } + ] + ) + jobs = paginate(jobs) + jobs.each(&:commit) # batch loads all commits in the page - present paginate(jobs), with: Entities::Ci::JobBasicWithProject + present jobs, with: Entities::Ci::JobBasicWithProject end desc 'Reset runner authentication token' do @@ -352,7 +361,7 @@ module API def authenticate_list_runners_jobs!(runner) return if current_user.admin? - forbidden!("No access granted") unless can?(current_user, :read_runner, runner) + forbidden!("No access granted") unless can?(current_user, :read_builds, runner) end end end diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb index de59cb4a7c3..d9806fa37d1 100644 --- a/lib/api/composer_packages.rb +++ b/lib/api/composer_packages.rb @@ -150,17 +150,18 @@ module API get 'archives/*package_name', urgency: :default do authorize_read_package!(authorized_user_project) - metadata = authorized_user_project + package = authorized_user_project .packages .composer .with_name(params[:package_name]) .with_composer_target(params[:sha]) .first - &.composer_metadatum + metadata = package&.composer_metadatum not_found! unless metadata track_package_event('pull_package', :composer, project: authorized_user_project, namespace: authorized_user_project.namespace) + package.touch_last_downloaded_at send_git_archive authorized_user_project.repository, ref: metadata.target_sha, format: 'zip', append_sha: true end diff --git a/lib/api/concerns/packages/conan_endpoints.rb b/lib/api/concerns/packages/conan_endpoints.rb index a90269b565c..d8c2eb4ff33 100644 --- a/lib/api/concerns/packages/conan_endpoints.rb +++ b/lib/api/concerns/packages/conan_endpoints.rb @@ -135,7 +135,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true get 'packages/:conan_package_reference', urgency: :low do - authorize!(:read_package, project) + authorize_read_package!(project) presenter = ::Packages::Conan::PackagePresenter.new( package, @@ -154,7 +154,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true get urgency: :low do - authorize!(:read_package, project) + authorize_read_package!(project) presenter = ::Packages::Conan::PackagePresenter.new(package, current_user, project) @@ -237,7 +237,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true post 'packages/:conan_package_reference/upload_urls', urgency: :low do - authorize!(:read_package, project) + authorize_read_package!(project) status 200 present package_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls @@ -250,7 +250,7 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true post 'upload_urls', urgency: :low do - authorize!(:read_package, project) + authorize_read_package!(project) status 200 present recipe_upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls diff --git a/lib/api/concerns/packages/debian_package_endpoints.rb b/lib/api/concerns/packages/debian_package_endpoints.rb index e8d27448f02..2883944a745 100644 --- a/lib/api/concerns/packages/debian_package_endpoints.rb +++ b/lib/api/concerns/packages/debian_package_endpoints.rb @@ -35,12 +35,30 @@ module API ::Packages::Debian::DistributionsFinder.new(container, codename_or_suite: params[:distribution]).execute.last! end - def present_package_file! + def present_distribution_package_file! not_found! unless params[:package_name].start_with?(params[:letter]) package_file = distribution_from!(user_project).package_files.with_file_name(params[:file_name]).last! - present_carrierwave_file!(package_file.file) + present_package_file!(package_file) + end + + def present_index_file!(file_type) + relation = "::Packages::Debian::#{project_or_group.class.name}ComponentFile".constantize + + relation = relation + .preload_distribution + .with_container(project_or_group) + .with_codename_or_suite(params[:distribution]) + .with_component_name(params[:component]) + .with_file_type(file_type) + .with_architecture_name(params[:architecture]) + .with_compression_type(nil) + .order_created_asc + + relation = relation.with_file_sha256(params[:file_sha256]) if params[:file_sha256] + + present_carrierwave_file!(relation.last!.file) end end @@ -66,6 +84,7 @@ module API namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release.gpg + # https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files desc 'The Release file signature' do detail 'This feature was introduced in GitLab 13.5' end @@ -76,6 +95,7 @@ module API end # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release + # https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files desc 'The unsigned Release file' do detail 'This feature was introduced in GitLab 13.5' end @@ -86,6 +106,7 @@ module API end # GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease + # https://wiki.debian.org/DebianRepository/Format#A.22Release.22_files desc 'The signed Release file' do detail 'This feature was introduced in GitLab 13.5' end @@ -97,31 +118,87 @@ module API 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 - 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' + namespace ':component', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do + params do + requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex + end + + namespace 'debian-installer/binary-:architecture' do + # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/Packages + # https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices + desc 'The installer (udeb) binary files index' do + detail 'This feature was introduced in GitLab 15.4' + end + + route_setting :authentication, authenticate_non_public: true + get 'Packages' do + present_index_file!(:di_packages) + end + + # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/debian-installer/binary-:architecture/by-hash/SHA256/:file_sha256 + # https://wiki.debian.org/DebianRepository/Format?action=show&redirect=RepositoryFormat#indices_acquisition_via_hashsums_.28by-hash.29 + desc 'The installer (udeb) binary files index by hash' do + detail 'This feature was introduced in GitLab 15.4' + end + + route_setting :authentication, authenticate_non_public: true + get 'by-hash/SHA256/:file_sha256' do + present_index_file!(:di_packages) + end + end + + namespace 'source', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do + # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/source/Sources + # https://wiki.debian.org/DebianRepository/Format#A.22Sources.22_Indices + desc 'The source files index' do + detail 'This feature was introduced in GitLab 15.4' + end + + route_setting :authentication, authenticate_non_public: true + get 'Sources' do + present_index_file!(:sources) + end + + # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/source/by-hash/SHA256/:file_sha256 + # https://wiki.debian.org/DebianRepository/Format?action=show&redirect=RepositoryFormat#indices_acquisition_via_hashsums_.28by-hash.29 + desc 'The source files index by hash' do + detail 'This feature was introduced in GitLab 15.4' + end + + route_setting :authentication, authenticate_non_public: true + get 'by-hash/SHA256/:file_sha256' do + present_index_file!(:sources) + end + end + + params do + requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex 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) + namespace 'binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do + # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages + # https://wiki.debian.org/DebianRepository/Format#A.22Packages.22_Indices + 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 + present_index_file!(:packages) + end + + # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/by-hash/SHA256/:file_sha256 + # https://wiki.debian.org/DebianRepository/Format?action=show&redirect=RepositoryFormat#indices_acquisition_via_hashsums_.28by-hash.29 + desc 'The binary files index by hash' do + detail 'This feature was introduced in GitLab 15.4' + end + + route_setting :authentication, authenticate_non_public: true + get 'by-hash/SHA256/:file_sha256' do + present_index_file!(:packages) + end end end end diff --git a/lib/api/debian_group_packages.rb b/lib/api/debian_group_packages.rb index 8bf4ac22802..0962d749558 100644 --- a/lib/api/debian_group_packages.rb +++ b/lib/api/debian_group_packages.rb @@ -48,7 +48,7 @@ module API 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! + present_distribution_package_file! end end end diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb index 06846d8f36e..9dedc4390f7 100644 --- a/lib/api/debian_project_packages.rb +++ b/lib/api/debian_project_packages.rb @@ -51,7 +51,7 @@ module API 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! + present_distribution_package_file! end params do diff --git a/lib/api/entities/batched_background_migration.rb b/lib/api/entities/batched_background_migration.rb new file mode 100644 index 00000000000..eba17ff98f4 --- /dev/null +++ b/lib/api/entities/batched_background_migration.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class BatchedBackgroundMigration < Grape::Entity + expose :id + expose :job_class_name + expose :table_name + expose :status, &:status_name + expose :progress + expose :created_at + end + end +end diff --git a/lib/api/entities/ci/job_basic.rb b/lib/api/entities/ci/job_basic.rb index 0badde4089e..3d9318ec428 100644 --- a/lib/api/entities/ci/job_basic.rb +++ b/lib/api/entities/ci/job_basic.rb @@ -18,6 +18,12 @@ module API expose :web_url do |job, _options| Gitlab::Routing.url_helpers.project_job_url(job.project, job) end + + expose :project do + expose :ci_job_token_scope_enabled do |job| + job.project.ci_job_token_scope_enabled? + end + end end end end diff --git a/lib/api/entities/ci/job_request/image.rb b/lib/api/entities/ci/job_request/image.rb index 83f64da6050..92d68269265 100644 --- a/lib/api/entities/ci/job_request/image.rb +++ b/lib/api/entities/ci/job_request/image.rb @@ -8,7 +8,7 @@ module API expose :name, :entrypoint expose :ports, using: Entities::Ci::JobRequest::Port - expose :pull_policy, if: ->(_) { ::Feature.enabled?(:ci_docker_image_pull_policy) } + expose :pull_policy end end end diff --git a/lib/api/entities/ci/job_request/service.rb b/lib/api/entities/ci/job_request/service.rb index 7d494c7e516..128591058fe 100644 --- a/lib/api/entities/ci/job_request/service.rb +++ b/lib/api/entities/ci/job_request/service.rb @@ -8,7 +8,7 @@ module API expose :name, :entrypoint expose :ports, using: Entities::Ci::JobRequest::Port - expose :pull_policy, if: ->(_) { ::Feature.enabled?(:ci_docker_image_pull_policy) } + expose :pull_policy expose :alias, :command expose :variables end diff --git a/lib/api/entities/merge_request_reviewer.rb b/lib/api/entities/merge_request_reviewer.rb index 3bf2ccc36aa..a47321ef929 100644 --- a/lib/api/entities/merge_request_reviewer.rb +++ b/lib/api/entities/merge_request_reviewer.rb @@ -4,7 +4,6 @@ module API module Entities class MergeRequestReviewer < Grape::Entity expose :reviewer, as: :user, using: Entities::UserBasic - expose :updated_state_by, using: Entities::UserBasic expose :state expose :created_at end diff --git a/lib/api/entities/ml/mlflow/experiment.rb b/lib/api/entities/ml/mlflow/experiment.rb new file mode 100644 index 00000000000..cfe366feaab --- /dev/null +++ b/lib/api/entities/ml/mlflow/experiment.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class Experiment < Grape::Entity + expose :experiment do + expose :experiment_id + expose :name + expose :lifecycle_stage + expose :artifact_location + end + + private + + def lifecycle_stage + object.deleted_on? ? 'deleted' : 'active' + end + + def experiment_id + object.iid.to_s + end + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/new_experiment.rb b/lib/api/entities/ml/mlflow/new_experiment.rb new file mode 100644 index 00000000000..09791839850 --- /dev/null +++ b/lib/api/entities/ml/mlflow/new_experiment.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class NewExperiment < Grape::Entity + expose :experiment_id + + private + + def experiment_id + object.iid.to_s + end + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/run.rb b/lib/api/entities/ml/mlflow/run.rb new file mode 100644 index 00000000000..c679330206e --- /dev/null +++ b/lib/api/entities/ml/mlflow/run.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class Run < Grape::Entity + expose :run do + expose(:info) { |candidate| RunInfo.represent(candidate) } + expose(:data) { |candidate| {} } + end + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/run_info.rb b/lib/api/entities/ml/mlflow/run_info.rb new file mode 100644 index 00000000000..096950e349d --- /dev/null +++ b/lib/api/entities/ml/mlflow/run_info.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class RunInfo < Grape::Entity + expose :run_id + expose :run_id, as: :run_uuid + expose(:experiment_id) { |candidate| candidate.experiment.iid.to_s } + expose(:start_time) { |candidate| candidate.start_time || 0 } + expose :end_time, expose_nil: false + expose(:status) { |candidate| candidate.status.to_s.upcase } + expose(:artifact_uri) { |candidate| 'not_implemented' } + expose(:lifecycle_stage) { |candidate| 'active' } + expose(:user_id) { |candidate| candidate.user_id.to_s } + + private + + def run_id + object.iid.to_s + end + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/update_run.rb b/lib/api/entities/ml/mlflow/update_run.rb new file mode 100644 index 00000000000..5acdaab0e33 --- /dev/null +++ b/lib/api/entities/ml/mlflow/update_run.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class UpdateRun < Grape::Entity + expose :run_info + + private + + def run_info + ::API::Entities::Ml::Mlflow::RunInfo.represent object + end + end + end + end + end +end diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb index 1efd457aa5f..18fc0576dd4 100644 --- a/lib/api/entities/package.rb +++ b/lib/api/entities/package.rb @@ -39,6 +39,7 @@ module API end expose :created_at + expose :last_downloaded_at expose :project_id, if: ->(_, opts) { opts[:group] } expose :project_path, if: ->(obj, opts) { opts[:group] && Ability.allowed?(opts[:user], :read_project, obj.project) } expose :tags diff --git a/lib/api/entities/personal_access_token_with_details.rb b/lib/api/entities/personal_access_token_with_details.rb deleted file mode 100644 index 5654bd4a1e1..00000000000 --- a/lib/api/entities/personal_access_token_with_details.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module API - module Entities - class PersonalAccessTokenWithDetails < Entities::PersonalAccessToken - expose :expired?, as: :expired - expose :expires_soon?, as: :expires_soon - expose :revoke_path do |token| - Gitlab::Routing.url_helpers.revoke_profile_personal_access_token_path(token) - end - end - end -end diff --git a/lib/api/entities/user_safe.rb b/lib/api/entities/user_safe.rb index fb99c2e960d..127a8ef2160 100644 --- a/lib/api/entities/user_safe.rb +++ b/lib/api/entities/user_safe.rb @@ -3,9 +3,13 @@ module API module Entities class UserSafe < Grape::Entity + include RequestAwareEntity + expose :id, :username expose :name do |user| - user.redacted_name(options[:current_user]) + current_user = request.respond_to?(:current_user) ? request.current_user : options.fetch(:current_user, nil) + + user.redacted_name(current_user) end end end diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index 0b1c06b3c26..ad5455c5de6 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -102,7 +102,7 @@ module API track_package_event('pull_package', :generic, project: project, user: current_user, namespace: project.namespace) - present_carrierwave_file!(package_file.file) + present_package_file!(package_file) end end end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 82bbab5d7d4..6b1fc0d4279 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -96,9 +96,9 @@ module API present options[:with].prepare_relation(projects, options), options end - def present_groups(params, groups) + def present_groups(params, groups, serializer: Entities::Group) options = { - with: Entities::Group, + with: serializer, current_user: current_user, statistics: params[:statistics] && current_user&.admin? } @@ -248,6 +248,8 @@ module API authorize! :admin_group, group + group.remove_avatar! if params.key?(:avatar) && params[:avatar].nil? + if update_group(group) present_group_details(params, group, with_projects: true) else @@ -392,6 +394,21 @@ module API end end + desc 'Get the groups to where the current group can be transferred to' + params do + optional :search, type: String, desc: 'Return list of namespaces matching the search criteria' + use :pagination + end + get ':id/transfer_locations', feature_category: :subgroups do + authorize! :admin_group, user_group + args = declared_params(include_missing: false) + + groups = ::Groups::AcceptingGroupTransfersFinder.new(current_user, user_group, args).execute + groups = groups.with_route + + present_groups params, groups, serializer: Entities::PublicGroupDetails + end + desc 'Transfer a group to a new parent group or promote a subgroup to a root group' params do optional :group_id, diff --git a/lib/api/helm_packages.rb b/lib/api/helm_packages.rb index a1b265bc8f3..f90084a7e57 100644 --- a/lib/api/helm_packages.rb +++ b/lib/api/helm_packages.rb @@ -67,7 +67,7 @@ module API track_package_event('pull_package', :helm, project: authorized_user_project, namespace: authorized_user_project.namespace) - present_carrierwave_file!(package_file.file) + present_package_file!(package_file) end desc 'Authorize a chart upload from workhorse' do diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 1d0f0c6e7bb..e29d76a5950 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -7,9 +7,12 @@ module API include Helpers::Pagination include Helpers::PaginationStrategies include Gitlab::Ci::Artifacts::Logger + include Gitlab::Utils::StrongMemoize SUDO_HEADER = "HTTP_SUDO" GITLAB_SHARED_SECRET_HEADER = "Gitlab-Shared-Secret" + GITLAB_SHELL_API_HEADER = "Gitlab-Shell-Api-Request" + GITLAB_SHELL_JWT_ISSUER = "gitlab-shell" SUDO_PARAM = :sudo API_USER_ENV = 'gitlab.api.user' API_TOKEN_ENV = 'gitlab.api.token' @@ -283,12 +286,22 @@ module API end def authenticate_by_gitlab_shell_token! - input = params['secret_token'] - input ||= Base64.decode64(headers[GITLAB_SHARED_SECRET_HEADER]) if headers.key?(GITLAB_SHARED_SECRET_HEADER) + if Feature.enabled?(:gitlab_shell_jwt_token) + begin + payload, _ = JSONWebToken::HMACToken.decode(headers[GITLAB_SHELL_API_HEADER], secret_token) + unauthorized! unless payload['iss'] == GITLAB_SHELL_JWT_ISSUER + rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature => ex + Gitlab::ErrorTracking.track_exception(ex) + unauthorized! + end + else + input = params['secret_token'] + input ||= Base64.decode64(headers[GITLAB_SHARED_SECRET_HEADER]) if headers.key?(GITLAB_SHARED_SECRET_HEADER) - input&.chomp! + input&.chomp! - unauthorized! unless Devise.secure_compare(secret_token, input) + unauthorized! unless Devise.secure_compare(secret_token, input) + end end def authenticated_with_can_read_all_resources! @@ -719,7 +732,13 @@ module API end def secret_token - Gitlab::Shell.secret_token + if Feature.enabled?(:gitlab_shell_jwt_token) + strong_memoize(:secret_token) do + File.read(Gitlab.config.gitlab_shell.secret_file) + end + else + Gitlab::Shell.secret_token + end end def authenticate_non_public? diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb index 2b10eebb009..e9af50b80be 100644 --- a/lib/api/helpers/groups_helpers.rb +++ b/lib/api/helpers/groups_helpers.rb @@ -11,8 +11,7 @@ module API optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the group' - # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 - optional :avatar, type: File, desc: 'Avatar image for the group' # rubocop:disable Scalability/FileUploads + optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for the group' optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group' optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users in this group to setup Two-factor authentication' optional :two_factor_grace_period, type: Integer, desc: 'Time before Two-factor authentication is enforced' diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb index 994d3c4c473..a9d91895cfe 100644 --- a/lib/api/helpers/packages/conan/api_helpers.rb +++ b/lib/api/helpers/packages/conan/api_helpers.rb @@ -23,7 +23,7 @@ module API end def present_download_urls(entity) - authorize!(:read_package, project) + authorize_read_package!(project) presenter = ::Packages::Conan::PackagePresenter.new( package, @@ -161,7 +161,7 @@ module API end def download_package_file(file_type) - authorize!(:read_package, project) + authorize_read_package!(project) package_file = ::Packages::Conan::PackageFileFinder .new( @@ -173,7 +173,7 @@ module API track_package_event('pull_package', :conan, category: 'API::ConanPackages', user: current_user, project: project, namespace: project.namespace) if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY - present_carrierwave_file!(package_file.file) + present_package_file!(package_file) end def find_or_create_package diff --git a/lib/api/helpers/packages/dependency_proxy_helpers.rb b/lib/api/helpers/packages/dependency_proxy_helpers.rb index b8ae1dddd7e..a09499e00d7 100644 --- a/lib/api/helpers/packages/dependency_proxy_helpers.rb +++ b/lib/api/helpers/packages/dependency_proxy_helpers.rb @@ -6,16 +6,18 @@ module API module DependencyProxyHelpers REGISTRY_BASE_URLS = { npm: 'https://registry.npmjs.org/', - pypi: 'https://pypi.org/simple/' + pypi: 'https://pypi.org/simple/', + maven: 'https://repo.maven.apache.org/maven2/' }.freeze APPLICATION_SETTING_NAMES = { npm: 'npm_package_requests_forwarding', - pypi: 'pypi_package_requests_forwarding' + pypi: 'pypi_package_requests_forwarding', + maven: 'maven_package_requests_forwarding' }.freeze def redirect_registry_request(forward_to_registry, package_type, options) - if forward_to_registry && redirect_registry_request_available?(package_type) + if forward_to_registry && redirect_registry_request_available?(package_type) && maven_forwarding_ff_enabled?(package_type, options[:target]) ::Gitlab::Tracking.event(self.options[:for].name, "#{package_type}_request_forward") redirect(registry_url(package_type, options)) else @@ -33,6 +35,8 @@ module API "#{base_url}#{options[:package_name]}" when :pypi "#{base_url}#{options[:package_name]}/" + when :maven + "#{base_url}#{options[:path]}/#{options[:file_name]}" end end @@ -46,6 +50,16 @@ module API .attributes .fetch(application_setting_name, false) end + + private + + def maven_forwarding_ff_enabled?(package_type, target) + return true unless package_type == :maven + return true if Feature.enabled?(:maven_central_request_forwarding) + return false unless target + + Feature.enabled?(:maven_central_request_forwarding, target.root_ancestor) + end end end end diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index 2221eec0f82..687c8330cc8 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -14,7 +14,7 @@ module API end def authorize_read_package!(subject = user_project) - authorize!(:read_package, subject) + authorize!(:read_package, subject.try(:packages_policy_subject) || subject) end def authorize_create_package!(subject = user_project) @@ -53,6 +53,11 @@ module API category = args.delete(:category) || self.options[:for].name ::Gitlab::Tracking.event(category, event_name.to_s, **args) end + + def present_package_file!(package_file, supports_direct_download: true) + package_file.package.touch_last_downloaded_at + present_carrierwave_file!(package_file.file, supports_direct_download: supports_direct_download) + end end end end diff --git a/lib/api/helpers/personal_access_tokens_helpers.rb b/lib/api/helpers/personal_access_tokens_helpers.rb new file mode 100644 index 00000000000..db28daa5396 --- /dev/null +++ b/lib/api/helpers/personal_access_tokens_helpers.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module API + module Helpers + module PersonalAccessTokensHelpers + def finder_params(current_user) + if current_user.can_admin_all_resources? + { user: user(params[:user_id]) } + else + { user: current_user, impersonation: false } + end + end + + def user(user_id) + UserFinder.new(user_id).find_by_id + end + + def restrict_non_admins! + return if params[:user_id].blank? + + unauthorized! unless Ability.allowed?(current_user, :read_user_personal_access_tokens, user(params[:user_id])) + end + + def find_token(id) + PersonalAccessToken.find(id) || not_found! + end + + def revoke_token(token) + service = ::PersonalAccessTokens::RevokeService.new(current_user, token: token).execute + + service.success? ? no_content! : bad_request!(nil) + end + end + end +end diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 628182ad1ab..7ca3f55b5a2 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -39,6 +39,7 @@ module API optional :emails_disabled, type: Boolean, desc: 'Disable email notifications' optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis' + optional :show_diff_preview_in_email, type: Boolean, desc: 'Include the code diff preview in merge request notification emails' optional :warn_about_potentially_unwanted_characters, type: Boolean, desc: 'Warn about Potentially Unwanted Characters' optional :enforce_auth_checks_on_uploads, type: Boolean, desc: 'Enforce auth check on uploads' optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' @@ -57,8 +58,7 @@ module API optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all threads are resolved' optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Deprecated: Use :topics instead' optional :topics, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The list of topics for a project' - # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 - optional :avatar, type: File, desc: 'Avatar image for project' # rubocop:disable Scalability/FileUploads + optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for project' optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests' optional :suggestion_commit_message, type: String, desc: 'The commit message used to apply merge request suggestions' @@ -160,6 +160,7 @@ module API :request_access_enabled, :resolve_outdated_diff_discussions, :restrict_user_defined_variables, + :show_diff_preview_in_email, :security_and_compliance_access_level, :squash_option, :shared_runners_enabled, diff --git a/lib/api/helpers/resource_events_helpers.rb b/lib/api/helpers/resource_events_helpers.rb new file mode 100644 index 00000000000..c47a58e8fce --- /dev/null +++ b/lib/api/helpers/resource_events_helpers.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module API + module Helpers + module ResourceEventsHelpers + def self.eventable_types + # This is a method instead of a constant, allowing EE to more easily extend it. + { + Issue => { feature_category: :team_planning, id_field: 'IID' }, + MergeRequest => { feature_category: :code_review, id_field: 'IID' } + } + end + end + end +end + +API::Helpers::ResourceEventsHelpers.prepend_mod_with('API::Helpers::ResourceEventsHelpers') diff --git a/lib/api/helpers/resource_label_events_helpers.rb b/lib/api/helpers/resource_label_events_helpers.rb deleted file mode 100644 index eeb68362c1d..00000000000 --- a/lib/api/helpers/resource_label_events_helpers.rb +++ /dev/null @@ -1,18 +0,0 @@ -# frozen_string_literal: true - -module API - module Helpers - module ResourceLabelEventsHelpers - def self.feature_category_per_eventable_type - # This is a method instead of a constant, allowing EE to more easily - # extend it. - { - Issue => :team_planning, - MergeRequest => :code_review - } - end - end - end -end - -API::Helpers::ResourceLabelEventsHelpers.prepend_mod_with('API::Helpers::ResourceLabelEventsHelpers') diff --git a/lib/api/integrations/slack/events.rb b/lib/api/integrations/slack/events.rb deleted file mode 100644 index 6227b75a9d7..00000000000 --- a/lib/api/integrations/slack/events.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -# This API endpoint handles all events sent from Slack once a Slack -# workspace has installed the GitLab Slack app. -# -# See https://api.slack.com/apis/connections/events-api. -module API - class Integrations - module Slack - class Events < ::API::Base - feature_category :integrations - - before { verify_slack_request! } - - helpers do - def verify_slack_request! - unauthorized! unless Request.verify!(request) - end - end - - namespace 'integrations/slack' do - post :events do - type = params['type'] - raise ArgumentError, "Unable to handle event type: '#{type}'" unless type == 'url_verification' - - status :ok - UrlVerification.call(params) - rescue ArgumentError => e - # Track the error, but respond with a `2xx` because we don't want to risk - # Slack rate-limiting, or disabling our app, due to error responses. - # See https://api.slack.com/apis/connections/events-api. - Gitlab::ErrorTracking.track_exception(e) - - no_content! - end - end - end - end - end -end diff --git a/lib/api/integrations/slack/events/url_verification.rb b/lib/api/integrations/slack/events/url_verification.rb deleted file mode 100644 index 4628b93665d..00000000000 --- a/lib/api/integrations/slack/events/url_verification.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -module API - class Integrations - module Slack - class Events - class UrlVerification - # When the GitLab Slack app is first configured to receive Slack events, - # Slack will issue a special request to the endpoint and expect it to respond - # with the `challenge` param. - # - # This must be done in-request, rather than on a queue. - # - # See https://api.slack.com/apis/connections/events-api. - def self.call(params) - { challenge: params[:challenge] } - end - end - end - end - end -end diff --git a/lib/api/integrations/slack/request.rb b/lib/api/integrations/slack/request.rb deleted file mode 100644 index df0109b07aa..00000000000 --- a/lib/api/integrations/slack/request.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module API - class Integrations - module Slack - module Request - VERIFICATION_VERSION = 'v0' - VERIFICATION_TIMESTAMP_HEADER = 'X-Slack-Request-Timestamp' - VERIFICATION_SIGNATURE_HEADER = 'X-Slack-Signature' - VERIFICATION_DELIMITER = ':' - VERIFICATION_HMAC_ALGORITHM = 'sha256' - VERIFICATION_TIMESTAMP_EXPIRY = 1.minute.to_i - - # Verify the request by comparing the given request signature in the header - # with a signature value that we compute according to the steps in: - # https://api.slack.com/authentication/verifying-requests-from-slack. - def self.verify!(request) - return false unless Gitlab::CurrentSettings.slack_app_signing_secret - - timestamp, signature = request.headers.values_at( - VERIFICATION_TIMESTAMP_HEADER, - VERIFICATION_SIGNATURE_HEADER - ) - - return false if timestamp.nil? || signature.nil? - return false if Time.current.to_i - timestamp.to_i >= VERIFICATION_TIMESTAMP_EXPIRY - - request.body.rewind - - basestring = [ - VERIFICATION_VERSION, - timestamp, - request.body.read - ].join(VERIFICATION_DELIMITER) - - hmac_digest = OpenSSL::HMAC.hexdigest( - VERIFICATION_HMAC_ALGORITHM, - Gitlab::CurrentSettings.slack_app_signing_secret, - basestring - ) - - # Signature will look like: 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503' - ActiveSupport::SecurityUtils.secure_compare( - signature, - "#{VERIFICATION_VERSION}=#{hmac_digest}" - ) - end - end - end - end -end diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 6f475fa8d74..c4464666020 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -133,11 +133,6 @@ module API 'Could not find a user for the given key' unless actor.user end - # TODO: backwards compatibility; remove after https://gitlab.com/gitlab-org/gitlab-shell/-/merge_requests/454 is merged - def two_factor_otp_check - { success: false, message: 'Feature is not available' } - end - def two_factor_manual_otp_check { success: false, message: 'Feature is not available' } end @@ -339,13 +334,6 @@ module API end end - # TODO: backwards compatibility; remove after https://gitlab.com/gitlab-org/gitlab-shell/-/merge_requests/454 is merged - post '/two_factor_otp_check', feature_category: :authentication_and_authorization do - status 200 - - two_factor_manual_otp_check - end - post '/two_factor_push_otp_check', feature_category: :authentication_and_authorization do status 200 diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index fb0221ee907..a3a25ec1696 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -22,6 +22,7 @@ module API end helpers ::API::Helpers::PackagesHelpers + helpers ::API::Helpers::Packages::DependencyProxyHelpers helpers do def path_exists?(path) @@ -76,7 +77,10 @@ module API format == 'jar' end - def present_carrierwave_file_with_head_support!(file, supports_direct_download: true) + def present_carrierwave_file_with_head_support!(package_file, supports_direct_download: true) + package_file.package.touch_last_downloaded_at + file = package_file.file + if head_request_on_aws_file?(file, supports_direct_download) && !file.file_storage? return redirect(signed_head_url(file)) end @@ -110,7 +114,31 @@ module API project || group, path: params[:path], order_by_package_file: order_by_package_file - ).execute! + ).execute + end + + def find_and_present_package_file(package, file_name, format, params) + project = package&.project + package_file = nil + + package_file = ::Packages::PackageFileFinder.new(package, file_name).execute if package + + no_package_found = package_file ? false : true + + redirect_registry_request(no_package_found, :maven, path: params[:path], file_name: params[:file_name], target: params[:target]) do + not_found!('Package') if no_package_found + + case format + when 'md5' + package_file.file_md5 + when 'sha1' + package_file.file_sha1 + else + track_package_event('pull_package', :maven, project: project, namespace: project&.namespace) if jar_file?(format) + + present_carrierwave_file_with_head_support!(package_file) + end + end end end @@ -138,6 +166,8 @@ module API package = fetch_package(file_name: file_name, project: project) + not_found!('Package') unless package + package_file = ::Packages::PackageFileFinder .new(package, file_name).execute! @@ -148,7 +178,7 @@ module API package_file.file_sha1 else track_package_event('pull_package', :maven, project: project, namespace: project.namespace) if jar_file?(format) - present_carrierwave_file_with_head_support!(package_file.file) + present_carrierwave_file_with_head_support!(package_file) end end @@ -166,31 +196,20 @@ module API route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do # return a similar failure to group = find_group(params[:id]) - not_found!('Group') unless path_exists?(params[:path]) - - file_name, format = extract_format(params[:file_name]) - group = find_group(params[:id]) + if Feature.disabled?(:maven_central_request_forwarding, group&.root_ancestor) + not_found!('Group') unless path_exists?(params[:path]) + end + not_found!('Group') unless can?(current_user, :read_group, group) + file_name, format = extract_format(params[:file_name]) package = fetch_package(file_name: file_name, group: group) - authorize_read_package!(package.project) + authorize_read_package!(package.project) if package - package_file = ::Packages::PackageFileFinder - .new(package, file_name).execute! - - case format - when 'md5' - package_file.file_md5 - when 'sha1' - package_file.file_sha1 - else - track_package_event('pull_package', :maven, project: package.project, namespace: package.project.namespace) if jar_file?(format) - - present_carrierwave_file_with_head_support!(package_file.file) - end + find_and_present_package_file(package, file_name, format, params.merge(target: group)) end end @@ -208,7 +227,9 @@ module API route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do # return a similar failure to user_project - not_found!('Project') unless path_exists?(params[:path]) + unless Feature.enabled?(:maven_central_request_forwarding, user_project&.root_ancestor) + not_found!('Project') unless path_exists?(params[:path]) + end authorize_read_package!(user_project) @@ -216,19 +237,7 @@ module API package = fetch_package(file_name: file_name, project: user_project) - package_file = ::Packages::PackageFileFinder - .new(package, file_name).execute! - - case format - when 'md5' - package_file.file_md5 - when 'sha1' - package_file.file_sha1 - else - track_package_event('pull_package', :maven, project: user_project, namespace: user_project.namespace) if jar_file?(format) - - present_carrierwave_file_with_head_support!(package_file.file) - end + find_and_present_package_file(package, file_name, format, params.merge(target: user_project)) end desc 'Workhorse authorize the maven package file upload' do diff --git a/lib/api/members.rb b/lib/api/members.rb index d26fdd09ee7..f4e38207aca 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -24,6 +24,7 @@ module API params do optional :query, type: String, desc: 'A query string to search for members' optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of user ids to look up for membership' + optional :skip_users, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of user ids to be skipped for membership' optional :show_seat_info, type: Boolean, desc: 'Show seat information for members' use :optional_filter_params_ee use :pagination diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index a8f58e91067..1dc0e1f0d22 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -212,7 +212,17 @@ module API recheck_mergeability_of(merge_requests: merge_requests) unless options[:skip_merge_status_recheck] - present_cached merge_requests, expires_in: 8.hours, cache_context: -> (mr) { "#{current_user&.cache_key}:#{mr.merge_status}" }, **options + present_cached merge_requests, + expires_in: 8.hours, + cache_context: -> (mr) do + [ + current_user&.cache_key, + mr.merge_status, + mr.merge_request_assignees.map(&:cache_key), + mr.merge_request_reviewers.map(&:cache_key) + ].join(":") + end, + **options end desc 'Create a merge request' do @@ -544,6 +554,19 @@ module API render_api_error!(e.message, 409) end + desc 'Remove merge request approvals' do + detail 'This feature was added in GitLab 15.4' + end + put ':id/merge_requests/:merge_request_iid/reset_approvals', feature_category: :code_review, urgency: :low do + merge_request = find_project_merge_request(params[:merge_request_iid]) + + unauthorized! unless current_user.bot? && merge_request.can_be_approved_by?(current_user) + + merge_request.approvals.delete_all + + status :accepted + end + desc 'List issues that will be closed on merge' do success Entities::MRNote end diff --git a/lib/api/ml/mlflow.rb b/lib/api/ml/mlflow.rb new file mode 100644 index 00000000000..4f5bd42f8f9 --- /dev/null +++ b/lib/api/ml/mlflow.rb @@ -0,0 +1,171 @@ +# frozen_string_literal: true + +require 'mime/types' + +module API + # MLFlow integration API, replicating the Rest API https://www.mlflow.org/docs/latest/rest-api.html#rest-api + module Ml + class Mlflow < ::API::Base + include APIGuard + + # The first part of the url is the namespace, the second part of the URL is what the MLFlow client calls + MLFLOW_API_PREFIX = ':id/ml/mflow/api/2.0/mlflow/' + + allow_access_with_scope :api + allow_access_with_scope :read_api, if: -> (request) { request.get? || request.head? } + + before do + authenticate! + not_found! unless Feature.enabled?(:ml_experiment_tracking, user_project) + end + + feature_category :mlops + + content_type :json, 'application/json' + default_format :json + + helpers do + def resource_not_found! + render_structured_api_error!({ error_code: 'RESOURCE_DOES_NOT_EXIST' }, 404) + end + + def resource_already_exists! + render_structured_api_error!({ error_code: 'RESOURCE_ALREADY_EXISTS' }, 400) + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'API to interface with MLFlow Client, REST API version 1.28.0' do + detail 'This feature is gated by :ml_experiment_tracking.' + end + namespace MLFLOW_API_PREFIX do + resource :experiments do + desc 'Fetch experiment by experiment_id' do + success Entities::Ml::Mlflow::Experiment + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment' + end + params do + optional :experiment_id, type: String, default: '', desc: 'Experiment ID, in reference to the project' + end + get 'get', urgency: :low do + experiment = ::Ml::Experiment.by_project_id_and_iid(user_project.id, params[:experiment_id]) + + resource_not_found! unless experiment + + present experiment, with: Entities::Ml::Mlflow::Experiment + end + + desc 'Fetch experiment by experiment_name' do + success Entities::Ml::Mlflow::Experiment + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment-by-name' + end + params do + optional :experiment_name, type: String, default: '', desc: 'Experiment name' + end + get 'get-by-name', urgency: :low do + experiment = ::Ml::Experiment.by_project_id_and_name(user_project, params[:experiment_name]) + + resource_not_found! unless experiment + + present experiment, with: Entities::Ml::Mlflow::Experiment + end + + desc 'Create experiment' do + success Entities::Ml::Mlflow::NewExperiment + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#create-experiment' + end + params do + requires :name, type: String, desc: 'Experiment name' + optional :artifact_location, type: String, desc: 'This will be ignored' + optional :tags, type: Array, desc: 'This will be ignored' + end + post 'create', urgency: :low do + resource_already_exists! if ::Ml::Experiment.has_record?(user_project.id, params[:name]) + + experiment = ::Ml::Experiment.create!(name: params[:name], + user: current_user, + project: user_project) + + present experiment, with: Entities::Ml::Mlflow::NewExperiment + end + end + + resource :runs do + desc 'Gets an MLFlow Run, which maps to GitLab Candidates' do + success Entities::Ml::Mlflow::Run + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-run' + end + params do + optional :run_id, type: String, desc: 'UUID of the candidate.' + optional :run_uuid, type: String, desc: 'This parameter is ignored' + end + get 'get', urgency: :low do + candidate = ::Ml::Candidate.with_project_id_and_iid(user_project.id, params[:run_id]) + + resource_not_found! unless candidate + + present candidate, with: Entities::Ml::Mlflow::Run + end + + desc 'Creates a Run.' do + success Entities::Ml::Mlflow::Run + detail ['https://www.mlflow.org/docs/1.28.0/rest-api.html#create-run', + 'MLFlow Runs map to GitLab Candidates'] + end + params do + requires :experiment_id, type: Integer, + desc: 'Id for the experiment, relative to the project' + optional :start_time, type: Integer, + desc: 'Unix timestamp in milliseconds of when the run started.', + default: 0 + optional :user_id, type: String, desc: 'This will be ignored' + optional :tags, type: Array, desc: 'This will be ignored' + end + post 'create', urgency: :low do + experiment = ::Ml::Experiment.by_project_id_and_iid(user_project.id, params[:experiment_id].to_i) + + resource_not_found! unless experiment + + candidate = ::Ml::Candidate.create!( + experiment: experiment, + user: current_user, + start_time: params[:start_time] || 0 + ) + + present candidate, with: Entities::Ml::Mlflow::Run + end + + desc 'Updates a Run.' do + success Entities::Ml::Mlflow::UpdateRun + detail ['https://www.mlflow.org/docs/1.28.0/rest-api.html#update-run', + 'MLFlow Runs map to GitLab Candidates'] + end + params do + optional :run_id, type: String, desc: 'UUID of the candidate.' + optional :status, type: String, + values: ::Ml::Candidate.statuses.keys.map(&:upcase), + desc: "Status of the run. Accepts: " \ + "#{::Ml::Candidate.statuses.keys.map(&:upcase)}." + optional :end_time, type: Integer, desc: 'Ending time of the run' + end + post 'update', urgency: :low do + candidate = ::Ml::Candidate.with_project_id_and_iid(user_project.id, params[:run_id]) + + resource_not_found! unless candidate + + candidate.status = params[:status].downcase if params[:status] + candidate.end_time = params[:end_time] if params[:end_time] + + candidate.save if candidate.valid? + + present candidate, with: Entities::Ml::Mlflow::UpdateRun + end + end + end + end + end + end +end diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index a12fbbb9bb6..eeb66c86b3b 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -66,6 +66,8 @@ module API optional :parent_id, type: Integer, desc: "The ID of the parent namespace. If no ID is specified, only top-level namespaces are considered." end get ':namespace/exists', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS, feature_category: :subgroups, urgency: :low do + check_rate_limit!(:namespace_exists, scope: current_user) + namespace_path = params[:namespace] existing_namespaces_within_the_parent = Namespace.without_project_namespaces.by_parent(params[:parent_id]) diff --git a/lib/api/npm_project_packages.rb b/lib/api/npm_project_packages.rb index 21bb2e69799..166c0b755fe 100644 --- a/lib/api/npm_project_packages.rb +++ b/lib/api/npm_project_packages.rb @@ -35,7 +35,7 @@ module API track_package_event('pull_package', package, category: 'API::NpmPackages', project: project, namespace: project.namespace) - present_carrierwave_file!(package_file.file) + present_package_file!(package_file) end desc 'Create NPM package' do diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb index 1e630cffea1..3e05ea13311 100644 --- a/lib/api/nuget_project_packages.rb +++ b/lib/api/nuget_project_packages.rb @@ -193,7 +193,7 @@ module API ) # nuget and dotnet don't support 302 Moved status codes, supports_direct_download has to be set to false - present_carrierwave_file!(package_file.file, supports_direct_download: false) + present_package_file!(package_file, supports_direct_download: false) end end end diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb index 0d7d2dc6a0c..1c00569bba2 100644 --- a/lib/api/personal_access_tokens.rb +++ b/lib/api/personal_access_tokens.rb @@ -18,34 +18,10 @@ module API before do authenticate! - restrict_non_admins! unless current_user.admin? + restrict_non_admins! unless current_user.can_admin_all_resources? end - helpers do - def finder_params(current_user) - current_user.admin? ? { user: user(params[:user_id]) } : { user: current_user, impersonation: false } - end - - def user(user_id) - UserFinder.new(user_id).find_by_id - end - - def restrict_non_admins! - return if params[:user_id].blank? - - unauthorized! unless Ability.allowed?(current_user, :read_user_personal_access_tokens, user(params[:user_id])) - end - - def find_token(id) - PersonalAccessToken.find(id) || not_found! - end - - def revoke_token(token) - service = ::PersonalAccessTokens::RevokeService.new(current_user, token: token).execute - - service.success? ? no_content! : bad_request!(nil) - end - end + helpers ::API::Helpers::PersonalAccessTokensHelpers resources :personal_access_tokens do get do @@ -63,14 +39,10 @@ module API present token, with: Entities::PersonalAccessToken else # Only admins should be informed if the token doesn't exist - current_user.admin? ? not_found! : unauthorized! + current_user.can_admin_all_resources? ? not_found! : unauthorized! end end - delete 'self' do - revoke_token(access_token) - end - delete ':id' do token = find_token(params[:id]) diff --git a/lib/api/personal_access_tokens/self_revocation.rb b/lib/api/personal_access_tokens/self_revocation.rb new file mode 100644 index 00000000000..22e07f4cc7b --- /dev/null +++ b/lib/api/personal_access_tokens/self_revocation.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module API + class PersonalAccessTokens + class SelfRevocation < ::API::Base + include APIGuard + + feature_category :authentication_and_authorization + + helpers ::API::Helpers::PersonalAccessTokensHelpers + + # As any token regardless of `scope` should be able to revoke itself + # all availabe scopes are allowed for this API class. + # Please be aware of the permissive scope when adding new endpoints to this class. + allow_access_with_scope(Gitlab::Auth.all_available_scopes) + + before { authenticate! } + + resource :personal_access_tokens do + delete 'self' do + revoke_token(access_token) + end + end + end + end +end diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 6ed480518ee..8c58cc585d8 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -453,6 +453,8 @@ module API filter_attributes_using_license!(attrs) verify_update_project_attrs!(user_project, attrs) + user_project.remove_avatar! if attrs.key?(:avatar) && attrs[:avatar].nil? + result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute if result[:status] == :success @@ -743,6 +745,22 @@ module API end end + desc 'Get the namespaces to where the project can be transferred' + params do + optional :search, type: String, desc: 'Return list of namespaces matching the search criteria' + use :pagination + end + get ":id/transfer_locations", feature_category: :projects do + authorize! :change_namespace, user_project + args = declared_params(include_missing: false) + args[:permission_scope] = :transfer_projects + + groups = ::Groups::UserGroupsFinder.new(current_user, current_user, args).execute + groups = groups.with_route + + present_groups(groups) + end + desc 'Show the storage information' do success Entities::ProjectRepositoryStorage end diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index f8a7a3c0ecc..ae583ca968a 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -120,7 +120,7 @@ module API track_package_event('pull_package', :pypi) - present_carrierwave_file!(package_file.file, supports_direct_download: true) + present_package_file!(package_file, supports_direct_download: true) end desc 'The PyPi Simple Group Index Endpoint' do @@ -180,7 +180,7 @@ module API track_package_event('pull_package', :pypi, project: project, namespace: project.namespace) - present_carrierwave_file!(package_file.file, supports_direct_download: true) + present_package_file!(package_file, supports_direct_download: true) end desc 'The PyPi Simple Project Index Endpoint' do diff --git a/lib/api/releases.rb b/lib/api/releases.rb index 10e879ec70b..cdfcce9dddb 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -100,6 +100,62 @@ module API present release, with: Entities::Release, current_user: current_user, include_html_description: params[:include_html_description] end + desc 'Download a project release asset file' do + detail 'This feature was introduced in GitLab 15.4.' + named 'download_release_asset_file' + end + params do + requires :tag_name, type: String, + desc: 'The name of the tag.', as: :tag + requires :file_path, type: String, + file_path: true, + desc: 'The path to the file to download, as specified when creating the release asset.' + end + route_setting :authentication, job_token_allowed: true + get ':id/releases/:tag_name/downloads/*file_path', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do + authorize_download_code! + + not_found! unless release + + link = release.links.find_by_filepath!("/#{params[:file_path]}") + + not_found! unless link + + redirect link.url + end + + desc 'Get the latest project release' do + detail 'This feature was introduced in GitLab 15.4.' + named 'get_latest_release' + end + params do + requires :suffix_path, type: String, file_path: true, desc: 'The path to be suffixed to the latest release' + end + route_setting :authentication, job_token_allowed: true + get ':id/releases/permalink/latest(/)(*suffix_path)', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do + authorize_download_code! + + # Try to find the latest release + latest_release = find_latest_release + not_found! unless latest_release + + # Build the full API URL with the tag of the latest release + redirect_url = api_v4_projects_releases_path(id: user_project.id, tag_name: latest_release.tag) + + # Include the additional suffix_path if present + redirect_url += "/#{params[:suffix_path]}" if params[:suffix_path].present? + + # Include any query parameter except `order_by` since we have plans to extend it in the future. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/352945 for reference. + query_parameters_except_order_by = get_query_params.except('order_by') + + if query_parameters_except_order_by.present? + redirect_url += "?#{query_parameters_except_order_by.compact.to_param}" + end + + redirect redirect_url + end + desc 'Create a new release' do detail 'This feature was introduced in GitLab 11.7.' named 'create_release' @@ -232,6 +288,16 @@ module API @release ||= user_project.releases.find_by_tag(params[:tag]) end + def find_latest_release + ReleasesFinder.new(user_project, current_user, { order_by: 'released_at', sort: 'desc' }).execute.first + end + + def get_query_params + return {} unless @request.query_string.present? + + Rack::Utils.parse_nested_query(@request.query_string) + end + def log_release_created_audit_event(release) # extended in EE end diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb index cd56809f45a..e74b6509a17 100644 --- a/lib/api/resource_label_events.rb +++ b/lib/api/resource_label_events.rb @@ -7,20 +7,22 @@ module API before { authenticate! } - Helpers::ResourceLabelEventsHelpers.feature_category_per_eventable_type.each do |eventable_type, feature_category| + Helpers::ResourceEventsHelpers.eventable_types.each do |eventable_type, details| parent_type = eventable_type.parent_class.to_s.underscore eventables_str = eventable_type.to_s.underscore.pluralize + human_eventable_str = eventable_type.to_s.underscore.humanize.downcase + feature_category = details[:feature_category] params do requires :id, type: String, desc: "The ID of a #{parent_type}" end resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc "Get a list of #{eventable_type.to_s.downcase} resource label events" do + desc "Get a list of #{human_eventable_str} resource label events" do success Entities::ResourceLabelEvent detail 'This feature was introduced in 11.3' end params do - requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable' + requires :eventable_id, types: [Integer, String], desc: "The #{details[:id_field]} of the #{human_eventable_str}" use :pagination end @@ -32,13 +34,13 @@ module API present ResourceLabelEvent.visible_to_user?(current_user, paginate(events)), with: Entities::ResourceLabelEvent end - desc "Get a single #{eventable_type.to_s.downcase} resource label event" do + desc "Get a single #{human_eventable_str} resource label event" do success Entities::ResourceLabelEvent detail 'This feature was introduced in 11.3' end params do requires :event_id, type: String, desc: 'The ID of a resource label event' - requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable' + requires :eventable_id, types: [Integer, String], desc: "The #{details[:id_field]} of the #{human_eventable_str}" end get ":id/#{eventables_str}/:eventable_id/resource_label_events/:event_id", feature_category: feature_category do eventable = find_noteable(eventable_type, params[:eventable_id]) diff --git a/lib/api/resource_state_events.rb b/lib/api/resource_state_events.rb index 4b92f320d6f..f817d55c505 100644 --- a/lib/api/resource_state_events.rb +++ b/lib/api/resource_state_events.rb @@ -7,41 +7,41 @@ module API before { authenticate! } - { - Issue => :team_planning, - MergeRequest => :code_review - }.each do |eventable_class, feature_category| - eventable_name = eventable_class.to_s.underscore + Helpers::ResourceEventsHelpers.eventable_types.each do |eventable_type, details| + parent_type = eventable_type.parent_class.to_s.underscore + eventables_str = eventable_type.to_s.underscore.pluralize + human_eventable_str = eventable_type.to_s.underscore.humanize.downcase + feature_category = details[:feature_category] params do - requires :id, type: String, desc: "The ID of a project" + requires :id, type: String, desc: "The ID of a #{parent_type}" end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc "Get a list of #{eventable_class.to_s.downcase} resource state events" do + resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc "Get a list of #{human_eventable_str} resource state events" do success Entities::ResourceStateEvent end params do - requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}" + requires :eventable_id, types: Integer, desc: "The #{details[:id_field]} of the #{human_eventable_str}" use :pagination end - get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events", feature_category: feature_category, urgency: :low do - eventable = find_noteable(eventable_class, params[:eventable_iid]) + get ":id/#{eventables_str}/:eventable_id/resource_state_events", feature_category: feature_category, urgency: :low do + eventable = find_noteable(eventable_type, params[:eventable_id]) events = ResourceStateEventFinder.new(current_user, eventable).execute present paginate(events), with: Entities::ResourceStateEvent end - desc "Get a single #{eventable_class.to_s.downcase} resource state event" do + desc "Get a single #{human_eventable_str} resource state event" do success Entities::ResourceStateEvent end params do - requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}" + requires :eventable_id, types: Integer, desc: "The #{details[:id_field]} of the #{human_eventable_str}" requires :event_id, type: Integer, desc: 'The ID of a resource state event' end - get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events/:event_id", feature_category: feature_category do - eventable = find_noteable(eventable_class, params[:eventable_iid]) + get ":id/#{eventables_str}/:eventable_id/resource_state_events/:event_id", feature_category: feature_category do + eventable = find_noteable(eventable_type, params[:eventable_id]) event = ResourceStateEventFinder.new(current_user, eventable).find(params[:event_id]) diff --git a/lib/api/rpm_project_packages.rb b/lib/api/rpm_project_packages.rb new file mode 100644 index 00000000000..d17470ae92d --- /dev/null +++ b/lib/api/rpm_project_packages.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true +module API + class RpmProjectPackages < ::API::Base + helpers ::API::Helpers::PackagesHelpers + helpers ::API::Helpers::Packages::BasicAuthHelpers + include ::API::Helpers::Authentication + + feature_category :package_registry + + before do + require_packages_enabled! + + not_found! unless ::Feature.enabled?(:rpm_packages, authorized_user_project) + + authorize_read_package!(authorized_user_project) + end + + authenticate_with do |accept| + accept.token_types(:personal_access_token_with_username, :deploy_token_with_username, :job_token_with_username) + .sent_through(:http_basic_auth) + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + namespace ':id/packages/rpm' do + desc 'Download repository metadata files' + params do + requires :file_name, type: String, desc: 'Repository metadata file name' + end + get 'repodata/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do + not_found! + end + + desc 'Download RPM package files' + params do + requires :package_file_id, type: Integer, desc: 'RPM package file id' + requires :file_name, type: String, desc: 'RPM package file name' + end + get '*package_file_id/*file_name', requirements: { file_name: API::NO_SLASH_URL_PART_REGEX } do + not_found! + end + + desc 'Upload a RPM package' + post do + authorize_create_package!(authorized_user_project) + + if authorized_user_project.actual_limits.exceeded?(:rpm_max_file_size, params[:file].size) + bad_request!('File is too large') + end + + not_found! + end + + desc 'Authorize package upload from workhorse' + post 'authorize' do + not_found! + end + end + end + end +end diff --git a/lib/api/rubygem_packages.rb b/lib/api/rubygem_packages.rb index 85bbd0879b7..b4d02613e4c 100644 --- a/lib/api/rubygem_packages.rb +++ b/lib/api/rubygem_packages.rb @@ -65,7 +65,7 @@ module API requires :file_name, type: String, desc: 'Package file name' end get "gems/:file_name", requirements: FILE_NAME_REQUIREMENTS do - authorize!(:read_package, user_project) + authorize_read_package!(user_project) package_files = ::Packages::PackageFile .for_rubygem_with_file_name(user_project, params[:file_name]) @@ -74,7 +74,7 @@ module API track_package_event('pull_package', :rubygems, project: user_project, namespace: user_project.namespace) - present_carrierwave_file!(package_file.file) + present_package_file!(package_file) end namespace 'api/v1' do diff --git a/lib/api/search.rb b/lib/api/search.rb index 7aa3cf8a5cb..44bb4228786 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -65,9 +65,26 @@ module API set_global_search_log_information + Gitlab::Metrics::GlobalSearchSlis.record_apdex( + elapsed: @search_duration_s, + search_type: search_type, + search_level: search_service.level, + search_scope: search_scope + ) + Gitlab::UsageDataCounters::SearchCounter.count(:all_searches) paginate(@results) + + ensure + # If we raise an error somewhere in the @search_duration_s benchmark block, we will end up here + # with a 200 status code, but an empty @search_duration_s. + Gitlab::Metrics::GlobalSearchSlis.record_error_rate( + error: @search_duration_s.nil? || (status < 200 || status >= 400), + search_type: search_type, + search_level: search_service(additional_params).level, + search_scope: search_scope + ) end def snippets? diff --git a/lib/api/settings.rb b/lib/api/settings.rb index c25a56d5f08..f393f862f55 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -98,6 +98,7 @@ module API optional :max_export_size, type: Integer, desc: 'Maximum export size in MB' optional :max_import_size, type: Integer, desc: 'Maximum import size in MB' optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' + optional :max_pages_custom_domains_per_project, type: Integer, desc: 'Maximum number of GitLab Pages custom domains per project' optional :metrics_method_call_threshold, type: Integer, desc: 'A method call is only tracked when it takes longer to complete than the given amount of milliseconds.' optional :password_authentication_enabled, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' # support legacy names, can be removed in v5 optional :password_authentication_enabled_for_web, type: Boolean, desc: 'Flag indicating if password authentication is enabled for the web interface' diff --git a/lib/api/tags.rb b/lib/api/tags.rb index 97a2aebf53b..c8ac68189f5 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -22,8 +22,8 @@ module API params do optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return tags sorted in updated by `asc` or `desc` order.' - optional :order_by, type: String, values: %w[name updated], default: 'updated', - desc: 'Return tags ordered by `name` or `updated` fields.' + optional :order_by, type: String, values: %w[name updated version], default: 'updated', + desc: 'Return tags ordered by `name`, `updated`, `version` fields.' optional :search, type: String, desc: 'Return list of tags matching the search criteria' optional :page_token, type: String, desc: 'Name of tag to start the paginaition from' use :pagination diff --git a/lib/api/topics.rb b/lib/api/topics.rb index a08b4c6c107..38cfdc44021 100644 --- a/lib/api/topics.rb +++ b/lib/api/topics.rb @@ -94,5 +94,25 @@ module API destroy_conditionally!(topic) end + + desc 'Merge topics' do + detail 'This feature was introduced in GitLab 15.4.' + success Entities::Projects::Topic + end + params do + requires :source_topic_id, type: Integer, desc: 'ID of source project topic' + requires :target_topic_id, type: Integer, desc: 'ID of target project topic' + end + post 'topics/merge' do + authenticated_as_admin! + + source_topic = ::Projects::Topic.find(params[:source_topic_id]) + target_topic = ::Projects::Topic.find(params[:target_topic_id]) + + response = ::Topics::MergeService.new(source_topic, target_topic).execute + render_api_error!(response.message, :bad_request) if response.error? + + present target_topic, with: Entities::Projects::Topic + end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index c93c0f601a0..1d1c633824e 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -54,8 +54,7 @@ module API optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' - # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 - optional :avatar, type: File, desc: 'Avatar image for user' # rubocop:disable Scalability/FileUploads + optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for user' optional :theme_id, type: Integer, desc: 'The GitLab theme for the user' optional :color_scheme_id, type: Integer, desc: 'The color scheme for the file viewer' optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile' @@ -733,7 +732,7 @@ module API unless user.can_be_deactivated? forbidden!('A blocked user cannot be deactivated by the API') if user.blocked? forbidden!('An internal user cannot be deactivated by the API') if user.internal? - forbidden!("The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated") + forbidden!("The user you are trying to deactivate has been active in the past #{Gitlab::CurrentSettings.deactivate_dormant_users_period} days and cannot be deactivated") end if user.deactivate |