diff options
Diffstat (limited to 'lib/api')
155 files changed, 1248 insertions, 414 deletions
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index 7e3d70a210a..e6ce62a1c6e 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -8,6 +8,8 @@ module API helpers ::API::Helpers::MembersHelpers + feature_category :authentication_and_authorization + %w[group project].each do |source_type| params do requires :id, type: String, desc: "The #{source_type} ID" diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb index 44c389d6f94..654d3a48162 100644 --- a/lib/api/admin/ci/variables.rb +++ b/lib/api/admin/ci/variables.rb @@ -8,6 +8,8 @@ module API before { authenticated_as_admin! } + feature_category :continuous_integration + namespace 'admin' do namespace 'ci' do namespace 'variables' do diff --git a/lib/api/admin/instance_clusters.rb b/lib/api/admin/instance_clusters.rb index ce1bdd65eff..679e231b283 100644 --- a/lib/api/admin/instance_clusters.rb +++ b/lib/api/admin/instance_clusters.rb @@ -5,6 +5,8 @@ module API class InstanceClusters < ::API::Base include PaginationParams + feature_category :kubernetes_management + before do authenticated_as_admin! end diff --git a/lib/api/admin/sidekiq.rb b/lib/api/admin/sidekiq.rb index c2e9de5fb4e..7e561783685 100644 --- a/lib/api/admin/sidekiq.rb +++ b/lib/api/admin/sidekiq.rb @@ -5,6 +5,8 @@ module API class Sidekiq < ::API::Base before { authenticated_as_admin! } + feature_category :not_owned + namespace 'admin' do namespace 'sidekiq' do namespace 'queues' do diff --git a/lib/api/api.rb b/lib/api/api.rb index 84b4d5a5835..ea149f25584 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -25,7 +25,8 @@ module API Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new, Gitlab::GrapeLogging::Loggers::PerfLogger.new, Gitlab::GrapeLogging::Loggers::CorrelationIdLogger.new, - Gitlab::GrapeLogging::Loggers::ContextLogger.new + Gitlab::GrapeLogging::Loggers::ContextLogger.new, + Gitlab::GrapeLogging::Loggers::ContentLogger.new ] allow_access_with_scope :api @@ -48,11 +49,17 @@ module API before do coerce_nil_params_to_array! + api_endpoint = env['api.endpoint'] + feature_category = api_endpoint.options[:for].try(:feature_category_for_app, api_endpoint).to_s + + header[Gitlab::Metrics::RequestsRackMiddleware::FEATURE_CATEGORY_HEADER] = feature_category + Gitlab::ApplicationContext.push( user: -> { @current_user }, project: -> { @project }, namespace: -> { @group }, - caller_id: route.origin + caller_id: route.origin, + feature_category: feature_category ) end @@ -115,7 +122,14 @@ module API format :json formatter :json, Gitlab::Json::GrapeFormatter - content_type :txt, "text/plain" + + # There is a small chance some users depend on the old behavior. + # We this change under a feature flag to see if affects GitLab.com users. + if Gitlab::Database.cached_table_exists?('features') && Feature.enabled?(:api_json_content_type) + content_type :json, 'application/json' + else + content_type :txt, 'text/plain' + end # Ensure the namespace is right, otherwise we might load Grape::API::Helpers helpers ::API::Helpers @@ -147,6 +161,8 @@ module API mount ::API::Commits mount ::API::CommitStatuses mount ::API::ContainerRegistryEvent + mount ::API::ContainerRepositories + mount ::API::DependencyProxy mount ::API::DeployKeys mount ::API::DeployTokens mount ::API::Deployments @@ -171,6 +187,7 @@ module API mount ::API::ImportBitbucketServer mount ::API::ImportGithub mount ::API::IssueLinks + mount ::API::Invitations mount ::API::Issues mount ::API::JobArtifacts mount ::API::Jobs @@ -202,7 +219,8 @@ module API mount ::API::DebianGroupPackages mount ::API::DebianProjectPackages mount ::API::MavenPackages - mount ::API::NpmPackages + mount ::API::NpmProjectPackages + mount ::API::NpmInstancePackages mount ::API::GenericPackages mount ::API::GoProxy mount ::API::Pages @@ -222,6 +240,7 @@ module API mount ::API::ProjectTemplates mount ::API::Terraform::State mount ::API::Terraform::StateVersion + mount ::API::PersonalAccessTokens mount ::API::ProtectedBranches mount ::API::ProtectedTags mount ::API::Releases @@ -266,7 +285,7 @@ module API end end - route :any, '*path' do + route :any, '*path', feature_category: :not_owned do error!('404 Not Found', 404) end end diff --git a/lib/api/appearance.rb b/lib/api/appearance.rb index 00b495bbc1e..fe498bf611b 100644 --- a/lib/api/appearance.rb +++ b/lib/api/appearance.rb @@ -4,6 +4,8 @@ module API class Appearance < ::API::Base before { authenticated_as_admin! } + feature_category :navigation + helpers do def current_appearance @current_appearance ||= (::Appearance.current || ::Appearance.new) diff --git a/lib/api/applications.rb b/lib/api/applications.rb index 2afe8763d9d..8b14e16b495 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -5,6 +5,8 @@ module API class Applications < ::API::Base before { authenticated_as_admin! } + feature_category :authentication_and_authorization + resource :applications do helpers do def validate_redirect_uri(value) diff --git a/lib/api/avatar.rb b/lib/api/avatar.rb index 5a9b9940fcf..a42d89ddf83 100644 --- a/lib/api/avatar.rb +++ b/lib/api/avatar.rb @@ -2,6 +2,8 @@ module API class Avatar < ::API::Base + feature_category :users + 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 6d40ae8f5ff..8ea4f32d3eb 100644 --- a/lib/api/award_emoji.rb +++ b/lib/api/award_emoji.rb @@ -6,9 +6,9 @@ module API before { authenticate! } AWARDABLES = [ - { type: 'issue', find_by: :iid }, - { type: 'merge_request', find_by: :iid }, - { type: 'snippet', find_by: :id } + { type: 'issue', find_by: :iid, feature_category: :issue_tracking }, + { type: 'merge_request', find_by: :iid, feature_category: :code_review }, + { type: 'snippet', find_by: :id, feature_category: :snippets } ].freeze params do @@ -34,7 +34,7 @@ module API params do use :pagination end - get endpoint do + get endpoint, feature_category: awardable_params[:feature_category] do if can_read_awardable? awards = awardable.award_emoji present paginate(awards), with: Entities::AwardEmoji @@ -50,7 +50,7 @@ module API params do requires :award_id, type: Integer, desc: 'The ID of the award' end - get "#{endpoint}/:award_id" do + get "#{endpoint}/:award_id", feature_category: awardable_params[:feature_category] do if can_read_awardable? present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji else @@ -65,7 +65,7 @@ module API params do requires :name, type: String, desc: 'The name of a award_emoji (without colons)' end - post endpoint do + post endpoint, feature_category: awardable_params[:feature_category] do not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable? service = AwardEmojis::AddService.new(awardable, params[:name], current_user).execute @@ -84,7 +84,7 @@ module API params do requires :award_id, type: Integer, desc: 'The ID of an award emoji' end - delete "#{endpoint}/:award_id" do + delete "#{endpoint}/:award_id", feature_category: awardable_params[:feature_category] do award = awardable.award_emoji.find(params[:award_id]) unauthorized! unless award.user == current_user || current_user.admin? diff --git a/lib/api/badges.rb b/lib/api/badges.rb index fc00594c9ec..04f155be4e1 100644 --- a/lib/api/badges.rb +++ b/lib/api/badges.rb @@ -8,6 +8,8 @@ module API helpers ::API::Helpers::BadgesHelpers + feature_category :continuous_integration + helpers do def find_source_if_admin(source_type) source = find_source(source_type, params[:id]) diff --git a/lib/api/base.rb b/lib/api/base.rb index e174cef3bad..33e47c18fcd 100644 --- a/lib/api/base.rb +++ b/lib/api/base.rb @@ -2,5 +2,30 @@ module API class Base < Grape::API::Instance # rubocop:disable API/Base + include ::Gitlab::WithFeatureCategory + + class << self + def feature_category_for_app(app) + feature_category_for_action(path_for_app(app)) + end + + def path_for_app(app) + normalize_path(app.namespace, app.options[:path].first) + end + + def route(methods, paths = ['/'], route_options = {}, &block) + if category = route_options.delete(:feature_category) + feature_category(category, Array(paths).map { |path| normalize_path(namespace, path) }) + end + + super + end + + private + + def normalize_path(namespace, path) + [namespace.presence, path.to_s.chomp('/').presence].compact.join('/') + end + end end end diff --git a/lib/api/boards.rb b/lib/api/boards.rb index d2d1628aff4..f4b23c507f4 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -9,6 +9,8 @@ module API before { authenticate! } + feature_category :boards + helpers do def board_parent user_project @@ -40,6 +42,43 @@ module API authorize!(:read_board, user_project) present board, with: Entities::Board end + + desc 'Create a project board' do + detail 'This feature was introduced in 10.4' + success Entities::Board + end + params do + requires :name, type: String, desc: 'The board name' + end + post '/' do + authorize!(:admin_board, board_parent) + + create_board + end + + desc 'Update a project board' do + detail 'This feature was introduced in 11.0' + success Entities::Board + end + params do + use :update_params + end + put '/:board_id' do + authorize!(:admin_board, board_parent) + + update_board + end + + desc 'Delete a project board' do + detail 'This feature was introduced in 10.4' + success Entities::Board + end + + delete '/:board_id' do + authorize!(:admin_board, board_parent) + + delete_board + end end params do diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb index 6a86c02bf4a..2ae82f78e01 100644 --- a/lib/api/boards_responses.rb +++ b/lib/api/boards_responses.rb @@ -10,6 +10,35 @@ module API board_parent.boards.find(params[:board_id]) end + def create_board + forbidden! unless board_parent.multiple_issue_boards_available? + + response = + ::Boards::CreateService.new(board_parent, current_user, { name: params[:name] }).execute + + present response.payload, with: Entities::Board + end + + def update_board + service = ::Boards::UpdateService.new(board_parent, current_user, declared_params(include_missing: false)) + service.execute(board) + + if board.valid? + present board, with: Entities::Board + else + bad_request!("Failed to save board #{board.errors.messages}") + end + end + + def delete_board + forbidden! unless board_parent.multiple_issue_boards_available? + + destroy_conditionally!(board) do |board| + service = ::Boards::DestroyService.new(board_parent, current_user) + service.execute(board) + end + end + def board_lists board.destroyable_lists end @@ -62,6 +91,12 @@ module API params :list_creation_params do requires :label_id, type: Integer, desc: 'The ID of an existing label' end + + params :update_params do + # Configurable issue boards are not available in CE/EE Core. + # https://docs.gitlab.com/ee/user/project/issue_board.html#configurable-issue-boards + optional :name, type: String, desc: 'The board name' + end end end end diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 37cce6eafba..6842e93a4de 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -10,6 +10,8 @@ module API after_validation { content_type "application/json" } + feature_category :source_code_management + before do require_repository_enabled! authorize! :download_code, user_project diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb index 8ce7694bbfd..0762c276aad 100644 --- a/lib/api/broadcast_messages.rb +++ b/lib/api/broadcast_messages.rb @@ -4,6 +4,8 @@ module API class BroadcastMessages < ::API::Base include PaginationParams + feature_category :navigation + resource :broadcast_messages do helpers do def find_message diff --git a/lib/api/ci/pipeline_schedules.rb b/lib/api/ci/pipeline_schedules.rb index b669acf668c..8a9ba2cbe0f 100644 --- a/lib/api/ci/pipeline_schedules.rb +++ b/lib/api/ci/pipeline_schedules.rb @@ -7,6 +7,8 @@ module API before { authenticate! } + feature_category :continuous_integration + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index 61e03ed1a95..1b36e27f6c9 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -7,6 +7,8 @@ module API before { authenticate_non_get! } + feature_category :continuous_integration + params do requires :id, type: String, desc: 'The project ID' end @@ -128,15 +130,9 @@ module API pipeline = user_project.all_pipelines.find(params[:pipeline_id]) - if Feature.enabled?(:ci_jobs_finder_refactor, default_enabled: true) - builds = ::Ci::JobsFinder - .new(current_user: current_user, pipeline: pipeline, params: params) - .execute - else - authorize!(:read_build, pipeline) - builds = pipeline.builds - builds = filter_builds(builds, params[:scope]) - end + builds = ::Ci::JobsFinder + .new(current_user: current_user, pipeline: pipeline, params: params) + .execute builds = builds.with_preloads @@ -157,16 +153,9 @@ module API pipeline = user_project.all_pipelines.find(params[:pipeline_id]) - 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 - else - authorize!(:read_pipeline, pipeline) - bridges = pipeline.bridges - bridges = filter_builds(bridges, params[:scope]) - end - + bridges = ::Ci::JobsFinder + .new(current_user: current_user, pipeline: pipeline, params: params, type: ::Ci::Bridge) + .execute bridges = bridges.with_preloads present paginate(bridges), with: Entities::Ci::Bridge @@ -246,21 +235,6 @@ module API end helpers do - # NOTE: This method should be removed once the ci_jobs_finder_refactor FF is - # removed. https://gitlab.com/gitlab-org/gitlab/-/issues/245183 - # rubocop: disable CodeReuse/ActiveRecord - def filter_builds(builds, scope) - return builds if scope.nil? || scope.empty? - - available_statuses = ::CommitStatus::AVAILABLE_STATUSES - - unknown = scope - available_statuses - render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty? - - builds.where(status: scope) - end - # rubocop: enable CodeReuse/ActiveRecord - def pipeline strong_memoize(:pipeline) do user_project.all_pipelines.find(params[:pipeline_id]) diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index ef679147c9f..85232b4ae1b 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -5,6 +5,10 @@ module API class Runner < ::API::Base helpers ::API::Helpers::Runner + content_type :txt, 'text/plain' + + feature_category :continuous_integration + resource :runners do desc 'Registers a new Runner' do success Entities::RunnerRegistrationDetails @@ -203,27 +207,18 @@ module API error!('400 Missing header Content-Range', 400) unless request.headers.key?('Content-Range') content_range = request.headers['Content-Range'] - content_range = content_range.split('-') - - # TODO: - # it seems that `Content-Range` as formatted by runner is wrong, - # the `byte_end` should point to final byte, but it points byte+1 - # that means that we have to calculate end of body, - # as we cannot use `content_length[1]` - # Issue: https://gitlab.com/gitlab-org/gitlab-runner/issues/3275 - - body_data = request.body.read - body_start = content_range[0].to_i - body_end = body_start + body_data.bytesize - - stream_size = job.trace.append(body_data, body_start) - unless stream_size == body_end - break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{stream_size}" }) + + result = ::Ci::AppendBuildTraceService + .new(job, content_range: content_range) + .execute(request.body.read) + + if result.status == 416 + break error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{result.stream_size}" }) end - status 202 + status result.status header 'Job-Status', job.status - header 'Range', "0-#{stream_size}" + header 'Range', "0-#{result.stream_size}" header 'X-GitLab-Trace-Update-Interval', job.trace.update_interval.to_s end diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index d37f10fe631..44ffc941cfa 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -7,6 +7,8 @@ module API before { authenticate! } + feature_category :continuous_integration + resource :runners do desc 'Get runners available for user' do success Entities::Runner diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index af103b8c1f8..26af921432c 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -4,6 +4,8 @@ require 'mime/types' module API class CommitStatuses < ::API::Base + feature_category :continuous_integration + 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 582ccd41847..a24848082a9 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -6,6 +6,8 @@ module API class Commits < ::API::Base include PaginationParams + feature_category :source_code_management + before do require_repository_enabled! authorize! :download_code, user_project diff --git a/lib/api/composer_packages.rb b/lib/api/composer_packages.rb index 1becbd668a3..0ac5cc45ccf 100644 --- a/lib/api/composer_packages.rb +++ b/lib/api/composer_packages.rb @@ -9,6 +9,8 @@ module API include ::API::Helpers::Packages::BasicAuthHelpers::Constants include ::Gitlab::Utils::StrongMemoize + feature_category :package_registry + content_type :json, 'application/json' default_format :json diff --git a/lib/api/conan_package_endpoints.rb b/lib/api/conan_package_endpoints.rb index 9b6867a328b..188a42f26f8 100644 --- a/lib/api/conan_package_endpoints.rb +++ b/lib/api/conan_package_endpoints.rb @@ -29,6 +29,8 @@ module API CONAN_FILES = (Gitlab::Regex::Packages::CONAN_RECIPE_FILES + Gitlab::Regex::Packages::CONAN_PACKAGE_FILES).freeze included do + feature_category :package_registry + helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::Packages::Conan::ApiHelpers helpers ::API::Helpers::RelatedResourcesHelpers diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb new file mode 100644 index 00000000000..a91db93b182 --- /dev/null +++ b/lib/api/concerns/packages/npm_endpoints.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +# NPM Package Manager Client API +# +# These API endpoints are not consumed directly by users, so there is no documentation for the +# individual endpoints. They are called by the NPM package manager client when users run commands +# like `npm install` or `npm publish`. The usage of the GitLab NPM registry is documented here: +# https://docs.gitlab.com/ee/user/packages/npm_registry/ +# +# Technical debt: https://gitlab.com/gitlab-org/gitlab/issues/35798 +# +# Caution: This Concern has to be included at the end of the API class +# The last route of this Concern has a globbing wildcard that will match all urls. +# As such, routes declared after the last route of this Concern will not match any url. +module API + module Concerns + module Packages + module NpmEndpoints + extend ActiveSupport::Concern + + included do + helpers ::API::Helpers::Packages::DependencyProxyHelpers + + before do + require_packages_enabled! + authenticate_non_get! + end + + params do + requires :package_name, type: String, desc: 'Package name' + end + namespace '-/package/*package_name' do + desc 'Get all tags for a given an NPM package' do + detail 'This feature was introduced in GitLab 12.7' + success ::API::Entities::NpmPackageTag + end + get 'dist-tags', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do + package_name = params[:package_name] + + bad_request!('Package Name') if package_name.blank? + + authorize_read_package!(project) + + packages = ::Packages::Npm::PackageFinder.new(project, package_name) + .execute + + not_found! if packages.empty? + + present ::Packages::Npm::PackagePresenter.new(package_name, packages), + with: ::API::Entities::NpmPackageTag + end + + params do + requires :tag, type: String, desc: "Package dist-tag" + end + namespace 'dist-tags/:tag', requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do + desc 'Create or Update the given tag for the given NPM package and version' do + detail 'This feature was introduced in GitLab 12.7' + end + put format: false do + package_name = params[:package_name] + version = env['api.request.body'] + tag = params[:tag] + + bad_request!('Package Name') if package_name.blank? + bad_request!('Version') if version.blank? + bad_request!('Tag') if tag.blank? + + authorize_create_package!(project) + + package = ::Packages::Npm::PackageFinder + .new(project, package_name) + .find_by_version(version) + not_found!('Package') unless package + + ::Packages::Npm::CreateTagService.new(package, tag).execute + + no_content! + end + + desc 'Deletes the given tag' do + detail 'This feature was introduced in GitLab 12.7' + end + delete format: false do + package_name = params[:package_name] + tag = params[:tag] + + bad_request!('Package Name') if package_name.blank? + bad_request!('Tag') if tag.blank? + + authorize_destroy_package!(project) + + package_tag = ::Packages::TagsFinder + .new(project, package_name, package_type: :npm) + .find_by_name(tag) + + not_found!('Package tag') unless package_tag + + ::Packages::RemoveTagService.new(package_tag).execute + + no_content! + end + end + end + + desc 'NPM registry metadata endpoint' do + detail 'This feature was introduced in GitLab 11.8' + end + params do + requires :package_name, type: String, desc: 'Package name' + end + route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true + get '*package_name', format: false, requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do + package_name = params[:package_name] + + packages = ::Packages::Npm::PackageFinder.new(project_or_nil, package_name) + .execute + + redirect_request = project_or_nil.blank? || packages.empty? + + redirect_registry_request(redirect_request, :npm, package_name: package_name) do + authorize_read_package!(project) + + not_found!('Packages') if packages.empty? + + present ::Packages::Npm::PackagePresenter.new(package_name, packages), + with: ::API::Entities::NpmPackage + end + end + end + end + end + end +end diff --git a/lib/api/container_registry_event.rb b/lib/api/container_registry_event.rb index 6c4b80b612a..9bad31f6661 100644 --- a/lib/api/container_registry_event.rb +++ b/lib/api/container_registry_event.rb @@ -4,6 +4,8 @@ module API class ContainerRegistryEvent < ::API::Base DOCKER_DISTRIBUTION_EVENTS_V1_JSON = 'application/vnd.docker.distribution.events.v1+json' + feature_category :package_registry + before { authenticate_registry_notification! } resource :container_registry_event do diff --git a/lib/api/container_repositories.rb b/lib/api/container_repositories.rb new file mode 100644 index 00000000000..c84527f26e7 --- /dev/null +++ b/lib/api/container_repositories.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module API + class ContainerRepositories < ::API::Base + include Gitlab::Utils::StrongMemoize + helpers ::API::Helpers::PackagesHelpers + + before { authenticate! } + + feature_category :container_registry + + namespace 'registry' do + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :repositories, requirements: { id: /[0-9]*/ } do + desc 'Get a container repository' do + detail 'This feature was introduced in GitLab 13.6.' + success Entities::ContainerRegistry::Repository + end + params do + optional :tags, type: Boolean, default: false, desc: 'Determines if tags should be included' + optional :tags_count, type: Boolean, default: false, desc: 'Determines if the tags count should be included' + end + get ':id' do + authorize!(:read_container_image, repository) + + present repository, with: Entities::ContainerRegistry::Repository, tags: params[:tags], tags_count: params[:tags_count], user: current_user + end + end + end + + helpers do + def repository + strong_memoize(:repository) do + ContainerRepository.find(params[:id]) + end + end + end + end +end diff --git a/lib/api/debian_package_endpoints.rb b/lib/api/debian_package_endpoints.rb index 168b3ca7a4f..c95c75b7e5c 100644 --- a/lib/api/debian_package_endpoints.rb +++ b/lib/api/debian_package_endpoints.rb @@ -26,6 +26,8 @@ module API }.freeze included do + feature_category :package_registry + helpers ::API::Helpers::PackagesHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers diff --git a/lib/api/dependency_proxy.rb b/lib/api/dependency_proxy.rb new file mode 100644 index 00000000000..3379bb2f029 --- /dev/null +++ b/lib/api/dependency_proxy.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module API + class DependencyProxy < ::API::Base + helpers ::API::Helpers::PackagesHelpers + + feature_category :dependency_proxy + + helpers do + def obtain_new_purge_cache_lease + Gitlab::ExclusiveLease + .new("dependency_proxy:delete_group_blobs:#{user_group.id}", + timeout: 1.hour) + .try_obtain + end + end + + before do + authorize! :admin_group, user_group + end + + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Deletes all dependency_proxy_blobs for a group' do + detail 'This feature was introduced in GitLab 12.10' + end + delete ':id/dependency_proxy/cache' do + not_found! unless user_group.dependency_proxy_feature_available? + + message = 'This request has already been made. It may take some time to purge the cache. You can run this at most once an hour for a given group' + render_api_error!(message, 409) unless obtain_new_purge_cache_lease + + # rubocop:disable CodeReuse/Worker + PurgeDependencyProxyCacheWorker.perform_async(current_user.id, user_group.id) + # rubocop:enable CodeReuse/Worker + end + end + end +end diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 314f5b6ee1d..0a541620c3a 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :continuous_delivery + helpers do def add_deploy_keys_project(project, attrs = {}) project.deploy_keys_projects.create(attrs) diff --git a/lib/api/deploy_tokens.rb b/lib/api/deploy_tokens.rb index 1c156b8b3bb..5fab590eb4e 100644 --- a/lib/api/deploy_tokens.rb +++ b/lib/api/deploy_tokens.rb @@ -4,6 +4,8 @@ module API class DeployTokens < ::API::Base include PaginationParams + feature_category :continuous_delivery + helpers do def scope_params scopes = params.delete(:scopes) diff --git a/lib/api/deployments.rb b/lib/api/deployments.rb index ff06bdbae16..5346fcf03c9 100644 --- a/lib/api/deployments.rb +++ b/lib/api/deployments.rb @@ -7,6 +7,8 @@ module API before { authenticate! } + feature_category :continuous_delivery + params do requires :id, type: String, desc: 'The project ID' end diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 3d2608c8c5a..4c4ec200060 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -8,7 +8,7 @@ module API before { authenticate! } - Helpers::DiscussionsHelpers.noteable_types.each do |noteable_type| + Helpers::DiscussionsHelpers.feature_category_per_noteable_type.each do |noteable_type, feature_category| parent_type = noteable_type.parent_class.to_s.underscore noteables_str = noteable_type.to_s.underscore.pluralize noteables_path = noteable_type == Commit ? "repository/#{noteables_str}" : noteables_str @@ -25,7 +25,7 @@ module API use :pagination end - get ":id/#{noteables_path}/:noteable_id/discussions" do + get ":id/#{noteables_path}/:noteable_id/discussions", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) discussion_ids = paginate(noteable.discussion_ids_relation) @@ -41,7 +41,7 @@ module API requires :discussion_id, type: String, desc: 'The ID of a discussion' requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable' end - get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do + get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) notes = readable_discussion_notes(noteable, params[:discussion_id]) @@ -91,7 +91,7 @@ module API end end end - post ":id/#{noteables_path}/:noteable_id/discussions" do + post ":id/#{noteables_path}/:noteable_id/discussions", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) type = params[:position] ? 'DiffNote' : 'DiscussionNote' id_key = noteable.is_a?(Commit) ? :commit_id : :noteable_id @@ -121,7 +121,7 @@ module API requires :discussion_id, type: String, desc: 'The ID of a discussion' requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable' end - get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do + get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) notes = readable_discussion_notes(noteable, params[:discussion_id]) @@ -141,7 +141,7 @@ module API requires :body, type: String, desc: 'The content of a note' optional :created_at, type: String, desc: 'The creation date of the note' end - post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes" do + post ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) notes = readable_discussion_notes(noteable, params[:discussion_id]) first_note = notes.first @@ -175,7 +175,7 @@ module API requires :discussion_id, type: String, desc: 'The ID of a discussion' requires :note_id, type: Integer, desc: 'The ID of a note' end - get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do + get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) get_note(noteable, params[:note_id]) @@ -192,7 +192,7 @@ module API optional :resolved, type: Boolean, desc: 'Mark note resolved/unresolved' exactly_one_of :body, :resolved end - put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do + put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) if params[:resolved].nil? @@ -210,7 +210,7 @@ module API requires :discussion_id, type: String, desc: 'The ID of a discussion' requires :note_id, type: Integer, desc: 'The ID of a note' end - delete ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id" do + delete ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) delete_note(noteable, params[:note_id]) @@ -225,7 +225,7 @@ module API requires :discussion_id, type: String, desc: 'The ID of a discussion' requires :resolved, type: Boolean, desc: 'Mark discussion resolved/unresolved' end - put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" do + put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) resolve_discussion(noteable, params[:discussion_id], params[:resolved]) diff --git a/lib/api/entities/board.rb b/lib/api/entities/board.rb index 5bb1cde0fa9..b7a50408313 100644 --- a/lib/api/entities/board.rb +++ b/lib/api/entities/board.rb @@ -4,6 +4,7 @@ module API module Entities class Board < Grape::Entity expose :id + expose :name expose :project, using: Entities::BasicProjectDetails expose :lists, using: Entities::List do |board| diff --git a/lib/api/entities/commit_signature.rb b/lib/api/entities/commit_signature.rb index b5232273521..505ce462edf 100644 --- a/lib/api/entities/commit_signature.rb +++ b/lib/api/entities/commit_signature.rb @@ -4,13 +4,28 @@ module API module Entities class CommitSignature < Grape::Entity expose :signature_type + expose :signature, merge: true do |commit, options| - if commit.signature.is_a?(GpgSignature) - ::API::Entities::GpgCommitSignature.represent commit.signature, options + if commit.signature.is_a?(GpgSignature) || commit.raw_commit_from_rugged? + ::API::Entities::GpgCommitSignature.represent commit_signature(commit), options elsif commit.signature.is_a?(X509CommitSignature) ::API::Entities::X509Signature.represent commit.signature, options end end + + expose :commit_source do |commit, _| + commit.raw_commit_from_rugged? ? "rugged" : "gitaly" + end + + private + + def commit_signature(commit) + if commit.raw_commit_from_rugged? + commit.gpg_commit.signature + else + commit.signature + end + end end end end diff --git a/lib/api/entities/container_registry.rb b/lib/api/entities/container_registry.rb index c430b73580b..c9c2c5156cc 100644 --- a/lib/api/entities/container_registry.rb +++ b/lib/api/entities/container_registry.rb @@ -10,6 +10,8 @@ module API end class Repository < Grape::Entity + include ::API::Helpers::RelatedResourcesHelpers + expose :id expose :name expose :path @@ -19,6 +21,13 @@ module API 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] } + expose :delete_api_path, if: ->(object, options) { Ability.allowed?(options[:user], :admin_container_image, object) } + + private + + def delete_api_path + expose_url api_v4_projects_registry_repositories_path(repository_id: object.id, id: object.project_id) + end end class TagDetails < Tag diff --git a/lib/api/entities/invitation.rb b/lib/api/entities/invitation.rb new file mode 100644 index 00000000000..342f4804cf3 --- /dev/null +++ b/lib/api/entities/invitation.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class Invitation < Grape::Entity + expose :access_level + expose :requested_at + expose :expires_at + expose :invite_email + expose :invite_token + expose :user_name, if: -> (member, _) { member.user.present? } + expose :created_by_name + end + end +end diff --git a/lib/api/entities/merge_request_changes.rb b/lib/api/entities/merge_request_changes.rb index a835d119736..488f33dfb93 100644 --- a/lib/api/entities/merge_request_changes.rb +++ b/lib/api/entities/merge_request_changes.rb @@ -4,7 +4,27 @@ module API module Entities class MergeRequestChanges < MergeRequest expose :diffs, as: :changes, using: Entities::Diff do |compare, _| - compare.raw_diffs(limits: false).to_a + Array(diff_collection(compare)) + end + + expose :overflow?, as: :overflow + + private + + def overflow? + expose_raw_diffs? ? false : diff_collection(object).overflow? + end + + def diff_collection(compare) + @diffs ||= if expose_raw_diffs? + compare.raw_diffs(limits: false) + else + compare.diffs.diffs + end + end + + def expose_raw_diffs? + options[:access_raw_diffs] || ::Feature.enabled?(:mrc_api_use_raw_diffs_from_gitaly, options[:project]) end end end diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb index b54f0e04a9d..e7153f9bebb 100644 --- a/lib/api/entities/package.rb +++ b/lib/api/entities/package.rb @@ -38,7 +38,8 @@ module API expose :project_path, if: ->(obj, opts) { opts[:group] && Ability.allowed?(opts[:user], :read_project, obj.project) } expose :tags - expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline + expose :pipeline, if: ->(package) { package.original_build_info }, using: Package::Pipeline + expose :pipelines, if: ->(package) { package.pipelines.present? }, using: Package::Pipeline expose :versions, using: ::API::Entities::PackageVersion, unless: ->(_, opts) { opts[:collection] } diff --git a/lib/api/entities/package_file.rb b/lib/api/entities/package_file.rb index 8be4e5a4316..2cc2f62a948 100644 --- a/lib/api/entities/package_file.rb +++ b/lib/api/entities/package_file.rb @@ -6,6 +6,7 @@ module API expose :id, :package_id, :created_at expose :file_name, :size expose :file_md5, :file_sha1 + expose :pipelines, if: ->(package_file) { package_file.pipelines.present? }, using: Package::Pipeline end end end diff --git a/lib/api/entities/package_version.rb b/lib/api/entities/package_version.rb index 5f3e86c3229..82522d3f423 100644 --- a/lib/api/entities/package_version.rb +++ b/lib/api/entities/package_version.rb @@ -8,7 +8,7 @@ module API expose :created_at expose :tags - expose :pipeline, if: ->(package) { package.build_info }, using: Package::Pipeline + expose :pipeline, if: ->(package) { package.original_build_info }, using: Package::Pipeline end end end diff --git a/lib/api/entities/project_hook.rb b/lib/api/entities/project_hook.rb index 751f9500252..6c71e5d317c 100644 --- a/lib/api/entities/project_hook.rb +++ b/lib/api/entities/project_hook.rb @@ -5,7 +5,7 @@ module API class ProjectHook < Hook expose :project_id, :issues_events, :confidential_issues_events expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events, :deployment_events - expose :job_events + expose :job_events, :releases_events expose :push_events_branch_filter end end diff --git a/lib/api/entities/release.rb b/lib/api/entities/release.rb index afe14cf33cf..44a46c5861e 100644 --- a/lib/api/entities/release.rb +++ b/lib/api/entities/release.rb @@ -30,8 +30,6 @@ module API expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? } expose :_links do expose :self_url, as: :self, expose_nil: false - expose :merge_requests_url, expose_nil: false - expose :issues_url, expose_nil: false expose :edit_url, expose_nil: false end diff --git a/lib/api/entities/releases/link.rb b/lib/api/entities/releases/link.rb index 654df2e2caf..c1d83a8924f 100644 --- a/lib/api/entities/releases/link.rb +++ b/lib/api/entities/releases/link.rb @@ -14,10 +14,8 @@ module API def direct_asset_url return object.url unless object.filepath - release = object.release - project = release.project - - Gitlab::Routing.url_helpers.project_release_url(project, release) << object.filepath + release = object.release.present + release.download_url(object.filepath) end end end diff --git a/lib/api/entities/user_status.rb b/lib/api/entities/user_status.rb index 9bc4cbf240f..1d5cc27e5ef 100644 --- a/lib/api/entities/user_status.rb +++ b/lib/api/entities/user_status.rb @@ -5,6 +5,7 @@ module API class UserStatus < Grape::Entity expose :emoji expose :message + expose :availability expose :message_html do |entity| MarkupHelper.markdown_field(entity, :message) end diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 0e780d4ef36..5dd2fa22690 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -7,6 +7,8 @@ module API before { authenticate! } + feature_category :continuous_delivery + params do requires :id, type: String, desc: 'The project ID' end diff --git a/lib/api/error_tracking.rb b/lib/api/error_tracking.rb index 03f83477954..0e44c8b1081 100644 --- a/lib/api/error_tracking.rb +++ b/lib/api/error_tracking.rb @@ -4,6 +4,8 @@ module API class ErrorTracking < ::API::Base before { authenticate! } + feature_category :error_tracking + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/events.rb b/lib/api/events.rb index 43efacf9c0b..233c62b5389 100644 --- a/lib/api/events.rb +++ b/lib/api/events.rb @@ -8,6 +8,8 @@ module API allow_access_with_scope :read_user, if: -> (request) { request.get? } + feature_category :users + resource :events do desc "List currently authenticated user's events" do detail 'This feature was introduced in GitLab 9.3.' diff --git a/lib/api/feature_flag_scopes.rb b/lib/api/feature_flag_scopes.rb index d77e243aa88..3f3bf4d9f42 100644 --- a/lib/api/feature_flag_scopes.rb +++ b/lib/api/feature_flag_scopes.rb @@ -7,6 +7,8 @@ module API ENVIRONMENT_SCOPE_ENDPOINT_REQUIREMENTS = FeatureFlags::FEATURE_FLAG_ENDPOINT_REQUIREMENTS .merge(environment_scope: API::NO_SLASH_URL_PART_REGEX) + feature_category :feature_flags + before do authorize_read_feature_flags! end diff --git a/lib/api/feature_flags.rb b/lib/api/feature_flags.rb index 613c3fb0f5b..67168ba9be6 100644 --- a/lib/api/feature_flags.rb +++ b/lib/api/feature_flags.rb @@ -7,6 +7,8 @@ module API FEATURE_FLAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS .merge(name: API::NO_SLASH_URL_PART_REGEX) + feature_category :feature_flags + before do authorize_read_feature_flags! end diff --git a/lib/api/feature_flags_user_lists.rb b/lib/api/feature_flags_user_lists.rb index e5218cfd7f1..086bcbcdc89 100644 --- a/lib/api/feature_flags_user_lists.rb +++ b/lib/api/feature_flags_user_lists.rb @@ -8,6 +8,8 @@ module API message.is_a?(String) ? { message: message }.to_json : message.to_json } + feature_category :feature_flags + before do authorize_admin_feature_flags_user_lists! end @@ -22,10 +24,13 @@ module API success ::API::Entities::FeatureFlag::UserList end params do + optional :search, type: String, desc: 'Returns the list of user lists matching the search critiera' + use :pagination end get do - present paginate(user_project.operations_feature_flags_user_lists), + user_lists = ::FeatureFlagsUserListsFinder.new(user_project, current_user, params).execute + present paginate(user_lists), with: ::API::Entities::FeatureFlag::UserList end diff --git a/lib/api/features.rb b/lib/api/features.rb index 5d2e545abd6..2c2e3e3d0c9 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -4,6 +4,8 @@ module API class Features < ::API::Base before { authenticated_as_admin! } + feature_category :feature_flags + helpers do def gate_value(params) case params[:value] @@ -61,6 +63,8 @@ module API mutually_exclusive :key, :project end post ':name' do + validate_feature_flag_name!(params[:name]) + feature = Feature.get(params[:name]) # rubocop:disable Gitlab/AvoidFeatureGet targets = gate_targets(params) value = gate_value(params) @@ -97,5 +101,13 @@ module API no_content! end end + + helpers do + def validate_feature_flag_name!(name) + # no-op + end + end end end + +API::Features.prepend_if_ee('EE::API::Features') diff --git a/lib/api/files.rb b/lib/api/files.rb index 6833fc429e2..cb73bde73f5 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -9,6 +9,8 @@ module API # Prevents returning plain/text responses for files with .txt extension after_validation { content_type "application/json" } + feature_category :source_code_management + helpers ::API::Helpers::HeadersHelpers helpers do diff --git a/lib/api/freeze_periods.rb b/lib/api/freeze_periods.rb index a83e36165a2..d001ced8581 100644 --- a/lib/api/freeze_periods.rb +++ b/lib/api/freeze_periods.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :continuous_delivery + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index a0c33ab65b9..3e1dd044c8d 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -7,6 +7,8 @@ module API file_name: API::NO_SLASH_URL_PART_REGEX }.freeze + feature_category :package_registry + before do require_packages_enabled! authenticate! diff --git a/lib/api/go_proxy.rb b/lib/api/go_proxy.rb index 30f0cfb4dfd..8fb4c561c40 100755 --- a/lib/api/go_proxy.rb +++ b/lib/api/go_proxy.rb @@ -4,11 +4,15 @@ module API helpers Gitlab::Golang helpers ::API::Helpers::PackagesHelpers + feature_category :package_registry + # basic semver, except case encoded (A => !a) MODULE_VERSION_REGEX = /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([-.!a-z0-9]+))?(?:\+([-.!a-z0-9]+))?/.freeze MODULE_VERSION_REQUIREMENTS = { module_version: MODULE_VERSION_REGEX }.freeze + content_type :txt, 'text/plain' + before { require_packages_enabled! } helpers do diff --git a/lib/api/group_boards.rb b/lib/api/group_boards.rb index d4574b22d99..ac5a1a2ce94 100644 --- a/lib/api/group_boards.rb +++ b/lib/api/group_boards.rb @@ -7,6 +7,8 @@ module API prepend_if_ee('EE::API::BoardsResponses') # rubocop: disable Cop/InjectEnterpriseEditionModule + feature_category :boards + before do authenticate! end diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb index 75429cf7a5c..a435b050042 100644 --- a/lib/api/group_clusters.rb +++ b/lib/api/group_clusters.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :kubernetes_management + params do requires :id, type: String, desc: 'The ID of the group' end diff --git a/lib/api/group_container_repositories.rb b/lib/api/group_container_repositories.rb index 1bb26b3931c..4fede0ad583 100644 --- a/lib/api/group_container_repositories.rb +++ b/lib/api/group_container_repositories.rb @@ -8,6 +8,8 @@ module API before { authorize_read_group_container_images! } + feature_category :package_registry + REPOSITORY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge( tag_name: API::NO_SLASH_URL_PART_REGEX) diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb index 6ebaa8de185..29ffbea687a 100644 --- a/lib/api/group_export.rb +++ b/lib/api/group_export.rb @@ -10,6 +10,8 @@ module API authorize! :admin_group, user_group end + feature_category :importers + params do requires :id, type: String, desc: 'The ID of a group' end diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb index e703a217fd5..4a752732652 100644 --- a/lib/api/group_import.rb +++ b/lib/api/group_import.rb @@ -2,6 +2,8 @@ module API class GroupImport < ::API::Base + feature_category :importers + helpers Helpers::FileUploadHelpers helpers do diff --git a/lib/api/group_labels.rb b/lib/api/group_labels.rb index 8443ddf10ce..bf3ac8800b7 100644 --- a/lib/api/group_labels.rb +++ b/lib/api/group_labels.rb @@ -7,6 +7,8 @@ module API before { authenticate! } + feature_category :issue_tracking + params do requires :id, type: String, desc: 'The ID of a group' end @@ -20,10 +22,16 @@ module API desc: 'Include issue and merge request counts' optional :include_ancestor_groups, type: Boolean, default: true, desc: 'Include ancestor groups' + optional :include_descendant_groups, type: Boolean, default: false, + desc: 'Include descendant groups. This feature was added in GitLab 13.6' + optional :only_group_labels, type: Boolean, default: true, + desc: 'Toggle to include only group labels or also project labels. This feature was added in GitLab 13.6' + optional :search, type: String, + desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6' use :pagination end get ':id/labels' do - get_labels(user_group, Entities::GroupLabel, include_ancestor_groups: params[:include_ancestor_groups]) + get_labels(user_group, Entities::GroupLabel, declared_params) end desc 'Get a single label' do @@ -33,9 +41,13 @@ module API params do optional :include_ancestor_groups, type: Boolean, default: true, desc: 'Include ancestor groups' + optional :include_descendant_groups, type: Boolean, default: false, + desc: 'Include descendant groups. This feature was added in GitLab 13.6' + optional :only_group_labels, type: Boolean, default: true, + desc: 'Toggle to include only group labels or also project labels. This feature was added in GitLab 13.6' end get ':id/labels/:name' do - get_label(user_group, Entities::GroupLabel, include_ancestor_groups: params[:include_ancestor_groups]) + get_label(user_group, Entities::GroupLabel, declared_params) end desc 'Create a new label' do diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb index aef9877b84c..dfffd3b1209 100644 --- a/lib/api/group_milestones.rb +++ b/lib/api/group_milestones.rb @@ -7,6 +7,8 @@ module API before { authenticate! } + feature_category :issue_tracking + params do requires :id, type: String, desc: 'The ID of a group' end diff --git a/lib/api/group_packages.rb b/lib/api/group_packages.rb index 5b6290df0dd..31b28c3990f 100644 --- a/lib/api/group_packages.rb +++ b/lib/api/group_packages.rb @@ -8,6 +8,8 @@ module API authorize_packages_access!(user_group) end + feature_category :package_registry + helpers ::API::Helpers::PackagesHelpers params do diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index ee110d67fa5..0c40db02eb5 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -7,6 +7,8 @@ module API before { authenticate! } before { authorize! :admin_build, user_group } + feature_category :continuous_integration + params do requires :id, type: String, desc: 'The ID of a group' end diff --git a/lib/api/groups.rb b/lib/api/groups.rb index bf3d6c3c7e0..a8b1cdab021 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -7,6 +7,8 @@ module API before { authenticate_non_get! } + feature_category :subgroups + helpers Helpers::GroupsHelpers helpers do @@ -46,7 +48,7 @@ module API find_params.fetch(:all_available, current_user&.can_read_all_resources?) groups = GroupsFinder.new(current_user, find_params).execute - groups = groups.search(params[:search]) if params[:search].present? + groups = groups.search(params[:search], include_parents: true) if params[:search].present? groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? order_options = { params[:order_by] => params[:sort] } order_options["id"] ||= "asc" diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index c8aee1f3479..147d8407142 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -89,16 +89,15 @@ module API @project ||= find_project!(params[:id]) end - def available_labels_for(label_parent, include_ancestor_groups: true) - search_params = { include_ancestor_groups: include_ancestor_groups } - + def available_labels_for(label_parent, params = { include_ancestor_groups: true, only_group_labels: true }) if label_parent.is_a?(Project) - search_params[:project_id] = label_parent.id + params.delete(:only_group_labels) + params[:project_id] = label_parent.id else - search_params.merge!(group_id: label_parent.id, only_group_labels: true) + params[:group_id] = label_parent.id end - LabelsFinder.new(current_user, search_params).execute + LabelsFinder.new(current_user, params).execute end def find_user(id) @@ -388,8 +387,8 @@ module API render_api_error!('401 Unauthorized', 401) end - def not_allowed! - render_api_error!('405 Method Not Allowed', 405) + def not_allowed!(message = nil) + render_api_error!(message || '405 Method Not Allowed', :method_not_allowed) end def not_acceptable! diff --git a/lib/api/helpers/discussions_helpers.rb b/lib/api/helpers/discussions_helpers.rb index 799d5582b38..3c0db1d0ea9 100644 --- a/lib/api/helpers/discussions_helpers.rb +++ b/lib/api/helpers/discussions_helpers.rb @@ -3,10 +3,15 @@ module API module Helpers module DiscussionsHelpers - def self.noteable_types + def self.feature_category_per_noteable_type # This is a method instead of a constant, allowing EE to more easily # extend it. - [Issue, Snippet, MergeRequest, Commit] + { + Issue => :issue_tracking, + Snippet => :snippets, + MergeRequest => :code_review, + Commit => :code_review + } end end end diff --git a/lib/api/helpers/issues_helpers.rb b/lib/api/helpers/issues_helpers.rb index 638b31cc7ba..b303f1f845d 100644 --- a/lib/api/helpers/issues_helpers.rb +++ b/lib/api/helpers/issues_helpers.rb @@ -5,6 +5,9 @@ module API module IssuesHelpers extend Grape::API::Helpers + params :negatable_issue_filter_params_ee do + end + params :optional_issue_params_ee do end diff --git a/lib/api/helpers/label_helpers.rb b/lib/api/helpers/label_helpers.rb index 2fb2d9b79cf..4018f2dec21 100644 --- a/lib/api/helpers/label_helpers.rb +++ b/lib/api/helpers/label_helpers.rb @@ -28,23 +28,23 @@ module API at_least_one_of :new_name, :color, :description end - def find_label(parent, id_or_title, include_ancestor_groups: true) - labels = available_labels_for(parent, include_ancestor_groups: include_ancestor_groups) + def find_label(parent, id_or_title, params = { include_ancestor_groups: true }) + labels = available_labels_for(parent, params) label = labels.find_by_id(id_or_title) || labels.find_by_title(id_or_title) label || not_found!('Label') end - def get_labels(parent, entity, include_ancestor_groups: true) - present paginate(available_labels_for(parent, include_ancestor_groups: include_ancestor_groups)), + def get_labels(parent, entity, params = {}) + present paginate(available_labels_for(parent, params)), with: entity, current_user: current_user, parent: parent, with_counts: params[:with_counts] end - def get_label(parent, entity, include_ancestor_groups: true) - label = find_label(parent, params_id_or_title, include_ancestor_groups: include_ancestor_groups) + def get_label(parent, entity, params = {}) + label = find_label(parent, params_id_or_title, params) present label, with: entity, current_user: current_user, parent: parent end diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index 5cc435e6801..431001c227d 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -20,12 +20,23 @@ module API # rubocop: disable CodeReuse/ActiveRecord def retrieve_members(source, params:, deep: false) - members = deep ? find_all_members(source) : source.members.where.not(user_id: nil) + members = deep ? find_all_members(source) : source_members(source).where.not(user_id: nil) members = members.includes(:user) members = members.references(:user).merge(User.search(params[:query])) if params[:query].present? members = members.where(user_id: params[:user_ids]) if params[:user_ids].present? members end + + def retrieve_member_invitations(source, query = nil) + members = source_members(source).where.not(invite_token: nil) + members = members.includes(:user) + members = members.where(invite_email: query) if query.present? + members + end + + def source_members(source) + source.members + end # rubocop: enable CodeReuse/ActiveRecord def find_all_members(source) @@ -48,6 +59,10 @@ module API def present_members(members) present members, with: Entities::Member, current_user: current_user, show_seat_info: params[:show_seat_info] end + + def present_member_invitations(invitations) + present invitations, with: Entities::Invitation, current_user: current_user + end end end end diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index f61bcfe963e..6798c4d284b 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -5,10 +5,12 @@ module API module NotesHelpers include ::RendersNotes - def self.noteable_types - # This is a method instead of a constant, allowing EE to more easily - # extend it. - [Issue, MergeRequest, Snippet] + def self.feature_category_per_noteable_type + { + Issue => :issue_tracking, + MergeRequest => :code_review, + Snippet => :snippets + } end def update_note(noteable, note_id) diff --git a/lib/api/helpers/packages/npm.rb b/lib/api/helpers/packages/npm.rb new file mode 100644 index 00000000000..c1f6a001201 --- /dev/null +++ b/lib/api/helpers/packages/npm.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +module API + module Helpers + module Packages + module Npm + include Gitlab::Utils::StrongMemoize + include ::API::Helpers::PackagesHelpers + + NPM_ENDPOINT_REQUIREMENTS = { + package_name: API::NO_SLASH_URL_PART_REGEX + }.freeze + + def endpoint_scope + params[:id].present? ? :project : :instance + end + + def project + strong_memoize(:project) do + case endpoint_scope + when :project + user_project + when :instance + # Simulate the same behavior as #user_project by re-using #find_project! + # but take care if the project_id is nil as #find_project! is not designed + # to handle it. + project_id = project_id_or_nil + + not_found!('Project') unless project_id + + find_project!(project_id) + end + end + end + + def project_or_nil + # mainly used by the metadata endpoint where we need to get a project + # and return nil if not found (no errors should be raised) + strong_memoize(:project_or_nil) do + next unless project_id_or_nil + + find_project(project_id_or_nil) + end + end + + def project_id_or_nil + strong_memoize(:project_id_or_nil) do + case endpoint_scope + when :project + params[:id] + when :instance + ::Packages::Package.npm + .with_name(params[:package_name]) + .first + &.project_id + end + end + end + end + end + end +end diff --git a/lib/api/helpers/resource_label_events_helpers.rb b/lib/api/helpers/resource_label_events_helpers.rb index 423bd4e704b..ad2733baffc 100644 --- a/lib/api/helpers/resource_label_events_helpers.rb +++ b/lib/api/helpers/resource_label_events_helpers.rb @@ -3,10 +3,13 @@ module API module Helpers module ResourceLabelEventsHelpers - def self.eventable_types + def self.feature_category_per_eventable_type # This is a method instead of a constant, allowing EE to more easily # extend it. - [Issue, MergeRequest] + { + Issue => :issue_tracking, + MergeRequest => :code_review + } end end end diff --git a/lib/api/import_bitbucket_server.rb b/lib/api/import_bitbucket_server.rb index a0238c24f3b..ecd78c6e6db 100644 --- a/lib/api/import_bitbucket_server.rb +++ b/lib/api/import_bitbucket_server.rb @@ -2,6 +2,8 @@ module API class ImportBitbucketServer < ::API::Base + feature_category :importers + helpers do def client @client ||= BitbucketServer::Client.new(credentials) diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index 61fce7a2c1b..c91a7700f58 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -2,6 +2,8 @@ module API class ImportGithub < ::API::Base + feature_category :importers + rescue_from Octokit::Unauthorized, with: :provider_unauthorized before do @@ -11,7 +13,7 @@ module API helpers do def client @client ||= if Feature.enabled?(:remove_legacy_github_client) - Gitlab::GithubImport::Client.new(params[:personal_access_token]) + Gitlab::GithubImport::Client.new(params[:personal_access_token], host: params[:github_hostname]) else Gitlab::LegacyGithubImport::Client.new(params[:personal_access_token], client_options) end @@ -22,7 +24,7 @@ module API end def client_options - {} + { host: params[:github_hostname] } end def provider @@ -43,6 +45,7 @@ module API requires :repo_id, type: Integer, desc: 'GitHub repository ID' optional :new_name, type: String, desc: 'New repo name' requires :target_namespace, type: String, desc: 'Namespace to import repo into' + optional :github_hostname, type: String, desc: 'Custom GitHub enterprise hostname' end post 'import/github' do result = Import::GithubService.new(client, current_user, params).execute(access_params, provider) diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index 6d8f13c36e6..61ef1d5bde0 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -7,10 +7,16 @@ module API before { authenticate_by_gitlab_shell_token! } before do + api_endpoint = env['api.endpoint'] + feature_category = api_endpoint.options[:for].try(:feature_category_for_app, api_endpoint).to_s + + header[Gitlab::Metrics::RequestsRackMiddleware::FEATURE_CATEGORY_HEADER] = feature_category + Gitlab::ApplicationContext.push( user: -> { actor&.user }, project: -> { project }, - caller_id: route.origin + caller_id: route.origin, + feature_category: feature_category ) end @@ -28,10 +34,10 @@ module API { status: success, message: message }.merge(extra_options).compact end - def lfs_authentication_url(project) + def lfs_authentication_url(container) # This is a separate method so that EE can alter its behaviour more # easily. - project.http_url_to_repo + container.lfs_http_url_to_repo end def check_allowed(params) @@ -122,13 +128,15 @@ module API # changes - changes as "oldrev newrev ref", see Gitlab::ChangesList # check_ip - optional, only in EE version, may limit access to # group resources based on its IP restrictions - post "/allowed" do + post "/allowed", feature_category: :source_code_management do # It was moved to a separate method so that EE can alter its behaviour more # easily. check_allowed(params) end - post "/lfs_authenticate" do + post "/lfs_authenticate", feature_category: :source_code_management do + not_found! unless container&.lfs_enabled? + status 200 unless actor.key_or_user @@ -139,14 +147,14 @@ module API Gitlab::LfsToken .new(actor.key_or_user) - .authentication_payload(lfs_authentication_url(project)) + .authentication_payload(lfs_authentication_url(container)) end # # Get a ssh key using the fingerprint # # rubocop: disable CodeReuse/ActiveRecord - get '/authorized_keys' do + get '/authorized_keys', feature_category: :source_code_management do fingerprint = params.fetch(:fingerprint) do Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint end @@ -159,11 +167,11 @@ module API # # Discover user by ssh key, user id or username # - get '/discover' do + get '/discover', feature_category: :authentication_and_authorization do present actor.user, with: Entities::UserSafe end - get '/check' do + get '/check', feature_category: :not_owned do { api_version: API.version, gitlab_version: Gitlab::VERSION, @@ -172,7 +180,7 @@ module API } end - post '/two_factor_recovery_codes' do + post '/two_factor_recovery_codes', feature_category: :authentication_and_authorization do status 200 actor.update_last_used_at! @@ -201,7 +209,7 @@ module API { success: true, recovery_codes: codes } end - post '/personal_access_token' do + post '/personal_access_token', feature_category: :authentication_and_authorization do status 200 actor.update_last_used_at! @@ -239,7 +247,7 @@ module API end result = ::PersonalAccessTokens::CreateService.new( - user, name: params[:name], scopes: params[:scopes], expires_at: expires_at + current_user: user, target_user: user, params: { name: params[:name], scopes: params[:scopes], expires_at: expires_at } ).execute unless result.status == :success @@ -251,7 +259,7 @@ module API { success: true, token: access_token.token, scopes: access_token.scopes, expires_at: access_token.expires_at } end - post '/pre_receive' do + post '/pre_receive', feature_category: :source_code_management do status 200 reference_counter_increased = Gitlab::ReferenceCounter.new(params[:gl_repository]).increase @@ -259,7 +267,7 @@ module API { reference_counter_increased: reference_counter_increased } end - post '/post_receive' do + post '/post_receive', feature_category: :source_code_management do status 200 response = PostReceiveService.new(actor.user, repository, project, params).execute @@ -267,7 +275,7 @@ module API present response, with: Entities::InternalPostReceive::Response end - post '/two_factor_config' do + post '/two_factor_config', feature_category: :authentication_and_authorization do status 200 break { success: false } unless Feature.enabled?(:two_factor_for_cli) @@ -289,7 +297,7 @@ module API end end - post '/two_factor_otp_check' do + post '/two_factor_otp_check', feature_category: :authentication_and_authorization do status 200 break { success: false } unless Feature.enabled?(:two_factor_for_cli) diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index 90e224b2ccb..d4690709de4 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -4,6 +4,8 @@ module API # Kubernetes Internal API module Internal class Kubernetes < ::API::Base + feature_category :kubernetes_management + before do check_feature_enabled authenticate_gitlab_kas_request! diff --git a/lib/api/internal/lfs.rb b/lib/api/internal/lfs.rb index 630f0ec77a8..66baa4f1034 100644 --- a/lib/api/internal/lfs.rb +++ b/lib/api/internal/lfs.rb @@ -7,6 +7,8 @@ module API before { authenticate_by_gitlab_shell_token! } + feature_category :source_code_management + helpers do def find_lfs_object(lfs_oid) LfsObject.find_by_oid(lfs_oid) diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb index 51136144c19..690f52d89f3 100644 --- a/lib/api/internal/pages.rb +++ b/lib/api/internal/pages.rb @@ -4,6 +4,8 @@ module API # Pages Internal API module Internal class Pages < ::API::Base + feature_category :pages + before do authenticate_gitlab_pages_request! end diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb new file mode 100644 index 00000000000..be8147908e9 --- /dev/null +++ b/lib/api/invitations.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module API + class Invitations < ::API::Base + include PaginationParams + + feature_category :users + + before { authenticate! } + + helpers ::API::Helpers::MembersHelpers + + %w[group project].each do |source_type| + params do + requires :id, type: String, desc: "The #{source_type} ID" + end + resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + desc 'Invite non-members by email address to a group or project.' do + detail 'This feature was introduced in GitLab 13.6' + success Entities::Invitation + end + params do + requires :email, types: [String, Array[String]], email_or_email_list: true, desc: 'The email address to invite, or multiple emails separated by comma' + requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)' + optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY' + end + post ":id/invitations" do + source = find_source(source_type, params[:id]) + + authorize_admin_source!(source_type, source) + + ::Members::InviteService.new(current_user, params).execute(source) + end + + desc 'Get a list of group or project invitations viewable by the authenticated user' do + detail 'This feature was introduced in GitLab 13.6' + success Entities::Invitation + end + params do + optional :query, type: String, desc: 'A query string to search for members' + use :pagination + end + get ":id/invitations" do + source = find_source(source_type, params[:id]) + query = params[:query] + + invitations = paginate(retrieve_member_invitations(source, query)) + + present_member_invitations invitations + end + end + end + end +end diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb index db4979c9052..e938dbbae87 100644 --- a/lib/api/issue_links.rb +++ b/lib/api/issue_links.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :issue_tracking + params do requires :id, type: String, desc: 'The ID of a project' requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue' diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 143f9e40736..6a6ee7a4e1c 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -8,6 +8,8 @@ module API before { authenticate_non_get! } + feature_category :issue_tracking + helpers do params :negatable_issue_filter_params do optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names' @@ -26,6 +28,8 @@ module API coerce_with: Validations::Validators::CheckAssigneesCount.coerce, desc: 'Return issues which are assigned to the user with the given username' mutually_exclusive :assignee_id, :assignee_username + + use :negatable_issue_filter_params_ee end params :issues_stats_params do diff --git a/lib/api/job_artifacts.rb b/lib/api/job_artifacts.rb index 536b361b308..1faa28d6f07 100644 --- a/lib/api/job_artifacts.rb +++ b/lib/api/job_artifacts.rb @@ -4,6 +4,8 @@ module API class JobArtifacts < ::API::Base before { authenticate_non_get! } + feature_category :continuous_integration + # EE::API::JobArtifacts would override the following helpers helpers do def authorize_download_artifacts! diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb index bdb23b4a9be..51659c2e8a1 100644 --- a/lib/api/jobs.rb +++ b/lib/api/jobs.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :continuous_integration + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/keys.rb b/lib/api/keys.rb index 2e4568029b5..fb1bedd5e92 100644 --- a/lib/api/keys.rb +++ b/lib/api/keys.rb @@ -5,6 +5,8 @@ module API class Keys < ::API::Base before { authenticate! } + feature_category :authentication_and_authorization + resource :keys do desc 'Get single ssh key by id. Only available to admin users' do success Entities::SSHKeyWithUser diff --git a/lib/api/labels.rb b/lib/api/labels.rb index 0cc9f33bd07..a8fc277989e 100644 --- a/lib/api/labels.rb +++ b/lib/api/labels.rb @@ -7,6 +7,8 @@ module API before { authenticate! } + feature_category :issue_tracking + params do requires :id, type: String, desc: 'The ID of a project' end @@ -19,10 +21,12 @@ module API desc: 'Include issue and merge request counts' optional :include_ancestor_groups, type: Boolean, default: true, desc: 'Include ancestor groups' + optional :search, type: String, + desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6' use :pagination end get ':id/labels' do - get_labels(user_project, Entities::ProjectLabel, include_ancestor_groups: params[:include_ancestor_groups]) + get_labels(user_project, Entities::ProjectLabel, declared_params) end desc 'Get a single label' do @@ -34,7 +38,7 @@ module API desc: 'Include ancestor groups' end get ':id/labels/:name' do - get_label(user_project, Entities::ProjectLabel, include_ancestor_groups: params[:include_ancestor_groups]) + get_label(user_project, Entities::ProjectLabel, declared_params) end desc 'Create a new label' do diff --git a/lib/api/lint.rb b/lib/api/lint.rb index bfd152f70b1..58181adaa93 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -2,6 +2,8 @@ module API class Lint < ::API::Base + feature_category :pipeline_authoring + namespace :ci do desc 'Validation of .gitlab-ci.yml content' params do @@ -15,9 +17,9 @@ module API status 200 response = if error.blank? - { status: 'valid', errors: [] } + { status: 'valid', errors: [], warnings: result.warnings } else - { status: 'invalid', errors: [error] } + { status: 'invalid', errors: [error], warnings: result.warnings } end response.tap do |response| @@ -44,5 +46,25 @@ module API present result, with: Entities::Ci::Lint::Result, current_user: current_user 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.6.' + end + params do + requires :content, type: String, desc: 'Content of .gitlab-ci.yml' + optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.' + end + post ':id/ci/lint' do + authorize! :download_code, user_project + + result = Gitlab::Ci::Lint + .new(project: user_project, current_user: current_user) + .validate(params[:content], dry_run: params[:dry_run]) + + status 200 + 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 97549abd273..de612ff8321 100644 --- a/lib/api/markdown.rb +++ b/lib/api/markdown.rb @@ -2,6 +2,8 @@ module API class Markdown < ::API::Base + feature_category :not_owned + 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 a3e2fa84c32..7b4e52d18e8 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -5,6 +5,8 @@ module API file_name: API::NO_SLASH_URL_PART_REGEX }.freeze + feature_category :package_registry + content_type :md5, 'text/plain' content_type :sha1, 'text/plain' content_type :binary, 'application/octet-stream' @@ -244,7 +246,7 @@ module API file_md5: params['file.md5'] } - ::Packages::CreatePackageFileService.new(package, file_params).execute + ::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job)).execute end end end diff --git a/lib/api/members.rb b/lib/api/members.rb index c28b3b1cc7c..803de51651a 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :authentication_and_authorization + helpers ::API::Helpers::MembersHelpers %w[group project].each do |source_type| @@ -134,7 +136,7 @@ module API source = find_source(source_type, params.delete(:id)) authorize_admin_source!(source_type, source) - member = source.members.find_by!(user_id: params[:user_id]) + member = source_members(source).find_by!(user_id: params[:user_id]) updated_member = ::Members::UpdateService .new(current_user, declared_params(include_missing: false)) @@ -157,7 +159,7 @@ module API # rubocop: disable CodeReuse/ActiveRecord delete ":id/members/:user_id" do source = find_source(source_type, params[:id]) - member = source.members.find_by!(user_id: params[:user_id]) + member = source_members(source).find_by!(user_id: params[:user_id]) destroy_conditionally!(member) do ::Members::DestroyService.new(current_user).execute(member, unassign_issuables: params[:unassign_issuables]) diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb index 14d6e3995ea..27ef0b9c7cd 100644 --- a/lib/api/merge_request_approvals.rb +++ b/lib/api/merge_request_approvals.rb @@ -4,6 +4,8 @@ module API class MergeRequestApprovals < ::API::Base before { authenticate_non_get! } + feature_category :code_review + helpers do params :ee_approval_params do end diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index 22023888bbd..0ffb38438eb 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -7,6 +7,8 @@ module API before { authenticate! } + feature_category :code_review + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index b24dd870c8b..d17e451093b 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -8,6 +8,8 @@ module API before { authenticate_non_get! } + feature_category :code_review + helpers Helpers::MergeRequestsHelpers # EE::API::MergeRequests would override the following helpers @@ -350,7 +352,11 @@ module API get ':id/merge_requests/:merge_request_iid/changes' do merge_request = find_merge_request_with_access(params[:merge_request_iid]) - present merge_request, with: Entities::MergeRequestChanges, current_user: current_user, project: user_project + present merge_request, + with: Entities::MergeRequestChanges, + current_user: current_user, + project: user_project, + access_raw_diffs: params.fetch(:access_raw_diffs, false) end desc 'Get the merge request pipelines' do diff --git a/lib/api/metrics/dashboard/annotations.rb b/lib/api/metrics/dashboard/annotations.rb index b6bc0af2202..0989340b3ea 100644 --- a/lib/api/metrics/dashboard/annotations.rb +++ b/lib/api/metrics/dashboard/annotations.rb @@ -4,6 +4,8 @@ module API module Metrics module Dashboard class Annotations < ::API::Base + feature_category :metrics + 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 cb6e7099247..909f7f0405d 100644 --- a/lib/api/metrics/user_starred_dashboards.rb +++ b/lib/api/metrics/user_starred_dashboards.rb @@ -3,6 +3,8 @@ module API module Metrics class UserStarredDashboards < ::API::Base + feature_category :metrics + 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 f98a1f6dd1d..25a901c18b6 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :subgroups + helpers do params :optional_list_params_ee do # EE::API::Namespaces would override this helper diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 0db537ca616..d249431b2f8 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -7,7 +7,7 @@ module API before { authenticate! } - Helpers::NotesHelpers.noteable_types.each do |noteable_type| + Helpers::NotesHelpers.feature_category_per_noteable_type.each do |noteable_type, feature_category| parent_type = noteable_type.parent_class.to_s.underscore noteables_str = noteable_type.to_s.underscore.pluralize @@ -29,7 +29,7 @@ module API use :pagination end # rubocop: disable CodeReuse/ActiveRecord - get ":id/#{noteables_str}/:noteable_id/notes" do + get ":id/#{noteables_str}/:noteable_id/notes", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) # We exclude notes that are cross-references and that cannot be viewed @@ -57,7 +57,7 @@ module API requires :note_id, type: Integer, desc: 'The ID of a note' requires :noteable_id, type: Integer, desc: 'The ID of the noteable' end - get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do + get ":id/#{noteables_str}/:noteable_id/notes/:note_id", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) get_note(noteable, params[:note_id]) end @@ -71,7 +71,7 @@ module API optional :confidential, type: Boolean, desc: 'Confidentiality note flag, default is false' optional :created_at, type: String, desc: 'The creation date of the note' end - post ":id/#{noteables_str}/:noteable_id/notes" do + post ":id/#{noteables_str}/:noteable_id/notes", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) opts = { @@ -104,7 +104,7 @@ module API optional :body, type: String, allow_blank: false, desc: 'The content of a note' optional :confidential, type: Boolean, desc: 'Confidentiality note flag' end - put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do + put ":id/#{noteables_str}/:noteable_id/notes/:note_id", feature_category: feature_category do noteable = find_noteable(noteable_type, params[:noteable_id]) update_note(noteable, params[:note_id]) diff --git a/lib/api/notification_settings.rb b/lib/api/notification_settings.rb index bad3f5ead7a..7d28394e034 100644 --- a/lib/api/notification_settings.rb +++ b/lib/api/notification_settings.rb @@ -5,6 +5,8 @@ module API class NotificationSettings < ::API::Base before { authenticate! } + feature_category :users + helpers ::API::Helpers::MembersHelpers resource :notification_settings do diff --git a/lib/api/npm_instance_packages.rb b/lib/api/npm_instance_packages.rb new file mode 100644 index 00000000000..12fc008e00f --- /dev/null +++ b/lib/api/npm_instance_packages.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true +module API + class NpmInstancePackages < ::API::Base + helpers ::API::Helpers::Packages::Npm + + feature_category :package_registry + + rescue_from ActiveRecord::RecordInvalid do |e| + render_api_error!(e.message, 400) + end + + namespace 'packages/npm' do + include ::API::Concerns::Packages::NpmEndpoints + end + end +end diff --git a/lib/api/npm_packages.rb b/lib/api/npm_packages.rb deleted file mode 100644 index 1443b28c1ee..00000000000 --- a/lib/api/npm_packages.rb +++ /dev/null @@ -1,173 +0,0 @@ -# frozen_string_literal: true -module API - class NpmPackages < ::API::Base - helpers ::API::Helpers::PackagesHelpers - helpers ::API::Helpers::Packages::DependencyProxyHelpers - - NPM_ENDPOINT_REQUIREMENTS = { - package_name: API::NO_SLASH_URL_PART_REGEX - }.freeze - - rescue_from ActiveRecord::RecordInvalid do |e| - render_api_error!(e.message, 400) - end - - before do - require_packages_enabled! - authenticate_non_get! - end - - helpers do - def project_by_package_name - strong_memoize(:project_by_package_name) do - ::Packages::Package.npm.with_name(params[:package_name]).first&.project - end - end - end - - desc 'Get all tags for a given an NPM package' do - detail 'This feature was introduced in GitLab 12.7' - success ::API::Entities::NpmPackageTag - end - params do - requires :package_name, type: String, desc: 'Package name' - end - get 'packages/npm/-/package/*package_name/dist-tags', format: false, requirements: NPM_ENDPOINT_REQUIREMENTS do - package_name = params[:package_name] - - bad_request!('Package Name') if package_name.blank? - - authorize_read_package!(project_by_package_name) - - packages = ::Packages::Npm::PackageFinder.new(project_by_package_name, package_name) - .execute - - present ::Packages::Npm::PackagePresenter.new(package_name, packages), - with: ::API::Entities::NpmPackageTag - end - - params do - requires :package_name, type: String, desc: 'Package name' - requires :tag, type: String, desc: "Package dist-tag" - end - namespace 'packages/npm/-/package/*package_name/dist-tags/:tag', requirements: NPM_ENDPOINT_REQUIREMENTS do - desc 'Create or Update the given tag for the given NPM package and version' do - detail 'This feature was introduced in GitLab 12.7' - end - put format: false do - package_name = params[:package_name] - version = env['api.request.body'] - tag = params[:tag] - - bad_request!('Package Name') if package_name.blank? - bad_request!('Version') if version.blank? - bad_request!('Tag') if tag.blank? - - authorize_create_package!(project_by_package_name) - - package = ::Packages::Npm::PackageFinder - .new(project_by_package_name, package_name) - .find_by_version(version) - not_found!('Package') unless package - - ::Packages::Npm::CreateTagService.new(package, tag).execute - - no_content! - end - - desc 'Deletes the given tag' do - detail 'This feature was introduced in GitLab 12.7' - end - delete format: false do - package_name = params[:package_name] - tag = params[:tag] - - bad_request!('Package Name') if package_name.blank? - bad_request!('Tag') if tag.blank? - - authorize_destroy_package!(project_by_package_name) - - package_tag = ::Packages::TagsFinder - .new(project_by_package_name, package_name, package_type: :npm) - .find_by_name(tag) - - not_found!('Package tag') unless package_tag - - ::Packages::RemoveTagService.new(package_tag).execute - - no_content! - end - end - - desc 'NPM registry endpoint at instance level' do - detail 'This feature was introduced in GitLab 11.8' - end - params do - requires :package_name, type: String, desc: 'Package name' - end - route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true - get 'packages/npm/*package_name', format: false, requirements: NPM_ENDPOINT_REQUIREMENTS do - package_name = params[:package_name] - - redirect_registry_request(project_by_package_name.blank?, :npm, package_name: package_name) do - authorize_read_package!(project_by_package_name) - - packages = ::Packages::Npm::PackageFinder - .new(project_by_package_name, package_name).execute - - present ::Packages::Npm::PackagePresenter.new(package_name, packages), - with: ::API::Entities::NpmPackage - end - end - - params do - requires :id, type: String, desc: 'The ID of a project' - end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - desc 'Download the NPM tarball' do - detail 'This feature was introduced in GitLab 11.8' - end - params do - requires :package_name, type: String, desc: 'Package name' - requires :file_name, type: String, desc: 'Package file name' - end - route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true - get ':id/packages/npm/*package_name/-/*file_name', format: false do - authorize_read_package!(user_project) - - package = user_project.packages.npm - .by_name_and_file_name(params[:package_name], params[:file_name]) - - package_file = ::Packages::PackageFileFinder - .new(package, params[:file_name]).execute! - - track_package_event('pull_package', package) - - present_carrierwave_file!(package_file.file) - end - - desc 'Create NPM package' do - detail 'This feature was introduced in GitLab 11.8' - end - params do - requires :package_name, type: String, desc: 'Package name' - requires :versions, type: Hash, desc: 'Package version info' - end - route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true - put ':id/packages/npm/:package_name', requirements: NPM_ENDPOINT_REQUIREMENTS do - authorize_create_package!(user_project) - - track_package_event('push_package', :npm) - - created_package = ::Packages::Npm::CreatePackageService - .new(user_project, current_user, params.merge(build: current_authenticated_job)).execute - - if created_package[:status] == :error - render_api_error!(created_package[:message], created_package[:http_status]) - else - created_package - end - end - end - end -end diff --git a/lib/api/npm_project_packages.rb b/lib/api/npm_project_packages.rb new file mode 100644 index 00000000000..887084dc9ae --- /dev/null +++ b/lib/api/npm_project_packages.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true +module API + class NpmProjectPackages < ::API::Base + helpers ::API::Helpers::Packages::Npm + + feature_category :package_registry + + rescue_from ActiveRecord::RecordInvalid do |e| + render_api_error!(e.message, 400) + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + namespace 'projects/:id/packages/npm' do + desc 'Download the NPM tarball' do + detail 'This feature was introduced in GitLab 11.8' + end + params do + requires :package_name, type: String, desc: 'Package name' + requires :file_name, type: String, desc: 'Package file name' + end + route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true + get '*package_name/-/*file_name', format: false do + authorize_read_package!(project) + + package = project.packages.npm + .by_name_and_file_name(params[:package_name], params[:file_name]) + + not_found!('Package') unless package + + package_file = ::Packages::PackageFileFinder + .new(package, params[:file_name]).execute! + + track_package_event('pull_package', package, category: 'API::NpmPackages') + + present_carrierwave_file!(package_file.file) + end + + desc 'Create NPM package' do + detail 'This feature was introduced in GitLab 11.8' + end + params do + requires :package_name, type: String, desc: 'Package name' + requires :versions, type: Hash, desc: 'Package version info' + end + route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true + put ':package_name', requirements: ::API::Helpers::Packages::Npm::NPM_ENDPOINT_REQUIREMENTS do + authorize_create_package!(project) + + track_package_event('push_package', :npm, category: 'API::NpmPackages') + + created_package = ::Packages::Npm::CreatePackageService + .new(project, current_user, params.merge(build: current_authenticated_job)).execute + + if created_package[:status] == :error + render_api_error!(created_package[:message], created_package[:http_status]) + else + created_package + end + end + + include ::API::Concerns::Packages::NpmEndpoints + end + end +end diff --git a/lib/api/nuget_packages.rb b/lib/api/nuget_packages.rb index 0f2c956a9df..65a85f3c930 100644 --- a/lib/api/nuget_packages.rb +++ b/lib/api/nuget_packages.rb @@ -10,6 +10,8 @@ module API helpers ::API::Helpers::PackagesManagerClientsHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers + feature_category :package_registry + POSITIVE_INTEGER_REGEX = %r{\A[1-9]\d*\z}.freeze NON_NEGATIVE_INTEGER_REGEX = %r{\A0|[1-9]\d*\z}.freeze diff --git a/lib/api/package_files.rb b/lib/api/package_files.rb index c1fc9a6e4d8..4a33f3e8af2 100644 --- a/lib/api/package_files.rb +++ b/lib/api/package_files.rb @@ -8,6 +8,8 @@ module API authorize_packages_access!(user_project) end + feature_category :package_registry + helpers ::API::Helpers::PackagesHelpers params do diff --git a/lib/api/pages.rb b/lib/api/pages.rb index 813307c498f..5f695f3853d 100644 --- a/lib/api/pages.rb +++ b/lib/api/pages.rb @@ -2,6 +2,8 @@ module API class Pages < ::API::Base + feature_category :pages + 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 00c51298c45..2e7f8475509 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -4,6 +4,8 @@ module API class PagesDomains < ::API::Base include PaginationParams + feature_category :pages + PAGES_DOMAINS_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(domain: API::NO_SLASH_URL_PART_REGEX) before do diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb new file mode 100644 index 00000000000..2c60938b75a --- /dev/null +++ b/lib/api/personal_access_tokens.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module API + class PersonalAccessTokens < ::API::Base + include ::API::PaginationParams + + feature_category :authentication_and_authorization + + desc 'Get all Personal Access Tokens' do + detail 'This feature was added in GitLab 13.3' + success Entities::PersonalAccessToken + end + params do + optional :user_id, type: Integer, desc: 'User ID' + + use :pagination + end + + before do + authenticate! + restrict_non_admins! unless current_user.admin? + end + + helpers do + def finder_params(current_user) + current_user.admin? ? { user: user(params[:user_id]) } : { user: current_user } + end + + def user(user_id) + UserFinder.new(user_id).find_by_id + end + + def restrict_non_admins! + return if params[:user_id].blank? + + unauthorized! unless Ability.allowed?(current_user, :read_user_personal_access_tokens, user(params[:user_id])) + end + + def find_token(id) + PersonalAccessToken.find(id) || not_found! + end + end + + resources :personal_access_tokens do + get do + tokens = PersonalAccessTokensFinder.new(finder_params(current_user), current_user).execute + + present paginate(tokens), with: Entities::PersonalAccessToken + end + + delete ':id' do + service = ::PersonalAccessTokens::RevokeService.new( + current_user, + token: find_token(params[:id]) + ).execute + + service.success? ? no_content! : bad_request!(nil) + end + end + end +end diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb index 46ccb4ba1a0..cfb0c5fd705 100644 --- a/lib/api/project_clusters.rb +++ b/lib/api/project_clusters.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :kubernetes_management + params do requires :id, type: String, desc: 'The ID of the project' end diff --git a/lib/api/project_container_repositories.rb b/lib/api/project_container_repositories.rb index d565531d372..3125de88de5 100644 --- a/lib/api/project_container_repositories.rb +++ b/lib/api/project_container_repositories.rb @@ -10,6 +10,8 @@ module API before { authorize_read_container_images! } + feature_category :package_registry + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/project_events.rb b/lib/api/project_events.rb index 3765473bc0e..69b47f9420d 100644 --- a/lib/api/project_events.rb +++ b/lib/api/project_events.rb @@ -6,6 +6,8 @@ module API include APIGuard helpers ::API::Helpers::EventsHelpers + feature_category :users + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index 184f89200ab..76b3dea723a 100644 --- a/lib/api/project_export.rb +++ b/lib/api/project_export.rb @@ -4,6 +4,8 @@ module API class ProjectExport < ::API::Base helpers Helpers::RateLimiter + feature_category :importers + before do not_found! unless Gitlab::CurrentSettings.project_export_enabled? authorize_admin_project diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index bc2d8c816a8..431ba199131 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -7,6 +7,8 @@ module API before { authenticate! } before { authorize_admin_project } + feature_category :integrations + helpers do params :project_hook_properties do requires :url, type: String, desc: "The URL to send the request to" @@ -21,6 +23,7 @@ module API optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events" optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" optional :deployment_events, type: Boolean, desc: "Trigger hook on deployment events" + optional :releases_events, type: Boolean, desc: "Trigger hook on release events" optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" optional :push_events_branch_filter, type: String, desc: "Trigger hook on specified branch only" diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 5c4e1d73ee1..15b06cea385 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -8,6 +8,8 @@ module API helpers Helpers::FileUploadHelpers helpers Helpers::RateLimiter + feature_category :importers + helpers do def import_params declared_params(include_missing: false) diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb index a81118f44bd..8675de33923 100644 --- a/lib/api/project_milestones.rb +++ b/lib/api/project_milestones.rb @@ -7,6 +7,8 @@ module API before { authenticate! } + feature_category :issue_tracking + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/project_packages.rb b/lib/api/project_packages.rb index b8d97b1243a..56e94333433 100644 --- a/lib/api/project_packages.rb +++ b/lib/api/project_packages.rb @@ -8,6 +8,8 @@ module API authorize_packages_access!(user_project) end + feature_category :package_registry + helpers ::API::Helpers::PackagesHelpers params do diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb index 38eb74663d3..fe6de3ea385 100644 --- a/lib/api/project_repository_storage_moves.rb +++ b/lib/api/project_repository_storage_moves.rb @@ -6,6 +6,8 @@ module API before { authenticated_as_admin! } + feature_category :gitaly + resource :project_repository_storage_moves do desc 'Get a list of all project repository storage moves' do detail 'This feature was introduced in GitLab 13.0.' diff --git a/lib/api/project_snapshots.rb b/lib/api/project_snapshots.rb index e19afb6e8e4..d33d2976b1c 100644 --- a/lib/api/project_snapshots.rb +++ b/lib/api/project_snapshots.rb @@ -6,6 +6,8 @@ module API before { authorize_read_git_snapshot! } + feature_category :source_code_management + resource :projects do desc 'Download a (possibly inconsistent) snapshot of a repository' do detail 'This feature was introduced in GitLab 10.7' diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index b4de260fe49..899984fe0ba 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -6,6 +6,8 @@ module API before { check_snippets_enabled } + feature_category :snippets + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/project_statistics.rb b/lib/api/project_statistics.rb index 1ead969fc81..3db8d20ebac 100644 --- a/lib/api/project_statistics.rb +++ b/lib/api/project_statistics.rb @@ -2,6 +2,8 @@ module API class ProjectStatistics < ::API::Base + feature_category :source_code_management + before do authenticate! authorize! :daily_statistics, user_project diff --git a/lib/api/project_templates.rb b/lib/api/project_templates.rb index 7d851de0237..af5d96969ef 100644 --- a/lib/api/project_templates.rb +++ b/lib/api/project_templates.rb @@ -12,6 +12,8 @@ module API before { authenticate_non_get! } + feature_category :templates + params do requires :id, type: String, desc: 'The ID of a project' requires :type, type: String, values: TEMPLATE_TYPES, desc: 'The type (dockerfiles|gitignores|gitlab_ci_ymls|licenses|metrics_dashboard_ymls|issues|merge_requests) of the template' diff --git a/lib/api/projects.rb b/lib/api/projects.rb index ecee76ae60c..2012c348cd1 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -11,6 +11,8 @@ module API before { authenticate_non_get! } + feature_category :projects, ['/projects/:id/custom_attributes', '/projects/:id/custom_attributes/:key'] + helpers do # EE::API::Projects would override this method def apply_filters(projects) @@ -150,7 +152,7 @@ module API use :statistics_params use :with_custom_attributes end - get ":user_id/projects" do + get ":user_id/projects", feature_category: :projects do user = find_user(params[:user_id]) not_found!('User') unless user @@ -167,7 +169,7 @@ module API use :collection_params use :statistics_params end - get ":user_id/starred_projects" do + get ":user_id/starred_projects", feature_category: :projects do user = find_user(params[:user_id]) not_found!('User') unless user @@ -187,7 +189,7 @@ module API use :statistics_params use :with_custom_attributes end - get do + get feature_category: :projects do present_projects load_projects end @@ -234,7 +236,7 @@ module API use :create_params end # rubocop: disable CodeReuse/ActiveRecord - post "user/:user_id" do + post "user/:user_id", feature_category: :projects do Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/21139') authenticated_as_admin! user = User.find_by(id: params.delete(:user_id)) @@ -270,7 +272,7 @@ module API optional :license, type: Boolean, default: false, desc: 'Include project license data' end - get ":id" do + get ":id", feature_category: :projects do options = { with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails, current_user: current_user, @@ -294,7 +296,7 @@ module API optional :path, type: String, desc: 'The path that will be assigned to the fork' optional :name, type: String, desc: 'The name that will be assigned to the fork' end - post ':id/fork' do + post ':id/fork', feature_category: :source_code_management do Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42284') not_found! unless can?(current_user, :fork_project, user_project) @@ -332,14 +334,14 @@ module API use :collection_params use :with_custom_attributes end - get ':id/forks' do + get ':id/forks', feature_category: :source_code_management do forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute present_projects forks, request_scope: user_project end desc 'Check pages access of this project' - get ':id/pages_access' do + get ':id/pages_access', feature_category: :pages do authorize! :read_pages_content, user_project unless user_project.public_pages? status 200 end @@ -357,7 +359,7 @@ module API at_least_one_of(*Helpers::ProjectsHelpers.update_params_at_least_one_of) end - put ':id' do + put ':id', feature_category: :projects do authorize_admin_project attrs = declared_params(include_missing: false) authorize! :rename_project, user_project if attrs[:name].present? @@ -381,7 +383,7 @@ module API desc 'Archive a project' do success Entities::Project end - post ':id/archive' do + post ':id/archive', feature_category: :projects do authorize!(:archive_project, user_project) ::Projects::UpdateService.new(user_project, current_user, archived: true).execute @@ -392,7 +394,7 @@ module API desc 'Unarchive a project' do success Entities::Project end - post ':id/unarchive' do + post ':id/unarchive', feature_category: :projects do authorize!(:archive_project, user_project) ::Projects::UpdateService.new(user_project, current_user, archived: false).execute @@ -403,7 +405,7 @@ module API desc 'Star a project' do success Entities::Project end - post ':id/star' do + post ':id/star', feature_category: :projects do if current_user.starred?(user_project) not_modified! else @@ -417,7 +419,7 @@ module API desc 'Unstar a project' do success Entities::Project end - post ':id/unstar' do + post ':id/unstar', feature_category: :projects do if current_user.starred?(user_project) current_user.toggle_star(user_project) user_project.reset @@ -435,21 +437,21 @@ module API optional :search, type: String, desc: 'Return list of users matching the search criteria' use :pagination end - get ':id/starrers' do + get ':id/starrers', feature_category: :projects do starrers = UsersStarProjectsFinder.new(user_project, params, current_user: current_user).execute present paginate(starrers), with: Entities::UserStarsProject end desc 'Get languages in project repository' - get ':id/languages' do + get ':id/languages', feature_category: :source_code_management do ::Projects::RepositoryLanguagesService .new(user_project, current_user) .execute.map { |lang| [lang.name, lang.share] }.to_h end desc 'Delete a project' - delete ":id" do + delete ":id", feature_category: :projects do authorize! :remove_project, user_project delete_project(user_project) @@ -459,7 +461,7 @@ module API params do requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from' end - post ":id/fork/:forked_from_id" do + post ":id/fork/:forked_from_id", feature_category: :source_code_management do authorize! :admin_project, user_project fork_from_project = find_project!(params[:forked_from_id]) @@ -478,7 +480,7 @@ module API end desc 'Remove a forked_from relationship' - delete ":id/fork" do + delete ":id/fork", feature_category: :source_code_management do authorize! :remove_fork_project, user_project result = destroy_conditionally!(user_project) do @@ -496,7 +498,7 @@ module API requires :group_access, type: Integer, values: Gitlab::Access.values, as: :link_group_access, desc: 'The group access level' optional :expires_at, type: Date, desc: 'Share expiration date' end - post ":id/share" do + post ":id/share", feature_category: :authentication_and_authorization do authorize! :admin_project, user_project group = Group.find_by_id(params[:group_id]) @@ -518,7 +520,7 @@ module API requires :group_id, type: Integer, desc: 'The ID of the group' end # rubocop: disable CodeReuse/ActiveRecord - delete ":id/share/:group_id" do + delete ":id/share/:group_id", feature_category: :authentication_and_authorization do authorize! :admin_project, user_project link = user_project.project_group_links.find_by(group_id: params[:group_id]) @@ -535,7 +537,7 @@ module API # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 requires :file, type: File, desc: 'The file to be uploaded' # rubocop:disable Scalability/FileUploads end - post ":id/uploads" do + post ":id/uploads", feature_category: :not_owned do upload = UploadService.new(user_project, params[:file]).execute present upload, with: Entities::ProjectUpload @@ -549,7 +551,7 @@ module API optional :skip_users, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Filter out users with the specified IDs' use :pagination end - get ':id/users' do + get ':id/users', feature_category: :authentication_and_authorization do users = DeclarativePolicy.subject_scope { user_project.team.users } users = users.search(params[:search]) if params[:search].present? users = users.where_not_in(params[:skip_users]) if params[:skip_users].present? @@ -560,7 +562,7 @@ module API desc 'Start the housekeeping task for a project' do detail 'This feature was introduced in GitLab 9.0.' end - post ':id/housekeeping' do + post ':id/housekeeping', feature_category: :source_code_management do authorize_admin_project begin @@ -574,7 +576,7 @@ module API params do requires :namespace, type: String, desc: 'The ID or path of the new namespace' end - put ":id/transfer" do + put ":id/transfer", feature_category: :projects do authorize! :change_namespace, user_project namespace = find_namespace!(params[:namespace]) diff --git a/lib/api/protected_branches.rb b/lib/api/protected_branches.rb index a448682d8bd..17574739a7c 100644 --- a/lib/api/protected_branches.rb +++ b/lib/api/protected_branches.rb @@ -8,6 +8,8 @@ module API before { authorize_admin_project } + feature_category :source_code_management + helpers Helpers::ProtectedBranchesHelpers params do diff --git a/lib/api/protected_tags.rb b/lib/api/protected_tags.rb index dd3e407ffc9..b9385df1f8d 100644 --- a/lib/api/protected_tags.rb +++ b/lib/api/protected_tags.rb @@ -8,6 +8,8 @@ module API before { authorize_admin_project } + feature_category :source_code_management + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index 5622bc6e42d..7104fb8d999 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -12,6 +12,8 @@ module API helpers ::API::Helpers::Packages::BasicAuthHelpers include ::API::Helpers::Packages::BasicAuthHelpers::Constants + feature_category :package_registry + default_format :json rescue_from ArgumentError do |e| diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index 23de9f9fc9f..d3a185a51c8 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -10,6 +10,8 @@ module API before { authorize! :read_release, user_project } + feature_category :release_orchestration + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/releases.rb b/lib/api/releases.rb index 3bd6ea77403..c20e618efd1 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -9,6 +9,8 @@ module API before { authorize_read_releases! } + feature_category :release_orchestration + params do requires :id, type: String, desc: 'The ID of a project' end @@ -89,7 +91,7 @@ module API optional :name, type: String, desc: 'The name of the release' optional :description, type: String, desc: 'Release notes with markdown support' optional :released_at, type: DateTime, desc: 'The date when the release will be/was ready.' - optional :milestones, type: Array, desc: 'The titles of the related milestones' + optional :milestones, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The titles of the related milestones' end put ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do authorize_update_release! diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb index f63ea04a529..83096772d32 100644 --- a/lib/api/remote_mirrors.rb +++ b/lib/api/remote_mirrors.rb @@ -4,6 +4,8 @@ module API class RemoteMirrors < ::API::Base include PaginationParams + feature_category :source_code_management + before do unauthorized! unless can?(current_user, :admin_remote_mirror, user_project) end diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index 38ac1f22a48..8af8ffc3b63 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -6,10 +6,14 @@ module API class Repositories < ::API::Base include PaginationParams + content_type :txt, 'text/plain' + helpers ::API::Helpers::HeadersHelpers before { authorize! :download_code, user_project } + feature_category :source_code_management + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/resource_label_events.rb b/lib/api/resource_label_events.rb index d3a219f0810..33589f6c393 100644 --- a/lib/api/resource_label_events.rb +++ b/lib/api/resource_label_events.rb @@ -7,7 +7,7 @@ module API before { authenticate! } - Helpers::ResourceLabelEventsHelpers.eventable_types.each do |eventable_type| + Helpers::ResourceLabelEventsHelpers.feature_category_per_eventable_type.each do |eventable_type, feature_category| parent_type = eventable_type.parent_class.to_s.underscore eventables_str = eventable_type.to_s.underscore.pluralize @@ -24,7 +24,7 @@ module API use :pagination end - get ":id/#{eventables_str}/:eventable_id/resource_label_events" do + get ":id/#{eventables_str}/:eventable_id/resource_label_events", feature_category: feature_category do eventable = find_noteable(eventable_type, params[:eventable_id]) events = eventable.resource_label_events.inc_relations @@ -40,7 +40,7 @@ module API requires :event_id, type: String, desc: 'The ID of a resource label event' requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable' end - get ":id/#{eventables_str}/:eventable_id/resource_label_events/:event_id" do + get ":id/#{eventables_str}/:eventable_id/resource_label_events/:event_id", feature_category: feature_category do eventable = find_noteable(eventable_type, params[:eventable_id]) event = eventable.resource_label_events.find(params[:event_id]) diff --git a/lib/api/resource_milestone_events.rb b/lib/api/resource_milestone_events.rb index 21411f68dd5..aeedd7ad109 100644 --- a/lib/api/resource_milestone_events.rb +++ b/lib/api/resource_milestone_events.rb @@ -7,7 +7,10 @@ module API before { authenticate! } - [Issue, MergeRequest].each do |eventable_type| + { + Issue => :issue_tracking, + MergeRequest => :code_review + }.each do |eventable_type, feature_category| parent_type = eventable_type.parent_class.to_s.underscore eventables_str = eventable_type.to_s.underscore.pluralize @@ -23,7 +26,7 @@ module API use :pagination end - get ":id/#{eventables_str}/:eventable_id/resource_milestone_events" do + get ":id/#{eventables_str}/:eventable_id/resource_milestone_events", feature_category: feature_category do eventable = find_noteable(eventable_type, params[:eventable_id]) events = ResourceMilestoneEventFinder.new(current_user, eventable).execute @@ -38,7 +41,7 @@ module API requires :event_id, type: String, desc: 'The ID of a resource milestone event' requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable' end - get ":id/#{eventables_str}/:eventable_id/resource_milestone_events/:event_id" do + get ":id/#{eventables_str}/:eventable_id/resource_milestone_events/:event_id", feature_category: feature_category do eventable = find_noteable(eventable_type, params[:eventable_id]) event = eventable.resource_milestone_events.find(params[:event_id]) diff --git a/lib/api/resource_state_events.rb b/lib/api/resource_state_events.rb index 9bfda39be90..3460aa2c00e 100644 --- a/lib/api/resource_state_events.rb +++ b/lib/api/resource_state_events.rb @@ -7,7 +7,10 @@ module API before { authenticate! } - [Issue, MergeRequest].each do |eventable_class| + { + Issue => :issue_tracking, + MergeRequest => :code_review + }.each do |eventable_class, feature_category| eventable_name = eventable_class.to_s.underscore params do @@ -22,7 +25,7 @@ module API use :pagination end - get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events" do + get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events", feature_category: feature_category do eventable = find_noteable(eventable_class, params[:eventable_iid]) events = ResourceStateEventFinder.new(current_user, eventable).execute @@ -37,7 +40,7 @@ module API requires :eventable_iid, types: Integer, desc: "The IID of the #{eventable_name}" requires :event_id, type: Integer, desc: 'The ID of a resource state event' end - get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events/:event_id" do + get ":id/#{eventable_name.pluralize}/:eventable_iid/resource_state_events/:event_id", feature_category: feature_category do eventable = find_noteable(eventable_class, params[:eventable_iid]) event = ResourceStateEventFinder.new(current_user, eventable).find(params[:event_id]) diff --git a/lib/api/search.rb b/lib/api/search.rb index 85f0a8e2e60..f0ffe6ba443 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :global_search + helpers do SCOPE_ENTITY = { merge_requests: Entities::MergeRequestBasic, @@ -35,8 +37,11 @@ module API state: params[:state], confidential: params[:confidential], snippets: snippets?, + basic_search: params[:basic_search], page: params[:page], - per_page: params[:per_page] + per_page: params[:per_page], + order_by: params[:order_by], + sort: params[:sort] }.merge(additional_params) results = SearchService.new(current_user, search_params).search_objects(preload_method) diff --git a/lib/api/services.rb b/lib/api/services.rb index 5f3d14010a8..cfcae13e518 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module API class Services < ::API::Base + feature_category :integrations + services = Helpers::ServicesHelpers.services service_classes = Helpers::ServicesHelpers.service_classes diff --git a/lib/api/settings.rb b/lib/api/settings.rb index dc917d9c529..b95856d99d1 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -4,6 +4,8 @@ module API class Settings < ::API::Base before { authenticated_as_admin! } + feature_category :not_owned + helpers Helpers::SettingsHelpers helpers do @@ -51,9 +53,9 @@ module API optional :default_projects_limit, type: Integer, desc: 'The maximum number of personal projects' optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility' optional :disabled_oauth_sign_in_sources, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Disable certain OAuth sign-in sources' - optional :domain_blacklist_enabled, type: Boolean, desc: 'Enable domain blacklist for sign ups' - optional :domain_blacklist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' - optional :domain_whitelist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' + optional :domain_denylist_enabled, type: Boolean, desc: 'Enable domain denylist for sign ups' + optional :domain_denylist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' + optional :domain_allowlist, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, desc: 'ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com' optional :eks_integration_enabled, type: Boolean, desc: 'Enable integration with Amazon EKS' given eks_integration_enabled: -> (val) { val } do requires :eks_account_id, type: String, desc: 'Amazon account ID for EKS integration' @@ -157,6 +159,7 @@ module API optional :issues_create_limit, type: Integer, desc: "Maximum number of issue creation requests allowed per minute per user. Set to 0 for unlimited requests per minute." optional :raw_blob_request_limit, type: Integer, desc: "Maximum number of requests per minute for each raw path. Set to 0 for unlimited requests per minute." optional :wiki_page_max_content_bytes, type: Integer, desc: "Maximum wiki page content size in bytes" + optional :require_admin_approval_after_user_signup, type: Boolean, desc: 'Require explicit admin approval for new signups' ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/api/sidekiq_metrics.rb b/lib/api/sidekiq_metrics.rb index b025dbfab37..680363d036e 100644 --- a/lib/api/sidekiq_metrics.rb +++ b/lib/api/sidekiq_metrics.rb @@ -6,6 +6,8 @@ module API class SidekiqMetrics < ::API::Base before { authenticated_as_admin! } + feature_category :not_owned + helpers do def queue_metrics Sidekiq::Queue.all.each_with_object({}) do |queue, hash| diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 2e67b9649bc..52b597fb788 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -5,6 +5,8 @@ module API class Snippets < ::API::Base include PaginationParams + feature_category :snippets + resource :snippets do helpers Helpers::SnippetsHelpers helpers do diff --git a/lib/api/statistics.rb b/lib/api/statistics.rb index fa7176491ba..1814e1a6782 100644 --- a/lib/api/statistics.rb +++ b/lib/api/statistics.rb @@ -4,6 +4,8 @@ module API class Statistics < ::API::Base before { authenticated_as_admin! } + feature_category :instance_statistics + COUNTED_ITEMS = [Project, User, Group, ForkNetworkMember, ForkNetwork, Issue, MergeRequest, Note, Snippet, Key, Milestone].freeze diff --git a/lib/api/submodules.rb b/lib/api/submodules.rb index e2ceb49c119..5c71a18c6d0 100644 --- a/lib/api/submodules.rb +++ b/lib/api/submodules.rb @@ -4,6 +4,8 @@ module API class Submodules < ::API::Base before { authenticate! } + feature_category :source_code_management + helpers do def commit_params(attrs) { diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb index 35a28da4736..914bab52929 100644 --- a/lib/api/subscriptions.rb +++ b/lib/api/subscriptions.rb @@ -11,25 +11,29 @@ module API type: 'merge_requests', entity: Entities::MergeRequest, source: Project, - finder: ->(id) { find_merge_request_with_access(id, :update_merge_request) } + finder: ->(id) { find_merge_request_with_access(id, :update_merge_request) }, + feature_category: :code_review }, { type: 'issues', entity: Entities::Issue, source: Project, - finder: ->(id) { find_project_issue(id) } + finder: ->(id) { find_project_issue(id) }, + feature_category: :issue_tracking }, { type: 'labels', entity: Entities::ProjectLabel, source: Project, - finder: ->(id) { find_label(user_project, id) } + finder: ->(id) { find_label(user_project, id) }, + feature_category: :issue_tracking }, { type: 'labels', entity: Entities::GroupLabel, source: Group, - finder: ->(id) { find_label(user_group, id) } + finder: ->(id) { find_label(user_group, id) }, + feature_category: :issue_tracking } ] @@ -44,7 +48,7 @@ module API desc 'Subscribe to a resource' do success subscribable[:entity] end - post ":id/#{subscribable[:type]}/:subscribable_id/subscribe" do + post ":id/#{subscribable[:type]}/:subscribable_id/subscribe", subscribable.slice(:feature_category) do parent = parent_resource(source_type) resource = instance_exec(params[:subscribable_id], &subscribable[:finder]) @@ -59,7 +63,7 @@ module API desc 'Unsubscribe from a resource' do success subscribable[:entity] end - post ":id/#{subscribable[:type]}/:subscribable_id/unsubscribe" do + post ":id/#{subscribable[:type]}/:subscribable_id/unsubscribe", subscribable.slice(:feature_category) do parent = parent_resource(source_type) resource = instance_exec(params[:subscribable_id], &subscribable[:finder]) diff --git a/lib/api/suggestions.rb b/lib/api/suggestions.rb index f23d279c3f4..a024d6de874 100644 --- a/lib/api/suggestions.rb +++ b/lib/api/suggestions.rb @@ -4,6 +4,8 @@ module API class Suggestions < ::API::Base before { authenticate! } + feature_category :code_review + resource :suggestions do desc 'Apply suggestion patch in the Merge Request it was created' do success Entities::Suggestion diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb index 2820d305d0f..42e16d47a0b 100644 --- a/lib/api/system_hooks.rb +++ b/lib/api/system_hooks.rb @@ -4,6 +4,8 @@ module API class SystemHooks < ::API::Base include PaginationParams + feature_category :integrations + before do authenticate! authenticated_as_admin! diff --git a/lib/api/tags.rb b/lib/api/tags.rb index b969394ec47..7636c45bdac 100644 --- a/lib/api/tags.rb +++ b/lib/api/tags.rb @@ -23,7 +23,7 @@ module API optional :search, type: String, desc: 'Return list of tags matching the search criteria' use :pagination end - get ':id/repository/tags' do + get ':id/repository/tags', feature_category: :source_code_management do tags = ::TagsFinder.new(user_project.repository, sort: "#{params[:order_by]}_#{params[:sort]}", search: params[:search]).execute @@ -37,7 +37,7 @@ module API params do requires :tag_name, type: String, desc: 'The name of the tag' end - get ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS do + get ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :source_code_management do tag = user_project.repository.find_tag(params[:tag_name]) not_found!('Tag') unless tag @@ -54,7 +54,7 @@ module API optional :message, type: String, desc: 'Specifying a message creates an annotated tag' optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database (deprecated in GitLab 11.7)' end - post ':id/repository/tags' do + post ':id/repository/tags', :release_orchestration do authorize_admin_tag result = ::Tags::CreateService.new(user_project, current_user) @@ -86,7 +86,7 @@ module API params do requires :tag_name, type: String, desc: 'The name of the tag' end - delete ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS do + delete ':id/repository/tags/:tag_name', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :source_code_management do authorize_admin_tag tag = user_project.repository.find_tag(params[:tag_name]) @@ -112,7 +112,7 @@ module API requires :tag_name, type: String, desc: 'The name of the tag', as: :tag requires :description, type: String, desc: 'Release notes with markdown support' end - post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS do + post ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do authorize_create_release! ## @@ -144,7 +144,7 @@ module API requires :tag_name, type: String, desc: 'The name of the tag', as: :tag requires :description, type: String, desc: 'Release notes with markdown support' end - put ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS do + put ':id/repository/tags/:tag_name/release', requirements: TAG_ENDPOINT_REQUIREMENTS, feature_category: :release_orchestration do authorize_update_release! result = ::Releases::UpdateService diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 0b427bbf5b9..b7fb35eac03 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -4,6 +4,8 @@ module API class Templates < ::API::Base include PaginationParams + feature_category :templates + GLOBAL_TEMPLATE_TYPES = { gitignores: { gitlab_version: 8.8 diff --git a/lib/api/terraform/state.rb b/lib/api/terraform/state.rb index 3dbde4639ca..c664c0a4590 100644 --- a/lib/api/terraform/state.rb +++ b/lib/api/terraform/state.rb @@ -7,6 +7,8 @@ module API class State < ::API::Base include ::Gitlab::Utils::StrongMemoize + feature_category :infrastructure_as_code + default_format :json before do @@ -51,7 +53,7 @@ module API no_content! if data.empty? remote_state_handler.handle_with_lock do |state| - state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial]) + state.update_file!(CarrierWaveStringFile.new(data), version: params[:serial], build: current_authenticated_job) end body false diff --git a/lib/api/terraform/state_version.rb b/lib/api/terraform/state_version.rb index b4a0efd7a2b..d3680323b9f 100644 --- a/lib/api/terraform/state_version.rb +++ b/lib/api/terraform/state_version.rb @@ -5,6 +5,8 @@ module API class StateVersion < ::API::Base default_format :json + feature_category :infrastructure_as_code + before do authenticate! authorize! :read_terraform_state, user_project diff --git a/lib/api/todos.rb b/lib/api/todos.rb index ce07d13cc9a..03850ba1c4e 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -6,6 +6,8 @@ module API before { authenticate! } + feature_category :issue_tracking + ISSUABLE_TYPES = { 'merge_requests' => ->(iid) { find_merge_request_with_access(iid) }, 'issues' => ->(iid) { find_project_issue(iid) } diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index 960d004a04c..aebbc95cbea 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -6,6 +6,8 @@ module API HTTP_GITLAB_EVENT_HEADER = "HTTP_#{WebHookService::GITLAB_EVENT_HEADER}".underscore.upcase + feature_category :continuous_integration + params do requires :id, type: String, desc: 'The ID of a project' end diff --git a/lib/api/unleash.rb b/lib/api/unleash.rb index 907422118f1..3148c56339a 100644 --- a/lib/api/unleash.rb +++ b/lib/api/unleash.rb @@ -4,6 +4,8 @@ module API class Unleash < ::API::Base include PaginationParams + feature_category :feature_flags + namespace :feature_flags do resource :unleash, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do params do diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb index fa5bfc1cbe9..7b038ec74bb 100644 --- a/lib/api/usage_data.rb +++ b/lib/api/usage_data.rb @@ -4,6 +4,8 @@ module API class UsageData < ::API::Base before { authenticate! } + feature_category :collection + namespace 'usage_data' do before do not_found! unless Feature.enabled?(:usage_data_api, default_enabled: true) diff --git a/lib/api/user_counts.rb b/lib/api/user_counts.rb index 6d9db53fec8..3071f08e1de 100644 --- a/lib/api/user_counts.rb +++ b/lib/api/user_counts.rb @@ -2,6 +2,8 @@ module API class UserCounts < ::API::Base + feature_category :navigation + 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 e7c1d644324..501ed629c7e 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -8,6 +8,8 @@ module API allow_access_with_scope :read_user, if: -> (request) { request.get? } + feature_category :users, ['/users/:id/custom_attributes', '/users/:id/custom_attributes/:key'] + resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do include CustomAttributesEndpoints @@ -63,9 +65,9 @@ module API params :sort_params do optional :order_by, type: String, values: %w[id name username created_at updated_at], - default: 'id', desc: 'Return users ordered by a field' + default: 'id', desc: 'Return users ordered by a field' optional :sort, type: String, values: %w[asc desc], default: 'desc', - desc: 'Return users sorted in ascending and descending order' + desc: 'Return users sorted in ascending and descending order' end end @@ -93,7 +95,7 @@ module API use :optional_index_params_ee end # rubocop: disable CodeReuse/ActiveRecord - get do + get feature_category: :users do authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) unless current_user&.admin? @@ -134,7 +136,7 @@ module API use :with_custom_attributes end # rubocop: disable CodeReuse/ActiveRecord - get ":id" do + get ":id", feature_category: :users do user = User.find_by(id: params[:id]) not_found!('User') unless user && can?(current_user, :read_user, user) @@ -149,7 +151,7 @@ module API params do requires :user_id, type: String, desc: 'The ID or username of the user' end - get ":user_id/status", requirements: API::USER_REQUIREMENTS do + get ":user_id/status", requirements: API::USER_REQUIREMENTS, feature_category: :users do user = find_user(params[:user_id]) not_found!('User') unless user && can?(current_user, :read_user, user) @@ -170,7 +172,7 @@ module API optional :force_random_password, type: Boolean, desc: 'Flag indicating a random password will be set' use :optional_attributes end - post do + post feature_category: :users do authenticated_as_admin! params = declared_params(include_missing: false) @@ -204,7 +206,7 @@ module API use :optional_attributes end # rubocop: disable CodeReuse/ActiveRecord - put ":id" do + put ":id", feature_category: :users do authenticated_as_admin! user = User.find_by(id: params.delete(:id)) @@ -245,7 +247,7 @@ module API requires :provider, type: String, desc: 'The external provider' end # rubocop: disable CodeReuse/ActiveRecord - delete ":id/identities/:provider" do + delete ":id/identities/:provider", feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -268,7 +270,7 @@ module API optional :expires_at, type: DateTime, desc: 'The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)' end # rubocop: disable CodeReuse/ActiveRecord - post ":id/keys" do + post ":id/keys", feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params.delete(:id)) @@ -291,7 +293,7 @@ module API requires :user_id, type: String, desc: 'The ID or username of the user' use :pagination end - get ':user_id/keys', requirements: API::USER_REQUIREMENTS do + get ':user_id/keys', requirements: API::USER_REQUIREMENTS, feature_category: :authentication_and_authorization do user = find_user(params[:user_id]) not_found!('User') unless user && can?(current_user, :read_user, user) @@ -307,7 +309,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord - delete ':id/keys/:key_id' do + delete ':id/keys/:key_id', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -332,7 +334,7 @@ module API requires :key, type: String, desc: 'The new GPG key' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/gpg_keys' do + post ':id/gpg_keys', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params.delete(:id)) @@ -357,7 +359,7 @@ module API use :pagination end # rubocop: disable CodeReuse/ActiveRecord - get ':id/gpg_keys' do + get ':id/gpg_keys', feature_category: :authentication_and_authorization do user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -374,7 +376,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - get ':id/gpg_keys/:key_id' do + get ':id/gpg_keys/:key_id', feature_category: :authentication_and_authorization do user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -393,7 +395,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - delete ':id/gpg_keys/:key_id' do + delete ':id/gpg_keys/:key_id', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -417,7 +419,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/gpg_keys/:key_id/revoke' do + post ':id/gpg_keys/:key_id/revoke', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -440,7 +442,7 @@ module API optional :skip_confirmation, type: Boolean, desc: 'Skip confirmation of email and assume it is verified' end # rubocop: disable CodeReuse/ActiveRecord - post ":id/emails" do + post ":id/emails", feature_category: :users do authenticated_as_admin! user = User.find_by(id: params.delete(:id)) @@ -464,7 +466,7 @@ module API use :pagination end # rubocop: disable CodeReuse/ActiveRecord - get ':id/emails' do + get ':id/emails', feature_category: :users do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -481,7 +483,7 @@ module API requires :email_id, type: Integer, desc: 'The ID of the email' end # rubocop: disable CodeReuse/ActiveRecord - delete ':id/emails/:email_id' do + delete ':id/emails/:email_id', feature_category: :users do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -503,7 +505,7 @@ module API optional :hard_delete, type: Boolean, desc: "Whether to remove a user's contributions" end # rubocop: disable CodeReuse/ActiveRecord - delete ":id" do + delete ":id", feature_category: :users do Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/20757') authenticated_as_admin! @@ -523,7 +525,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/activate' do + post ':id/activate', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) @@ -538,7 +540,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/deactivate' do + post ':id/deactivate', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -564,7 +566,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/block' do + post ':id/block', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -589,7 +591,7 @@ module API requires :id, type: Integer, desc: 'The ID of the user' end # rubocop: disable CodeReuse/ActiveRecord - post ':id/unblock' do + post ':id/unblock', feature_category: :authentication_and_authorization do authenticated_as_admin! user = User.find_by(id: params[:id]) not_found!('User') unless user @@ -612,7 +614,7 @@ module API optional :type, type: String, values: %w[Project Namespace] use :pagination end - get ":user_id/memberships" do + get ":user_id/memberships", feature_category: :users do authenticated_as_admin! user = find_user_by_id(params) @@ -656,7 +658,9 @@ module API use :pagination optional :state, type: String, default: 'all', values: %w[all active inactive], desc: 'Filters (all|active|inactive) impersonation_tokens' end - get { present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken } + get feature_category :authentication_and_authorization do + present paginate(finder(declared_params(include_missing: false)).execute), with: Entities::ImpersonationToken + end desc 'Create a impersonation token. Available only for admins.' do detail 'This feature was introduced in GitLab 9.0' @@ -667,7 +671,7 @@ module API optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the impersonation token' optional :scopes, type: Array, desc: 'The array of scopes of the impersonation token' end - post do + post feature_category: :authentication_and_authorization do impersonation_token = finder.build(declared_params(include_missing: false)) if impersonation_token.save @@ -684,7 +688,7 @@ module API params do requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' end - get ':impersonation_token_id' do + get ':impersonation_token_id', feature_category: :authentication_and_authorization do present find_impersonation_token, with: Entities::ImpersonationToken end @@ -694,7 +698,7 @@ module API params do requires :impersonation_token_id, type: Integer, desc: 'The ID of the impersonation token' end - delete ':impersonation_token_id' do + delete ':impersonation_token_id', feature_category: :authentication_and_authorization do token = find_impersonation_token destroy_conditionally!(token) do @@ -702,6 +706,40 @@ module API end end end + + resource :personal_access_tokens do + helpers do + def target_user + find_user_by_id(params) + end + end + + before { authenticated_as_admin! } + + desc 'Create a personal access token. Available only for admins.' do + detail 'This feature was introduced in GitLab 13.6' + success Entities::PersonalAccessTokenWithToken + end + params do + requires :name, type: String, desc: 'The name of the personal access token' + requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::Gitlab::Auth.all_available_scopes.map(&:to_s), + desc: 'The array of scopes of the personal access token' + optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token' + end + post feature_category: :authentication_and_authorization do + not_found! unless Feature.enabled?(:pat_creation_api_for_admin) + + response = ::PersonalAccessTokens::CreateService.new( + current_user: current_user, target_user: target_user, params: declared_params(include_missing: false) + ).execute + + if response.success? + present response.payload[:personal_access_token], with: Entities::PersonalAccessTokenWithToken + else + render_api_error!(response.message, response.http_status || :unprocessable_entity) + end + end + end end end @@ -716,7 +754,7 @@ module API desc 'Get the currently authenticated user' do success Entities::UserPublic end - get do + get feature_category: :users do entity = if current_user.admin? Entities::UserWithAdmin @@ -734,7 +772,7 @@ module API params do use :pagination end - get "keys" do + get "keys", feature_category: :authentication_and_authorization do keys = current_user.keys.preload_users present paginate(keys), with: Entities::SSHKey @@ -747,7 +785,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord - get "keys/:key_id" do + get "keys/:key_id", feature_category: :authentication_and_authorization do key = current_user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key @@ -763,7 +801,7 @@ module API requires :title, type: String, desc: 'The title of the new SSH key' optional :expires_at, type: DateTime, desc: 'The expiration date of the SSH key in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ)' end - post "keys" do + post "keys", feature_category: :authentication_and_authorization do key = ::Keys::CreateService.new(current_user, declared_params(include_missing: false)).execute if key.persisted? @@ -780,7 +818,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord - delete "keys/:key_id" do + delete "keys/:key_id", feature_category: :authentication_and_authorization do key = current_user.keys.find_by(id: params[:key_id]) not_found!('Key') unless key @@ -798,7 +836,7 @@ module API params do use :pagination end - get 'gpg_keys' do + get 'gpg_keys', feature_category: :authentication_and_authorization do present paginate(current_user.gpg_keys), with: Entities::GpgKey end @@ -810,7 +848,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - get 'gpg_keys/:key_id' do + get 'gpg_keys/:key_id', feature_category: :authentication_and_authorization do key = current_user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key @@ -825,7 +863,7 @@ module API params do requires :key, type: String, desc: 'The new GPG key' end - post 'gpg_keys' do + post 'gpg_keys', feature_category: :authentication_and_authorization do key = ::GpgKeys::CreateService.new(current_user, declared_params(include_missing: false)).execute if key.persisted? @@ -842,7 +880,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the GPG key' end # rubocop: disable CodeReuse/ActiveRecord - post 'gpg_keys/:key_id/revoke' do + post 'gpg_keys/:key_id/revoke', feature_category: :authentication_and_authorization do key = current_user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key @@ -858,7 +896,7 @@ module API requires :key_id, type: Integer, desc: 'The ID of the SSH key' end # rubocop: disable CodeReuse/ActiveRecord - delete 'gpg_keys/:key_id' do + delete 'gpg_keys/:key_id', feature_category: :authentication_and_authorization do key = current_user.gpg_keys.find_by(id: params[:key_id]) not_found!('GPG Key') unless key @@ -875,7 +913,7 @@ module API params do use :pagination end - get "emails" do + get "emails", feature_category: :users do present paginate(current_user.emails), with: Entities::Email end @@ -886,7 +924,7 @@ module API requires :email_id, type: Integer, desc: 'The ID of the email' end # rubocop: disable CodeReuse/ActiveRecord - get "emails/:email_id" do + get "emails/:email_id", feature_category: :users do email = current_user.emails.find_by(id: params[:email_id]) not_found!('Email') unless email @@ -900,7 +938,7 @@ module API params do requires :email, type: String, desc: 'The new email' end - post "emails" do + post "emails", feature_category: :users do email = Emails::CreateService.new(current_user, declared_params.merge(user: current_user)).execute if email.errors.blank? @@ -915,7 +953,7 @@ module API requires :email_id, type: Integer, desc: 'The ID of the email' end # rubocop: disable CodeReuse/ActiveRecord - delete "emails/:email_id" do + delete "emails/:email_id", feature_category: :users do email = current_user.emails.find_by(id: params[:email_id]) not_found!('Email') unless email @@ -931,7 +969,7 @@ module API use :pagination end # rubocop: disable CodeReuse/ActiveRecord - get "activities" do + get "activities", feature_category: :users do authenticated_as_admin! activities = User @@ -948,8 +986,9 @@ module API params do optional :emoji, type: String, desc: "The emoji to set on the status" optional :message, type: String, desc: "The status message to set" + optional :availability, type: String, desc: "The availability of user to set" end - put "status" do + put "status", feature_category: :users do forbidden! unless can?(current_user, :update_user_status, current_user) if ::Users::SetStatusService.new(current_user, declared_params).execute @@ -962,7 +1001,7 @@ module API desc 'get the status of the current user' do success Entities::UserStatus end - get 'status' do + get 'status', feature_category: :users do present current_user.status || {}, with: Entities::UserStatus end end diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb index aed88e6091c..327335aec2d 100644 --- a/lib/api/v3/github.rb +++ b/lib/api/v3/github.rb @@ -22,6 +22,8 @@ module API include PaginationParams + feature_category :integrations + before do authorize_jira_user_agent!(request) authenticate! diff --git a/lib/api/validations/validators/email_or_email_list.rb b/lib/api/validations/validators/email_or_email_list.rb new file mode 100644 index 00000000000..b7f2a0cd443 --- /dev/null +++ b/lib/api/validations/validators/email_or_email_list.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module API + module Validations + module Validators + class EmailOrEmailList < Grape::Validations::Base + def validate_param!(attr_name, params) + value = params[attr_name] + + return unless value + + return if value.split(',').map { |v| ValidateEmail.valid?(v) }.all? + + raise Grape::Exceptions::Validation, + params: [@scope.full_name(attr_name)], + message: "contains an invalid email address" + end + end + end + end +end diff --git a/lib/api/variables.rb b/lib/api/variables.rb index f5de3d844e6..94fa98b7a14 100644 --- a/lib/api/variables.rb +++ b/lib/api/variables.rb @@ -7,6 +7,8 @@ module API before { authenticate! } before { authorize! :admin_build, user_project } + feature_category :continuous_integration + helpers do def filter_variable_parameters(params) # This method exists so that EE can more easily filter out certain diff --git a/lib/api/version.rb b/lib/api/version.rb index 841b55f8d6c..f8072658cc6 100644 --- a/lib/api/version.rb +++ b/lib/api/version.rb @@ -9,6 +9,8 @@ module API before { authenticate! } + feature_category :not_owned + METADATA_QUERY = <<~EOF { metadata { diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 21f457046f1..3fa42be47a9 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -4,6 +4,8 @@ module API class Wikis < ::API::Base helpers ::API::Helpers::WikisHelpers + feature_category :wiki + helpers do attr_reader :container |