diff options
Diffstat (limited to 'lib/api')
165 files changed, 1695 insertions, 266 deletions
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index 5305b25538f..7e3d70a210a 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::Instance + class AccessRequests < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb index 8721d94d642..44c389d6f94 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::Instance + class Variables < ::API::Base include PaginationParams before { authenticated_as_admin! } diff --git a/lib/api/admin/instance_clusters.rb b/lib/api/admin/instance_clusters.rb index 8208d10c089..ce1bdd65eff 100644 --- a/lib/api/admin/instance_clusters.rb +++ b/lib/api/admin/instance_clusters.rb @@ -2,7 +2,7 @@ module API module Admin - class InstanceClusters < Grape::API::Instance + class InstanceClusters < ::API::Base include PaginationParams before do @@ -37,6 +37,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 :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster' + optional :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace' 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' @@ -70,6 +71,7 @@ module API 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 :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace' 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 diff --git a/lib/api/admin/sidekiq.rb b/lib/api/admin/sidekiq.rb index f4c84f2eee8..c2e9de5fb4e 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::Instance + class Sidekiq < ::API::Base before { authenticated_as_admin! } namespace 'admin' do diff --git a/lib/api/api.rb b/lib/api/api.rb index b37751e1b47..84b4d5a5835 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::Instance + class API < ::API::Base include APIGuard LOG_FILENAME = Rails.root.join("log", "api_json.log") @@ -153,6 +153,9 @@ module API mount ::API::Environments mount ::API::ErrorTracking mount ::API::Events + mount ::API::FeatureFlags + mount ::API::FeatureFlagScopes + mount ::API::FeatureFlagsUserLists mount ::API::Features mount ::API::Files mount ::API::FreezePeriods @@ -196,6 +199,8 @@ module API mount ::API::ComposerPackages mount ::API::ConanProjectPackages mount ::API::ConanInstancePackages + mount ::API::DebianGroupPackages + mount ::API::DebianProjectPackages mount ::API::MavenPackages mount ::API::NpmPackages mount ::API::GenericPackages @@ -216,6 +221,7 @@ module API mount ::API::ProjectStatistics mount ::API::ProjectTemplates mount ::API::Terraform::State + mount ::API::Terraform::StateVersion mount ::API::ProtectedBranches mount ::API::ProtectedTags mount ::API::Releases @@ -236,6 +242,7 @@ module API mount ::API::Templates mount ::API::Todos mount ::API::Triggers + mount ::API::Unleash mount ::API::UsageData mount ::API::UserCounts mount ::API::Users @@ -245,6 +252,7 @@ module API end mount ::API::Internal::Base + mount ::API::Internal::Lfs mount ::API::Internal::Pages mount ::API::Internal::Kubernetes diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index bf5044b4832..0a486307653 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -19,6 +19,7 @@ module API end use AdminModeMiddleware + use ResponseCoercerMiddleware helpers HelperMethods @@ -188,6 +189,44 @@ module API end end + # Prior to Rack v2.1.x, returning a body of [nil] or [201] worked + # because the body was coerced to a string. However, this no longer + # works in Rack v2.1.0+. The Rack spec + # (https://github.com/rack/rack/blob/master/SPEC.rdoc#the-body-) + # says: + # + # The Body must respond to `each` and must only yield String values + # + # Because it's easy to return the wrong body type, this middleware + # will: + # + # 1. Inspect each element of the body if it is an Array. + # 2. Coerce each value to a string if necessary. + # 3. Flag a test and development error. + class ResponseCoercerMiddleware < ::Grape::Middleware::Base + def call(env) + response = super(env) + + status = response[0] + body = response[2] + + return response if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY[status] + return response unless body.is_a?(Array) + + body.map! do |part| + if part.is_a?(String) + part + else + err = ArgumentError.new("The response body should be a String, but it is of type #{part.class}") + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err) + part.to_s + end + end + + response + end + end + class AdminModeMiddleware < ::Grape::Middleware::Base def after # Use a Grape middleware since the Grape `after` blocks might run diff --git a/lib/api/appearance.rb b/lib/api/appearance.rb index f98004af480..00b495bbc1e 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::Instance + class Appearance < ::API::Base before { authenticated_as_admin! } helpers do diff --git a/lib/api/applications.rb b/lib/api/applications.rb index 4f2c3ee79ef..2afe8763d9d 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -2,7 +2,7 @@ module API # External applications API - class Applications < Grape::API::Instance + class Applications < ::API::Base before { authenticated_as_admin! } resource :applications do diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb index 9501e777fff..5a9b9940fcf 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::Instance + class Avatar < ::API::Base 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 0a3df3ed96e..6d40ae8f5ff 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::Instance + class AwardEmoji < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/badges.rb b/lib/api/badges.rb index f9728ffc446..fc00594c9ec 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::Instance + class Badges < ::API::Base include PaginationParams before { authenticate_non_get! } diff --git a/lib/api/base.rb b/lib/api/base.rb new file mode 100644 index 00000000000..e174cef3bad --- /dev/null +++ b/lib/api/base.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module API + class Base < Grape::API::Instance # rubocop:disable API/Base + end +end diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 1f5086127a8..d2d1628aff4 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::Instance + class Boards < ::API::Base include BoardsResponses include PaginationParams diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb index 68497a08fb8..6a86c02bf4a 100644 --- a/lib/api/boards_responses.rb +++ b/lib/api/boards_responses.rb @@ -45,7 +45,7 @@ module API def destroy_list(list) destroy_conditionally!(list) do |list| service = ::Boards::Lists::DestroyService.new(board_parent, current_user) - unless service.execute(list) + if service.execute(list).error? render_api_error!({ error: 'List could not be deleted!' }, 400) end end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 44f7610384e..37cce6eafba 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -3,7 +3,7 @@ require 'mime/types' module API - class Branches < Grape::API::Instance + class Branches < ::API::Base include PaginationParams BRANCH_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(branch: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index dcf950d7a03..8ce7694bbfd 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::Instance + class BroadcastMessages < ::API::Base include PaginationParams resource :broadcast_messages do diff --git a/lib/api/ci/pipeline_schedules.rb b/lib/api/ci/pipeline_schedules.rb index 1afdb0ad34c..18caf85f109 100644 --- a/lib/api/ci/pipeline_schedules.rb +++ b/lib/api/ci/pipeline_schedules.rb @@ -2,7 +2,7 @@ module API module Ci - class PipelineSchedules < Grape::API::Instance + class PipelineSchedules < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index 045f81074a7..61e03ed1a95 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -2,7 +2,7 @@ module API module Ci - class Pipelines < Grape::API::Instance + class Pipelines < ::API::Base include PaginationParams before { authenticate_non_get! } @@ -128,7 +128,7 @@ module API pipeline = user_project.all_pipelines.find(params[:pipeline_id]) - if Feature.enabled?(:ci_jobs_finder_refactor) + if Feature.enabled?(:ci_jobs_finder_refactor, default_enabled: true) builds = ::Ci::JobsFinder .new(current_user: current_user, pipeline: pipeline, params: params) .execute @@ -157,7 +157,7 @@ module API pipeline = user_project.all_pipelines.find(params[:pipeline_id]) - if Feature.enabled?(:ci_jobs_finder_refactor) + if Feature.enabled?(:ci_jobs_finder_refactor, default_enabled: true) bridges = ::Ci::JobsFinder .new(current_user: current_user, pipeline: pipeline, params: params, type: ::Ci::Bridge) .execute diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 08903dce3dc..ef679147c9f 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -2,7 +2,7 @@ module API module Ci - class Runner < Grape::API::Instance + class Runner < ::API::Base helpers ::API::Helpers::Runner resource :runners do @@ -72,6 +72,7 @@ module API post '/verify' do authenticate_runner! status 200 + body "200" end end @@ -181,7 +182,9 @@ module API .new(job, declared_params(include_missing: false)) service.execute.then do |result| + header 'X-GitLab-Trace-Update-Interval', result.backoff status result.status + body result.status.to_s end end @@ -292,6 +295,7 @@ module API if result[:status] == :success status :created + body "201" else render_api_error!(result[:message], result[:http_status]) end diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index 7bca72f8028..d37f10fe631 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -2,7 +2,7 @@ module API module Ci - class Runners < Grape::API::Instance + class Runners < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 9f5a6e87505..af103b8c1f8 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::Instance + class CommitStatuses < ::API::Base params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 20877fb5c5f..582ccd41847 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -3,7 +3,7 @@ require 'mime/types' module API - class Commits < Grape::API::Instance + class Commits < ::API::Base include PaginationParams before do @@ -62,19 +62,15 @@ module API first_parent: first_parent, order: order) - commit_count = - if all || path || before || after || first_parent - user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all, first_parent: first_parent) - else - # Cacheable commit count. - user_project.repository.commit_count_for_ref(ref) - end + serializer = with_stats ? Entities::CommitWithStats : Entities::Commit + # This tells kaminari that there is 1 more commit after the one we've + # loaded, meaning there will be a next page, if the currently loaded set + # of commits is equal to the requested page size. + commit_count = offset + commits.size + 1 paginated_commits = Kaminari.paginate_array(commits, total_count: commit_count) - serializer = with_stats ? Entities::CommitWithStats : Entities::Commit - - present paginate(paginated_commits), with: serializer + present paginate(paginated_commits, exclude_total_headers: true), with: serializer end desc 'Commit multiple file changes as one commit' do diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb index 31d097c4bea..1becbd668a3 100644 --- a/lib/api/composer_packages.rb +++ b/lib/api/composer_packages.rb @@ -2,7 +2,7 @@ # PHP composer support (https://getcomposer.org/) module API - class ComposerPackages < Grape::API::Instance + class ComposerPackages < ::API::Base helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::RelatedResourcesHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers @@ -26,6 +26,10 @@ module API render_api_error!(e.message, 400) end + rescue_from Packages::Composer::ComposerJsonService::InvalidJson do |e| + render_api_error!(e.message, 422) + end + helpers do def packages strong_memoize(:packages) do @@ -123,7 +127,7 @@ module API bad_request! end - package_event('push_package') + track_package_event('push_package', :composer) ::Packages::Composer::CreatePackageService .new(authorized_user_project, current_user, declared_params) diff --git a/lib/api/conan_instance_packages.rb b/lib/api/conan_instance_packages.rb index 209748d79fa..08265201328 100644 --- a/lib/api/conan_instance_packages.rb +++ b/lib/api/conan_instance_packages.rb @@ -2,7 +2,7 @@ # Conan Instance-Level Package Manager Client API module API - class ConanInstancePackages < Grape::API::Instance + class ConanInstancePackages < ::API::Base namespace 'packages/conan/v1' do include ConanPackageEndpoints end diff --git a/lib/api/conan_package_endpoints.rb b/lib/api/conan_package_endpoints.rb index 445447cfcd2..9b6867a328b 100644 --- a/lib/api/conan_package_endpoints.rb +++ b/lib/api/conan_package_endpoints.rb @@ -246,7 +246,7 @@ module API delete do authorize!(:destroy_package, project) - package_event('delete_package', category: 'API::ConanPackages') + track_package_event('delete_package', :conan, category: 'API::ConanPackages') package.destroy end diff --git a/lib/api/conan_project_packages.rb b/lib/api/conan_project_packages.rb index c51992231a7..db8cd187811 100644 --- a/lib/api/conan_project_packages.rb +++ b/lib/api/conan_project_packages.rb @@ -2,7 +2,7 @@ # Conan Project-Level Package Manager Client API module API - class ConanProjectPackages < Grape::API::Instance + class ConanProjectPackages < ::API::Base params do requires :id, type: Integer, desc: 'The ID of a project', regexp: %r{\A[1-9]\d*\z} end diff --git a/lib/api/container_registry_event.rb b/lib/api/container_registry_event.rb index 0b7c35cadbd..6c4b80b612a 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::Instance + class ContainerRegistryEvent < ::API::Base DOCKER_DISTRIBUTION_EVENTS_V1_JSON = 'application/vnd.docker.distribution.events.v1+json' before { authenticate_registry_notification! } diff --git a/lib/api/debian_group_packages.rb b/lib/api/debian_group_packages.rb new file mode 100644 index 00000000000..e3cacc4132f --- /dev/null +++ b/lib/api/debian_group_packages.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module API + class DebianGroupPackages < ::API::Base + params do + requires :id, type: String, desc: 'The ID of a group' + end + + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + before do + not_found! unless ::Feature.enabled?(:debian_packages, user_group) + + authorize_read_package!(user_group) + end + + namespace ':id/-/packages/debian' do + include DebianPackageEndpoints + end + end + end +end diff --git a/lib/api/debian_package_endpoints.rb b/lib/api/debian_package_endpoints.rb new file mode 100644 index 00000000000..168b3ca7a4f --- /dev/null +++ b/lib/api/debian_package_endpoints.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module API + module DebianPackageEndpoints + extend ActiveSupport::Concern + + DISTRIBUTION_REGEX = %r{[a-zA-Z0-9][a-zA-Z0-9.-]*}.freeze + COMPONENT_REGEX = %r{[a-z-]+}.freeze + ARCHITECTURE_REGEX = %r{[a-z][a-z0-9]*}.freeze + LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze + PACKAGE_REGEX = API::NO_SLASH_URL_PART_REGEX + DISTRIBUTION_REQUIREMENTS = { + distribution: DISTRIBUTION_REGEX + }.freeze + COMPONENT_ARCHITECTURE_REQUIREMENTS = { + component: COMPONENT_REGEX, + architecture: ARCHITECTURE_REGEX + }.freeze + COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS = { + component: COMPONENT_REGEX, + letter: LETTER_REGEX, + source_package: PACKAGE_REGEX + }.freeze + FILE_NAME_REQUIREMENTS = { + file_name: API::NO_SLASH_URL_PART_REGEX + }.freeze + + included do + helpers ::API::Helpers::PackagesHelpers + helpers ::API::Helpers::Packages::BasicAuthHelpers + + format :txt + + 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 + + before do + require_packages_enabled! + end + + params do + requires :distribution, type: String, desc: 'The Debian Codename', regexp: Gitlab::Regex.debian_distribution_regex + end + + namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do + # GET {projects|groups}/:id/-/packages/debian/dists/*distribution/Release.gpg + desc 'The Release file signature' do + detail 'This feature was introduced in GitLab 13.5' + end + + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth + get 'Release.gpg' do + not_found! + end + + # GET {projects|groups}/:id/-/packages/debian/dists/*distribution/Release + desc 'The unsigned Release file' do + detail 'This feature was introduced in GitLab 13.5' + end + + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth + get 'Release' do + # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286 + 'TODO Release' + end + + # GET {projects|groups}/:id/-/packages/debian/dists/*distribution/InRelease + desc 'The signed Release file' do + detail 'This feature was introduced in GitLab 13.5' + end + + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth + get 'InRelease' do + not_found! + end + + params do + requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex + requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex + end + + namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do + # GET {projects|groups}/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages + desc 'The binary files index' do + detail 'This feature was introduced in GitLab 13.5' + end + + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth + get 'Packages' do + # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286 + 'TODO Packages' + end + end + end + + params do + requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex + requires :letter, type: String, desc: 'The Debian Classification (first-letter or lib-first-letter)' + requires :source_package, type: String, desc: 'The Debian Source Package Name', regexp: Gitlab::Regex.debian_package_name_regex + end + + namespace 'pool/:component/:letter/:source_package', requirements: COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS do + # GET {projects|groups}/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name + params do + requires :file_name, type: String, desc: 'The Debian File Name' + end + desc 'The package' do + detail 'This feature was introduced in GitLab 13.5' + end + + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth + get ':file_name', requirements: FILE_NAME_REQUIREMENTS do + # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286 + 'TODO File' + end + end + end + end +end diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb new file mode 100644 index 00000000000..bcb4e8c8cbc --- /dev/null +++ b/lib/api/debian_project_packages.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module API + class DebianProjectPackages < ::API::Base + params do + requires :id, type: String, desc: 'The ID of a project' + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + before do + not_found! unless ::Feature.enabled?(:debian_packages, user_project) + + authorize_read_package! + end + + namespace ':id/-/packages/debian' do + include DebianPackageEndpoints + + params do + requires :file_name, type: String, desc: 'The file name' + end + + namespace 'incoming/:file_name', requirements: FILE_NAME_REQUIREMENTS do + # PUT {projects|groups}/:id/-/packages/debian/incoming/:file_name + params do + requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)' + end + + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth + put do + authorize_upload!(authorized_user_project) + bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:debian_max_file_size, params[:file].size) + + track_package_event('push_package', :debian) + + created! + rescue ObjectStorage::RemoteStoreError => e + Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id }) + + forbidden! + end + + # PUT {projects|groups}/:id/-/packages/debian/incoming/:file_name/authorize + route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth + post 'authorize' do + authorize_workhorse!( + subject: authorized_user_project, + has_length: false, + maximum_size: authorized_user_project.actual_limits.debian_max_file_size + ) + end + end + end + end + end +end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index ad37b7578ad..314f5b6ee1d 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::Instance + class DeployKeys < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb index 96aa2445f56..1c156b8b3bb 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::Instance + class DeployTokens < ::API::Base include PaginationParams helpers do diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index 87144fd31cc..ff06bdbae16 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::Instance + class Deployments < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index c431ec8e1e4..3d2608c8c5a 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::Instance + class Discussions < ::API::Base include PaginationParams helpers ::API::Helpers::NotesHelpers helpers ::RendersNotes diff --git a/lib/api/entities/ci/lint/result.rb b/lib/api/entities/ci/lint/result.rb new file mode 100644 index 00000000000..0e4aa238ba2 --- /dev/null +++ b/lib/api/entities/ci/lint/result.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + module Ci + module Lint + class Result < Grape::Entity + expose :valid?, as: :valid + expose :errors + expose :warnings + expose :merged_yaml + end + end + end + end +end diff --git a/lib/api/entities/cluster.rb b/lib/api/entities/cluster.rb index 4cb54e988ce..67459092a33 100644 --- a/lib/api/entities/cluster.rb +++ b/lib/api/entities/cluster.rb @@ -4,7 +4,7 @@ module API module Entities class Cluster < Grape::Entity expose :id, :name, :created_at, :domain - expose :provider_type, :platform_type, :environment_scope, :cluster_type + expose :provider_type, :platform_type, :environment_scope, :cluster_type, :namespace_per_environment expose :user, using: Entities::UserBasic expose :platform_kubernetes, using: Entities::Platform::Kubernetes expose :provider_gcp, using: Entities::Provider::Gcp diff --git a/lib/api/entities/container_registry.rb b/lib/api/entities/container_registry.rb index cff627ab50a..c430b73580b 100644 --- a/lib/api/entities/container_registry.rb +++ b/lib/api/entities/container_registry.rb @@ -16,6 +16,7 @@ module API expose :project_id expose :location expose :created_at + expose :expiration_policy_started_at, as: :cleanup_policy_started_at expose :tags_count, if: -> (_, options) { options[:tags_count] } expose :tags, using: Tag, if: -> (_, options) { options[:tags] } end diff --git a/lib/api/entities/feature_flag.rb b/lib/api/entities/feature_flag.rb new file mode 100644 index 00000000000..82fdb20af00 --- /dev/null +++ b/lib/api/entities/feature_flag.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + class FeatureFlag < Grape::Entity + expose :name + expose :description + expose :active + expose :version, if: :feature_flags_new_version_enabled + expose :created_at + expose :updated_at + expose :scopes, using: FeatureFlag::LegacyScope + expose :strategies, using: FeatureFlag::Strategy, if: :feature_flags_new_version_enabled + end + end +end diff --git a/lib/api/entities/feature_flag/detailed_legacy_scope.rb b/lib/api/entities/feature_flag/detailed_legacy_scope.rb new file mode 100644 index 00000000000..47078c1dfde --- /dev/null +++ b/lib/api/entities/feature_flag/detailed_legacy_scope.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module API + module Entities + class FeatureFlag < Grape::Entity + class DetailedLegacyScope < LegacyScope + expose :name + end + end + end +end diff --git a/lib/api/entities/feature_flag/legacy_scope.rb b/lib/api/entities/feature_flag/legacy_scope.rb new file mode 100644 index 00000000000..7329f71c599 --- /dev/null +++ b/lib/api/entities/feature_flag/legacy_scope.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + class FeatureFlag < Grape::Entity + class LegacyScope < Grape::Entity + expose :id + expose :active + expose :environment_scope + expose :strategies + expose :created_at + expose :updated_at + end + end + end +end diff --git a/lib/api/entities/feature_flag/scope.rb b/lib/api/entities/feature_flag/scope.rb new file mode 100644 index 00000000000..906fe718257 --- /dev/null +++ b/lib/api/entities/feature_flag/scope.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module API + module Entities + class FeatureFlag < Grape::Entity + class Scope < Grape::Entity + expose :id + expose :environment_scope + end + end + end +end diff --git a/lib/api/entities/feature_flag/strategy.rb b/lib/api/entities/feature_flag/strategy.rb new file mode 100644 index 00000000000..32699be0ee3 --- /dev/null +++ b/lib/api/entities/feature_flag/strategy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class FeatureFlag < Grape::Entity + class Strategy < Grape::Entity + expose :id + expose :name + expose :parameters + expose :scopes, using: FeatureFlag::Scope + end + end + end +end diff --git a/lib/api/entities/feature_flag/user_list.rb b/lib/api/entities/feature_flag/user_list.rb new file mode 100644 index 00000000000..bc8b12ea22e --- /dev/null +++ b/lib/api/entities/feature_flag/user_list.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module API + module Entities + class FeatureFlag < Grape::Entity + class UserList < Grape::Entity + include RequestAwareEntity + + expose :id + expose :iid + expose :project_id + expose :created_at + expose :updated_at + expose :name + expose :user_xids + + expose :path do |list| + project_feature_flags_user_list_path(list.project, list) + end + + expose :edit_path do |list| + edit_project_feature_flags_user_list_path(list.project, list) + end + end + end + end +end diff --git a/lib/api/entities/job_request/cache.rb b/lib/api/entities/job_request/cache.rb index a75affbaf84..cd533d7e5b3 100644 --- a/lib/api/entities/job_request/cache.rb +++ b/lib/api/entities/job_request/cache.rb @@ -4,7 +4,7 @@ module API module Entities module JobRequest class Cache < Grape::Entity - expose :key, :untracked, :paths, :policy + expose :key, :untracked, :paths, :policy, :when end end end diff --git a/lib/api/entities/member.rb b/lib/api/entities/member.rb index 14e97f41e77..ad62f92e5a0 100644 --- a/lib/api/entities/member.rb +++ b/lib/api/entities/member.rb @@ -5,6 +5,7 @@ module API class Member < Grape::Entity expose :user, merge: true, using: UserBasic expose :access_level + expose :created_at expose :expires_at end end diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb index d903f50befa..b54f0e04a9d 100644 --- a/lib/api/entities/package.rb +++ b/lib/api/entities/package.rb @@ -7,7 +7,19 @@ module API extend ::API::Entities::EntityHelpers expose :id - expose :name + + expose :name do |package| + if package.conan? + package.conan_recipe + else + package.name + end + end + + expose :conan_package_name, if: ->(package) { package.conan? } do |package| + package.name + end + expose :version expose :package_type diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index fb599d68d72..82a44c75382 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -84,6 +84,7 @@ module API expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) } expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :ci_default_git_depth + expose :ci_forward_deployment_enabled expose :public_builds, as: :public_jobs expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options| project.build_allow_git_fetch ? 'fetch' : 'clone' diff --git a/lib/api/entities/snippet.rb b/lib/api/entities/snippet.rb index 40488eb882d..85148c03d18 100644 --- a/lib/api/entities/snippet.rb +++ b/lib/api/entities/snippet.rb @@ -17,7 +17,7 @@ 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| + expose :files do |snippet, options| snippet.list_files.map do |file| { path: file, @@ -25,10 +25,6 @@ module API } 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/unleash_feature.rb b/lib/api/entities/unleash_feature.rb new file mode 100644 index 00000000000..8ee87d1fc11 --- /dev/null +++ b/lib/api/entities/unleash_feature.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module API + module Entities + class UnleashFeature < Grape::Entity + expose :name + expose :description, unless: ->(feature) { feature.description.nil? } + expose :active, as: :enabled + expose :strategies do |flag| + flag.strategies.map do |strategy| + if legacy_strategy?(strategy) + UnleashLegacyStrategy.represent(strategy) + elsif gitlab_user_list_strategy?(strategy) + UnleashGitlabUserListStrategy.represent(strategy) + else + UnleashStrategy.represent(strategy) + end + end + end + + private + + def legacy_strategy?(strategy) + !strategy.respond_to?(:name) + end + + def gitlab_user_list_strategy?(strategy) + strategy.name == ::Operations::FeatureFlags::Strategy::STRATEGY_GITLABUSERLIST + end + end + end +end diff --git a/lib/api/entities/unleash_gitlab_user_list_strategy.rb b/lib/api/entities/unleash_gitlab_user_list_strategy.rb new file mode 100644 index 00000000000..5617f8002d9 --- /dev/null +++ b/lib/api/entities/unleash_gitlab_user_list_strategy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class UnleashGitlabUserListStrategy < Grape::Entity + expose :name do |_strategy| + ::Operations::FeatureFlags::Strategy::STRATEGY_USERWITHID + end + expose :parameters do |strategy| + { userIds: strategy.user_list.user_xids } + end + end + end +end diff --git a/lib/api/entities/unleash_legacy_strategy.rb b/lib/api/entities/unleash_legacy_strategy.rb new file mode 100644 index 00000000000..5d5954f8da0 --- /dev/null +++ b/lib/api/entities/unleash_legacy_strategy.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + class UnleashLegacyStrategy < Grape::Entity + expose :name do |strategy| + strategy['name'] + end + expose :parameters do |strategy| + strategy['parameters'] + end + end + end +end diff --git a/lib/api/entities/unleash_strategy.rb b/lib/api/entities/unleash_strategy.rb new file mode 100644 index 00000000000..7627ce3873c --- /dev/null +++ b/lib/api/entities/unleash_strategy.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module API + module Entities + class UnleashStrategy < Grape::Entity + expose :name + expose :parameters + end + end +end diff --git a/lib/api/entities/user_with_admin.rb b/lib/api/entities/user_with_admin.rb index c225ade6eb6..ab7bc738ff8 100644 --- a/lib/api/entities/user_with_admin.rb +++ b/lib/api/entities/user_with_admin.rb @@ -8,3 +8,5 @@ module API end end end + +API::Entities::UserWithAdmin.prepend_if_ee('EE::API::Entities::UserWithAdmin') diff --git a/lib/api/environments.rb b/lib/api/environments.rb index b825904e2c5..0e780d4ef36 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::Instance + class Environments < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/error_tracking.rb b/lib/api/error_tracking.rb index 64ec6f0a57a..03f83477954 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::Instance + class ErrorTracking < ::API::Base before { authenticate! } params do diff --git a/lib/api/events.rb b/lib/api/events.rb index 0b79431a76d..43efacf9c0b 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::Instance + class Events < ::API::Base include PaginationParams include APIGuard helpers ::API::Helpers::EventsHelpers diff --git a/lib/api/feature_flag_scopes.rb b/lib/api/feature_flag_scopes.rb new file mode 100644 index 00000000000..d77e243aa88 --- /dev/null +++ b/lib/api/feature_flag_scopes.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +module API + class FeatureFlagScopes < ::API::Base + include PaginationParams + + ENVIRONMENT_SCOPE_ENDPOINT_REQUIREMENTS = FeatureFlags::FEATURE_FLAG_ENDPOINT_REQUIREMENTS + .merge(environment_scope: API::NO_SLASH_URL_PART_REGEX) + + before do + authorize_read_feature_flags! + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + resource :feature_flag_scopes do + desc 'Get all effective feature flags under the environment' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag::DetailedLegacyScope + end + params do + requires :environment, type: String, desc: 'The environment name' + end + get do + present scopes_for_environment, with: ::API::Entities::FeatureFlag::DetailedLegacyScope + end + end + + params do + requires :name, type: String, desc: 'The name of the feature flag' + end + resource 'feature_flags/:name', requirements: FeatureFlags::FEATURE_FLAG_ENDPOINT_REQUIREMENTS do + resource :scopes do + desc 'Get all scopes of a feature flag' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag::LegacyScope + end + params do + use :pagination + end + get do + present paginate(feature_flag.scopes), with: ::API::Entities::FeatureFlag::LegacyScope + end + + desc 'Create a scope of a feature flag' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag::LegacyScope + end + params do + requires :environment_scope, type: String, desc: 'The environment scope of the scope' + requires :active, type: Boolean, desc: 'Whether the scope is active' + requires :strategies, type: JSON, desc: 'The strategies of the scope' + end + post do + authorize_update_feature_flag! + + result = ::FeatureFlags::UpdateService + .new(user_project, current_user, scopes_attributes: [declared_params]) + .execute(feature_flag) + + if result[:status] == :success + present scope, with: ::API::Entities::FeatureFlag::LegacyScope + else + render_api_error!(result[:message], result[:http_status]) + end + end + + params do + requires :environment_scope, type: String, desc: 'URL-encoded environment scope' + end + resource ':environment_scope', requirements: ENVIRONMENT_SCOPE_ENDPOINT_REQUIREMENTS do + desc 'Get a scope of a feature flag' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag::LegacyScope + end + get do + present scope, with: ::API::Entities::FeatureFlag::LegacyScope + end + + desc 'Update a scope of a feature flag' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag::LegacyScope + end + params do + optional :active, type: Boolean, desc: 'Whether the scope is active' + optional :strategies, type: JSON, desc: 'The strategies of the scope' + end + put do + authorize_update_feature_flag! + + scope_attributes = declared_params.merge(id: scope.id) + + result = ::FeatureFlags::UpdateService + .new(user_project, current_user, scopes_attributes: [scope_attributes]) + .execute(feature_flag) + + if result[:status] == :success + updated_scope = result[:feature_flag].scopes + .find { |scope| scope.environment_scope == params[:environment_scope] } + + present updated_scope, with: ::API::Entities::FeatureFlag::LegacyScope + else + render_api_error!(result[:message], result[:http_status]) + end + end + + desc 'Delete a scope from a feature flag' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag::LegacyScope + end + delete do + authorize_update_feature_flag! + + param = { scopes_attributes: [{ id: scope.id, _destroy: true }] } + + result = ::FeatureFlags::UpdateService + .new(user_project, current_user, param) + .execute(feature_flag) + + if result[:status] == :success + status :no_content + else + render_api_error!(result[:message], result[:http_status]) + end + end + end + end + end + end + + helpers do + def authorize_read_feature_flags! + authorize! :read_feature_flag, user_project + end + + def authorize_update_feature_flag! + authorize! :update_feature_flag, feature_flag + end + + def feature_flag + @feature_flag ||= user_project.operations_feature_flags + .find_by_name!(params[:name]) + end + + def scope + @scope ||= feature_flag.scopes + .find_by_environment_scope!(CGI.unescape(params[:environment_scope])) + end + + def scopes_for_environment + Operations::FeatureFlagScope + .for_unleash_client(user_project, params[:environment]) + end + end + end +end diff --git a/lib/api/feature_flags.rb b/lib/api/feature_flags.rb new file mode 100644 index 00000000000..613c3fb0f5b --- /dev/null +++ b/lib/api/feature_flags.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true + +module API + class FeatureFlags < ::API::Base + include PaginationParams + + FEATURE_FLAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS + .merge(name: API::NO_SLASH_URL_PART_REGEX) + + before do + authorize_read_feature_flags! + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + resource :feature_flags do + desc 'Get all feature flags of a project' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag + end + params do + optional :scope, type: String, desc: 'The scope of feature flags', + values: %w[enabled disabled] + use :pagination + end + get do + feature_flags = ::FeatureFlagsFinder + .new(user_project, current_user, declared_params(include_missing: false)) + .execute + + present_entity(paginate(feature_flags)) + end + + desc 'Create a new feature flag' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag + end + params do + requires :name, type: String, desc: 'The name of feature flag' + optional :description, type: String, desc: 'The description of the feature flag' + optional :active, type: Boolean, desc: 'Active/inactive value of the flag' + optional :version, type: String, desc: 'The version of the feature flag' + optional :scopes, type: Array do + requires :environment_scope, type: String, desc: 'The environment scope of the scope' + requires :active, type: Boolean, desc: 'Active/inactive of the scope' + requires :strategies, type: JSON, desc: 'The strategies of the scope' + end + optional :strategies, type: Array do + requires :name, type: String, desc: 'The strategy name' + requires :parameters, type: JSON, desc: 'The strategy parameters' + optional :scopes, type: Array do + requires :environment_scope, type: String, desc: 'The environment scope of the scope' + end + end + end + post do + authorize_create_feature_flag! + + attrs = declared_params(include_missing: false) + + ensure_post_version_2_flags_enabled! if attrs[:version] == 'new_version_flag' + + rename_key(attrs, :scopes, :scopes_attributes) + rename_key(attrs, :strategies, :strategies_attributes) + update_value(attrs, :strategies_attributes) do |strategies| + strategies.map { |s| rename_key(s, :scopes, :scopes_attributes) } + end + + result = ::FeatureFlags::CreateService + .new(user_project, current_user, attrs) + .execute + + if result[:status] == :success + present_entity(result[:feature_flag]) + else + render_api_error!(result[:message], result[:http_status]) + end + end + end + + params do + requires :feature_flag_name, type: String, desc: 'The name of the feature flag' + end + resource 'feature_flags/:feature_flag_name', requirements: FEATURE_FLAG_ENDPOINT_REQUIREMENTS do + desc 'Get a feature flag of a project' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag + end + get do + authorize_read_feature_flag! + + present_entity(feature_flag) + end + + desc 'Enable a strategy for a feature flag on an environment' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag + end + params do + requires :environment_scope, type: String, desc: 'The environment scope of the feature flag' + requires :strategy, type: JSON, desc: 'The strategy to be enabled on the scope' + end + post :enable do + not_found! unless Feature.enabled?(:feature_flag_api, user_project) + render_api_error!('Version 2 flags not supported', :unprocessable_entity) if new_version_flag_present? + + result = ::FeatureFlags::EnableService + .new(user_project, current_user, params).execute + + if result[:status] == :success + status :ok + present_entity(result[:feature_flag]) + else + render_api_error!(result[:message], result[:http_status]) + end + end + + desc 'Disable a strategy for a feature flag on an environment' do + detail 'This feature is going to be introduced in GitLab 12.5 if `feature_flag_api` feature flag is removed' + success ::API::Entities::FeatureFlag + end + params do + requires :environment_scope, type: String, desc: 'The environment scope of the feature flag' + requires :strategy, type: JSON, desc: 'The strategy to be disabled on the scope' + end + post :disable do + not_found! unless Feature.enabled?(:feature_flag_api, user_project) + render_api_error!('Version 2 flags not supported', :unprocessable_entity) if feature_flag.new_version_flag? + + result = ::FeatureFlags::DisableService + .new(user_project, current_user, params).execute + + if result[:status] == :success + status :ok + present_entity(result[:feature_flag]) + else + render_api_error!(result[:message], result[:http_status]) + end + end + + desc 'Update a feature flag' do + detail 'This feature will be introduced in GitLab 13.1 if feature_flags_new_version feature flag is removed' + success ::API::Entities::FeatureFlag + end + params do + optional :name, type: String, desc: 'The name of the feature flag' + optional :description, type: String, desc: 'The description of the feature flag' + optional :active, type: Boolean, desc: 'Active/inactive value of the flag' + optional :strategies, type: Array do + optional :id, type: Integer, desc: 'The strategy id' + optional :name, type: String, desc: 'The strategy type' + optional :parameters, type: JSON, desc: 'The strategy parameters' + optional :_destroy, type: Boolean, desc: 'Delete the strategy when true' + optional :scopes, type: Array do + optional :id, type: Integer, desc: 'The environment scope id' + optional :environment_scope, type: String, desc: 'The environment scope of the scope' + optional :_destroy, type: Boolean, desc: 'Delete the scope when true' + end + end + end + put do + not_found! unless feature_flags_new_version_enabled? + authorize_update_feature_flag! + render_api_error!('PUT operations are not supported for legacy feature flags', :unprocessable_entity) if feature_flag.legacy_flag? + + attrs = declared_params(include_missing: false) + + rename_key(attrs, :strategies, :strategies_attributes) + update_value(attrs, :strategies_attributes) do |strategies| + strategies.map { |s| rename_key(s, :scopes, :scopes_attributes) } + end + + result = ::FeatureFlags::UpdateService + .new(user_project, current_user, attrs) + .execute(feature_flag) + + if result[:status] == :success + present_entity(result[:feature_flag]) + else + render_api_error!(result[:message], result[:http_status]) + end + end + + desc 'Delete a feature flag' do + detail 'This feature was introduced in GitLab 12.5' + success ::API::Entities::FeatureFlag + end + delete do + authorize_destroy_feature_flag! + + result = ::FeatureFlags::DestroyService + .new(user_project, current_user, declared_params(include_missing: false)) + .execute(feature_flag) + + if result[:status] == :success + present_entity(result[:feature_flag]) + else + render_api_error!(result[:message], result[:http_status]) + end + end + end + end + + helpers do + def authorize_read_feature_flags! + authorize! :read_feature_flag, user_project + end + + def authorize_read_feature_flag! + authorize! :read_feature_flag, feature_flag + end + + def authorize_create_feature_flag! + authorize! :create_feature_flag, user_project + end + + def authorize_update_feature_flag! + authorize! :update_feature_flag, feature_flag + end + + def authorize_destroy_feature_flag! + authorize! :destroy_feature_flag, feature_flag + end + + def present_entity(result) + present result, + with: ::API::Entities::FeatureFlag, + feature_flags_new_version_enabled: feature_flags_new_version_enabled? + end + + def ensure_post_version_2_flags_enabled! + unless feature_flags_new_version_enabled? + render_api_error!('Version 2 flags are not enabled for this project', :unprocessable_entity) + end + end + + def feature_flag + @feature_flag ||= if feature_flags_new_version_enabled? + user_project.operations_feature_flags.find_by_name!(params[:feature_flag_name]) + else + user_project.operations_feature_flags.legacy_flag.find_by_name!(params[:feature_flag_name]) + end + end + + def new_version_flag_present? + user_project.operations_feature_flags.new_version_flag.find_by_name(params[:name]).present? + end + + def feature_flags_new_version_enabled? + Feature.enabled?(:feature_flags_new_version, user_project, default_enabled: true) + end + + def rename_key(hash, old_key, new_key) + hash[new_key] = hash.delete(old_key) if hash.key?(old_key) + hash + end + + def update_value(hash, key) + hash[key] = yield(hash[key]) if hash.key?(key) + hash + end + end + end +end diff --git a/lib/api/feature_flags_user_lists.rb b/lib/api/feature_flags_user_lists.rb new file mode 100644 index 00000000000..e5218cfd7f1 --- /dev/null +++ b/lib/api/feature_flags_user_lists.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +module API + class FeatureFlagsUserLists < ::API::Base + include PaginationParams + + error_formatter :json, -> (message, _backtrace, _options, _env, _original_exception) { + message.is_a?(String) ? { message: message }.to_json : message.to_json + } + + before do + authorize_admin_feature_flags_user_lists! + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource 'projects/:id', requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + resource :feature_flags_user_lists do + desc 'Get all feature flags user lists of a project' do + detail 'This feature was introduced in GitLab 12.10' + success ::API::Entities::FeatureFlag::UserList + end + params do + use :pagination + end + get do + present paginate(user_project.operations_feature_flags_user_lists), + with: ::API::Entities::FeatureFlag::UserList + end + + desc 'Create a feature flags user list for a project' do + detail 'This feature was introduced in GitLab 12.10' + success ::API::Entities::FeatureFlag::UserList + end + params do + requires :name, type: String, desc: 'The name of the list' + requires :user_xids, type: String, desc: 'A comma separated list of external user ids' + end + post do + list = user_project.operations_feature_flags_user_lists.create(declared_params) + + if list.save + present list, with: ::API::Entities::FeatureFlag::UserList + else + render_api_error!(list.errors.full_messages, :bad_request) + end + end + end + + params do + requires :iid, type: String, desc: 'The internal id of the user list' + end + resource 'feature_flags_user_lists/:iid' do + desc 'Get a single feature flag user list belonging to a project' do + detail 'This feature was introduced in GitLab 12.10' + success ::API::Entities::FeatureFlag::UserList + end + get do + present user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid]), + with: ::API::Entities::FeatureFlag::UserList + end + + desc 'Update a feature flag user list' do + detail 'This feature was introduced in GitLab 12.10' + success ::API::Entities::FeatureFlag::UserList + end + params do + optional :name, type: String, desc: 'The name of the list' + optional :user_xids, type: String, desc: 'A comma separated list of external user ids' + end + put do + list = user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid]) + + if list.update(declared_params(include_missing: false)) + present list, with: ::API::Entities::FeatureFlag::UserList + else + render_api_error!(list.errors.full_messages, :bad_request) + end + end + + desc 'Delete a feature flag user list' do + detail 'This feature was introduced in GitLab 12.10' + end + delete do + list = user_project.operations_feature_flags_user_lists.find_by_iid!(params[:iid]) + unless list.destroy + render_api_error!(list.errors.full_messages, :conflict) + end + end + end + end + + helpers do + def authorize_admin_feature_flags_user_lists! + authorize! :admin_feature_flags_user_lists, user_project + end + end + end +end diff --git a/lib/api/features.rb b/lib/api/features.rb index 9d011d658f6..5d2e545abd6 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::Instance + class Features < ::API::Base before { authenticated_as_admin! } helpers do diff --git a/lib/api/files.rb b/lib/api/files.rb index 748bdfa894d..6833fc429e2 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::Instance + class Files < ::API::Base include APIGuard FILE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(file_path: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/freeze_periods.rb b/lib/api/freeze_periods.rb index b8254ee9ab4..a83e36165a2 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::Instance + class FreezePeriods < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index 98b8a40c7c9..a0c33ab65b9 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -1,7 +1,12 @@ # frozen_string_literal: true module API - class GenericPackages < Grape::API::Instance + class GenericPackages < ::API::Base + GENERIC_PACKAGES_REQUIREMENTS = { + package_name: API::NO_SLASH_URL_PART_REGEX, + file_name: API::NO_SLASH_URL_PART_REGEX + }.freeze + before do require_packages_enabled! authenticate! @@ -17,17 +22,94 @@ module API route_setting :authentication, job_token_allowed: true namespace ':id/packages/generic' do - get 'ping' do - :pong + namespace ':package_name/*package_version/:file_name', requirements: GENERIC_PACKAGES_REQUIREMENTS do + desc 'Workhorse authorize generic package file' do + detail 'This feature was introduced in GitLab 13.5' + end + + route_setting :authentication, job_token_allowed: true + + params do + requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true + requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex + requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true + end + + put 'authorize' do + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.generic_packages_max_file_size) + end + + desc 'Upload package file' do + detail 'This feature was introduced in GitLab 13.5' + end + + params do + requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true + requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex + requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true + 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 + + put do + authorize_upload!(project) + bad_request!('File is too large') if max_file_size_exceeded? + + track_event('push_package') + + create_package_file_params = declared_params.merge(build: current_authenticated_job) + ::Packages::Generic::CreatePackageFileService + .new(project, current_user, create_package_file_params) + .execute + + created! + rescue ObjectStorage::RemoteStoreError => e + Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project.id }) + + forbidden! + end + + desc 'Download package file' do + detail 'This feature was introduced in GitLab 13.5' + end + + params do + requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true + requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex + requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true + end + + route_setting :authentication, job_token_allowed: true + + get do + authorize_read_package!(project) + + package = ::Packages::Generic::PackageFinder.new(project).execute!(params[:package_name], params[:package_version]) + package_file = ::Packages::PackageFileFinder.new(package, params[:file_name]).execute! + + track_event('pull_package') + + present_carrierwave_file!(package_file.file) + end end end end helpers do include ::API::Helpers::PackagesHelpers + include ::API::Helpers::Packages::BasicAuthHelpers def require_generic_packages_available! - not_found! unless Feature.enabled?(:generic_packages, user_project) + not_found! unless Feature.enabled?(:generic_packages, project, default_enabled: true) + end + + def project + authorized_user_project + end + + def max_file_size_exceeded? + project.actual_limits.exceeded?(:generic_packages_max_file_size, params[:file].size) end end end diff --git a/lib/api/github/entities.rb b/lib/api/github/entities.rb index c28a0b8eb7e..fe228c9a2d2 100644 --- a/lib/api/github/entities.rb +++ b/lib/api/github/entities.rb @@ -119,7 +119,9 @@ module API expose :username, as: :login expose :user_url, as: :url expose :user_url, as: :html_url - expose :avatar_url + expose :avatar_url do |user| + user.avatar_url(only_path: false) + end private diff --git a/lib/api/go_proxy.rb b/lib/api/go_proxy.rb index c0207f9169c..30f0cfb4dfd 100755 --- a/lib/api/go_proxy.rb +++ b/lib/api/go_proxy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module API - class GoProxy < Grape::API::Instance + class GoProxy < ::API::Base helpers Gitlab::Golang helpers ::API::Helpers::PackagesHelpers diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb index 7efc12121d2..d4574b22d99 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::Instance + class GroupBoards < ::API::Base include BoardsResponses include PaginationParams diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb index ae41d9f13b8..75429cf7a5c 100644 --- a/lib/api/group_clusters.rb +++ b/lib/api/group_clusters.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupClusters < Grape::API::Instance + class GroupClusters < ::API::Base include PaginationParams before { authenticate! } @@ -41,6 +41,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 :environment_scope, default: '*', type: String, desc: 'The associated environment to the cluster' + optional :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace' 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' @@ -74,6 +75,7 @@ module API 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 :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace' 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' diff --git a/lib/api/group_container_repositories.rb b/lib/api/group_container_repositories.rb index 25b3059f63b..1bb26b3931c 100644 --- a/lib/api/group_container_repositories.rb +++ b/lib/api/group_container_repositories.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true module API - class GroupContainerRepositories < Grape::API::Instance + class GroupContainerRepositories < ::API::Base include PaginationParams + helpers ::API::Helpers::PackagesHelpers + before { authorize_read_group_container_images! } REPOSITORY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge( @@ -27,7 +29,7 @@ module API user: current_user, subject: user_group ).execute - track_event('list_repositories') + track_package_event('list_repositories', :container) present paginate(repositories), with: Entities::ContainerRegistry::Repository, tags: params[:tags], tags_count: params[:tags_count] end diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb index dc14813eefc..6ebaa8de185 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::Instance + class GroupExport < ::API::Base helpers Helpers::RateLimiter before do diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb index b82d9fc519a..e703a217fd5 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::Instance + class GroupImport < ::API::Base helpers Helpers::FileUploadHelpers helpers do diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb index 56f2b769464..8443ddf10ce 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::Instance + class GroupLabels < ::API::Base include PaginationParams helpers ::API::Helpers::LabelHelpers diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb index 82f5df79356..aef9877b84c 100644 --- a/lib/api/group_milestones.rb +++ b/lib/api/group_milestones.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupMilestones < Grape::API::Instance + class GroupMilestones < ::API::Base include MilestoneResponses include PaginationParams diff --git a/lib/api/group_packages.rb b/lib/api/group_packages.rb index aa047e260f5..5b6290df0dd 100644 --- a/lib/api/group_packages.rb +++ b/lib/api/group_packages.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class GroupPackages < Grape::API::Instance + class GroupPackages < ::API::Base include PaginationParams before do diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index e7b8cd10197..ee110d67fa5 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::Instance + class GroupVariables < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 813e41b4d39..bf3d6c3c7e0 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::Instance + class Groups < ::API::Base include PaginationParams include Helpers::CustomAttributes @@ -29,7 +29,12 @@ module API # rubocop: disable CodeReuse/ActiveRecord def find_groups(params, parent_id = nil) - find_params = params.slice(:all_available, :custom_attributes, :owned, :min_access_level) + find_params = params.slice( + :all_available, + :custom_attributes, + :owned, :min_access_level, + :include_parent_descendants + ) find_params[:parent] = if params[:top_level_only] [nil] @@ -309,6 +314,19 @@ module API present_groups params, groups end + desc 'Get a list of descendant groups of this group.' do + success Entities::Group + end + params do + use :group_list_params + use :with_custom_attributes + end + get ":id/descendant_groups" do + finder_params = declared_params(include_missing: false).merge(include_parent_descendants: true) + groups = find_groups(finder_params, params[:id]) + present_groups params, groups + end + desc 'Transfer a project to the group namespace. Available only for admin.' do success Entities::GroupDetail end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 1912a06682e..c8aee1f3479 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -522,7 +522,7 @@ module API else header(*Gitlab::Workhorse.send_url(file.url)) status :ok - body + body "" end end @@ -544,7 +544,6 @@ module API feature_name = "usage_data_#{event_name}" return unless Feature.enabled?(feature_name) - return unless Gitlab::CurrentSettings.usage_ping_enabled? Gitlab::UsageDataCounters::HLLRedisCounter.track_event(values, event_name) rescue => error diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb index f3dfc093926..ba07a70ee32 100644 --- a/lib/api/helpers/groups_helpers.rb +++ b/lib/api/helpers/groups_helpers.rb @@ -24,6 +24,7 @@ module API optional :lfs_enabled, type: Boolean, desc: 'Enable/disable LFS for the projects in this group' optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' optional :default_branch_protection, type: Integer, values: ::Gitlab::Access.protection_values, desc: 'Determine if developers can push to master' + optional :shared_runners_setting, type: String, values: ::Namespace::SHARED_RUNNERS_SETTINGS, desc: 'Enable/disable shared runners for the group and its subgroups and projects' end params :optional_params_ee do diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb index e4163c63575..9b38eeb1e72 100644 --- a/lib/api/helpers/merge_requests_helpers.rb +++ b/lib/api/helpers/merge_requests_helpers.rb @@ -73,6 +73,13 @@ module API optional :not, type: Hash, desc: 'Parameters to negate' do use :merge_requests_negatable_params end + + optional :deployed_before, + 'Return merge requests deployed before the given date/time' + optional :deployed_after, + 'Return merge requests deployed after the given date/time' + optional :environment, + 'Returns merge requests deployed to the given environment' end params :optional_scope_param do diff --git a/lib/api/helpers/packages/conan/api_helpers.rb b/lib/api/helpers/packages/conan/api_helpers.rb index dcbf933a4e1..934e18bdd0a 100644 --- a/lib/api/helpers/packages/conan/api_helpers.rb +++ b/lib/api/helpers/packages/conan/api_helpers.rb @@ -158,7 +158,7 @@ module API conan_package_reference: params[:conan_package_reference] ).execute! - package_event('pull_package', category: 'API::ConanPackages') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY + track_package_event('pull_package', :conan, category: 'API::ConanPackages') if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY present_carrierwave_file!(package_file.file) end @@ -169,7 +169,7 @@ module API def track_push_package_event if params[:file_name] == ::Packages::Conan::FileMetadatum::PACKAGE_BINARY && params[:file].size > 0 # rubocop: disable Style/ZeroLengthPredicate - package_event('push_package', category: 'API::ConanPackages') + track_package_event('push_package', :conan, category: 'API::ConanPackages') end end diff --git a/lib/api/helpers/packages/dependency_proxy_helpers.rb b/lib/api/helpers/packages/dependency_proxy_helpers.rb index 254af7690a2..577ba97d68a 100644 --- a/lib/api/helpers/packages/dependency_proxy_helpers.rb +++ b/lib/api/helpers/packages/dependency_proxy_helpers.rb @@ -10,6 +10,7 @@ module API def redirect_registry_request(forward_to_registry, package_type, options) if forward_to_registry && redirect_registry_request_available? + track_event("#{package_type}_request_forward") redirect(registry_url(package_type, options)) else yield diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index 403f5ea3851..e1898d28ef7 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -40,7 +40,7 @@ module API params = { has_length: has_length } params[:maximum_size] = maximum_size unless has_length - ::Packages::PackageFileUploader.workhorse_authorize(params) + ::Packages::PackageFileUploader.workhorse_authorize(**params) end def authorize_upload!(subject = user_project) @@ -48,7 +48,8 @@ module API require_gitlab_workhorse! end - def package_event(event_name, **args) + def track_package_event(event_name, scope, **args) + ::Packages::CreateEventService.new(nil, current_user, event_name: event_name, scope: scope).execute track_event(event_name, **args) end end diff --git a/lib/api/helpers/pagination.rb b/lib/api/helpers/pagination.rb index a6ae9a87f98..227aec224e5 100644 --- a/lib/api/helpers/pagination.rb +++ b/lib/api/helpers/pagination.rb @@ -3,8 +3,8 @@ module API module Helpers module Pagination - def paginate(relation) - Gitlab::Pagination::OffsetPagination.new(self).paginate(relation) + def paginate(*args) + Gitlab::Pagination::OffsetPagination.new(self).paginate(*args) end end end diff --git a/lib/api/helpers/presentable.rb b/lib/api/helpers/presentable.rb index a5186cc56ea..40e1b266df5 100644 --- a/lib/api/helpers/presentable.rb +++ b/lib/api/helpers/presentable.rb @@ -23,7 +23,7 @@ module API def initialize(object, options = {}) options = options.opts_hash if options.is_a?(Grape::Entity::Options) - super(object.present(options), options) + super(object.present(**options), options) end end end diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 8c20f5b8fc2..0364ba2ad9e 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -83,9 +83,18 @@ module API params :optional_filter_params_ee do end + params :optional_update_params_ce do + optional :ci_forward_deployment_enabled, type: Boolean, desc: 'Skip older deployment jobs that are still pending' + end + params :optional_update_params_ee do end + params :optional_update_params do + use :optional_update_params_ce + use :optional_update_params_ee + end + params :optional_container_expiration_policy_params do optional :cadence, type: String, desc: 'Container expiration policy cadence for recurring job' optional :keep_n, type: String, desc: 'Container expiration policy number of images to keep' @@ -108,6 +117,7 @@ module API :builds_access_level, :ci_config_path, :ci_default_git_depth, + :ci_forward_deployment_enabled, :container_registry_enabled, :container_expiration_policy_attributes, :default_branch, diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 34a2fb09875..1c85669a626 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -51,9 +51,7 @@ module API job_forbidden!(job, 'Job is not running') unless job.running? end - if Gitlab::Ci::Features.job_heartbeats_runner?(job.project) - job.runner&.heartbeat(get_runner_ip) - end + job.runner&.heartbeat(get_runner_ip) job end diff --git a/lib/api/helpers/services_helpers.rb b/lib/api/helpers/services_helpers.rb index 4bceda51900..4adb27a7414 100644 --- a/lib/api/helpers/services_helpers.rb +++ b/lib/api/helpers/services_helpers.rb @@ -381,6 +381,12 @@ module API type: String, desc: 'The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces…' }, + { + required: false, + name: :branches_to_be_notified, + type: String, + desc: 'Branches for which notifications are to be sent' + }, chat_notification_events ].flatten, 'hipchat' => [ diff --git a/lib/api/helpers/settings_helpers.rb b/lib/api/helpers/settings_helpers.rb index 65aec6ae2e7..451e578fdd6 100644 --- a/lib/api/helpers/settings_helpers.rb +++ b/lib/api/helpers/settings_helpers.rb @@ -12,6 +12,7 @@ module API def self.optional_attributes [*::ApplicationSettingsHelper.visible_attributes, *::ApplicationSettingsHelper.external_authorization_service_attributes, + *::ApplicationSettingsHelper.deprecated_attributes, :performance_bar_allowed_group_id].freeze end end diff --git a/lib/api/helpers/snippets_helpers.rb b/lib/api/helpers/snippets_helpers.rb index 9224381735f..42f56680ded 100644 --- a/lib/api/helpers/snippets_helpers.rb +++ b/lib/api/helpers/snippets_helpers.rb @@ -93,7 +93,7 @@ module API def validate_params_for_multiple_files(snippet) return unless params[:content] || params[:file_name] - if Feature.enabled?(:snippet_multiple_files, current_user) && snippet.multiple_files? + if snippet.multiple_files? render_api_error!({ error: _('To update Snippets with multiple files, you must use the `files` parameter') }, 400) end end diff --git a/lib/api/import_bitbucket_server.rb b/lib/api/import_bitbucket_server.rb index df3235420e9..a0238c24f3b 100644 --- a/lib/api/import_bitbucket_server.rb +++ b/lib/api/import_bitbucket_server.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ImportBitbucketServer < Grape::API::Instance + class ImportBitbucketServer < ::API::Base helpers do def client @client ||= BitbucketServer::Client.new(credentials) diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index 0bab891eada..61fce7a2c1b 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::Instance + class ImportGithub < ::API::Base rescue_from Octokit::Unauthorized, with: :provider_unauthorized before do diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index ff687a57888..6d8f13c36e6 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::Instance + class Base < ::API::Base before { authenticate_by_gitlab_shell_token! } before do @@ -99,6 +99,14 @@ module API @project = @container = access_checker.container end end + + def validate_actor_key(actor, key_id) + return 'Could not find a user without a key' unless key_id + + return 'Could not find the given key' unless actor.key + + 'Could not find a user for the given key' unless actor.user + end end namespace 'internal' do @@ -163,28 +171,23 @@ module API redis: redis_ping } end + post '/two_factor_recovery_codes' do status 200 actor.update_last_used_at! user = actor.user - if params[:key_id] - unless actor.key - break { success: false, message: 'Could not find the given key' } - end - - if actor.key.is_a?(DeployKey) - break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' } - end + error_message = validate_actor_key(actor, params[:key_id]) - unless user - break { success: false, message: 'Could not find a user for the given key' } - end - elsif params[:user_id] && user.nil? + if params[:user_id] && user.nil? break { success: false, message: 'Could not find the given user' } + elsif error_message + break { success: false, message: error_message } end + break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' } if actor.key.is_a?(DeployKey) + unless user.two_factor_enabled? break { success: false, message: 'Two-factor authentication is not enabled for this user' } end @@ -204,20 +207,14 @@ module API actor.update_last_used_at! user = actor.user - if params[:key_id] - unless actor.key - break { success: false, message: 'Could not find the given key' } - end + error_message = validate_actor_key(actor, params[:key_id]) - if actor.key.is_a?(DeployKey) - break { success: false, message: 'Deploy keys cannot be used to create personal access tokens' } - end + break { success: false, message: 'Deploy keys cannot be used to create personal access tokens' } if actor.key.is_a?(DeployKey) - unless user - break { success: false, message: 'Could not find a user for the given key' } - end - elsif params[:user_id] && user.nil? + if params[:user_id] && user.nil? break { success: false, message: 'Could not find the given user' } + elsif error_message + break { success: false, message: error_message } end if params[:name].blank? @@ -269,6 +266,53 @@ module API present response, with: Entities::InternalPostReceive::Response end + + post '/two_factor_config' do + status 200 + + break { success: false } unless Feature.enabled?(:two_factor_for_cli) + + actor.update_last_used_at! + user = actor.user + + error_message = validate_actor_key(actor, params[:key_id]) + + if error_message + { success: false, message: error_message } + elsif actor.key.is_a?(DeployKey) + { success: true, two_factor_required: false } + else + { + success: true, + two_factor_required: user.two_factor_enabled? + } + end + end + + post '/two_factor_otp_check' do + status 200 + + break { success: false } unless Feature.enabled?(:two_factor_for_cli) + + actor.update_last_used_at! + user = actor.user + + error_message = validate_actor_key(actor, params[:key_id]) + + break { success: false, message: error_message } if error_message + + break { success: false, message: 'Deploy keys cannot be used for Two Factor' } if actor.key.is_a?(DeployKey) + + break { success: false, message: 'Two-factor authentication is not enabled for this user' } unless user.two_factor_enabled? + + otp_validation_result = ::Users::ValidateOtpService.new(user).execute(params.fetch(:otp_attempt)) + + if otp_validation_result[:status] == :success + { success: true } + else + { success: false, message: 'Invalid OTP' } + end + end end end end diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index 6d5dfd086e7..8175b81f900 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -3,7 +3,7 @@ module API # Kubernetes Internal API module Internal - class Kubernetes < Grape::API::Instance + class Kubernetes < ::API::Base before do check_feature_enabled authenticate_gitlab_kas_request! diff --git a/lib/api/internal/lfs.rb b/lib/api/internal/lfs.rb new file mode 100644 index 00000000000..630f0ec77a8 --- /dev/null +++ b/lib/api/internal/lfs.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module API + module Internal + class Lfs < ::API::Base + use Rack::Sendfile + + before { authenticate_by_gitlab_shell_token! } + + helpers do + def find_lfs_object(lfs_oid) + LfsObject.find_by_oid(lfs_oid) + end + end + + namespace 'internal' do + namespace 'lfs' do + desc 'Get LFS URL for object ID' do + detail 'This feature was introduced in GitLab 13.5.' + end + params do + requires :oid, type: String, desc: 'The object ID to query' + requires :gl_repository, type: String, desc: "Project identifier (e.g. project-1)" + end + get "/" do + lfs_object = find_lfs_object(params[:oid]) + + not_found! unless lfs_object + + _, project, repo_type = Gitlab::GlRepository.parse(params[:gl_repository]) + + not_found! unless repo_type.project? && project + not_found! unless lfs_object.project_allowed_access?(project) + + file = lfs_object.file + + not_found! unless file&.exists? + + content_type 'application/octet-stream' + + if file.file_storage? + sendfile file.path + else + workhorse_headers = Gitlab::Workhorse.send_url(file.url) + header workhorse_headers[0], workhorse_headers[1] + env['api.format'] = :binary + body "" + end + end + end + end + end + end +end diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb index 5f8d23f15fa..51136144c19 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::Instance + class Pages < ::API::Base before do authenticate_gitlab_pages_request! end diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb index 6cc5b344f47..db4979c9052 100644 --- a/lib/api/issue_links.rb +++ b/lib/api/issue_links.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class IssueLinks < Grape::API::Instance + class IssueLinks < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 0e5b0fae6e2..143f9e40736 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::Instance + class Issues < ::API::Base include PaginationParams helpers Helpers::IssuesHelpers helpers Helpers::RateLimiter @@ -231,9 +231,6 @@ module API authorize! :create_issue, user_project - params.delete(:created_at) unless current_user.can?(:set_issue_created_at, user_project) - params.delete(:iid) unless current_user.can?(:set_issue_iid, user_project) - issue_params = declared_params(include_missing: false) issue_params[:system_note_timestamp] = params[:created_at] @@ -279,8 +276,6 @@ module API issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) authorize! :update_issue, issue - # Setting updated_at is allowed only for admins and owners - params.delete(:updated_at) unless current_user.can?(:set_issue_updated_at, user_project) issue.system_note_timestamp = params[:updated_at] update_params = declared_params(include_missing: false).merge(request: request, api: true) diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index bc7bc956580..536b361b308 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::Instance + class JobArtifacts < ::API::Base 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 ad46d948f3b..bdb23b4a9be 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::Instance + class Jobs < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/keys.rb b/lib/api/keys.rb index c014641ca04..2e4568029b5 100644 --- a/lib/api/keys.rb +++ b/lib/api/keys.rb @@ -2,7 +2,7 @@ module API # Keys API - class Keys < Grape::API::Instance + class Keys < ::API::Base before { authenticate! } resource :keys do diff --git a/lib/api/labels.rb b/lib/api/labels.rb index edf4a8ca14e..0cc9f33bd07 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::Instance + class Labels < ::API::Base include PaginationParams helpers ::API::Helpers::LabelHelpers diff --git a/lib/api/lint.rb b/lib/api/lint.rb index f7796b1e969..bfd152f70b1 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -1,24 +1,48 @@ # frozen_string_literal: true module API - class Lint < Grape::API::Instance + class Lint < ::API::Base namespace :ci do desc 'Validation of .gitlab-ci.yml content' params do requires :content, type: String, desc: 'Content of .gitlab-ci.yml' + optional :include_merged_yaml, type: Boolean, desc: 'Whether or not to include merged CI config yaml in the response' end post '/lint' do - error = Gitlab::Ci::YamlProcessor.validation_message(params[:content], - user: current_user) + result = Gitlab::Ci::YamlProcessor.new(params[:content], user: current_user).execute + error = result.errors.first status 200 - if error.blank? - { status: 'valid', errors: [] } - else - { status: 'invalid', errors: [error] } + response = if error.blank? + { status: 'valid', errors: [] } + else + { status: 'invalid', errors: [error] } + end + + response.tap do |response| + response[:merged_yaml] = result.merged_yaml if params[:include_merged_yaml] end end end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Validation of .gitlab-ci.yml content' do + detail 'This feature was introduced in GitLab 13.5.' + end + params do + optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.' + end + get ':id/ci/lint' do + authorize! :download_code, user_project + + content = user_project.repository.gitlab_ci_yml_for(user_project.commit.id, user_project.ci_config_path_or_default) + result = Gitlab::Ci::Lint + .new(project: user_project, current_user: current_user) + .validate(content, dry_run: params[:dry_run]) + + present result, with: Entities::Ci::Lint::Result, current_user: current_user + end + end end end diff --git a/lib/api/markdown.rb b/lib/api/markdown.rb index a0822271cca..97549abd273 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::Instance + class Markdown < ::API::Base 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 index e6d9a9a7c20..a3e2fa84c32 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module API - class MavenPackages < Grape::API::Instance + class MavenPackages < ::API::Base MAVEN_ENDPOINT_REQUIREMENTS = { file_name: API::NO_SLASH_URL_PART_REGEX }.freeze @@ -32,10 +32,10 @@ module API end def verify_package_file(package_file, uploaded_file) - stored_sha1 = Digest::SHA256.hexdigest(package_file.file_sha1) - expected_sha1 = uploaded_file.sha256 + stored_sha256 = Digest::SHA256.hexdigest(package_file.file_sha1) + expected_sha256 = uploaded_file.sha256 - if stored_sha1 == expected_sha1 + if stored_sha256 == expected_sha256 no_content! else conflict! @@ -107,7 +107,7 @@ module API when 'sha1' package_file.file_sha1 else - package_event('pull_package') if jar_file?(format) + track_package_event('pull_package', :maven) if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end end @@ -145,7 +145,7 @@ module API when 'sha1' package_file.file_sha1 else - package_event('pull_package') if jar_file?(format) + track_package_event('pull_package', :maven) if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end @@ -181,7 +181,7 @@ module API when 'sha1' package_file.file_sha1 else - package_event('pull_package') if jar_file?(format) + track_package_event('pull_package', :maven) if jar_file?(format) present_carrierwave_file_with_head_support!(package_file.file) end @@ -231,9 +231,9 @@ module API verify_package_file(package_file, params[:file]) when 'md5' - nil + '' else - package_event('push_package') if jar_file?(format) + track_package_event('push_package', :maven) if jar_file?(format) file_params = { file: params[:file], diff --git a/lib/api/members.rb b/lib/api/members.rb index 4edf94c6350..c28b3b1cc7c 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::Instance + class Members < ::API::Base include PaginationParams before { authenticate! } @@ -88,8 +88,8 @@ module API success Entities::Member end params do - requires :user_id, type: Integer, desc: 'The user ID of the new member' requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)' + requires :user_id, types: [Integer, String], desc: 'The user ID of the new member or multiple IDs separated by commas.' optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' end # rubocop: disable CodeReuse/ActiveRecord @@ -97,20 +97,26 @@ module API source = find_source(source_type, params[:id]) authorize_admin_source!(source_type, source) - member = source.members.find_by(user_id: params[:user_id]) - conflict!('Member already exists') if member + if params[:user_id].to_s.include?(',') + create_service_params = params.except(:user_id).merge({ user_ids: params[:user_id] }) - user = User.find_by_id(params[:user_id]) - not_found!('User') unless user + ::Members::CreateService.new(current_user, create_service_params).execute(source) + elsif params[:user_id].present? + member = source.members.find_by(user_id: params[:user_id]) + conflict!('Member already exists') if member - member = create_member(current_user, user, source, params) + user = User.find_by_id(params[:user_id]) + not_found!('User') unless user - if !member - not_allowed! # This currently can only be reached in EE - elsif member.valid? && member.persisted? - present_members(member) - else - render_validation_error!(member) + member = create_member(current_user, user, source, params) + + if !member + not_allowed! # This currently can only be reached in EE + elsif member.valid? && member.persisted? + present_members(member) + else + render_validation_error!(member) + end end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb index 035ed9f0e04..14d6e3995ea 100644 --- a/lib/api/merge_request_approvals.rb +++ b/lib/api/merge_request_approvals.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class MergeRequestApprovals < ::Grape::API::Instance + class MergeRequestApprovals < ::API::Base before { authenticate_non_get! } helpers do diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index 3e43fe8b257..22023888bbd 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::Instance + class MergeRequestDiffs < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 4bd72b267a9..b24dd870c8b 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::Instance + class MergeRequests < ::API::Base include PaginationParams CONTEXT_COMMITS_POST_LIMIT = 20 diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb index e07762ac6d3..b6bc0af2202 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::Instance + class Annotations < ::API::Base 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 263d2394276..cb6e7099247 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::Instance + class UserStarredDashboards < ::API::Base resource :projects do desc 'Marks selected metrics dashboard as starred' do success Entities::Metrics::UserStarredDashboard diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index e1f279df045..f98a1f6dd1d 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::Instance + class Namespaces < ::API::Base include PaginationParams before { authenticate! } @@ -32,7 +32,9 @@ module API get do namespaces = current_user.admin ? Namespace.all : current_user.namespaces - namespaces = namespaces.include_gitlab_subscription if Gitlab.ee? + namespaces = namespaces.include_route + + namespaces = namespaces.include_gitlab_subscription_with_hosted_plan if Gitlab.ee? namespaces = namespaces.search(params[:search]) if params[:search].present? diff --git a/lib/api/notes.rb b/lib/api/notes.rb index e4989243f3d..0db537ca616 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::Instance + class Notes < ::API::Base include PaginationParams helpers ::API::Helpers::NotesHelpers diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index f8b621c1c38..bad3f5ead7a 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::Instance + class NotificationSettings < ::API::Base before { authenticate! } helpers ::API::Helpers::MembersHelpers diff --git a/lib/api/npm_packages.rb b/lib/api/npm_packages.rb index fca405b76b7..1443b28c1ee 100644 --- a/lib/api/npm_packages.rb +++ b/lib/api/npm_packages.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true module API - class NpmPackages < Grape::API::Instance + class NpmPackages < ::API::Base helpers ::API::Helpers::PackagesHelpers helpers ::API::Helpers::Packages::DependencyProxyHelpers @@ -141,7 +141,7 @@ module API package_file = ::Packages::PackageFileFinder .new(package, params[:file_name]).execute! - package_event('pull_package') + track_package_event('pull_package', package) present_carrierwave_file!(package_file.file) end @@ -157,7 +157,7 @@ module API put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do authorize_create_package!(user_project) - package_event('push_package') + track_package_event('push_package', :npm) created_package = ::Packages::Npm::CreatePackageService .new(user_project, current_user, params.merge(build: current_authenticated_job)).execute diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb index f84a3acbe6d..0f2c956a9df 100644 --- a/lib/api/nuget_packages.rb +++ b/lib/api/nuget_packages.rb @@ -6,7 +6,7 @@ # called by the NuGet package manager client when users run commands # like `nuget install` or `nuget push`. module API - class NugetPackages < Grape::API::Instance + class NugetPackages < ::API::Base helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers @@ -42,7 +42,7 @@ module API def package_finder(finder_params = {}) ::Packages::Nuget::PackageFinder.new( authorized_user_project, - finder_params.merge(package_name: params[:package_name]) + **finder_params.merge(package_name: params[:package_name]) ) end end @@ -73,7 +73,7 @@ module API get 'index', format: :json do authorize_read_package!(authorized_user_project) - track_event('nuget_service_index') + track_package_event('cli_metadata', :nuget) present ::Packages::Nuget::ServiceIndexPresenter.new(authorized_user_project), with: ::API::Entities::Nuget::ServiceIndex @@ -105,7 +105,7 @@ module API package_file = ::Packages::CreatePackageFileService.new(package, file_params) .execute - package_event('push_package') + track_package_event('push_package', :nuget) ::Packages::Nuget::ExtractionWorker.perform_async(package_file.id) # rubocop:disable CodeReuse/Worker @@ -198,7 +198,7 @@ module API not_found!('Package') unless package_file - package_event('pull_package') + track_package_event('pull_package', :nuget) # 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) @@ -233,7 +233,7 @@ module API .new(authorized_user_project, params[:q], search_options) .execute - package_event('search_package') + track_package_event('search_package', :nuget) present ::Packages::Nuget::SearchResultsPresenter.new(search), with: ::API::Entities::Nuget::SearchResults diff --git a/lib/api/package_files.rb b/lib/api/package_files.rb index 17b92df629c..c1fc9a6e4d8 100644 --- a/lib/api/package_files.rb +++ b/lib/api/package_files.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class PackageFiles < Grape::API::Instance + class PackageFiles < ::API::Base include PaginationParams before do diff --git a/lib/api/pages.rb b/lib/api/pages.rb index 79a6b527581..813307c498f 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::Instance + class Pages < ::API::Base 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 7d27b575efa..00c51298c45 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::Instance + class PagesDomains < ::API::Base include PaginationParams PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb index 0e5605984e6..46ccb4ba1a0 100644 --- a/lib/api/project_clusters.rb +++ b/lib/api/project_clusters.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectClusters < Grape::API::Instance + class ProjectClusters < ::API::Base include PaginationParams before { authenticate! } @@ -45,6 +45,7 @@ module API 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 :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace' 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 @@ -78,6 +79,7 @@ module API 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 :namespace_per_environment, default: true, type: Boolean, desc: 'Deploy each environment to a separate Kubernetes namespace' 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' diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb index 8f2a62bc5a4..d565531d372 100644 --- a/lib/api/project_container_repositories.rb +++ b/lib/api/project_container_repositories.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true module API - class ProjectContainerRepositories < Grape::API::Instance + class ProjectContainerRepositories < ::API::Base include PaginationParams + helpers ::API::Helpers::PackagesHelpers REPOSITORY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge( tag_name: API::NO_SLASH_URL_PART_REGEX) - before { error!('404 Not Found', 404) unless Feature.enabled?(:container_registry_api, user_project, default_enabled: true) } before { authorize_read_container_images! } params do @@ -28,7 +28,7 @@ module API user: current_user, subject: user_project ).execute - track_event( 'list_repositories') + track_package_event('list_repositories', :container) present paginate(repositories), with: Entities::ContainerRegistry::Repository, tags: params[:tags], tags_count: params[:tags_count] end @@ -43,7 +43,7 @@ module API authorize_admin_container_image! DeleteContainerRepositoryWorker.perform_async(current_user.id, repository.id) # rubocop:disable CodeReuse/Worker - track_event('delete_repository') + track_package_event('delete_repository', :container) status :accepted end @@ -60,7 +60,7 @@ module API authorize_read_container_image! tags = Kaminari.paginate_array(repository.tags) - track_event('list_tags') + track_package_event('list_tags', :container) present paginate(tags), with: Entities::ContainerRegistry::Tag end @@ -89,7 +89,7 @@ module API declared_params.except(:repository_id).merge(container_expiration_policy: false)) # rubocop:enable CodeReuse/Worker - track_event('delete_tag_bulk') + track_package_event('delete_tag_bulk', :container) status :accepted end @@ -125,7 +125,7 @@ module API .execute(repository) if result[:status] == :success - track_event('delete_tag') + track_package_event('delete_tag', :container) status :ok else diff --git a/lib/api/project_events.rb b/lib/api/project_events.rb index 726e693826e..3765473bc0e 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::Instance + class ProjectEvents < ::API::Base include PaginationParams include APIGuard helpers ::API::Helpers::EventsHelpers diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index 377d61689b3..184f89200ab 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::Instance + class ProjectExport < ::API::Base helpers Helpers::RateLimiter before do @@ -55,7 +55,7 @@ module API export_strategy = if after_export_params[:url].present? params = after_export_params.slice(:url, :http_method).symbolize_keys - Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy.new(params) + Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy.new(**params) end if export_strategy&.invalid? diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index e68a3b106b1..bc2d8c816a8 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::Instance + class ProjectHooks < ::API::Base include PaginationParams before { authenticate! } @@ -104,7 +104,9 @@ module API delete ":id/hooks/:hook_id" do hook = user_project.hooks.find(params.delete(:hook_id)) - destroy_conditionally!(hook) + destroy_conditionally!(hook) do + WebHooks::DestroyService.new(current_user).execute(hook) + end end end end diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 9f43c3c7993..5c4e1d73ee1 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true module API - class ProjectImport < Grape::API::Instance + class ProjectImport < ::API::Base include PaginationParams - MAXIMUM_FILE_SIZE = 50.megabytes - helpers Helpers::ProjectsHelpers helpers Helpers::FileUploadHelpers helpers Helpers::RateLimiter diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb index 2f8dd1085dc..a81118f44bd 100644 --- a/lib/api/project_milestones.rb +++ b/lib/api/project_milestones.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectMilestones < Grape::API::Instance + class ProjectMilestones < ::API::Base include PaginationParams include MilestoneResponses diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb index 359514f1f78..b8d97b1243a 100644 --- a/lib/api/project_packages.rb +++ b/lib/api/project_packages.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ProjectPackages < Grape::API::Instance + class ProjectPackages < ::API::Base include PaginationParams before do diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb index c318907542b..38eb74663d3 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::Instance + class ProjectRepositoryStorageMoves < ::API::Base include PaginationParams before { authenticated_as_admin! } @@ -69,7 +69,7 @@ module API success Entities::ProjectRepositoryStorageMove end params do - requires :destination_storage_name, type: String, desc: 'The destination storage shard' + optional :destination_storage_name, type: String, desc: 'The destination storage shard' end post ':id/repository_storage_moves' do storage_move = user_project.repository_storage_moves.build( diff --git a/lib/api/project_snapshots.rb b/lib/api/project_snapshots.rb index 360000861fc..e19afb6e8e4 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::Instance + class ProjectSnapshots < ::API::Base helpers ::API::Helpers::ProjectSnapshotsHelpers before { authorize_read_git_snapshot! } diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index f6e87fece89..b4de260fe49 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true module API - class ProjectSnippets < Grape::API::Instance + class ProjectSnippets < ::API::Base include PaginationParams - before { authenticate! } before { check_snippets_enabled } params do @@ -37,6 +36,8 @@ module API use :pagination end get ":id/snippets" do + authenticate! + present paginate(snippets_for_current_user), with: Entities::ProjectSnippet, current_user: current_user end @@ -48,6 +49,9 @@ module API end get ":id/snippets/:snippet_id" do snippet = snippets_for_current_user.find(params[:snippet_id]) + + not_found!('Snippet') unless snippet + present snippet, with: Entities::ProjectSnippet, current_user: current_user end @@ -63,6 +67,8 @@ module API use :create_file_params end post ":id/snippets" do + authenticate! + authorize! :create_snippet, user_project snippet_params = process_create_params(declared_params(include_missing: false)) @@ -97,6 +103,8 @@ module API end # rubocop: disable CodeReuse/ActiveRecord put ":id/snippets/:snippet_id" do + authenticate! + snippet = snippets_for_current_user.find_by(id: params.delete(:snippet_id)) not_found!('Snippet') unless snippet @@ -125,6 +133,8 @@ module API end # rubocop: disable CodeReuse/ActiveRecord delete ":id/snippets/:snippet_id" do + authenticate! + snippet = snippets_for_current_user.find_by(id: params[:snippet_id]) not_found!('Snippet') unless snippet diff --git a/lib/api/project_statistics.rb b/lib/api/project_statistics.rb index 2196801096f..1ead969fc81 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::Instance + class ProjectStatistics < ::API::Base before do authenticate! authorize! :daily_statistics, user_project diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb index 48c3dbed3b0..7d851de0237 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::Instance + class ProjectTemplates < ::API::Base include PaginationParams TEMPLATE_TYPES = %w[dockerfiles gitignores gitlab_ci_ymls licenses metrics_dashboard_ymls issues merge_requests].freeze diff --git a/lib/api/projects.rb b/lib/api/projects.rb index abbdb11a3f7..ecee76ae60c 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::Instance + class Projects < ::API::Base include PaginationParams include Helpers::CustomAttributes @@ -353,7 +353,7 @@ module API optional :path, type: String, desc: 'The path of the repository' use :optional_project_params - use :optional_update_params_ee + use :optional_update_params at_least_one_of(*Helpers::ProjectsHelpers.update_params_at_least_one_of) end diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index b0a7f898eec..a448682d8bd 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::Instance + class ProtectedBranches < ::API::Base 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 aaa31cb7cc6..dd3e407ffc9 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::Instance + class ProtectedTags < ::API::Base 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 index c07db68f8a8..5622bc6e42d 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -6,7 +6,7 @@ # called by the PyPI package manager client when users run commands # like `pip install` or `twine upload`. module API - class PypiPackages < Grape::API::Instance + class PypiPackages < ::API::Base helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::RelatedResourcesHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers @@ -33,7 +33,7 @@ module API def find_package_versions packages = packages_finder - .with_name(params[:package_name]) + .with_normalized_pypi_name(params[:package_name]) not_found!('Package') if packages.empty? @@ -72,7 +72,7 @@ module API 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 - package_event('pull_package') + track_package_event('pull_package', :pypi) present_carrierwave_file!(package_file.file, supports_direct_download: true) end @@ -91,7 +91,7 @@ module API get 'simple/*package_name', format: :txt do authorize_read_package!(authorized_user_project) - package_event('list_package') + track_package_event('list_package', :pypi) packages = find_package_versions presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project) @@ -122,7 +122,7 @@ module API authorize_upload!(authorized_user_project) bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:pypi_max_file_size, params[:content].size) - package_event('push_package') + track_package_event('push_package', :pypi) ::Packages::Pypi::CreatePackageService .new(authorized_user_project, current_user, declared_params) diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index 9624b8924e5..23de9f9fc9f 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::Instance + class Links < ::API::Base include PaginationParams RELEASE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS diff --git a/lib/api/releases.rb b/lib/api/releases.rb index 3c38721129f..3bd6ea77403 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::Instance + class Releases < ::API::Base include PaginationParams RELEASE_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS @@ -19,9 +19,13 @@ module API end params do use :pagination + optional :order_by, type: String, values: %w[released_at created_at], default: 'released_at', + desc: 'Return releases ordered by `released_at` or `created_at`.' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return releases sorted in `asc` or `desc` order.' end get ':id/releases' do - releases = ::ReleasesFinder.new(user_project, current_user).execute + releases = ::ReleasesFinder.new(user_project, current_user, declared_params.slice(:order_by, :sort)).execute present paginate(releases), with: Entities::Release, current_user: current_user end @@ -152,7 +156,7 @@ module API end def authorize_create_evidence! - # This is a separate method so that EE can extend its behaviour + # extended in EE end def release @@ -160,15 +164,15 @@ module API end def log_release_created_audit_event(release) - # This is a separate method so that EE can extend its behaviour + # extended in EE end def log_release_updated_audit_event - # This is a separate method so that EE can extend its behaviour + # extended in EE end def log_release_milestones_updated_audit_event - # This is a separate method so that EE can extend its behaviour + # extended in EE end end end diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb index d1def05808b..f63ea04a529 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::Instance + class RemoteMirrors < ::API::Base include PaginationParams before do diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 81702f8f02a..38ac1f22a48 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -3,7 +3,7 @@ require 'mime/types' module API - class Repositories < Grape::API::Instance + class Repositories < ::API::Base include PaginationParams helpers ::API::Helpers::HeadersHelpers diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb index a8d3419528c..d3a219f0810 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::Instance + class ResourceLabelEvents < ::API::Base include PaginationParams helpers ::API::Helpers::NotesHelpers diff --git a/lib/api/resource_milestone_events.rb b/lib/api/resource_milestone_events.rb index a8f221f8740..21411f68dd5 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::Instance + class ResourceMilestoneEvents < ::API::Base include PaginationParams helpers ::API::Helpers::NotesHelpers diff --git a/lib/api/resource_state_events.rb b/lib/api/resource_state_events.rb index 1c1a90c09a3..9bfda39be90 100644 --- a/lib/api/resource_state_events.rb +++ b/lib/api/resource_state_events.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class ResourceStateEvents < Grape::API::Instance + class ResourceStateEvents < ::API::Base include PaginationParams helpers ::API::Helpers::NotesHelpers diff --git a/lib/api/search.rb b/lib/api/search.rb index b9c6a823f4f..85f0a8e2e60 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::Instance + class Search < ::API::Base include PaginationParams before { authenticate! } @@ -33,6 +33,7 @@ module API scope: params[:scope], search: params[:search], state: params[:state], + confidential: params[:confidential], snippets: snippets?, page: params[:page], per_page: params[:per_page] @@ -62,12 +63,6 @@ module API # Defining this method here as a noop allows us to easily extend it in # EE, without having to modify this file directly. end - - def check_users_search_allowed! - if params[:scope].to_sym == :users && Feature.disabled?(:users_search, default_enabled: true) - render_api_error!({ error: _("Scope not supported with disabled 'users_search' feature!") }, 400) - end - end end resource :search do @@ -81,11 +76,11 @@ module API desc: 'The scope of the search', values: Helpers::SearchHelpers.global_search_scopes optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states + optional :confidential, type: Boolean, desc: 'Filter results by confidentiality' use :pagination end get do verify_search_scope!(resource: nil) - check_users_search_allowed! present search, with: entity end @@ -103,11 +98,11 @@ module API desc: 'The scope of the search', values: Helpers::SearchHelpers.group_search_scopes optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states + optional :confidential, type: Boolean, desc: 'Filter results by confidentiality' use :pagination end get ':id/(-/)search' do verify_search_scope!(resource: user_group) - check_users_search_allowed! present search(group_id: user_group.id), with: entity end @@ -126,11 +121,10 @@ module API values: Helpers::SearchHelpers.project_search_scopes optional :ref, type: String, desc: 'The name of a repository branch or tag. If not given, the default branch is used' optional :state, type: String, desc: 'Filter results by state', values: Helpers::SearchHelpers.search_states + optional :confidential, type: Boolean, desc: 'Filter results by confidentiality' use :pagination end get ':id/(-/)search' do - check_users_search_allowed! - present search({ project_id: user_project.id, repository_ref: params[:ref] }), with: entity end end diff --git a/lib/api/services.rb b/lib/api/services.rb index 9ee1822339c..5f3d14010a8 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::Instance + class Services < ::API::Base services = Helpers::ServicesHelpers.services service_classes = Helpers::ServicesHelpers.service_classes diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 6e5534d0c9a..dc917d9c529 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::Instance + class Settings < ::API::Base before { authenticated_as_admin! } helpers Helpers::SettingsHelpers @@ -29,7 +29,8 @@ module API success Entities::ApplicationSetting end params do - optional :admin_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.' + optional :admin_notification_email, type: String, desc: 'Deprecated: Use :abuse_notification_email instead. Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.' + optional :abuse_notification_email, type: String, desc: 'Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.' optional :after_sign_up_text, type: String, desc: 'Text shown after sign up' optional :after_sign_out_path, type: String, desc: 'We will redirect users to this page after they sign out' optional :akismet_enabled, type: Boolean, desc: 'Helps prevent bots from creating issues' @@ -73,6 +74,7 @@ module API optional :gravatar_enabled, type: Boolean, desc: 'Flag indicating if the Gravatar service is enabled' optional :help_page_hide_commercial_content, type: Boolean, desc: 'Hide marketing-related entries from help' optional :help_page_support_url, type: String, desc: 'Alternate support URL for help page and help dropdown' + optional :help_page_documentation_base_url, type: String, desc: 'Alternate documentation pages URL' optional :help_page_text, type: String, desc: 'Custom text displayed on the help page' optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page' optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)' @@ -194,6 +196,11 @@ module API attrs[:allow_local_requests_from_web_hooks_and_services] = attrs.delete(:allow_local_requests_from_hooks_and_services) end + # support legacy names, can be removed in v5 + if attrs.has_key?(:admin_notification_email) + attrs[:abuse_notification_email] = attrs.delete(:admin_notification_email) + end + # since 13.0 it's not possible to disable hashed storage - support can be removed in 14.0 attrs.delete(:hashed_storage_enabled) if attrs.has_key?(:hashed_storage_enabled) diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb index 77f2b1e871e..b025dbfab37 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::Instance + class SidekiqMetrics < ::API::Base before { authenticated_as_admin! } helpers do diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index c6ef35875fc..2e67b9649bc 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -2,11 +2,9 @@ module API # Snippets API - class Snippets < Grape::API::Instance + class Snippets < ::API::Base include PaginationParams - before { authenticate! } - resource :snippets do helpers Helpers::SnippetsHelpers helpers do @@ -23,7 +21,7 @@ module API end end - desc 'Get a snippets list for authenticated user' do + desc 'Get a snippets list for an authenticated user' do detail 'This feature was introduced in GitLab 8.15.' success Entities::Snippet end @@ -31,6 +29,8 @@ module API use :pagination end get do + authenticate! + present paginate(snippets_for_current_user), with: Entities::Snippet, current_user: current_user end @@ -42,6 +42,8 @@ module API use :pagination end get 'public' do + authenticate! + present paginate(public_snippets), with: Entities::PersonalSnippet, current_user: current_user end @@ -74,6 +76,8 @@ module API use :create_file_params end post do + authenticate! + authorize! :create_snippet attrs = process_create_params(declared_params(include_missing: false)) @@ -109,6 +113,8 @@ module API use :minimum_update_params end put ':id' do + authenticate! + snippet = snippets_for_current_user.find_by_id(params.delete(:id)) break not_found!('Snippet') unless snippet @@ -139,6 +145,8 @@ module API requires :id, type: Integer, desc: 'The ID of a snippet' end delete ':id' do + authenticate! + snippet = snippets_for_current_user.find_by_id(params.delete(:id)) break not_found!('Snippet') unless snippet diff --git a/lib/api/statistics.rb b/lib/api/statistics.rb index 3869fd3ac76..fa7176491ba 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::Instance + class Statistics < ::API::Base 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 34d21d3d7d8..e2ceb49c119 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::Instance + class Submodules < ::API::Base before { authenticate! } helpers do diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb index 533663fb087..35a28da4736 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::Instance + class Subscriptions < ::API::Base helpers ::API::Helpers::LabelHelpers before { authenticate! } diff --git a/lib/api/suggestions.rb b/lib/api/suggestions.rb index 38e96c080f2..f23d279c3f4 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::Instance + class Suggestions < ::API::Base before { authenticate! } resource :suggestions do diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index d8e0a425625..2820d305d0f 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::Instance + class SystemHooks < ::API::Base include PaginationParams before do @@ -70,7 +70,9 @@ module API hook = SystemHook.find_by(id: params[:id]) not_found!('System hook') unless hook - destroy_conditionally!(hook) + destroy_conditionally!(hook) do + WebHooks::DestroyService.new(current_user).execute(hook) + end end # rubocop: enable CodeReuse/ActiveRecord end diff --git a/lib/api/tags.rb b/lib/api/tags.rb index c1fbd3ca7c6..b969394ec47 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::Instance + class Tags < ::API::Base 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 80a97aae429..0b427bbf5b9 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::Instance + class Templates < ::API::Base include PaginationParams GLOBAL_TEMPLATE_TYPES = { diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index 7063a3d08b5..4168cce21ef 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -4,7 +4,7 @@ require_dependency 'api/validations/validators/limit' module API module Terraform - class State < Grape::API::Instance + class State < ::API::Base include ::Gitlab::Utils::StrongMemoize default_format :json diff --git a/lib/api/terraform/state_version.rb b/lib/api/terraform/state_version.rb new file mode 100644 index 00000000000..b4a0efd7a2b --- /dev/null +++ b/lib/api/terraform/state_version.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module API + module Terraform + class StateVersion < ::API::Base + default_format :json + + before do + authenticate! + authorize! :read_terraform_state, user_project + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + namespace ':id/terraform/state/:name/versions/:serial' do + params do + requires :name, type: String, desc: 'The name of a Terraform state' + requires :serial, type: Integer, desc: 'The version number of the state' + end + + helpers do + def remote_state_handler + ::Terraform::RemoteStateHandler.new(user_project, current_user, name: params[:name]) + end + + def find_version(serial) + remote_state_handler.find_with_lock do |state| + version = state.versions.find_by_version(serial) + + if version.present? + yield version + else + not_found! + end + end + end + end + + desc 'Get a terraform state version' + route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth + get do + find_version(params[:serial]) do |version| + env['api.format'] = :binary # Bypass json serialization + body version.file.read + status :ok + end + end + + desc 'Delete a terraform state version' + route_setting :authentication, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth + delete do + authorize! :admin_terraform_state, user_project + + find_version(params[:serial]) do |version| + version.destroy! + + body false + status :no_content + end + end + end + end + end + end +end diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 5eae92a251e..ce07d13cc9a 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::Instance + class Todos < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index f398bbf3e32..960d004a04c 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::Instance + class Triggers < ::API::Base include PaginationParams HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase diff --git a/lib/api/unleash.rb b/lib/api/unleash.rb new file mode 100644 index 00000000000..907422118f1 --- /dev/null +++ b/lib/api/unleash.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module API + class Unleash < ::API::Base + include PaginationParams + + namespace :feature_flags do + resource :unleash, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + params do + requires :project_id, type: String, desc: 'The ID of a project' + optional :instance_id, type: String, desc: 'The Instance ID of Unleash Client' + optional :app_name, type: String, desc: 'The Application Name of Unleash Client' + end + route_param :project_id do + before do + authorize_by_unleash_instance_id! + end + + get do + # not supported yet + status :ok + end + + desc 'Get a list of features (deprecated, v2 client support)' + get 'features' do + present :version, 1 + present :features, feature_flags, with: ::API::Entities::UnleashFeature + end + + desc 'Get a list of features' + get 'client/features' do + present :version, 1 + present :features, feature_flags, with: ::API::Entities::UnleashFeature + end + + post 'client/register' do + # not supported yet + status :ok + end + + post 'client/metrics' do + # not supported yet + status :ok + end + end + end + end + + helpers do + def project + @project ||= find_project(params[:project_id]) + end + + def unleash_instance_id + env['HTTP_UNLEASH_INSTANCEID'] || params[:instance_id] + end + + def unleash_app_name + env['HTTP_UNLEASH_APPNAME'] || params[:app_name] + end + + def authorize_by_unleash_instance_id! + unauthorized! unless Operations::FeatureFlagsClient + .find_for_project_and_token(project, unleash_instance_id) + end + + def feature_flags + return [] unless unleash_app_name.present? + + legacy_flags = Operations::FeatureFlagScope.for_unleash_client(project, unleash_app_name) + new_version_flags = Operations::FeatureFlag.for_unleash_client(project, unleash_app_name) + + legacy_flags + new_version_flags + end + end + end +end diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb index a1512197ee1..fa5bfc1cbe9 100644 --- a/lib/api/usage_data.rb +++ b/lib/api/usage_data.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module API - class UsageData < Grape::API::Instance + class UsageData < ::API::Base before { authenticate! } namespace 'usage_data' do before do - not_found! unless Feature.enabled?(:usage_data_api) + not_found! unless Feature.enabled?(:usage_data_api, default_enabled: true) forbidden!('Invalid CSRF token is provided') unless verified_request? end diff --git a/lib/api/user_counts.rb b/lib/api/user_counts.rb index 90127ecbc73..6d9db53fec8 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::Instance + class UserCounts < ::API::Base 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 73bb43b88fc..e7c1d644324 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::Instance + class Users < ::API::Base include PaginationParams include APIGuard include Helpers::CustomAttributes @@ -348,7 +348,7 @@ module API end # rubocop: enable CodeReuse/ActiveRecord - desc 'Get the GPG keys of a specified user. Available only for admins.' do + desc 'Get the GPG keys of a specified user.' do detail 'This feature was added in GitLab 10.0' success Entities::GpgKey end @@ -358,8 +358,6 @@ module API end # rubocop: disable CodeReuse/ActiveRecord get ':id/gpg_keys' do - authenticated_as_admin! - user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -367,6 +365,26 @@ module API end # rubocop: enable CodeReuse/ActiveRecord + desc 'Get a specific GPG key for a given user.' do + detail 'This feature was added in GitLab 13.5' + success Entities::GpgKey + end + params do + requires :id, type: Integer, desc: 'The ID of the user' + requires :key_id, type: Integer, desc: 'The ID of the GPG key' + end + # rubocop: disable CodeReuse/ActiveRecord + get ':id/gpg_keys/:key_id' do + user = User.find_by(id: params[:id]) + not_found!('User') unless user + + key = user.gpg_keys.find_by(id: params[:key_id]) + not_found!('GPG Key') unless key + + present key, with: Entities::GpgKey + end + # rubocop: enable CodeReuse/ActiveRecord + desc 'Delete an existing GPG key from a specified user. Available only for admins.' do detail 'This feature was added in GitLab 10.0' end @@ -529,10 +547,15 @@ module API unless user.can_be_deactivated? forbidden!('A blocked user cannot be deactivated by the API') if user.blocked? + forbidden!('An internal user cannot be deactivated by the API') if user.internal? forbidden!("The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated") end - user.deactivate + if user.deactivate + true + else + render_api_error!(user.errors.full_messages, 400) + end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb index 593f90460ac..aed88e6091c 100644 --- a/lib/api/v3/github.rb +++ b/lib/api/v3/github.rb @@ -7,7 +7,7 @@ # module API module V3 - class Github < Grape::API::Instance + class Github < ::API::Base NO_SLASH_URL_PART_REGEX = %r{[^/]+}.freeze ENDPOINT_REQUIREMENTS = { namespace: NO_SLASH_URL_PART_REGEX, @@ -51,7 +51,7 @@ module API def find_project_with_access(params) project = find_project!( - ::Gitlab::Jira::Dvcs.restore_full_path(params.slice(:namespace, :project).symbolize_keys) + ::Gitlab::Jira::Dvcs.restore_full_path(**params.slice(:namespace, :project).symbolize_keys) ) not_found! unless can?(current_user, :download_code, project) project diff --git a/lib/api/variables.rb b/lib/api/variables.rb index 0b3ec10f1b4..f5de3d844e6 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::Instance + class Variables < ::API::Base include PaginationParams before { authenticate! } diff --git a/lib/api/version.rb b/lib/api/version.rb index 6a480fc2bd9..841b55f8d6c 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::Instance + class Version < ::API::Base helpers ::API::Helpers::GraphqlHelpers include APIGuard diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 4eba12157bd..21f457046f1 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module API - class Wikis < Grape::API::Instance + class Wikis < ::API::Base helpers ::API::Helpers::WikisHelpers helpers do |