diff options
Diffstat (limited to 'lib/api')
60 files changed, 618 insertions, 302 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb index b23b11d0c29..5e449022676 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -254,6 +254,7 @@ module API mount ::API::NugetProjectPackages mount ::API::PackageFiles mount ::API::Pages + mount ::API::PagesDomains mount ::API::PersonalAccessTokens::SelfInformation mount ::API::PersonalAccessTokens mount ::API::ProjectClusters @@ -296,6 +297,7 @@ module API mount ::API::UsageData mount ::API::UsageDataNonSqlMetrics mount ::API::UsageDataQueries + mount ::API::Users mount ::API::UserCounts mount ::API::Wikis @@ -318,7 +320,6 @@ module API mount ::API::Labels mount ::API::Notes mount ::API::NotificationSettings - mount ::API::PagesDomains mount ::API::ProjectEvents mount ::API::ProjectMilestones mount ::API::ProtectedTags @@ -333,7 +334,6 @@ module API mount ::API::Todos mount ::API::UsageData mount ::API::UsageDataNonSqlMetrics - mount ::API::Users mount ::API::Ml::Mlflow end diff --git a/lib/api/appearance.rb b/lib/api/appearance.rb index 2cef1b27504..99278bdf8b0 100644 --- a/lib/api/appearance.rb +++ b/lib/api/appearance.rb @@ -26,10 +26,11 @@ module API end params do optional :title, type: String, desc: 'Instance title on the sign in / sign up page' - optional :short_title, type: String, desc: 'Short title for Progressive Web App' + optional :pwa_short_name, type: String, desc: 'Optional, short name for Progressive Web App' optional :description, type: String, desc: 'Markdown text shown on the sign in / sign up page' # TODO: remove rubocop disable - https://gitlab.com/gitlab-org/gitlab/issues/14960 optional :logo, type: File, desc: 'Instance image used on the sign in / sign up page' # rubocop:disable Scalability/FileUploads + optional :pwa_icon, type: File, desc: 'Icon used for Progressive Web App' # rubocop:disable Scalability/FileUploads optional :header_logo, type: File, desc: 'Instance image used for the main navigation bar' # rubocop:disable Scalability/FileUploads optional :favicon, type: File, desc: 'Instance favicon in .ico/.png format' # rubocop:disable Scalability/FileUploads optional :new_project_guidelines, type: String, desc: 'Markdown text shown on the new project page' diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 845e42c2ed8..5ae1a80a7fd 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -24,6 +24,7 @@ module API helpers do params :filter_params do optional :search, type: String, desc: 'Return list of branches matching the search criteria' + optional :regex, type: String, desc: 'Return list of branches matching the regex' optional :sort, type: String, desc: 'Return list of branches sorted by the given field', values: %w[name_asc updated_asc updated_desc] end end diff --git a/lib/api/bulk_imports.rb b/lib/api/bulk_imports.rb index a28db321348..6c07b15329e 100644 --- a/lib/api/bulk_imports.rb +++ b/lib/api/bulk_imports.rb @@ -33,7 +33,7 @@ module API end before do - not_found! unless ::BulkImports::Features.enabled? + not_found! unless Gitlab::CurrentSettings.bulk_import_enabled? authenticate! end @@ -61,12 +61,30 @@ module API type: String, desc: 'Source entity type (only `group_entity` is supported)', values: %w[group_entity] - requires :source_full_path, type: String, desc: 'Source full path of the entity to import' - requires :destination_namespace, type: String, desc: 'Destination namespace for the entity' - optional :destination_slug, type: String, desc: 'Destination slug for the entity' + requires :source_full_path, + type: String, + desc: 'Relative path of the source entity to import', + source_full_path: true, + documentation: { example: "'source/full/path' not 'https://example.com/source/full/path'" } + requires :destination_namespace, + type: String, + desc: 'Destination namespace for the entity', + destination_namespace_path: true, + documentation: { example: "'destination_namespace' or 'destination/namespace'" } + optional :destination_slug, + type: String, + desc: 'Destination slug for the entity', + destination_slug_path: true, + documentation: { example: "'destination_slug' not 'destination/slug'" } optional :destination_name, type: String, - desc: 'Deprecated: Use :destination_slug instead. Destination slug for the entity' + desc: 'Deprecated: Use :destination_slug instead. Destination slug for the entity', + destination_slug_path: true, + documentation: { example: "'destination_slug' not 'destination/slug'" } + optional :migrate_projects, + type: Boolean, + default: true, + desc: 'Indicates group migration should include nested projects' mutually_exclusive :destination_slug, :destination_name at_least_one_of :destination_slug, :destination_name diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb index bb57a717f7c..ed1c7dfbfa2 100644 --- a/lib/api/ci/jobs.rb +++ b/lib/api/ci/jobs.rb @@ -53,11 +53,15 @@ module API authorize_read_builds! - builds = user_project.builds.order('id DESC') + builds = user_project.builds.order(id: :desc) builds = filter_builds(builds, params[:scope]) builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, :tags, pipeline: :project) - present paginate(builds, without_count: true), with: Entities::Ci::Job + if Feature.enabled?(:jobs_api_keyset_pagination, user_project) + present paginate_with_strategies(builds, paginator_params: { without_count: true }), with: Entities::Ci::Job + else + present paginate(builds, without_count: true), with: Entities::Ci::Job + end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index b073eb49bf1..6b4394114df 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -312,6 +312,7 @@ module API optional :artifact_format, type: String, desc: %q(The format of artifact), default: 'zip', values: ::Ci::JobArtifact.file_formats.keys optional :metadata, type: ::API::Validations::Types::WorkhorseFile, desc: %(The artifact metadata to store (generated by Multipart middleware)), documentation: { type: 'file' } + optional :accessibility, type: String, desc: %q(Specify accessibility level of artifact private/public) end post '/:id/artifacts', feature_category: :build_artifacts, urgency: :low do not_allowed! unless Gitlab.config.artifacts.enabled diff --git a/lib/api/concerns/packages/debian_distribution_endpoints.rb b/lib/api/concerns/packages/debian_distribution_endpoints.rb index 76b996f2301..6fe3f432edb 100644 --- a/lib/api/concerns/packages/debian_distribution_endpoints.rb +++ b/lib/api/concerns/packages/debian_distribution_endpoints.rb @@ -80,10 +80,10 @@ module API use :optional_distribution_params end post '/' do - authorize_create_package!(project_or_group) + authorize_create_package!(project_or_group(:read_project)) distribution_params = declared_params(include_missing: false) - result = ::Packages::Debian::CreateDistributionService.new(project_or_group, current_user, distribution_params).execute + result = ::Packages::Debian::CreateDistributionService.new(project_or_group(:read_project), current_user, distribution_params).execute created_distribution = result.payload[:distribution] if result.success? @@ -183,7 +183,7 @@ module API use :optional_distribution_params end put '/:codename' do - authorize_create_package!(project_or_group) + authorize_create_package!(project_or_group(:read_project)) distribution_params = declared_params(include_missing: false).except(:codename) result = ::Packages::Debian::UpdateDistributionService.new(distribution, distribution_params).execute @@ -214,7 +214,7 @@ module API use :optional_distribution_params end delete '/:codename' do - authorize_destroy_package!(project_or_group) + authorize_destroy_package!(project_or_group(:read_project)) accepted! if distribution.destroy diff --git a/lib/api/concerns/packages/debian_package_endpoints.rb b/lib/api/concerns/packages/debian_package_endpoints.rb index 842250d351b..181759a7f38 100644 --- a/lib/api/concerns/packages/debian_package_endpoints.rb +++ b/lib/api/concerns/packages/debian_package_endpoints.rb @@ -35,10 +35,10 @@ module API ::Packages::Debian::DistributionsFinder.new(container, codename_or_suite: params[:distribution]).execute.last! end - def present_distribution_package_file! + def present_distribution_package_file!(project) not_found! unless params[:package_name].start_with?(params[:letter]) - package_file = distribution_from!(user_project).package_files.with_file_name(params[:file_name]).last! + package_file = distribution_from!(project).package_files.with_file_name(params[:file_name]).last! present_package_file!(package_file) end diff --git a/lib/api/concerns/packages/nuget_endpoints.rb b/lib/api/concerns/packages/nuget_endpoints.rb index 31ecb529c3c..5f32f0544f4 100644 --- a/lib/api/concerns/packages/nuget_endpoints.rb +++ b/lib/api/concerns/packages/nuget_endpoints.rb @@ -64,7 +64,7 @@ module API tags %w[nuget_packages] end get 'index', format: :json, urgency: :default do - authorize_read_package!(project_or_group) + authorize_packages_access!(project_or_group, required_permission) track_package_event('cli_metadata', :nuget, **snowplow_gitlab_standard_context.merge(category: 'API::NugetPackages')) @@ -78,7 +78,7 @@ module API end namespace '/metadata/*package_name' do after_validation do - authorize_read_package!(project_or_group) + authorize_packages_access!(project_or_group, required_permission) end desc 'The NuGet Metadata Service - Package name level' do @@ -124,7 +124,7 @@ module API end namespace '/query' do after_validation do - authorize_read_package!(project_or_group) + authorize_packages_access!(project_or_group, required_permission) end desc 'The NuGet Search Service' do diff --git a/lib/api/debian_group_packages.rb b/lib/api/debian_group_packages.rb index 105a0955912..483d0dd9c90 100644 --- a/lib/api/debian_group_packages.rb +++ b/lib/api/debian_group_packages.rb @@ -12,10 +12,6 @@ module API resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do helpers do - def user_project - @project ||= find_project!(params[:project_id]) - end - def project_or_group user_group end @@ -55,7 +51,7 @@ module API route_setting :authentication, authenticate_non_public: true get 'pool/:distribution/:project_id/:letter/:package_name/:package_version/:file_name', requirements: PACKAGE_FILE_REQUIREMENTS do - present_distribution_package_file! + present_distribution_package_file!(find_project!(params[:project_id])) end end end diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb index 23a542e4183..353f64b8dd1 100644 --- a/lib/api/debian_project_packages.rb +++ b/lib/api/debian_project_packages.rb @@ -21,16 +21,16 @@ module API resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do helpers do def project_or_group - user_project + user_project(action: :read_package) end end after_validation do require_packages_enabled! - not_found! unless ::Feature.enabled?(:debian_packages, user_project) + not_found! unless ::Feature.enabled?(:debian_packages, project_or_group) - authorize_read_package! + authorize_read_package!(project_or_group) end params do @@ -58,7 +58,7 @@ module API route_setting :authentication, authenticate_non_public: true get 'pool/:distribution/:letter/:package_name/:package_version/:file_name', requirements: PACKAGE_FILE_REQUIREMENTS do - present_distribution_package_file! + present_distribution_package_file!(project_or_group) end params do diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index d3a25a076a0..768ffac41ce 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -254,7 +254,7 @@ module API def readable_discussion_notes(noteable, discussion_ids) notes = noteable.notes .with_discussion_ids(discussion_ids) - .inc_relations_for_view + .inc_relations_for_view(noteable) .includes(:noteable) .fresh diff --git a/lib/api/entities/appearance.rb b/lib/api/entities/appearance.rb index 94a39568393..cabdf68c23a 100644 --- a/lib/api/entities/appearance.rb +++ b/lib/api/entities/appearance.rb @@ -4,13 +4,17 @@ module API module Entities class Appearance < Grape::Entity expose :title - expose :short_title + expose :pwa_short_name expose :description expose :logo do |appearance, options| appearance.logo.url end + expose :pwa_icon do |appearance, options| + appearance.pwa_icon.url + end + expose :header_logo do |appearance, options| appearance.header_logo.url end diff --git a/lib/api/entities/application_setting.rb b/lib/api/entities/application_setting.rb index db51d4380d0..8aace9126d6 100644 --- a/lib/api/entities/application_setting.rb +++ b/lib/api/entities/application_setting.rb @@ -43,6 +43,11 @@ module API # This field is deprecated and always returns true expose(:housekeeping_bitmaps_enabled) { |_settings, _options| true } + + # These fields are deprecated and always returns housekeeping_optimize_repository_period value + expose(:housekeeping_full_repack_period) { |settings, _options| settings.housekeeping_optimize_repository_period } + expose(:housekeeping_gc_period) { |settings, _options| settings.housekeeping_optimize_repository_period } + expose(:housekeeping_incremental_repack_period) { |settings, _options| settings.housekeeping_optimize_repository_period } end end end diff --git a/lib/api/entities/basic_project_details.rb b/lib/api/entities/basic_project_details.rb index 2585b2d0b6d..f89e5adca6d 100644 --- a/lib/api/entities/basic_project_details.rb +++ b/lib/api/entities/basic_project_details.rb @@ -15,7 +15,10 @@ module API expose :ssh_url_to_repo, documentation: { type: 'string', example: 'git@gitlab.example.com:gitlab/gitlab.git' } expose :http_url_to_repo, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab.git' } expose :web_url, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab' } - expose :readme_url, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab/blob/master/README.md' } + with_options if: ->(_, _) { user_has_access_to_project_repository? } do + expose :readme_url, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab/blob/master/README.md' } + expose :forks_count, documentation: { type: 'integer', example: 1 } + end expose :license_url, if: :license, documentation: { type: 'string', example: 'https://gitlab.example.com/gitlab/gitlab/blob/master/LICENCE' } do |project| license = project.repository.license_blob @@ -33,7 +36,6 @@ module API project.avatar_url(only_path: false) end - expose :forks_count, documentation: { type: 'integer', example: 1 } expose :star_count, documentation: { type: 'integer', example: 1 } expose :last_activity_at, documentation: { type: 'dateTime', example: '2013-09-30T13:46:02Z' } expose :namespace, using: 'API::Entities::NamespaceBasic' @@ -74,6 +76,10 @@ module API project.topics.pluck(:name).sort # rubocop:disable CodeReuse/ActiveRecord end end + + def user_has_access_to_project_repository? + Ability.allowed?(options[:current_user], :read_code, project) + end end end end diff --git a/lib/api/entities/bulk_imports/entity.rb b/lib/api/entities/bulk_imports/entity.rb index 8f9fbe57935..176d10b2580 100644 --- a/lib/api/entities/bulk_imports/entity.rb +++ b/lib/api/entities/bulk_imports/entity.rb @@ -9,7 +9,11 @@ module API expose :status_name, as: :status, documentation: { type: 'string', example: 'created', values: %w[created started finished timeout failed] } + expose :entity_type, documentation: { type: 'string', values: %w[group project] } expose :source_full_path, documentation: { type: 'string', example: 'source_group' } + expose :full_path, as: :destination_full_path, documentation: { + type: 'string', example: 'some_group/source_project' + } expose :destination_name, documentation: { type: 'string', example: 'destination_slug' } # deprecated expose :destination_slug, documentation: { type: 'string', example: 'destination_slug' } expose :destination_namespace, documentation: { type: 'string', example: 'destination_path' } @@ -19,6 +23,7 @@ module API expose :created_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' } expose :updated_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' } expose :failures, using: EntityFailure, documentation: { is_array: true } + expose :migrate_projects, documentation: { type: 'boolean', example: true } end end end diff --git a/lib/api/entities/ml/mlflow/run_info.rb b/lib/api/entities/ml/mlflow/run_info.rb index d3934545ba4..60e4416e011 100644 --- a/lib/api/entities/ml/mlflow/run_info.rb +++ b/lib/api/entities/ml/mlflow/run_info.rb @@ -10,6 +10,7 @@ module API expose(:experiment_id) { |candidate| candidate.experiment.iid.to_s } expose(:start_time) { |candidate| candidate.start_time || 0 } expose :end_time, expose_nil: false + expose :name, as: :run_name, expose_nil: false expose(:status) { |candidate| candidate.status.to_s.upcase } expose(:artifact_uri) { |candidate, options| "#{options[:packages_url]}#{candidate.artifact_root}" } expose(:lifecycle_stage) { |candidate| 'active' } diff --git a/lib/api/entities/project_integration_basic.rb b/lib/api/entities/project_integration_basic.rb index aa0ad158b83..b7c56d7cca1 100644 --- a/lib/api/entities/project_integration_basic.rb +++ b/lib/api/entities/project_integration_basic.rb @@ -14,6 +14,7 @@ module API expose :commit_events, documentation: { type: 'boolean' } expose :push_events, documentation: { type: 'boolean' } expose :issues_events, documentation: { type: 'boolean' } + expose :incident_events, documentation: { type: 'boolean' } expose :confidential_issues_events, documentation: { type: 'boolean' } expose :merge_requests_events, documentation: { type: 'boolean' } expose :tag_push_events, documentation: { type: 'boolean' } diff --git a/lib/api/entities/remote_mirror.rb b/lib/api/entities/remote_mirror.rb index 9fb5b2697bc..f64ec71bffb 100644 --- a/lib/api/entities/remote_mirror.rb +++ b/lib/api/entities/remote_mirror.rb @@ -16,3 +16,5 @@ module API end end end + +API::Entities::RemoteMirror.prepend_mod diff --git a/lib/api/environments.rb b/lib/api/environments.rb index 01d46ee7bfb..64510a9615a 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -12,6 +12,8 @@ module API feature_category :continuous_delivery urgency :low + MIN_SEARCH_LENGTH = 3 + params do requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project owned by the authenticated user' end @@ -29,7 +31,7 @@ module API params do use :pagination optional :name, type: String, desc: 'Return the environment with this name. Mutually exclusive with search' - optional :search, type: String, desc: 'Return list of environments matching the search criteria. Mutually exclusive with name' + optional :search, type: String, desc: "Return list of environments matching the search criteria. Mutually exclusive with name. Must be at least #{MIN_SEARCH_LENGTH} characters." optional :states, type: String, values: Environment.valid_states.map(&:to_s), @@ -39,6 +41,10 @@ module API get ':id/environments' do authorize! :read_environment, user_project + if Feature.enabled?(:environment_search_api_min_chars, user_project) && params[:search].present? && params[:search].length < MIN_SEARCH_LENGTH + bad_request!("Search query is less than #{MIN_SEARCH_LENGTH} characters") + end + environments = ::Environments::EnvironmentsFinder.new(user_project, current_user, declared_params(include_missing: false)).execute present paginate(environments), with: Entities::Environment, current_user: current_user @@ -182,6 +188,35 @@ module API present environment, with: Entities::Environment, current_user: current_user end + desc 'Stop stale environments' do + detail 'It returns `200` if stale environment check was scheduled successfully' + failure [ + { code: 400, message: 'Bad request' }, + { code: 401, message: 'Unauthorized' } + ] + tags %w[environments] + end + params do + requires :before, + type: DateTime, + desc: 'Stop all environments that were last modified or deployed to before this date.' + end + post ':id/environments/stop_stale' do + authorize! :stop_environment, user_project + + bad_request!('Invalid Date') if params[:before] < 10.years.ago || params[:before] > 1.week.ago + + service_response = ::Environments::StopStaleService.new(user_project, current_user, params.slice(:before)).execute + + if service_response.error? + status 400 + else + status 200 + end + + present message: service_response.message + end + desc 'Get a specific environment' do success Entities::Environment failure [ diff --git a/lib/api/files.rb b/lib/api/files.rb index b02f1a8728b..18638abd184 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -151,19 +151,6 @@ module API present blame_ranges, with: Entities::BlameRange end - desc 'Get raw file metadata from repository' - params do - requires :file_path, type: String, file_path: true, - desc: 'The url encoded path to the file.', documentation: { example: 'lib%2Fclass%2Erb' } - optional :ref, type: String, - desc: 'The name of branch, tag or commit', allow_blank: false, documentation: { example: 'main' } - end - head ":id/repository/files/:file_path/raw", requirements: FILE_ENDPOINT_REQUIREMENTS, urgency: :low do - assign_file_vars! - - set_http_headers(blob_data) - end - desc 'Get raw file contents from the repository' do success File end diff --git a/lib/api/group_debian_distributions.rb b/lib/api/group_debian_distributions.rb index 0364e2e7b56..8b6d4b8c4b2 100644 --- a/lib/api/group_debian_distributions.rb +++ b/lib/api/group_debian_distributions.rb @@ -19,7 +19,7 @@ module API namespace ':id/-' do helpers do - def project_or_group + def project_or_group(_ = nil) user_group end end diff --git a/lib/api/group_export.rb b/lib/api/group_export.rb index eb0a01e0d3d..37dfbfdb925 100644 --- a/lib/api/group_export.rb +++ b/lib/api/group_export.rb @@ -64,67 +64,73 @@ module API end end - desc 'Start relations export' do - detail 'This feature was introduced in GitLab 13.12' - tags %w[group_export] - success code: 202 - failure [ - { code: 401, message: 'Unauthorized' }, - { code: 403, message: 'Forbidden' }, - { code: 404, message: 'Not found' }, - { code: 503, message: 'Service unavailable' } - ] - end - post ':id/export_relations' do - response = ::BulkImports::ExportService.new(portable: user_group, user: current_user).execute + resource do + before do + not_found! unless Gitlab::CurrentSettings.bulk_import_enabled? + end - if response.success? - accepted! - else - render_api_error!(message: 'Group relations export could not be started.') + desc 'Start relations export' do + detail 'This feature was introduced in GitLab 13.12' + tags %w[group_export] + success code: 202 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] end - end + post ':id/export_relations' do + response = ::BulkImports::ExportService.new(portable: user_group, user: current_user).execute - desc 'Download relations export' do - detail 'This feature was introduced in GitLab 13.12' - produces %w[application/octet-stream application/json] - tags %w[group_export] - success code: 200 - failure [ - { code: 401, message: 'Unauthorized' }, - { code: 403, message: 'Forbidden' }, - { code: 404, message: 'Not found' }, - { code: 503, message: 'Service unavailable' } - ] - end - params do - requires :relation, type: String, desc: 'Group relation name' - end - get ':id/export_relations/download' do - export = user_group.bulk_import_exports.find_by_relation(params[:relation]) - file = export&.upload&.export_file + if response.success? + accepted! + else + render_api_error!(message: 'Group relations export could not be started.') + end + end - if file - present_carrierwave_file!(file) - else - render_api_error!('404 Not found', 404) + desc 'Download relations export' do + detail 'This feature was introduced in GitLab 13.12' + produces %w[application/octet-stream application/json] + tags %w[group_export] + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] end - end + params do + requires :relation, type: String, desc: 'Group relation name' + end + get ':id/export_relations/download' do + export = user_group.bulk_import_exports.find_by_relation(params[:relation]) + file = export&.upload&.export_file - desc 'Relations export status' do - detail 'This feature was introduced in GitLab 13.12' - is_array true - tags %w[group_export] - success code: 200, model: Entities::BulkImports::ExportStatus - failure [ - { code: 401, message: 'Unauthorized' }, - { code: 403, message: 'Forbidden' }, - { code: 404, message: 'Not found' }, - { code: 503, message: 'Service unavailable' } - ] - end - get ':id/export_relations/status' do - present user_group.bulk_import_exports, with: Entities::BulkImports::ExportStatus + if file + present_carrierwave_file!(file) + else + render_api_error!('404 Not found', 404) + end + end + + desc 'Relations export status' do + detail 'This feature was introduced in GitLab 13.12' + is_array true + tags %w[group_export] + success code: 200, model: Entities::BulkImports::ExportStatus + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] + end + get ':id/export_relations/status' do + present user_group.bulk_import_exports, with: Entities::BulkImports::ExportStatus + end end end end diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 0b5a471ea12..38430aac455 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -608,6 +608,8 @@ module API if file.file_storage? present_disk_file!(file.path, file.filename) elsif supports_direct_download && file.class.direct_download_enabled? + return redirect(signed_head_url(file)) if head_request_on_aws_file?(file) + redirect(cdn_fronted_url(file)) else header(*Gitlab::Workhorse.send_url(file.url)) @@ -695,8 +697,31 @@ module API unprocessable_entity!('User must be authenticated to use search') end + def validate_search_rate_limit! + return unless Feature.enabled?(:rate_limit_issuable_searches) + + if current_user + check_rate_limit!(:search_rate_limit, scope: [current_user]) + else + check_rate_limit!(:search_rate_limit_unauthenticated, scope: [ip_address]) + end + end + private + def head_request_on_aws_file?(file) + request.head? && file.fog_credentials[:provider] == 'AWS' + end + + def signed_head_url(file) + fog_storage = ::Fog::Storage.new(file.fog_credentials) + fog_dir = fog_storage.directories.new(key: file.fog_directory) + fog_file = fog_dir.files.new(key: file.path) + expire_at = ::Fog::Time.now + file.fog_authenticated_url_expiration + + fog_file.collection.head_url(fog_file.key, expire_at) + end + # rubocop:disable Gitlab/ModuleWithInstanceVariables def initial_current_user return @initial_current_user if defined?(@initial_current_user) diff --git a/lib/api/helpers/award_emoji.rb b/lib/api/helpers/award_emoji.rb index f8417366ea4..f0b3cafc3d2 100644 --- a/lib/api/helpers/award_emoji.rb +++ b/lib/api/helpers/award_emoji.rb @@ -6,7 +6,7 @@ module API def self.awardables [ { type: 'issue', resource: :projects, find_by: :iid, feature_category: :team_planning }, - { type: 'merge_request', resource: :projects, find_by: :iid, feature_category: :code_review }, + { type: 'merge_request', resource: :projects, find_by: :iid, feature_category: :code_review_workflow }, { type: 'snippet', resource: :projects, find_by: :id, feature_category: :source_code_management } ] end diff --git a/lib/api/helpers/discussions_helpers.rb b/lib/api/helpers/discussions_helpers.rb index 182ada54a12..d497bc66015 100644 --- a/lib/api/helpers/discussions_helpers.rb +++ b/lib/api/helpers/discussions_helpers.rb @@ -9,8 +9,8 @@ module API { Issue => :team_planning, Snippet => :source_code_management, - MergeRequest => :code_review, - Commit => :code_review + MergeRequest => :code_review_workflow, + Commit => :code_review_workflow } end end diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb index 543449c0349..31328facd69 100644 --- a/lib/api/helpers/integrations_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -63,6 +63,12 @@ module API }, { required: false, + name: :incident_channel, + type: String, + desc: 'The name of the channel to receive incident_events notifications' + }, + { + required: false, name: :confidential_issue_channel, type: String, desc: 'The name of the channel to receive confidential_issues_events notifications' @@ -116,6 +122,12 @@ module API }, { required: false, + name: :incident_events, + type: Boolean, + desc: 'Enable notifications for incident_events' + }, + { + required: false, name: :confidential_issues_events, type: Boolean, desc: 'Enable notifications for confidential_issues_events' @@ -161,6 +173,26 @@ module API def self.integrations { + 'apple-app-store' => [ + { + required: true, + name: :app_store_issuer_id, + type: String, + desc: 'The Apple App Store Connect Issuer ID' + }, + { + required: true, + name: :app_store_key_id, + type: String, + desc: 'The Apple App Store Connect Key ID' + }, + { + required: true, + name: :app_store_private_key, + type: String, + desc: 'The Apple App Store Connect Private Key' + } + ], 'asana' => [ { required: true, @@ -871,6 +903,7 @@ module API def self.integration_classes [ + ::Integrations::AppleAppStore, ::Integrations::Asana, ::Integrations::Assembla, ::Integrations::Bamboo, diff --git a/lib/api/helpers/members_helpers.rb b/lib/api/helpers/members_helpers.rb index 6a3cf5c87ae..b0ea4388d9b 100644 --- a/lib/api/helpers/members_helpers.rb +++ b/lib/api/helpers/members_helpers.rb @@ -40,6 +40,9 @@ module API end def source_members(source) + return source.namespace_members if source.is_a?(Project) && + Feature.enabled?(:project_members_index_by_project_namespace, source) + source.members end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb index 302dac4abf7..da499abe475 100644 --- a/lib/api/helpers/notes_helpers.rb +++ b/lib/api/helpers/notes_helpers.rb @@ -8,7 +8,7 @@ module API def self.feature_category_per_noteable_type { Issue => :team_planning, - MergeRequest => :code_review, + MergeRequest => :code_review_workflow, Snippet => :source_code_management } end diff --git a/lib/api/helpers/packages_helpers.rb b/lib/api/helpers/packages_helpers.rb index 8d913268405..1d35c316913 100644 --- a/lib/api/helpers/packages_helpers.rb +++ b/lib/api/helpers/packages_helpers.rb @@ -6,6 +6,7 @@ module API extend ::Gitlab::Utils::Override MAX_PACKAGE_FILE_SIZE = 50.megabytes.freeze + ALLOWED_REQUIRED_PERMISSIONS = %i[read_package read_group].freeze def require_packages_enabled! not_found! unless ::Gitlab.config.packages.enabled @@ -27,9 +28,15 @@ module API authorize!(:destroy_package, subject) end - def authorize_packages_access!(subject = user_project) + def authorize_packages_access!(subject = user_project, required_permission = :read_package) require_packages_enabled! - authorize_read_package!(subject) + return forbidden! unless required_permission.in?(ALLOWED_REQUIRED_PERMISSIONS) + + if required_permission == :read_package + authorize_read_package!(subject) + else + authorize!(required_permission, subject) + end end def authorize_workhorse!(subject: user_project, has_length: true, maximum_size: MAX_PACKAGE_FILE_SIZE) diff --git a/lib/api/helpers/pagination_strategies.rb b/lib/api/helpers/pagination_strategies.rb index 4e244ea589e..5fbc3081ee8 100644 --- a/lib/api/helpers/pagination_strategies.rb +++ b/lib/api/helpers/pagination_strategies.rb @@ -3,13 +3,14 @@ module API module Helpers module PaginationStrategies - def paginate_with_strategies(relation, request_scope = nil) + # paginator_params are only currently supported with offset pagination + def paginate_with_strategies(relation, request_scope = nil, paginator_params: {}) paginator = paginator(relation, request_scope) result = if block_given? - yield(paginator.paginate(relation)) + yield(paginator.paginate(relation, **paginator_params)) else - paginator.paginate(relation) + paginator.paginate(relation, **paginator_params) end result.tap do |records, _| diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 9d370176e62..c5636fa06de 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -205,6 +205,9 @@ module API def filter_attributes_using_license!(attrs) end + def filter_attributes_under_feature_flag!(attrs, project) + end + def validate_git_import_url!(import_url) return if import_url.blank? diff --git a/lib/api/helpers/rate_limiter.rb b/lib/api/helpers/rate_limiter.rb index 03f3cd649b1..be92277c25a 100644 --- a/lib/api/helpers/rate_limiter.rb +++ b/lib/api/helpers/rate_limiter.rb @@ -10,25 +10,14 @@ module API # See app/controllers/concerns/check_rate_limit.rb for Rails controllers version module RateLimiter def check_rate_limit!(key, scope:, **options) - return if bypass_header_set? - return unless rate_limiter.throttled?(key, scope: scope, **options) - - rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user) + return unless Gitlab::ApplicationRateLimiter.throttled_request?( + request, current_user, key, scope: scope, **options + ) return yield if block_given? render_api_error!({ error: _('This endpoint has been requested too many times. Try again later.') }, 429) end - - private - - def rate_limiter - ::Gitlab::ApplicationRateLimiter - end - - def bypass_header_set? - ::Gitlab::Throttle.bypass_header.present? && request.get_header(Gitlab::Throttle.bypass_header) == '1' - end end end end diff --git a/lib/api/helpers/remote_mirrors_helpers.rb b/lib/api/helpers/remote_mirrors_helpers.rb new file mode 100644 index 00000000000..efd81a5ac5a --- /dev/null +++ b/lib/api/helpers/remote_mirrors_helpers.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module API + module Helpers + module RemoteMirrorsHelpers + extend ActiveSupport::Concern + extend Grape::API::Helpers + + params :mirror_branches_setting_ce do + optional :only_protected_branches, type: Boolean, desc: 'Determines if only protected branches are mirrored' + end + + params :mirror_branches_setting_ee do + end + + params :mirror_branches_setting do + use :mirror_branches_setting_ce + use :mirror_branches_setting_ee + end + + def verify_mirror_branches_setting(attrs, project); end + end + end +end + +API::Helpers::RemoteMirrorsHelpers.prepend_mod diff --git a/lib/api/helpers/resource_events_helpers.rb b/lib/api/helpers/resource_events_helpers.rb index c47a58e8fce..11cb65056cd 100644 --- a/lib/api/helpers/resource_events_helpers.rb +++ b/lib/api/helpers/resource_events_helpers.rb @@ -7,7 +7,7 @@ module API # This is a method instead of a constant, allowing EE to more easily extend it. { Issue => { feature_category: :team_planning, id_field: 'IID' }, - MergeRequest => { feature_category: :code_review, id_field: 'IID' } + MergeRequest => { feature_category: :code_review_workflow, id_field: 'IID' } } end end diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index d742e3732a8..6330a4458f3 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -8,6 +8,7 @@ module API urgency :low rescue_from Octokit::Unauthorized, with: :provider_unauthorized + rescue_from Gitlab::GithubImport::RateLimitError, with: :too_many_requests helpers do def client @@ -33,6 +34,10 @@ module API def provider_unauthorized error!("Access denied to your #{Gitlab::ImportSources.title(provider.to_s)} account.", 401) end + + def too_many_requests + error!('Too Many Requests', 429) + end end desc 'Import a GitHub project' do @@ -51,7 +56,7 @@ module API requires :personal_access_token, type: String, desc: 'GitHub personal access token' 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' + requires :target_namespace, type: String, allow_blank: false, desc: 'Namespace or group to import repository into' optional :github_hostname, type: String, desc: 'Custom GitHub enterprise hostname' optional :optional_stages, type: Hash, desc: 'Optional stages of import to be performed' end @@ -92,5 +97,32 @@ module API render_api_error!(result[:message], result[:http_status]) end end + + desc 'Import User Gists' do + detail 'This feature was introduced in GitLab 15.8' + success code: 202 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 422, message: 'Unprocessable Entity' }, + { code: 429, message: 'Too Many Requests' } + ] + end + params do + requires :personal_access_token, type: String, desc: 'GitHub personal access token' + end + post 'import/github/gists' do + not_found! if Feature.disabled?(:github_import_gists) + + authorize! :create_snippet + + result = Import::Github::GistsImportService.new(current_user, client, access_params).execute + + if result[:status] == :success + status 202 + else + status result[:http_status] + { errors: result[:message] } + end + end end end diff --git a/lib/api/internal/base.rb b/lib/api/internal/base.rb index dbd5c5f9db1..3f6e052f7b6 100644 --- a/lib/api/internal/base.rb +++ b/lib/api/internal/base.rb @@ -44,16 +44,12 @@ module API # This is a separate method so that EE can alter its behaviour more # easily. - if Feature.enabled?(:rate_limit_gitlab_shell) - check_rate_limit!(:gitlab_shell_operation, scope: [params[:action], params[:project], actor.key_or_user]) - end + check_rate_limit!(:gitlab_shell_operation, scope: [params[:action], params[:project], actor.key_or_user]) - if Feature.enabled?(:rate_limit_gitlab_shell_by_ip, actor.user) - rate_limiter = Gitlab::Auth::IpRateLimiter.new(request.ip) + rate_limiter = Gitlab::Auth::IpRateLimiter.new(request.ip) - unless rate_limiter.trusted_ip? - check_rate_limit!(:gitlab_shell_operation, scope: [params[:action], params[:project], rate_limiter.ip]) - end + unless rate_limiter.trusted_ip? + check_rate_limit!(:gitlab_shell_operation, scope: [params[:action], params[:project], rate_limiter.ip]) end # Stores some Git-specific env thread-safely diff --git a/lib/api/invitations.rb b/lib/api/invitations.rb index 6aefdf146cf..872dab26469 100644 --- a/lib/api/invitations.rb +++ b/lib/api/invitations.rb @@ -90,7 +90,7 @@ module API .new(current_user, update_params) .execute(invite) - updated_member = result[:member] + updated_member = result[:members].first if result[:status] == :success present_members updated_member diff --git a/lib/api/issues.rb b/lib/api/issues.rb index b08819e34e3..7b6306938cf 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -116,6 +116,7 @@ module API get '/issues_statistics' do authenticate! unless params[:scope] == 'all' validate_anonymous_search_access! if params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? present issues_statistics, with: Grape::Presenters::Presenter end @@ -134,6 +135,7 @@ module API get do authenticate! unless params[:scope] == 'all' validate_anonymous_search_access! if params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? issues = paginate(find_issues) options = { @@ -173,6 +175,7 @@ module API end get ":id/issues" do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? issues = paginate(find_issues(group_id: user_group.id, include_subgroups: true)) options = { @@ -192,6 +195,7 @@ module API end get ":id/issues_statistics" do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? present issues_statistics(group_id: user_group.id, include_subgroups: true), with: Grape::Presenters::Presenter end @@ -211,6 +215,7 @@ module API end get ":id/issues" do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? issues = paginate(find_issues(project_id: user_project.id)) options = { @@ -230,6 +235,7 @@ module API end get ":id/issues_statistics" do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? present issues_statistics(project_id: user_project.id), with: Grape::Presenters::Presenter end diff --git a/lib/api/members.rb b/lib/api/members.rb index 76f4364106b..32c5227a939 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -151,7 +151,7 @@ module API .new(current_user, declared_params(include_missing: false)) .execute(member) - updated_member = result[:member] + updated_member = result[:members].first if result[:status] == :success present_members updated_member diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb index c7f0f88eacc..e7193035ce0 100644 --- a/lib/api/merge_request_diffs.rb +++ b/lib/api/merge_request_diffs.rb @@ -7,7 +7,7 @@ module API before { authenticate! } - feature_category :code_review + feature_category :code_review_workflow params do requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index a9572cf7ce6..25fbeca01dc 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -13,7 +13,7 @@ module API # These endpoints are defined in `TimeTrackingEndpoints` and is shared by # API::Issues. In order to be able to define the feature category of these # endpoints, we need to define them at the top-level by route. - feature_category :code_review, [ + feature_category :code_review_workflow, [ '/projects/:id/merge_requests/:merge_request_iid/time_estimate', '/projects/:id/merge_requests/:merge_request_iid/reset_time_estimate', '/projects/:id/merge_requests/:merge_request_iid/add_spent_time', @@ -105,20 +105,12 @@ module API options end - def authorize_push_to_merge_request!(merge_request) - forbidden!('Source branch does not exist') unless - merge_request.source_branch_exists? + def authorize_merge_request_rebase!(merge_request) + result = ::MergeRequests::RebaseService + .new(project: merge_request.source_project, current_user: current_user) + .validate(merge_request) - user_access = Gitlab::UserAccess.new( - current_user, - container: merge_request.source_project - ) - - forbidden!('Cannot push to source branch') unless - user_access.can_push_to_branch?(merge_request.source_branch) - - forbidden!('Source branch is protected from force push') unless - merge_request.permits_force_push? + forbidden!(result.message) if result.error? end def recheck_mergeability_of(merge_requests:) @@ -146,9 +138,10 @@ module API use :merge_requests_params use :optional_scope_param end - get feature_category: :code_review, urgency: :low do + get feature_category: :code_review_workflow, urgency: :low do authenticate! unless params[:scope] == 'all' validate_anonymous_search_access! if params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? merge_requests = find_merge_requests present merge_requests, serializer_options_for(merge_requests) @@ -175,8 +168,9 @@ module API default: true, desc: 'Returns merge requests from non archived projects only.' end - get ":id/merge_requests", feature_category: :code_review, urgency: :low do + get ":id/merge_requests", feature_category: :code_review_workflow, urgency: :low do validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? merge_requests = find_merge_requests(group_id: user_group.id, include_subgroups: true) present merge_requests, serializer_options_for(merge_requests).merge(group: user_group) @@ -241,9 +235,10 @@ module API desc: 'Returns the request having the given `iid`.', documentation: { is_array: true } end - get ":id/merge_requests", feature_category: :code_review, urgency: :low do + get ":id/merge_requests", feature_category: :code_review_workflow, urgency: :low do authorize! :read_merge_request, user_project validate_anonymous_search_access! if declared_params[:search].present? + validate_search_rate_limit! if declared_params[:search].present? merge_requests = find_merge_requests(project_id: user_project.id) @@ -286,7 +281,7 @@ module API desc: 'The target project of the merge request defaults to the :id of the project.' use :optional_params end - post ":id/merge_requests", feature_category: :code_review, urgency: :low do + post ":id/merge_requests", feature_category: :code_review_workflow, urgency: :low do Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20770') authorize! :create_merge_request_from, user_project @@ -314,7 +309,7 @@ module API params do requires :merge_request_iid, type: Integer, desc: 'The internal ID of the merge request.' end - delete ":id/merge_requests/:merge_request_iid", feature_category: :code_review, urgency: :low do + delete ":id/merge_requests/:merge_request_iid", feature_category: :code_review_workflow, urgency: :low do merge_request = find_project_merge_request(params[:merge_request_iid]) authorize!(:destroy_merge_request, merge_request) @@ -339,7 +334,7 @@ module API ] tags %w[merge_requests] end - get ':id/merge_requests/:merge_request_iid', feature_category: :code_review, urgency: :low do + get ':id/merge_requests/:merge_request_iid', feature_category: :code_review_workflow, urgency: :low do merge_request = find_merge_request_with_access(params[:merge_request_iid]) present merge_request, @@ -360,7 +355,7 @@ module API ] tags %w[merge_requests] end - get ':id/merge_requests/:merge_request_iid/participants', feature_category: :code_review, urgency: :low do + get ':id/merge_requests/:merge_request_iid/participants', feature_category: :code_review_workflow, urgency: :low do merge_request = find_merge_request_with_access(params[:merge_request_iid]) participants = ::Kaminari.paginate_array(merge_request.visible_participants(current_user)) @@ -376,7 +371,7 @@ module API ] tags %w[merge_requests] end - get ':id/merge_requests/:merge_request_iid/reviewers', feature_category: :code_review, urgency: :low do + get ':id/merge_requests/:merge_request_iid/reviewers', feature_category: :code_review_workflow, urgency: :low do merge_request = find_merge_request_with_access(params[:merge_request_iid]) reviewers = ::Kaminari.paginate_array(merge_request.merge_request_reviewers) @@ -392,7 +387,7 @@ module API ] tags %w[merge_requests] end - get ':id/merge_requests/:merge_request_iid/commits', feature_category: :code_review, urgency: :low do + get ':id/merge_requests/:merge_request_iid/commits', feature_category: :code_review_workflow, urgency: :low do merge_request = find_merge_request_with_access(params[:merge_request_iid]) commits = @@ -410,7 +405,7 @@ module API ] tags %w[merge_requests] end - get ':id/merge_requests/:merge_request_iid/context_commits', feature_category: :code_review, urgency: :high do + get ':id/merge_requests/:merge_request_iid/context_commits', feature_category: :code_review_workflow, urgency: :high do merge_request = find_merge_request_with_access(params[:merge_request_iid]) context_commits = paginate(merge_request.merge_request_context_commits).map(&:to_commit) @@ -434,7 +429,7 @@ module API ] tags %w[merge_requests] end - post ':id/merge_requests/:merge_request_iid/context_commits', feature_category: :code_review do + post ':id/merge_requests/:merge_request_iid/context_commits', feature_category: :code_review_workflow do commit_ids = params[:commits] if commit_ids.size > CONTEXT_COMMITS_POST_LIMIT @@ -471,7 +466,7 @@ module API ] tags %w[merge_requests] end - delete ':id/merge_requests/:merge_request_iid/context_commits', feature_category: :code_review do + delete ':id/merge_requests/:merge_request_iid/context_commits', feature_category: :code_review_workflow do commit_ids = params[:commits] merge_request = find_merge_request_with_access(params[:merge_request_iid]) @@ -495,7 +490,7 @@ module API ] tags %w[merge_requests] end - get ':id/merge_requests/:merge_request_iid/changes', feature_category: :code_review, urgency: :low do + get ':id/merge_requests/:merge_request_iid/changes', feature_category: :code_review_workflow, urgency: :low do merge_request = find_merge_request_with_access(params[:merge_request_iid]) present merge_request, @@ -517,7 +512,7 @@ module API params do use :pagination end - get ':id/merge_requests/:merge_request_iid/diffs', feature_category: :code_review, urgency: :low do + get ':id/merge_requests/:merge_request_iid/diffs', feature_category: :code_review_workflow, urgency: :low do merge_request = find_merge_request_with_access(params[:merge_request_iid]) present paginate(merge_request.merge_request_diff.paginated_diffs(params[:page], params[:per_page])).diffs, with: Entities::Diff @@ -585,7 +580,7 @@ module API use :optional_params at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of) end - put ':id/merge_requests/:merge_request_iid', feature_category: :code_review, urgency: :low do + put ':id/merge_requests/:merge_request_iid', feature_category: :code_review_workflow, urgency: :low do Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20772') merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request) @@ -627,7 +622,7 @@ module API optional :sha, type: String, desc: 'If present, then this SHA must match the HEAD of the source branch, otherwise the merge fails.' optional :squash, type: Grape::API::Boolean, desc: 'If `true`, the commits are squashed into a single commit on merge.' end - put ':id/merge_requests/:merge_request_iid/merge', feature_category: :code_review, urgency: :low do + put ':id/merge_requests/:merge_request_iid/merge', feature_category: :code_review_workflow, urgency: :low do Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/4796') merge_request = find_project_merge_request(params[:merge_request_iid]) @@ -678,7 +673,7 @@ module API ] tags %w[merge_requests] end - get ':id/merge_requests/:merge_request_iid/merge_ref', feature_category: :code_review do + get ':id/merge_requests/:merge_request_iid/merge_ref', feature_category: :code_review_workflow do merge_request = find_project_merge_request(params[:merge_request_iid]) result = ::MergeRequests::MergeabilityCheckService.new(merge_request).execute(recheck: true) @@ -701,7 +696,7 @@ module API ] tags %w[merge_requests] end - post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds', feature_category: :code_review do + post ':id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds', feature_category: :code_review_workflow do merge_request = find_project_merge_request(params[:merge_request_iid]) unauthorized! unless merge_request.can_cancel_auto_merge?(current_user) @@ -721,10 +716,10 @@ module API params do optional :skip_ci, type: Boolean, desc: 'Set to true to skip creating a CI pipeline.' end - put ':id/merge_requests/:merge_request_iid/rebase', feature_category: :code_review, urgency: :low do + put ':id/merge_requests/:merge_request_iid/rebase', feature_category: :code_review_workflow, urgency: :low do merge_request = find_project_merge_request(params[:merge_request_iid]) - authorize_push_to_merge_request!(merge_request) + authorize_merge_request_rebase!(merge_request) merge_request.rebase_async(current_user.id, skip_ci: params[:skip_ci]) @@ -744,7 +739,7 @@ module API params do use :pagination end - get ':id/merge_requests/:merge_request_iid/closes_issues', feature_category: :code_review, urgency: :low do + get ':id/merge_requests/:merge_request_iid/closes_issues', feature_category: :code_review_workflow, urgency: :low do merge_request = find_merge_request_with_access(params[:merge_request_iid]) issues = ::Kaminari.paginate_array(merge_request.visible_closing_issues_for(current_user)) issues = paginate(issues) diff --git a/lib/api/ml/mlflow.rb b/lib/api/ml/mlflow.rb index 54bbe0ee465..e7ed8e2e70c 100644 --- a/lib/api/ml/mlflow.rb +++ b/lib/api/ml/mlflow.rb @@ -166,9 +166,10 @@ module API default: 0 optional :user_id, type: String, desc: 'This will be ignored' optional :tags, type: Array, desc: 'Tags are stored, but not displayed' + optional :run_name, type: String, desc: 'A name for this run' end post 'create', urgency: :low do - present candidate_repository.create!(experiment, params[:start_time], params[:tags]), + present candidate_repository.create!(experiment, params[:start_time], params[:tags], params[:run_name]), with: Entities::Ml::Mlflow::Run, packages_url: packages_url end diff --git a/lib/api/nuget_group_packages.rb b/lib/api/nuget_group_packages.rb index c93b24ee544..2afcb915b06 100644 --- a/lib/api/nuget_group_packages.rb +++ b/lib/api/nuget_group_packages.rb @@ -42,6 +42,10 @@ module API def snowplow_gitlab_standard_context { namespace: find_authorized_group! } end + + def required_permission + :read_group + end end params do diff --git a/lib/api/nuget_project_packages.rb b/lib/api/nuget_project_packages.rb index aa517661791..8e974cb9cbe 100644 --- a/lib/api/nuget_project_packages.rb +++ b/lib/api/nuget_project_packages.rb @@ -90,6 +90,10 @@ module API created! end + + def required_permission + :read_package + end end params do diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb index 967847a8e62..15c1a78839f 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -54,7 +54,7 @@ module API end params do - requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' + requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project owned by the authenticated user' end resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do before do @@ -63,6 +63,8 @@ module API desc 'Get all pages domains' do success Entities::PagesDomain + tags %w[pages_domains] + is_array true end params do use :pagination diff --git a/lib/api/project_debian_distributions.rb b/lib/api/project_debian_distributions.rb index 1e27f5c8856..856b4097b5a 100644 --- a/lib/api/project_debian_distributions.rb +++ b/lib/api/project_debian_distributions.rb @@ -14,13 +14,13 @@ module API after_validation do require_packages_enabled! - not_found! unless ::Feature.enabled?(:debian_packages, user_project) + not_found! unless ::Feature.enabled?(:debian_packages, project_or_group) end namespace ':id' do helpers do - def project_or_group - user_project + def project_or_group(action = :read_package) + user_project(action: action) end end diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index e4e950fb603..19e5ed3f9e0 100644 --- a/lib/api/project_export.rb +++ b/lib/api/project_export.rb @@ -5,109 +5,114 @@ module API feature_category :importers urgency :low - before do - not_found! unless Gitlab::CurrentSettings.project_export_enabled? - authorize_admin_project - end - params do requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project' end resource :projects, requirements: { id: %r{[^/]+} } do - desc 'Get export status' do - detail 'This feature was introduced in GitLab 10.6.' - success code: 200, model: Entities::ProjectExportStatus - failure [ - { code: 401, message: 'Unauthorized' }, - { code: 403, message: 'Forbidden' }, - { code: 404, message: 'Not found' }, - { code: 503, message: 'Service unavailable' } - ] - tags ['project_export'] - end - get ':id/export' do - present user_project, with: Entities::ProjectExportStatus - end + resource do + before do + not_found! unless Gitlab::CurrentSettings.project_export_enabled? - desc 'Download export' do - detail 'This feature was introduced in GitLab 10.6.' - success code: 200 - failure [ - { code: 401, message: 'Unauthorized' }, - { code: 403, message: 'Forbidden' }, - { code: 404, message: 'Not found' }, - { code: 503, message: 'Service unavailable' } - ] - tags ['project_export'] - produces %w[application/octet-stream application/json] - end - get ':id/export/download' do - check_rate_limit! :project_download_export, scope: [current_user, user_project.namespace] + authorize_admin_project + end + + desc 'Get export status' do + detail 'This feature was introduced in GitLab 10.6.' + success code: 200, model: Entities::ProjectExportStatus + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] + tags ['project_export'] + end + get ':id/export' do + present user_project, with: Entities::ProjectExportStatus + end - if user_project.export_file_exists? - if user_project.export_archive_exists? - present_carrierwave_file!(user_project.export_file) + desc 'Download export' do + detail 'This feature was introduced in GitLab 10.6.' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 503, message: 'Service unavailable' } + ] + tags ['project_export'] + produces %w[application/octet-stream application/json] + end + get ':id/export/download' do + check_rate_limit! :project_download_export, scope: [current_user, user_project.namespace] + + if user_project.export_file_exists? + if user_project.export_archive_exists? + present_carrierwave_file!(user_project.export_file) + else + render_api_error!('The project export file is not available yet', 404) + end else - render_api_error!('The project export file is not available yet', 404) + render_api_error!('404 Not found or has expired', 404) end - else - render_api_error!('404 Not found or has expired', 404) end - end - desc 'Start export' do - detail 'This feature was introduced in GitLab 10.6.' - success code: 202 - failure [ - { code: 400, message: 'Bad request' }, - { code: 401, message: 'Unauthorized' }, - { code: 403, message: 'Forbidden' }, - { code: 404, message: 'Not found' }, - { code: 429, message: 'Too many requests' }, - { code: 503, message: 'Service unavailable' } - ] - tags ['project_export'] - end - params do - optional :description, type: String, desc: 'Override the project description' - optional :upload, type: Hash do - optional :url, type: String, desc: 'The URL to upload the project' - optional :http_method, type: String, default: 'PUT', values: %w[PUT POST], - desc: 'HTTP method to upload the exported project' + desc 'Start export' do + detail 'This feature was introduced in GitLab 10.6.' + success code: 202 + failure [ + { code: 400, message: 'Bad request' }, + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' }, + { code: 429, message: 'Too many requests' }, + { code: 503, message: 'Service unavailable' } + ] + tags ['project_export'] end - end - post ':id/export' do - check_rate_limit! :project_export, scope: current_user + params do + optional :description, type: String, desc: 'Override the project description' + optional :upload, type: Hash do + optional :url, type: String, desc: 'The URL to upload the project' + optional :http_method, type: String, default: 'PUT', values: %w[PUT POST], + desc: 'HTTP method to upload the exported project' + end + end + post ':id/export' do + check_rate_limit! :project_export, scope: current_user - user_project.remove_exports + user_project.remove_exports - project_export_params = declared_params(include_missing: false) - after_export_params = project_export_params.delete(:upload) || {} + project_export_params = declared_params(include_missing: false) + after_export_params = project_export_params.delete(:upload) || {} - export_strategy = if after_export_params[:url].present? - params = after_export_params.slice(:url, :http_method).symbolize_keys + export_strategy = if after_export_params[:url].present? + params = after_export_params.slice(:url, :http_method).symbolize_keys - Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy.new(**params) - end + Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy.new(**params) + end - if export_strategy&.invalid? - render_validation_error!(export_strategy) - else - begin - user_project.add_export_job(current_user: current_user, - after_export_strategy: export_strategy, - params: project_export_params) - rescue Project::ExportLimitExceeded => e - render_api_error!(e.message, 400) + if export_strategy&.invalid? + render_validation_error!(export_strategy) + else + begin + user_project.add_export_job(current_user: current_user, + after_export_strategy: export_strategy, + params: project_export_params) + rescue Project::ExportLimitExceeded => e + render_api_error!(e.message, 400) + end end - end - accepted! + accepted! + end end resource do before do - not_found! unless ::Feature.enabled?(:bulk_import) + not_found! unless Gitlab::CurrentSettings.bulk_import_enabled? + + authorize_admin_project end desc 'Start relations export' do diff --git a/lib/api/projects.rb b/lib/api/projects.rb index de39419b70b..5077f02fcc1 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -490,6 +490,7 @@ module API attrs = translate_params_for_compatibility(attrs) attrs = add_import_params(attrs) filter_attributes_using_license!(attrs) + filter_attributes_under_feature_flag!(attrs, user_project) verify_update_project_attrs!(user_project, attrs) user_project.remove_avatar! if attrs.key?(:avatar) && attrs[:avatar].nil? diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb index 0e83d086a6e..b21bcb4a903 100644 --- a/lib/api/release/links.rb +++ b/lib/api/release/links.rb @@ -56,7 +56,7 @@ module API params do requires :name, type: String, desc: 'The name of the link. Link names must be unique in the release' requires :url, type: String, desc: 'The URL of the link. Link URLs must be unique in the release.' - optional :filepath, type: String, desc: 'Optional path for a direct asset link' + optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link', as: :filepath optional :link_type, type: String, values: %w[other runbook image package], @@ -108,7 +108,7 @@ module API params do optional :name, type: String, desc: 'The name of the link' optional :url, type: String, desc: 'The URL of the link' - optional :filepath, type: String, desc: 'Optional path for a direct asset link' + optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link', as: :filepath optional :link_type, type: String, values: %w[other runbook image package], diff --git a/lib/api/releases.rb b/lib/api/releases.rb index e6884e66200..e69dc756551 100644 --- a/lib/api/releases.rb +++ b/lib/api/releases.rb @@ -150,18 +150,19 @@ module API params do requires :tag_name, type: String, desc: 'The Git tag the release is associated with', as: :tag - requires :file_path, + requires :direct_asset_path, type: String, file_path: true, - desc: 'The path to the file to download, as specified when creating the release asset' + desc: 'The path to the file to download, as specified when creating the release asset', + as: :filepath end route_setting :authentication, job_token_allowed: true - get ':id/releases/:tag_name/downloads/*file_path', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do + get ':id/releases/:tag_name/downloads/*direct_asset_path', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do authorize_read_code! not_found! unless release - link = release.links.find_by_filepath!("/#{params[:file_path]}") + link = release.links.find_by_filepath!("/#{params[:filepath]}") not_found! unless link @@ -237,7 +238,7 @@ module API optional :links, type: Array do requires :name, type: String, desc: 'The name of the link. Link names must be unique within the release' requires :url, type: String, desc: 'The URL of the link. Link URLs must be unique within the release' - optional :filepath, type: String, desc: 'Optional path for a direct asset link' + optional :direct_asset_path, type: String, desc: 'Optional path for a direct asset link', as: :filepath optional :link_type, type: String, desc: 'The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`' end end diff --git a/lib/api/remote_mirrors.rb b/lib/api/remote_mirrors.rb index f7ea5a6ad2b..c3c7d9370e0 100644 --- a/lib/api/remote_mirrors.rb +++ b/lib/api/remote_mirrors.rb @@ -3,6 +3,7 @@ module API class RemoteMirrors < ::API::Base include PaginationParams + helpers Helpers::RemoteMirrorsHelpers feature_category :source_code_management @@ -60,14 +61,13 @@ module API params do requires :url, type: String, desc: 'The URL for a remote mirror', documentation: { example: 'https://*****:*****@example.com/gitlab/example.git' } optional :enabled, type: Boolean, desc: 'Determines if the mirror is enabled', documentation: { example: false } - optional :only_protected_branches, type: Boolean, desc: 'Determines if only protected branches are mirrored', - documentation: { example: false } optional :keep_divergent_refs, type: Boolean, desc: 'Determines if divergent refs are kept on the target', documentation: { example: false } + use :mirror_branches_setting end post ':id/remote_mirrors' do create_params = declared_params(include_missing: false) - + verify_mirror_branches_setting(create_params, user_project) new_mirror = user_project.remote_mirrors.create(create_params) if new_mirror.persisted? @@ -89,10 +89,9 @@ module API params do requires :mirror_id, type: String, desc: 'The ID of a remote mirror' optional :enabled, type: Boolean, desc: 'Determines if the mirror is enabled', documentation: { example: true } - optional :only_protected_branches, type: Boolean, desc: 'Determines if only protected branches are mirrored', - documentation: { example: false } optional :keep_divergent_refs, type: Boolean, desc: 'Determines if divergent refs are kept on the target', documentation: { example: false } + use :mirror_branches_setting end put ':id/remote_mirrors/:mirror_id' do mirror = user_project.remote_mirrors.find(params[:mirror_id]) @@ -100,6 +99,7 @@ module API mirror_params = declared_params(include_missing: false) mirror_params[:id] = mirror_params.delete(:mirror_id) + verify_mirror_branches_setting(mirror_params, user_project) update_params = { remote_mirrors_attributes: mirror_params } result = ::Projects::UpdateService diff --git a/lib/api/resource_milestone_events.rb b/lib/api/resource_milestone_events.rb index 5640e88ae6e..3eff3e8ad36 100644 --- a/lib/api/resource_milestone_events.rb +++ b/lib/api/resource_milestone_events.rb @@ -11,7 +11,7 @@ module API { Issue => :team_planning, - MergeRequest => :code_review + MergeRequest => :code_review_workflow }.each do |eventable_type, feature_category| parent_type = eventable_type.parent_class.to_s.underscore eventables_str = eventable_type.to_s.underscore.pluralize diff --git a/lib/api/rubygem_packages.rb b/lib/api/rubygem_packages.rb index af0ceb1acfc..896d8fcc727 100644 --- a/lib/api/rubygem_packages.rb +++ b/lib/api/rubygem_packages.rb @@ -25,13 +25,19 @@ module API .sent_through(:http_token) end + helpers do + def project + user_project(action: :read_package) + end + end + before do require_packages_enabled! authenticate_non_get! end after_validation do - not_found! unless Feature.enabled?(:rubygem_packages, user_project) + not_found! unless Feature.enabled?(:rubygem_packages, project) end params do @@ -85,14 +91,14 @@ module API requires :file_name, type: String, desc: 'Package file name', documentation: { type: 'file' } end get "gems/:file_name", requirements: FILE_NAME_REQUIREMENTS do - authorize_read_package!(user_project) + authorize_read_package!(project) package_files = ::Packages::PackageFile - .for_rubygem_with_file_name(user_project, params[:file_name]) + .for_rubygem_with_file_name(project, params[:file_name]) package_file = package_files.installable.last! - track_package_event('pull_package', :rubygems, project: user_project, namespace: user_project.namespace) + track_package_event('pull_package', :rubygems, project: project, namespace: project.namespace) present_package_file!(package_file) end @@ -109,9 +115,9 @@ module API end post 'gems/authorize' do authorize_workhorse!( - subject: user_project, + subject: project, has_length: false, - maximum_size: user_project.actual_limits.rubygems_max_file_size + maximum_size: project.actual_limits.rubygems_max_file_size ) end @@ -129,16 +135,16 @@ module API requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)', documentation: { type: 'file' } end post 'gems' do - authorize_upload!(user_project) - bad_request!('File is too large') if user_project.actual_limits.exceeded?(:rubygems_max_file_size, params[:file].size) + authorize_upload!(project) + bad_request!('File is too large') if project.actual_limits.exceeded?(:rubygems_max_file_size, params[:file].size) - track_package_event('push_package', :rubygems, user: current_user, project: user_project, namespace: user_project.namespace) + track_package_event('push_package', :rubygems, user: current_user, project: project, namespace: project.namespace) package_file = nil ApplicationRecord.transaction do package = ::Packages::CreateTemporaryPackageService.new( - user_project, current_user, declared_params.merge(build: current_authenticated_job) + project, current_user, declared_params.merge(build: current_authenticated_job) ).execute(:rubygems, name: ::Packages::Rubygems::TEMPORARY_PACKAGE_NAME) file_params = { @@ -159,7 +165,7 @@ module API bad_request!('Package creation failed') end rescue ObjectStorage::RemoteStoreError => e - Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: user_project.id }) + Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: project.id }) forbidden! end @@ -179,13 +185,13 @@ module API optional :gems, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma delimited gem names' end get 'dependencies' do - authorize_read_package! + authorize_read_package!(project) if params[:gems].blank? status :ok else results = params[:gems].map do |gem_name| - service_result = Packages::Rubygems::DependencyResolverService.new(user_project, current_user, gem_name: gem_name).execute + service_result = Packages::Rubygems::DependencyResolverService.new(project, current_user, gem_name: gem_name).execute render_api_error!(service_result.message, service_result.http_status) if service_result.error? service_result.payload diff --git a/lib/api/search.rb b/lib/api/search.rb index cf6a1385783..2204437f2ec 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -59,8 +59,13 @@ module API end def search(additional_params = {}) + search_service = search_service(additional_params) + if search_service.global_search? && !search_service.global_search_enabled_for_scope? + forbidden!('Global Search is disabled for this scope') + end + @search_duration_s = Benchmark.realtime do - @results = search_service(additional_params).search_objects(preload_method) + @results = search_service.search_objects(preload_method) end set_global_search_log_information(additional_params) @@ -68,7 +73,7 @@ module API Gitlab::Metrics::GlobalSearchSlis.record_apdex( elapsed: @search_duration_s, search_type: search_type(additional_params), - search_level: search_service(additional_params).level, + search_level: search_service.level, search_scope: search_scope ) diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 8b47604fe86..06b576a982b 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -85,9 +85,15 @@ module API optional :home_page_url, type: String, desc: 'We will redirect non-logged in users to this page' optional :housekeeping_enabled, type: Boolean, desc: 'Enable automatic repository housekeeping (git repack, git gc)' given housekeeping_enabled: ->(val) { val } do - requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run." - requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run." - requires :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run." + optional :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run." + optional :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run." + optional :housekeeping_incremental_repack_period, type: Integer, desc: "Number of Git pushes after which an incremental 'git repack' is run." + + optional :housekeeping_optimize_repository_period, type: Integer, desc: "Number of Git pushes after which Gitaly is asked to optimize a repository." + + # Requires either all three deprecated attributes (housekeeping_full_repack_period, housekeeping_gc_period, housekeeping_incremental_repack_period) or housekeeping_optimize_repository_period + all_or_none_of :housekeeping_full_repack_period, :housekeeping_gc_period, :housekeeping_incremental_repack_period + exactly_one_of :housekeeping_incremental_repack_period, :housekeeping_optimize_repository_period end optional :html_emails_enabled, type: Boolean, desc: 'By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format.' optional :import_sources, type: Array[String], coerce_with: Validations::Types::CommaSeparatedToArray.coerce, @@ -188,6 +194,7 @@ module API optional :jira_connect_application_key, type: String, desc: "Application ID of the OAuth application that should be used to authenticate with the GitLab.com for Jira Cloud app" optional :jira_connect_proxy_url, type: String, desc: "URL of the GitLab instance that should be used as a proxy for the GitLab.com for Jira Cloud app" optional :bulk_import_enabled, type: Boolean, desc: 'Enable migrating GitLab groups and projects by direct transfer' + optional :allow_runner_registration_token, type: Boolean, desc: 'Allow registering runners using a registration token' Gitlab::SSHPublicKey.supported_types.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/api/subscriptions.rb b/lib/api/subscriptions.rb index cda30dc957f..52e5ab30d06 100644 --- a/lib/api/subscriptions.rb +++ b/lib/api/subscriptions.rb @@ -15,7 +15,7 @@ module API entity: Entities::MergeRequest, source: Project, finder: ->(id) { find_merge_request_with_access(id, :update_merge_request) }, - feature_category: :code_review + feature_category: :code_review_workflow }, { type: 'issues', diff --git a/lib/api/suggestions.rb b/lib/api/suggestions.rb index 6260983087f..eee83d5655b 100644 --- a/lib/api/suggestions.rb +++ b/lib/api/suggestions.rb @@ -4,7 +4,7 @@ module API class Suggestions < ::API::Base before { authenticate! } - feature_category :code_review + feature_category :code_review_workflow resource :suggestions do desc 'Apply suggestion patch in the Merge Request it was created' do diff --git a/lib/api/users.rb b/lib/api/users.rb index d2d45c94291..a9b09596728 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -62,6 +62,7 @@ module API optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for user', documentation: { type: 'file' } optional :theme_id, type: Integer, desc: 'The GitLab theme for the user' optional :color_scheme_id, type: Integer, desc: 'The color scheme for the file viewer' + # TODO: Add `allow_blank: false` in 16.0. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/387005 optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile' optional :note, type: String, desc: 'Admin note for this user' optional :view_diffs_file_by_file, type: Boolean, desc: 'Flag indicating the user sees only one file diff per page' @@ -294,6 +295,12 @@ module API authenticated_as_admin! params = declared_params(include_missing: false) + + # TODO: Remove in 16.0. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/387005 + if params.key?(:private_profile) && params[:private_profile].nil? + params[:private_profile] = Gitlab::CurrentSettings.user_defaults_to_private_profile + end + user = ::Users::AuthorizedCreateService.new(current_user, params).execute if user.persisted? @@ -341,6 +348,12 @@ module API .where.not(id: user.id).exists? user_params = declared_params(include_missing: false) + + # TODO: Remove in 16.0. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/387005 + if user_params.key?(:private_profile) && user_params[:private_profile].nil? + user_params[:private_profile] = Gitlab::CurrentSettings.user_defaults_to_private_profile + end + admin_making_changes_for_another_user = (current_user != user) if user_params[:password].present? @@ -824,7 +837,8 @@ module API elsif user.deactivated? forbidden!('Deactivated users cannot be unblocked by the API') else - user.activate + result = ::Users::UnblockService.new(current_user).execute(user) + result.success? end end # rubocop: enable CodeReuse/ActiveRecord @@ -1020,6 +1034,25 @@ module API end end + helpers do + def set_user_status(include_missing_params:) + forbidden! unless can?(current_user, :update_user_status, current_user) + + if ::Users::SetStatusService.new(current_user, declared_params(include_missing: include_missing_params)).execute + present current_user.status, with: Entities::UserStatus + else + render_validation_error!(current_user.status) + end + end + + params :set_user_status_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" + optional :clear_status_after, type: String, desc: "Automatically clear emoji, message and availability fields after a certain time", values: UserStatus::CLEAR_STATUS_QUICK_OPTIONS.keys + end + end + desc "Get the currently authenticated user's SSH keys" do success Entities::SSHKey end @@ -1299,21 +1332,30 @@ module API desc 'Set the status of the current user' do success Entities::UserStatus + detail 'Any parameters that are not passed will be nullified.' end 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" - optional :clear_status_after, type: String, desc: "Automatically clear emoji, message and availability fields after a certain time", values: UserStatus::CLEAR_STATUS_QUICK_OPTIONS.keys + use :set_user_status_params end put "status", feature_category: :users do - forbidden! unless can?(current_user, :update_user_status, current_user) + set_user_status(include_missing_params: true) + end - if ::Users::SetStatusService.new(current_user, declared_params).execute - present current_user.status, with: Entities::UserStatus - else - render_validation_error!(current_user.status) + desc 'Set the status of the current user' do + success Entities::UserStatus + detail 'Any parameters that are not passed will be ignored.' + end + params do + use :set_user_status_params + end + patch "status", feature_category: :users do + if declared_params(include_missing: false).empty? + status :ok + + break end + + set_user_status(include_missing_params: false) end desc 'get the status of the current user' do diff --git a/lib/api/validations/validators/bulk_imports.rb b/lib/api/validations/validators/bulk_imports.rb new file mode 100644 index 00000000000..8d49607f64c --- /dev/null +++ b/lib/api/validations/validators/bulk_imports.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module API + module Validations + module Validators + module BulkImports + class DestinationSlugPath < Grape::Validations::Base + def validate_param!(attr_name, params) + unless params[attr_name] =~ Gitlab::Regex.group_path_regex # rubocop: disable Style/GuardClause + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: "cannot start with a dash or forward slash, or end with a period or forward slash. " \ + "It can only contain alphanumeric characters, periods, underscores, and dashes. " \ + "E.g. 'destination_namespace' not 'destination/namespace'" + ) + end + end + end + + class DestinationNamespacePath < Grape::Validations::Base + def validate_param!(attr_name, params) + return if params[attr_name].blank? + + unless params[attr_name] =~ Gitlab::Regex.bulk_import_namespace_path_regex # rubocop: disable Style/GuardClause + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: "cannot start with a dash or forward slash, or end with a period or forward slash. " \ + "It can only contain alphanumeric characters, periods, underscores, forward slashes " \ + "and dashes. E.g. 'destination_namespace' or 'destination/namespace'" + ) + end + end + end + + class SourceFullPath < Grape::Validations::Base + def validate_param!(attr_name, params) + unless params[attr_name] =~ Gitlab::Regex.bulk_import_namespace_path_regex # rubocop: disable Style/GuardClause + raise Grape::Exceptions::Validation.new( + params: [@scope.full_name(attr_name)], + message: "must be a relative path and not include protocol, sub-domain, or domain information. " \ + "E.g. 'source/full/path' not 'https://example.com/source/full/path'" \ + ) + end + end + end + end + end + end +end |