diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 09:40:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 09:40:42 +0000 |
commit | ee664acb356f8123f4f6b00b73c1e1cf0866c7fb (patch) | |
tree | f8479f94a28f66654c6a4f6fb99bad6b4e86a40e /lib/api | |
parent | 62f7d5c5b69180e82ae8196b7b429eeffc8e7b4f (diff) | |
download | gitlab-ce-ee664acb356f8123f4f6b00b73c1e1cf0866c7fb.tar.gz |
Add latest changes from gitlab-org/gitlab@15-5-stable-eev15.5.0-rc42
Diffstat (limited to 'lib/api')
70 files changed, 661 insertions, 293 deletions
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb index e6ce62a1c6e..74f6515f07f 100644 --- a/lib/api/access_requests.rb +++ b/lib/api/access_requests.rb @@ -12,7 +12,8 @@ module API %w[group project].each do |source_type| params do - requires :id, type: String, desc: "The #{source_type} ID" + requires :id, type: String, + desc: "The ID or URL-encoded path of the #{source_type} owned by the authenticated user" end resource source_type.pluralize, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do desc "Gets a list of access requests for a #{source_type}." do @@ -54,7 +55,8 @@ module API end params do requires :user_id, type: Integer, desc: 'The user ID of the access requester' - optional :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)' + optional :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, the Developer role)', + default: 30 end # rubocop: disable CodeReuse/ActiveRecord put ':id/access_requests/:user_id/approve' do diff --git a/lib/api/admin/batched_background_migrations.rb b/lib/api/admin/batched_background_migrations.rb index 675f3365bd3..e8cc08a23be 100644 --- a/lib/api/admin/batched_background_migrations.rb +++ b/lib/api/admin/batched_background_migrations.rb @@ -61,6 +61,11 @@ module API end put do Gitlab::Database::SharedModel.using_connection(base_model.connection) do + unless batched_background_migration.paused? + msg = 'You can resume only `paused` batched background migrations.' + render_api_error!(msg, 422) + end + batched_background_migration.execute! present_entity(batched_background_migration) end @@ -81,6 +86,11 @@ module API end put do Gitlab::Database::SharedModel.using_connection(base_model.connection) do + unless batched_background_migration.active? + msg = 'You can pause only `active` batched background migrations.' + render_api_error!(msg, 422) + end + batched_background_migration.pause! present_entity(batched_background_migration) end diff --git a/lib/api/alert_management_alerts.rb b/lib/api/alert_management_alerts.rb index bbb7e7280c9..f03f133f6f7 100644 --- a/lib/api/alert_management_alerts.rb +++ b/lib/api/alert_management_alerts.rb @@ -32,7 +32,8 @@ module API success Entities::MetricImage end params do - requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The image file to be uploaded' + requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The image file to be uploaded', + documentation: { type: 'file' } optional :url, type: String, desc: 'The url to view more metric info' optional :url_text, type: String, desc: 'A description of the image or URL' end diff --git a/lib/api/api.rb b/lib/api/api.rb index 443bf1d649a..933c3f69075 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -3,6 +3,7 @@ module API class API < ::API::Base include APIGuard + include Helpers::OpenApi LOG_FILENAME = Rails.root.join("log", "api_json.log") @@ -165,6 +166,13 @@ module API ::Users::ActivityService.new(@current_user).execute end + # Mount endpoints to include in the OpenAPI V2 documentation here + namespace do + mount ::API::Metadata + + add_open_api_documentation! + end + # Keep in alphabetical order mount ::API::AccessRequests mount ::API::Admin::BatchedBackgroundMigrations @@ -250,7 +258,6 @@ module API mount ::API::MergeRequestApprovals mount ::API::MergeRequestDiffs mount ::API::MergeRequests - mount ::API::Metadata mount ::API::Metrics::Dashboard::Annotations mount ::API::Metrics::UserStarredDashboards mount ::API::Namespaces @@ -263,7 +270,7 @@ module API mount ::API::PackageFiles mount ::API::Pages mount ::API::PagesDomains - mount ::API::PersonalAccessTokens::SelfRevocation + mount ::API::PersonalAccessTokens::SelfInformation mount ::API::PersonalAccessTokens mount ::API::ProjectClusters mount ::API::ProjectContainerRepositories @@ -316,7 +323,6 @@ module API mount ::API::UsageDataQueries mount ::API::UserCounts mount ::API::Users - mount ::API::Version mount ::API::Wikis mount ::API::Ml::Mlflow end diff --git a/lib/api/applications.rb b/lib/api/applications.rb index 70488621f33..4048215160f 100644 --- a/lib/api/applications.rb +++ b/lib/api/applications.rb @@ -41,6 +41,9 @@ module API end desc 'Delete an application' + params do + requires :id, type: Integer, desc: 'The ID of the application (not the application_id)' + end delete ':id' do application = ApplicationsFinder.new(params).execute break not_found!('Application') unless application diff --git a/lib/api/badges.rb b/lib/api/badges.rb index f969eec8431..0a3f247ffd6 100644 --- a/lib/api/badges.rb +++ b/lib/api/badges.rb @@ -31,6 +31,7 @@ module API end params do use :pagination + optional :name, type: String, desc: 'Name for the badge' end get ":id/badges", urgency: :low do source = find_source(source_type, params[:id]) diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 5588818cbaf..7e6b0214c03 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -52,19 +52,13 @@ module API merged_branch_names = repository.merged_branch_names(branches.map(&:name)) - expiry_time = if Feature.enabled?(:increase_branch_cache_expiry, type: :ops) - 60.minutes - else - 10.minutes - end - present_cached( branches, with: Entities::Branch, current_user: current_user, project: user_project, merged_branch_names: merged_branch_names, - expires_in: expiry_time, + expires_in: 60.minutes, cache_context: -> (branch) { [current_user&.cache_key, merged_branch_names.include?(branch.name)] } ) end diff --git a/lib/api/bulk_imports.rb b/lib/api/bulk_imports.rb index 2c6adc0f37b..c54632919be 100644 --- a/lib/api/bulk_imports.rb +++ b/lib/api/bulk_imports.rb @@ -32,11 +32,16 @@ module API end end - before { authenticate! } + before do + not_found! unless ::BulkImports::Features.enabled? + + authenticate! + end resource :bulk_imports do desc 'Start a new GitLab Migration' do detail 'This feature was introduced in GitLab 14.2.' + success Entities::BulkImport end params do requires :configuration, type: Hash, desc: 'The source GitLab instance configuration' do @@ -83,6 +88,7 @@ module API desc 'List all GitLab Migrations' do detail 'This feature was introduced in GitLab 14.1.' + success Entities::BulkImport end params do use :pagination @@ -97,6 +103,7 @@ module API desc "List all GitLab Migrations' entities" do detail 'This feature was introduced in GitLab 14.1.' + success Entities::BulkImports::Entity end params do use :pagination @@ -116,6 +123,7 @@ module API desc 'Get GitLab Migration details' do detail 'This feature was introduced in GitLab 14.1.' + success Entities::BulkImport end params do requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration" @@ -126,6 +134,7 @@ module API desc "List GitLab Migration entities" do detail 'This feature was introduced in GitLab 14.1.' + success Entities::BulkImports::Entity end params do requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration" @@ -139,6 +148,7 @@ module API desc 'Get GitLab Migration entity details' do detail 'This feature was introduced in GitLab 14.1.' + success Entities::BulkImports::Entity end params do requires :import_id, type: Integer, desc: "The ID of user's GitLab Migration" diff --git a/lib/api/ci/job_artifacts.rb b/lib/api/ci/job_artifacts.rb index b3a0a9ef54a..37c7cc73c46 100644 --- a/lib/api/ci/job_artifacts.rb +++ b/lib/api/ci/job_artifacts.rb @@ -38,7 +38,7 @@ module API latest_build = user_project.latest_successful_build_for_ref!(params[:job], params[:ref_name]) authorize_read_job_artifacts!(latest_build) - present_artifacts_file!(latest_build.artifacts_file) + present_artifacts_file!(latest_build.artifacts_file, project: latest_build.project) end desc 'Download a specific file from artifacts archive from a ref' do @@ -80,7 +80,7 @@ module API build = find_build!(params[:job_id]) authorize_read_job_artifacts!(build) - present_artifacts_file!(build.artifacts_file) + present_artifacts_file!(build.artifacts_file, project: build.project) end desc 'Download a specific file from artifacts archive' do diff --git a/lib/api/ci/resource_groups.rb b/lib/api/ci/resource_groups.rb index e3fd887475a..ea6d3cc8fd4 100644 --- a/lib/api/ci/resource_groups.rb +++ b/lib/api/ci/resource_groups.rb @@ -38,6 +38,25 @@ module API present resource_group, with: Entities::Ci::ResourceGroup end + desc 'List upcoming jobs of a resource group' do + success Entities::Ci::JobBasic + end + params do + requires :key, type: String, desc: 'The key of the resource group' + + use :pagination + end + get ':id/resource_groups/:key/upcoming_jobs' do + authorize! :read_resource_group, resource_group + authorize! :read_build, user_project + + upcoming_processables = resource_group + .upcoming_processables + .preload(:user, pipeline: :project) # rubocop:disable CodeReuse/ActiveRecord + + present paginate(upcoming_processables), with: Entities::Ci::JobBasic + end + desc 'Edit a resource group' do success Entities::Ci::ResourceGroup end diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 9e4a700d0f3..2d2dcc544f9 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -332,7 +332,7 @@ module API authenticate_job!(require_running: false) end - present_artifacts_file!(current_job.artifacts_file, supports_direct_download: params[:direct_download]) + present_artifacts_file!(current_job.artifacts_file, project: current_job.project, supports_direct_download: params[:direct_download]) end end end diff --git a/lib/api/ci/secure_files.rb b/lib/api/ci/secure_files.rb index c1f47dd67ce..68431df203b 100644 --- a/lib/api/ci/secure_files.rb +++ b/lib/api/ci/secure_files.rb @@ -74,6 +74,10 @@ module API file_too_large! unless secure_file.file.size < ::Ci::SecureFile::FILE_SIZE_LIMIT.to_i if secure_file.save + if Feature.enabled?(:secure_files_metadata_parsers, user_project) + ::Ci::ParseSecureFileMetadataWorker.perform_async(secure_file.id) # rubocop:disable CodeReuse/Worker + end + present secure_file, with: Entities::Ci::SecureFile else render_validation_error!(secure_file) diff --git a/lib/api/ci/variables.rb b/lib/api/ci/variables.rb index f9707960b9d..c9e1d115d03 100644 --- a/lib/api/ci/variables.rb +++ b/lib/api/ci/variables.rb @@ -33,6 +33,9 @@ module API end params do requires :key, type: String, desc: 'The key of the variable' + optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production' do + optional :environment_scope, type: String, desc: 'The environment scope of the variable' + end end # rubocop: disable CodeReuse/ActiveRecord get ':id/variables/:key', urgency: :low do @@ -78,7 +81,9 @@ module API optional :masked, type: Boolean, desc: 'Whether the variable is masked' optional :variable_type, type: String, values: ::Ci::Variable.variable_types.keys, desc: 'The type of variable, must be one of env_var or file' optional :environment_scope, type: String, desc: 'The environment_scope of the variable' - optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production' + optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production' do + optional :environment_scope, type: String, desc: 'The environment scope of the variable' + end end # rubocop: disable CodeReuse/ActiveRecord put ':id/variables/:key' do @@ -104,7 +109,9 @@ module API end params do requires :key, type: String, desc: 'The key of the variable' - optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production' + optional :filter, type: Hash, desc: 'Available filters: [environment_scope]. Example: filter[environment_scope]=production' do + optional :environment_scope, type: String, desc: 'The environment scope of the variable' + end end # rubocop: disable CodeReuse/ActiveRecord delete ':id/variables/:key' do diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb index 5a6d06dcdd9..7d8b58fd7b6 100644 --- a/lib/api/commit_statuses.rb +++ b/lib/api/commit_statuses.rb @@ -52,8 +52,8 @@ module API optional :ref, type: String, desc: 'The ref' optional :target_url, type: String, desc: 'The target URL to associate with this status' optional :description, type: String, desc: 'A short description of the status' - optional :name, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"' - optional :context, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"' + optional :name, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"', documentation: { default: 'default' } + optional :context, type: String, desc: 'A string label to differentiate this status from the status of other systems. Default: "default"', documentation: { default: 'default' } optional :coverage, type: Float, desc: 'The total code coverage' optional :pipeline_id, type: Integer, desc: 'An existing pipeline ID, when multiple pipelines on the same commit SHA have been triggered' end diff --git a/lib/api/concerns/packages/npm_endpoints.rb b/lib/api/concerns/packages/npm_endpoints.rb index d6e006df976..4cc680068b6 100644 --- a/lib/api/concerns/packages/npm_endpoints.rb +++ b/lib/api/concerns/packages/npm_endpoints.rb @@ -116,7 +116,12 @@ module API redirect_request = project_or_nil.blank? || packages.empty? - redirect_registry_request(redirect_request, :npm, package_name: package_name) do + redirect_registry_request( + forward_to_registry: redirect_request, + package_type: :npm, + target: project_or_nil, + package_name: package_name + ) do authorize_read_package!(project) not_found!('Packages') if packages.empty? diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb index 9dedc4390f7..03f0f97b805 100644 --- a/lib/api/debian_project_packages.rb +++ b/lib/api/debian_project_packages.rb @@ -81,11 +81,7 @@ module API package = ::Packages::Debian::FindOrCreateIncomingService.new(authorized_user_project, current_user).execute - package_file = ::Packages::Debian::CreatePackageFileService.new(package, file_params).execute - - if params['file_name'].end_with? '.changes' - ::Packages::Debian::ProcessChangesWorker.perform_async(package_file.id, current_user.id) # rubocop:disable CodeReuse/Worker - end + ::Packages::Debian::CreatePackageFileService.new(package: package, current_user: current_user, params: file_params).execute created! rescue ObjectStorage::RemoteStoreError => e diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index ca13db8701e..c53f4bca5a7 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -161,9 +161,7 @@ module API end end - desc 'Delete deploy key for a project' do - success Key - end + desc 'Delete deploy key for a project' params do requires :key_id, type: Integer, desc: 'The ID of the deploy key' end diff --git a/lib/api/entities/bulk_imports/entity_failure.rb b/lib/api/entities/bulk_imports/entity_failure.rb index a3dbe3280ee..56312745868 100644 --- a/lib/api/entities/bulk_imports/entity_failure.rb +++ b/lib/api/entities/bulk_imports/entity_failure.rb @@ -4,11 +4,16 @@ module API module Entities module BulkImports class EntityFailure < Grape::Entity - expose :pipeline_class - expose :pipeline_step + expose :relation + expose :pipeline_step, as: :step + expose :exception_message do |failure| + ::Projects::ImportErrorFilter.filter_message(failure.exception_message.truncate(72)) + end expose :exception_class expose :correlation_id_value expose :created_at + expose :pipeline_class + expose :pipeline_step end end end diff --git a/lib/api/entities/ci/job_basic.rb b/lib/api/entities/ci/job_basic.rb index 3d9318ec428..fb975475cf5 100644 --- a/lib/api/entities/ci/job_basic.rb +++ b/lib/api/entities/ci/job_basic.rb @@ -21,7 +21,7 @@ module API expose :project do expose :ci_job_token_scope_enabled do |job| - job.project.ci_job_token_scope_enabled? + job.project.ci_outbound_job_token_scope_enabled? end end end diff --git a/lib/api/entities/ci/runner.rb b/lib/api/entities/ci/runner.rb index e29d55771f2..f034eb5c94c 100644 --- a/lib/api/entities/ci/runner.rb +++ b/lib/api/entities/ci/runner.rb @@ -7,7 +7,7 @@ module API expose :id expose :description expose :ip_address - expose :active # TODO Remove in %16.0 in favor of `paused` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/351109 + expose :active # TODO Remove in v5 in favor of `paused` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/375709 expose :paused do |runner| !runner.active end @@ -16,7 +16,7 @@ module API expose :name expose :online?, as: :online # DEPRECATED - # TODO Remove in %16.0 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/344648 + # TODO Remove in v5 in favor of `status` for REST calls, see https://gitlab.com/gitlab-org/gitlab/-/issues/375709 expose :deprecated_rest_status, as: :status end end diff --git a/lib/api/entities/license.rb b/lib/api/entities/license.rb index d7a414344c1..8ecf8a430fe 100644 --- a/lib/api/entities/license.rb +++ b/lib/api/entities/license.rb @@ -2,6 +2,7 @@ module API module Entities + # Serializes a Licensee::License class License < Entities::LicenseBasic expose :popular?, as: :popular expose(:description) { |license| license.meta['description'] } diff --git a/lib/api/entities/license_basic.rb b/lib/api/entities/license_basic.rb index 08af68785a9..0916738d21d 100644 --- a/lib/api/entities/license_basic.rb +++ b/lib/api/entities/license_basic.rb @@ -2,10 +2,16 @@ module API module Entities + # Serializes a Gitlab::Git::DeclaredLicense class LicenseBasic < Grape::Entity expose :key, :name, :nickname expose :url, as: :html_url - expose(:source_url) { |license| license.meta['source'] } + + # This was dropped: + # https://github.com/github/choosealicense.com/commit/325806b42aa3d5b78e84120327ec877bc936dbdd#diff-66df8f1997786f7052d29010f2cbb4c66391d60d24ca624c356acc0ab986f139 + expose :source_url do |_| + nil + end end end end diff --git a/lib/api/entities/merge_request_approvals.rb b/lib/api/entities/merge_request_approvals.rb index 0c464844ae7..6810952b2fc 100644 --- a/lib/api/entities/merge_request_approvals.rb +++ b/lib/api/entities/merge_request_approvals.rb @@ -8,7 +8,7 @@ module API end expose :user_can_approve do |merge_request, options| - merge_request.can_be_approved_by?(options[:current_user]) + merge_request.eligible_for_approval_by?(options[:current_user]) end expose :approved do |merge_request| diff --git a/lib/api/entities/metadata.rb b/lib/api/entities/metadata.rb new file mode 100644 index 00000000000..daa491ec42a --- /dev/null +++ b/lib/api/entities/metadata.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module API + module Entities + class Metadata < Grape::Entity + expose :version + expose :revision + expose :kas do + expose :enabled, documentation: { type: 'boolean' } + expose :externalUrl + expose :version + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/experiment.rb b/lib/api/entities/ml/mlflow/experiment.rb index cfe366feaab..54e0fe63985 100644 --- a/lib/api/entities/ml/mlflow/experiment.rb +++ b/lib/api/entities/ml/mlflow/experiment.rb @@ -5,22 +5,10 @@ module API module Ml module Mlflow class Experiment < Grape::Entity - expose :experiment do - expose :experiment_id - expose :name - expose :lifecycle_stage - expose :artifact_location - end - - private - - def lifecycle_stage - object.deleted_on? ? 'deleted' : 'active' - end - - def experiment_id - object.iid.to_s - end + expose(:experiment_id) { |experiment| experiment.iid.to_s } + expose :name + expose(:lifecycle_stage) { |experiment| experiment.deleted_on? ? 'deleted' : 'active' } + expose(:artifact_location) { |experiment| 'not_implemented' } end end end diff --git a/lib/api/entities/ml/mlflow/get_experiment.rb b/lib/api/entities/ml/mlflow/get_experiment.rb new file mode 100644 index 00000000000..f28d2ce76f6 --- /dev/null +++ b/lib/api/entities/ml/mlflow/get_experiment.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class GetExperiment < Grape::Entity + expose :itself, using: Experiment, as: :experiment + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/list_experiment.rb b/lib/api/entities/ml/mlflow/list_experiment.rb new file mode 100644 index 00000000000..515015bf4b7 --- /dev/null +++ b/lib/api/entities/ml/mlflow/list_experiment.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class ListExperiment < Grape::Entity + expose :experiments, with: Experiment + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/metric.rb b/lib/api/entities/ml/mlflow/metric.rb new file mode 100644 index 00000000000..963aaa5f144 --- /dev/null +++ b/lib/api/entities/ml/mlflow/metric.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class Metric < Grape::Entity + expose :name, as: :key + expose :value + expose :tracked_at, as: :timestamp + expose :step, expose_nil: false + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/run.rb b/lib/api/entities/ml/mlflow/run.rb index c679330206e..a8e1cfe08dd 100644 --- a/lib/api/entities/ml/mlflow/run.rb +++ b/lib/api/entities/ml/mlflow/run.rb @@ -7,7 +7,10 @@ module API class Run < Grape::Entity expose :run do expose(:info) { |candidate| RunInfo.represent(candidate) } - expose(:data) { |candidate| {} } + expose :data do + expose :metrics, using: Metric + expose :params, using: RunParam + end end end end diff --git a/lib/api/entities/ml/mlflow/run_param.rb b/lib/api/entities/ml/mlflow/run_param.rb new file mode 100644 index 00000000000..75fee738f8b --- /dev/null +++ b/lib/api/entities/ml/mlflow/run_param.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class RunParam < Grape::Entity + expose :name, as: :key + expose :value + end + end + end + end +end diff --git a/lib/api/entities/ml/mlflow/update_run.rb b/lib/api/entities/ml/mlflow/update_run.rb index 5acdaab0e33..090d69b8895 100644 --- a/lib/api/entities/ml/mlflow/update_run.rb +++ b/lib/api/entities/ml/mlflow/update_run.rb @@ -10,7 +10,7 @@ module API private def run_info - ::API::Entities::Ml::Mlflow::RunInfo.represent object + RunInfo.represent object end end end diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb index 1739bdd639e..f158695f605 100644 --- a/lib/api/entities/project.rb +++ b/lib/api/entities/project.rb @@ -80,6 +80,7 @@ module API expose(:analytics_access_level) { |project, options| project_feature_string_access_level(project, :analytics) } expose(:container_registry_access_level) { |project, options| project_feature_string_access_level(project, :container_registry) } expose(:security_and_compliance_access_level) { |project, options| project_feature_string_access_level(project, :security_and_compliance) } + expose(:releases_access_level) { |project, options| project_feature_string_access_level(project, :releases) } expose :emails_disabled expose :shared_runners_enabled @@ -103,7 +104,7 @@ module API expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :ci_default_git_depth expose :ci_forward_deployment_enabled - expose :ci_job_token_scope_enabled + expose(:ci_job_token_scope_enabled) { |p, _| p.ci_outbound_job_token_scope_enabled? } expose :ci_separated_caches expose :ci_opt_in_jwt expose :ci_allow_fork_pipelines_to_run_in_parent_project diff --git a/lib/api/entities/user_with_admin.rb b/lib/api/entities/user_with_admin.rb index f9c1a646a4f..53fef7a46e2 100644 --- a/lib/api/entities/user_with_admin.rb +++ b/lib/api/entities/user_with_admin.rb @@ -6,6 +6,7 @@ module API expose :admin?, as: :is_admin expose :note expose :namespace_id + expose :created_by, with: UserBasic end end end diff --git a/lib/api/environments.rb b/lib/api/environments.rb index c4b67f83941..42d5e6a73b3 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -40,7 +40,7 @@ module API params do requires :name, type: String, desc: 'The name of the environment to be created' optional :external_url, type: String, desc: 'URL on which this deployment is viewable' - optional :slug, absence: { message: "is automatically generated and cannot be changed" } + optional :slug, absence: { message: "is automatically generated and cannot be changed" }, documentation: { hidden: true } optional :tier, type: String, values: Environment.tiers.keys, desc: 'The tier of the environment to be created' end post ':id/environments' do @@ -64,7 +64,7 @@ module API # TODO: disallow renaming via the API https://gitlab.com/gitlab-org/gitlab/-/issues/338897 optional :name, type: String, desc: 'DEPRECATED: Renaming environment can lead to errors, this will be removed in 15.0' optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable' - optional :slug, absence: { message: "is automatically generated and cannot be changed" } + optional :slug, absence: { message: "is automatically generated and cannot be changed" }, documentation: { hidden: true } optional :tier, type: String, values: Environment.tiers.keys, desc: 'The tier of the environment to be created' end put ':id/environments/:environment_id' do diff --git a/lib/api/features.rb b/lib/api/features.rb index f89da48acea..9d4e6eee82c 100644 --- a/lib/api/features.rb +++ b/lib/api/features.rb @@ -7,6 +7,7 @@ module API feature_category :feature_flags urgency :low + # TODO: remove these helpers with feature flag set_feature_flag_service helpers do def gate_value(params) case params[:value] @@ -87,35 +88,49 @@ module API mutually_exclusive :key, :project end post ':name' do - validate_feature_flag_name!(params[:name]) unless params[:force] - - targets = gate_targets(params) - value = gate_value(params) - key = gate_key(params) - - case value - when true - if gate_specified?(params) - targets.each { |target| Feature.enable(params[:name], target) } - else - Feature.enable(params[:name]) - end - when false - if gate_specified?(params) - targets.each { |target| Feature.disable(params[:name], target) } + if Feature.enabled?(:set_feature_flag_service) + flag_params = declared_params(include_missing: false) + response = ::Admin::SetFeatureFlagService + .new(feature_flag_name: params[:name], params: flag_params) + .execute + + if response.success? + present response.payload[:feature_flag], + with: Entities::Feature, current_user: current_user else - Feature.disable(params[:name]) + bad_request!(response.message) end else - if key == :percentage_of_actors - Feature.enable_percentage_of_actors(params[:name], value) + validate_feature_flag_name!(params[:name]) unless params[:force] + + targets = gate_targets(params) + value = gate_value(params) + key = gate_key(params) + + case value + when true + if gate_specified?(params) + targets.each { |target| Feature.enable(params[:name], target) } + else + Feature.enable(params[:name]) + end + when false + if gate_specified?(params) + targets.each { |target| Feature.disable(params[:name], target) } + else + Feature.disable(params[:name]) + end else - Feature.enable_percentage_of_time(params[:name], value) + if key == :percentage_of_actors + Feature.enable_percentage_of_actors(params[:name], value) + else + Feature.enable_percentage_of_time(params[:name], value) + end end - end - present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet - with: Entities::Feature, current_user: current_user + present Feature.get(params[:name]), # rubocop:disable Gitlab/AvoidFeatureGet + with: Entities::Feature, current_user: current_user + end rescue Feature::Target::UnknowTargetError => e bad_request!(e.message) end @@ -128,6 +143,7 @@ module API end end + # TODO: remove this helper with feature flag set_feature_flag_service helpers do def validate_feature_flag_name!(name) # no-op diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb index ad5455c5de6..0098b074f05 100644 --- a/lib/api/generic_packages.rb +++ b/lib/api/generic_packages.rb @@ -40,6 +40,8 @@ module API end put 'authorize' do + project = authorized_user_project + authorize_workhorse!(subject: project, maximum_size: project.actual_limits.generic_packages_max_file_size) end @@ -59,6 +61,8 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true, deploy_token_allowed: true put do + project = authorized_user_project + authorize_upload!(project) bad_request!('File is too large') if max_file_size_exceeded? @@ -95,6 +99,8 @@ module API route_setting :authentication, job_token_allowed: true, basic_auth_personal_access_token: true, deploy_token_allowed: true get do + project = authorized_user_project(action: :read_package) + authorize_read_package!(project) package = ::Packages::Generic::PackageFinder.new(project).execute!(params[:package_name], params[:package_version]) @@ -112,12 +118,8 @@ module API include ::API::Helpers::PackagesHelpers include ::API::Helpers::Packages::BasicAuthHelpers - def project - authorized_user_project - end - def max_file_size_exceeded? - project.actual_limits.exceeded?(:generic_packages_max_file_size, params[:file].size) + authorized_user_project.actual_limits.exceeded?(:generic_packages_max_file_size, params[:file].size) end end end diff --git a/lib/api/group_import.rb b/lib/api/group_import.rb index abb8c10efc6..cef9b542c9e 100644 --- a/lib/api/group_import.rb +++ b/lib/api/group_import.rb @@ -54,7 +54,7 @@ module API params do requires :path, type: String, desc: 'Group path' requires :name, type: String, desc: 'Group name' - requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The group export file to be imported' + requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The group export file to be imported', documentation: { type: 'file' } optional :parent_id, type: Integer, desc: "The ID of the parent group that the group will be imported into. Defaults to the current user's namespace." end post 'import' do diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 6b1fc0d4279..ca99e30fbf7 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -123,6 +123,12 @@ module API end def present_groups_with_pagination_strategies(params, groups) + # Prevent Rails from optimizing the count query and inadvertadly creating a poor performing databse query. + # https://gitlab.com/gitlab-org/gitlab/-/issues/368969 + if Feature.enabled?(:present_groups_select_all) + groups = groups.select(groups.arel_table[Arel.star]) + end + return present_groups(params, groups) if current_user.present? options = { diff --git a/lib/api/helm_packages.rb b/lib/api/helm_packages.rb index f90084a7e57..fa2537bcfc4 100644 --- a/lib/api/helm_packages.rb +++ b/lib/api/helm_packages.rb @@ -44,9 +44,10 @@ module API end get ":channel/index.yaml" do - authorize_read_package!(authorized_user_project) + project = authorized_user_project(action: :read_package) + authorize_read_package!(project) - packages = Packages::Helm::PackagesFinder.new(authorized_user_project, params[:channel]).execute + packages = Packages::Helm::PackagesFinder.new(project, params[:channel]).execute env['api.format'] = :yaml present ::Packages::Helm::IndexPresenter.new(params[:id], params[:channel], packages), @@ -61,11 +62,12 @@ module API requires :file_name, type: String, desc: 'Helm package file name' end get ":channel/charts/:file_name.tgz" do - authorize_read_package!(authorized_user_project) + project = authorized_user_project(action: :read_package) + authorize_read_package!(project) - package_file = Packages::Helm::PackageFilesFinder.new(authorized_user_project, params[:channel], file_name: "#{params[:file_name]}.tgz").most_recent! + package_file = Packages::Helm::PackageFilesFinder.new(project, params[:channel], file_name: "#{params[:file_name]}.tgz").most_recent! - track_package_event('pull_package', :helm, project: authorized_user_project, namespace: authorized_user_project.namespace) + track_package_event('pull_package', :helm, project: project, namespace: project.namespace) present_package_file!(package_file) end @@ -89,7 +91,7 @@ module API end params do requires :channel, type: String, desc: 'Helm channel', regexp: Gitlab::Regex.helm_channel_regex - requires :chart, type: ::API::Validations::Types::WorkhorseFile, desc: 'The chart file to be published (generated by Multipart middleware)' + requires :chart, type: ::API::Validations::Types::WorkhorseFile, desc: 'The chart file to be published (generated by Multipart middleware)', documentation: { type: 'file' } end post "api/:channel/charts" do authorize_upload!(authorized_user_project) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index e29d76a5950..0eb4fbb196c 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -18,6 +18,7 @@ module API API_TOKEN_ENV = 'gitlab.api.token' API_EXCEPTION_ENV = 'gitlab.api.exception' API_RESPONSE_STATUS_CODE = 'gitlab.api.response_status_code' + INTEGER_ID_REGEX = /^-?\d+$/.freeze def declared_params(options = {}) options = { include_parent_namespaces: false }.merge(options) @@ -139,7 +140,7 @@ module API projects = Project.without_deleted.not_hidden - if id.is_a?(Integer) || id =~ /^\d+$/ + if id.is_a?(Integer) || id =~ INTEGER_ID_REGEX projects.find_by(id: id) elsif id.include?("/") projects.find_by_full_path(id) @@ -168,7 +169,7 @@ module API # rubocop: disable CodeReuse/ActiveRecord def find_group(id) - if id.to_s =~ /^\d+$/ + if id.to_s =~ INTEGER_ID_REGEX Group.find_by(id: id) else Group.find_by_full_path(id) @@ -203,7 +204,7 @@ module API # rubocop: disable CodeReuse/ActiveRecord def find_namespace(id) - if id.to_s =~ /^\d+$/ + if id.to_s =~ INTEGER_ID_REGEX Namespace.without_project_namespaces.find_by(id: id) else find_namespace_by_path(id) @@ -286,22 +287,11 @@ module API end def authenticate_by_gitlab_shell_token! - if Feature.enabled?(:gitlab_shell_jwt_token) - begin - payload, _ = JSONWebToken::HMACToken.decode(headers[GITLAB_SHELL_API_HEADER], secret_token) - unauthorized! unless payload['iss'] == GITLAB_SHELL_JWT_ISSUER - rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature => ex - Gitlab::ErrorTracking.track_exception(ex) - unauthorized! - end - else - input = params['secret_token'] - input ||= Base64.decode64(headers[GITLAB_SHARED_SECRET_HEADER]) if headers.key?(GITLAB_SHARED_SECRET_HEADER) - - input&.chomp! - - unauthorized! unless Devise.secure_compare(secret_token, input) - end + payload, _ = JSONWebToken::HMACToken.decode(headers[GITLAB_SHELL_API_HEADER], secret_token) + unauthorized! unless payload['iss'] == GITLAB_SHELL_JWT_ISSUER + rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature => ex + Gitlab::ErrorTracking.track_exception(ex) + unauthorized! end def authenticated_with_can_read_all_resources! @@ -602,19 +592,19 @@ module API end end - def present_artifacts_file!(file, **args) + def present_artifacts_file!(file, project:, **args) log_artifacts_filesize(file&.model) - present_carrierwave_file!(file, **args) + present_carrierwave_file!(file, project: project, **args) end - def present_carrierwave_file!(file, supports_direct_download: true) + def present_carrierwave_file!(file, project: nil, supports_direct_download: true) return not_found! unless file&.exists? if file.file_storage? present_disk_file!(file.path, file.filename) elsif supports_direct_download && file.class.direct_download_enabled? - redirect(file.url) + redirect(cdn_fronted_url(file, project)) else header(*Gitlab::Workhorse.send_url(file.url)) status :ok @@ -622,6 +612,16 @@ module API end end + def cdn_fronted_url(file, project) + if file.respond_to?(:cdn_enabled_url) + result = file.cdn_enabled_url(project, ip_address) + Gitlab::ApplicationContext.push(artifact_used_cdn: result.used_cdn) + result.url + else + file.url + end + end + def increment_counter(event_name) Gitlab::UsageDataCounters.count(event_name) rescue StandardError => error @@ -732,13 +732,7 @@ module API end def secret_token - if Feature.enabled?(:gitlab_shell_jwt_token) - strong_memoize(:secret_token) do - File.read(Gitlab.config.gitlab_shell.secret_file) - end - else - Gitlab::Shell.secret_token - end + Gitlab::Shell.secret_token end def authenticate_non_public? diff --git a/lib/api/helpers/groups_helpers.rb b/lib/api/helpers/groups_helpers.rb index e9af50b80be..74c8b582fde 100644 --- a/lib/api/helpers/groups_helpers.rb +++ b/lib/api/helpers/groups_helpers.rb @@ -11,7 +11,7 @@ module API optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the group' - optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for the group' + optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for the group', documentation: { type: 'file' } optional :share_with_group_lock, type: Boolean, desc: 'Prevent sharing a project with another group within this group' optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users in this group to setup Two-factor authentication' optional :two_factor_grace_period, type: Integer, desc: 'Time before Two-factor authentication is enforced' diff --git a/lib/api/helpers/open_api.rb b/lib/api/helpers/open_api.rb new file mode 100644 index 00000000000..11602244b57 --- /dev/null +++ b/lib/api/helpers/open_api.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module API + module Helpers + module OpenApi + extend ActiveSupport::Concern + + class_methods do + def add_open_api_documentation! + return if Rails.env.production? + + open_api_config = YAML.load_file(Rails.root.join('config/open_api.yml'))['metadata'].deep_symbolize_keys + + add_swagger_documentation(open_api_config) + end + end + end + end +end diff --git a/lib/api/helpers/packages/basic_auth_helpers.rb b/lib/api/helpers/packages/basic_auth_helpers.rb index ebedb3b7563..a62bb1d4991 100644 --- a/lib/api/helpers/packages/basic_auth_helpers.rb +++ b/lib/api/helpers/packages/basic_auth_helpers.rb @@ -14,15 +14,27 @@ module API include Constants include Gitlab::Utils::StrongMemoize - def authorized_user_project - @authorized_user_project ||= authorized_project_find! + def authorized_user_project(action: :read_project) + strong_memoize("authorized_user_project_#{action}") do + authorized_project_find!(action: action) + end end - def authorized_project_find! + def authorized_project_find!(action: :read_project) project = find_project(params[:id]) - unless project && can?(current_user, :read_project, project) - return unauthorized_or! { not_found! } + return unauthorized_or! { not_found! } unless project + + case action + when :read_package + unless can?(current_user, :read_package, project&.packages_policy_subject) + # guest users can have :read_project but not :read_package + return forbidden! if can?(current_user, :read_project, project) + + return unauthorized_or! { not_found! } + end + else + return unauthorized_or! { not_found! } unless can?(current_user, action, project) end project diff --git a/lib/api/helpers/packages/dependency_proxy_helpers.rb b/lib/api/helpers/packages/dependency_proxy_helpers.rb index a09499e00d7..dc81e5e1b51 100644 --- a/lib/api/helpers/packages/dependency_proxy_helpers.rb +++ b/lib/api/helpers/packages/dependency_proxy_helpers.rb @@ -16,8 +16,8 @@ module API maven: 'maven_package_requests_forwarding' }.freeze - def redirect_registry_request(forward_to_registry, package_type, options) - if forward_to_registry && redirect_registry_request_available?(package_type) && maven_forwarding_ff_enabled?(package_type, options[:target]) + def redirect_registry_request(forward_to_registry: false, package_type: nil, target: nil, **options) + if forward_to_registry && redirect_registry_request_available?(package_type, target) && maven_forwarding_ff_enabled?(package_type, target) ::Gitlab::Tracking.event(self.options[:for].name, "#{package_type}_request_forward") redirect(registry_url(package_type, options)) else @@ -40,15 +40,19 @@ module API end end - def redirect_registry_request_available?(package_type) + def redirect_registry_request_available?(package_type, target) application_setting_name = APPLICATION_SETTING_NAMES[package_type] raise ArgumentError, "Can't find application setting for package_type #{package_type}" unless application_setting_name - ::Gitlab::CurrentSettings - .current_application_settings - .attributes - .fetch(application_setting_name, false) + if target.present? && Feature.enabled?(:cascade_package_forwarding_settings, target) + target.public_send(application_setting_name) # rubocop:disable GitlabSecurity/PublicSend + else + ::Gitlab::CurrentSettings + .current_application_settings + .attributes + .fetch(application_setting_name, false) + end end private diff --git a/lib/api/helpers/personal_access_tokens_helpers.rb b/lib/api/helpers/personal_access_tokens_helpers.rb index db28daa5396..4fd72d89f4c 100644 --- a/lib/api/helpers/personal_access_tokens_helpers.rb +++ b/lib/api/helpers/personal_access_tokens_helpers.rb @@ -4,11 +4,14 @@ module API module Helpers module PersonalAccessTokensHelpers def finder_params(current_user) - if current_user.can_admin_all_resources? - { user: user(params[:user_id]) } - else - { user: current_user, impersonation: false } - end + user_param = + if current_user.can_admin_all_resources? + { user: user(params[:user_id]) } + else + { user: current_user, impersonation: false } + end + + declared(params, include_missing: false).merge(user_param) end def user(user_id) diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index 7ca3f55b5a2..9839828a5b4 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -36,6 +36,7 @@ module API optional :analytics_access_level, type: String, values: %w(disabled private enabled), desc: 'Analytics access level. One of `disabled`, `private` or `enabled`' optional :container_registry_access_level, type: String, values: %w(disabled private enabled), desc: 'Controls visibility of the container registry. One of `disabled`, `private` or `enabled`. `private` will make the container registry accessible only to project members (reporter role and above). `enabled` will make the container registry accessible to everyone who has access to the project. `disabled` will disable the container registry' optional :security_and_compliance_access_level, type: String, values: %w(disabled private enabled), desc: 'Security and compliance access level. One of `disabled`, `private` or `enabled`' + optional :releases_access_level, type: String, values: %w(disabled private enabled), desc: 'Releases access level. One of `disabled`, `private` or `enabled`' optional :emails_disabled, type: Boolean, desc: 'Disable email notifications' optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis' @@ -58,7 +59,7 @@ module API optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all threads are resolved' optional :tag_list, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Deprecated: Use :topics instead' optional :topics, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The list of topics for a project' - optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for project' + optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for project', documentation: { type: 'file' } optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line' optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests' optional :suggestion_commit_message, type: String, desc: 'The commit message used to apply merge request suggestions' @@ -72,7 +73,7 @@ module API optional :repository_storage, type: String, desc: 'Which storage shard the repository is on. Available only to admins' optional :packages_enabled, type: Boolean, desc: 'Enable project packages feature' optional :squash_option, type: String, values: %w(never always default_on default_off), desc: 'Squash default for project. One of `never`, `always`, `default_on`, or `default_off`.' - optional :mr_default_target_self, Boolean, desc: 'Merge requests of this forked project targets itself by default' + optional :mr_default_target_self, type: Boolean, desc: 'Merge requests of this forked project targets itself by default' end params :optional_project_params_ee do @@ -179,6 +180,7 @@ module API :keep_latest_artifact, :mr_default_target_self, :enforce_auth_checks_on_uploads, + :releases_access_level, # TODO: remove in API v5, replaced by *_access_level :issues_enabled, diff --git a/lib/api/import_github.rb b/lib/api/import_github.rb index 46ca8e4c428..493cc038f46 100644 --- a/lib/api/import_github.rb +++ b/lib/api/import_github.rb @@ -43,6 +43,7 @@ module API 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' + optional :optional_stages, type: Hash, desc: 'Optional stages of import to be performed' end post 'import/github' do result = Import::GithubService.new(client, current_user, params).execute(access_params, provider) @@ -54,5 +55,20 @@ module API { errors: result[:message] } end end + + params do + requires :project_id, type: Integer, desc: 'ID of importing project to be canceled' + end + post 'import/github/cancel' do + project = Project.imported_from(provider.to_s).find(params[:project_id]) + result = Import::Github::CancelProjectImportService.new(project, current_user).execute + + if result[:status] == :success + status :ok + present ProjectSerializer.new.represent(project, serializer: :import) + else + render_api_error!(result[:message], result[:http_status]) + end + end end end diff --git a/lib/api/internal/pages.rb b/lib/api/internal/pages.rb index 20ca7038471..6be2679af14 100644 --- a/lib/api/internal/pages.rb +++ b/lib/api/internal/pages.rb @@ -59,7 +59,8 @@ module API # Gitlab::Pages::CacheControl present_cached virtual_domain, cache_context: nil, - with: Entities::Internal::Pages::VirtualDomain + with: Entities::Internal::Pages::VirtualDomain, + expires_in: ::Gitlab::Pages::CacheControl::EXPIRE else present virtual_domain, with: Entities::Internal::Pages::VirtualDomain end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index b6ad34424a6..b8b4019765d 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -272,17 +272,21 @@ module API begin spam_params = ::Spam::SpamParams.new_from_request(request: request) - issue = ::Issues::CreateService.new(project: user_project, - current_user: current_user, - params: issue_params, - spam_params: spam_params).execute + result = ::Issues::CreateService.new(project: user_project, + current_user: current_user, + params: issue_params, + spam_params: spam_params).execute + + if result.success? + present result[:issue], with: Entities::Issue, current_user: current_user, project: user_project + elsif result[:issue] + issue = result[:issue] - if issue.valid? - present issue, with: Entities::Issue, current_user: current_user, project: user_project - else with_captcha_check_rest_api(spammable: issue) do render_validation_error!(issue) end + else + render_api_error!(result.errors.join(', '), result.http_status || 422) end rescue ::ActiveRecord::RecordNotUnique render_api_error!('Duplicated issue', 409) diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index a3a25ec1696..72313d6a588 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -125,7 +125,13 @@ module API no_package_found = package_file ? false : true - redirect_registry_request(no_package_found, :maven, path: params[:path], file_name: params[:file_name], target: params[:target]) do + redirect_registry_request( + forward_to_registry: no_package_found, + package_type: :maven, + target: params[:target], + path: params[:path], + file_name: params[:file_name] + ) do not_found!('Package') if no_package_found case format diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 1dc0e1f0d22..a0e7d0b10cd 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -218,6 +218,7 @@ module API [ current_user&.cache_key, mr.merge_status, + mr.labels.map(&:cache_key), mr.merge_request_assignees.map(&:cache_key), mr.merge_request_reviewers.map(&:cache_key) ].join(":") @@ -560,7 +561,7 @@ module API put ':id/merge_requests/:merge_request_iid/reset_approvals', feature_category: :code_review, urgency: :low do merge_request = find_project_merge_request(params[:merge_request_iid]) - unauthorized! unless current_user.bot? && merge_request.can_be_approved_by?(current_user) + unauthorized! unless current_user.bot? && merge_request.eligible_for_approval_by?(current_user) merge_request.approvals.delete_all diff --git a/lib/api/metadata.rb b/lib/api/metadata.rb index c4984f0e7f0..3e42ffe336a 100644 --- a/lib/api/metadata.rb +++ b/lib/api/metadata.rb @@ -25,15 +25,76 @@ module API } EOF - desc 'Get the metadata information of the GitLab instance.' do + helpers do + def run_metadata_query + run_graphql!( + query: METADATA_QUERY, + context: { current_user: current_user }, + transform: ->(result) { result.dig('data', 'metadata') } + ) + end + end + + desc 'Retrieve metadata information for this GitLab instance.' do detail 'This feature was introduced in GitLab 15.2.' + success [ + { + code: 200, + model: Entities::Metadata, + message: 'successful operation', + examples: { + successful_response: { + 'value' => { + version: "15.0-pre", + revision: "c401a659d0c", + kas: { + enabled: true, + externalUrl: "grpc://gitlab.example.com:8150", + version: "15.0.0" + } + } + } + } + } + ] + failure [{ code: 401, message: 'unauthorized operation' }] + tags %w[metadata] end get '/metadata' do - run_graphql!( - query: METADATA_QUERY, - context: { current_user: current_user }, - transform: ->(result) { result.dig('data', 'metadata') } - ) + run_metadata_query + end + + # Support the deprecated `/version` route. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/366287 + desc 'Get the version information of the GitLab instance.' do + detail 'This feature was introduced in GitLab 8.13 and deprecated in 15.5. ' \ + 'We recommend you instead use the Metadata API.' + success [ + { + code: 200, + model: Entities::Metadata, + message: 'successful operation', + examples: { + 'Example' => { + 'value' => { + version: "15.0-pre", + revision: "c401a659d0c", + kas: { + enabled: true, + externalUrl: "grpc://gitlab.example.com:8150", + version: "15.0.0" + } + } + } + } + } + ] + failure [{ code: 401, message: 'unauthorized operation' }] + tags %w[metadata] + end + + get '/version' do + run_metadata_query end end end diff --git a/lib/api/ml/mlflow.rb b/lib/api/ml/mlflow.rb index 4f5bd42f8f9..2ffb04ebcbd 100644 --- a/lib/api/ml/mlflow.rb +++ b/lib/api/ml/mlflow.rb @@ -9,20 +9,28 @@ module API include APIGuard # The first part of the url is the namespace, the second part of the URL is what the MLFlow client calls - MLFLOW_API_PREFIX = ':id/ml/mflow/api/2.0/mlflow/' + MLFLOW_API_PREFIX = ':id/ml/mlflow/api/2.0/mlflow/' allow_access_with_scope :api allow_access_with_scope :read_api, if: -> (request) { request.get? || request.head? } + feature_category :mlops + + content_type :json, 'application/json' + default_format :json + before do + # MLFlow Client considers any status code different than 200 an error, even 201 + status 200 + authenticate! + not_found! unless Feature.enabled?(:ml_experiment_tracking, user_project) end - feature_category :mlops - - content_type :json, 'application/json' - default_format :json + rescue_from ActiveRecord::ActiveRecordError do |e| + invalid_parameter!(e.message) + end helpers do def resource_not_found! @@ -32,6 +40,34 @@ module API def resource_already_exists! render_structured_api_error!({ error_code: 'RESOURCE_ALREADY_EXISTS' }, 400) end + + def invalid_parameter!(message = nil) + render_structured_api_error!({ error_code: 'INVALID_PARAMETER_VALUE', message: message }, 400) + end + + def experiment_repository + ::Ml::ExperimentTracking::ExperimentRepository.new(user_project, current_user) + end + + def candidate_repository + ::Ml::ExperimentTracking::CandidateRepository.new(user_project, current_user) + end + + def experiment + @experiment ||= find_experiment!(params[:experiment_id], params[:experiment_name]) + end + + def candidate + @candidate ||= find_candidate!(params[:run_id]) + end + + def find_experiment!(iid, name) + experiment_repository.by_iid_or_name(iid: iid, name: name) || resource_not_found! + end + + def find_candidate!(iid) + candidate_repository.by_iid(iid) || resource_not_found! + end end params do @@ -44,33 +80,35 @@ module API namespace MLFLOW_API_PREFIX do resource :experiments do desc 'Fetch experiment by experiment_id' do - success Entities::Ml::Mlflow::Experiment + success Entities::Ml::Mlflow::GetExperiment detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment' end params do optional :experiment_id, type: String, default: '', desc: 'Experiment ID, in reference to the project' end get 'get', urgency: :low do - experiment = ::Ml::Experiment.by_project_id_and_iid(user_project.id, params[:experiment_id]) - - resource_not_found! unless experiment - - present experiment, with: Entities::Ml::Mlflow::Experiment + present experiment, with: Entities::Ml::Mlflow::GetExperiment end desc 'Fetch experiment by experiment_name' do - success Entities::Ml::Mlflow::Experiment + success Entities::Ml::Mlflow::GetExperiment detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-experiment-by-name' end params do optional :experiment_name, type: String, default: '', desc: 'Experiment name' end get 'get-by-name', urgency: :low do - experiment = ::Ml::Experiment.by_project_id_and_name(user_project, params[:experiment_name]) + present experiment, with: Entities::Ml::Mlflow::GetExperiment + end - resource_not_found! unless experiment + desc 'List experiments' do + success Entities::Ml::Mlflow::ListExperiment + detail 'https://www.mlflow.org/docs/latest/rest-api.html#list-experiments' + end + get 'list', urgency: :low do + response = { experiments: experiment_repository.all } - present experiment, with: Entities::Ml::Mlflow::Experiment + present response, with: Entities::Ml::Mlflow::ListExperiment end desc 'Create experiment' do @@ -83,33 +121,13 @@ module API optional :tags, type: Array, desc: 'This will be ignored' end post 'create', urgency: :low do - resource_already_exists! if ::Ml::Experiment.has_record?(user_project.id, params[:name]) - - experiment = ::Ml::Experiment.create!(name: params[:name], - user: current_user, - project: user_project) - - present experiment, with: Entities::Ml::Mlflow::NewExperiment + present experiment_repository.create!(params[:name]), with: Entities::Ml::Mlflow::NewExperiment + rescue ActiveRecord::RecordInvalid + resource_already_exists! end end resource :runs do - desc 'Gets an MLFlow Run, which maps to GitLab Candidates' do - success Entities::Ml::Mlflow::Run - detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-run' - end - params do - optional :run_id, type: String, desc: 'UUID of the candidate.' - optional :run_uuid, type: String, desc: 'This parameter is ignored' - end - get 'get', urgency: :low do - candidate = ::Ml::Candidate.with_project_id_and_iid(user_project.id, params[:run_id]) - - resource_not_found! unless candidate - - present candidate, with: Entities::Ml::Mlflow::Run - end - desc 'Creates a Run.' do success Entities::Ml::Mlflow::Run detail ['https://www.mlflow.org/docs/1.28.0/rest-api.html#create-run', @@ -125,16 +143,18 @@ module API optional :tags, type: Array, desc: 'This will be ignored' end post 'create', urgency: :low do - experiment = ::Ml::Experiment.by_project_id_and_iid(user_project.id, params[:experiment_id].to_i) - - resource_not_found! unless experiment - - candidate = ::Ml::Candidate.create!( - experiment: experiment, - user: current_user, - start_time: params[:start_time] || 0 - ) + present candidate_repository.create!(experiment, params[:start_time]), with: Entities::Ml::Mlflow::Run + end + desc 'Gets an MLFlow Run, which maps to GitLab Candidates' do + success Entities::Ml::Mlflow::Run + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#get-run' + end + params do + requires :run_id, type: String, desc: 'UUID of the candidate.' + optional :run_uuid, type: String, desc: 'This parameter is ignored' + end + get 'get', urgency: :low do present candidate, with: Entities::Ml::Mlflow::Run end @@ -144,7 +164,7 @@ module API 'MLFlow Runs map to GitLab Candidates'] end params do - optional :run_id, type: String, desc: 'UUID of the candidate.' + requires :run_id, type: String, desc: 'UUID of the candidate.' optional :status, type: String, values: ::Ml::Candidate.statuses.keys.map(&:upcase), desc: "Status of the run. Accepts: " \ @@ -152,16 +172,79 @@ module API optional :end_time, type: Integer, desc: 'Ending time of the run' end post 'update', urgency: :low do - candidate = ::Ml::Candidate.with_project_id_and_iid(user_project.id, params[:run_id]) + candidate_repository.update(candidate, params[:status], params[:end_time]) - resource_not_found! unless candidate + present candidate, with: Entities::Ml::Mlflow::UpdateRun + end - candidate.status = params[:status].downcase if params[:status] - candidate.end_time = params[:end_time] if params[:end_time] + desc 'Logs a metric to a run.' do + summary 'Log a metric for a run. A metric is a key-value pair (string key, float value) with an '\ + 'associated timestamp. Examples include the various metrics that represent ML model accuracy. '\ + 'A metric can be logged multiple times.' + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#log-metric' + end + params do + requires :run_id, type: String, desc: 'UUID of the run.' + requires :key, type: String, desc: 'Name for the metric.' + requires :value, type: Float, desc: 'Value of the metric.' + requires :timestamp, type: Integer, desc: 'Unix timestamp in milliseconds when metric was recorded' + optional :step, type: Integer, desc: 'Step at which the metric was recorded' + end + post 'log-metric', urgency: :low do + candidate_repository.add_metric!( + candidate, + params[:key], + params[:value], + params[:timestamp], + params[:step] + ) + + {} + end - candidate.save if candidate.valid? + desc 'Logs a parameter to a run.' do + summary 'Log a param used for a run. A param is a key-value pair (string key, string value). '\ + 'Examples include hyperparameters used for ML model training and constant dates and values '\ + 'used in an ETL pipeline. A param can be logged only once for a run, duplicate will be .'\ + 'ignored' - present candidate, with: Entities::Ml::Mlflow::UpdateRun + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#log-param' + end + params do + requires :run_id, type: String, desc: 'UUID of the run.' + requires :key, type: String, desc: 'Name for the parameter.' + requires :value, type: String, desc: 'Value for the parameter.' + end + post 'log-parameter', urgency: :low do + bad_request! unless candidate_repository.add_param!(candidate, params[:key], params[:value]) + + {} + end + + desc 'Logs multiple parameters and metrics.' do + summary 'Log a batch of metrics and params for a run. Validation errors will block the entire batch, '\ + 'duplicate errors will be ignored.' + + detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#log-param' + end + params do + requires :run_id, type: String, desc: 'UUID of the run.' + optional :metrics, type: Array, default: [] do + requires :key, type: String, desc: 'Name for the metric.' + requires :value, type: Float, desc: 'Value of the metric.' + requires :timestamp, type: Integer, desc: 'Unix timestamp in milliseconds when metric was recorded' + optional :step, type: Integer, desc: 'Step at which the metric was recorded' + end + optional :params, type: Array, default: [] do + requires :key, type: String, desc: 'Name for the metric.' + requires :value, type: String, desc: 'Value of the metric.' + end + end + post 'log-batch', urgency: :low do + candidate_repository.add_metrics(candidate, params[:metrics]) + candidate_repository.add_params(candidate, params[:params]) + + {} end end end diff --git a/lib/api/notes.rb b/lib/api/notes.rb index 77c479c529a..8ce875cdc03 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -73,7 +73,7 @@ module API params do requires :noteable_id, type: Integer, desc: 'The ID of the noteable' requires :body, type: String, desc: 'The content of a note' - optional :confidential, type: Boolean, desc: '[Deprecated in 15.3] Renamed to internal' + optional :confidential, type: Boolean, desc: '[Deprecated in 15.5] Renamed to internal' optional :internal, type: Boolean, desc: 'Internal note flag, default is false' optional :created_at, type: String, desc: 'The creation date of the note' optional :merge_request_diff_head_sha, type: String, desc: 'The SHA of the head commit' diff --git a/lib/api/pages_domains.rb b/lib/api/pages_domains.rb index 34d3a5150da..9cf61967ba4 100644 --- a/lib/api/pages_domains.rb +++ b/lib/api/pages_domains.rb @@ -106,7 +106,9 @@ module API pages_domain_params = declared(params, include_parent_namespaces: false) - pages_domain = user_project.pages_domains.create(pages_domain_params) + pages_domain = ::PagesDomains::CreateService + .new(user_project, current_user, pages_domain_params) + .execute if pages_domain.persisted? present pages_domain, with: Entities::PagesDomain @@ -136,7 +138,9 @@ module API pages_domain_params.delete(:user_provided_key) end - if pages_domain.update(pages_domain_params) + service = ::PagesDomains::UpdateService.new(user_project, current_user, pages_domain_params) + + if service.execute(pages_domain) present pages_domain, with: Entities::PagesDomain else render_validation_error!(pages_domain) @@ -150,7 +154,9 @@ module API delete ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do authorize! :update_pages, user_project - pages_domain.destroy + ::PagesDomains::DeleteService + .new(user_project, current_user) + .execute(pages_domain) no_content! end diff --git a/lib/api/personal_access_tokens.rb b/lib/api/personal_access_tokens.rb index 1c00569bba2..a2903faa4ad 100644 --- a/lib/api/personal_access_tokens.rb +++ b/lib/api/personal_access_tokens.rb @@ -11,7 +11,15 @@ module API success Entities::PersonalAccessToken end params do - optional :user_id, type: Integer, desc: 'User ID' + optional :user_id, type: Integer, desc: 'Filter PATs by User ID' + optional :revoked, type: Boolean, desc: 'Filter PATs where revoked state matches parameter' + optional :state, type: String, desc: 'Filter PATs which are either active or not', + values: %w[active inactive] + optional :created_before, type: DateTime, desc: 'Filter PATs which were created before given datetime' + optional :created_after, type: DateTime, desc: 'Filter PATs which were created after given datetime' + optional :last_used_before, type: DateTime, desc: 'Filter PATs which were used before given datetime' + optional :last_used_after, type: DateTime, desc: 'Filter PATs which were used after given datetime' + optional :search, type: String, desc: 'Filters PATs by its name' use :pagination end diff --git a/lib/api/personal_access_tokens/self_revocation.rb b/lib/api/personal_access_tokens/self_information.rb index 22e07f4cc7b..89850614f94 100644 --- a/lib/api/personal_access_tokens/self_revocation.rb +++ b/lib/api/personal_access_tokens/self_information.rb @@ -2,21 +2,25 @@ module API class PersonalAccessTokens - class SelfRevocation < ::API::Base + class SelfInformation < ::API::Base include APIGuard feature_category :authentication_and_authorization helpers ::API::Helpers::PersonalAccessTokensHelpers - # As any token regardless of `scope` should be able to revoke itself - # all availabe scopes are allowed for this API class. + # As any token regardless of `scope` should be able to view/revoke itself + # all available scopes are allowed for this API class. # Please be aware of the permissive scope when adding new endpoints to this class. allow_access_with_scope(Gitlab::Auth.all_available_scopes) before { authenticate! } resource :personal_access_tokens do + get 'self' do + present access_token, with: Entities::PersonalAccessToken + end + delete 'self' do revoke_token(access_token) end diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb index d610b5e4f95..29fdfe45566 100644 --- a/lib/api/project_export.rb +++ b/lib/api/project_export.rb @@ -46,7 +46,8 @@ module API 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', desc: 'HTTP method to upload the exported 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 diff --git a/lib/api/project_import.rb b/lib/api/project_import.rb index 7a66044c5b6..0da8c1ecedd 100644 --- a/lib/api/project_import.rb +++ b/lib/api/project_import.rb @@ -55,7 +55,7 @@ module API params do requires :path, type: String, desc: 'The new project path and name' - requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The project export file to be imported' + requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The project export file to be imported', documentation: { type: 'file' } optional :name, type: String, desc: 'The name of the project to be imported. Defaults to the path of the project if not provided.' optional :namespace, type: String, desc: "The ID or name of the namespace that the project will be imported into. Defaults to the current user's namespace." optional :overwrite, type: Boolean, default: false, desc: 'If there is a project in the same namespace and with the same name overwrite it' diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 8c58cc585d8..bb97f4fa7ce 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -375,7 +375,7 @@ module API optional :name, type: String, desc: 'The name that will be assigned to the fork' optional :description, type: String, desc: 'The description that will be assigned to the fork' optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The visibility of the fork' - optional :mr_default_target_self, Boolean, desc: 'Merge requests of this forked project targets itself by default' + optional :mr_default_target_self, type: Boolean, desc: 'Merge requests of this forked project targets itself by default' end post ':id/fork', feature_category: :source_code_management do Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20759') diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb index ae583ca968a..1f27fcce879 100644 --- a/lib/api/pypi_packages.rb +++ b/lib/api/pypi_packages.rb @@ -56,7 +56,12 @@ module API packages = Packages::Pypi::PackagesFinder.new(current_user, group_or_project, { package_name: params[:package_name] }).execute empty_packages = packages.empty? - redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do + redirect_registry_request( + forward_to_registry: empty_packages, + package_type: :pypi, + target: group_or_project, + package_name: params[:package_name] + ) do not_found!('Package') if empty_packages presenter = ::Packages::Pypi::SimplePackageVersionsPresenter.new(packages, group_or_project) diff --git a/lib/api/search.rb b/lib/api/search.rb index 44bb4228786..ff17696ed3e 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -63,7 +63,7 @@ module API @results = search_service(additional_params).search_objects(preload_method) end - set_global_search_log_information + set_global_search_log_information(additional_params) Gitlab::Metrics::GlobalSearchSlis.record_apdex( elapsed: @search_duration_s, @@ -105,7 +105,7 @@ module API # EE, without having to modify this file directly. end - def search_type + def search_type(additional_params = {}) 'basic' end @@ -113,10 +113,10 @@ module API params[:scope] end - def set_global_search_log_information + def set_global_search_log_information(additional_params) Gitlab::Instrumentation::GlobalSearchApi.set_information( - type: search_type, - level: search_service.level, + type: search_type(additional_params), + level: search_service(additional_params).level, scope: search_scope, search_duration_s: @search_duration_s ) diff --git a/lib/api/settings.rb b/lib/api/settings.rb index f393f862f55..8c8b6c0a1ba 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -132,7 +132,7 @@ module API requires :recaptcha_private_key, type: String, desc: 'Generate private key at http://www.google.com/recaptcha' end optional :repository_checks_enabled, type: Boolean, desc: "GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues." - optional :repository_storages_weighted, type: Hash, coerce_with: Validations::Types::HashOfIntegerValues.coerce, desc: 'Storage paths for new projects with a weighted value ranging from 0 to 100' + optional :repository_storages_weighted, type: Hash, coerce_with: Validations::Types::HashOfIntegerValues.coerce, desc: 'Storage paths for new projects with a weighted value ranging from 0 to 100', documentation: { type: 'Object', additional_properties: Integer } optional :require_two_factor_authentication, type: Boolean, desc: 'Require all users to set up Two-factor authentication' given require_two_factor_authentication: ->(val) { val } do requires :two_factor_grace_period, type: Integer, desc: 'Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication' diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index 4e70ebddf94..5f8e6c806cb 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -6,6 +6,7 @@ module API include PaginationParams feature_category :snippets + urgency :low resource :snippets do helpers Helpers::SnippetsHelpers @@ -51,7 +52,7 @@ module API use :pagination end - get 'public', urgency: :low do + get 'public' do authenticate! filter_params = declared_params(include_missing: false).merge(only_personal: true) @@ -192,7 +193,7 @@ module API params do use :raw_file_params end - get ":id/files/:ref/:file_path/raw", urgency: :low, requirements: { file_path: API::NO_SLASH_URL_PART_REGEX } do + get ":id/files/:ref/:file_path/raw", requirements: { file_path: API::NO_SLASH_URL_PART_REGEX } do snippet = snippets.find_by_id(params.delete(:id)) not_found!('Snippet') unless snippet&.repo_exists? diff --git a/lib/api/support/git_access_actor.rb b/lib/api/support/git_access_actor.rb index f450630afdd..16861a146ae 100644 --- a/lib/api/support/git_access_actor.rb +++ b/lib/api/support/git_access_actor.rb @@ -57,3 +57,5 @@ module API end end end + +API::Support::GitAccessActor.prepend_mod_with('API::Support::GitAccessActor') diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 85a299c5673..a80ef514943 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -7,15 +7,18 @@ module API GLOBAL_TEMPLATE_TYPES = { gitignores: { gitlab_version: 8.8, - feature_category: :source_code_management + feature_category: :source_code_management, + file_type: '.gitignore' }, gitlab_ci_ymls: { gitlab_version: 8.9, - feature_category: :pipeline_authoring + feature_category: :pipeline_authoring, + file_type: 'GitLab CI/CD YAML' }, dockerfiles: { gitlab_version: 8.15, - feature_category: :source_code_management + feature_category: :source_code_management, + file_type: 'Dockerfile' } }.freeze @@ -26,7 +29,7 @@ module API end end - desc 'Get the list of the available license template' do + desc 'Get all license templates' do detail 'This feature was introduced in GitLab 8.7.' success ::API::Entities::License end @@ -43,12 +46,14 @@ module API present paginate(::Kaminari.paginate_array(templates)), with: ::API::Entities::License end - desc 'Get the text for a specific license' do + desc 'Get a single license template' do detail 'This feature was introduced in GitLab 8.7.' success ::API::Entities::License end params do - requires :name, type: String, desc: 'The name of the template' + requires :name, type: String, desc: 'The name of the license template' + optional :project, type: String, desc: 'The copyrighted project name' + optional :fullname, type: String, desc: 'The full-name of the copyright holder' end get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ }, feature_category: :source_code_management do template = TemplateFinder.build(:licenses, nil, name: params[:name]).execute @@ -65,8 +70,9 @@ module API GLOBAL_TEMPLATE_TYPES.each do |template_type, properties| gitlab_version = properties[:gitlab_version] + file_type = properties[:file_type] - desc 'Get the list of the available template' do + desc "Get all #{file_type} templates" do detail "This feature was introduced in GitLab #{gitlab_version}." success Entities::TemplatesList end @@ -78,12 +84,12 @@ module API present paginate(templates), with: Entities::TemplatesList end - desc 'Get the text for a specific template present in local filesystem' do + desc "Get a single #{file_type} template" do detail "This feature was introduced in GitLab #{gitlab_version}." success Entities::Template end params do - requires :name, type: String, desc: 'The name of the template' + requires :name, type: String, desc: "The name of the #{file_type} template" end get "templates/#{template_type}/:name", requirements: { name: /[\w\.-]+/ }, feature_category: properties[:feature_category] do finder = TemplateFinder.build(template_type, nil, name: declared(params)[:name]) diff --git a/lib/api/todos.rb b/lib/api/todos.rb index f1779df7cc6..57745ee8802 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -15,17 +15,17 @@ module API }.freeze params do - requires :id, type: String, desc: 'The ID of a project' + requires :id, type: String, 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 ISSUABLE_TYPES.each do |type, finder| type_id_str = "#{type.singularize}_iid".to_sym - desc 'Create a todo on an issuable' do + desc 'Create a to-do item on an issuable' do success Entities::Todo end params do - requires type_id_str, type: Integer, desc: 'The IID of an issuable' + requires type_id_str, type: Integer, desc: 'The internal ID of an issuable' end post ":id/#{type}/:#{type_id_str}/todo" do issuable = instance_exec(params[type_id_str], &finder) @@ -44,12 +44,12 @@ module API resource :todos do helpers do params :todo_filters do - optional :action, String, values: Todo::ACTION_NAMES.values.map(&:to_s) - optional :author_id, Integer - optional :state, String, values: Todo.state_machine.states.map(&:name).map(&:to_s) - optional :type, String, values: TodosFinder.todo_types - optional :project_id, Integer - optional :group_id, Integer + optional :action, type: String, values: Todo::ACTION_NAMES.values.map(&:to_s), desc: 'The action to be filtered' + optional :author_id, type: Integer, desc: 'The ID of an author' + optional :project_id, type: Integer, desc: 'The ID of a project' + optional :group_id, type: Integer, desc: 'The ID of a group' + optional :state, type: String, values: Todo.state_machine.states.map(&:name).map(&:to_s), desc: 'The state of the to-do item' + optional :type, type: String, values: TodosFinder.todo_types.map(&:to_s), desc: 'The type of to-do item' end def find_todos @@ -81,7 +81,7 @@ module API end end - desc 'Get a todo list' do + desc 'Get a list of to-do items' do success Entities::Todo end params do @@ -96,11 +96,11 @@ module API present todos, options end - desc 'Mark a todo as done' do + desc 'Mark a to-do item as done' do success Entities::Todo end params do - requires :id, type: Integer, desc: 'The ID of the todo being marked as done' + requires :id, type: Integer, desc: 'The ID of to-do item' end post ':id/mark_as_done' do todo = current_user.todos.find(params[:id]) @@ -110,7 +110,7 @@ module API present todo, with: Entities::Todo, current_user: current_user end - desc 'Mark all todos as done' + desc 'Mark all to-do items as done' post '/mark_as_done' do todos = find_todos diff --git a/lib/api/users.rb b/lib/api/users.rb index 1d1c633824e..7f44e46f1ca 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -50,11 +50,13 @@ module API optional :provider, type: String, desc: 'The external provider' optional :bio, type: String, desc: 'The biography of the user' optional :location, type: String, desc: 'The location of the user' + optional :pronouns, type: String, desc: 'The pronouns of the user' optional :public_email, type: String, desc: 'The public email of the user' + optional :commit_email, type: String, desc: 'The commit email, _private for private commit email' optional :admin, type: Boolean, desc: 'Flag indicating the user is an administrator' optional :can_create_group, type: Boolean, desc: 'Flag indicating the user can create groups' optional :external, type: Boolean, desc: 'Flag indicating the user is an external user' - optional :avatar, type: ::API::Validations::Types::WorkhorseFile, desc: 'Avatar image for user' + 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' optional :private_profile, type: Boolean, desc: 'Flag indicating the user has a private profile' @@ -187,7 +189,10 @@ module API user = find_user(params[:id]) not_found!('User') unless user - if current_user.follow(user) + followee = current_user.follow(user) + if followee&.errors&.any? + render_api_error!(followee.errors.full_messages.join(', '), 400) + elsif followee&.persisted? present user, with: Entities::UserBasic else not_modified! @@ -885,7 +890,7 @@ module API params do requires :name, type: String, desc: 'The name of the impersonation token' 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' + optional :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'The array of scopes of the impersonation token' end post feature_category: :authentication_and_authorization do impersonation_token = finder.build(declared_params(include_missing: false)) diff --git a/lib/api/version.rb b/lib/api/version.rb deleted file mode 100644 index bdce88ab827..00000000000 --- a/lib/api/version.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -module API - class Version < ::API::Base - helpers ::API::Helpers::GraphqlHelpers - include APIGuard - - allow_access_with_scope :read_user, if: -> (request) { request.get? || request.head? } - - before { authenticate! } - - feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned - - METADATA_QUERY = <<~EOF - { - metadata { - version - revision - } - } - EOF - - desc 'Get the version information of the GitLab instance.' do - detail 'This feature was introduced in GitLab 8.13.' - end - get '/version' do - run_graphql!( - query: METADATA_QUERY, - context: { current_user: current_user }, - transform: ->(result) { result.dig('data', 'metadata') } - ) - end - end -end diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index 082be1f7e11..bb8ad5c4285 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -133,7 +133,7 @@ module API success Entities::WikiAttachment end params do - requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded' + requires :file, types: [Rack::Multipart::UploadedFile, ::API::Validations::Types::WorkhorseFile], desc: 'The attachment file to be uploaded', documentation: { type: 'file' } optional :branch, type: String, desc: 'The name of the branch' end post ":id/wikis/attachments" do |