Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib/api
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 15:26:25 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 15:26:25 +0300
commita09983ae35713f5a2bbb100981116d31ce99826e (patch)
tree2ee2af7bd104d57086db360a7e6d8c9d5d43667a /lib/api
parent18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff)
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'lib/api')
-rw-r--r--lib/api/access_requests.rb2
-rw-r--r--lib/api/admin/ci/variables.rb2
-rw-r--r--lib/api/admin/instance_clusters.rb134
-rw-r--r--lib/api/admin/sidekiq.rb2
-rw-r--r--lib/api/api.rb27
-rw-r--r--lib/api/api_guard.rb12
-rw-r--r--lib/api/appearance.rb2
-rw-r--r--lib/api/applications.rb2
-rw-r--r--lib/api/avatar.rb2
-rw-r--r--lib/api/award_emoji.rb2
-rw-r--r--lib/api/badges.rb2
-rw-r--r--lib/api/boards.rb2
-rw-r--r--lib/api/branches.rb13
-rw-r--r--lib/api/broadcast_messages.rb2
-rw-r--r--lib/api/ci/pipeline_schedules.rb217
-rw-r--r--lib/api/ci/pipelines.rb189
-rw-r--r--lib/api/ci/runner.rb318
-rw-r--r--lib/api/ci/runners.rb289
-rw-r--r--lib/api/commit_statuses.rb4
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/composer_packages.rb156
-rw-r--r--lib/api/conan_packages.rb309
-rw-r--r--lib/api/container_registry_event.rb2
-rw-r--r--lib/api/deploy_keys.rb2
-rw-r--r--lib/api/deploy_tokens.rb6
-rw-r--r--lib/api/deployments.rb2
-rw-r--r--lib/api/discussions.rb18
-rw-r--r--lib/api/entities/approvals.rb9
-rw-r--r--lib/api/entities/basic_project_details.rb3
-rw-r--r--lib/api/entities/conan_package/conan_package_manifest.rb11
-rw-r--r--lib/api/entities/conan_package/conan_package_snapshot.rb11
-rw-r--r--lib/api/entities/conan_package/conan_recipe_manifest.rb11
-rw-r--r--lib/api/entities/conan_package/conan_recipe_snapshot.rb11
-rw-r--r--lib/api/entities/conan_package/conan_upload_urls.rb11
-rw-r--r--lib/api/entities/entity_helpers.rb19
-rw-r--r--lib/api/entities/go_module_version.rb10
-rw-r--r--lib/api/entities/group.rb1
-rw-r--r--lib/api/entities/group_detail.rb1
-rw-r--r--lib/api/entities/issuable_entity.rb36
-rw-r--r--lib/api/entities/issue_basic.rb8
-rw-r--r--lib/api/entities/merge_request_approvals.rb24
-rw-r--r--lib/api/entities/merge_request_basic.rb21
-rw-r--r--lib/api/entities/npm_package.rb11
-rw-r--r--lib/api/entities/npm_package_tag.rb9
-rw-r--r--lib/api/entities/nuget/dependency.rb14
-rw-r--r--lib/api/entities/nuget/dependency_group.rb14
-rw-r--r--lib/api/entities/nuget/metadatum.rb13
-rw-r--r--lib/api/entities/nuget/package_metadata.rb13
-rw-r--r--lib/api/entities/nuget/package_metadata_catalog_entry.rb19
-rw-r--r--lib/api/entities/nuget/packages_metadata.rb12
-rw-r--r--lib/api/entities/nuget/packages_metadata_item.rb15
-rw-r--r--lib/api/entities/nuget/packages_versions.rb11
-rw-r--r--lib/api/entities/nuget/search_result.rb21
-rw-r--r--lib/api/entities/nuget/search_result_version.rb13
-rw-r--r--lib/api/entities/nuget/search_results.rb12
-rw-r--r--lib/api/entities/nuget/service_index.rb12
-rw-r--r--lib/api/entities/package.rb42
-rw-r--r--lib/api/entities/package/pipeline.rb11
-rw-r--r--lib/api/entities/package_file.rb11
-rw-r--r--lib/api/entities/package_version.rb14
-rw-r--r--lib/api/entities/project.rb2
-rw-r--r--lib/api/entities/project_statistics.rb1
-rw-r--r--lib/api/entities/release.rb9
-rw-r--r--lib/api/entities/resource_state_event.rb18
-rw-r--r--lib/api/entities/snippet.rb12
-rw-r--r--lib/api/entities/user.rb2
-rw-r--r--lib/api/environments.rb2
-rw-r--r--lib/api/error_tracking.rb2
-rw-r--r--lib/api/events.rb2
-rw-r--r--lib/api/features.rb2
-rw-r--r--lib/api/files.rb4
-rw-r--r--lib/api/freeze_periods.rb2
-rwxr-xr-xlib/api/go_proxy.rb135
-rw-r--r--lib/api/group_boards.rb2
-rw-r--r--lib/api/group_clusters.rb18
-rw-r--r--lib/api/group_container_repositories.rb2
-rw-r--r--lib/api/group_export.rb2
-rw-r--r--lib/api/group_import.rb2
-rw-r--r--lib/api/group_labels.rb2
-rw-r--r--lib/api/group_milestones.rb6
-rw-r--r--lib/api/group_packages.rb44
-rw-r--r--lib/api/group_variables.rb6
-rw-r--r--lib/api/groups.rb11
-rw-r--r--lib/api/helpers.rb33
-rw-r--r--lib/api/helpers/common_helpers.rb20
-rw-r--r--lib/api/helpers/internal_helpers.rb4
-rw-r--r--lib/api/helpers/merge_requests_helpers.rb40
-rw-r--r--lib/api/helpers/packages/basic_auth_helpers.rb57
-rw-r--r--lib/api/helpers/packages/conan/api_helpers.rb225
-rw-r--r--lib/api/helpers/packages/dependency_proxy_helpers.rb36
-rw-r--r--lib/api/helpers/packages_helpers.rb52
-rw-r--r--lib/api/helpers/packages_manager_clients_helpers.rb63
-rw-r--r--lib/api/helpers/projects_helpers.rb6
-rw-r--r--lib/api/helpers/runner.rb7
-rw-r--r--lib/api/helpers/services_helpers.rb33
-rw-r--r--lib/api/helpers/snippets_helpers.rb26
-rw-r--r--lib/api/helpers/users_helpers.rb7
-rw-r--r--lib/api/helpers/wikis_helpers.rb35
-rw-r--r--lib/api/import_bitbucket_server.rb44
-rw-r--r--lib/api/import_github.rb2
-rw-r--r--lib/api/internal/base.rb8
-rw-r--r--lib/api/internal/pages.rb2
-rw-r--r--lib/api/issues.rb41
-rw-r--r--lib/api/job_artifacts.rb2
-rw-r--r--lib/api/jobs.rb4
-rw-r--r--lib/api/keys.rb2
-rw-r--r--lib/api/labels.rb2
-rw-r--r--lib/api/lint.rb2
-rw-r--r--lib/api/markdown.rb2
-rw-r--r--lib/api/maven_packages.rb251
-rw-r--r--lib/api/members.rb12
-rw-r--r--lib/api/merge_request_approvals.rb78
-rw-r--r--lib/api/merge_request_diffs.rb2
-rw-r--r--lib/api/merge_requests.rb31
-rw-r--r--lib/api/metrics/dashboard/annotations.rb2
-rw-r--r--lib/api/metrics/user_starred_dashboards.rb2
-rw-r--r--lib/api/milestone_responses.rb2
-rw-r--r--lib/api/namespaces.rb2
-rw-r--r--lib/api/notes.rb4
-rw-r--r--lib/api/notification_settings.rb2
-rw-r--r--lib/api/npm_packages.rb173
-rw-r--r--lib/api/nuget_packages.rb221
-rw-r--r--lib/api/package_files.rb33
-rw-r--r--lib/api/pages.rb2
-rw-r--r--lib/api/pages_domains.rb2
-rw-r--r--lib/api/pagination_params.rb2
-rw-r--r--lib/api/pipeline_schedules.rb215
-rw-r--r--lib/api/pipelines.rb187
-rw-r--r--lib/api/project_clusters.rb18
-rw-r--r--lib/api/project_container_repositories.rb2
-rw-r--r--lib/api/project_events.rb2
-rw-r--r--lib/api/project_export.rb2
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/project_import.rb2
-rw-r--r--lib/api/project_milestones.rb6
-rw-r--r--lib/api/project_packages.rb71
-rw-r--r--lib/api/project_repository_storage_moves.rb2
-rw-r--r--lib/api/project_snapshots.rb2
-rw-r--r--lib/api/project_snippets.rb23
-rw-r--r--lib/api/project_statistics.rb2
-rw-r--r--lib/api/project_templates.rb2
-rw-r--r--lib/api/projects.rb32
-rw-r--r--lib/api/projects_relation_builder.rb9
-rw-r--r--lib/api/protected_branches.rb2
-rw-r--r--lib/api/protected_tags.rb2
-rw-r--r--lib/api/pypi_packages.rb148
-rw-r--r--lib/api/release/links.rb2
-rw-r--r--lib/api/releases.rb4
-rw-r--r--lib/api/remote_mirrors.rb2
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/resource_label_events.rb2
-rw-r--r--lib/api/resource_milestone_events.rb5
-rw-r--r--lib/api/resource_state_events.rb50
-rw-r--r--lib/api/runner.rb297
-rw-r--r--lib/api/runners.rb287
-rw-r--r--lib/api/search.rb5
-rw-r--r--lib/api/services.rb2
-rw-r--r--lib/api/settings.rb11
-rw-r--r--lib/api/sidekiq_metrics.rb2
-rw-r--r--lib/api/snippets.rb28
-rw-r--r--lib/api/statistics.rb2
-rw-r--r--lib/api/submodules.rb2
-rw-r--r--lib/api/subscriptions.rb2
-rw-r--r--lib/api/suggestions.rb4
-rw-r--r--lib/api/system_hooks.rb2
-rw-r--r--lib/api/tags.rb2
-rw-r--r--lib/api/templates.rb2
-rw-r--r--lib/api/terraform/state.rb12
-rw-r--r--lib/api/todos.rb2
-rw-r--r--lib/api/triggers.rb4
-rw-r--r--lib/api/user_counts.rb2
-rw-r--r--lib/api/users.rb35
-rw-r--r--lib/api/validations/types/comma_separated_to_array.rb2
-rw-r--r--lib/api/validations/types/comma_separated_to_integer_array.rb15
-rw-r--r--lib/api/validations/types/labels_list.rb24
-rw-r--r--lib/api/validations/types/safe_file.rb15
-rw-r--r--lib/api/validations/types/workhorse_file.rb13
-rw-r--r--lib/api/variables.rb32
-rw-r--r--lib/api/version.rb2
-rw-r--r--lib/api/wikis.rb206
180 files changed, 4376 insertions, 1474 deletions
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index ee8dc822098..5305b25538f 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class AccessRequests < Grape::API
+ class AccessRequests < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb
index df731148bac..6b0ff5e9395 100644
--- a/lib/api/admin/ci/variables.rb
+++ b/lib/api/admin/ci/variables.rb
@@ -3,7 +3,7 @@
module API
module Admin
module Ci
- class Variables < Grape::API
+ class Variables < Grape::API::Instance
include PaginationParams
before { authenticated_as_admin! }
diff --git a/lib/api/admin/instance_clusters.rb b/lib/api/admin/instance_clusters.rb
new file mode 100644
index 00000000000..8208d10c089
--- /dev/null
+++ b/lib/api/admin/instance_clusters.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+module API
+ module Admin
+ class InstanceClusters < Grape::API::Instance
+ include PaginationParams
+
+ before do
+ authenticated_as_admin!
+ end
+
+ namespace 'admin' do
+ desc "Get list of all instance clusters" do
+ detail "This feature was introduced in GitLab 13.2."
+ end
+ get '/clusters' do
+ authorize! :read_cluster, clusterable_instance
+ present paginate(clusters_for_current_user), with: Entities::Cluster
+ end
+
+ desc "Get a single instance cluster" do
+ detail "This feature was introduced in GitLab 13.2."
+ end
+ params do
+ requires :cluster_id, type: Integer, desc: "The cluster ID"
+ end
+ get '/clusters/:cluster_id' do
+ authorize! :read_cluster, cluster
+
+ present cluster, with: Entities::Cluster
+ end
+
+ desc "Add an instance cluster" do
+ detail "This feature was introduced in GitLab 13.2."
+ end
+ params do
+ requires :name, type: String, desc: 'Cluster name'
+ optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
+ optional :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster'
+ optional :domain, type: String, desc: 'Cluster base domain'
+ optional :management_project_id, type: Integer, desc: 'The ID of the management project'
+ optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
+ requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
+ requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API'
+ requires :token, type: String, desc: 'Token to authenticate against Kubernetes'
+ optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
+ optional :namespace, type: String, desc: 'Unique namespace related to Project'
+ optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
+ end
+ end
+ post '/clusters/add' do
+ authorize! :add_cluster, clusterable_instance
+
+ user_cluster = ::Clusters::CreateService
+ .new(current_user, create_cluster_user_params)
+ .execute
+
+ if user_cluster.persisted?
+ present user_cluster, with: Entities::Cluster
+ else
+ render_validation_error!(user_cluster)
+ end
+ end
+
+ desc "Update an instance cluster" do
+ detail "This feature was introduced in GitLab 13.2."
+ end
+ params do
+ requires :cluster_id, type: Integer, desc: 'The cluster ID'
+ optional :name, type: String, desc: 'Cluster name'
+ optional :enabled, type: Boolean, desc: 'Enable or disable Gitlab\'s connection to your Kubernetes cluster'
+ optional :environment_scope, type: String, desc: 'The associated environment to the cluster'
+ optional :domain, type: String, desc: 'Cluster base domain'
+ optional :management_project_id, type: Integer, desc: 'The ID of the management project'
+ optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
+ optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
+ optional :token, type: String, desc: 'Token to authenticate against Kubernetes'
+ optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
+ optional :namespace, type: String, desc: 'Unique namespace related to Project'
+ end
+ end
+ put '/clusters/:cluster_id' do
+ authorize! :update_cluster, cluster
+
+ update_service = ::Clusters::UpdateService.new(current_user, update_cluster_params)
+
+ if update_service.execute(cluster)
+ present cluster, with: Entities::ClusterProject
+ else
+ render_validation_error!(cluster)
+ end
+ end
+
+ desc "Remove a cluster" do
+ detail "This feature was introduced in GitLab 13.2."
+ end
+ params do
+ requires :cluster_id, type: Integer, desc: "The cluster ID"
+ end
+ delete '/clusters/:cluster_id' do
+ authorize! :admin_cluster, cluster
+
+ destroy_conditionally!(cluster)
+ end
+ end
+
+ helpers do
+ def clusterable_instance
+ Clusters::Instance.new
+ end
+
+ def clusters_for_current_user
+ @clusters_for_current_user ||= ClustersFinder.new(clusterable_instance, current_user, :all).execute
+ end
+
+ def cluster
+ @cluster ||= clusters_for_current_user.find(params[:cluster_id])
+ end
+
+ def create_cluster_user_params
+ declared_params.merge({
+ provider_type: :user,
+ platform_type: :kubernetes,
+ clusterable: clusterable_instance
+ })
+ end
+
+ def update_cluster_params
+ declared_params(include_missing: false).without(:cluster_id)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/admin/sidekiq.rb b/lib/api/admin/sidekiq.rb
index a700bea0fd7..f4c84f2eee8 100644
--- a/lib/api/admin/sidekiq.rb
+++ b/lib/api/admin/sidekiq.rb
@@ -2,7 +2,7 @@
module API
module Admin
- class Sidekiq < Grape::API
+ class Sidekiq < Grape::API::Instance
before { authenticated_as_admin! }
namespace 'admin' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index fb67258f331..a89dc0fa6fa 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class API < Grape::API
+ class API < Grape::API::Instance
include APIGuard
LOG_FILENAME = Rails.root.join("log", "api_json.log")
@@ -46,6 +46,8 @@ module API
end
before do
+ coerce_nil_params_to_array!
+
Gitlab::ApplicationContext.push(
user: -> { @current_user },
project: -> { @project },
@@ -108,6 +110,7 @@ module API
end
format :json
+ formatter :json, Gitlab::Json::GrapeFormatter
content_type :txt, "text/plain"
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
@@ -122,6 +125,7 @@ module API
# Keep in alphabetical order
mount ::API::AccessRequests
mount ::API::Admin::Ci::Variables
+ mount ::API::Admin::InstanceClusters
mount ::API::Admin::Sidekiq
mount ::API::Appearance
mount ::API::Applications
@@ -131,6 +135,10 @@ module API
mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
+ mount ::API::Ci::Pipelines
+ mount ::API::Ci::PipelineSchedules
+ mount ::API::Ci::Runner
+ mount ::API::Ci::Runners
mount ::API::Commits
mount ::API::CommitStatuses
mount ::API::ContainerRegistryEvent
@@ -152,6 +160,7 @@ module API
mount ::API::Groups
mount ::API::GroupContainerRepositories
mount ::API::GroupVariables
+ mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
mount ::API::Issues
mount ::API::JobArtifacts
@@ -163,6 +172,7 @@ module API
mount ::API::Members
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
+ mount ::API::MergeRequestApprovals
mount ::API::Metrics::Dashboard::Annotations
mount ::API::Metrics::UserStarredDashboards
mount ::API::Namespaces
@@ -170,11 +180,20 @@ module API
mount ::API::Discussions
mount ::API::ResourceLabelEvents
mount ::API::ResourceMilestoneEvents
+ mount ::API::ResourceStateEvents
mount ::API::NotificationSettings
+ mount ::API::ProjectPackages
+ mount ::API::GroupPackages
+ mount ::API::PackageFiles
+ mount ::API::NugetPackages
+ mount ::API::PypiPackages
+ mount ::API::ComposerPackages
+ mount ::API::ConanPackages
+ mount ::API::MavenPackages
+ mount ::API::NpmPackages
+ mount ::API::GoProxy
mount ::API::Pages
mount ::API::PagesDomains
- mount ::API::Pipelines
- mount ::API::PipelineSchedules
mount ::API::ProjectClusters
mount ::API::ProjectContainerRepositories
mount ::API::ProjectEvents
@@ -195,8 +214,6 @@ module API
mount ::API::Release::Links
mount ::API::RemoteMirrors
mount ::API::Repositories
- mount ::API::Runner
- mount ::API::Runners
mount ::API::Search
mount ::API::Services
mount ::API::Settings
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index c6557fce541..4b87861a3de 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -43,7 +43,6 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
- prepend_if_ee('EE::API::APIGuard::HelperMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
include Gitlab::Auth::AuthFinders
def access_token
@@ -66,7 +65,7 @@ module API
def find_user_from_sources
deploy_token_from_request ||
- find_user_from_access_token ||
+ find_user_from_bearer_token ||
find_user_from_job_token ||
find_user_from_warden
end
@@ -153,7 +152,14 @@ module API
{ scope: e.scopes })
end
- response.finish
+ status, headers, body = response.finish
+
+ # Grape expects a Rack::Response
+ # (https://github.com/ruby-grape/grape/commit/c117bff7d22971675f4b34367d3a98bc31c8fc02),
+ # so we need to recreate the response again even though
+ # response.finish already does this.
+ # (https://github.com/nov/rack-oauth2/blob/40c9a99fd80486ccb8de0e4869ae384547c0d703/lib/rack/oauth2/server/abstract/error.rb#L26).
+ Rack::Response.new(body, status, headers)
end
end
end
diff --git a/lib/api/appearance.rb b/lib/api/appearance.rb
index 71a35bb4493..f98004af480 100644
--- a/lib/api/appearance.rb
+++ b/lib/api/appearance.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Appearance < Grape::API
+ class Appearance < Grape::API::Instance
before { authenticated_as_admin! }
helpers do
diff --git a/lib/api/applications.rb b/lib/api/applications.rb
index 70e6b8395d7..4e8d68c8d09 100644
--- a/lib/api/applications.rb
+++ b/lib/api/applications.rb
@@ -2,7 +2,7 @@
module API
# External applications API
- class Applications < Grape::API
+ class Applications < Grape::API::Instance
before { authenticated_as_admin! }
resource :applications do
diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb
index 0f14d003065..9501e777fff 100644
--- a/lib/api/avatar.rb
+++ b/lib/api/avatar.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Avatar < Grape::API
+ class Avatar < Grape::API::Instance
resource :avatar do
desc 'Return avatar url for a user' do
success Entities::Avatar
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 8e3b3ff8ce5..0a3df3ed96e 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class AwardEmoji < Grape::API
+ class AwardEmoji < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/badges.rb b/lib/api/badges.rb
index d2152fad07b..f6cd3f83ff3 100644
--- a/lib/api/badges.rb
+++ b/lib/api/badges.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Badges < Grape::API
+ class Badges < Grape::API::Instance
include PaginationParams
before { authenticate_non_get! }
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
index 87818903705..1f5086127a8 100644
--- a/lib/api/boards.rb
+++ b/lib/api/boards.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Boards < Grape::API
+ class Boards < Grape::API::Instance
include BoardsResponses
include PaginationParams
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 081e8ffe4f0..5e9c2caf8f5 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -3,7 +3,7 @@
require 'mime/types'
module API
- class Branches < Grape::API
+ class Branches < Grape::API::Instance
include PaginationParams
BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX)
@@ -32,14 +32,21 @@ module API
params do
use :pagination
use :filter_params
+
+ optional :page_token, type: String, desc: 'Name of branch to start the paginaition from'
end
get ':id/repository/branches' do
user_project.preload_protected_branches
repository = user_project.repository
- branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute
- branches = paginate(::Kaminari.paginate_array(branches))
+ if Feature.enabled?(:branch_list_keyset_pagination, user_project)
+ branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute(gitaly_pagination: true)
+ else
+ branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute
+ branches = paginate(::Kaminari.paginate_array(branches))
+ end
+
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
present(
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index 42e7dc751f0..dcf950d7a03 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class BroadcastMessages < Grape::API
+ class BroadcastMessages < Grape::API::Instance
include PaginationParams
resource :broadcast_messages do
diff --git a/lib/api/ci/pipeline_schedules.rb b/lib/api/ci/pipeline_schedules.rb
new file mode 100644
index 00000000000..80ad8aa04dd
--- /dev/null
+++ b/lib/api/ci/pipeline_schedules.rb
@@ -0,0 +1,217 @@
+# frozen_string_literal: true
+
+module API
+ module Ci
+ class PipelineSchedules < Grape::API::Instance
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get all pipeline schedules' do
+ success Entities::PipelineSchedule
+ end
+ params do
+ use :pagination
+ optional :scope, type: String, values: %w[active inactive],
+ desc: 'The scope of pipeline schedules'
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ get ':id/pipeline_schedules' do
+ authorize! :read_pipeline_schedule, user_project
+
+ schedules = ::Ci::PipelineSchedulesFinder.new(user_project).execute(scope: params[:scope])
+ .preload([:owner, :last_pipeline])
+ present paginate(schedules), with: Entities::PipelineSchedule
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ desc 'Get a single pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ end
+ get ':id/pipeline_schedules/:pipeline_schedule_id' do
+ present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ end
+
+ desc 'Create a new pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :description, type: String, desc: 'The description of pipeline schedule'
+ requires :ref, type: String, desc: 'The branch/tag name will be triggered', allow_blank: false
+ requires :cron, type: String, desc: 'The cron'
+ optional :cron_timezone, type: String, default: 'UTC', desc: 'The timezone'
+ optional :active, type: Boolean, default: true, desc: 'The activation of pipeline schedule'
+ end
+ post ':id/pipeline_schedules' do
+ authorize! :create_pipeline_schedule, user_project
+
+ pipeline_schedule = ::Ci::CreatePipelineScheduleService
+ .new(user_project, current_user, declared_params(include_missing: false))
+ .execute
+
+ if pipeline_schedule.persisted?
+ present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ else
+ render_validation_error!(pipeline_schedule)
+ end
+ end
+
+ desc 'Edit a pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ optional :description, type: String, desc: 'The description of pipeline schedule'
+ optional :ref, type: String, desc: 'The branch/tag name will be triggered'
+ optional :cron, type: String, desc: 'The cron'
+ optional :cron_timezone, type: String, desc: 'The timezone'
+ optional :active, type: Boolean, desc: 'The activation of pipeline schedule'
+ end
+ put ':id/pipeline_schedules/:pipeline_schedule_id' do
+ authorize! :update_pipeline_schedule, pipeline_schedule
+
+ if pipeline_schedule.update(declared_params(include_missing: false))
+ present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ else
+ render_validation_error!(pipeline_schedule)
+ end
+ end
+
+ desc 'Take ownership of a pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ end
+ post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
+ authorize! :update_pipeline_schedule, pipeline_schedule
+
+ if pipeline_schedule.own!(current_user)
+ present pipeline_schedule, with: Entities::PipelineScheduleDetails
+ else
+ render_validation_error!(pipeline_schedule)
+ end
+ end
+
+ desc 'Delete a pipeline schedule' do
+ success Entities::PipelineScheduleDetails
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ end
+ delete ':id/pipeline_schedules/:pipeline_schedule_id' do
+ authorize! :admin_pipeline_schedule, pipeline_schedule
+
+ destroy_conditionally!(pipeline_schedule)
+ end
+
+ desc 'Play a scheduled pipeline immediately' do
+ detail 'This feature was added in GitLab 12.8'
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ end
+ post ':id/pipeline_schedules/:pipeline_schedule_id/play' do
+ authorize! :play_pipeline_schedule, pipeline_schedule
+
+ job_id = RunPipelineScheduleWorker # rubocop:disable CodeReuse/Worker
+ .perform_async(pipeline_schedule.id, current_user.id)
+
+ if job_id
+ created!
+ else
+ render_api_error!('Unable to schedule pipeline run immediately', 500)
+ end
+ end
+
+ desc 'Create a new pipeline schedule variable' do
+ success Entities::Variable
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ requires :key, type: String, desc: 'The key of the variable'
+ requires :value, type: String, desc: 'The value of the variable'
+ optional :variable_type, type: String, values: ::Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
+ end
+ post ':id/pipeline_schedules/:pipeline_schedule_id/variables' do
+ authorize! :update_pipeline_schedule, pipeline_schedule
+
+ variable_params = declared_params(include_missing: false)
+ variable = pipeline_schedule.variables.create(variable_params)
+ if variable.persisted?
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ desc 'Edit a pipeline schedule variable' do
+ success Entities::Variable
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ requires :key, type: String, desc: 'The key of the variable'
+ optional :value, type: String, desc: 'The value of the variable'
+ optional :variable_type, type: String, values: ::Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
+ end
+ put ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
+ authorize! :update_pipeline_schedule, pipeline_schedule
+
+ if pipeline_schedule_variable.update(declared_params(include_missing: false))
+ present pipeline_schedule_variable, with: Entities::Variable
+ else
+ render_validation_error!(pipeline_schedule_variable)
+ end
+ end
+
+ desc 'Delete a pipeline schedule variable' do
+ success Entities::Variable
+ end
+ params do
+ requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
+ requires :key, type: String, desc: 'The key of the variable'
+ end
+ delete ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
+ authorize! :admin_pipeline_schedule, pipeline_schedule
+
+ status :accepted
+ present pipeline_schedule_variable.destroy, with: Entities::Variable
+ end
+ end
+
+ helpers do
+ # rubocop: disable CodeReuse/ActiveRecord
+ def pipeline_schedule
+ @pipeline_schedule ||=
+ user_project
+ .pipeline_schedules
+ .preload(:owner, :last_pipeline)
+ .find_by(id: params.delete(:pipeline_schedule_id)).tap do |pipeline_schedule|
+ unless can?(current_user, :read_pipeline_schedule, pipeline_schedule)
+ not_found!('Pipeline Schedule')
+ end
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def pipeline_schedule_variable
+ @pipeline_schedule_variable ||=
+ pipeline_schedule.variables.find_by(key: params[:key]).tap do |pipeline_schedule_variable|
+ unless pipeline_schedule_variable
+ not_found!('Pipeline Schedule Variable')
+ end
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+ end
+end
diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb
new file mode 100644
index 00000000000..33bb8b38d92
--- /dev/null
+++ b/lib/api/ci/pipelines.rb
@@ -0,0 +1,189 @@
+# frozen_string_literal: true
+
+module API
+ module Ci
+ class Pipelines < Grape::API::Instance
+ include PaginationParams
+
+ before { authenticate_non_get! }
+
+ params do
+ requires :id, type: String, desc: 'The project ID'
+ end
+ resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get all Pipelines of the project' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::PipelineBasic
+ end
+ params do
+ use :pagination
+ optional :scope, type: String, values: %w[running pending finished branches tags],
+ desc: 'The scope of pipelines'
+ optional :status, type: String, values: ::Ci::HasStatus::AVAILABLE_STATUSES,
+ desc: 'The status of pipelines'
+ optional :ref, type: String, desc: 'The ref of pipelines'
+ optional :sha, type: String, desc: 'The sha of pipelines'
+ optional :yaml_errors, type: Boolean, desc: 'Returns pipelines with invalid configurations'
+ optional :name, type: String, desc: 'The name of the user who triggered pipelines'
+ optional :username, type: String, desc: 'The username of the user who triggered pipelines'
+ optional :updated_before, type: DateTime, desc: 'Return pipelines updated before the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
+ optional :updated_after, type: DateTime, desc: 'Return pipelines updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
+ optional :order_by, type: String, values: ::Ci::PipelinesFinder::ALLOWED_INDEXED_COLUMNS, default: 'id',
+ desc: 'Order pipelines'
+ optional :sort, type: String, values: %w[asc desc], default: 'desc',
+ desc: 'Sort pipelines'
+ end
+ get ':id/pipelines' do
+ authorize! :read_pipeline, user_project
+ authorize! :read_build, user_project
+
+ pipelines = ::Ci::PipelinesFinder.new(user_project, current_user, params).execute
+ present paginate(pipelines), with: Entities::PipelineBasic
+ end
+
+ desc 'Create a new pipeline' do
+ detail 'This feature was introduced in GitLab 8.14'
+ success Entities::Pipeline
+ end
+ params do
+ requires :ref, type: String, desc: 'Reference'
+ optional :variables, Array, desc: 'Array of variables available in the pipeline'
+ end
+ post ':id/pipeline' do
+ Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42124')
+
+ authorize! :create_pipeline, user_project
+
+ pipeline_params = declared_params(include_missing: false)
+ .merge(variables_attributes: params[:variables])
+ .except(:variables)
+
+ new_pipeline = ::Ci::CreatePipelineService.new(user_project,
+ current_user,
+ pipeline_params)
+ .execute(:api, ignore_skip_ci: true, save_on_errors: false)
+
+ if new_pipeline.persisted?
+ present new_pipeline, with: Entities::Pipeline
+ else
+ render_validation_error!(new_pipeline)
+ end
+ end
+
+ desc 'Gets a the latest pipeline for the project branch' do
+ detail 'This feature was introduced in GitLab 12.3'
+ success Entities::Pipeline
+ end
+ params do
+ optional :ref, type: String, desc: 'branch ref of pipeline'
+ end
+ get ':id/pipelines/latest' do
+ authorize! :read_pipeline, latest_pipeline
+
+ present latest_pipeline, with: Entities::Pipeline
+ end
+
+ desc 'Gets a specific pipeline for the project' do
+ detail 'This feature was introduced in GitLab 8.11'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id' do
+ authorize! :read_pipeline, pipeline
+
+ present pipeline, with: Entities::Pipeline
+ end
+
+ desc 'Gets the variables for a given pipeline' do
+ detail 'This feature was introduced in GitLab 11.11'
+ success Entities::Variable
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id/variables' do
+ authorize! :read_pipeline_variable, pipeline
+
+ present pipeline.variables, with: Entities::Variable
+ end
+
+ desc 'Gets the test report for a given pipeline' do
+ detail 'This feature was introduced in GitLab 13.0. Disabled by default behind feature flag `junit_pipeline_view`'
+ success TestReportEntity
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ get ':id/pipelines/:pipeline_id/test_report' do
+ not_found! unless Feature.enabled?(:junit_pipeline_view, user_project)
+
+ authorize! :read_build, pipeline
+
+ present pipeline.test_reports, with: TestReportEntity, details: true
+ end
+
+ desc 'Deletes a pipeline' do
+ detail 'This feature was introduced in GitLab 11.6'
+ http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']]
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ delete ':id/pipelines/:pipeline_id' do
+ authorize! :destroy_pipeline, pipeline
+
+ destroy_conditionally!(pipeline) do
+ ::Ci::DestroyPipelineService.new(user_project, current_user).execute(pipeline)
+ end
+ end
+
+ desc 'Retry builds in the pipeline' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ post ':id/pipelines/:pipeline_id/retry' do
+ authorize! :update_pipeline, pipeline
+
+ pipeline.retry_failed(current_user)
+
+ present pipeline, with: Entities::Pipeline
+ end
+
+ desc 'Cancel all builds in the pipeline' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Pipeline
+ end
+ params do
+ requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
+ end
+ post ':id/pipelines/:pipeline_id/cancel' do
+ authorize! :update_pipeline, pipeline
+
+ pipeline.cancel_running
+
+ status 200
+ present pipeline.reset, with: Entities::Pipeline
+ end
+ end
+
+ helpers do
+ def pipeline
+ strong_memoize(:pipeline) do
+ user_project.ci_pipelines.find(params[:pipeline_id])
+ end
+ end
+
+ def latest_pipeline
+ strong_memoize(:latest_pipeline) do
+ user_project.latest_pipeline_for_ref(params[:ref])
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb
new file mode 100644
index 00000000000..31be1bb7e3e
--- /dev/null
+++ b/lib/api/ci/runner.rb
@@ -0,0 +1,318 @@
+# frozen_string_literal: true
+
+module API
+ module Ci
+ class Runner < Grape::API::Instance
+ helpers ::API::Helpers::Runner
+
+ resource :runners do
+ desc 'Registers a new Runner' do
+ success Entities::RunnerRegistrationDetails
+ http_codes [[201, 'Runner was created'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: 'Registration token'
+ optional :description, type: String, desc: %q(Runner's description)
+ optional :info, type: Hash, desc: %q(Runner's metadata)
+ optional :active, type: Boolean, desc: 'Should Runner be active'
+ optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
+ optional :access_level, type: String, values: ::Ci::Runner.access_levels.keys,
+ desc: 'The access_level of the runner'
+ optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
+ optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: %q(List of Runner's tags)
+ optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
+ end
+ post '/' do
+ attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :access_level, :maximum_timeout])
+ .merge(get_runner_details_from_request)
+
+ attributes =
+ if runner_registration_token_valid?
+ # Create shared runner. Requires admin access
+ attributes.merge(runner_type: :instance_type)
+ elsif project = Project.find_by_runners_token(params[:token])
+ # Create a specific runner for the project
+ attributes.merge(runner_type: :project_type, projects: [project])
+ elsif group = Group.find_by_runners_token(params[:token])
+ # Create a specific runner for the group
+ attributes.merge(runner_type: :group_type, groups: [group])
+ else
+ forbidden!
+ end
+
+ runner = ::Ci::Runner.create(attributes)
+
+ if runner.persisted?
+ present runner, with: Entities::RunnerRegistrationDetails
+ else
+ render_validation_error!(runner)
+ end
+ end
+
+ desc 'Deletes a registered Runner' do
+ http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ end
+ delete '/' do
+ authenticate_runner!
+
+ runner = ::Ci::Runner.find_by_token(params[:token])
+
+ destroy_conditionally!(runner)
+ end
+
+ desc 'Validates authentication credentials' do
+ http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ end
+ post '/verify' do
+ authenticate_runner!
+ status 200
+ end
+ end
+
+ resource :jobs do
+ before do
+ Gitlab::ApplicationContext.push(
+ user: -> { current_job&.user },
+ project: -> { current_job&.project }
+ )
+ end
+
+ desc 'Request a job' do
+ success Entities::JobRequest::Response
+ http_codes [[201, 'Job was scheduled'],
+ [204, 'No job for Runner'],
+ [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runner's authentication token)
+ optional :last_update, type: String, desc: %q(Runner's queue last_update token)
+ optional :info, type: Hash, desc: %q(Runner's metadata) do
+ optional :name, type: String, desc: %q(Runner's name)
+ optional :version, type: String, desc: %q(Runner's version)
+ optional :revision, type: String, desc: %q(Runner's revision)
+ optional :platform, type: String, desc: %q(Runner's platform)
+ optional :architecture, type: String, desc: %q(Runner's architecture)
+ optional :executor, type: String, desc: %q(Runner's executor)
+ optional :features, type: Hash, desc: %q(Runner's features)
+ end
+ optional :session, type: Hash, desc: %q(Runner's session data) do
+ optional :url, type: String, desc: %q(Session's url)
+ optional :certificate, type: String, desc: %q(Session's certificate)
+ optional :authorization, type: String, desc: %q(Session's authorization)
+ end
+ optional :job_age, type: Integer, desc: %q(Job should be older than passed age in seconds to be ran on runner)
+ end
+
+ # Since we serialize the build output ourselves to ensure Gitaly
+ # gRPC calls succeed, we need a custom Grape format to handle
+ # this:
+ # 1. Grape will ordinarily call `JSON.dump` when Content-Type is set
+ # to application/json. To avoid this, we need to define a custom type in
+ # `content_type` and a custom formatter to go with it.
+ # 2. Grape will parse the request input with the parser defined for
+ # `content_type`. If no such parser exists, it will be treated as text. We
+ # reuse the existing JSON parser to preserve the previous behavior.
+ content_type :build_json, 'application/json'
+ formatter :build_json, ->(object, _) { object }
+ parser :build_json, ::Grape::Parser::Json
+
+ post '/request' do
+ authenticate_runner!
+
+ unless current_runner.active?
+ header 'X-GitLab-Last-Update', current_runner.ensure_runner_queue_value
+ break no_content!
+ end
+
+ runner_params = declared_params(include_missing: false)
+
+ if current_runner.runner_queue_value_latest?(runner_params[:last_update])
+ header 'X-GitLab-Last-Update', runner_params[:last_update]
+ Gitlab::Metrics.add_event(:build_not_found_cached)
+ break no_content!
+ end
+
+ new_update = current_runner.ensure_runner_queue_value
+ result = ::Ci::RegisterJobService.new(current_runner).execute(runner_params)
+
+ if result.valid?
+ if result.build_json
+ Gitlab::Metrics.add_event(:build_found)
+ env['api.format'] = :build_json
+ body result.build_json
+ else
+ Gitlab::Metrics.add_event(:build_not_found)
+ header 'X-GitLab-Last-Update', new_update
+ no_content!
+ end
+ else
+ # We received build that is invalid due to concurrency conflict
+ Gitlab::Metrics.add_event(:build_invalid)
+ conflict!
+ end
+ end
+
+ desc 'Updates a job' do
+ http_codes [[200, 'Job was updated'], [403, 'Forbidden']]
+ end
+ params do
+ requires :token, type: String, desc: %q(Runners's authentication token)
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :trace, type: String, desc: %q(Job's full trace)
+ optional :state, type: String, desc: %q(Job's status: success, failed)
+ optional :failure_reason, type: String, desc: %q(Job's failure_reason)
+ end
+ put '/:id' do
+ job = authenticate_job!
+
+ job.trace.set(params[:trace]) if params[:trace]
+
+ Gitlab::Metrics.add_event(:update_build)
+
+ case params[:state].to_s
+ when 'running'
+ job.touch if job.needs_touch?
+ when 'success'
+ job.success!
+ when 'failed'
+ job.drop!(params[:failure_reason] || :unknown_failure)
+ end
+ end
+
+ desc 'Appends a patch to the job trace' do
+ http_codes [[202, 'Trace was patched'],
+ [400, 'Missing Content-Range header'],
+ [403, 'Forbidden'],
+ [416, 'Range not satisfiable']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ end
+ patch '/:id/trace' do
+ job = authenticate_job!
+
+ error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
+ content_range = request.headers['Content-Range']
+ content_range = content_range.split('-')
+
+ # TODO:
+ # it seems that `Content-Range` as formatted by runner is wrong,
+ # the `byte_end` should point to final byte, but it points byte+1
+ # that means that we have to calculate end of body,
+ # as we cannot use `content_length[1]`
+ # Issue: https://gitlab.com/gitlab-org/gitlab-runner/issues/3275
+
+ body_data = request.body.read
+ body_start = content_range[0].to_i
+ body_end = body_start + body_data.bytesize
+
+ stream_size = job.trace.append(body_data, body_start)
+ unless stream_size == body_end
+ break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{stream_size}" })
+ end
+
+ status 202
+ header 'Job-Status', job.status
+ header 'Range', "0-#{stream_size}"
+ header 'X-GitLab-Trace-Update-Interval', job.trace.update_interval.to_s
+ end
+
+ desc 'Authorize artifacts uploading for job' do
+ http_codes [[200, 'Upload allowed'],
+ [403, 'Forbidden'],
+ [405, 'Artifacts support not enabled'],
+ [413, 'File too large']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+
+ # NOTE:
+ # In current runner, filesize parameter would be empty here. This is because archive is streamed by runner,
+ # so the archive size is not known ahead of time. Streaming is done to not use additional I/O on
+ # Runner to first save, and then send via Network.
+ optional :filesize, type: Integer, desc: %q(Artifacts filesize)
+
+ optional :artifact_type, type: String, desc: %q(The type of artifact),
+ default: 'archive', values: ::Ci::JobArtifact.file_types.keys
+ end
+ post '/:id/artifacts/authorize' do
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ require_gitlab_workhorse!
+
+ job = authenticate_job!
+
+ result = ::Ci::CreateJobArtifactsService.new(job).authorize(artifact_type: params[:artifact_type], filesize: params[:filesize])
+
+ if result[:status] == :success
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+ status :ok
+ result[:headers]
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ desc 'Upload artifacts for job' do
+ success Entities::JobRequest::Response
+ http_codes [[201, 'Artifact uploaded'],
+ [400, 'Bad request'],
+ [403, 'Forbidden'],
+ [405, 'Artifacts support not enabled'],
+ [413, 'File too large']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: %(The artifact file to store (generated by Multipart middleware))
+ optional :token, type: String, desc: %q(Job's authentication token)
+ optional :expire_in, type: String, desc: %q(Specify when artifacts should expire)
+ optional :artifact_type, type: String, desc: %q(The type of artifact),
+ default: 'archive', values: ::Ci::JobArtifact.file_types.keys
+ optional :artifact_format, type: String, desc: %q(The format of artifact),
+ default: 'zip', values: ::Ci::JobArtifact.file_formats.keys
+ optional :metadata, type: ::API::Validations::Types::WorkhorseFile, desc: %(The artifact metadata to store (generated by Multipart middleware))
+ end
+ post '/:id/artifacts' do
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ require_gitlab_workhorse!
+
+ job = authenticate_job!
+
+ artifacts = params[:file]
+ metadata = params[:metadata]
+
+ result = ::Ci::CreateJobArtifactsService.new(job).execute(artifacts, params, metadata_file: metadata)
+
+ if result[:status] == :success
+ status :created
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ desc 'Download the artifacts file for job' do
+ http_codes [[200, 'Upload allowed'],
+ [403, 'Forbidden'],
+ [404, 'Artifact not found']]
+ end
+ params do
+ requires :id, type: Integer, desc: %q(Job's ID)
+ optional :token, type: String, desc: %q(Job's authentication token)
+ optional :direct_download, default: false, type: Boolean, desc: %q(Perform direct download from remote storage instead of proxying artifacts)
+ end
+ get '/:id/artifacts' do
+ job = authenticate_job!(require_running: false)
+
+ present_carrierwave_file!(job.artifacts_file, supports_direct_download: params[:direct_download])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb
new file mode 100644
index 00000000000..2c156a71160
--- /dev/null
+++ b/lib/api/ci/runners.rb
@@ -0,0 +1,289 @@
+# frozen_string_literal: true
+
+module API
+ module Ci
+ class Runners < Grape::API::Instance
+ include PaginationParams
+
+ before { authenticate! }
+
+ resource :runners do
+ desc 'Get runners available for user' do
+ success Entities::Runner
+ end
+ params do
+ optional :scope, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES,
+ desc: 'The scope of specific runners to show'
+ optional :type, type: String, values: ::Ci::Runner::AVAILABLE_TYPES,
+ desc: 'The type of the runners to show'
+ optional :status, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES,
+ desc: 'The status of the runners to show'
+ optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The tags of the runners to show'
+ use :pagination
+ end
+ get do
+ runners = current_user.ci_owned_runners
+ runners = filter_runners(runners, params[:scope], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES)
+ runners = filter_runners(runners, params[:type], allowed_scopes: ::Ci::Runner::AVAILABLE_TYPES)
+ runners = filter_runners(runners, params[:status], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES)
+ runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
+
+ present paginate(runners), with: Entities::Runner
+ end
+
+ desc 'Get all runners - shared and specific' do
+ success Entities::Runner
+ end
+ params do
+ optional :scope, type: String, values: ::Ci::Runner::AVAILABLE_SCOPES,
+ desc: 'The scope of specific runners to show'
+ optional :type, type: String, values: ::Ci::Runner::AVAILABLE_TYPES,
+ desc: 'The type of the runners to show'
+ optional :status, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES,
+ desc: 'The status of the runners to show'
+ optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The tags of the runners to show'
+ use :pagination
+ end
+ get 'all' do
+ authenticated_as_admin!
+
+ runners = ::Ci::Runner.all
+ runners = filter_runners(runners, params[:scope])
+ runners = filter_runners(runners, params[:type], allowed_scopes: ::Ci::Runner::AVAILABLE_TYPES)
+ runners = filter_runners(runners, params[:status], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES)
+ runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
+
+ present paginate(runners), with: Entities::Runner
+ end
+
+ desc "Get runner's details" do
+ success Entities::RunnerDetails
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ end
+ get ':id' do
+ runner = get_runner(params[:id])
+ authenticate_show_runner!(runner)
+
+ present runner, with: Entities::RunnerDetails, current_user: current_user
+ end
+
+ desc "Update runner's details" do
+ success Entities::RunnerDetails
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ optional :description, type: String, desc: 'The description of the runner'
+ optional :active, type: Boolean, desc: 'The state of a runner'
+ optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The list of tags for a runner'
+ optional :run_untagged, type: Boolean, desc: 'Flag indicating the runner can execute untagged jobs'
+ optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
+ optional :access_level, type: String, values: ::Ci::Runner.access_levels.keys,
+ desc: 'The access_level of the runner'
+ optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
+ at_least_one_of :description, :active, :tag_list, :run_untagged, :locked, :access_level, :maximum_timeout
+ end
+ put ':id' do
+ runner = get_runner(params.delete(:id))
+ authenticate_update_runner!(runner)
+ update_service = ::Ci::UpdateRunnerService.new(runner)
+
+ if update_service.update(declared_params(include_missing: false))
+ present runner, with: Entities::RunnerDetails, current_user: current_user
+ else
+ render_validation_error!(runner)
+ end
+ end
+
+ desc 'Remove a runner' do
+ success Entities::Runner
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ end
+ delete ':id' do
+ runner = get_runner(params[:id])
+
+ authenticate_delete_runner!(runner)
+
+ destroy_conditionally!(runner)
+ end
+
+ desc 'List jobs running on a runner' do
+ success Entities::JobBasicWithProject
+ end
+ params do
+ requires :id, type: Integer, desc: 'The ID of the runner'
+ optional :status, type: String, desc: 'Status of the job', values: ::Ci::Build::AVAILABLE_STATUSES
+ optional :order_by, type: String, desc: 'Order by `id` or not', values: ::Ci::RunnerJobsFinder::ALLOWED_INDEXED_COLUMNS
+ optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Sort by asc (ascending) or desc (descending)'
+ use :pagination
+ end
+ get ':id/jobs' do
+ runner = get_runner(params[:id])
+ authenticate_list_runners_jobs!(runner)
+
+ jobs = ::Ci::RunnerJobsFinder.new(runner, params).execute
+
+ present paginate(jobs), with: Entities::JobBasicWithProject
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before { authorize_admin_project }
+
+ desc 'Get runners available for project' do
+ success Entities::Runner
+ end
+ params do
+ optional :scope, type: String, values: ::Ci::Runner::AVAILABLE_SCOPES,
+ desc: 'The scope of specific runners to show'
+ optional :type, type: String, values: ::Ci::Runner::AVAILABLE_TYPES,
+ desc: 'The type of the runners to show'
+ optional :status, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES,
+ desc: 'The status of the runners to show'
+ optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The tags of the runners to show'
+ use :pagination
+ end
+ get ':id/runners' do
+ runners = ::Ci::Runner.owned_or_instance_wide(user_project.id)
+ # scope is deprecated (for project runners), however api documentation still supports it.
+ # Not including them in `apply_filter` method as it's not supported for group runners
+ runners = filter_runners(runners, params[:scope])
+ runners = apply_filter(runners, params)
+
+ present paginate(runners), with: Entities::Runner
+ end
+
+ desc 'Enable a runner for a project' do
+ success Entities::Runner
+ end
+ params do
+ requires :runner_id, type: Integer, desc: 'The ID of the runner'
+ end
+ post ':id/runners' do
+ runner = get_runner(params[:runner_id])
+ authenticate_enable_runner!(runner)
+
+ if runner.assign_to(user_project)
+ present runner, with: Entities::Runner
+ else
+ render_validation_error!(runner)
+ end
+ end
+
+ desc "Disable project's runner" do
+ success Entities::Runner
+ end
+ params do
+ requires :runner_id, type: Integer, desc: 'The ID of the runner'
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ delete ':id/runners/:runner_id' do
+ runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
+ not_found!('Runner') unless runner_project
+
+ runner = runner_project.runner
+ forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
+
+ destroy_conditionally!(runner_project)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before { authorize_admin_group }
+
+ desc 'Get runners available for group' do
+ success Entities::Runner
+ end
+ params do
+ optional :type, type: String, values: ::Ci::Runner::AVAILABLE_TYPES,
+ desc: 'The type of the runners to show'
+ optional :status, type: String, values: ::Ci::Runner::AVAILABLE_STATUSES,
+ desc: 'The status of the runners to show'
+ optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The tags of the runners to show'
+ use :pagination
+ end
+ get ':id/runners' do
+ runners = ::Ci::Runner.belonging_to_group(user_group.id, include_ancestors: true)
+ runners = apply_filter(runners, params)
+
+ present paginate(runners), with: Entities::Runner
+ end
+ end
+
+ helpers do
+ def filter_runners(runners, scope, allowed_scopes: ::Ci::Runner::AVAILABLE_SCOPES)
+ return runners unless scope.present?
+
+ unless allowed_scopes.include?(scope)
+ render_api_error!('Scope contains invalid value', 400)
+ end
+
+ # Support deprecated scopes
+ if runners.respond_to?("deprecated_#{scope}")
+ scope = "deprecated_#{scope}"
+ end
+
+ runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def apply_filter(runners, params)
+ runners = filter_runners(runners, params[:type], allowed_scopes: ::Ci::Runner::AVAILABLE_TYPES)
+ runners = filter_runners(runners, params[:status], allowed_scopes: ::Ci::Runner::AVAILABLE_STATUSES)
+ runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
+
+ runners
+ end
+
+ def get_runner(id)
+ runner = ::Ci::Runner.find(id)
+ not_found!('Runner') unless runner
+ runner
+ end
+
+ def authenticate_show_runner!(runner)
+ return if runner.instance_type? || current_user.admin?
+
+ forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
+ end
+
+ def authenticate_update_runner!(runner)
+ return if current_user.admin?
+
+ forbidden!("No access granted") unless can?(current_user, :update_runner, runner)
+ end
+
+ def authenticate_delete_runner!(runner)
+ return if current_user.admin?
+
+ forbidden!("Runner associated with more than one project") if runner.projects.count > 1
+ forbidden!("No access granted") unless can?(current_user, :delete_runner, runner)
+ end
+
+ def authenticate_enable_runner!(runner)
+ forbidden!("Runner is a group runner") if runner.group_type?
+
+ return if current_user.admin?
+
+ forbidden!("Runner is locked") if runner.locked?
+ forbidden!("No access granted") unless can?(current_user, :assign_runner, runner)
+ end
+
+ def authenticate_list_runners_jobs!(runner)
+ return if current_user.admin?
+
+ forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index b4c5d7869a2..140351c9e5c 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -3,7 +3,7 @@
require 'mime/types'
module API
- class CommitStatuses < Grape::API
+ class CommitStatuses < Grape::API::Instance
params do
requires :id, type: String, desc: 'The ID of a project'
end
@@ -60,7 +60,7 @@ module API
not_found! 'Commit' unless commit
- # Since the CommitStatus is attached to Ci::Pipeline (in the future Pipeline)
+ # Since the CommitStatus is attached to ::Ci::Pipeline (in the future Pipeline)
# We need to always have the pipeline object
# To have a valid pipeline object that can be attached to specific MR
# Other CI service needs to send `ref`
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 086a1b7c402..1a0fe393753 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -3,7 +3,7 @@
require 'mime/types'
module API
- class Commits < Grape::API
+ class Commits < Grape::API::Instance
include PaginationParams
before do
diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb
new file mode 100644
index 00000000000..726dc89271a
--- /dev/null
+++ b/lib/api/composer_packages.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+# PHP composer support (https://getcomposer.org/)
+module API
+ class ComposerPackages < Grape::API::Instance
+ helpers ::API::Helpers::PackagesManagerClientsHelpers
+ helpers ::API::Helpers::RelatedResourcesHelpers
+ helpers ::API::Helpers::Packages::BasicAuthHelpers
+ include ::API::Helpers::Packages::BasicAuthHelpers::Constants
+ include ::Gitlab::Utils::StrongMemoize
+
+ content_type :json, 'application/json'
+ default_format :json
+
+ COMPOSER_ENDPOINT_REQUIREMENTS = {
+ package_name: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ default_format :json
+
+ rescue_from ArgumentError do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ rescue_from ActiveRecord::RecordInvalid do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ helpers do
+ def packages
+ strong_memoize(:packages) do
+ packages = ::Packages::Composer::PackagesFinder.new(current_user, user_group).execute
+
+ if params[:package_name].present?
+ packages = packages.with_name(params[:package_name])
+ end
+
+ packages
+ end
+ end
+
+ def presenter
+ @presenter ||= ::Packages::Composer::PackagesPresenter.new(user_group, packages)
+ end
+ end
+
+ before do
+ require_packages_enabled!
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+
+ resource :group, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before do
+ user_group
+ end
+
+ desc 'Composer packages endpoint at group level'
+
+ route_setting :authentication, job_token_allowed: true
+
+ get ':id/-/packages/composer/packages' do
+ presenter.root
+ end
+
+ desc 'Composer packages endpoint at group level for packages list'
+
+ params do
+ requires :sha, type: String, desc: 'Shasum of current json'
+ end
+
+ route_setting :authentication, job_token_allowed: true
+
+ get ':id/-/packages/composer/p/:sha' do
+ presenter.provider
+ end
+
+ desc 'Composer packages endpoint at group level for package versions metadata'
+
+ params do
+ requires :package_name, type: String, file_path: true, desc: 'The Composer package name'
+ end
+
+ route_setting :authentication, job_token_allowed: true
+
+ get ':id/-/packages/composer/*package_name', requirements: COMPOSER_ENDPOINT_REQUIREMENTS, file_path: true do
+ not_found! if packages.empty?
+
+ presenter.package_versions
+ end
+ end
+
+ params do
+ requires :id, type: Integer, desc: 'The ID of a project'
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before do
+ unauthorized_user_project!
+ end
+
+ desc 'Composer packages endpoint for registering packages'
+
+ namespace ':id/packages/composer' do
+ route_setting :authentication, job_token_allowed: true
+
+ params do
+ optional :branch, type: String, desc: 'The name of the branch'
+ optional :tag, type: String, desc: 'The name of the tag'
+ exactly_one_of :tag, :branch
+ end
+
+ post do
+ authorize_create_package!(authorized_user_project)
+
+ if params[:branch].present?
+ params[:branch] = find_branch!(params[:branch])
+ elsif params[:tag].present?
+ params[:tag] = find_tag!(params[:tag])
+ else
+ bad_request!
+ end
+
+ track_event('register_package')
+
+ ::Packages::Composer::CreatePackageService
+ .new(authorized_user_project, current_user, declared_params)
+ .execute
+
+ created!
+ end
+
+ params do
+ requires :sha, type: String, desc: 'Shasum of current json'
+ requires :package_name, type: String, file_path: true, desc: 'The Composer package name'
+ end
+
+ get 'archives/*package_name' do
+ metadata = unauthorized_user_project
+ .packages
+ .composer
+ .with_name(params[:package_name])
+ .with_composer_target(params[:sha])
+ .first
+ &.composer_metadatum
+
+ not_found! unless metadata
+
+ send_git_archive unauthorized_user_project.repository, ref: metadata.target_sha, format: 'zip', append_sha: true
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/conan_packages.rb b/lib/api/conan_packages.rb
new file mode 100644
index 00000000000..1d941e422a7
--- /dev/null
+++ b/lib/api/conan_packages.rb
@@ -0,0 +1,309 @@
+# frozen_string_literal: true
+
+# Conan Package Manager Client API
+#
+# These API endpoints are not consumed directly by users, so there is no documentation for the
+# individual endpoints. They are called by the Conan package manager client when users run commands
+# like `conan install` or `conan upload`. The usage of the GitLab Conan repository is documented here:
+# https://docs.gitlab.com/ee/user/packages/conan_repository/#installing-a-package
+#
+# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798
+module API
+ class ConanPackages < Grape::API::Instance
+ helpers ::API::Helpers::PackagesManagerClientsHelpers
+
+ PACKAGE_REQUIREMENTS = {
+ package_name: API::NO_SLASH_URL_PART_REGEX,
+ package_version: API::NO_SLASH_URL_PART_REGEX,
+ package_username: API::NO_SLASH_URL_PART_REGEX,
+ package_channel: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ FILE_NAME_REQUIREMENTS = {
+ file_name: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ PACKAGE_COMPONENT_REGEX = Gitlab::Regex.conan_recipe_component_regex
+ CONAN_REVISION_REGEX = Gitlab::Regex.conan_revision_regex
+
+ before do
+ require_packages_enabled!
+
+ # Personal access token will be extracted from Bearer or Basic authorization
+ # in the overridden find_personal_access_token or find_user_from_job_token helpers
+ authenticate!
+ end
+
+ namespace 'packages/conan/v1' do
+ desc 'Ping the Conan API' do
+ detail 'This feature was introduced in GitLab 12.2'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get 'ping' do
+ header 'X-Conan-Server-Capabilities', [].join(',')
+ end
+
+ desc 'Search for packages' do
+ detail 'This feature was introduced in GitLab 12.4'
+ end
+ params do
+ requires :q, type: String, desc: 'Search query'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get 'conans/search' do
+ service = ::Packages::Conan::SearchService.new(current_user, query: params[:q]).execute
+ service.payload
+ end
+
+ namespace 'users' do
+ format :txt
+
+ desc 'Authenticate user against conan CLI' do
+ detail 'This feature was introduced in GitLab 12.2'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get 'authenticate' do
+ unauthorized! unless token
+
+ token.to_jwt
+ end
+
+ desc 'Check for valid user credentials per conan CLI' do
+ detail 'This feature was introduced in GitLab 12.4'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get 'check_credentials' do
+ authenticate!
+ :ok
+ end
+ end
+
+ params do
+ requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
+ requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
+ requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
+ requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
+ end
+ namespace 'conans/:package_name/:package_version/:package_username/:package_channel', requirements: PACKAGE_REQUIREMENTS do
+ # Get the snapshot
+ #
+ # the snapshot is a hash of { filename: md5 hash }
+ # md5 hash is the has of that file. This hash is used to diff the files existing on the client
+ # to determine which client files need to be uploaded if no recipe exists the snapshot is empty
+ desc 'Package Snapshot' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan package ID'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get 'packages/:conan_package_reference' do
+ authorize!(:read_package, project)
+
+ presenter = ::Packages::Conan::PackagePresenter.new(
+ recipe,
+ current_user,
+ project,
+ conan_package_reference: params[:conan_package_reference]
+ )
+
+ present presenter, with: ::API::Entities::ConanPackage::ConanPackageSnapshot
+ end
+
+ desc 'Recipe Snapshot' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get do
+ authorize!(:read_package, project)
+
+ presenter = ::Packages::Conan::PackagePresenter.new(recipe, current_user, project)
+
+ present presenter, with: ::API::Entities::ConanPackage::ConanRecipeSnapshot
+ end
+
+ # Get the manifest
+ # returns the download urls for the existing recipe in the registry
+ #
+ # the manifest is a hash of { filename: url }
+ # where the url is the download url for the file
+ desc 'Package Digest' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan package ID'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get 'packages/:conan_package_reference/digest' do
+ present_package_download_urls
+ end
+
+ desc 'Recipe Digest' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get 'digest' do
+ present_recipe_download_urls
+ end
+
+ # Get the download urls
+ #
+ # returns the download urls for the existing recipe or package in the registry
+ #
+ # the manifest is a hash of { filename: url }
+ # where the url is the download url for the file
+ desc 'Package Download Urls' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan package ID'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get 'packages/:conan_package_reference/download_urls' do
+ present_package_download_urls
+ end
+
+ desc 'Recipe Download Urls' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get 'download_urls' do
+ present_recipe_download_urls
+ end
+
+ # Get the upload urls
+ #
+ # request body contains { filename: filesize } where the filename is the
+ # name of the file the conan client is requesting to upload
+ #
+ # returns { filename: url }
+ # where the url is the upload url for the file that the conan client will use
+ desc 'Package Upload Urls' do
+ detail 'This feature was introduced in GitLab 12.4'
+ end
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan package ID'
+ end
+ route_setting :authentication, job_token_allowed: true
+ post 'packages/:conan_package_reference/upload_urls' do
+ authorize!(:read_package, project)
+
+ status 200
+ upload_urls = package_upload_urls(::Packages::Conan::FileMetadatum::PACKAGE_FILES)
+
+ present upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls
+ end
+
+ desc 'Recipe Upload Urls' do
+ detail 'This feature was introduced in GitLab 12.4'
+ end
+ route_setting :authentication, job_token_allowed: true
+ post 'upload_urls' do
+ authorize!(:read_package, project)
+
+ status 200
+ upload_urls = recipe_upload_urls(::Packages::Conan::FileMetadatum::RECIPE_FILES)
+
+ present upload_urls, with: ::API::Entities::ConanPackage::ConanUploadUrls
+ end
+
+ desc 'Delete Package' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+ route_setting :authentication, job_token_allowed: true
+ delete do
+ authorize!(:destroy_package, project)
+
+ track_event('delete_package')
+
+ package.destroy
+ end
+ end
+
+ params do
+ requires :package_name, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package name'
+ requires :package_version, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package version'
+ requires :package_username, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package username'
+ requires :package_channel, type: String, regexp: PACKAGE_COMPONENT_REGEX, desc: 'Package channel'
+ requires :recipe_revision, type: String, regexp: CONAN_REVISION_REGEX, desc: 'Conan Recipe Revision'
+ end
+ namespace 'files/:package_name/:package_version/:package_username/:package_channel/:recipe_revision', requirements: PACKAGE_REQUIREMENTS do
+ before do
+ authenticate_non_get!
+ end
+
+ params do
+ requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.conan_file_name_regex
+ end
+ namespace 'export/:file_name', requirements: FILE_NAME_REQUIREMENTS do
+ desc 'Download recipe files' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get do
+ download_package_file(:recipe_file)
+ end
+
+ desc 'Upload recipe package files' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+ params do
+ use :workhorse_upload_params
+ end
+ route_setting :authentication, job_token_allowed: true
+ put do
+ upload_package_file(:recipe_file)
+ end
+
+ desc 'Workhorse authorize the conan recipe file' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+ route_setting :authentication, job_token_allowed: true
+ put 'authorize' do
+ authorize_workhorse!(subject: project)
+ end
+ end
+
+ params do
+ requires :conan_package_reference, type: String, desc: 'Conan Package ID'
+ requires :package_revision, type: String, desc: 'Conan Package Revision'
+ requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.conan_file_name_regex
+ end
+ namespace 'package/:conan_package_reference/:package_revision/:file_name', requirements: FILE_NAME_REQUIREMENTS do
+ desc 'Download package files' do
+ detail 'This feature was introduced in GitLab 12.5'
+ end
+ route_setting :authentication, job_token_allowed: true
+ get do
+ download_package_file(:package_file)
+ end
+
+ desc 'Workhorse authorize the conan package file' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+ route_setting :authentication, job_token_allowed: true
+ put 'authorize' do
+ authorize_workhorse!(subject: project)
+ end
+
+ desc 'Upload package files' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+ params do
+ use :workhorse_upload_params
+ end
+ route_setting :authentication, job_token_allowed: true
+ put do
+ upload_package_file(:package_file)
+ end
+ end
+ end
+ end
+
+ helpers do
+ include Gitlab::Utils::StrongMemoize
+ include ::API::Helpers::RelatedResourcesHelpers
+ include ::API::Helpers::Packages::Conan::ApiHelpers
+ end
+ end
+end
diff --git a/lib/api/container_registry_event.rb b/lib/api/container_registry_event.rb
index 6d93cc65336..0b7c35cadbd 100644
--- a/lib/api/container_registry_event.rb
+++ b/lib/api/container_registry_event.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ContainerRegistryEvent < Grape::API
+ class ContainerRegistryEvent < Grape::API::Instance
DOCKER_DISTRIBUTION_EVENTS_V1_JSON = 'application/vnd.docker.distribution.events.v1+json'
before { authenticate_registry_notification! }
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 3259b615369..ad37b7578ad 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class DeployKeys < Grape::API
+ class DeployKeys < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb
index 0fbbd96cf02..96aa2445f56 100644
--- a/lib/api/deploy_tokens.rb
+++ b/lib/api/deploy_tokens.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class DeployTokens < Grape::API
+ class DeployTokens < Grape::API::Instance
include PaginationParams
helpers do
@@ -56,7 +56,7 @@ module API
params do
requires :name, type: String, desc: "New deploy token's name"
- requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
+ requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", "write_registry", "read_package_registry", or "write_package_registry".'
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
@@ -119,7 +119,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the deploy token'
- requires :scopes, type: Array[String], values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
+ requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::DeployToken::AVAILABLE_SCOPES.map(&:to_s),
desc: 'Indicates the deploy token scopes. Must be at least one of "read_repository", "read_registry", "write_registry", "read_package_registry", or "write_package_registry".'
optional :expires_at, type: DateTime, desc: 'Expiration date for the deploy token. Does not expire if no value is provided.'
optional :username, type: String, desc: 'Username for deploy token. Default is `gitlab+deploy-token-{n}`'
diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb
index cb1dca11e87..87144fd31cc 100644
--- a/lib/api/deployments.rb
+++ b/lib/api/deployments.rb
@@ -2,7 +2,7 @@
module API
# Deployments RESTful API endpoints
- class Deployments < Grape::API
+ class Deployments < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
index 7b453ada41c..c431ec8e1e4 100644
--- a/lib/api/discussions.rb
+++ b/lib/api/discussions.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Discussions < Grape::API
+ class Discussions < Grape::API::Instance
include PaginationParams
helpers ::API::Helpers::NotesHelpers
helpers ::RendersNotes
@@ -76,10 +76,18 @@ module API
optional :y, type: Integer, desc: 'Y coordinate in the image'
optional :line_range, type: Hash, desc: 'Multi-line start and end' do
- requires :start_line_code, type: String, desc: 'Start line code for multi-line note'
- requires :end_line_code, type: String, desc: 'End line code for multi-line note'
- requires :start_line_type, type: String, desc: 'Start line type for multi-line note'
- requires :end_line_type, type: String, desc: 'End line type for multi-line note'
+ optional :start, type: Hash do
+ optional :line_code, type: String, desc: 'Start line code for multi-line note'
+ optional :type, type: String, desc: 'Start line type for multi-line note'
+ optional :old_line, type: String, desc: 'Start old_line line number'
+ optional :new_line, type: String, desc: 'Start new_line line number'
+ end
+ optional :end, type: Hash do
+ optional :line_code, type: String, desc: 'End line code for multi-line note'
+ optional :type, type: String, desc: 'End line type for multi-line note'
+ optional :old_line, type: String, desc: 'End old_line line number'
+ optional :new_line, type: String, desc: 'End new_line line number'
+ end
end
end
end
diff --git a/lib/api/entities/approvals.rb b/lib/api/entities/approvals.rb
new file mode 100644
index 00000000000..74973772831
--- /dev/null
+++ b/lib/api/entities/approvals.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Approvals < Grape::Entity
+ expose :user, using: ::API::Entities::UserBasic
+ end
+ end
+end
diff --git a/lib/api/entities/basic_project_details.rb b/lib/api/entities/basic_project_details.rb
index 13bc19456b3..cf0b32bed26 100644
--- a/lib/api/entities/basic_project_details.rb
+++ b/lib/api/entities/basic_project_details.rb
@@ -33,7 +33,8 @@ module API
project.avatar_url(only_path: false)
end
- expose :star_count, :forks_count
+ expose :forks_count
+ expose :star_count
expose :last_activity_at
expose :namespace, using: 'API::Entities::NamespaceBasic'
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
diff --git a/lib/api/entities/conan_package/conan_package_manifest.rb b/lib/api/entities/conan_package/conan_package_manifest.rb
new file mode 100644
index 00000000000..e6acfe1912f
--- /dev/null
+++ b/lib/api/entities/conan_package/conan_package_manifest.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module ConanPackage
+ class ConanPackageManifest < Grape::Entity
+ expose :package_urls, merge: true
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/conan_package/conan_package_snapshot.rb b/lib/api/entities/conan_package/conan_package_snapshot.rb
new file mode 100644
index 00000000000..d7fdda09b5a
--- /dev/null
+++ b/lib/api/entities/conan_package/conan_package_snapshot.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module ConanPackage
+ class ConanPackageSnapshot < Grape::Entity
+ expose :package_snapshot, merge: true
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/conan_package/conan_recipe_manifest.rb b/lib/api/entities/conan_package/conan_recipe_manifest.rb
new file mode 100644
index 00000000000..ecaa142cef9
--- /dev/null
+++ b/lib/api/entities/conan_package/conan_recipe_manifest.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module ConanPackage
+ class ConanRecipeManifest < Grape::Entity
+ expose :recipe_urls, merge: true
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/conan_package/conan_recipe_snapshot.rb b/lib/api/entities/conan_package/conan_recipe_snapshot.rb
new file mode 100644
index 00000000000..09a60d23727
--- /dev/null
+++ b/lib/api/entities/conan_package/conan_recipe_snapshot.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module ConanPackage
+ class ConanRecipeSnapshot < Grape::Entity
+ expose :recipe_snapshot, merge: true
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/conan_package/conan_upload_urls.rb b/lib/api/entities/conan_package/conan_upload_urls.rb
new file mode 100644
index 00000000000..c14963c87f5
--- /dev/null
+++ b/lib/api/entities/conan_package/conan_upload_urls.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module ConanPackage
+ class ConanUploadUrls < Grape::Entity
+ expose :upload_urls, merge: true
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/entity_helpers.rb b/lib/api/entities/entity_helpers.rb
new file mode 100644
index 00000000000..3a68044ad35
--- /dev/null
+++ b/lib/api/entities/entity_helpers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module EntityHelpers
+ def can_read(attr, &block)
+ ->(obj, opts) { Ability.allowed?(opts[:user], "read_#{attr}".to_sym, yield(obj)) }
+ end
+
+ def can_destroy(attr, &block)
+ ->(obj, opts) { Ability.allowed?(opts[:user], "destroy_#{attr}".to_sym, yield(obj)) }
+ end
+
+ def expose_restricted(attr, &block)
+ expose attr, if: can_read(attr, &block)
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/go_module_version.rb b/lib/api/entities/go_module_version.rb
new file mode 100644
index 00000000000..643e25df9e0
--- /dev/null
+++ b/lib/api/entities/go_module_version.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class GoModuleVersion < Grape::Entity
+ expose :name, as: 'Version'
+ expose :time, as: 'Time'
+ end
+ end
+end
diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb
index 8a6a5b7057c..e430eba4880 100644
--- a/lib/api/entities/group.rb
+++ b/lib/api/entities/group.rb
@@ -31,6 +31,7 @@ module API
expose :wiki_size
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
+ expose :snippets_size
end
end
end
diff --git a/lib/api/entities/group_detail.rb b/lib/api/entities/group_detail.rb
index 93dc41da81d..2d9d4ca7992 100644
--- a/lib/api/entities/group_detail.rb
+++ b/lib/api/entities/group_detail.rb
@@ -7,6 +7,7 @@ module API
SharedGroupWithGroup.represent(group.shared_with_group_links.public_or_visible_to_user(group, options[:current_user]))
end
expose :runners_token, if: lambda { |group, options| options[:user_can_admin_group] }
+
expose :projects, using: Entities::Project do |group, options|
projects = GroupProjectsFinder.new(
group: group,
diff --git a/lib/api/entities/issuable_entity.rb b/lib/api/entities/issuable_entity.rb
index 5bee59de539..e2c674c0b8b 100644
--- a/lib/api/entities/issuable_entity.rb
+++ b/lib/api/entities/issuable_entity.rb
@@ -8,10 +8,38 @@ module API
expose :title, :description
expose :state, :created_at, :updated_at
- # Avoids an N+1 query when metadata is included
- def issuable_metadata(subject, options, method, args = nil)
- cached_subject = options.dig(:issuable_metadata, subject.id)
- (cached_subject || subject).public_send(method, *args) # rubocop: disable GitlabSecurity/PublicSend
+ def presented
+ lazy_issuable_metadata
+
+ super
+ end
+
+ def issuable_metadata
+ options.dig(:issuable_metadata, object.id) || lazy_issuable_metadata
+ end
+
+ protected
+
+ # This method will preload the `issuable_metadata` for the current
+ # entity according to the current top-level entity options, such
+ # as the current_user.
+ def lazy_issuable_metadata
+ BatchLoader.for(object).batch(key: [current_user, :issuable_metadata]) do |models, loader, args|
+ current_user = args[:key].first
+
+ issuable_metadata = Gitlab::IssuableMetadata.new(current_user, models)
+ metadata_by_id = issuable_metadata.data
+
+ models.each do |issuable|
+ loader.call(issuable, metadata_by_id[issuable.id])
+ end
+ end
+ end
+
+ private
+
+ def current_user
+ options[:current_user]
end
end
end
diff --git a/lib/api/entities/issue_basic.rb b/lib/api/entities/issue_basic.rb
index af92f4124f1..cf96c6556ec 100644
--- a/lib/api/entities/issue_basic.rb
+++ b/lib/api/entities/issue_basic.rb
@@ -21,10 +21,10 @@ module API
issue.assignees.first
end
- expose(:user_notes_count) { |issue, options| issuable_metadata(issue, options, :user_notes_count) }
- expose(:merge_requests_count) { |issue, options| issuable_metadata(issue, options, :merge_requests_count, options[:current_user]) }
- expose(:upvotes) { |issue, options| issuable_metadata(issue, options, :upvotes) }
- expose(:downvotes) { |issue, options| issuable_metadata(issue, options, :downvotes) }
+ expose(:user_notes_count) { |issue, options| issuable_metadata.user_notes_count }
+ expose(:merge_requests_count) { |issue, options| issuable_metadata.merge_requests_count }
+ expose(:upvotes) { |issue, options| issuable_metadata.upvotes }
+ expose(:downvotes) { |issue, options| issuable_metadata.downvotes }
expose :due_date
expose :confidential
expose :discussion_locked
diff --git a/lib/api/entities/merge_request_approvals.rb b/lib/api/entities/merge_request_approvals.rb
new file mode 100644
index 00000000000..e3d58d687c4
--- /dev/null
+++ b/lib/api/entities/merge_request_approvals.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class MergeRequestApprovals < Grape::Entity
+ expose :user_has_approved do |merge_request, options|
+ merge_request.approved_by?(options[:current_user])
+ end
+
+ expose :user_can_approve do |merge_request, options|
+ !merge_request.approved_by?(options[:current_user]) &&
+ options[:current_user].can?(:approve_merge_request, merge_request)
+ end
+
+ expose :approved do |merge_request|
+ merge_request.approvals.present?
+ end
+
+ expose :approved_by, using: ::API::Entities::Approvals do |merge_request|
+ merge_request.approvals
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb
index 1643f267938..69523e3637b 100644
--- a/lib/api/entities/merge_request_basic.rb
+++ b/lib/api/entities/merge_request_basic.rb
@@ -22,13 +22,11 @@ module API
MarkupHelper.markdown_field(entity, :description)
end
expose :target_branch, :source_branch
- expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) }
- expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) }
- expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) }
- expose :assignee, using: ::API::Entities::UserBasic do |merge_request|
- merge_request.assignee
- end
- expose :author, :assignees, using: Entities::UserBasic
+ expose(:user_notes_count) { |merge_request, options| issuable_metadata.user_notes_count }
+ expose(:upvotes) { |merge_request, options| issuable_metadata.upvotes }
+ expose(:downvotes) { |merge_request, options| issuable_metadata.downvotes }
+
+ expose :author, :assignees, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :labels do |merge_request, options|
if options[:with_labels_details]
@@ -57,9 +55,12 @@ module API
expose :discussion_locked
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
- expose :allow_collaboration, if: -> (merge_request, _) { merge_request.for_fork? }
- # Deprecated
- expose :allow_collaboration, as: :allow_maintainer_to_push, if: -> (merge_request, _) { merge_request.for_fork? }
+
+ with_options if: -> (merge_request, _) { merge_request.for_fork? } do
+ expose :allow_collaboration
+ # Deprecated
+ expose :allow_collaboration, as: :allow_maintainer_to_push
+ end
# reference is deprecated in favour of references
# Introduced [Gitlab 12.6](https://gitlab.com/gitlab-org/gitlab/merge_requests/20354)
diff --git a/lib/api/entities/npm_package.rb b/lib/api/entities/npm_package.rb
new file mode 100644
index 00000000000..b094f3acdb6
--- /dev/null
+++ b/lib/api/entities/npm_package.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class NpmPackage < Grape::Entity
+ expose :name
+ expose :versions
+ expose :dist_tags, as: 'dist-tags'
+ end
+ end
+end
diff --git a/lib/api/entities/npm_package_tag.rb b/lib/api/entities/npm_package_tag.rb
new file mode 100644
index 00000000000..7f458fa037f
--- /dev/null
+++ b/lib/api/entities/npm_package_tag.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class NpmPackageTag < Grape::Entity
+ expose :dist_tags, merge: true
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/dependency.rb b/lib/api/entities/nuget/dependency.rb
new file mode 100644
index 00000000000..b61c37f5882
--- /dev/null
+++ b/lib/api/entities/nuget/dependency.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class Dependency < Grape::Entity
+ expose :id, as: :@id
+ expose :type, as: :@type
+ expose :name, as: :id
+ expose :range
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/dependency_group.rb b/lib/api/entities/nuget/dependency_group.rb
new file mode 100644
index 00000000000..dcab9359fcf
--- /dev/null
+++ b/lib/api/entities/nuget/dependency_group.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class DependencyGroup < Grape::Entity
+ expose :id, as: :@id
+ expose :type, as: :@type
+ expose :target_framework, as: :targetFramework, expose_nil: false
+ expose :dependencies, using: ::API::Entities::Nuget::Dependency
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/metadatum.rb b/lib/api/entities/nuget/metadatum.rb
new file mode 100644
index 00000000000..87caef41a85
--- /dev/null
+++ b/lib/api/entities/nuget/metadatum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class Metadatum < Grape::Entity
+ expose :project_url, as: :projectUrl, expose_nil: false
+ expose :license_url, as: :licenseUrl, expose_nil: false
+ expose :icon_url, as: :iconUrl, expose_nil: false
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/package_metadata.rb b/lib/api/entities/nuget/package_metadata.rb
new file mode 100644
index 00000000000..e1c2a1ae161
--- /dev/null
+++ b/lib/api/entities/nuget/package_metadata.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class PackageMetadata < Grape::Entity
+ expose :json_url, as: :@id
+ expose :archive_url, as: :packageContent
+ expose :catalog_entry, as: :catalogEntry, using: ::API::Entities::Nuget::PackageMetadataCatalogEntry
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/package_metadata_catalog_entry.rb b/lib/api/entities/nuget/package_metadata_catalog_entry.rb
new file mode 100644
index 00000000000..5533f857596
--- /dev/null
+++ b/lib/api/entities/nuget/package_metadata_catalog_entry.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class PackageMetadataCatalogEntry < Grape::Entity
+ expose :json_url, as: :@id
+ expose :authors
+ expose :dependency_groups, as: :dependencyGroups, using: ::API::Entities::Nuget::DependencyGroup
+ expose :package_name, as: :id
+ expose :package_version, as: :version
+ expose :tags
+ expose :archive_url, as: :packageContent
+ expose :summary
+ expose :metadatum, using: ::API::Entities::Nuget::Metadatum, merge: true
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/packages_metadata.rb b/lib/api/entities/nuget/packages_metadata.rb
new file mode 100644
index 00000000000..1cdf2491725
--- /dev/null
+++ b/lib/api/entities/nuget/packages_metadata.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class PackagesMetadata < Grape::Entity
+ expose :count
+ expose :items, using: ::API::Entities::Nuget::PackagesMetadataItem
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/packages_metadata_item.rb b/lib/api/entities/nuget/packages_metadata_item.rb
new file mode 100644
index 00000000000..84cc79166f3
--- /dev/null
+++ b/lib/api/entities/nuget/packages_metadata_item.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class PackagesMetadataItem < Grape::Entity
+ expose :json_url, as: :@id
+ expose :lower_version, as: :lower
+ expose :upper_version, as: :upper
+ expose :packages_count, as: :count
+ expose :packages, as: :items, using: ::API::Entities::Nuget::PackageMetadata
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/packages_versions.rb b/lib/api/entities/nuget/packages_versions.rb
new file mode 100644
index 00000000000..498c6970d5c
--- /dev/null
+++ b/lib/api/entities/nuget/packages_versions.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class PackagesVersions < Grape::Entity
+ expose :versions
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/search_result.rb b/lib/api/entities/nuget/search_result.rb
new file mode 100644
index 00000000000..8e028cbad95
--- /dev/null
+++ b/lib/api/entities/nuget/search_result.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class SearchResult < Grape::Entity
+ expose :type, as: :@type
+ expose :authors
+ expose :name, as: :id
+ expose :name, as: :title
+ expose :summary
+ expose :total_downloads, as: :totalDownloads
+ expose :verified
+ expose :version
+ expose :versions, using: ::API::Entities::Nuget::SearchResultVersion
+ expose :tags
+ expose :metadatum, using: ::API::Entities::Nuget::Metadatum, merge: true
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/search_result_version.rb b/lib/api/entities/nuget/search_result_version.rb
new file mode 100644
index 00000000000..9032c964c44
--- /dev/null
+++ b/lib/api/entities/nuget/search_result_version.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class SearchResultVersion < Grape::Entity
+ expose :json_url, as: :@id
+ expose :version
+ expose :downloads
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/search_results.rb b/lib/api/entities/nuget/search_results.rb
new file mode 100644
index 00000000000..22a77dc7b6c
--- /dev/null
+++ b/lib/api/entities/nuget/search_results.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class SearchResults < Grape::Entity
+ expose :total_count, as: :totalHits
+ expose :data, using: ::API::Entities::Nuget::SearchResult
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/nuget/service_index.rb b/lib/api/entities/nuget/service_index.rb
new file mode 100644
index 00000000000..e57bd04adb9
--- /dev/null
+++ b/lib/api/entities/nuget/service_index.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ module Nuget
+ class ServiceIndex < Grape::Entity
+ expose :version
+ expose :resources
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb
new file mode 100644
index 00000000000..73473f16da9
--- /dev/null
+++ b/lib/api/entities/package.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Package < Grape::Entity
+ include ::API::Helpers::RelatedResourcesHelpers
+ extend ::API::Entities::EntityHelpers
+
+ expose :id
+ expose :name
+ expose :version
+ expose :package_type
+
+ expose :_links do
+ expose :web_path do |package|
+ if ::Gitlab.ee?
+ ::Gitlab::Routing.url_helpers.project_package_path(package.project, package)
+ end
+ end
+
+ expose :delete_api_path, if: can_destroy(:package, &:project) do |package|
+ expose_url api_v4_projects_packages_path(package_id: package.id, id: package.project_id)
+ end
+ end
+
+ expose :created_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
+
+ expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline
+
+ expose :versions, using: ::API::Entities::PackageVersion
+
+ private
+
+ def project_path
+ object.project.full_path
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/package/pipeline.rb b/lib/api/entities/package/pipeline.rb
new file mode 100644
index 00000000000..e91a12e47fa
--- /dev/null
+++ b/lib/api/entities/package/pipeline.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class Package < Grape::Entity
+ class Pipeline < ::API::Entities::PipelineBasic
+ expose :user, using: ::API::Entities::UserBasic
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/package_file.rb b/lib/api/entities/package_file.rb
new file mode 100644
index 00000000000..8be4e5a4316
--- /dev/null
+++ b/lib/api/entities/package_file.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PackageFile < Grape::Entity
+ expose :id, :package_id, :created_at
+ expose :file_name, :size
+ expose :file_md5, :file_sha1
+ end
+ end
+end
diff --git a/lib/api/entities/package_version.rb b/lib/api/entities/package_version.rb
new file mode 100644
index 00000000000..5f3e86c3229
--- /dev/null
+++ b/lib/api/entities/package_version.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class PackageVersion < Grape::Entity
+ expose :id
+ expose :version
+ expose :created_at
+ expose :tags
+
+ expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline
+ end
+ end
+end
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index 55a57501858..e3c5177cd0b 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -51,6 +51,8 @@ module API
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
+ expose :service_desk_enabled
+ expose :service_desk_address
expose(:can_create_merge_request_in) do |project, options|
Ability.allowed?(options[:current_user], :create_merge_request_in, project)
diff --git a/lib/api/entities/project_statistics.rb b/lib/api/entities/project_statistics.rb
index e5f6165da31..32201e88eaf 100644
--- a/lib/api/entities/project_statistics.rb
+++ b/lib/api/entities/project_statistics.rb
@@ -9,6 +9,7 @@ module API
expose :wiki_size
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
+ expose :snippets_size
end
end
end
diff --git a/lib/api/entities/release.rb b/lib/api/entities/release.rb
index 99fa496d368..afe14cf33cf 100644
--- a/lib/api/entities/release.rb
+++ b/lib/api/entities/release.rb
@@ -5,9 +5,7 @@ module API
class Release < Grape::Entity
include ::API::Helpers::Presentable
- expose :name do |release, _|
- can_download_code? ? release.name : "Release-#{release.id}"
- end
+ expose :name
expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? }
expose :description
expose :description_html do |entity|
@@ -23,10 +21,7 @@ module API
expose :tag_path, expose_nil: false
expose :assets do
- expose :assets_count, as: :count do |release, _|
- assets_to_exclude = can_download_code? ? [] : [:sources]
- release.assets_count(except: assets_to_exclude)
- end
+ expose :assets_count, as: :count
expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? }
expose :links, using: Entities::Releases::Link do |release, options|
release.links.sorted
diff --git a/lib/api/entities/resource_state_event.rb b/lib/api/entities/resource_state_event.rb
new file mode 100644
index 00000000000..f71a38e4115
--- /dev/null
+++ b/lib/api/entities/resource_state_event.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class ResourceStateEvent < Grape::Entity
+ expose :id
+ expose :user, using: Entities::UserBasic
+ expose :created_at
+ expose :resource_type do |event, _options|
+ event.issuable.class.name
+ end
+ expose :resource_id do |event, _options|
+ event.issuable.id
+ end
+ expose :state
+ end
+ end
+end
diff --git a/lib/api/entities/snippet.rb b/lib/api/entities/snippet.rb
index 19c89603cbc..40488eb882d 100644
--- a/lib/api/entities/snippet.rb
+++ b/lib/api/entities/snippet.rb
@@ -17,6 +17,18 @@ module API
expose :file_name do |snippet|
snippet.file_name_on_repo || snippet.file_name
end
+ expose :files, if: ->(snippet, options) { snippet_multiple_files?(snippet, options[:current_user]) } do |snippet, options|
+ snippet.list_files.map do |file|
+ {
+ path: file,
+ raw_url: Gitlab::UrlBuilder.build(snippet, file: file, ref: snippet.repository.root_ref)
+ }
+ end
+ end
+
+ def snippet_multiple_files?(snippet, current_user)
+ ::Feature.enabled?(:snippet_multiple_files, current_user) && snippet.repository_exists?
+ end
end
end
end
diff --git a/lib/api/entities/user.rb b/lib/api/entities/user.rb
index adf954ab02d..4aa5c9b7236 100644
--- a/lib/api/entities/user.rb
+++ b/lib/api/entities/user.rb
@@ -5,7 +5,7 @@ module API
class User < UserBasic
include UsersHelper
expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
- expose :bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title
+ expose :bio, :bio_html, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title
expose :work_information do |user|
work_information(user)
end
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 28019ce7796..b825904e2c5 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -2,7 +2,7 @@
module API
# Environments RESTfull API endpoints
- class Environments < Grape::API
+ class Environments < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/error_tracking.rb b/lib/api/error_tracking.rb
index 14888037f53..64ec6f0a57a 100644
--- a/lib/api/error_tracking.rb
+++ b/lib/api/error_tracking.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ErrorTracking < Grape::API
+ class ErrorTracking < Grape::API::Instance
before { authenticate! }
params do
diff --git a/lib/api/events.rb b/lib/api/events.rb
index e4c017fab42..0b79431a76d 100644
--- a/lib/api/events.rb
+++ b/lib/api/events.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Events < Grape::API
+ class Events < Grape::API::Instance
include PaginationParams
include APIGuard
helpers ::API::Helpers::EventsHelpers
diff --git a/lib/api/features.rb b/lib/api/features.rb
index 3fb3fc92e42..9d011d658f6 100644
--- a/lib/api/features.rb
+++ b/lib/api/features.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Features < Grape::API
+ class Features < Grape::API::Instance
before { authenticated_as_admin! }
helpers do
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 76ab9a2190b..748bdfa894d 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Files < Grape::API
+ class Files < Grape::API::Instance
include APIGuard
FILE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX)
@@ -56,7 +56,7 @@ module API
ref: params[:ref],
blob_id: @blob.id,
commit_id: @commit.id,
- last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path])
+ last_commit_id: @repo.last_commit_id_for_path(@commit.sha, params[:file_path], literal_pathspec: true)
}
end
diff --git a/lib/api/freeze_periods.rb b/lib/api/freeze_periods.rb
index 9c7e5a5832d..b8254ee9ab4 100644
--- a/lib/api/freeze_periods.rb
+++ b/lib/api/freeze_periods.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class FreezePeriods < Grape::API
+ class FreezePeriods < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/go_proxy.rb b/lib/api/go_proxy.rb
new file mode 100755
index 00000000000..c0207f9169c
--- /dev/null
+++ b/lib/api/go_proxy.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+module API
+ class GoProxy < Grape::API::Instance
+ helpers Gitlab::Golang
+ helpers ::API::Helpers::PackagesHelpers
+
+ # basic semver, except case encoded (A => !a)
+ MODULE_VERSION_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.!a-z0-9]+))?(?:\+([-.!a-z0-9]+))?/.freeze
+
+ MODULE_VERSION_REQUIREMENTS = { module_version: MODULE_VERSION_REGEX }.freeze
+
+ before { require_packages_enabled! }
+
+ helpers do
+ def case_decode(str)
+ # Converts "github.com/!azure" to "github.com/Azure"
+ #
+ # From `go help goproxy`:
+ #
+ # > To avoid problems when serving from case-sensitive file systems,
+ # > the <module> and <version> elements are case-encoded, replacing
+ # > every uppercase letter with an exclamation mark followed by the
+ # > corresponding lower-case letter: github.com/Azure encodes as
+ # > github.com/!azure.
+
+ str.gsub(/![[:alpha:]]/) { |s| s[1..].upcase }
+ end
+
+ def find_project!(id)
+ # based on API::Helpers::Packages::BasicAuthHelpers#authorized_project_find!
+
+ project = find_project(id)
+
+ return project if project && can?(current_user, :read_project, project)
+
+ if current_user
+ not_found!('Project')
+ else
+ unauthorized!
+ end
+ end
+
+ def find_module
+ not_found! unless Feature.enabled?(:go_proxy, user_project)
+
+ module_name = case_decode params[:module_name]
+ bad_request!('Module Name') if module_name.blank?
+
+ mod = ::Packages::Go::ModuleFinder.new(user_project, module_name).execute
+
+ not_found! unless mod
+
+ mod
+ end
+
+ def find_version
+ module_version = case_decode params[:module_version]
+ ver = ::Packages::Go::VersionFinder.new(find_module).find(module_version)
+
+ not_found! unless ver&.valid?
+
+ ver
+
+ rescue ArgumentError
+ not_found!
+ end
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :module_name, type: String, desc: 'Module name', coerce_with: ->(val) { CGI.unescape(val) }
+ end
+ route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before do
+ authorize_read_package!
+ end
+
+ namespace ':id/packages/go/*module_name/@v' do
+ desc 'Get all tagged versions for a given Go module' do
+ detail 'See `go help goproxy`, GET $GOPROXY/<module>/@v/list. This feature was introduced in GitLab 13.1.'
+ end
+ get 'list' do
+ mod = find_module
+
+ content_type 'text/plain'
+ mod.versions.map { |t| t.name }.join("\n")
+ end
+
+ desc 'Get information about the given module version' do
+ detail 'See `go help goproxy`, GET $GOPROXY/<module>/@v/<version>.info. This feature was introduced in GitLab 13.1.'
+ success ::API::Entities::GoModuleVersion
+ end
+ params do
+ requires :module_version, type: String, desc: 'Module version'
+ end
+ get ':module_version.info', requirements: MODULE_VERSION_REQUIREMENTS do
+ ver = find_version
+
+ present ::Packages::Go::ModuleVersionPresenter.new(ver), with: ::API::Entities::GoModuleVersion
+ end
+
+ desc 'Get the module file of the given module version' do
+ detail 'See `go help goproxy`, GET $GOPROXY/<module>/@v/<version>.mod. This feature was introduced in GitLab 13.1.'
+ end
+ params do
+ requires :module_version, type: String, desc: 'Module version'
+ end
+ get ':module_version.mod', requirements: MODULE_VERSION_REQUIREMENTS do
+ ver = find_version
+
+ content_type 'text/plain'
+ ver.gomod
+ end
+
+ desc 'Get a zip of the source of the given module version' do
+ detail 'See `go help goproxy`, GET $GOPROXY/<module>/@v/<version>.zip. This feature was introduced in GitLab 13.1.'
+ end
+ params do
+ requires :module_version, type: String, desc: 'Module version'
+ end
+ get ':module_version.zip', requirements: MODULE_VERSION_REQUIREMENTS do
+ ver = find_version
+
+ content_type 'application/zip'
+ env['api.format'] = :binary
+ header['Content-Disposition'] = ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: ver.name + '.zip')
+ header['Content-Transfer-Encoding'] = 'binary'
+ status :ok
+ body ver.archive.string
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb
index 88d04e70e11..7efc12121d2 100644
--- a/lib/api/group_boards.rb
+++ b/lib/api/group_boards.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class GroupBoards < Grape::API
+ class GroupBoards < Grape::API::Instance
include BoardsResponses
include PaginationParams
diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb
index 2c12c6387fb..ae41d9f13b8 100644
--- a/lib/api/group_clusters.rb
+++ b/lib/api/group_clusters.rb
@@ -1,23 +1,11 @@
# frozen_string_literal: true
module API
- class GroupClusters < Grape::API
+ class GroupClusters < Grape::API::Instance
include PaginationParams
before { authenticate! }
- # EE::API::GroupClusters will
- # override these methods
- helpers do
- params :create_params_ee do
- end
-
- params :update_params_ee do
- end
- end
-
- prepend_if_ee('EE::API::GroupClusters') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
params do
requires :id, type: String, desc: 'The ID of the group'
end
@@ -52,6 +40,7 @@ module API
params do
requires :name, type: String, desc: 'Cluster name'
optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
+ optional :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster'
optional :domain, type: String, desc: 'Cluster base domain'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
@@ -62,7 +51,6 @@ module API
optional :namespace, type: String, desc: 'Unique namespace related to Group'
optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end
- use :create_params_ee
end
post ':id/clusters/user' do
authorize! :add_cluster, user_group
@@ -85,6 +73,7 @@ module API
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :environment_scope, type: String, desc: 'The associated environment to the cluster'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
@@ -92,7 +81,6 @@ module API
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Group'
end
- use :update_params_ee
end
put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster
diff --git a/lib/api/group_container_repositories.rb b/lib/api/group_container_repositories.rb
index d34317b5271..25b3059f63b 100644
--- a/lib/api/group_container_repositories.rb
+++ b/lib/api/group_container_repositories.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class GroupContainerRepositories < Grape::API
+ class GroupContainerRepositories < Grape::API::Instance
include PaginationParams
before { authorize_read_group_container_images! }
diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb
index d3010b6d147..dc14813eefc 100644
--- a/lib/api/group_export.rb
+++ b/lib/api/group_export.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class GroupExport < Grape::API
+ class GroupExport < Grape::API::Instance
helpers Helpers::RateLimiter
before do
diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb
index afcbc24d3ce..b82d9fc519a 100644
--- a/lib/api/group_import.rb
+++ b/lib/api/group_import.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class GroupImport < Grape::API
+ class GroupImport < Grape::API::Instance
helpers Helpers::FileUploadHelpers
helpers do
diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb
index 7585293031f..56f2b769464 100644
--- a/lib/api/group_labels.rb
+++ b/lib/api/group_labels.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class GroupLabels < Grape::API
+ class GroupLabels < Grape::API::Instance
include PaginationParams
helpers ::API::Helpers::LabelHelpers
diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb
index 9e9f5101285..82f5df79356 100644
--- a/lib/api/group_milestones.rb
+++ b/lib/api/group_milestones.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
module API
- class GroupMilestones < Grape::API
+ class GroupMilestones < Grape::API::Instance
include MilestoneResponses
include PaginationParams
- before do
- authenticate!
- end
+ before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a group'
diff --git a/lib/api/group_packages.rb b/lib/api/group_packages.rb
new file mode 100644
index 00000000000..aa047e260f5
--- /dev/null
+++ b/lib/api/group_packages.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module API
+ class GroupPackages < Grape::API::Instance
+ include PaginationParams
+
+ before do
+ authorize_packages_access!(user_group)
+ end
+
+ helpers ::API::Helpers::PackagesHelpers
+
+ params do
+ requires :id, type: String, desc: "Group's ID or path"
+ optional :exclude_subgroups, type: Boolean, default: false, desc: 'Determines if subgroups should be excluded'
+ end
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get all project packages within a group' do
+ detail 'This feature was introduced in GitLab 12.5'
+ success ::API::Entities::Package
+ end
+ params do
+ use :pagination
+ optional :order_by, type: String, values: %w[created_at name version type project_path], default: 'created_at',
+ desc: 'Return packages ordered by `created_at`, `name`, `version` or `type` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'asc',
+ desc: 'Return packages sorted in `asc` or `desc` order.'
+ optional :package_type, type: String, values: Packages::Package.package_types.keys,
+ desc: 'Return packages of a certain type'
+ optional :package_name, type: String,
+ desc: 'Return packages with this name'
+ end
+ get ':id/packages' do
+ packages = Packages::GroupPackagesFinder.new(
+ current_user,
+ user_group,
+ declared(params).slice(:exclude_subgroups, :order_by, :sort, :package_type, :package_name)
+ ).execute
+
+ present paginate(packages), with: ::API::Entities::Package, user: current_user, group: true
+ end
+ end
+ end
+end
diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb
index 916f89649a5..d3ca1c79e73 100644
--- a/lib/api/group_variables.rb
+++ b/lib/api/group_variables.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class GroupVariables < Grape::API
+ class GroupVariables < Grape::API::Instance
include PaginationParams
before { authenticate! }
@@ -48,7 +48,7 @@ module API
requires :value, type: String, desc: 'The value of the variable'
optional :protected, type: String, desc: 'Whether the variable is protected'
optional :masked, type: String, desc: 'Whether the variable is masked'
- optional :variable_type, type: String, values: Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
+ optional :variable_type, type: String, values: ::Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
end
post ':id/variables' do
variable_params = declared_params(include_missing: false)
@@ -70,7 +70,7 @@ module API
optional :value, type: String, desc: 'The value of the variable'
optional :protected, type: String, desc: 'Whether the variable is protected'
optional :masked, type: String, desc: 'Whether the variable is masked'
- optional :variable_type, type: String, values: Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
+ optional :variable_type, type: String, values: ::Ci::GroupVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
end
# rubocop: disable CodeReuse/ActiveRecord
put ':id/variables/:key' do
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 6e07bb46721..9ac3ac818fc 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Groups < Grape::API
+ class Groups < Grape::API::Instance
include PaginationParams
include Helpers::CustomAttributes
@@ -16,7 +16,7 @@ module API
params :group_list_params do
use :statistics_params
- optional :skip_groups, type: Array[Integer], desc: 'Array of group ids to exclude from list'
+ optional :skip_groups, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :search, type: String, desc: 'Search for a specific group'
optional :owned, type: Boolean, default: false, desc: 'Limit by owned by authenticated user'
@@ -76,9 +76,6 @@ module API
params: project_finder_params,
options: finder_options
).execute
- projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
- projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
- projects = projects.visible_to_user_and_access_level(current_user, params[:min_access_level]) if params[:min_access_level]
projects = reorder_projects(projects)
paginate(projects)
end
@@ -221,7 +218,7 @@ module API
success Entities::Project
end
params do
- optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ optional :archived, type: Boolean, desc: 'Limit by archived status'
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
@@ -258,7 +255,7 @@ module API
success Entities::Project
end
params do
- optional :archived, type: Boolean, default: false, desc: 'Limit by archived status'
+ optional :archived, type: Boolean, desc: 'Limit by archived status'
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria'
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index bbdb45da3b1..01b89959c14 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -41,6 +41,16 @@ module API
end
end
+ def job_token_authentication?
+ initial_current_user && @current_authenticated_job.present? # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+
+ # Returns the job associated with the token provided for
+ # authentication, if any
+ def current_authenticated_job
+ @current_authenticated_job
+ end
+
# rubocop:disable Gitlab/ModuleWithInstanceVariables
# We can't rewrite this with StrongMemoize because `sudo!` would
# actually write to `@current_user`, and `sudo?` would immediately
@@ -79,12 +89,6 @@ module API
@project ||= find_project!(params[:id])
end
- def wiki_page
- page = ProjectWiki.new(user_project, current_user).find_page(params[:slug])
-
- page || not_found!('Wiki Page')
- end
-
def available_labels_for(label_parent, include_ancestor_groups: true)
search_params = { include_ancestor_groups: include_ancestor_groups }
@@ -374,6 +378,12 @@ module API
render_api_error!(message.join(' '), 404)
end
+ def check_sha_param!(params, merge_request)
+ if params[:sha] && merge_request.diff_head_sha != params[:sha]
+ render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
+ end
+ end
+
def unauthorized!
render_api_error!('401 Unauthorized', 401)
end
@@ -416,10 +426,14 @@ module API
def render_validation_error!(model)
if model.errors.any?
- render_api_error!(model.errors.messages || '400 Bad Request', 400)
+ render_api_error!(model_error_messages(model) || '400 Bad Request', 400)
end
end
+ def model_error_messages(model)
+ model.errors.messages
+ end
+
def render_spam_error!
render_api_error!({ error: 'Spam detected' }, 400)
end
@@ -490,7 +504,7 @@ module API
header['X-Sendfile'] = path
body
else
- file path
+ sendfile path
end
end
@@ -534,6 +548,8 @@ module API
def project_finder_params_ce
finder_params = project_finder_params_visibility_ce
+ finder_params[:with_issues_enabled] = true if params[:with_issues_enabled].present?
+ finder_params[:with_merge_requests_enabled] = true if params[:with_merge_requests_enabled].present?
finder_params[:without_deleted] = true
finder_params[:search] = params[:search] if params[:search]
finder_params[:search_namespaces] = true if params[:search_namespaces].present?
@@ -543,6 +559,7 @@ module API
finder_params[:id_before] = params[:id_before] if params[:id_before]
finder_params[:last_activity_after] = params[:last_activity_after] if params[:last_activity_after]
finder_params[:last_activity_before] = params[:last_activity_before] if params[:last_activity_before]
+ finder_params[:repository_storage] = params[:repository_storage] if params[:repository_storage]
finder_params
end
diff --git a/lib/api/helpers/common_helpers.rb b/lib/api/helpers/common_helpers.rb
index 32a15381f27..a44fd4b0a5b 100644
--- a/lib/api/helpers/common_helpers.rb
+++ b/lib/api/helpers/common_helpers.rb
@@ -12,6 +12,26 @@ module API
end
end
end
+
+ # Grape v1.3.3 no longer automatically coerces an Array
+ # type to an empty array if the value is nil.
+ def coerce_nil_params_to_array!
+ keys_to_coerce = params_with_array_types
+
+ params.each do |key, val|
+ params[key] = Array(val) if val.nil? && keys_to_coerce.include?(key)
+ end
+ end
+
+ def params_with_array_types
+ options[:route_options][:params].map do |key, val|
+ param_type = val[:type]
+ # Search for parameters with Array types (e.g. "[String]", "[Integer]", etc.)
+ if param_type =~ %r(\[\w*\])
+ key
+ end
+ end.compact.to_set
+ end
end
end
end
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index b05e82a541d..b69930b447c 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -118,8 +118,8 @@ module API
{
repository: repository.gitaly_repository,
- address: Gitlab::GitalyClient.address(container.repository_storage),
- token: Gitlab::GitalyClient.token(container.repository_storage),
+ address: Gitlab::GitalyClient.address(repository.shard),
+ token: Gitlab::GitalyClient.token(repository.shard),
features: Feature::Gitaly.server_feature_flags
}
end
diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb
index 9dab2a88f0b..4d5350498a7 100644
--- a/lib/api/helpers/merge_requests_helpers.rb
+++ b/lib/api/helpers/merge_requests_helpers.rb
@@ -5,7 +5,30 @@ module API
module MergeRequestsHelpers
extend Grape::API::Helpers
+ params :merge_requests_negatable_params do
+ optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
+ optional :author_username, type: String, desc: 'Return merge requests which are authored by the user with the given username'
+ mutually_exclusive :author_id, :author_username
+
+ optional :assignee_id,
+ types: [Integer, String],
+ integer_none_any: true,
+ desc: 'Return merge requests which are assigned to the user with the given ID'
+ optional :assignee_username, type: Array[String], check_assignees_count: true,
+ coerce_with: Validations::Validators::CheckAssigneesCount.coerce,
+ desc: 'Return merge requests which are assigned to the user with the given username'
+ mutually_exclusive :assignee_id, :assignee_username
+
+ optional :labels,
+ type: Array[String],
+ coerce_with: Validations::Types::CommaSeparatedToArray.coerce,
+ desc: 'Comma-separated list of label names'
+ optional :milestone, type: String, desc: 'Return merge requests for a specific milestone'
+ optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
+ end
+
params :merge_requests_base_params do
+ use :merge_requests_negatable_params
optional :state,
type: String,
values: %w[opened closed locked merged all],
@@ -21,11 +44,6 @@ module API
values: %w[asc desc],
default: 'desc',
desc: 'Return merge requests sorted in `asc` or `desc` order.'
- optional :milestone, type: String, desc: 'Return merge requests for a specific milestone'
- optional :labels,
- type: Array[String],
- coerce_with: Validations::Types::LabelsList.coerce,
- desc: 'Comma-separated list of label names'
optional :with_labels_details, type: Boolean, desc: 'Return titles of labels and other details', default: false
optional :with_merge_status_recheck, type: Boolean, desc: 'Request that stale merge statuses be rechecked asynchronously', default: false
optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time'
@@ -37,19 +55,10 @@ module API
values: %w[simple],
desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request'
- optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
- optional :author_username, type: String, desc: 'Return merge requests which are authored by the user with the given username'
- mutually_exclusive :author_id, :author_username
-
- optional :assignee_id,
- types: [Integer, String],
- integer_none_any: true,
- desc: 'Return merge requests which are assigned to the user with the given ID'
optional :scope,
type: String,
values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all],
desc: 'Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`'
- optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
optional :source_branch, type: String, desc: 'Return merge requests with the given source branch'
optional :source_project_id, type: Integer, desc: 'Return merge requests with the given source project id'
optional :target_branch, type: String, desc: 'Return merge requests with the given target branch'
@@ -58,6 +67,9 @@ module API
desc: 'Search merge requests for text present in the title, description, or any combination of these'
optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma'
optional :wip, type: String, values: %w[yes no], desc: 'Search merge requests for WIP in the title'
+ optional :not, type: Hash, desc: 'Parameters to negate' do
+ use :merge_requests_negatable_params
+ end
end
params :optional_scope_param do
diff --git a/lib/api/helpers/packages/basic_auth_helpers.rb b/lib/api/helpers/packages/basic_auth_helpers.rb
new file mode 100644
index 00000000000..835b5f4614c
--- /dev/null
+++ b/lib/api/helpers/packages/basic_auth_helpers.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module Packages
+ module BasicAuthHelpers
+ module Constants
+ AUTHENTICATE_REALM_HEADER = 'Www-Authenticate: Basic realm'
+ AUTHENTICATE_REALM_NAME = 'GitLab Packages Registry'
+ end
+
+ include Constants
+
+ def find_personal_access_token
+ find_personal_access_token_from_http_basic_auth
+ end
+
+ def unauthorized_user_project
+ @unauthorized_user_project ||= find_project(params[:id])
+ end
+
+ def unauthorized_user_project!
+ unauthorized_user_project || not_found!
+ end
+
+ def authorized_user_project
+ @authorized_user_project ||= authorized_project_find!
+ end
+
+ def authorized_project_find!
+ project = unauthorized_user_project
+
+ unless project && can?(current_user, :read_project, project)
+ return unauthorized_or! { not_found! }
+ end
+
+ project
+ end
+
+ def authorize!(action, subject = :global, reason = nil)
+ return if can?(current_user, action, subject)
+
+ unauthorized_or! { forbidden!(reason) }
+ end
+
+ def unauthorized_or!
+ current_user ? yield : unauthorized_with_header!
+ end
+
+ def unauthorized_with_header!
+ header(AUTHENTICATE_REALM_HEADER, AUTHENTICATE_REALM_NAME)
+ unauthorized!
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb
new file mode 100644
index 00000000000..30e690a5a1d
--- /dev/null
+++ b/lib/api/helpers/packages/conan/api_helpers.rb
@@ -0,0 +1,225 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module Packages
+ module Conan
+ module ApiHelpers
+ def present_download_urls(entity)
+ authorize!(:read_package, project)
+
+ presenter = ::Packages::Conan::PackagePresenter.new(
+ recipe,
+ current_user,
+ project,
+ conan_package_reference: params[:conan_package_reference]
+ )
+
+ render_api_error!("No recipe manifest found", 404) if yield(presenter).empty?
+
+ present presenter, with: entity
+ end
+
+ def present_package_download_urls
+ present_download_urls(::API::Entities::ConanPackage::ConanPackageManifest, &:package_urls)
+ end
+
+ def present_recipe_download_urls
+ present_download_urls(::API::Entities::ConanPackage::ConanRecipeManifest, &:recipe_urls)
+ end
+
+ def recipe_upload_urls(file_names)
+ { upload_urls: Hash[
+ file_names.collect do |file_name|
+ [file_name, recipe_file_upload_url(file_name)]
+ end
+ ] }
+ end
+
+ def package_upload_urls(file_names)
+ { upload_urls: Hash[
+ file_names.collect do |file_name|
+ [file_name, package_file_upload_url(file_name)]
+ end
+ ] }
+ end
+
+ def package_file_upload_url(file_name)
+ expose_url(
+ api_v4_packages_conan_v1_files_package_path(
+ package_name: params[:package_name],
+ package_version: params[:package_version],
+ package_username: params[:package_username],
+ package_channel: params[:package_channel],
+ recipe_revision: '0',
+ conan_package_reference: params[:conan_package_reference],
+ package_revision: '0',
+ file_name: file_name
+ )
+ )
+ end
+
+ def recipe_file_upload_url(file_name)
+ expose_url(
+ api_v4_packages_conan_v1_files_export_path(
+ package_name: params[:package_name],
+ package_version: params[:package_version],
+ package_username: params[:package_username],
+ package_channel: params[:package_channel],
+ recipe_revision: '0',
+ file_name: file_name
+ )
+ )
+ end
+
+ def recipe
+ "%{package_name}/%{package_version}@%{package_username}/%{package_channel}" % params.symbolize_keys
+ end
+
+ def project
+ strong_memoize(:project) do
+ full_path = ::Packages::Conan::Metadatum.full_path_from(package_username: params[:package_username])
+ Project.find_by_full_path(full_path)
+ end
+ end
+
+ def package
+ strong_memoize(:package) do
+ project.packages
+ .with_name(params[:package_name])
+ .with_version(params[:package_version])
+ .with_conan_channel(params[:package_channel])
+ .order_created
+ .last
+ end
+ end
+
+ def token
+ strong_memoize(:token) do
+ token = nil
+ token = ::Gitlab::ConanToken.from_personal_access_token(access_token) if access_token
+ token = ::Gitlab::ConanToken.from_deploy_token(deploy_token_from_request) if deploy_token_from_request
+ token = ::Gitlab::ConanToken.from_job(find_job_from_token) if find_job_from_token
+ token
+ end
+ end
+
+ def download_package_file(file_type)
+ authorize!(:read_package, project)
+
+ package_file = ::Packages::Conan::PackageFileFinder
+ .new(
+ package,
+ params[:file_name].to_s,
+ conan_file_type: file_type,
+ conan_package_reference: params[:conan_package_reference]
+ ).execute!
+
+ track_event('pull_package') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY
+
+ present_carrierwave_file!(package_file.file)
+ end
+
+ def find_or_create_package
+ package || ::Packages::Conan::CreatePackageService.new(project, current_user, params).execute
+ end
+
+ def track_push_package_event
+ if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY && params['file.size'] > 0
+ track_event('push_package')
+ end
+ end
+
+ def create_package_file_with_type(file_type, current_package)
+ unless params['file.size'] == 0
+ # conan sends two upload requests, the first has no file, so we skip record creation if file.size == 0
+ ::Packages::Conan::CreatePackageFileService.new(current_package, uploaded_package_file, params.merge(conan_file_type: file_type)).execute
+ end
+ end
+
+ def upload_package_file(file_type)
+ authorize_upload!(project)
+
+ current_package = find_or_create_package
+
+ track_push_package_event
+
+ create_package_file_with_type(file_type, current_package)
+ rescue ObjectStorage::RemoteStoreError => e
+ Gitlab::ErrorTracking.track_exception(e, file_name: params[:file_name], project_id: project.id)
+
+ forbidden!
+ end
+
+ def find_personal_access_token
+ personal_access_token = find_personal_access_token_from_conan_jwt ||
+ find_personal_access_token_from_http_basic_auth
+
+ personal_access_token
+ end
+
+ def find_user_from_job_token
+ return unless route_authentication_setting[:job_token_allowed]
+
+ job = find_job_from_token || raise(::Gitlab::Auth::UnauthorizedError)
+
+ job.user
+ end
+
+ def deploy_token_from_request
+ find_deploy_token_from_conan_jwt || find_deploy_token_from_http_basic_auth
+ end
+
+ def find_job_from_token
+ find_job_from_conan_jwt || find_job_from_http_basic_auth
+ end
+
+ # We need to override this one because it
+ # looks into Bearer authorization header
+ def find_oauth_access_token
+ end
+
+ def find_personal_access_token_from_conan_jwt
+ token = decode_oauth_token_from_jwt
+
+ return unless token
+
+ PersonalAccessToken.find_by_id_and_user_id(token.access_token_id, token.user_id)
+ end
+
+ def find_deploy_token_from_conan_jwt
+ token = decode_oauth_token_from_jwt
+
+ return unless token
+
+ deploy_token = DeployToken.active.find_by_token(token.access_token_id.to_s)
+ # note: uesr_id is not a user record id, but is the attribute set on ConanToken
+ return if token.user_id != deploy_token&.username
+
+ deploy_token
+ end
+
+ def find_job_from_conan_jwt
+ token = decode_oauth_token_from_jwt
+
+ return unless token
+
+ ::Ci::Build.find_by_token(token.access_token_id.to_s)
+ end
+
+ def decode_oauth_token_from_jwt
+ jwt = Doorkeeper::OAuth::Token.from_bearer_authorization(current_request)
+
+ return unless jwt
+
+ token = ::Gitlab::ConanToken.decode(jwt)
+
+ return unless token && token.access_token_id && token.user_id
+
+ token
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/packages/dependency_proxy_helpers.rb b/lib/api/helpers/packages/dependency_proxy_helpers.rb
new file mode 100644
index 00000000000..254af7690a2
--- /dev/null
+++ b/lib/api/helpers/packages/dependency_proxy_helpers.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module Packages
+ module DependencyProxyHelpers
+ REGISTRY_BASE_URLS = {
+ npm: 'https://registry.npmjs.org/'
+ }.freeze
+
+ def redirect_registry_request(forward_to_registry, package_type, options)
+ if forward_to_registry && redirect_registry_request_available?
+ redirect(registry_url(package_type, options))
+ else
+ yield
+ end
+ end
+
+ def registry_url(package_type, options)
+ base_url = REGISTRY_BASE_URLS[package_type]
+
+ raise ArgumentError, "Can't build registry_url for package_type #{package_type}" unless base_url
+
+ case package_type
+ when :npm
+ "#{base_url}#{options[:package_name]}"
+ end
+ end
+
+ def redirect_registry_request_available?
+ ::Gitlab::CurrentSettings.current_application_settings.npm_package_requests_forwarding
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb
new file mode 100644
index 00000000000..c6037d52de9
--- /dev/null
+++ b/lib/api/helpers/packages_helpers.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module PackagesHelpers
+ MAX_PACKAGE_FILE_SIZE = 50.megabytes.freeze
+
+ def require_packages_enabled!
+ not_found! unless ::Gitlab.config.packages.enabled
+ end
+
+ def require_dependency_proxy_enabled!
+ not_found! unless ::Gitlab.config.dependency_proxy.enabled
+ end
+
+ def authorize_read_package!(subject = user_project)
+ authorize!(:read_package, subject)
+ end
+
+ def authorize_create_package!(subject = user_project)
+ authorize!(:create_package, subject)
+ end
+
+ def authorize_destroy_package!(subject = user_project)
+ authorize!(:destroy_package, subject)
+ end
+
+ def authorize_packages_access!(subject = user_project)
+ require_packages_enabled!
+ authorize_read_package!(subject)
+ end
+
+ def authorize_workhorse!(subject: user_project, has_length: true, maximum_size: MAX_PACKAGE_FILE_SIZE)
+ authorize_upload!(subject)
+
+ Gitlab::Workhorse.verify_api_request!(headers)
+
+ status 200
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+
+ params = { has_length: has_length }
+ params[:maximum_size] = maximum_size unless has_length
+ ::Packages::PackageFileUploader.workhorse_authorize(params)
+ end
+
+ def authorize_upload!(subject = user_project)
+ authorize_create_package!(subject)
+ require_gitlab_workhorse!
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/packages_manager_clients_helpers.rb b/lib/api/helpers/packages_manager_clients_helpers.rb
new file mode 100644
index 00000000000..7b5d0dd708d
--- /dev/null
+++ b/lib/api/helpers/packages_manager_clients_helpers.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module PackagesManagerClientsHelpers
+ extend Grape::API::Helpers
+ include ::API::Helpers::PackagesHelpers
+
+ params :workhorse_upload_params do
+ optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)'
+ optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
+ optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
+ optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
+ optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
+ optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
+ optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
+ end
+
+ def find_personal_access_token_from_http_basic_auth
+ return unless headers
+
+ token = decode_token
+
+ return unless token
+
+ PersonalAccessToken.find_by_token(token)
+ end
+
+ def find_job_from_http_basic_auth
+ return unless headers
+
+ token = decode_token
+
+ return unless token
+
+ ::Ci::Build.find_by_token(token)
+ end
+
+ def find_deploy_token_from_http_basic_auth
+ return unless headers
+
+ token = decode_token
+
+ return unless token
+
+ DeployToken.active.find_by_token(token)
+ end
+
+ def uploaded_package_file(param_name = :file)
+ uploaded_file = UploadedFile.from_params(params, param_name, ::Packages::PackageFileUploader.workhorse_local_upload_path)
+ bad_request!('Missing package file!') unless uploaded_file
+ uploaded_file
+ end
+
+ private
+
+ def decode_token
+ encoded_credentials = headers['Authorization'].to_s.split('Basic ', 2).second
+ Base64.decode64(encoded_credentials || '').split(':', 2).second
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 8a115d42929..76e5bb95c4d 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -6,6 +6,8 @@ module API
extend ActiveSupport::Concern
extend Grape::API::Helpers
+ STATISTICS_SORT_PARAMS = %w[storage_size repository_size wiki_size].freeze
+
params :optional_project_params_ce do
optional :description, type: String, desc: 'The description of the project'
optional :build_git_strategy, type: String, values: %w(fetch clone), desc: 'The Git strategy. Defaults to `fetch`'
@@ -13,6 +15,7 @@ module API
optional :auto_cancel_pending_pipelines, type: String, values: %w(disabled enabled), desc: 'Auto-cancel pending pipelines'
optional :build_coverage_regex, type: String, desc: 'Test coverage parsing'
optional :ci_config_path, type: String, desc: 'The path to CI config file. Defaults to `.gitlab-ci.yml`'
+ optional :service_desk_enabled, type: Boolean, desc: 'Disable or enable the service desk'
# TODO: remove in API v5, replaced by *_access_level
optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled'
@@ -46,7 +49,7 @@ module API
optional :only_allow_merge_if_pipeline_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed'
optional :allow_merge_on_skipped_pipeline, type: Boolean, desc: 'Allow to merge if pipeline is skipped'
optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved'
- optional :tag_list, type: Array[String], desc: 'The list of tags for a project'
+ optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The list of tags 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 :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
@@ -134,6 +137,7 @@ module API
:suggestion_commit_message,
:repository_storage,
:compliance_framework_setting,
+ :service_desk_enabled,
# TODO: remove in API v5, replaced by *_access_level
:issues_enabled,
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index 293d7ed9a6a..34a2fb09875 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -60,7 +60,7 @@ module API
def current_job
strong_memoize(:current_job) do
- Ci::Build.find_by_id(params[:id])
+ ::Ci::Build.find_by_id(params[:id])
end
end
@@ -69,11 +69,6 @@ module API
token && job.valid_token?(token)
end
- def max_artifacts_size(job)
- max_size = job.project.closest_setting(:max_artifacts_size)
- max_size.megabytes.to_i
- end
-
def job_forbidden!(job, reason)
header 'Job-Status', job.status
forbidden!(reason)
diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb
index 3d6039cacaa..d4870b96575 100644
--- a/lib/api/helpers/services_helpers.rb
+++ b/lib/api/helpers/services_helpers.rb
@@ -234,18 +234,6 @@ module API
name: :project_url,
type: String,
desc: 'Project URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'Description'
- },
- {
- required: false,
- name: :title,
- type: String,
- desc: 'Title'
}
],
'buildkite' => [
@@ -288,6 +276,14 @@ module API
desc: 'Campfire room'
}
],
+ 'confluence' => [
+ {
+ required: true,
+ name: :confluence_url,
+ type: String,
+ desc: 'The URL of the Confluence Cloud Workspace hosted on atlassian.net'
+ }
+ ],
'custom-issue-tracker' => [
{
required: true,
@@ -306,18 +302,6 @@ module API
name: :project_url,
type: String,
desc: 'Project URL'
- },
- {
- required: false,
- name: :description,
- type: String,
- desc: 'Description'
- },
- {
- required: false,
- name: :title,
- type: String,
- desc: 'Title'
}
],
'discord' => [
@@ -757,6 +741,7 @@ module API
::BambooService,
::BugzillaService,
::BuildkiteService,
+ ::ConfluenceService,
::CampfireService,
::CustomIssueTrackerService,
::DiscordService,
diff --git a/lib/api/helpers/snippets_helpers.rb b/lib/api/helpers/snippets_helpers.rb
index 20aeca6a9d3..f95d066bd7c 100644
--- a/lib/api/helpers/snippets_helpers.rb
+++ b/lib/api/helpers/snippets_helpers.rb
@@ -3,15 +3,37 @@
module API
module Helpers
module SnippetsHelpers
+ extend Grape::API::Helpers
+
+ params :raw_file_params do
+ requires :file_path, type: String, file_path: true, desc: 'The url encoded path to the file, e.g. lib%2Fclass%2Erb'
+ requires :ref, type: String, desc: 'The name of branch, tag or commit'
+ end
+
def content_for(snippet)
if snippet.empty_repo?
+ env['api.format'] = :txt
+ content_type 'text/plain'
+ header['Content-Disposition'] = 'attachment'
+
snippet.content
else
blob = snippet.blobs.first
- blob.load_all_data!
- blob.data
+
+ send_git_blob(blob.repository, blob)
end
end
+
+ def file_content_for(snippet)
+ repo = snippet.repository
+ commit = repo.commit(params[:ref])
+ not_found!('Reference') unless commit
+
+ blob = repo.blob_at(commit.sha, params[:file_path])
+ not_found!('File') unless blob
+
+ send_git_blob(repo, blob)
+ end
end
end
end
diff --git a/lib/api/helpers/users_helpers.rb b/lib/api/helpers/users_helpers.rb
index 99eefc1cbb9..2d7b22e66b3 100644
--- a/lib/api/helpers/users_helpers.rb
+++ b/lib/api/helpers/users_helpers.rb
@@ -11,6 +11,13 @@ module API
params :optional_index_params_ee do
end
+
+ def model_error_messages(model)
+ super.tap do |error_messages|
+ # Remapping errors from nested associations.
+ error_messages[:bio] = error_messages.delete(:"user_detail.bio") if error_messages.has_key?(:"user_detail.bio")
+ end
+ end
end
end
end
diff --git a/lib/api/helpers/wikis_helpers.rb b/lib/api/helpers/wikis_helpers.rb
new file mode 100644
index 00000000000..49da1e317ab
--- /dev/null
+++ b/lib/api/helpers/wikis_helpers.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module API
+ module Helpers
+ module WikisHelpers
+ def self.wiki_resource_kinds
+ [:projects]
+ end
+
+ def find_container(kind)
+ return user_project if kind == :projects
+
+ raise "Unknown wiki container #{kind}"
+ end
+
+ def wiki_page
+ Wiki.for_container(container, current_user).find_page(params[:slug]) || not_found!('Wiki Page')
+ end
+
+ def commit_params(attrs)
+ base_params = { branch_name: attrs[:branch] }
+ file_details = case attrs[:file]
+ when Hash # legacy format: TODO remove when we drop support for non accelerated uploads
+ { file_name: attrs[:file][:filename], file_content: attrs[:file][:tempfile].read }
+ else
+ { file_name: attrs[:file].original_filename, file_content: attrs[:file].read }
+ end
+
+ base_params.merge(file_details)
+ end
+ end
+ end
+end
+
+API::Helpers::WikisHelpers.prepend_if_ee('EE::API::Helpers::WikisHelpers')
diff --git a/lib/api/import_bitbucket_server.rb b/lib/api/import_bitbucket_server.rb
new file mode 100644
index 00000000000..df3235420e9
--- /dev/null
+++ b/lib/api/import_bitbucket_server.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module API
+ class ImportBitbucketServer < Grape::API::Instance
+ helpers do
+ def client
+ @client ||= BitbucketServer::Client.new(credentials)
+ end
+
+ def credentials
+ @credentials ||= {
+ base_uri: params[:bitbucket_server_url],
+ user: params[:bitbucket_server_username],
+ password: params[:personal_access_token]
+ }
+ end
+ end
+
+ desc 'Import a BitBucket Server repository' do
+ detail 'This feature was introduced in GitLab 13.2.'
+ success ::ProjectEntity
+ end
+
+ params do
+ requires :bitbucket_server_url, type: String, desc: 'Bitbucket Server URL'
+ requires :bitbucket_server_username, type: String, desc: 'BitBucket Server Username'
+ requires :personal_access_token, type: String, desc: 'BitBucket Server personal access token/password'
+ requires :bitbucket_server_project, type: String, desc: 'BitBucket Server Project Key'
+ requires :bitbucket_server_repo, type: String, desc: 'BitBucket Server Repository Name'
+ optional :new_name, type: String, desc: 'New repo name'
+ optional :new_namespace, type: String, desc: 'Namespace to import repo into'
+ end
+
+ post 'import/bitbucket_server' do
+ result = Import::BitbucketServerService.new(client, current_user, params).execute(credentials)
+
+ if result[:status] == :success
+ present ProjectSerializer.new.represent(result[:project], serializer: :import)
+ else
+ render_api_error!({ error: result[:message] }, result[:http_status])
+ end
+ end
+ end
+end
diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb
index f31cc15dc62..1e839816006 100644
--- a/lib/api/import_github.rb
+++ b/lib/api/import_github.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ImportGithub < Grape::API
+ class ImportGithub < Grape::API::Instance
rescue_from Octokit::Unauthorized, with: :provider_unauthorized
before do
diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb
index 79c407b9581..6d4a4fc9c8b 100644
--- a/lib/api/internal/base.rb
+++ b/lib/api/internal/base.rb
@@ -3,7 +3,7 @@
module API
# Internal access API
module Internal
- class Base < Grape::API
+ class Base < Grape::API::Instance
before { authenticate_by_gitlab_shell_token! }
before do
@@ -63,15 +63,13 @@ module API
gl_project_path: gl_repository_path,
gl_id: Gitlab::GlId.gl_id(actor.user),
gl_username: actor.username,
- git_config_options: [],
+ git_config_options: ["uploadpack.allowFilter=true",
+ "uploadpack.allowAnySHA1InWant=true"],
gitaly: gitaly_payload(params[:action]),
gl_console_messages: check_result.console_messages
}
# Custom option for git-receive-pack command
- if Feature.enabled?(:gitaly_upload_pack_filter, project, default_enabled: true)
- payload[:git_config_options] << "uploadpack.allowFilter=true" << "uploadpack.allowAnySHA1InWant=true"
- end
receive_max_input_size = Gitlab::CurrentSettings.receive_max_input_size.to_i
diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb
index 6c8da414e4d..5f8d23f15fa 100644
--- a/lib/api/internal/pages.rb
+++ b/lib/api/internal/pages.rb
@@ -3,7 +3,7 @@
module API
# Pages Internal API
module Internal
- class Pages < Grape::API
+ class Pages < Grape::API::Instance
before do
authenticate_gitlab_pages_request!
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 2374ac11f4a..455511caabb 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Issues < Grape::API
+ class Issues < Grape::API::Instance
include PaginationParams
helpers Helpers::IssuesHelpers
helpers Helpers::RateLimiter
@@ -10,9 +10,9 @@ module API
helpers do
params :negatable_issue_filter_params do
- optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
+ optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
- optional :iids, type: Array[Integer], desc: 'The IID array of issues'
+ optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues'
optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these'
optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma'
@@ -62,12 +62,12 @@ module API
params :issue_params do
optional :description, type: String, desc: 'The description of an issue'
- optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
+ optional :assignee_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The array of user IDs to assign issue'
optional :assignee_id, type: Integer, desc: '[Deprecated] The ID of a user to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue'
- optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
- optional :add_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
- optional :remove_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
+ optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
+ optional :add_labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
+ optional :remove_labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential'
optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked"
@@ -107,7 +107,6 @@ module API
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
- issuable_metadata: Gitlab::IssuableMetadata.new(current_user, issues).data,
include_subscribed: false
}
@@ -133,7 +132,6 @@ module API
with: Entities::Issue,
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
- issuable_metadata: Gitlab::IssuableMetadata.new(current_user, issues).data,
include_subscribed: false,
group: user_group
}
@@ -170,7 +168,6 @@ module API
with_labels_details: declared_params[:with_labels_details],
current_user: current_user,
project: user_project,
- issuable_metadata: Gitlab::IssuableMetadata.new(current_user, issues).data,
include_subscribed: false
}
@@ -289,6 +286,30 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
+ desc 'Reorder an existing issue' do
+ success Entities::Issue
+ end
+ params do
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ optional :move_after_id, type: Integer, desc: 'The ID of the issue we want to be after'
+ optional :move_before_id, type: Integer, desc: 'The ID of the issue we want to be before'
+ at_least_one_of :move_after_id, :move_before_id
+ end
+ # rubocop: disable CodeReuse/ActiveRecord
+ put ':id/issues/:issue_iid/reorder' do
+ issue = user_project.issues.find_by(iid: params[:issue_iid])
+ not_found!('Issue') unless issue
+
+ authorize! :update_issue, issue
+
+ if ::Issues::ReorderService.new(user_project, current_user, params).execute(issue)
+ present issue, with: Entities::Issue, current_user: current_user, project: user_project
+ else
+ render_api_error!({ error: 'Unprocessable Entity' }, 422)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
desc 'Move an existing issue' do
success Entities::Issue
end
diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb
index 6a82256cc96..61c279a76e9 100644
--- a/lib/api/job_artifacts.rb
+++ b/lib/api/job_artifacts.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class JobArtifacts < Grape::API
+ class JobArtifacts < Grape::API::Instance
before { authenticate_non_get! }
# EE::API::JobArtifacts would override the following helpers
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 61a7fc107ef..bcc00429dd6 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Jobs < Grape::API
+ class Jobs < Grape::API::Instance
include PaginationParams
before { authenticate! }
@@ -160,7 +160,7 @@ module API
authorize!(:update_build, build)
break forbidden!('Job is not retryable') unless build.retryable?
- build = Ci::Build.retry(build, current_user)
+ build = ::Ci::Build.retry(build, current_user)
present build, with: Entities::Job
end
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
index b730e027063..c014641ca04 100644
--- a/lib/api/keys.rb
+++ b/lib/api/keys.rb
@@ -2,7 +2,7 @@
module API
# Keys API
- class Keys < Grape::API
+ class Keys < Grape::API::Instance
before { authenticate! }
resource :keys do
diff --git a/lib/api/labels.rb b/lib/api/labels.rb
index 2b283d82e4a..edf4a8ca14e 100644
--- a/lib/api/labels.rb
+++ b/lib/api/labels.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Labels < Grape::API
+ class Labels < Grape::API::Instance
include PaginationParams
helpers ::API::Helpers::LabelHelpers
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index a7672021db0..f7796b1e969 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Lint < Grape::API
+ class Lint < Grape::API::Instance
namespace :ci do
desc 'Validation of .gitlab-ci.yml content'
params do
diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb
index de77bef43ce..a0822271cca 100644
--- a/lib/api/markdown.rb
+++ b/lib/api/markdown.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Markdown < Grape::API
+ class Markdown < Grape::API::Instance
params do
requires :text, type: String, desc: "The markdown text to render"
optional :gfm, type: Boolean, desc: "Render text using GitLab Flavored Markdown"
diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb
new file mode 100644
index 00000000000..32a45c59cfa
--- /dev/null
+++ b/lib/api/maven_packages.rb
@@ -0,0 +1,251 @@
+# frozen_string_literal: true
+module API
+ class MavenPackages < Grape::API::Instance
+ MAVEN_ENDPOINT_REQUIREMENTS = {
+ file_name: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ content_type :md5, 'text/plain'
+ content_type :sha1, 'text/plain'
+ content_type :binary, 'application/octet-stream'
+
+ rescue_from ActiveRecord::RecordInvalid do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ before do
+ require_packages_enabled!
+ authenticate_non_get!
+ end
+
+ helpers ::API::Helpers::PackagesHelpers
+
+ helpers do
+ def extract_format(file_name)
+ name, _, format = file_name.rpartition('.')
+
+ if %w(md5 sha1).include?(format)
+ [name, format]
+ else
+ [file_name, format]
+ end
+ end
+
+ def verify_package_file(package_file, uploaded_file)
+ stored_sha1 = Digest::SHA256.hexdigest(package_file.file_sha1)
+ expected_sha1 = uploaded_file.sha256
+
+ if stored_sha1 == expected_sha1
+ no_content!
+ else
+ conflict!
+ end
+ end
+
+ def find_project_by_path(path)
+ project_path = path.rpartition('/').first
+ Project.find_by_full_path(project_path)
+ end
+
+ def jar_file?(format)
+ format == 'jar'
+ end
+
+ def present_carrierwave_file_with_head_support!(file, supports_direct_download: true)
+ if head_request_on_aws_file?(file, supports_direct_download) && !file.file_storage?
+ return redirect(signed_head_url(file))
+ end
+
+ present_carrierwave_file!(file, supports_direct_download: supports_direct_download)
+ end
+
+ def signed_head_url(file)
+ fog_storage = ::Fog::Storage.new(file.fog_credentials)
+ fog_dir = fog_storage.directories.new(key: file.fog_directory)
+ fog_file = fog_dir.files.new(key: file.path)
+ expire_at = ::Fog::Time.now + file.fog_authenticated_url_expiration
+
+ fog_file.collection.head_url(fog_file.key, expire_at)
+ end
+
+ def head_request_on_aws_file?(file, supports_direct_download)
+ Gitlab.config.packages.object_store.enabled &&
+ supports_direct_download &&
+ file.class.direct_download_enabled? &&
+ request.head? &&
+ file.fog_credentials[:provider] == 'AWS'
+ end
+ end
+
+ desc 'Download the maven package file at instance level' do
+ detail 'This feature was introduced in GitLab 11.6'
+ end
+ params do
+ requires :path, type: String, desc: 'Package path'
+ requires :file_name, type: String, desc: 'Package file name'
+ end
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ get 'packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
+ file_name, format = extract_format(params[:file_name])
+
+ # To avoid name collision we require project path and project package be the same.
+ # For packages that have different name from the project we should use
+ # the endpoint that includes project id
+ project = find_project_by_path(params[:path])
+
+ authorize_read_package!(project)
+
+ package = ::Packages::Maven::PackageFinder
+ .new(params[:path], current_user, project: project).execute!
+
+ 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_event('pull_package') if jar_file?(format)
+ present_carrierwave_file_with_head_support!(package_file.file)
+ end
+ end
+
+ desc 'Download the maven package file at a group level' do
+ detail 'This feature was introduced in GitLab 11.7'
+ end
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ params do
+ requires :path, type: String, desc: 'Package path'
+ requires :file_name, type: String, desc: 'Package file name'
+ end
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
+ file_name, format = extract_format(params[:file_name])
+
+ group = find_group(params[:id])
+
+ not_found!('Group') unless can?(current_user, :read_group, group)
+
+ package = ::Packages::Maven::PackageFinder
+ .new(params[:path], current_user, group: group).execute!
+
+ authorize_read_package!(package.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_event('pull_package') if jar_file?(format)
+
+ present_carrierwave_file_with_head_support!(package_file.file)
+ end
+ 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 'Download the maven package file' do
+ detail 'This feature was introduced in GitLab 11.3'
+ end
+ params do
+ requires :path, type: String, desc: 'Package path'
+ requires :file_name, type: String, desc: 'Package file name'
+ end
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
+ authorize_read_package!(user_project)
+
+ file_name, format = extract_format(params[:file_name])
+
+ package = ::Packages::Maven::PackageFinder
+ .new(params[:path], current_user, project: user_project).execute!
+
+ package_file = ::Packages::PackageFileFinder
+ .new(package, file_name).execute!
+
+ case format
+ when 'md5'
+ package_file.file_md5
+ when 'sha1'
+ package_file.file_sha1
+ else
+ track_event('pull_package') if jar_file?(format)
+
+ present_carrierwave_file_with_head_support!(package_file.file)
+ end
+ end
+
+ desc 'Workhorse authorize the maven package file upload' do
+ detail 'This feature was introduced in GitLab 11.3'
+ end
+ params do
+ requires :path, type: String, desc: 'Package path'
+ requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex
+ end
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ put ':id/packages/maven/*path/:file_name/authorize', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
+ authorize_upload!
+
+ status 200
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+ ::Packages::PackageFileUploader.workhorse_authorize(has_length: true)
+ end
+
+ desc 'Upload the maven package file' do
+ detail 'This feature was introduced in GitLab 11.3'
+ end
+ params do
+ requires :path, type: String, desc: 'Package path'
+ requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.maven_file_name_regex
+ requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
+ end
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ put ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
+ authorize_upload!
+
+ file_name, format = extract_format(params[:file_name])
+
+ package = ::Packages::Maven::FindOrCreatePackageService
+ .new(user_project, current_user, params.merge(build: current_authenticated_job)).execute
+
+ case format
+ when 'sha1'
+ # After uploading a file, Maven tries to upload a sha1 and md5 version of it.
+ # Since we store md5/sha1 in database we simply need to validate our hash
+ # against one uploaded by Maven. We do this for `sha1` format.
+ package_file = ::Packages::PackageFileFinder
+ .new(package, file_name).execute!
+
+ verify_package_file(package_file, params[:file])
+ when 'md5'
+ nil
+ else
+ track_event('push_package') if jar_file?(format)
+
+ file_params = {
+ file: params[:file],
+ size: params['file.size'],
+ file_name: file_name,
+ file_type: params['file.type'],
+ file_sha1: params['file.sha1'],
+ file_md5: params['file.md5']
+ }
+
+ ::Packages::CreatePackageFileService.new(package, file_params).execute
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 37d4ca29b68..4edf94c6350 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Members < Grape::API
+ class Members < Grape::API::Instance
include PaginationParams
before { authenticate! }
@@ -18,7 +18,7 @@ module API
end
params do
optional :query, type: String, desc: 'A query string to search for members'
- optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
+ optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of user ids to look up for membership'
optional :show_seat_info, type: Boolean, desc: 'Show seat information for members'
use :optional_filter_params_ee
use :pagination
@@ -37,7 +37,7 @@ module API
end
params do
optional :query, type: String, desc: 'A query string to search for members'
- optional :user_ids, type: Array[Integer], desc: 'Array of user ids to look up for membership'
+ optional :user_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of user ids to look up for membership'
optional :show_seat_info, type: Boolean, desc: 'Show seat information for members'
use :pagination
end
@@ -107,7 +107,7 @@ module API
if !member
not_allowed! # This currently can only be reached in EE
- elsif member.persisted? && member.valid?
+ elsif member.valid? && member.persisted?
present_members(member)
else
render_validation_error!(member)
@@ -145,6 +145,8 @@ module API
desc 'Removes a user from a group or project.'
params do
requires :user_id, type: Integer, desc: 'The user ID of the member'
+ optional :unassign_issuables, type: Boolean, default: false,
+ desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project'
end
# rubocop: disable CodeReuse/ActiveRecord
delete ":id/members/:user_id" do
@@ -152,7 +154,7 @@ module API
member = source.members.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do
- ::Members::DestroyService.new(current_user).execute(member)
+ ::Members::DestroyService.new(current_user).execute(member, unassign_issuables: params[:unassign_issuables])
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb
new file mode 100644
index 00000000000..035ed9f0e04
--- /dev/null
+++ b/lib/api/merge_request_approvals.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module API
+ class MergeRequestApprovals < ::Grape::API::Instance
+ before { authenticate_non_get! }
+
+ helpers do
+ params :ee_approval_params do
+ end
+
+ def present_approval(merge_request)
+ present merge_request, with: ::API::Entities::MergeRequestApprovals, current_user: current_user
+ end
+ end
+
+ resource :projects, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ segment ':id/merge_requests/:merge_request_iid' do
+ # Get the status of the merge request's approvals
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_iid (required) - IID of MR
+ # Examples:
+ # GET /projects/:id/merge_requests/:merge_request_iid/approvals
+ desc 'List approvals for merge request'
+ get 'approvals' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid])
+
+ present_approval(merge_request)
+ end
+
+ # Approve a merge request
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_iid (required) - IID of MR
+ # Examples:
+ # POST /projects/:id/merge_requests/:merge_request_iid/approve
+ #
+ desc 'Approve a merge request'
+ params do
+ optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
+
+ use :ee_approval_params
+ end
+ post 'approve' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid], :approve_merge_request)
+
+ check_sha_param!(params, merge_request)
+
+ success =
+ ::MergeRequests::ApprovalService
+ .new(user_project, current_user, params)
+ .execute(merge_request)
+
+ unauthorized! unless success
+
+ present_approval(merge_request)
+ end
+
+ desc 'Remove an approval from a merge request'
+ post 'unapprove' do
+ merge_request = find_merge_request_with_access(params[:merge_request_iid], :approve_merge_request)
+
+ success = ::MergeRequests::RemoveApprovalService
+ .new(user_project, current_user)
+ .execute(merge_request)
+
+ not_found! unless success
+
+ present_approval(merge_request)
+ end
+ end
+ end
+ end
+end
+
+API::MergeRequestApprovals.prepend_if_ee('EE::API::MergeRequestApprovals')
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 6ad30aa56e0..3e43fe8b257 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -2,7 +2,7 @@
module API
# MergeRequestDiff API
- class MergeRequestDiffs < Grape::API
+ class MergeRequestDiffs < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 773a451d3a8..2e6ac40a593 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class MergeRequests < Grape::API
+ class MergeRequests < Grape::API::Instance
include PaginationParams
CONTEXT_COMMITS_POST_LIMIT = 20
@@ -44,7 +44,9 @@ module API
def find_merge_requests(args = {})
args = declared_params.merge(args)
args[:milestone_title] = args.delete(:milestone)
+ args[:not][:milestone_title] = args[:not]&.delete(:milestone)
args[:label_name] = args.delete(:labels)
+ args[:not][:label_name] = args[:not]&.delete(:labels)
args[:scope] = args[:scope].underscore if args[:scope]
merge_requests = MergeRequestsFinder.new(current_user, args).execute
@@ -60,16 +62,8 @@ module API
# rubocop: enable CodeReuse/ActiveRecord
def merge_request_pipelines_with_access
- authorize! :read_pipeline, user_project
-
mr = find_merge_request_with_access(params[:merge_request_iid])
- mr.all_pipelines
- end
-
- def check_sha_param!(params, merge_request)
- if params[:sha] && merge_request.diff_head_sha != params[:sha]
- render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
- end
+ ::Ci::PipelinesForMergeRequestFinder.new(mr, current_user).execute
end
def automatically_mergeable?(merge_when_pipeline_succeeds, merge_request)
@@ -91,7 +85,6 @@ module API
if params[:view] == 'simple'
options[:with] = Entities::MergeRequestSimple
else
- options[:issuable_metadata] = Gitlab::IssuableMetadata.new(current_user, merge_requests).data
options[:skip_merge_status_recheck] = !declared_params[:with_merge_status_recheck]
end
@@ -179,11 +172,11 @@ module API
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
- optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
+ optional :assignee_ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The array of user IDs to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
- optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
- optional :add_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
- optional :remove_labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
+ optional :labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
+ optional :add_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
+ optional :remove_labels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
optional :allow_collaboration, type: Boolean, desc: 'Allow commits from members who can merge to the target branch'
optional :allow_maintainer_to_push, type: Boolean, as: :allow_collaboration, desc: '[deprecated] See allow_collaboration'
@@ -198,7 +191,7 @@ module API
end
params do
use :merge_requests_params
- optional :iids, type: Array[Integer], desc: 'The IID array of merge requests'
+ optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of merge requests'
end
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
@@ -315,7 +308,7 @@ module API
end
params do
- requires :commits, type: Array, allow_blank: false, desc: 'List of context commits sha'
+ requires :commits, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, allow_blank: false, desc: 'List of context commits sha'
end
desc 'create context commits of merge request' do
success Entities::Commit
@@ -345,7 +338,7 @@ module API
end
params do
- requires :commits, type: Array, allow_blank: false, desc: 'List of context commits sha'
+ requires :commits, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, allow_blank: false, desc: 'List of context commits sha'
end
desc 'remove context commits of merge request'
delete ':id/merge_requests/:merge_request_iid/context_commits' do
@@ -389,8 +382,6 @@ module API
success Entities::Pipeline
end
post ':id/merge_requests/:merge_request_iid/pipelines' do
- authorize! :create_pipeline, user_project
-
pipeline = ::MergeRequests::CreatePipelineService
.new(user_project, current_user, allow_duplicate: true)
.execute(find_merge_request_with_access(params[:merge_request_iid]))
diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb
index c8ec4d29657..e07762ac6d3 100644
--- a/lib/api/metrics/dashboard/annotations.rb
+++ b/lib/api/metrics/dashboard/annotations.rb
@@ -3,7 +3,7 @@
module API
module Metrics
module Dashboard
- class Annotations < Grape::API
+ class Annotations < Grape::API::Instance
desc 'Create a new monitoring dashboard annotation' do
success Entities::Metrics::Dashboard::Annotation
end
diff --git a/lib/api/metrics/user_starred_dashboards.rb b/lib/api/metrics/user_starred_dashboards.rb
index 85fc0f33ed8..263d2394276 100644
--- a/lib/api/metrics/user_starred_dashboards.rb
+++ b/lib/api/metrics/user_starred_dashboards.rb
@@ -2,7 +2,7 @@
module API
module Metrics
- class UserStarredDashboards < Grape::API
+ class UserStarredDashboards < Grape::API::Instance
resource :projects do
desc 'Marks selected metrics dashboard as starred' do
success Entities::Metrics::UserStarredDashboard
diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb
index 62e159ab003..8ff885983bc 100644
--- a/lib/api/milestone_responses.rb
+++ b/lib/api/milestone_responses.rb
@@ -15,7 +15,7 @@ module API
params :list_params do
optional :state, type: String, values: %w[active closed all], default: 'all',
desc: 'Return "active", "closed", or "all" milestones'
- optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones'
+ optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IIDs of the milestones'
optional :title, type: String, desc: 'The title of the milestones'
optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
use :pagination
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index e40a5dde7ce..e1f279df045 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Namespaces < Grape::API
+ class Namespaces < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 3eafc1ead77..bfd09dcd496 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Notes < Grape::API
+ class Notes < Grape::API::Instance
include PaginationParams
helpers ::API::Helpers::NotesHelpers
@@ -68,6 +68,7 @@ module API
params do
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
requires :body, type: String, desc: 'The content of a note'
+ optional :confidential, type: Boolean, desc: 'Confidentiality note flag, default is false'
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
@@ -77,6 +78,7 @@ module API
note: params[:body],
noteable_type: noteables_str.classify,
noteable_id: noteable.id,
+ confidential: params[:confidential],
created_at: params[:created_at]
}
diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb
index 8cb46bd3ad6..f8b621c1c38 100644
--- a/lib/api/notification_settings.rb
+++ b/lib/api/notification_settings.rb
@@ -2,7 +2,7 @@
module API
# notification_settings API
- class NotificationSettings < Grape::API
+ class NotificationSettings < Grape::API::Instance
before { authenticate! }
helpers ::API::Helpers::MembersHelpers
diff --git a/lib/api/npm_packages.rb b/lib/api/npm_packages.rb
new file mode 100644
index 00000000000..21ca57b7985
--- /dev/null
+++ b/lib/api/npm_packages.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+module API
+ class NpmPackages < Grape::API::Instance
+ helpers ::API::Helpers::PackagesHelpers
+ helpers ::API::Helpers::Packages::DependencyProxyHelpers
+
+ NPM_ENDPOINT_REQUIREMENTS = {
+ package_name: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ rescue_from ActiveRecord::RecordInvalid do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ before do
+ require_packages_enabled!
+ authenticate_non_get!
+ end
+
+ helpers do
+ def project_by_package_name
+ strong_memoize(:project_by_package_name) do
+ ::Packages::Package.npm.with_name(params[:package_name]).first&.project
+ end
+ end
+ end
+
+ desc 'Get all tags for a given an NPM package' do
+ detail 'This feature was introduced in GitLab 12.7'
+ success ::API::Entities::NpmPackageTag
+ end
+ params do
+ requires :package_name, type: String, desc: 'Package name'
+ end
+ get 'packages/npm/-/package/*package_name/dist-tags', format: false, requirements: NPM_ENDPOINT_REQUIREMENTS do
+ package_name = params[:package_name]
+
+ bad_request!('Package Name') if package_name.blank?
+
+ authorize_read_package!(project_by_package_name)
+
+ packages = ::Packages::Npm::PackageFinder.new(project_by_package_name, package_name)
+ .execute
+
+ present ::Packages::Npm::PackagePresenter.new(package_name, packages),
+ with: ::API::Entities::NpmPackageTag
+ end
+
+ params do
+ requires :package_name, type: String, desc: 'Package name'
+ requires :tag, type: String, desc: "Package dist-tag"
+ end
+ namespace 'packages/npm/-/package/*package_name/dist-tags/:tag', requirements: NPM_ENDPOINT_REQUIREMENTS do
+ desc 'Create or Update the given tag for the given NPM package and version' do
+ detail 'This feature was introduced in GitLab 12.7'
+ end
+ put format: false do
+ package_name = params[:package_name]
+ version = env['api.request.body']
+ tag = params[:tag]
+
+ bad_request!('Package Name') if package_name.blank?
+ bad_request!('Version') if version.blank?
+ bad_request!('Tag') if tag.blank?
+
+ authorize_create_package!(project_by_package_name)
+
+ package = ::Packages::Npm::PackageFinder
+ .new(project_by_package_name, package_name)
+ .find_by_version(version)
+ not_found!('Package') unless package
+
+ ::Packages::Npm::CreateTagService.new(package, tag).execute
+
+ no_content!
+ end
+
+ desc 'Deletes the given tag' do
+ detail 'This feature was introduced in GitLab 12.7'
+ end
+ delete format: false do
+ package_name = params[:package_name]
+ tag = params[:tag]
+
+ bad_request!('Package Name') if package_name.blank?
+ bad_request!('Tag') if tag.blank?
+
+ authorize_destroy_package!(project_by_package_name)
+
+ package_tag = ::Packages::TagsFinder
+ .new(project_by_package_name, package_name, package_type: :npm)
+ .find_by_name(tag)
+
+ not_found!('Package tag') unless package_tag
+
+ ::Packages::RemoveTagService.new(package_tag).execute
+
+ no_content!
+ end
+ end
+
+ desc 'NPM registry endpoint at instance level' do
+ detail 'This feature was introduced in GitLab 11.8'
+ end
+ params do
+ requires :package_name, type: String, desc: 'Package name'
+ end
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ get 'packages/npm/*package_name', format: false, requirements: NPM_ENDPOINT_REQUIREMENTS do
+ package_name = params[:package_name]
+
+ redirect_registry_request(project_by_package_name.blank?, :npm, package_name: package_name) do
+ authorize_read_package!(project_by_package_name)
+
+ packages = ::Packages::Npm::PackageFinder
+ .new(project_by_package_name, package_name).execute
+
+ present ::Packages::Npm::PackagePresenter.new(package_name, packages),
+ with: ::API::Entities::NpmPackage
+ 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 'Download the NPM tarball' do
+ detail 'This feature was introduced in GitLab 11.8'
+ end
+ params do
+ requires :package_name, type: String, desc: 'Package name'
+ requires :file_name, type: String, desc: 'Package file name'
+ end
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ get ':id/packages/npm/*package_name/-/*file_name', format: false do
+ authorize_read_package!(user_project)
+
+ package = user_project.packages.npm
+ .by_name_and_file_name(params[:package_name], params[:file_name])
+
+ package_file = ::Packages::PackageFileFinder
+ .new(package, params[:file_name]).execute!
+
+ track_event('pull_package')
+
+ present_carrierwave_file!(package_file.file)
+ end
+
+ desc 'Create NPM package' do
+ detail 'This feature was introduced in GitLab 11.8'
+ end
+ params do
+ requires :package_name, type: String, desc: 'Package name'
+ requires :versions, type: Hash, desc: 'Package version info'
+ end
+ route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
+ put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do
+ authorize_create_package!(user_project)
+
+ track_event('push_package')
+
+ created_package = ::Packages::Npm::CreatePackageService
+ .new(user_project, current_user, params.merge(build: current_authenticated_job)).execute
+
+ if created_package[:status] == :error
+ render_api_error!(created_package[:message], created_package[:http_status])
+ else
+ created_package
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb
new file mode 100644
index 00000000000..eb7d320a0f5
--- /dev/null
+++ b/lib/api/nuget_packages.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+# NuGet Package Manager Client API
+#
+# These API endpoints are not meant to be consumed directly by users. They are
+# called by the NuGet package manager client when users run commands
+# like `nuget install` or `nuget push`.
+module API
+ class NugetPackages < Grape::API::Instance
+ helpers ::API::Helpers::PackagesManagerClientsHelpers
+ helpers ::API::Helpers::Packages::BasicAuthHelpers
+
+ POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze
+ NON_NEGATIVE_INTEGER_REGEX = %r{\A0|[1-9]\d*\z}.freeze
+
+ PACKAGE_FILENAME = 'package.nupkg'
+
+ default_format :json
+
+ rescue_from ArgumentError do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ helpers do
+ def find_packages
+ packages = package_finder.execute
+
+ not_found!('Packages') unless packages.exists?
+
+ packages
+ end
+
+ def find_package
+ package = package_finder(package_version: params[:package_version]).execute
+ .first
+
+ not_found!('Package') unless package
+
+ package
+ end
+
+ def package_finder(finder_params = {})
+ ::Packages::Nuget::PackageFinder.new(
+ authorized_user_project,
+ finder_params.merge(package_name: params[:package_name])
+ )
+ end
+ end
+
+ before do
+ require_packages_enabled!
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project', regexp: POSITIVE_INTEGER_REGEX
+ end
+ route_setting :authentication, deploy_token_allowed: true
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before do
+ authorized_user_project
+ end
+
+ namespace ':id/packages/nuget' do
+ # https://docs.microsoft.com/en-us/nuget/api/service-index
+ desc 'The NuGet Service Index' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+ route_setting :authentication, deploy_token_allowed: true
+ get 'index', format: :json do
+ authorize_read_package!(authorized_user_project)
+
+ track_event('nuget_service_index')
+
+ present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project),
+ with: ::API::Entities::Nuget::ServiceIndex
+ end
+
+ # https://docs.microsoft.com/en-us/nuget/api/package-publish-resource
+ desc 'The NuGet Package Publish endpoint' do
+ detail 'This feature was introduced in GitLab 12.6'
+ end
+ params do
+ requires :package, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
+ end
+ route_setting :authentication, deploy_token_allowed: true
+ put do
+ authorize_upload!(authorized_user_project)
+
+ file_params = params.merge(
+ file: params[:package],
+ file_name: PACKAGE_FILENAME
+ )
+
+ package = ::Packages::Nuget::CreatePackageService.new(authorized_user_project, current_user)
+ .execute
+
+ package_file = ::Packages::CreatePackageFileService.new(package, file_params)
+ .execute
+
+ track_event('push_package')
+
+ ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker
+
+ created!
+ rescue ObjectStorage::RemoteStoreError => e
+ Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id })
+
+ forbidden!
+ end
+ route_setting :authentication, deploy_token_allowed: true
+ put 'authorize' do
+ authorize_workhorse!(subject: authorized_user_project, has_length: false)
+ end
+
+ params do
+ requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX
+ end
+ namespace '/metadata/*package_name' do
+ before do
+ authorize_read_package!(authorized_user_project)
+ end
+
+ # https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource
+ desc 'The NuGet Metadata Service - Package name level' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+ route_setting :authentication, deploy_token_allowed: true
+ get 'index', format: :json do
+ present ::Packages::Nuget::PackagesMetadataPresenter.new(find_packages),
+ with: ::API::Entities::Nuget::PackagesMetadata
+ end
+
+ desc 'The NuGet Metadata Service - Package name and version level' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+ params do
+ requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
+ end
+ route_setting :authentication, deploy_token_allowed: true
+ get '*package_version', format: :json do
+ present ::Packages::Nuget::PackageMetadataPresenter.new(find_package),
+ with: ::API::Entities::Nuget::PackageMetadata
+ end
+ end
+
+ # https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource
+ params do
+ requires :package_name, type: String, desc: 'The NuGet package name', regexp: API::NO_SLASH_URL_PART_REGEX
+ end
+ namespace '/download/*package_name' do
+ before do
+ authorize_read_package!(authorized_user_project)
+ end
+
+ desc 'The NuGet Content Service - index request' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+ route_setting :authentication, deploy_token_allowed: true
+ get 'index', format: :json do
+ present ::Packages::Nuget::PackagesVersionsPresenter.new(find_packages),
+ with: ::API::Entities::Nuget::PackagesVersions
+ end
+
+ desc 'The NuGet Content Service - content request' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+ params do
+ requires :package_version, type: String, desc: 'The NuGet package version', regexp: API::NO_SLASH_URL_PART_REGEX
+ requires :package_filename, type: String, desc: 'The NuGet package filename', regexp: API::NO_SLASH_URL_PART_REGEX
+ end
+ route_setting :authentication, deploy_token_allowed: true
+ get '*package_version/*package_filename', format: :nupkg do
+ filename = "#{params[:package_filename]}.#{params[:format]}"
+ package_file = ::Packages::PackageFileFinder.new(find_package, filename, with_file_name_like: true)
+ .execute
+
+ not_found!('Package') unless package_file
+
+ track_event('pull_package')
+
+ # 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)
+ end
+ end
+
+ params do
+ requires :q, type: String, desc: 'The search term'
+ optional :skip, type: Integer, desc: 'The number of results to skip', default: 0, regexp: NON_NEGATIVE_INTEGER_REGEX
+ optional :take, type: Integer, desc: 'The number of results to return', default: Kaminari.config.default_per_page, regexp: POSITIVE_INTEGER_REGEX
+ optional :prerelease, type: Boolean, desc: 'Include prerelease versions', default: true
+ end
+ namespace '/query' do
+ before do
+ authorize_read_package!(authorized_user_project)
+ end
+
+ # https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource
+ desc 'The NuGet Search Service' do
+ detail 'This feature was introduced in GitLab 12.8'
+ end
+ route_setting :authentication, deploy_token_allowed: true
+ get format: :json do
+ search_options = {
+ include_prerelease_versions: params[:prerelease],
+ per_page: params[:take],
+ padding: params[:skip]
+ }
+ search = Packages::Nuget::SearchService
+ .new(authorized_user_project, params[:q], search_options)
+ .execute
+
+ track_event('search_package')
+
+ present ::Packages::Nuget::SearchResultsPresenter.new(search),
+ with: ::API::Entities::Nuget::SearchResults
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/package_files.rb b/lib/api/package_files.rb
new file mode 100644
index 00000000000..17b92df629c
--- /dev/null
+++ b/lib/api/package_files.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module API
+ class PackageFiles < Grape::API::Instance
+ include PaginationParams
+
+ before do
+ authorize_packages_access!(user_project)
+ end
+
+ helpers ::API::Helpers::PackagesHelpers
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :package_id, type: Integer, desc: 'The ID of a package'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get all package files' do
+ detail 'This feature was introduced in GitLab 11.8'
+ success ::API::Entities::PackageFile
+ end
+ params do
+ use :pagination
+ end
+ get ':id/packages/:package_id/package_files' do
+ package = ::Packages::PackageFinder
+ .new(user_project, params[:package_id]).execute
+
+ present paginate(package.package_files), with: ::API::Entities::PackageFile
+ end
+ end
+ end
+end
diff --git a/lib/api/pages.rb b/lib/api/pages.rb
index ee7fe669519..79a6b527581 100644
--- a/lib/api/pages.rb
+++ b/lib/api/pages.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Pages < Grape::API
+ class Pages < Grape::API::Instance
before do
require_pages_config_enabled!
authenticated_with_can_read_all_resources!
diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb
index 4c3d2d131ac..7d27b575efa 100644
--- a/lib/api/pages_domains.rb
+++ b/lib/api/pages_domains.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class PagesDomains < Grape::API
+ class PagesDomains < Grape::API::Instance
include PaginationParams
PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX)
diff --git a/lib/api/pagination_params.rb b/lib/api/pagination_params.rb
index ae03595eb25..a232b58d3f7 100644
--- a/lib/api/pagination_params.rb
+++ b/lib/api/pagination_params.rb
@@ -4,7 +4,7 @@ module API
# Concern for declare pagination params.
#
# @example
- # class CustomApiResource < Grape::API
+ # class CustomApiResource < Grape::API::Instance
# include PaginationParams
#
# params do
diff --git a/lib/api/pipeline_schedules.rb b/lib/api/pipeline_schedules.rb
deleted file mode 100644
index edc99590cdb..00000000000
--- a/lib/api/pipeline_schedules.rb
+++ /dev/null
@@ -1,215 +0,0 @@
-# frozen_string_literal: true
-
-module API
- class PipelineSchedules < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- desc 'Get all pipeline schedules' do
- success Entities::PipelineSchedule
- end
- params do
- use :pagination
- optional :scope, type: String, values: %w[active inactive],
- desc: 'The scope of pipeline schedules'
- end
- # rubocop: disable CodeReuse/ActiveRecord
- get ':id/pipeline_schedules' do
- authorize! :read_pipeline_schedule, user_project
-
- schedules = Ci::PipelineSchedulesFinder.new(user_project).execute(scope: params[:scope])
- .preload([:owner, :last_pipeline])
- present paginate(schedules), with: Entities::PipelineSchedule
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- desc 'Get a single pipeline schedule' do
- success Entities::PipelineScheduleDetails
- end
- params do
- requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
- end
- get ':id/pipeline_schedules/:pipeline_schedule_id' do
- present pipeline_schedule, with: Entities::PipelineScheduleDetails
- end
-
- desc 'Create a new pipeline schedule' do
- success Entities::PipelineScheduleDetails
- end
- params do
- requires :description, type: String, desc: 'The description of pipeline schedule'
- requires :ref, type: String, desc: 'The branch/tag name will be triggered', allow_blank: false
- requires :cron, type: String, desc: 'The cron'
- optional :cron_timezone, type: String, default: 'UTC', desc: 'The timezone'
- optional :active, type: Boolean, default: true, desc: 'The activation of pipeline schedule'
- end
- post ':id/pipeline_schedules' do
- authorize! :create_pipeline_schedule, user_project
-
- pipeline_schedule = Ci::CreatePipelineScheduleService
- .new(user_project, current_user, declared_params(include_missing: false))
- .execute
-
- if pipeline_schedule.persisted?
- present pipeline_schedule, with: Entities::PipelineScheduleDetails
- else
- render_validation_error!(pipeline_schedule)
- end
- end
-
- desc 'Edit a pipeline schedule' do
- success Entities::PipelineScheduleDetails
- end
- params do
- requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
- optional :description, type: String, desc: 'The description of pipeline schedule'
- optional :ref, type: String, desc: 'The branch/tag name will be triggered'
- optional :cron, type: String, desc: 'The cron'
- optional :cron_timezone, type: String, desc: 'The timezone'
- optional :active, type: Boolean, desc: 'The activation of pipeline schedule'
- end
- put ':id/pipeline_schedules/:pipeline_schedule_id' do
- authorize! :update_pipeline_schedule, pipeline_schedule
-
- if pipeline_schedule.update(declared_params(include_missing: false))
- present pipeline_schedule, with: Entities::PipelineScheduleDetails
- else
- render_validation_error!(pipeline_schedule)
- end
- end
-
- desc 'Take ownership of a pipeline schedule' do
- success Entities::PipelineScheduleDetails
- end
- params do
- requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
- end
- post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
- authorize! :update_pipeline_schedule, pipeline_schedule
-
- if pipeline_schedule.own!(current_user)
- present pipeline_schedule, with: Entities::PipelineScheduleDetails
- else
- render_validation_error!(pipeline_schedule)
- end
- end
-
- desc 'Delete a pipeline schedule' do
- success Entities::PipelineScheduleDetails
- end
- params do
- requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
- end
- delete ':id/pipeline_schedules/:pipeline_schedule_id' do
- authorize! :admin_pipeline_schedule, pipeline_schedule
-
- destroy_conditionally!(pipeline_schedule)
- end
-
- desc 'Play a scheduled pipeline immediately' do
- detail 'This feature was added in GitLab 12.8'
- end
- params do
- requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
- end
- post ':id/pipeline_schedules/:pipeline_schedule_id/play' do
- authorize! :play_pipeline_schedule, pipeline_schedule
-
- job_id = RunPipelineScheduleWorker # rubocop:disable CodeReuse/Worker
- .perform_async(pipeline_schedule.id, current_user.id)
-
- if job_id
- created!
- else
- render_api_error!('Unable to schedule pipeline run immediately', 500)
- end
- end
-
- desc 'Create a new pipeline schedule variable' do
- success Entities::Variable
- end
- params do
- requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
- requires :key, type: String, desc: 'The key of the variable'
- requires :value, type: String, desc: 'The value of the variable'
- optional :variable_type, type: String, values: Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
- end
- post ':id/pipeline_schedules/:pipeline_schedule_id/variables' do
- authorize! :update_pipeline_schedule, pipeline_schedule
-
- variable_params = declared_params(include_missing: false)
- variable = pipeline_schedule.variables.create(variable_params)
- if variable.persisted?
- present variable, with: Entities::Variable
- else
- render_validation_error!(variable)
- end
- end
-
- desc 'Edit a pipeline schedule variable' do
- success Entities::Variable
- end
- params do
- requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
- requires :key, type: String, desc: 'The key of the variable'
- optional :value, type: String, desc: 'The value of the variable'
- optional :variable_type, type: String, values: Ci::PipelineScheduleVariable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
- end
- put ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
- authorize! :update_pipeline_schedule, pipeline_schedule
-
- if pipeline_schedule_variable.update(declared_params(include_missing: false))
- present pipeline_schedule_variable, with: Entities::Variable
- else
- render_validation_error!(pipeline_schedule_variable)
- end
- end
-
- desc 'Delete a pipeline schedule variable' do
- success Entities::Variable
- end
- params do
- requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
- requires :key, type: String, desc: 'The key of the variable'
- end
- delete ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
- authorize! :admin_pipeline_schedule, pipeline_schedule
-
- status :accepted
- present pipeline_schedule_variable.destroy, with: Entities::Variable
- end
- end
-
- helpers do
- # rubocop: disable CodeReuse/ActiveRecord
- def pipeline_schedule
- @pipeline_schedule ||=
- user_project
- .pipeline_schedules
- .preload(:owner, :last_pipeline)
- .find_by(id: params.delete(:pipeline_schedule_id)).tap do |pipeline_schedule|
- unless can?(current_user, :read_pipeline_schedule, pipeline_schedule)
- not_found!('Pipeline Schedule')
- end
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # rubocop: disable CodeReuse/ActiveRecord
- def pipeline_schedule_variable
- @pipeline_schedule_variable ||=
- pipeline_schedule.variables.find_by(key: params[:key]).tap do |pipeline_schedule_variable|
- unless pipeline_schedule_variable
- not_found!('Pipeline Schedule Variable')
- end
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
- end
-end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
deleted file mode 100644
index c09bca26a41..00000000000
--- a/lib/api/pipelines.rb
+++ /dev/null
@@ -1,187 +0,0 @@
-# frozen_string_literal: true
-
-module API
- class Pipelines < Grape::API
- include PaginationParams
-
- before { authenticate_non_get! }
-
- params do
- requires :id, type: String, desc: 'The project ID'
- end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- desc 'Get all Pipelines of the project' do
- detail 'This feature was introduced in GitLab 8.11.'
- success Entities::PipelineBasic
- end
- params do
- use :pagination
- optional :scope, type: String, values: %w[running pending finished branches tags],
- desc: 'The scope of pipelines'
- optional :status, type: String, values: HasStatus::AVAILABLE_STATUSES,
- desc: 'The status of pipelines'
- optional :ref, type: String, desc: 'The ref of pipelines'
- optional :sha, type: String, desc: 'The sha of pipelines'
- optional :yaml_errors, type: Boolean, desc: 'Returns pipelines with invalid configurations'
- optional :name, type: String, desc: 'The name of the user who triggered pipelines'
- optional :username, type: String, desc: 'The username of the user who triggered pipelines'
- optional :updated_before, type: DateTime, desc: 'Return pipelines updated before the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
- optional :updated_after, type: DateTime, desc: 'Return pipelines updated after the specified datetime. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
- optional :order_by, type: String, values: Ci::PipelinesFinder::ALLOWED_INDEXED_COLUMNS, default: 'id',
- desc: 'Order pipelines'
- optional :sort, type: String, values: %w[asc desc], default: 'desc',
- desc: 'Sort pipelines'
- end
- get ':id/pipelines' do
- authorize! :read_pipeline, user_project
- authorize! :read_build, user_project
-
- pipelines = Ci::PipelinesFinder.new(user_project, current_user, params).execute
- present paginate(pipelines), with: Entities::PipelineBasic
- end
-
- desc 'Create a new pipeline' do
- detail 'This feature was introduced in GitLab 8.14'
- success Entities::Pipeline
- end
- params do
- requires :ref, type: String, desc: 'Reference'
- optional :variables, Array, desc: 'Array of variables available in the pipeline'
- end
- post ':id/pipeline' do
- Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42124')
-
- authorize! :create_pipeline, user_project
-
- pipeline_params = declared_params(include_missing: false)
- .merge(variables_attributes: params[:variables])
- .except(:variables)
-
- new_pipeline = Ci::CreatePipelineService.new(user_project,
- current_user,
- pipeline_params)
- .execute(:api, ignore_skip_ci: true, save_on_errors: false)
-
- if new_pipeline.persisted?
- present new_pipeline, with: Entities::Pipeline
- else
- render_validation_error!(new_pipeline)
- end
- end
-
- desc 'Gets a the latest pipeline for the project branch' do
- detail 'This feature was introduced in GitLab 12.3'
- success Entities::Pipeline
- end
- params do
- optional :ref, type: String, desc: 'branch ref of pipeline'
- end
- get ':id/pipelines/latest' do
- authorize! :read_pipeline, latest_pipeline
-
- present latest_pipeline, with: Entities::Pipeline
- end
-
- desc 'Gets a specific pipeline for the project' do
- detail 'This feature was introduced in GitLab 8.11'
- success Entities::Pipeline
- end
- params do
- requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
- end
- get ':id/pipelines/:pipeline_id' do
- authorize! :read_pipeline, pipeline
-
- present pipeline, with: Entities::Pipeline
- end
-
- desc 'Gets the variables for a given pipeline' do
- detail 'This feature was introduced in GitLab 11.11'
- success Entities::Variable
- end
- params do
- requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
- end
- get ':id/pipelines/:pipeline_id/variables' do
- authorize! :read_pipeline_variable, pipeline
-
- present pipeline.variables, with: Entities::Variable
- end
-
- desc 'Gets the test report for a given pipeline' do
- detail 'This feature was introduced in GitLab 13.0. Disabled by default behind feature flag `junit_pipeline_view`'
- success TestReportEntity
- end
- params do
- requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
- end
- get ':id/pipelines/:pipeline_id/test_report' do
- not_found! unless Feature.enabled?(:junit_pipeline_view, user_project)
-
- authorize! :read_build, pipeline
-
- present pipeline.test_reports, with: TestReportEntity
- end
-
- desc 'Deletes a pipeline' do
- detail 'This feature was introduced in GitLab 11.6'
- http_codes [[204, 'Pipeline was deleted'], [403, 'Forbidden']]
- end
- params do
- requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
- end
- delete ':id/pipelines/:pipeline_id' do
- authorize! :destroy_pipeline, pipeline
-
- destroy_conditionally!(pipeline) do
- ::Ci::DestroyPipelineService.new(user_project, current_user).execute(pipeline)
- end
- end
-
- desc 'Retry builds in the pipeline' do
- detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Pipeline
- end
- params do
- requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
- end
- post ':id/pipelines/:pipeline_id/retry' do
- authorize! :update_pipeline, pipeline
-
- pipeline.retry_failed(current_user)
-
- present pipeline, with: Entities::Pipeline
- end
-
- desc 'Cancel all builds in the pipeline' do
- detail 'This feature was introduced in GitLab 8.11.'
- success Entities::Pipeline
- end
- params do
- requires :pipeline_id, type: Integer, desc: 'The pipeline ID'
- end
- post ':id/pipelines/:pipeline_id/cancel' do
- authorize! :update_pipeline, pipeline
-
- pipeline.cancel_running
-
- status 200
- present pipeline.reset, with: Entities::Pipeline
- end
- end
-
- helpers do
- def pipeline
- strong_memoize(:pipeline) do
- user_project.ci_pipelines.find(params[:pipeline_id])
- end
- end
-
- def latest_pipeline
- strong_memoize(:latest_pipeline) do
- user_project.latest_pipeline_for_ref(params[:ref])
- end
- end
- end
- end
-end
diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb
index 299301aabc4..0e5605984e6 100644
--- a/lib/api/project_clusters.rb
+++ b/lib/api/project_clusters.rb
@@ -1,23 +1,11 @@
# frozen_string_literal: true
module API
- class ProjectClusters < Grape::API
+ class ProjectClusters < Grape::API::Instance
include PaginationParams
before { authenticate! }
- # EE::API::ProjectClusters will
- # override these methods
- helpers do
- params :create_params_ee do
- end
-
- params :update_params_ee do
- end
- end
-
- prepend_if_ee('EE::API::ProjectClusters') # rubocop: disable Cop/InjectEnterpriseEditionModule
-
params do
requires :id, type: String, desc: 'The ID of the project'
end
@@ -56,6 +44,7 @@ module API
requires :name, type: String, desc: 'Cluster name'
optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
@@ -65,7 +54,6 @@ module API
optional :namespace, type: String, desc: 'Unique namespace related to Project'
optional :authorization_type, type: String, values: ::Clusters::Platforms::Kubernetes.authorization_types.keys, default: 'rbac', desc: 'Cluster authorization type, defaults to RBAC'
end
- use :create_params_ee
end
post ':id/clusters/user' do
authorize! :add_cluster, user_project
@@ -89,6 +77,7 @@ module API
requires :cluster_id, type: Integer, desc: 'The cluster ID'
optional :name, type: String, desc: 'Cluster name'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :environment_scope, type: String, desc: 'The associated environment to the cluster'
optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
optional :api_url, type: String, desc: 'URL to access the Kubernetes API'
@@ -96,7 +85,6 @@ module API
optional :ca_cert, type: String, desc: 'TLS certificate (needed if API is using a self-signed TLS certificate)'
optional :namespace, type: String, desc: 'Unique namespace related to Project'
end
- use :update_params_ee
end
put ':id/clusters/:cluster_id' do
authorize! :update_cluster, cluster
diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb
index 2a0099018d9..8f2a62bc5a4 100644
--- a/lib/api/project_container_repositories.rb
+++ b/lib/api/project_container_repositories.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectContainerRepositories < Grape::API
+ class ProjectContainerRepositories < Grape::API::Instance
include PaginationParams
REPOSITORY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(
diff --git a/lib/api/project_events.rb b/lib/api/project_events.rb
index 734311e1142..726e693826e 100644
--- a/lib/api/project_events.rb
+++ b/lib/api/project_events.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectEvents < Grape::API
+ class ProjectEvents < Grape::API::Instance
include PaginationParams
include APIGuard
helpers ::API::Helpers::EventsHelpers
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index 4b35f245b8c..d11c47f8d78 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectExport < Grape::API
+ class ProjectExport < Grape::API::Instance
helpers Helpers::RateLimiter
before do
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 0e7576c9243..7cea44e6304 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectHooks < Grape::API
+ class ProjectHooks < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb
index 17d08d14a20..9f43c3c7993 100644
--- a/lib/api/project_import.rb
+++ b/lib/api/project_import.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectImport < Grape::API
+ class ProjectImport < Grape::API::Instance
include PaginationParams
MAXIMUM_FILE_SIZE = 50.megabytes
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
index 8643854a655..2f8dd1085dc 100644
--- a/lib/api/project_milestones.rb
+++ b/lib/api/project_milestones.rb
@@ -1,13 +1,11 @@
# frozen_string_literal: true
module API
- class ProjectMilestones < Grape::API
+ class ProjectMilestones < Grape::API::Instance
include PaginationParams
include MilestoneResponses
- before do
- authenticate!
- end
+ before { authenticate! }
params do
requires :id, type: String, desc: 'The ID of a project'
diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb
new file mode 100644
index 00000000000..359514f1f78
--- /dev/null
+++ b/lib/api/project_packages.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module API
+ class ProjectPackages < Grape::API::Instance
+ include PaginationParams
+
+ before do
+ authorize_packages_access!(user_project)
+ end
+
+ helpers ::API::Helpers::PackagesHelpers
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get all project packages' do
+ detail 'This feature was introduced in GitLab 11.8'
+ success ::API::Entities::Package
+ end
+ params do
+ use :pagination
+ optional :order_by, type: String, values: %w[created_at name version type], default: 'created_at',
+ desc: 'Return packages ordered by `created_at`, `name`, `version` or `type` fields.'
+ optional :sort, type: String, values: %w[asc desc], default: 'asc',
+ desc: 'Return packages sorted in `asc` or `desc` order.'
+ optional :package_type, type: String, values: Packages::Package.package_types.keys,
+ desc: 'Return packages of a certain type'
+ optional :package_name, type: String,
+ desc: 'Return packages with this name'
+ end
+ get ':id/packages' do
+ packages = ::Packages::PackagesFinder.new(
+ user_project,
+ declared_params.slice(:order_by, :sort, :package_type, :package_name)
+ ).execute
+
+ present paginate(packages), with: ::API::Entities::Package, user: current_user
+ end
+
+ desc 'Get a single project package' do
+ detail 'This feature was introduced in GitLab 11.9'
+ success ::API::Entities::Package
+ end
+ params do
+ requires :package_id, type: Integer, desc: 'The ID of a package'
+ end
+ get ':id/packages/:package_id' do
+ package = ::Packages::PackageFinder
+ .new(user_project, params[:package_id]).execute
+
+ present package, with: ::API::Entities::Package, user: current_user
+ end
+
+ desc 'Remove a package' do
+ detail 'This feature was introduced in GitLab 11.9'
+ end
+ params do
+ requires :package_id, type: Integer, desc: 'The ID of a package'
+ end
+ delete ':id/packages/:package_id' do
+ authorize_destroy_package!(user_project)
+
+ package = ::Packages::PackageFinder
+ .new(user_project, params[:package_id]).execute
+
+ destroy_conditionally!(package)
+ end
+ end
+ end
+end
diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb
index 5de623102fb..c318907542b 100644
--- a/lib/api/project_repository_storage_moves.rb
+++ b/lib/api/project_repository_storage_moves.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectRepositoryStorageMoves < Grape::API
+ class ProjectRepositoryStorageMoves < Grape::API::Instance
include PaginationParams
before { authenticated_as_admin! }
diff --git a/lib/api/project_snapshots.rb b/lib/api/project_snapshots.rb
index 175fbb2ce92..360000861fc 100644
--- a/lib/api/project_snapshots.rb
+++ b/lib/api/project_snapshots.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectSnapshots < Grape::API
+ class ProjectSnapshots < Grape::API::Instance
helpers ::API::Helpers::ProjectSnapshotsHelpers
before { authorize_read_git_snapshot! }
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 68f4a0dcb65..09934502e85 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectSnippets < Grape::API
+ class ProjectSnippets < Grape::API::Instance
include PaginationParams
before { authenticate! }
@@ -37,7 +37,7 @@ module API
use :pagination
end
get ":id/snippets" do
- present paginate(snippets_for_current_user), with: Entities::ProjectSnippet
+ present paginate(snippets_for_current_user), with: Entities::ProjectSnippet, current_user: current_user
end
desc 'Get a single project snippet' do
@@ -48,7 +48,7 @@ module API
end
get ":id/snippets/:snippet_id" do
snippet = snippets_for_current_user.find(params[:snippet_id])
- present snippet, with: Entities::ProjectSnippet
+ present snippet, with: Entities::ProjectSnippet, current_user: current_user
end
desc 'Create a new project snippet' do
@@ -71,7 +71,7 @@ module API
snippet = service_response.payload[:snippet]
if service_response.success?
- present snippet, with: Entities::ProjectSnippet
+ present snippet, with: Entities::ProjectSnippet, current_user: current_user
else
render_spam_error! if snippet.spam?
@@ -107,7 +107,7 @@ module API
snippet = service_response.payload[:snippet]
if service_response.success?
- present snippet, with: Entities::ProjectSnippet
+ present snippet, with: Entities::ProjectSnippet, current_user: current_user
else
render_spam_error! if snippet.spam?
@@ -147,10 +147,19 @@ module API
snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
not_found!('Snippet') unless snippet
- env['api.format'] = :txt
- content_type 'text/plain'
present content_for(snippet)
end
+
+ desc 'Get raw project snippet file contents from the repository'
+ params do
+ use :raw_file_params
+ end
+ get ":id/snippets/:snippet_id/files/:ref/:file_path/raw", requirements: { file_path: API::NO_SLASH_URL_PART_REGEX } do
+ snippet = snippets_for_current_user.find_by(id: params[:snippet_id])
+ not_found!('Snippet') unless snippet&.repo_exists?
+
+ present file_content_for(snippet)
+ end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Get the user agent details for a project snippet' do
diff --git a/lib/api/project_statistics.rb b/lib/api/project_statistics.rb
index 14ee0f75513..2196801096f 100644
--- a/lib/api/project_statistics.rb
+++ b/lib/api/project_statistics.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectStatistics < Grape::API
+ class ProjectStatistics < Grape::API::Instance
before do
authenticate!
authorize! :daily_statistics, user_project
diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb
index cfcc7f5212d..f0fe4d85c8f 100644
--- a/lib/api/project_templates.rb
+++ b/lib/api/project_templates.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProjectTemplates < Grape::API
+ class ProjectTemplates < Grape::API::Instance
include PaginationParams
TEMPLATE_TYPES = %w[dockerfiles gitignores gitlab_ci_ymls licenses].freeze
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index e00fb61f478..d24dab63bd9 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -3,7 +3,7 @@
require_dependency 'declarative_policy'
module API
- class Projects < Grape::API
+ class Projects < Grape::API::Instance
include PaginationParams
include Helpers::CustomAttributes
@@ -17,6 +17,7 @@ module API
projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics]
+ projects = projects.joins(:statistics) if params[:order_by].include?('project_statistics') # rubocop: disable CodeReuse/ActiveRecord
lang = params[:with_programming_language]
projects = projects.with_programming_language(lang) if lang
@@ -28,6 +29,20 @@ module API
attrs.delete(:repository_storage) unless can?(current_user, :change_repository_storage, project)
end
+ def verify_project_filters!(attrs)
+ attrs.delete(:repository_storage) unless can?(current_user, :use_project_statistics_filters)
+ end
+
+ def verify_statistics_order_by_projects!
+ return unless Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS.include?(params[:order_by])
+
+ params[:order_by] = if can?(current_user, :use_project_statistics_filters)
+ "project_statistics.#{params[:order_by]}"
+ else
+ route.params['order_by'][:default]
+ end
+ end
+
def delete_project(user_project)
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
@@ -52,8 +67,9 @@ module API
end
params :sort_params do
- optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at],
- default: 'created_at', desc: 'Return projects ordered by field'
+ optional :order_by, type: String,
+ values: %w[id name path created_at updated_at last_activity_at] + Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS,
+ default: 'created_at', desc: "Return projects ordered by field. #{Helpers::ProjectsHelpers::STATISTICS_SORT_PARAMS.join(', ')} are only available to admins."
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return projects sorted in ascending and descending order'
end
@@ -75,6 +91,7 @@ module API
optional :id_before, type: Integer, desc: 'Limit results to projects with IDs less than the specified ID'
optional :last_activity_after, type: DateTime, desc: 'Limit results to projects with last_activity after specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
optional :last_activity_before, type: DateTime, desc: 'Limit results to projects with last_activity before specified time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ'
+ optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins'
use :optional_filter_params_ee
end
@@ -88,10 +105,15 @@ module API
end
def load_projects
- ProjectsFinder.new(current_user: current_user, params: project_finder_params).execute
+ params = project_finder_params
+ verify_project_filters!(params)
+
+ ProjectsFinder.new(current_user: current_user, params: params).execute
end
def present_projects(projects, options = {})
+ verify_statistics_order_by_projects!
+
projects = reorder_projects(projects)
projects = apply_filters(projects)
@@ -524,7 +546,7 @@ module API
end
params do
optional :search, type: String, desc: 'Return list of users matching the search criteria'
- optional :skip_users, type: Array[Integer], desc: 'Filter out users with the specified IDs'
+ optional :skip_users, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Filter out users with the specified IDs'
use :pagination
end
get ':id/users' do
diff --git a/lib/api/projects_relation_builder.rb b/lib/api/projects_relation_builder.rb
index 263468c9aa6..6dfd82d109f 100644
--- a/lib/api/projects_relation_builder.rb
+++ b/lib/api/projects_relation_builder.rb
@@ -8,6 +8,10 @@ module API
def prepare_relation(projects_relation, options = {})
projects_relation = preload_relation(projects_relation, options)
execute_batch_counting(projects_relation)
+ # Call the forks count method on every project, so the BatchLoader would load them all at
+ # once when the entities are rendered
+ projects_relation.each(&:forks_count)
+
projects_relation
end
@@ -19,16 +23,11 @@ module API
projects_relation
end
- def batch_forks_counting(projects_relation)
- ::Projects::BatchForksCountService.new(forks_counting_projects(projects_relation)).refresh_cache
- end
-
def batch_open_issues_counting(projects_relation)
::Projects::BatchOpenIssuesCountService.new(projects_relation).refresh_cache
end
def execute_batch_counting(projects_relation)
- batch_forks_counting(projects_relation)
batch_open_issues_counting(projects_relation)
end
end
diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb
index 1fd86d1e720..b0a7f898eec 100644
--- a/lib/api/protected_branches.rb
+++ b/lib/api/protected_branches.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProtectedBranches < Grape::API
+ class ProtectedBranches < Grape::API::Instance
include PaginationParams
BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
diff --git a/lib/api/protected_tags.rb b/lib/api/protected_tags.rb
index ee13473c848..aaa31cb7cc6 100644
--- a/lib/api/protected_tags.rb
+++ b/lib/api/protected_tags.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ProtectedTags < Grape::API
+ class ProtectedTags < Grape::API::Instance
include PaginationParams
TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(name: API::NO_SLASH_URL_PART_REGEX)
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
new file mode 100644
index 00000000000..a6caacd7df8
--- /dev/null
+++ b/lib/api/pypi_packages.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+# PyPI Package Manager Client API
+#
+# These API endpoints are not meant to be consumed directly by users. They are
+# called by the PyPI package manager client when users run commands
+# like `pip install` or `twine upload`.
+module API
+ class PypiPackages < Grape::API::Instance
+ helpers ::API::Helpers::PackagesManagerClientsHelpers
+ helpers ::API::Helpers::RelatedResourcesHelpers
+ helpers ::API::Helpers::Packages::BasicAuthHelpers
+ include ::API::Helpers::Packages::BasicAuthHelpers::Constants
+
+ default_format :json
+
+ rescue_from ArgumentError do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ rescue_from ActiveRecord::RecordInvalid do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ rescue_from ActiveRecord::RecordInvalid do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ helpers do
+ def packages_finder(project = authorized_user_project)
+ project
+ .packages
+ .pypi
+ .has_version
+ .processed
+ end
+
+ def find_package_versions
+ packages = packages_finder
+ .with_name(params[:package_name])
+
+ not_found!('Package') if packages.empty?
+
+ packages
+ end
+ end
+
+ before do
+ require_packages_enabled!
+ end
+
+ params do
+ requires :id, type: Integer, desc: 'The ID of a project'
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ before do
+ unauthorized_user_project!
+ end
+
+ namespace ':id/packages/pypi' do
+ desc 'The PyPi package download endpoint' do
+ detail 'This feature was introduced in GitLab 12.10'
+ end
+
+ params do
+ requires :file_identifier, type: String, desc: 'The PyPi package file identifier', file_path: true
+ requires :sha256, type: String, desc: 'The PyPi package sha256 check sum'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true
+ get 'files/:sha256/*file_identifier' do
+ project = unauthorized_user_project!
+
+ filename = "#{params[:file_identifier]}.#{params[:format]}"
+ package = packages_finder(project).by_file_name_and_sha256(filename, params[:sha256])
+ package_file = ::Packages::PackageFileFinder.new(package, filename, with_file_name_like: false).execute
+
+ track_event('pull_package')
+
+ present_carrierwave_file!(package_file.file, supports_direct_download: true)
+ end
+
+ desc 'The PyPi Simple Endpoint' do
+ detail 'This feature was introduced in GitLab 12.10'
+ end
+
+ params do
+ requires :package_name, type: String, file_path: true, desc: 'The PyPi package name'
+ end
+
+ # An Api entry point but returns an HTML file instead of JSON.
+ # PyPi simple API returns the package descriptor as a simple HTML file.
+ route_setting :authentication, deploy_token_allowed: true
+ get 'simple/*package_name', format: :txt do
+ authorize_read_package!(authorized_user_project)
+
+ track_event('list_package')
+
+ packages = find_package_versions
+ presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project)
+
+ # Adjusts grape output format
+ # to be HTML
+ content_type "text/html; charset=utf-8"
+ env['api.format'] = :binary
+
+ body presenter.body
+ end
+
+ desc 'The PyPi Package upload endpoint' do
+ detail 'This feature was introduced in GitLab 12.10'
+ end
+
+ params do
+ requires :content, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
+ requires :requires_python, type: String
+ requires :name, type: String
+ requires :version, type: String
+ optional :md5_digest, type: String
+ optional :sha256_digest, type: String
+ end
+
+ route_setting :authentication, deploy_token_allowed: true
+ post do
+ authorize_upload!(authorized_user_project)
+
+ track_event('push_package')
+
+ ::Packages::Pypi::CreatePackageService
+ .new(authorized_user_project, current_user, declared_params)
+ .execute
+
+ created!
+ rescue ObjectStorage::RemoteStoreError => e
+ Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:name], project_id: authorized_user_project.id })
+
+ forbidden!
+ end
+
+ route_setting :authentication, deploy_token_allowed: true
+ post 'authorize' do
+ authorize_workhorse!(subject: authorized_user_project, has_length: false)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb
index 07c27f39539..7e1815480a5 100644
--- a/lib/api/release/links.rb
+++ b/lib/api/release/links.rb
@@ -2,7 +2,7 @@
module API
module Release
- class Links < Grape::API
+ class Links < Grape::API::Instance
include PaginationParams
RELEASE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index a5bb1a44f1f..30c5e06053e 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Releases < Grape::API
+ class Releases < Grape::API::Instance
include PaginationParams
RELEASE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS
@@ -54,7 +54,7 @@ module API
requires :url, type: String
end
end
- optional :milestones, type: Array, desc: 'The titles of the related milestones', default: []
+ optional :milestones, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The titles of the related milestones', default: []
optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready. Defaults to the current time.'
end
route_setting :authentication, job_token_allowed: true
diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb
index 0808541d3c7..d1def05808b 100644
--- a/lib/api/remote_mirrors.rb
+++ b/lib/api/remote_mirrors.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class RemoteMirrors < Grape::API
+ class RemoteMirrors < Grape::API::Instance
include PaginationParams
before do
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index bf4f08ce390..81702f8f02a 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -3,7 +3,7 @@
require 'mime/types'
module API
- class Repositories < Grape::API
+ class Repositories < Grape::API::Instance
include PaginationParams
helpers ::API::Helpers::HeadersHelpers
@@ -143,7 +143,7 @@ module API
success Entities::Commit
end
params do
- requires :refs, type: Array[String]
+ requires :refs, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce
end
get ':id/repository/merge_base' do
refs = params[:refs]
diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb
index 1fa6898b92c..a8d3419528c 100644
--- a/lib/api/resource_label_events.rb
+++ b/lib/api/resource_label_events.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ResourceLabelEvents < Grape::API
+ class ResourceLabelEvents < Grape::API::Instance
include PaginationParams
helpers ::API::Helpers::NotesHelpers
diff --git a/lib/api/resource_milestone_events.rb b/lib/api/resource_milestone_events.rb
index 30ff5a9b4be..a8f221f8740 100644
--- a/lib/api/resource_milestone_events.rb
+++ b/lib/api/resource_milestone_events.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class ResourceMilestoneEvents < Grape::API
+ class ResourceMilestoneEvents < Grape::API::Instance
include PaginationParams
helpers ::API::Helpers::NotesHelpers
@@ -26,8 +26,7 @@ module API
get ":id/#{eventables_str}/:eventable_id/resource_milestone_events" do
eventable = find_noteable(eventable_type, params[:eventable_id])
- opts = { page: params[:page], per_page: params[:per_page] }
- events = ResourceMilestoneEventFinder.new(current_user, eventable, opts).execute
+ events = ResourceMilestoneEventFinder.new(current_user, eventable).execute
present paginate(events), with: Entities::ResourceMilestoneEvent
end
diff --git a/lib/api/resource_state_events.rb b/lib/api/resource_state_events.rb
new file mode 100644
index 00000000000..1c1a90c09a3
--- /dev/null
+++ b/lib/api/resource_state_events.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module API
+ class ResourceStateEvents < Grape::API::Instance
+ include PaginationParams
+ helpers ::API::Helpers::NotesHelpers
+
+ before { authenticate! }
+
+ [Issue, MergeRequest].each do |eventable_class|
+ eventable_name = eventable_class.to_s.underscore
+
+ params do
+ requires :id, type: String, desc: "The ID of a project"
+ end
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc "Get a list of #{eventable_class.to_s.downcase} resource state events" do
+ success Entities::ResourceStateEvent
+ end
+ params do
+ requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}"
+ use :pagination
+ end
+
+ get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events" do
+ eventable = find_noteable(eventable_class, params[:eventable_iid])
+
+ 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
+ success Entities::ResourceStateEvent
+ end
+ params do
+ requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}"
+ 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" do
+ eventable = find_noteable(eventable_class, params[:eventable_iid])
+
+ event = ResourceStateEventFinder.new(current_user, eventable).find(params[:event_id])
+
+ present event, with: Entities::ResourceStateEvent
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
deleted file mode 100644
index 5f08ebe4a06..00000000000
--- a/lib/api/runner.rb
+++ /dev/null
@@ -1,297 +0,0 @@
-# frozen_string_literal: true
-
-module API
- class Runner < Grape::API
- helpers ::API::Helpers::Runner
-
- resource :runners do
- desc 'Registers a new Runner' do
- success Entities::RunnerRegistrationDetails
- http_codes [[201, 'Runner was created'], [403, 'Forbidden']]
- end
- params do
- requires :token, type: String, desc: 'Registration token'
- optional :description, type: String, desc: %q(Runner's description)
- optional :info, type: Hash, desc: %q(Runner's metadata)
- optional :active, type: Boolean, desc: 'Should Runner be active'
- optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
- optional :access_level, type: String, values: Ci::Runner.access_levels.keys,
- desc: 'The access_level of the runner'
- optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
- optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
- optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
- end
- post '/' do
- attributes = attributes_for_keys([:description, :active, :locked, :run_untagged, :tag_list, :access_level, :maximum_timeout])
- .merge(get_runner_details_from_request)
-
- attributes =
- if runner_registration_token_valid?
- # Create shared runner. Requires admin access
- attributes.merge(runner_type: :instance_type)
- elsif project = Project.find_by_runners_token(params[:token])
- # Create a specific runner for the project
- attributes.merge(runner_type: :project_type, projects: [project])
- elsif group = Group.find_by_runners_token(params[:token])
- # Create a specific runner for the group
- attributes.merge(runner_type: :group_type, groups: [group])
- else
- forbidden!
- end
-
- runner = Ci::Runner.create(attributes)
-
- if runner.persisted?
- present runner, with: Entities::RunnerRegistrationDetails
- else
- render_validation_error!(runner)
- end
- end
-
- desc 'Deletes a registered Runner' do
- http_codes [[204, 'Runner was deleted'], [403, 'Forbidden']]
- end
- params do
- requires :token, type: String, desc: %q(Runner's authentication token)
- end
- delete '/' do
- authenticate_runner!
-
- runner = Ci::Runner.find_by_token(params[:token])
-
- destroy_conditionally!(runner)
- end
-
- desc 'Validates authentication credentials' do
- http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']]
- end
- params do
- requires :token, type: String, desc: %q(Runner's authentication token)
- end
- post '/verify' do
- authenticate_runner!
- status 200
- end
- end
-
- resource :jobs do
- before do
- Gitlab::ApplicationContext.push(
- user: -> { current_job&.user },
- project: -> { current_job&.project }
- )
- end
-
- desc 'Request a job' do
- success Entities::JobRequest::Response
- http_codes [[201, 'Job was scheduled'],
- [204, 'No job for Runner'],
- [403, 'Forbidden']]
- end
- params do
- requires :token, type: String, desc: %q(Runner's authentication token)
- optional :last_update, type: String, desc: %q(Runner's queue last_update token)
- optional :info, type: Hash, desc: %q(Runner's metadata) do
- optional :name, type: String, desc: %q(Runner's name)
- optional :version, type: String, desc: %q(Runner's version)
- optional :revision, type: String, desc: %q(Runner's revision)
- optional :platform, type: String, desc: %q(Runner's platform)
- optional :architecture, type: String, desc: %q(Runner's architecture)
- optional :executor, type: String, desc: %q(Runner's executor)
- optional :features, type: Hash, desc: %q(Runner's features)
- end
- optional :session, type: Hash, desc: %q(Runner's session data) do
- optional :url, type: String, desc: %q(Session's url)
- optional :certificate, type: String, desc: %q(Session's certificate)
- optional :authorization, type: String, desc: %q(Session's authorization)
- end
- optional :job_age, type: Integer, desc: %q(Job should be older than passed age in seconds to be ran on runner)
- end
- post '/request' do
- authenticate_runner!
-
- unless current_runner.active?
- header 'X-GitLab-Last-Update', current_runner.ensure_runner_queue_value
- break no_content!
- end
-
- runner_params = declared_params(include_missing: false)
-
- if current_runner.runner_queue_value_latest?(runner_params[:last_update])
- header 'X-GitLab-Last-Update', runner_params[:last_update]
- Gitlab::Metrics.add_event(:build_not_found_cached)
- break no_content!
- end
-
- new_update = current_runner.ensure_runner_queue_value
- result = ::Ci::RegisterJobService.new(current_runner).execute(runner_params)
-
- if result.valid?
- if result.build
- Gitlab::Metrics.add_event(:build_found)
- present Ci::BuildRunnerPresenter.new(result.build), with: Entities::JobRequest::Response
- else
- Gitlab::Metrics.add_event(:build_not_found)
- header 'X-GitLab-Last-Update', new_update
- no_content!
- end
- else
- # We received build that is invalid due to concurrency conflict
- Gitlab::Metrics.add_event(:build_invalid)
- conflict!
- end
- end
-
- desc 'Updates a job' do
- http_codes [[200, 'Job was updated'], [403, 'Forbidden']]
- end
- params do
- requires :token, type: String, desc: %q(Runners's authentication token)
- requires :id, type: Integer, desc: %q(Job's ID)
- optional :trace, type: String, desc: %q(Job's full trace)
- optional :state, type: String, desc: %q(Job's status: success, failed)
- optional :failure_reason, type: String, desc: %q(Job's failure_reason)
- end
- put '/:id' do
- job = authenticate_job!
-
- job.trace.set(params[:trace]) if params[:trace]
-
- Gitlab::Metrics.add_event(:update_build)
-
- case params[:state].to_s
- when 'running'
- job.touch if job.needs_touch?
- when 'success'
- job.success!
- when 'failed'
- job.drop!(params[:failure_reason] || :unknown_failure)
- end
- end
-
- desc 'Appends a patch to the job trace' do
- http_codes [[202, 'Trace was patched'],
- [400, 'Missing Content-Range header'],
- [403, 'Forbidden'],
- [416, 'Range not satisfiable']]
- end
- params do
- requires :id, type: Integer, desc: %q(Job's ID)
- optional :token, type: String, desc: %q(Job's authentication token)
- end
- patch '/:id/trace' do
- job = authenticate_job!
-
- error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range')
- content_range = request.headers['Content-Range']
- content_range = content_range.split('-')
-
- # TODO:
- # it seems that `Content-Range` as formatted by runner is wrong,
- # the `byte_end` should point to final byte, but it points byte+1
- # that means that we have to calculate end of body,
- # as we cannot use `content_length[1]`
- # Issue: https://gitlab.com/gitlab-org/gitlab-runner/issues/3275
-
- body_data = request.body.read
- body_start = content_range[0].to_i
- body_end = body_start + body_data.bytesize
-
- stream_size = job.trace.append(body_data, body_start)
- unless stream_size == body_end
- break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{stream_size}" })
- end
-
- status 202
- header 'Job-Status', job.status
- header 'Range', "0-#{stream_size}"
- header 'X-GitLab-Trace-Update-Interval', job.trace.update_interval.to_s
- end
-
- desc 'Authorize artifacts uploading for job' do
- http_codes [[200, 'Upload allowed'],
- [403, 'Forbidden'],
- [405, 'Artifacts support not enabled'],
- [413, 'File too large']]
- end
- params do
- requires :id, type: Integer, desc: %q(Job's ID)
- optional :token, type: String, desc: %q(Job's authentication token)
- optional :filesize, type: Integer, desc: %q(Artifacts filesize)
- optional :artifact_type, type: String, desc: %q(The type of artifact),
- default: 'archive', values: Ci::JobArtifact.file_types.keys
- end
- post '/:id/artifacts/authorize' do
- not_allowed! unless Gitlab.config.artifacts.enabled
- require_gitlab_workhorse!
- Gitlab::Workhorse.verify_api_request!(headers)
-
- job = authenticate_job!
-
- service = Ci::AuthorizeJobArtifactService.new(job, params, max_size: max_artifacts_size(job))
-
- forbidden! if service.forbidden?
- file_too_large! if service.too_large?
-
- status 200
- content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
- service.headers
- end
-
- desc 'Upload artifacts for job' do
- success Entities::JobRequest::Response
- http_codes [[201, 'Artifact uploaded'],
- [400, 'Bad request'],
- [403, 'Forbidden'],
- [405, 'Artifacts support not enabled'],
- [413, 'File too large']]
- end
- params do
- requires :id, type: Integer, desc: %q(Job's ID)
- requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: %(The artifact file to store (generated by Multipart middleware))
- optional :token, type: String, desc: %q(Job's authentication token)
- optional :expire_in, type: String, desc: %q(Specify when artifacts should expire)
- optional :artifact_type, type: String, desc: %q(The type of artifact),
- default: 'archive', values: Ci::JobArtifact.file_types.keys
- optional :artifact_format, type: String, desc: %q(The format of artifact),
- default: 'zip', values: Ci::JobArtifact.file_formats.keys
- optional :metadata, type: ::API::Validations::Types::WorkhorseFile, desc: %(The artifact metadata to store (generated by Multipart middleware))
- end
- post '/:id/artifacts' do
- not_allowed! unless Gitlab.config.artifacts.enabled
- require_gitlab_workhorse!
-
- job = authenticate_job!
-
- artifacts = params[:file]
- metadata = params[:metadata]
-
- file_too_large! unless artifacts.size < max_artifacts_size(job)
-
- result = Ci::CreateJobArtifactsService.new(job.project).execute(job, artifacts, params, metadata_file: metadata)
-
- if result[:status] == :success
- status :created
- else
- render_api_error!(result[:message], result[:http_status])
- end
- end
-
- desc 'Download the artifacts file for job' do
- http_codes [[200, 'Upload allowed'],
- [403, 'Forbidden'],
- [404, 'Artifact not found']]
- end
- params do
- requires :id, type: Integer, desc: %q(Job's ID)
- optional :token, type: String, desc: %q(Job's authentication token)
- optional :direct_download, default: false, type: Boolean, desc: %q(Perform direct download from remote storage instead of proxying artifacts)
- end
- get '/:id/artifacts' do
- job = authenticate_job!(require_running: false)
-
- present_carrierwave_file!(job.artifacts_file, supports_direct_download: params[:direct_download])
- end
- end
- end
-end
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
deleted file mode 100644
index 43ee1dd1f71..00000000000
--- a/lib/api/runners.rb
+++ /dev/null
@@ -1,287 +0,0 @@
-# frozen_string_literal: true
-
-module API
- class Runners < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- resource :runners do
- desc 'Get runners available for user' do
- success Entities::Runner
- end
- params do
- optional :scope, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
- desc: 'The scope of specific runners to show'
- optional :type, type: String, values: Ci::Runner::AVAILABLE_TYPES,
- desc: 'The type of the runners to show'
- optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
- desc: 'The status of the runners to show'
- optional :tag_list, type: Array[String], desc: 'The tags of the runners to show'
- use :pagination
- end
- get do
- runners = current_user.ci_owned_runners
- runners = filter_runners(runners, params[:scope], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES)
- runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES)
- runners = filter_runners(runners, params[:status], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES)
- runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
-
- present paginate(runners), with: Entities::Runner
- end
-
- desc 'Get all runners - shared and specific' do
- success Entities::Runner
- end
- params do
- optional :scope, type: String, values: Ci::Runner::AVAILABLE_SCOPES,
- desc: 'The scope of specific runners to show'
- optional :type, type: String, values: Ci::Runner::AVAILABLE_TYPES,
- desc: 'The type of the runners to show'
- optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
- desc: 'The status of the runners to show'
- optional :tag_list, type: Array[String], desc: 'The tags of the runners to show'
- use :pagination
- end
- get 'all' do
- authenticated_as_admin!
-
- runners = Ci::Runner.all
- runners = filter_runners(runners, params[:scope])
- runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES)
- runners = filter_runners(runners, params[:status], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES)
- runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
-
- present paginate(runners), with: Entities::Runner
- end
-
- desc "Get runner's details" do
- success Entities::RunnerDetails
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the runner'
- end
- get ':id' do
- runner = get_runner(params[:id])
- authenticate_show_runner!(runner)
-
- present runner, with: Entities::RunnerDetails, current_user: current_user
- end
-
- desc "Update runner's details" do
- success Entities::RunnerDetails
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the runner'
- optional :description, type: String, desc: 'The description of the runner'
- optional :active, type: Boolean, desc: 'The state of a runner'
- optional :tag_list, type: Array[String], desc: 'The list of tags for a runner'
- optional :run_untagged, type: Boolean, desc: 'Flag indicating the runner can execute untagged jobs'
- optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
- optional :access_level, type: String, values: Ci::Runner.access_levels.keys,
- desc: 'The access_level of the runner'
- optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
- at_least_one_of :description, :active, :tag_list, :run_untagged, :locked, :access_level, :maximum_timeout
- end
- put ':id' do
- runner = get_runner(params.delete(:id))
- authenticate_update_runner!(runner)
- update_service = Ci::UpdateRunnerService.new(runner)
-
- if update_service.update(declared_params(include_missing: false))
- present runner, with: Entities::RunnerDetails, current_user: current_user
- else
- render_validation_error!(runner)
- end
- end
-
- desc 'Remove a runner' do
- success Entities::Runner
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the runner'
- end
- delete ':id' do
- runner = get_runner(params[:id])
-
- authenticate_delete_runner!(runner)
-
- destroy_conditionally!(runner)
- end
-
- desc 'List jobs running on a runner' do
- success Entities::JobBasicWithProject
- end
- params do
- requires :id, type: Integer, desc: 'The ID of the runner'
- optional :status, type: String, desc: 'Status of the job', values: Ci::Build::AVAILABLE_STATUSES
- optional :order_by, type: String, desc: 'Order by `id` or not', values: Ci::RunnerJobsFinder::ALLOWED_INDEXED_COLUMNS
- optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Sort by asc (ascending) or desc (descending)'
- use :pagination
- end
- get ':id/jobs' do
- runner = get_runner(params[:id])
- authenticate_list_runners_jobs!(runner)
-
- jobs = Ci::RunnerJobsFinder.new(runner, params).execute
-
- present paginate(jobs), with: Entities::JobBasicWithProject
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- before { authorize_admin_project }
-
- desc 'Get runners available for project' do
- success Entities::Runner
- end
- params do
- optional :scope, type: String, values: Ci::Runner::AVAILABLE_SCOPES,
- desc: 'The scope of specific runners to show'
- optional :type, type: String, values: Ci::Runner::AVAILABLE_TYPES,
- desc: 'The type of the runners to show'
- optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
- desc: 'The status of the runners to show'
- optional :tag_list, type: Array[String], desc: 'The tags of the runners to show'
- use :pagination
- end
- get ':id/runners' do
- runners = Ci::Runner.owned_or_instance_wide(user_project.id)
- # scope is deprecated (for project runners), however api documentation still supports it.
- # Not including them in `apply_filter` method as it's not supported for group runners
- runners = filter_runners(runners, params[:scope])
- runners = apply_filter(runners, params)
-
- present paginate(runners), with: Entities::Runner
- end
-
- desc 'Enable a runner for a project' do
- success Entities::Runner
- end
- params do
- requires :runner_id, type: Integer, desc: 'The ID of the runner'
- end
- post ':id/runners' do
- runner = get_runner(params[:runner_id])
- authenticate_enable_runner!(runner)
-
- if runner.assign_to(user_project)
- present runner, with: Entities::Runner
- else
- render_validation_error!(runner)
- end
- end
-
- desc "Disable project's runner" do
- success Entities::Runner
- end
- params do
- requires :runner_id, type: Integer, desc: 'The ID of the runner'
- end
- # rubocop: disable CodeReuse/ActiveRecord
- delete ':id/runners/:runner_id' do
- runner_project = user_project.runner_projects.find_by(runner_id: params[:runner_id])
- not_found!('Runner') unless runner_project
-
- runner = runner_project.runner
- forbidden!("Only one project associated with the runner. Please remove the runner instead") if runner.projects.count == 1
-
- destroy_conditionally!(runner_project)
- end
- # rubocop: enable CodeReuse/ActiveRecord
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a group'
- end
- resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- before { authorize_admin_group }
-
- desc 'Get runners available for group' do
- success Entities::Runner
- end
- params do
- optional :type, type: String, values: Ci::Runner::AVAILABLE_TYPES,
- desc: 'The type of the runners to show'
- optional :status, type: String, values: Ci::Runner::AVAILABLE_STATUSES,
- desc: 'The status of the runners to show'
- optional :tag_list, type: Array[String], desc: 'The tags of the runners to show'
- use :pagination
- end
- get ':id/runners' do
- runners = Ci::Runner.belonging_to_group(user_group.id, include_ancestors: true)
- runners = apply_filter(runners, params)
-
- present paginate(runners), with: Entities::Runner
- end
- end
-
- helpers do
- def filter_runners(runners, scope, allowed_scopes: ::Ci::Runner::AVAILABLE_SCOPES)
- return runners unless scope.present?
-
- unless allowed_scopes.include?(scope)
- render_api_error!('Scope contains invalid value', 400)
- end
-
- # Support deprecated scopes
- if runners.respond_to?("deprecated_#{scope}")
- scope = "deprecated_#{scope}"
- end
-
- runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def apply_filter(runners, params)
- runners = filter_runners(runners, params[:type], allowed_scopes: Ci::Runner::AVAILABLE_TYPES)
- runners = filter_runners(runners, params[:status], allowed_scopes: Ci::Runner::AVAILABLE_STATUSES)
- runners = runners.tagged_with(params[:tag_list]) if params[:tag_list]
-
- runners
- end
-
- def get_runner(id)
- runner = Ci::Runner.find(id)
- not_found!('Runner') unless runner
- runner
- end
-
- def authenticate_show_runner!(runner)
- return if runner.instance_type? || current_user.admin?
-
- forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
- end
-
- def authenticate_update_runner!(runner)
- return if current_user.admin?
-
- forbidden!("No access granted") unless can?(current_user, :update_runner, runner)
- end
-
- def authenticate_delete_runner!(runner)
- return if current_user.admin?
-
- forbidden!("Runner associated with more than one project") if runner.projects.count > 1
- forbidden!("No access granted") unless can?(current_user, :delete_runner, runner)
- end
-
- def authenticate_enable_runner!(runner)
- forbidden!("Runner is a group runner") if runner.group_type?
-
- return if current_user.admin?
-
- forbidden!("Runner is locked") if runner.locked?
- forbidden!("No access granted") unless can?(current_user, :assign_runner, runner)
- end
-
- def authenticate_list_runners_jobs!(runner)
- return if current_user.admin?
-
- forbidden!("No access granted") unless can?(current_user, :read_runner, runner)
- end
- end
- end
-end
diff --git a/lib/api/search.rb b/lib/api/search.rb
index ac00d3682a0..53095e0b81a 100644
--- a/lib/api/search.rb
+++ b/lib/api/search.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Search < Grape::API
+ class Search < Grape::API::Instance
include PaginationParams
before { authenticate! }
@@ -24,7 +24,8 @@ module API
merge_requests: :with_api_entity_associations,
projects: :with_api_entity_associations,
issues: :with_api_entity_associations,
- milestones: :with_api_entity_associations
+ milestones: :with_api_entity_associations,
+ commits: :with_api_commit_entity_associations
}.freeze
def search(additional_params = {})
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 5fd5c6bd9b0..9ee1822339c 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
module API
- class Services < Grape::API
+ class Services < Grape::API::Instance
services = Helpers::ServicesHelpers.services
service_classes = Helpers::ServicesHelpers.service_classes
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index 0bf5eed26b4..3463e29041b 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Settings < Grape::API
+ class Settings < Grape::API::Instance
before { authenticated_as_admin! }
helpers Helpers::SettingsHelpers
@@ -49,7 +49,7 @@ module API
optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects'
optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
- optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
+ optional :disabled_oauth_sign_in_sources, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Disable certain OAuth sign-in sources'
optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups'
optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com'
@@ -79,7 +79,8 @@ module API
requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run."
end
optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.'
- optional :import_sources, type: Array[String], values: %w[github bitbucket bitbucket_server gitlab google_code fogbugz git gitlab_project gitea manifest phabricator],
+ optional :import_sources, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce,
+ values: %w[github bitbucket bitbucket_server gitlab google_code fogbugz git gitlab_project gitea manifest phabricator],
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size for each job's artifacts"
optional :max_attachment_size, type: Integer, desc: 'Maximum attachment size in MB'
@@ -113,13 +114,13 @@ module API
requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha'
end
optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues."
- optional :repository_storages, type: Array[String], desc: 'Storage paths for new projects'
+ optional :repository_storages, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Storage paths for new projects'
optional :repository_storages_weighted, type: Hash, desc: 'Storage paths for new projects with a weighted value between 0 and 100'
optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to set up Two-factor authentication'
given require_two_factor_authentication: ->(val) { val } do
requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication'
end
- optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
+ optional :restricted_visibility_levels, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Selected levels cannot be used by non-admin users for groups, projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
optional :send_user_confirmation_email, type: Boolean, desc: 'Send confirmation email on sign-up'
optional :session_expire_delay, type: Integer, desc: 'Session duration in minutes. GitLab restart is required to apply changes.'
optional :shared_runners_enabled, type: Boolean, desc: 'Enable shared runners for new projects'
diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb
index 693c20cb73a..de1373144e3 100644
--- a/lib/api/sidekiq_metrics.rb
+++ b/lib/api/sidekiq_metrics.rb
@@ -3,7 +3,7 @@
require 'sidekiq/api'
module API
- class SidekiqMetrics < Grape::API
+ class SidekiqMetrics < Grape::API::Instance
before { authenticated_as_admin! }
helpers do
diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb
index be58b832f97..118045e3af2 100644
--- a/lib/api/snippets.rb
+++ b/lib/api/snippets.rb
@@ -2,7 +2,7 @@
module API
# Snippets API
- class Snippets < Grape::API
+ class Snippets < Grape::API::Instance
include PaginationParams
before { authenticate! }
@@ -31,7 +31,7 @@ module API
use :pagination
end
get do
- present paginate(snippets_for_current_user), with: Entities::Snippet
+ present paginate(snippets_for_current_user), with: Entities::Snippet, current_user: current_user
end
desc 'List all public personal snippets current_user has access to' do
@@ -42,7 +42,7 @@ module API
use :pagination
end
get 'public' do
- present paginate(public_snippets), with: Entities::PersonalSnippet
+ present paginate(public_snippets), with: Entities::PersonalSnippet, current_user: current_user
end
desc 'Get a single snippet' do
@@ -57,7 +57,7 @@ module API
break not_found!('Snippet') unless snippet
- present snippet, with: Entities::PersonalSnippet
+ present snippet, with: Entities::PersonalSnippet, current_user: current_user
end
desc 'Create new snippet' do
@@ -82,7 +82,7 @@ module API
snippet = service_response.payload[:snippet]
if service_response.success?
- present snippet, with: Entities::PersonalSnippet
+ present snippet, with: Entities::PersonalSnippet, current_user: current_user
else
render_spam_error! if snippet.spam?
@@ -116,7 +116,7 @@ module API
snippet = service_response.payload[:snippet]
if service_response.success?
- present snippet, with: Entities::PersonalSnippet
+ present snippet, with: Entities::PersonalSnippet, current_user: current_user
else
render_spam_error! if snippet.spam?
@@ -155,14 +155,22 @@ module API
end
get ":id/raw" do
snippet = snippets.find_by_id(params.delete(:id))
- break not_found!('Snippet') unless snippet
+ not_found!('Snippet') unless snippet
- env['api.format'] = :txt
- content_type 'text/plain'
- header['Content-Disposition'] = 'attachment'
present content_for(snippet)
end
+ desc 'Get raw snippet file contents from the repository'
+ params do
+ use :raw_file_params
+ end
+ get ":id/files/:ref/:file_path/raw", requirements: { file_path: API::NO_SLASH_URL_PART_REGEX } do
+ snippet = snippets.find_by_id(params.delete(:id))
+ not_found!('Snippet') unless snippet&.repo_exists?
+
+ present file_content_for(snippet)
+ end
+
desc 'Get the user agent details for a snippet' do
success Entities::UserAgentDetail
end
diff --git a/lib/api/statistics.rb b/lib/api/statistics.rb
index d2dce34dfa5..3869fd3ac76 100644
--- a/lib/api/statistics.rb
+++ b/lib/api/statistics.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Statistics < Grape::API
+ class Statistics < Grape::API::Instance
before { authenticated_as_admin! }
COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue,
diff --git a/lib/api/submodules.rb b/lib/api/submodules.rb
index 72d7d994102..34d21d3d7d8 100644
--- a/lib/api/submodules.rb
+++ b/lib/api/submodules.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Submodules < Grape::API
+ class Submodules < Grape::API::Instance
before { authenticate! }
helpers do
diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb
index dfb54446ddf..533663fb087 100644
--- a/lib/api/subscriptions.rb
+++ b/lib/api/subscriptions.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Subscriptions < Grape::API
+ class Subscriptions < Grape::API::Instance
helpers ::API::Helpers::LabelHelpers
before { authenticate! }
diff --git a/lib/api/suggestions.rb b/lib/api/suggestions.rb
index 05aaa8a6f41..38e96c080f2 100644
--- a/lib/api/suggestions.rb
+++ b/lib/api/suggestions.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Suggestions < Grape::API
+ class Suggestions < Grape::API::Instance
before { authenticate! }
resource :suggestions do
@@ -25,7 +25,7 @@ module API
success Entities::Suggestion
end
params do
- requires :ids, type: Array[String], desc: "An array of suggestion ID's"
+ requires :ids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: "An array of suggestion ID's"
end
put 'batch_apply' do
ids = params[:ids]
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 51fae0e54aa..d8e0a425625 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class SystemHooks < Grape::API
+ class SystemHooks < Grape::API::Instance
include PaginationParams
before do
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 796b1450602..c1fbd3ca7c6 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Tags < Grape::API
+ class Tags < Grape::API::Instance
include PaginationParams
TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 51f357d9477..80a97aae429 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Templates < Grape::API
+ class Templates < Grape::API::Instance
include PaginationParams
GLOBAL_TEMPLATE_TYPES = {
diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb
index e7c9627c753..f6e966defce 100644
--- a/lib/api/terraform/state.rb
+++ b/lib/api/terraform/state.rb
@@ -4,14 +4,14 @@ require_dependency 'api/validations/validators/limit'
module API
module Terraform
- class State < Grape::API
+ class State < Grape::API::Instance
include ::Gitlab::Utils::StrongMemoize
default_format :json
before do
authenticate!
- authorize! :admin_terraform_state, user_project
+ authorize! :read_terraform_state, user_project
end
params do
@@ -46,6 +46,8 @@ module API
desc 'Add a new terraform state or update an existing one'
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
post do
+ authorize! :admin_terraform_state, user_project
+
data = request.body.read
no_content! if data.empty?
@@ -59,6 +61,8 @@ module API
desc 'Delete a terraform state of a certain name'
route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
delete do
+ authorize! :admin_terraform_state, user_project
+
remote_state_handler.handle_with_lock do |state|
state.destroy!
status :ok
@@ -77,6 +81,8 @@ module API
requires :Path, type: String, desc: 'Terraform path'
end
post '/lock' do
+ authorize! :admin_terraform_state, user_project
+
status_code = :ok
lock_info = {
'Operation' => params[:Operation],
@@ -108,6 +114,8 @@ module API
optional :ID, type: String, limit: 255, desc: 'Terraform state lock ID'
end
delete '/lock' do
+ authorize! :admin_terraform_state, user_project
+
remote_state_handler.unlock!
status :ok
rescue ::Terraform::RemoteStateHandler::StateLockedError
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index e36ddf21277..4a73e3e0e94 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Todos < Grape::API
+ class Todos < Grape::API::Instance
include PaginationParams
before { authenticate! }
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index e1829403941..de67a149274 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Triggers < Grape::API
+ class Triggers < Grape::API::Instance
include PaginationParams
HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase
@@ -32,7 +32,7 @@ module API
project = find_project(params[:id])
not_found! unless project
- result = Ci::PipelineTriggerService.new(project, nil, params).execute
+ result = ::Ci::PipelineTriggerService.new(project, nil, params).execute
not_found! unless result
if result[:http_status]
diff --git a/lib/api/user_counts.rb b/lib/api/user_counts.rb
index 8df4b381bbf..90127ecbc73 100644
--- a/lib/api/user_counts.rb
+++ b/lib/api/user_counts.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class UserCounts < Grape::API
+ class UserCounts < Grape::API::Instance
resource :user_counts do
desc 'Return the user specific counts' do
detail 'Open MR Count'
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 3d8ae09edf1..7942777287b 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Users < Grape::API
+ class Users < Grape::API::Instance
include PaginationParams
include APIGuard
include Helpers::CustomAttributes
@@ -117,6 +117,8 @@ module API
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
users, options = with_custom_attributes(users, { with: entity, current_user: current_user })
+ users = users.preload(:user_detail)
+
present paginate(users), options
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -328,9 +330,9 @@ module API
user = User.find_by(id: params.delete(:id))
not_found!('User') unless user
- key = user.gpg_keys.new(declared_params(include_missing: false))
+ key = ::GpgKeys::CreateService.new(user, declared_params(include_missing: false)).execute
- if key.save
+ if key.persisted?
present key, with: Entities::GpgKey
else
render_validation_error!(key)
@@ -374,9 +376,10 @@ module API
key = user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
- key.destroy
-
- no_content!
+ destroy_conditionally!(key) do |key|
+ destroy_service = ::GpgKeys::DestroyService.new(current_user)
+ destroy_service.execute(key)
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -730,9 +733,9 @@ module API
optional :expires_at, type: DateTime, desc: 'The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)'
end
post "keys" do
- key = current_user.keys.new(declared_params)
+ key = ::Keys::CreateService.new(current_user, declared_params(include_missing: false)).execute
- if key.save
+ if key.persisted?
present key, with: Entities::SSHKey
else
render_validation_error!(key)
@@ -750,7 +753,10 @@ module API
key = current_user.keys.find_by(id: params[:key_id])
not_found!('Key') unless key
- destroy_conditionally!(key)
+ destroy_conditionally!(key) do |key|
+ destroy_service = ::Keys::DestroyService.new(current_user)
+ destroy_service.execute(key)
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -789,9 +795,9 @@ module API
requires :key, type: String, desc: 'The new GPG key'
end
post 'gpg_keys' do
- key = current_user.gpg_keys.new(declared_params)
+ key = ::GpgKeys::CreateService.new(current_user, declared_params(include_missing: false)).execute
- if key.save
+ if key.persisted?
present key, with: Entities::GpgKey
else
render_validation_error!(key)
@@ -825,9 +831,10 @@ module API
key = current_user.gpg_keys.find_by(id: params[:key_id])
not_found!('GPG Key') unless key
- key.destroy
-
- no_content!
+ destroy_conditionally!(key) do |key|
+ destroy_service = ::GpgKeys::DestroyService.new(current_user)
+ destroy_service.execute(key)
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/api/validations/types/comma_separated_to_array.rb b/lib/api/validations/types/comma_separated_to_array.rb
index b551878abd1..409eb67a3d3 100644
--- a/lib/api/validations/types/comma_separated_to_array.rb
+++ b/lib/api/validations/types/comma_separated_to_array.rb
@@ -10,7 +10,7 @@ module API
when String
value.split(',').map(&:strip)
when Array
- value.map { |v| v.to_s.split(',').map(&:strip) }.flatten
+ value.flat_map { |v| v.to_s.split(',').map(&:strip) }
else
[]
end
diff --git a/lib/api/validations/types/comma_separated_to_integer_array.rb b/lib/api/validations/types/comma_separated_to_integer_array.rb
new file mode 100644
index 00000000000..b8ab08b3fd4
--- /dev/null
+++ b/lib/api/validations/types/comma_separated_to_integer_array.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module API
+ module Validations
+ module Types
+ class CommaSeparatedToIntegerArray < CommaSeparatedToArray
+ def self.coerce
+ lambda do |value|
+ super.call(value).map(&:to_i)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/validations/types/labels_list.rb b/lib/api/validations/types/labels_list.rb
deleted file mode 100644
index 60277b99106..00000000000
--- a/lib/api/validations/types/labels_list.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Validations
- module Types
- class LabelsList
- def self.coerce
- lambda do |value|
- case value
- when String
- value.split(',').map(&:strip)
- when Array
- value.flat_map { |v| v.to_s.split(',').map(&:strip) }
- when LabelsList
- value
- else
- []
- end
- end
- end
- end
- end
- end
-end
diff --git a/lib/api/validations/types/safe_file.rb b/lib/api/validations/types/safe_file.rb
deleted file mode 100644
index 53b5790bfa2..00000000000
--- a/lib/api/validations/types/safe_file.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-# This module overrides the Grape type validator defined in
-# https://github.com/ruby-grape/grape/blob/master/lib/grape/validations/types/file.rb
-module API
- module Validations
- module Types
- class SafeFile < ::Grape::Validations::Types::File
- def value_coerced?(value)
- super && value[:tempfile].is_a?(Tempfile)
- end
- end
- end
- end
-end
diff --git a/lib/api/validations/types/workhorse_file.rb b/lib/api/validations/types/workhorse_file.rb
index 18d111f6556..e65e94fc8db 100644
--- a/lib/api/validations/types/workhorse_file.rb
+++ b/lib/api/validations/types/workhorse_file.rb
@@ -3,15 +3,14 @@
module API
module Validations
module Types
- class WorkhorseFile < Virtus::Attribute
- def coerce(input)
- # Processing of multipart file objects
- # is already taken care of by Gitlab::Middleware::Multipart.
- # Nothing to do here.
- input
+ class WorkhorseFile
+ def self.parse(value)
+ raise "#{value.class} is not an UploadedFile type" unless parsed?(value)
+
+ value
end
- def value_coerced?(value)
+ def self.parsed?(value)
value.is_a?(::UploadedFile)
end
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index 192b06b8a1b..50d137ec7c1 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Variables < Grape::API
+ class Variables < Grape::API::Instance
include PaginationParams
before { authenticate! }
@@ -13,6 +13,15 @@ module API
# parameters, without having to modify the source code directly.
params
end
+
+ def find_variable(params)
+ variables = ::Ci::VariablesFinder.new(user_project, params).execute.to_a
+
+ return variables.first unless ::Gitlab::Ci::Features.variables_api_filter_environment_scope?
+ return variables.first unless variables.many? # rubocop: disable CodeReuse/ActiveRecord
+
+ conflict!("There are multiple variables with provided parameters. Please use 'filter[environment_scope]'")
+ end
end
params do
@@ -39,10 +48,8 @@ module API
end
# rubocop: disable CodeReuse/ActiveRecord
get ':id/variables/:key' do
- key = params[:key]
- variable = user_project.variables.find_by(key: key)
-
- break not_found!('Variable') unless variable
+ variable = find_variable(params)
+ not_found!('Variable') unless variable
present variable, with: Entities::Variable
end
@@ -56,7 +63,7 @@ module API
requires :value, type: String, desc: 'The value of the variable'
optional :protected, type: Boolean, desc: 'Whether the variable is protected'
optional :masked, type: Boolean, desc: 'Whether the variable is masked'
- optional :variable_type, type: String, values: Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
+ optional :variable_type, type: String, values: ::Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file. Defaults to env_var'
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
end
post ':id/variables' do
@@ -80,16 +87,16 @@ module API
optional :value, type: String, desc: 'The value of the variable'
optional :protected, type: Boolean, desc: 'Whether the variable is protected'
optional :masked, type: Boolean, desc: 'Whether the variable is masked'
- optional :variable_type, type: String, values: Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
+ optional :variable_type, type: String, values: ::Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file'
optional :environment_scope, type: String, desc: 'The environment_scope of the variable'
+ optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production'
end
# rubocop: disable CodeReuse/ActiveRecord
put ':id/variables/:key' do
- variable = user_project.variables.find_by(key: params[:key])
-
- break not_found!('Variable') unless variable
+ variable = find_variable(params)
+ not_found!('Variable') unless variable
- variable_params = declared_params(include_missing: false).except(:key)
+ variable_params = declared_params(include_missing: false).except(:key, :filter)
variable_params = filter_variable_parameters(variable_params)
if variable.update(variable_params)
@@ -105,10 +112,11 @@ module API
end
params do
requires :key, type: String, desc: 'The key of the variable'
+ optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production'
end
# rubocop: disable CodeReuse/ActiveRecord
delete ':id/variables/:key' do
- variable = user_project.variables.find_by(key: params[:key])
+ variable = find_variable(params)
not_found!('Variable') unless variable
# Variables don't have a timestamp. Therefore, destroy unconditionally.
diff --git a/lib/api/version.rb b/lib/api/version.rb
index 2d8c90260fa..6a480fc2bd9 100644
--- a/lib/api/version.rb
+++ b/lib/api/version.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module API
- class Version < Grape::API
+ class Version < Grape::API::Instance
helpers ::API::Helpers::GraphqlHelpers
include APIGuard
diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb
index c1bf3a64923..713136e0887 100644
--- a/lib/api/wikis.rb
+++ b/lib/api/wikis.rb
@@ -1,25 +1,11 @@
# frozen_string_literal: true
module API
- class Wikis < Grape::API
+ class Wikis < Grape::API::Instance
+ helpers ::API::Helpers::WikisHelpers
+
helpers do
- def commit_params(attrs)
- # In order to avoid service disruption this can work with an old workhorse without the acceleration
- # the first branch of this if must be removed when we drop support for non accelerated uploads
- if attrs[:file].is_a?(Hash)
- {
- file_name: attrs[:file][:filename],
- file_content: attrs[:file][:tempfile].read,
- branch_name: attrs[:branch]
- }
- else
- {
- file_name: attrs[:file].original_filename,
- file_content: attrs[:file].read,
- branch_name: attrs[:branch]
- }
- end
- end
+ attr_reader :container
params :common_wiki_page_params do
optional :format,
@@ -32,108 +18,118 @@ module API
WIKI_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(slug: API::NO_SLASH_URL_PART_REGEX)
- resource :projects, requirements: WIKI_ENDPOINT_REQUIREMENTS do
- desc 'Get a list of wiki pages' do
- success Entities::WikiPageBasic
- end
- params do
- optional :with_content, type: Boolean, default: false, desc: "Include pages' content"
- end
- get ':id/wikis' do
- authorize! :read_wiki, user_project
-
- entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic
+ ::API::Helpers::WikisHelpers.wiki_resource_kinds.each do |container_resource|
+ resource container_resource, requirements: WIKI_ENDPOINT_REQUIREMENTS do
+ after_validation do
+ @container = Gitlab::Lazy.new { find_container(container_resource) }
+ end
- present user_project.wiki.list_pages(load_content: params[:with_content]), with: entity
- end
+ desc 'Get a list of wiki pages' do
+ success Entities::WikiPageBasic
+ end
+ params do
+ optional :with_content, type: Boolean, default: false, desc: "Include pages' content"
+ end
+ get ':id/wikis' do
+ authorize! :read_wiki, container
- desc 'Get a wiki page' do
- success Entities::WikiPage
- end
- params do
- requires :slug, type: String, desc: 'The slug of a wiki page'
- end
- get ':id/wikis/:slug' do
- authorize! :read_wiki, user_project
+ entity = params[:with_content] ? Entities::WikiPage : Entities::WikiPageBasic
- present wiki_page, with: Entities::WikiPage
- end
+ present container.wiki.list_pages(load_content: params[:with_content]), with: entity
+ end
- desc 'Create a wiki page' do
- success Entities::WikiPage
- end
- params do
- requires :title, type: String, desc: 'Title of a wiki page'
- requires :content, type: String, desc: 'Content of a wiki page'
- use :common_wiki_page_params
- end
- post ':id/wikis' do
- authorize! :create_wiki, user_project
+ desc 'Get a wiki page' do
+ success Entities::WikiPage
+ end
+ params do
+ requires :slug, type: String, desc: 'The slug of a wiki page'
+ end
+ get ':id/wikis/:slug' do
+ authorize! :read_wiki, container
- page = WikiPages::CreateService.new(container: user_project, current_user: current_user, params: params).execute
+ present wiki_page, with: Entities::WikiPage
+ end
- if page.valid?
- present page, with: Entities::WikiPage
- else
- render_validation_error!(page)
+ desc 'Create a wiki page' do
+ success Entities::WikiPage
end
- end
+ params do
+ requires :title, type: String, desc: 'Title of a wiki page'
+ requires :content, type: String, desc: 'Content of a wiki page'
+ use :common_wiki_page_params
+ end
+ post ':id/wikis' do
+ authorize! :create_wiki, container
- desc 'Update a wiki page' do
- success Entities::WikiPage
- end
- params do
- optional :title, type: String, desc: 'Title of a wiki page'
- optional :content, type: String, desc: 'Content of a wiki page'
- use :common_wiki_page_params
- at_least_one_of :content, :title, :format
- end
- put ':id/wikis/:slug' do
- authorize! :create_wiki, user_project
+ page = WikiPages::CreateService.new(container: container, current_user: current_user, params: params).execute
- page = WikiPages::UpdateService.new(container: user_project, current_user: current_user, params: params).execute(wiki_page)
+ if page.valid?
+ present page, with: Entities::WikiPage
+ else
+ render_validation_error!(page)
+ end
+ end
- if page.valid?
- present page, with: Entities::WikiPage
- else
- render_validation_error!(page)
+ desc 'Update a wiki page' do
+ success Entities::WikiPage
+ end
+ params do
+ optional :title, type: String, desc: 'Title of a wiki page'
+ optional :content, type: String, desc: 'Content of a wiki page'
+ use :common_wiki_page_params
+ at_least_one_of :content, :title, :format
+ end
+ put ':id/wikis/:slug' do
+ authorize! :create_wiki, container
+
+ page = WikiPages::UpdateService
+ .new(container: container, current_user: current_user, params: params)
+ .execute(wiki_page)
+
+ if page.valid?
+ present page, with: Entities::WikiPage
+ else
+ render_validation_error!(page)
+ end
end
- end
- desc 'Delete a wiki page'
- params do
- requires :slug, type: String, desc: 'The slug of a wiki page'
- end
- delete ':id/wikis/:slug' do
- authorize! :admin_wiki, user_project
+ desc 'Delete a wiki page'
+ params do
+ requires :slug, type: String, desc: 'The slug of a wiki page'
+ end
+ delete ':id/wikis/:slug' do
+ authorize! :admin_wiki, container
- WikiPages::DestroyService.new(container: user_project, current_user: current_user).execute(wiki_page)
+ WikiPages::DestroyService
+ .new(container: container, current_user: current_user)
+ .execute(wiki_page)
- no_content!
- end
+ no_content!
+ end
- desc 'Upload an attachment to the wiki repository' do
- detail 'This feature was introduced in GitLab 11.3.'
- success Entities::WikiAttachment
- end
- params do
- requires :file, types: [::API::Validations::Types::SafeFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded'
- optional :branch, type: String, desc: 'The name of the branch'
- end
- post ":id/wikis/attachments" do
- authorize! :create_wiki, user_project
-
- result = ::Wikis::CreateAttachmentService.new(
- container: user_project,
- current_user: current_user,
- params: commit_params(declared_params(include_missing: false))
- ).execute
-
- if result[:status] == :success
- status(201)
- present OpenStruct.new(result[:result]), with: Entities::WikiAttachment
- else
- render_api_error!(result[:message], 400)
+ desc 'Upload an attachment to the wiki repository' do
+ detail 'This feature was introduced in GitLab 11.3.'
+ success Entities::WikiAttachment
+ end
+ params do
+ requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded'
+ optional :branch, type: String, desc: 'The name of the branch'
+ end
+ post ":id/wikis/attachments" do
+ authorize! :create_wiki, container
+
+ result = ::Wikis::CreateAttachmentService.new(
+ container: container,
+ current_user: current_user,
+ params: commit_params(declared_params(include_missing: false))
+ ).execute
+
+ if result[:status] == :success
+ status(201)
+ present OpenStruct.new(result[:result]), with: Entities::WikiAttachment
+ else
+ render_api_error!(result[:message], 400)
+ end
end
end
end