From a09983ae35713f5a2bbb100981116d31ce99826e Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Jul 2020 12:26:25 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-2-stable-ee --- lib/api/access_requests.rb | 2 +- lib/api/admin/ci/variables.rb | 2 +- lib/api/admin/instance_clusters.rb | 134 +++++++++ lib/api/admin/sidekiq.rb | 2 +- lib/api/api.rb | 27 +- lib/api/api_guard.rb | 12 +- lib/api/appearance.rb | 2 +- lib/api/applications.rb | 2 +- lib/api/avatar.rb | 2 +- lib/api/award_emoji.rb | 2 +- lib/api/badges.rb | 2 +- lib/api/boards.rb | 2 +- lib/api/branches.rb | 13 +- lib/api/broadcast_messages.rb | 2 +- lib/api/ci/pipeline_schedules.rb | 217 ++++++++++++++ lib/api/ci/pipelines.rb | 189 ++++++++++++ lib/api/ci/runner.rb | 318 +++++++++++++++++++++ lib/api/ci/runners.rb | 289 +++++++++++++++++++ lib/api/commit_statuses.rb | 4 +- lib/api/commits.rb | 2 +- lib/api/composer_packages.rb | 156 ++++++++++ lib/api/conan_packages.rb | 309 ++++++++++++++++++++ lib/api/container_registry_event.rb | 2 +- lib/api/deploy_keys.rb | 2 +- lib/api/deploy_tokens.rb | 6 +- lib/api/deployments.rb | 2 +- lib/api/discussions.rb | 18 +- lib/api/entities/approvals.rb | 9 + lib/api/entities/basic_project_details.rb | 3 +- .../conan_package/conan_package_manifest.rb | 11 + .../conan_package/conan_package_snapshot.rb | 11 + .../conan_package/conan_recipe_manifest.rb | 11 + .../conan_package/conan_recipe_snapshot.rb | 11 + .../entities/conan_package/conan_upload_urls.rb | 11 + lib/api/entities/entity_helpers.rb | 19 ++ lib/api/entities/go_module_version.rb | 10 + lib/api/entities/group.rb | 1 + lib/api/entities/group_detail.rb | 1 + lib/api/entities/issuable_entity.rb | 36 ++- lib/api/entities/issue_basic.rb | 8 +- lib/api/entities/merge_request_approvals.rb | 24 ++ lib/api/entities/merge_request_basic.rb | 21 +- lib/api/entities/npm_package.rb | 11 + lib/api/entities/npm_package_tag.rb | 9 + lib/api/entities/nuget/dependency.rb | 14 + lib/api/entities/nuget/dependency_group.rb | 14 + lib/api/entities/nuget/metadatum.rb | 13 + lib/api/entities/nuget/package_metadata.rb | 13 + .../nuget/package_metadata_catalog_entry.rb | 19 ++ lib/api/entities/nuget/packages_metadata.rb | 12 + lib/api/entities/nuget/packages_metadata_item.rb | 15 + lib/api/entities/nuget/packages_versions.rb | 11 + lib/api/entities/nuget/search_result.rb | 21 ++ lib/api/entities/nuget/search_result_version.rb | 13 + lib/api/entities/nuget/search_results.rb | 12 + lib/api/entities/nuget/service_index.rb | 12 + lib/api/entities/package.rb | 42 +++ lib/api/entities/package/pipeline.rb | 11 + lib/api/entities/package_file.rb | 11 + lib/api/entities/package_version.rb | 14 + lib/api/entities/project.rb | 2 + lib/api/entities/project_statistics.rb | 1 + lib/api/entities/release.rb | 9 +- lib/api/entities/resource_state_event.rb | 18 ++ lib/api/entities/snippet.rb | 12 + lib/api/entities/user.rb | 2 +- lib/api/environments.rb | 2 +- lib/api/error_tracking.rb | 2 +- lib/api/events.rb | 2 +- lib/api/features.rb | 2 +- lib/api/files.rb | 4 +- lib/api/freeze_periods.rb | 2 +- lib/api/go_proxy.rb | 135 +++++++++ lib/api/group_boards.rb | 2 +- lib/api/group_clusters.rb | 18 +- lib/api/group_container_repositories.rb | 2 +- lib/api/group_export.rb | 2 +- lib/api/group_import.rb | 2 +- lib/api/group_labels.rb | 2 +- lib/api/group_milestones.rb | 6 +- lib/api/group_packages.rb | 44 +++ lib/api/group_variables.rb | 6 +- lib/api/groups.rb | 11 +- lib/api/helpers.rb | 33 ++- lib/api/helpers/common_helpers.rb | 20 ++ lib/api/helpers/internal_helpers.rb | 4 +- lib/api/helpers/merge_requests_helpers.rb | 40 ++- lib/api/helpers/packages/basic_auth_helpers.rb | 57 ++++ lib/api/helpers/packages/conan/api_helpers.rb | 225 +++++++++++++++ .../helpers/packages/dependency_proxy_helpers.rb | 36 +++ lib/api/helpers/packages_helpers.rb | 52 ++++ .../helpers/packages_manager_clients_helpers.rb | 63 ++++ lib/api/helpers/projects_helpers.rb | 6 +- lib/api/helpers/runner.rb | 7 +- lib/api/helpers/services_helpers.rb | 33 +-- lib/api/helpers/snippets_helpers.rb | 26 +- lib/api/helpers/users_helpers.rb | 7 + lib/api/helpers/wikis_helpers.rb | 35 +++ lib/api/import_bitbucket_server.rb | 44 +++ lib/api/import_github.rb | 2 +- lib/api/internal/base.rb | 8 +- lib/api/internal/pages.rb | 2 +- lib/api/issues.rb | 41 ++- lib/api/job_artifacts.rb | 2 +- lib/api/jobs.rb | 4 +- lib/api/keys.rb | 2 +- lib/api/labels.rb | 2 +- lib/api/lint.rb | 2 +- lib/api/markdown.rb | 2 +- lib/api/maven_packages.rb | 251 ++++++++++++++++ lib/api/members.rb | 12 +- lib/api/merge_request_approvals.rb | 78 +++++ lib/api/merge_request_diffs.rb | 2 +- lib/api/merge_requests.rb | 31 +- lib/api/metrics/dashboard/annotations.rb | 2 +- lib/api/metrics/user_starred_dashboards.rb | 2 +- lib/api/milestone_responses.rb | 2 +- lib/api/namespaces.rb | 2 +- lib/api/notes.rb | 4 +- lib/api/notification_settings.rb | 2 +- lib/api/npm_packages.rb | 173 +++++++++++ lib/api/nuget_packages.rb | 221 ++++++++++++++ lib/api/package_files.rb | 33 +++ lib/api/pages.rb | 2 +- lib/api/pages_domains.rb | 2 +- lib/api/pagination_params.rb | 2 +- lib/api/pipeline_schedules.rb | 215 -------------- lib/api/pipelines.rb | 187 ------------ lib/api/project_clusters.rb | 18 +- lib/api/project_container_repositories.rb | 2 +- lib/api/project_events.rb | 2 +- lib/api/project_export.rb | 2 +- lib/api/project_hooks.rb | 2 +- lib/api/project_import.rb | 2 +- lib/api/project_milestones.rb | 6 +- lib/api/project_packages.rb | 71 +++++ lib/api/project_repository_storage_moves.rb | 2 +- lib/api/project_snapshots.rb | 2 +- lib/api/project_snippets.rb | 23 +- lib/api/project_statistics.rb | 2 +- lib/api/project_templates.rb | 2 +- lib/api/projects.rb | 32 ++- lib/api/projects_relation_builder.rb | 9 +- lib/api/protected_branches.rb | 2 +- lib/api/protected_tags.rb | 2 +- lib/api/pypi_packages.rb | 148 ++++++++++ lib/api/release/links.rb | 2 +- lib/api/releases.rb | 4 +- lib/api/remote_mirrors.rb | 2 +- lib/api/repositories.rb | 4 +- lib/api/resource_label_events.rb | 2 +- lib/api/resource_milestone_events.rb | 5 +- lib/api/resource_state_events.rb | 50 ++++ lib/api/runner.rb | 297 ------------------- lib/api/runners.rb | 287 ------------------- lib/api/search.rb | 5 +- lib/api/services.rb | 2 +- lib/api/settings.rb | 11 +- lib/api/sidekiq_metrics.rb | 2 +- lib/api/snippets.rb | 28 +- lib/api/statistics.rb | 2 +- lib/api/submodules.rb | 2 +- lib/api/subscriptions.rb | 2 +- lib/api/suggestions.rb | 4 +- lib/api/system_hooks.rb | 2 +- lib/api/tags.rb | 2 +- lib/api/templates.rb | 2 +- lib/api/terraform/state.rb | 12 +- lib/api/todos.rb | 2 +- lib/api/triggers.rb | 4 +- lib/api/user_counts.rb | 2 +- lib/api/users.rb | 35 ++- .../validations/types/comma_separated_to_array.rb | 2 +- .../types/comma_separated_to_integer_array.rb | 15 + lib/api/validations/types/labels_list.rb | 24 -- lib/api/validations/types/safe_file.rb | 15 - lib/api/validations/types/workhorse_file.rb | 13 +- lib/api/variables.rb | 32 ++- lib/api/version.rb | 2 +- lib/api/wikis.rb | 206 +++++++------ 180 files changed, 4376 insertions(+), 1474 deletions(-) create mode 100644 lib/api/admin/instance_clusters.rb create mode 100644 lib/api/ci/pipeline_schedules.rb create mode 100644 lib/api/ci/pipelines.rb create mode 100644 lib/api/ci/runner.rb create mode 100644 lib/api/ci/runners.rb create mode 100644 lib/api/composer_packages.rb create mode 100644 lib/api/conan_packages.rb create mode 100644 lib/api/entities/approvals.rb create mode 100644 lib/api/entities/conan_package/conan_package_manifest.rb create mode 100644 lib/api/entities/conan_package/conan_package_snapshot.rb create mode 100644 lib/api/entities/conan_package/conan_recipe_manifest.rb create mode 100644 lib/api/entities/conan_package/conan_recipe_snapshot.rb create mode 100644 lib/api/entities/conan_package/conan_upload_urls.rb create mode 100644 lib/api/entities/entity_helpers.rb create mode 100644 lib/api/entities/go_module_version.rb create mode 100644 lib/api/entities/merge_request_approvals.rb create mode 100644 lib/api/entities/npm_package.rb create mode 100644 lib/api/entities/npm_package_tag.rb create mode 100644 lib/api/entities/nuget/dependency.rb create mode 100644 lib/api/entities/nuget/dependency_group.rb create mode 100644 lib/api/entities/nuget/metadatum.rb create mode 100644 lib/api/entities/nuget/package_metadata.rb create mode 100644 lib/api/entities/nuget/package_metadata_catalog_entry.rb create mode 100644 lib/api/entities/nuget/packages_metadata.rb create mode 100644 lib/api/entities/nuget/packages_metadata_item.rb create mode 100644 lib/api/entities/nuget/packages_versions.rb create mode 100644 lib/api/entities/nuget/search_result.rb create mode 100644 lib/api/entities/nuget/search_result_version.rb create mode 100644 lib/api/entities/nuget/search_results.rb create mode 100644 lib/api/entities/nuget/service_index.rb create mode 100644 lib/api/entities/package.rb create mode 100644 lib/api/entities/package/pipeline.rb create mode 100644 lib/api/entities/package_file.rb create mode 100644 lib/api/entities/package_version.rb create mode 100644 lib/api/entities/resource_state_event.rb create mode 100755 lib/api/go_proxy.rb create mode 100644 lib/api/group_packages.rb create mode 100644 lib/api/helpers/packages/basic_auth_helpers.rb create mode 100644 lib/api/helpers/packages/conan/api_helpers.rb create mode 100644 lib/api/helpers/packages/dependency_proxy_helpers.rb create mode 100644 lib/api/helpers/packages_helpers.rb create mode 100644 lib/api/helpers/packages_manager_clients_helpers.rb create mode 100644 lib/api/helpers/wikis_helpers.rb create mode 100644 lib/api/import_bitbucket_server.rb create mode 100644 lib/api/maven_packages.rb create mode 100644 lib/api/merge_request_approvals.rb create mode 100644 lib/api/npm_packages.rb create mode 100644 lib/api/nuget_packages.rb create mode 100644 lib/api/package_files.rb delete mode 100644 lib/api/pipeline_schedules.rb delete mode 100644 lib/api/pipelines.rb create mode 100644 lib/api/project_packages.rb create mode 100644 lib/api/pypi_packages.rb create mode 100644 lib/api/resource_state_events.rb delete mode 100644 lib/api/runner.rb delete mode 100644 lib/api/runners.rb create mode 100644 lib/api/validations/types/comma_separated_to_integer_array.rb delete mode 100644 lib/api/validations/types/labels_list.rb delete mode 100644 lib/api/validations/types/safe_file.rb (limited to 'lib/api') 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 and 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//@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//@v/.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//@v/.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//@v/.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 -- cgit v1.2.3