summaryrefslogtreecommitdiff
path: root/lib/api
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-19 23:18:09 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-19 23:18:09 +0000
commit6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch)
treedc4d20fe6064752c0bd323187252c77e0a89144b /lib/api
parent9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff)
downloadgitlab-ce-6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde.tar.gz
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'lib/api')
-rw-r--r--lib/api/admin/batched_background_migrations.rb108
-rw-r--r--lib/api/api.rb5
-rw-r--r--lib/api/branches.rb37
-rw-r--r--lib/api/ci/job_artifacts.rb2
-rw-r--r--lib/api/ci/jobs.rb7
-rw-r--r--lib/api/ci/runners.rb15
-rw-r--r--lib/api/composer_packages.rb5
-rw-r--r--lib/api/concerns/packages/conan_endpoints.rb8
-rw-r--r--lib/api/concerns/packages/debian_package_endpoints.rb123
-rw-r--r--lib/api/debian_group_packages.rb2
-rw-r--r--lib/api/debian_project_packages.rb2
-rw-r--r--lib/api/entities/batched_background_migration.rb14
-rw-r--r--lib/api/entities/ci/job_basic.rb6
-rw-r--r--lib/api/entities/ci/job_request/image.rb2
-rw-r--r--lib/api/entities/ci/job_request/service.rb2
-rw-r--r--lib/api/entities/merge_request_reviewer.rb1
-rw-r--r--lib/api/entities/ml/mlflow/experiment.rb28
-rw-r--r--lib/api/entities/ml/mlflow/new_experiment.rb19
-rw-r--r--lib/api/entities/ml/mlflow/run.rb16
-rw-r--r--lib/api/entities/ml/mlflow/run_info.rb27
-rw-r--r--lib/api/entities/ml/mlflow/update_run.rb19
-rw-r--r--lib/api/entities/package.rb1
-rw-r--r--lib/api/entities/personal_access_token_with_details.rb13
-rw-r--r--lib/api/entities/user_safe.rb6
-rw-r--r--lib/api/generic_packages.rb2
-rw-r--r--lib/api/groups.rb21
-rw-r--r--lib/api/helm_packages.rb2
-rw-r--r--lib/api/helpers.rb29
-rw-r--r--lib/api/helpers/groups_helpers.rb3
-rw-r--r--lib/api/helpers/packages/conan/api_helpers.rb6
-rw-r--r--lib/api/helpers/packages/dependency_proxy_helpers.rb20
-rw-r--r--lib/api/helpers/packages_helpers.rb7
-rw-r--r--lib/api/helpers/personal_access_tokens_helpers.rb35
-rw-r--r--lib/api/helpers/projects_helpers.rb5
-rw-r--r--lib/api/helpers/resource_events_helpers.rb17
-rw-r--r--lib/api/helpers/resource_label_events_helpers.rb18
-rw-r--r--lib/api/integrations/slack/events.rb40
-rw-r--r--lib/api/integrations/slack/events/url_verification.rb22
-rw-r--r--lib/api/integrations/slack/request.rb51
-rw-r--r--lib/api/internal/base.rb12
-rw-r--r--lib/api/maven_packages.rb79
-rw-r--r--lib/api/members.rb1
-rw-r--r--lib/api/merge_requests.rb25
-rw-r--r--lib/api/ml/mlflow.rb171
-rw-r--r--lib/api/namespaces.rb2
-rw-r--r--lib/api/npm_project_packages.rb2
-rw-r--r--lib/api/nuget_project_packages.rb2
-rw-r--r--lib/api/personal_access_tokens.rb34
-rw-r--r--lib/api/personal_access_tokens/self_revocation.rb26
-rw-r--r--lib/api/projects.rb18
-rw-r--r--lib/api/pypi_packages.rb4
-rw-r--r--lib/api/releases.rb66
-rw-r--r--lib/api/resource_label_events.rb12
-rw-r--r--lib/api/resource_state_events.rb30
-rw-r--r--lib/api/rpm_project_packages.rb63
-rw-r--r--lib/api/rubygem_packages.rb4
-rw-r--r--lib/api/search.rb17
-rw-r--r--lib/api/settings.rb1
-rw-r--r--lib/api/tags.rb4
-rw-r--r--lib/api/topics.rb20
-rw-r--r--lib/api/users.rb5
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