diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-16 13:42:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-16 13:42:19 +0300 |
commit | 84d1bd786125c1c14a3ba5f63e38a4cc736a9027 (patch) | |
tree | f550fa965f507077e20dbb6d61a8269a99ef7107 /lib/api | |
parent | 3a105e36e689f7b75482236712f1a47fd5a76814 (diff) |
Add latest changes from gitlab-org/gitlab@16-8-stable-eev16.8.0-rc42
Diffstat (limited to 'lib/api')
36 files changed, 458 insertions, 456 deletions
diff --git a/lib/api/admin/ci/variables.rb b/lib/api/admin/ci/variables.rb index 3277acc1b52..a443f7f3476 100644 --- a/lib/api/admin/ci/variables.rb +++ b/lib/api/admin/ci/variables.rb @@ -54,6 +54,10 @@ module API type: String, desc: 'The key of the variable. Max 255 characters' + optional :description, + type: String, + desc: 'The description of the variable' + requires :value, type: String, desc: 'The value of a variable' @@ -98,6 +102,10 @@ module API type: String, desc: 'The key of a variable' + optional :description, + type: String, + desc: 'The description of the variable' + optional :value, type: String, desc: 'The value of a variable' diff --git a/lib/api/api.rb b/lib/api/api.rb index 97e09795f49..94b433193dd 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -330,7 +330,7 @@ module API mount ::API::Suggestions mount ::API::SystemHooks mount ::API::Tags - mount ::API::Terraform::Modules::V1::Packages + mount ::API::Terraform::Modules::V1::NamespacePackages mount ::API::Terraform::Modules::V1::ProjectPackages mount ::API::Terraform::State mount ::API::Terraform::StateVersion diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index b5123ab49dc..f369fc5e183 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -183,7 +183,7 @@ module API .new(current_user: current_user, pipeline: pipeline, params: params) .execute - builds = builds.with_preloads + builds = builds.with_preloads.preload(:metadata) # rubocop:disable CodeReuse/ActiveRecord -- preload job.archived? present paginate(builds), with: Entities::Ci::Job end diff --git a/lib/api/ci/runners.rb b/lib/api/ci/runners.rb index 17bee275c51..300c30faf4a 100644 --- a/lib/api/ci/runners.rb +++ b/lib/api/ci/runners.rb @@ -94,6 +94,14 @@ module API forbidden!("No access granted") unless can?(current_user, :read_builds, runner) end + + def preload_job_associations(jobs) + jobs.preload( # rubocop: disable CodeReuse/ActiveRecord -- this preload is tightly related to the endpoint + :user, + { pipeline: { project: [:route, { namespace: :route }] } }, + { project: [:route, { namespace: :route }] } + ) + end end resource :runners do @@ -217,25 +225,24 @@ module API end params do requires :id, type: Integer, desc: 'The ID of a runner' + optional :system_id, type: String, desc: 'System ID associated with the runner manager' optional :status, type: String, desc: 'Status of the job', values: ::Ci::Build::AVAILABLE_STATUSES optional :order_by, type: String, desc: 'Order by `id`', values: ::Ci::RunnerJobsFinder::ALLOWED_INDEXED_COLUMNS optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Sort by `asc` or `desc` order. ' \ - 'Specify `order_by` as well, including for `id`' + 'Specify `order_by` as well, including for `id`' + optional :cursor, type: String, desc: 'Cursor for obtaining the next set of records' use :pagination end get ':id/jobs' do runner = get_runner(params[:id]) authenticate_list_runners_jobs!(runner) + # Optimize query when filtering by runner managers by not asking for count + paginator_params = params[:pagination] == :keyset || params[:system_id].blank? ? {} : { without_count: true } + jobs = ::Ci::RunnerJobsFinder.new(runner, current_user, params).execute - jobs = jobs.preload( # rubocop: disable CodeReuse/ActiveRecord - [ - :user, - { pipeline: { project: [:route, { namespace: :route }] } }, - { project: [:route, { namespace: :route }] } - ] - ) - jobs = paginate(jobs) + jobs = preload_job_associations(jobs) + jobs = paginate_with_strategies(jobs, paginator_params: paginator_params) jobs.each(&:commit) # batch loads all commits in the page present jobs, with: Entities::Ci::JobBasicWithProject diff --git a/lib/api/draft_notes.rb b/lib/api/draft_notes.rb index 6fadc68233d..3d046ec4a9b 100644 --- a/lib/api/draft_notes.rb +++ b/lib/api/draft_notes.rb @@ -22,7 +22,9 @@ module API end def delete_draft_note(draft_note) - ::DraftNotes::DestroyService.new(user_project, current_user).execute(draft_note) + ::DraftNotes::DestroyService + .new(merge_request(params: params), current_user) + .execute(draft_note) end def publish_draft_note(params:) diff --git a/lib/api/entities/bulk_imports/entity_failure.rb b/lib/api/entities/bulk_imports/entity_failure.rb index 08708a7c961..4771e6cb894 100644 --- a/lib/api/entities/bulk_imports/entity_failure.rb +++ b/lib/api/entities/bulk_imports/entity_failure.rb @@ -6,7 +6,7 @@ module API class EntityFailure < Grape::Entity expose :relation, documentation: { type: 'string', example: 'label' } expose :exception_message, documentation: { type: 'string', example: 'error message' } do |failure| - ::Projects::ImportErrorFilter.filter_message(failure.exception_message.truncate(72)) + ::Projects::ImportErrorFilter.filter_message(failure.exception_message).truncate(255) end expose :exception_class, documentation: { type: 'string', example: 'Exception' } expose :correlation_id_value, documentation: { type: 'string', example: 'dfcf583058ed4508e4c7c617bd7f0edd' } diff --git a/lib/api/entities/ci/job.rb b/lib/api/entities/ci/job.rb index d9e6b7eed75..2f748d28abf 100644 --- a/lib/api/entities/ci/job.rb +++ b/lib/api/entities/ci/job.rb @@ -12,6 +12,7 @@ module API expose :runner, with: ::API::Entities::Ci::Runner expose :artifacts_expire_at, documentation: { type: 'dateTime', example: '2016-01-19T09:05:50.355Z' } + expose :archived?, as: :archived, documentation: { type: 'boolean', example: false } expose( :tag_list, diff --git a/lib/api/entities/diff.rb b/lib/api/entities/diff.rb index cc53736a5b1..e1e6ce26263 100644 --- a/lib/api/entities/diff.rb +++ b/lib/api/entities/diff.rb @@ -16,6 +16,7 @@ module API expose :new_file?, as: :new_file, documentation: { type: 'boolean' } expose :renamed_file?, as: :renamed_file, documentation: { type: 'boolean' } expose :deleted_file?, as: :deleted_file, documentation: { type: 'boolean' } + expose :generated?, as: :generated_file, documentation: { type: 'boolean' } end end end diff --git a/lib/api/entities/group.rb b/lib/api/entities/group.rb index 1a1765c2e0a..14491c2396a 100644 --- a/lib/api/entities/group.rb +++ b/lib/api/entities/group.rb @@ -23,6 +23,7 @@ module API expose :full_name, :full_path expose :created_at expose :parent_id + expose :organization_id expose :shared_runners_setting expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb index 56519e2bf08..39bb54bfc5a 100644 --- a/lib/api/entities/merge_request_basic.rb +++ b/lib/api/entities/merge_request_basic.rb @@ -97,6 +97,11 @@ module API expose :squash expose :squash_on_merge?, as: :squash_on_merge expose :task_completion_status + + # #cannot_be_merged? is generally indicative of conflicts, and is set via + # MergeRequests::MergeabilityCheckService. However, it can also indicate + # that either #has_no_commits? or #branch_missing? are true. + # expose :cannot_be_merged?, as: :has_conflicts expose :mergeable_discussions_state?, as: :blocking_discussions_resolved diff --git a/lib/api/entities/ml/mlflow/model_version.rb b/lib/api/entities/ml/mlflow/model_version.rb index 10fdf3822a5..d57def4e1f2 100644 --- a/lib/api/entities/ml/mlflow/model_version.rb +++ b/lib/api/entities/ml/mlflow/model_version.rb @@ -18,7 +18,7 @@ module API expose :run_id expose :status expose :status_message - expose :metadata + expose :metadata, as: :tags, using: KeyValue expose :run_link expose :aliases, documentation: { is_array: true, type: String } @@ -68,10 +68,6 @@ module API "" end - def metadata - [] - end - def run_link "" end diff --git a/lib/api/entities/ml/mlflow/search_experiments.rb b/lib/api/entities/ml/mlflow/search_experiments.rb new file mode 100644 index 00000000000..9673cb1b6fd --- /dev/null +++ b/lib/api/entities/ml/mlflow/search_experiments.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Ml + module Mlflow + class SearchExperiments < Grape::Entity # rubocop:disable Search/NamespacedClass -- Not related to search + expose :experiments, with: Experiment + expose :next_page_token + end + end + end + end +end diff --git a/lib/api/entities/pages/deployments.rb b/lib/api/entities/pages/deployments.rb new file mode 100644 index 00000000000..143fbe93344 --- /dev/null +++ b/lib/api/entities/pages/deployments.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Pages + class Deployments < Grape::Entity + expose :created_at + expose :url + expose :path_prefix + expose :root_directory + end + end + end +end diff --git a/lib/api/entities/pages/project_settings.rb b/lib/api/entities/pages/project_settings.rb new file mode 100644 index 00000000000..81a48fe8bd3 --- /dev/null +++ b/lib/api/entities/pages/project_settings.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module API + module Entities + module Pages + class ProjectSettings < Grape::Entity + expose :url + expose :deployments, using: "API::Entities::Pages::Deployments" + expose :unique_domain_enabled?, as: :is_unique_domain_enabled + expose :force_https?, as: :force_https + end + end + end +end diff --git a/lib/api/entities/user_preferences.rb b/lib/api/entities/user_preferences.rb index e04ddd52f29..3824e3f1b37 100644 --- a/lib/api/entities/user_preferences.rb +++ b/lib/api/entities/user_preferences.rb @@ -8,5 +8,3 @@ module API end end end - -API::Entities::UserPreferences.prepend_mod_with('API::Entities::UserPreferences', with_descendants: true) diff --git a/lib/api/group_variables.rb b/lib/api/group_variables.rb index f320fa06394..94702c36c85 100644 --- a/lib/api/group_variables.rb +++ b/lib/api/group_variables.rb @@ -106,11 +106,31 @@ module API declared_params(include_missing: false) ) - variable = ::Ci::ChangeVariableService.new( - container: user_group, - current_user: current_user, - params: { action: :update, variable_params: filtered_params } - ).execute + # If the 'filter' parameter is provided, the user is updating a scoped variable + # and we need to use `find_variable` to make sure we update the correct one. + # However, this would result in an error response in case the user attempts to + # update a scoped variable without providing a filter. This error response is + # technically correct, because updating a scoped variable without specifying + # the targeted scope causes non-deterministic behavior. But this endpoint was + # originally introduced without the ability to specify a filter, and returning + # an error in these cases now would be considered a breaking change. + # Thus we only use the new/correct code if the user provided a filter. + # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136475 + if params.key?('filter') + variable = find_variable(user_group, params) + + variable = ::Ci::ChangeVariableService.new( + container: user_group, + current_user: current_user, + params: { action: :update, variable: variable, variable_params: filtered_params } + ).execute + else + variable = ::Ci::ChangeVariableService.new( + container: user_group, + current_user: current_user, + params: { action: :update, variable_params: filtered_params } + ).execute + end if variable.valid? present variable, with: Entities::Ci::Variable diff --git a/lib/api/groups.rb b/lib/api/groups.rb index 1ff64cd2ffd..7b755a76f29 100644 --- a/lib/api/groups.rb +++ b/lib/api/groups.rb @@ -213,11 +213,15 @@ module API requires :name, type: String, desc: 'The name of the group' requires :path, type: String, desc: 'The path of the group' optional :parent_id, type: Integer, desc: 'The parent group id for creating nested group' + optional :organization_id, type: Integer, desc: 'The organization id for the group' use :optional_params end post feature_category: :groups_and_projects, urgency: :low do - parent_group = find_group!(params[:parent_id]) if params[:parent_id].present? + organization = find_organization!(params[:organization_id]) if params[:organization_id].present? + authorize! :create_group, organization if organization + + parent_group = find_group!(params[:parent_id], organization: organization) if params[:parent_id].present? if parent_group authorize! :create_subgroup, parent_group else diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index f5dcbc07704..a59734d643d 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -184,8 +184,7 @@ module API return true unless job_token_authentication? return true unless route_authentication_setting[:job_token_scope] == :project - ::Feature.enabled?(:ci_job_token_scope, project) && - current_authenticated_job.project == project + current_authenticated_job.project == project end # rubocop: disable CodeReuse/ActiveRecord @@ -212,18 +211,25 @@ module API not_found!('Pipeline') end + def find_organization!(id) + organization = Organizations::Organization.find_by_id(id) + check_organization_access(organization) + end + # rubocop: disable CodeReuse/ActiveRecord - def find_group(id) + def find_group(id, organization: nil) + collection = organization.present? ? Group.in_organization(organization) : Group.all + if id.to_s =~ INTEGER_ID_REGEX - Group.find_by(id: id) + collection.find_by(id: id) else - Group.find_by_full_path(id) + collection.find_by_full_path(id) end end # rubocop: enable CodeReuse/ActiveRecord - def find_group!(id) - group = find_group(id) + def find_group!(id, organization: nil) + group = find_group(id, organization: organization) check_group_access(group) end @@ -836,6 +842,12 @@ module API @sudo_identifier ||= params[SUDO_PARAM] || env[SUDO_HEADER] end + def check_organization_access(organization) + return organization if can?(current_user, :read_organization, organization) + + not_found!('Organization') + end + def secret_token Gitlab::Shell.secret_token end diff --git a/lib/api/helpers/integrations_helpers.rb b/lib/api/helpers/integrations_helpers.rb index dd3009ff1d7..b450718a7d0 100644 --- a/lib/api/helpers/integrations_helpers.rb +++ b/lib/api/helpers/integrations_helpers.rb @@ -7,35 +7,6 @@ module API # The data structures inside this model are returned using class methods, # allowing EE to extend them where necessary. module IntegrationsHelpers - def self.chat_notification_settings - [ - { - required: true, - name: :webhook, - type: String, - desc: 'The chat webhook' - }, - { - required: false, - name: :username, - type: String, - desc: 'The chat username' - }, - { - required: false, - name: :channel, - type: String, - desc: 'The default chat channel' - }, - { - required: false, - name: :branches_to_be_notified, - type: String, - desc: 'Branches for which notifications are to be sent' - } - ].freeze - end - def self.chat_notification_flags [ { @@ -129,58 +100,8 @@ module API 'apple-app-store' => ::Integrations::AppleAppStore.api_fields, 'asana' => ::Integrations::Asana.api_fields, 'assembla' => ::Integrations::Assembla.api_fields, - 'bamboo' => [ - { - required: true, - name: :bamboo_url, - type: String, - desc: 'Bamboo root URL like https://bamboo.example.com' - }, - { - required: false, - name: :enable_ssl_verification, - type: ::Grape::API::Boolean, - desc: 'Enable SSL verification' - }, - { - required: true, - name: :build_key, - type: String, - desc: 'Bamboo build plan key like' - }, - { - required: true, - name: :username, - type: String, - desc: 'A user with API access, if applicable' - }, - { - required: true, - name: :password, - type: String, - desc: 'Password of the user' - } - ], - 'bugzilla' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - } - ], + 'bamboo' => ::Integrations::Bamboo.api_fields, + 'bugzilla' => ::Integrations::Bugzilla.api_fields, 'buildkite' => [ { required: true, @@ -201,54 +122,9 @@ module API desc: 'DEPRECATED: This parameter has no effect since SSL verification will always be enabled' } ], - 'campfire' => [ - { - required: true, - name: :token, - type: String, - desc: 'Campfire token' - }, - { - required: false, - name: :subdomain, - type: String, - desc: 'Campfire subdomain' - }, - { - required: false, - name: :room, - type: String, - desc: 'Campfire room' - } - ], - 'confluence' => [ - { - required: true, - name: :confluence_url, - type: String, - desc: 'The URL of the Confluence Cloud Workspace hosted on atlassian.net' - } - ], - 'custom-issue-tracker' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New issue URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - } - ], + 'campfire' => ::Integrations::Campfire.api_fields, + 'confluence' => ::Integrations::Confluence.api_fields, + 'custom-issue-tracker' => ::Integrations::CustomIssueTracker.api_fields, 'datadog' => [ { required: true, @@ -293,19 +169,9 @@ module API desc: 'Custom tags in Datadog. Specify one tag per line in the format: "key:value\nkey2:value2"' } ], + 'diffblue-cover' => ::Integrations::DiffblueCover.api_fields, 'discord' => [ - { - required: true, - name: :webhook, - type: String, - desc: 'Discord webhook. For example, https://discord.com/api/webhooks/…' - }, - { - required: false, - name: :branches_to_be_notified, - type: String, - desc: 'Branches for which notifications are to be sent' - }, + ::Integrations::Discord.api_fields, chat_notification_flags, chat_notification_channels ].flatten, @@ -355,40 +221,8 @@ module API desc: 'Branches for which notifications are to be sent' } ], - 'external-wiki' => [ - { - required: true, - name: :external_wiki_url, - type: String, - desc: 'The URL of the external wiki' - } - ], - 'google-play' => [ - { - required: true, - name: :package_name, - type: String, - desc: 'The package name of the app in Google Play' - }, - { - required: true, - name: :service_account_key, - type: String, - desc: 'The Google Play service account key' - }, - { - required: true, - name: :service_account_key_file_name, - type: String, - desc: 'The filename of the Google Play service account key' - }, - { - required: false, - name: :google_play_protected_refs, - type: ::Grape::API::Boolean, - desc: 'Only enable for protected refs' - } - ], + 'external-wiki' => ::Integrations::ExternalWiki.api_fields, + 'google-play' => ::Integrations::GooglePlay.api_fields, 'hangouts-chat' => [ { required: true, @@ -403,32 +237,7 @@ module API desc: 'Branches for which notifications are to be sent' } ].flatten, - 'harbor' => [ - { - required: true, - name: :url, - type: String, - desc: 'The base URL to the Harbor instance which is being linked to this GitLab project. For example, https://demo.goharbor.io.' - }, - { - required: true, - name: :project_name, - type: String, - desc: 'The Project name to the Harbor instance. For example, testproject.' - }, - { - required: true, - name: :username, - type: String, - desc: 'The username created from Harbor interface.' - }, - { - required: true, - name: :password, - type: String, - desc: 'The password of the user.' - } - ], + 'harbor' => ::Integrations::Harbor.api_fields, 'irker' => [ { required: true, @@ -555,14 +364,7 @@ module API desc: 'Enable comments inside Jira issues on each GitLab event (commit / merge request)' } ], - 'mattermost-slash-commands' => [ - { - required: true, - name: :token, - type: String, - desc: 'The Mattermost token' - } - ], + 'mattermost-slash-commands' => ::Integrations::MattermostSlashCommands.api_fields, 'slack-slash-commands' => [ { required: true, @@ -697,77 +499,12 @@ module API desc: 'The sound of the notification' } ], - 'redmine' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'The new issue URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'The project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'The issues URL' - } - ], - 'ewm' => [ - { - required: true, - name: :new_issue_url, - type: String, - desc: 'New Issue URL' - }, - { - required: true, - name: :project_url, - type: String, - desc: 'Project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'Issues URL' - } - ], - 'youtrack' => [ - { - required: true, - name: :project_url, - type: String, - desc: 'The project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'The issues URL' - } - ], - 'clickup' => [ - { - required: true, - name: :project_url, - type: String, - desc: 'The project URL' - }, - { - required: true, - name: :issues_url, - type: String, - desc: 'The issues URL' - } - ], + 'redmine' => ::Integrations::Redmine.api_fields, + 'ewm' => ::Integrations::Ewm.api_fields, + 'youtrack' => ::Integrations::Youtrack.api_fields, + 'clickup' => ::Integrations::Clickup.api_fields, 'slack' => [ - chat_notification_settings, - chat_notification_flags, + ::Integrations::Slack.api_fields, chat_notification_channels ].flatten, 'microsoft-teams' => [ @@ -786,8 +523,7 @@ module API chat_notification_flags ].flatten, 'mattermost' => [ - chat_notification_settings, - chat_notification_flags, + ::Integrations::Mattermost.api_fields, chat_notification_channels ].flatten, 'teamcity' => [ @@ -879,20 +615,7 @@ module API desc: 'The product ID of ZenTao project' } ], - 'squash-tm' => [ - { - required: true, - name: :url, - type: String, - desc: 'The Squash TM webhook URL' - }, - { - required: false, - name: :token, - type: String, - desc: 'The secret token' - } - ] + 'squash-tm' => ::Integrations::SquashTm.api_fields } end @@ -909,6 +632,7 @@ module API ::Integrations::Confluence, ::Integrations::CustomIssueTracker, ::Integrations::Datadog, + ::Integrations::DiffblueCover, ::Integrations::Discord, ::Integrations::DroneCi, ::Integrations::EmailsOnPush, diff --git a/lib/api/helpers/kubernetes/agent_helpers.rb b/lib/api/helpers/kubernetes/agent_helpers.rb index eca26c023cf..18f47fa9955 100644 --- a/lib/api/helpers/kubernetes/agent_helpers.rb +++ b/lib/api/helpers/kubernetes/agent_helpers.rb @@ -40,7 +40,6 @@ module API def increment_unique_events events = params[:unique_counters]&.slice( - :agent_users_using_ci_tunnel, :k8s_api_proxy_requests_unique_agents_via_ci_access, :k8s_api_proxy_requests_unique_agents_via_user_access, :k8s_api_proxy_requests_unique_agents_via_pat_access, @@ -60,6 +59,8 @@ module API ) return if event_lists.blank? + event_lists[:agent_users_using_ci_tunnel] = event_lists.values.flatten + users, projects = load_users_and_projects(event_lists) event_lists.each do |event_name, events| track_events_for(event_name, events, users, projects) diff --git a/lib/api/helpers/packages/maven.rb b/lib/api/helpers/packages/maven.rb index 6c50f4c00a1..f7c8da3e641 100644 --- a/lib/api/helpers/packages/maven.rb +++ b/lib/api/helpers/packages/maven.rb @@ -19,11 +19,11 @@ module API documentation: { example: 'mypkg-1.0-SNAPSHOT.jar' } end - def extract_format(file_name) + def extract_format(file_name, skip_fips_check: false) name, _, format = file_name.rpartition('.') if %w[md5 sha1].include?(format) - unprocessable_entity! if Gitlab::FIPS.enabled? && format == 'md5' + unprocessable_entity! if !skip_fips_check && Gitlab::FIPS.enabled? && format == 'md5' [name, format] else diff --git a/lib/api/helpers/user_preferences_helpers.rb b/lib/api/helpers/user_preferences_helpers.rb deleted file mode 100644 index 846ad354156..00000000000 --- a/lib/api/helpers/user_preferences_helpers.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -module API - module Helpers - module UserPreferencesHelpers - extend ActiveSupport::Concern - extend Grape::API::Helpers - - def update_user_namespace_settings(attrs) - # This method will be redefined in EE. - attrs - end - end - end -end - -API::Helpers::UserPreferencesHelpers.prepend_mod_with('API::Helpers::UserPreferencesHelpers') diff --git a/lib/api/internal/kubernetes.rb b/lib/api/internal/kubernetes.rb index d3a4d94f8ca..286192f8093 100644 --- a/lib/api/internal/kubernetes.rb +++ b/lib/api/internal/kubernetes.rb @@ -127,7 +127,6 @@ module API end optional :unique_counters, type: Hash do - optional :agent_users_using_ci_tunnel, type: Array[Integer], desc: 'An array of user ids that have interacted with CI Tunnel' optional :k8s_api_proxy_requests_unique_users_via_ci_access, type: Array[Integer], desc: 'An array of users that have interacted with the CI tunnel via `ci_access`' optional :k8s_api_proxy_requests_unique_agents_via_ci_access, type: Array[Integer], desc: 'An array of agents that have interacted with the CI tunnel via `ci_access`' optional :k8s_api_proxy_requests_unique_users_via_user_access, type: Array[Integer], desc: 'An array of users that have interacted with the CI tunnel via `user_access`' diff --git a/lib/api/maven_packages.rb b/lib/api/maven_packages.rb index 14c3fccee32..e969935383a 100644 --- a/lib/api/maven_packages.rb +++ b/lib/api/maven_packages.rb @@ -260,7 +260,13 @@ module API authorize_upload! bad_request!('File is too large') if user_project.actual_limits.exceeded?(:maven_max_file_size, params[:file].size) - file_name, format = extract_format(params[:file_name]) + # In FIPS mode, we've already told Workhorse not to generate a + # MD5 checksum via UploadHashFunctions, and the FIPS check above + # ensures that Workhorse obeys that. However, Gradle will attempt to issue a PUT request + # with the MD5 checksum, and the publish step will fail if this endpoint returns a + # 422 (https://github.com/gradle/gradle/blob/v8.5.0/platforms/software/maven/src/main/java/org/gradle/api/publish/maven/internal/publisher/AbstractMavenPublisher.java#L240), + # so we need to skip the second FIPS check here. + file_name, format = extract_format(params[:file_name], skip_fips_check: true) ::Gitlab::Database::LoadBalancing::Session.current.use_primary do result = ::Packages::Maven::FindOrCreatePackageService diff --git a/lib/api/members.rb b/lib/api/members.rb index 56a15c41e1c..908733d4aa1 100644 --- a/lib/api/members.rb +++ b/lib/api/members.rb @@ -176,7 +176,7 @@ module API source = find_source(source_type, params[:id]) member = source_members(source).find_by!(user_id: params[:user_id]) - check_rate_limit!(:member_delete, scope: [source, current_user]) + check_rate_limit!(:members_delete, scope: [source, current_user]) destroy_conditionally!(member) do ::Members::DestroyService.new(current_user).execute(member, skip_subresources: params[:skip_subresources], unassign_issuables: params[:unassign_issuables]) diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb index d0c9400039a..23d7dafdc0a 100644 --- a/lib/api/merge_request_approvals.rb +++ b/lib/api/merge_request_approvals.rb @@ -104,7 +104,8 @@ module API put 'reset_approvals', urgency: :low do merge_request = find_project_merge_request(params[:merge_request_iid]) - unauthorized! unless current_user.can?(:reset_merge_request_approvals, merge_request) + unauthorized! unless current_user.can?(:reset_merge_request_approvals, merge_request) && + !merge_request.merged? merge_request.approvals.delete_all diff --git a/lib/api/ml/mlflow/api_helpers.rb b/lib/api/ml/mlflow/api_helpers.rb index 66d79753110..c81c66ca798 100644 --- a/lib/api/ml/mlflow/api_helpers.rb +++ b/lib/api/ml/mlflow/api_helpers.rb @@ -5,6 +5,7 @@ module API module Mlflow module ApiHelpers OUTER_QUOTES_REGEXP = /^("|')|("|')?$/ + GITLAB_TAG_PREFIX = 'gitlab.' def check_api_read! not_found! unless can?(current_user, :read_model_experiments, user_project) @@ -113,6 +114,27 @@ module API { name: filter } end + def gitlab_tags + return unless params[:tags].present? + + tags = params[:tags] + gitlab_params = {} + + tags.each do |tag| + key, value = tag.values_at(:key, :value) + + gitlab_params[key.delete_prefix(GITLAB_TAG_PREFIX)] = value if key&.starts_with?(GITLAB_TAG_PREFIX) + end + + gitlab_params + end + + def custom_version + return unless gitlab_tags + + gitlab_tags['version'] + end + def find_experiment!(iid, name) experiment_repository.by_iid_or_name(iid: iid, name: name) || resource_not_found! end diff --git a/lib/api/ml/mlflow/experiments.rb b/lib/api/ml/mlflow/experiments.rb index 1a501291941..511922782e8 100644 --- a/lib/api/ml/mlflow/experiments.rb +++ b/lib/api/ml/mlflow/experiments.rb @@ -47,6 +47,42 @@ module API present response, with: Entities::Ml::Mlflow::ListExperiment end + desc 'Search experiments' do + success Entities::Ml::Mlflow::ListExperiment + detail 'https://www.mlflow.org/docs/latest/rest-api.html#list-experiments' + end + params do + optional :max_results, + type: Integer, + desc: 'Maximum number of experiments to fetch in a page. Default is 200, maximum is 1000.', + default: 200 + optional :order_by, + type: String, + desc: 'Order criteria. Can be by a column of the experiment (created_at, name).', + default: 'created_at DESC' + optional :page_token, + type: String, + desc: 'Token for pagination' + optional :filter, + type: String, + desc: 'This parameter is ignored' + end + post 'search', urgency: :low do + max_results = [params[:max_results], 1000].min + + finder_params = model_order_params(params) + + finder = ::Projects::Ml::ExperimentFinder.new(user_project, finder_params) + paginator = finder.execute.keyset_paginate(cursor: params[:page_token], per_page: max_results) + + result = { + experiments: paginator.records, + next_page_token: paginator.cursor_for_next_page + } + + present result, with: Entities::Ml::Mlflow::SearchExperiments + end + desc 'Create experiment' do success Entities::Ml::Mlflow::NewExperiment detail 'https://www.mlflow.org/docs/1.28.0/rest-api.html#create-experiment' diff --git a/lib/api/ml/mlflow/model_versions.rb b/lib/api/ml/mlflow/model_versions.rb index 4b211cf540c..53ba4ff36f1 100644 --- a/lib/api/ml/mlflow/model_versions.rb +++ b/lib/api/ml/mlflow/model_versions.rb @@ -27,13 +27,16 @@ module API desc: 'Register model under this name This field is required.' optional :description, type: String, desc: 'Optional description for model version.' + optional :tags, type: Array, desc: 'Additional metadata for a model version.' end post 'create', urgency: :low do present ::Ml::CreateModelVersionService.new( model, { model_name: params[:name], - description: params[:description] + description: params[:description], + metadata: params[:tags], + version: custom_version } ).execute, with: Entities::Ml::Mlflow::ModelVersion, diff --git a/lib/api/ml/mlflow/registered_models.rb b/lib/api/ml/mlflow/registered_models.rb index a68a2767a74..3f4996a94c0 100644 --- a/lib/api/ml/mlflow/registered_models.rb +++ b/lib/api/ml/mlflow/registered_models.rb @@ -31,13 +31,17 @@ module API optional :tags, type: Array, desc: 'Additional metadata for registered model.' end post 'create', urgency: :low do - present ::Ml::CreateModelService.new( + model = ::Ml::CreateModelService.new( user_project, params[:name], current_user, params[:description], params[:tags] - ).execute, + ).execute + + resource_already_exists! unless model.persisted? + + present model, with: Entities::Ml::Mlflow::RegisteredModel, root: :registered_model rescue ActiveRecord::RecordInvalid diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb index 750dc7fc2a1..17425c288fc 100644 --- a/lib/api/namespaces.rb +++ b/lib/api/namespaces.rb @@ -34,6 +34,7 @@ module API params do optional :search, type: String, desc: 'Returns a list of namespaces the user is authorized to view based on the search criteria' optional :owned_only, type: Boolean, desc: 'In GitLab 14.2 and later, returns a list of owned namespaces only' + optional :top_level_only, type: Boolean, default: false, desc: 'Only include top level namespaces' use :pagination use :optional_list_params_ee @@ -43,6 +44,8 @@ module API namespaces = current_user.admin ? Namespace.all : current_user.namespaces(owned_only: owned_only) + namespaces = namespaces.top_most if params[:top_level_only] + namespaces = namespaces.without_project_namespaces.include_route namespaces = namespaces.include_gitlab_subscription_with_hosted_plan if Gitlab.ee? diff --git a/lib/api/npm_project_packages.rb b/lib/api/npm_project_packages.rb index e1d0455b1e2..c6c99944ca0 100644 --- a/lib/api/npm_project_packages.rb +++ b/lib/api/npm_project_packages.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true module API class NpmProjectPackages < ::API::Base + ERROR_REASON_TO_HTTP_STATUS_MAPPTING = { + ::Packages::Npm::CreatePackageService::ERROR_REASON_INVALID_PARAMETER => 400, + ::Packages::Npm::CreatePackageService::ERROR_REASON_PACKAGE_LEASE_TAKEN => 400, + ::Packages::Npm::CreatePackageService::ERROR_REASON_PACKAGE_EXISTS => 403, + ::Packages::Npm::CreatePackageService::ERROR_REASON_PACKAGE_PROTECTED => 403 + }.freeze + helpers ::API::Helpers::Packages::Npm feature_category :package_registry @@ -14,6 +21,10 @@ module API def endpoint_scope :project end + + def error_reason_to_http_status(reason) + ERROR_REASON_TO_HTTP_STATUS_MAPPTING.fetch(reason, 400) + end end params do @@ -74,12 +85,13 @@ module API else authorize_create_package!(project) - created_package = ::Packages::Npm::CreatePackageService + service_response = ::Packages::Npm::CreatePackageService .new(project, current_user, params.merge(build: current_authenticated_job)).execute - if created_package[:status] == :error - render_structured_api_error!({ message: created_package[:message], error: created_package[:message] }, created_package[:http_status]) + if service_response.error? + render_structured_api_error!({ message: service_response.message, error: service_response.message }, error_reason_to_http_status(service_response.reason)) else + created_package = service_response[:package] enqueue_sync_metadata_cache_worker(project, created_package.name) track_package_event('push_package', :npm, category: 'API::NpmPackages', project: project, namespace: project.namespace) created_package diff --git a/lib/api/pages.rb b/lib/api/pages.rb index 0cedf7d975f..30e126b34cb 100644 --- a/lib/api/pages.rb +++ b/lib/api/pages.rb @@ -6,7 +6,6 @@ module API before do require_pages_config_enabled! - authenticated_with_can_read_all_resources! end params do @@ -24,12 +23,30 @@ module API tags %w[pages] end delete ':id/pages' do + authenticated_with_can_read_all_resources! authorize! :remove_pages, user_project ::Pages::DeleteService.new(user_project, current_user).execute no_content! end + + desc 'Get pages settings' do + detail 'Get pages URL and other settings. This feature was introduced in Gitlab 16.8' + success code: 200 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 404, message: 'Not Found' } + ] + tags %w[pages] + end + get ':id/pages' do + authorize! :read_pages, user_project + + break not_found! unless user_project.pages_enabled? + + present ::Pages::ProjectSettings.new(user_project), with: Entities::Pages::ProjectSettings + end end end end diff --git a/lib/api/terraform/modules/v1/packages.rb b/lib/api/terraform/modules/v1/namespace_packages.rb index 9e82a849c98..1999fc42aba 100644 --- a/lib/api/terraform/modules/v1/packages.rb +++ b/lib/api/terraform/modules/v1/namespace_packages.rb @@ -4,7 +4,7 @@ module API module Terraform module Modules module V1 - class Packages < ::API::Base + class NamespacePackages < ::API::Base include ::API::Helpers::Authentication helpers ::API::Helpers::PackagesHelpers helpers ::API::Helpers::Packages::BasicAuthHelpers @@ -29,8 +29,10 @@ module API end helpers do + include ::Gitlab::Utils::StrongMemoize + params :module_name do - requires :module_name, type: String, desc: "", regexp: API::NO_SLASH_URL_PART_REGEX + requires :module_name, type: String, desc: '', regexp: API::NO_SLASH_URL_PART_REGEX requires :module_system, type: String, regexp: API::NO_SLASH_URL_PART_REGEX end @@ -39,10 +41,9 @@ module API end def module_namespace - strong_memoize(:module_namespace) do - find_namespace(params[:module_namespace]) - end + find_namespace(params[:module_namespace]) end + strong_memoize_attr :module_namespace def finder_params { @@ -55,26 +56,23 @@ module API end def packages - strong_memoize(:packages) do - ::Packages::GroupPackagesFinder.new( - current_user, - module_namespace, - finder_params - ).execute - end + ::Packages::GroupPackagesFinder.new( + current_user, + module_namespace, + finder_params + ).execute end + strong_memoize_attr :packages def package - strong_memoize(:package) do - packages.first - end + packages.first end + strong_memoize_attr :package def package_file - strong_memoize(:package_file) do - package.installable_package_files.first - end + package.installable_package_files.first end + strong_memoize_attr :package_file end params do @@ -82,7 +80,8 @@ module API includes :module_name end - namespace 'packages/terraform/modules/v1/:module_namespace/:module_name/:module_system', requirements: TERRAFORM_MODULE_REQUIREMENTS do + namespace 'packages/terraform/modules/v1/:module_namespace/:module_name/:module_system', + requirements: TERRAFORM_MODULE_REQUIREMENTS do authenticate_with do |accept| accept.token_types(:personal_access_token, :deploy_token, :job_token) .sent_through(:http_bearer_token) @@ -118,7 +117,9 @@ module API get 'download' do latest_version = packages.order_version.last&.version - render_api_error!({ error: "No version found for #{params[:module_name]} module" }, :not_found) if latest_version.nil? + if latest_version.nil? + render_api_error!({ error: "No version found for #{params[:module_name]} module" }, :not_found) + end download_path = api_v4_packages_terraform_modules_v1_module_version_download_path( { @@ -145,7 +146,9 @@ module API get do latest_package = packages.order_version.last - render_api_error!({ error: "No version found for #{params[:module_name]} module" }, :not_found) if latest_package&.version.nil? + if latest_package&.version.nil? + render_api_error!({ error: "No version found for #{params[:module_name]} module" }, :not_found) + end presenter = ::Terraform::ModuleVersionPresenter.new(latest_package, params[:module_system]) present presenter, with: ::API::Entities::Terraform::ModuleVersion @@ -181,13 +184,18 @@ module API jwt_token = Gitlab::TerraformRegistryToken.from_token(token_from_namespace_inheritable).encoded end - header 'X-Terraform-Get', module_file_path.sub(%r{module_version/file$}, "#{params[:module_version]}/file?token=#{jwt_token}&archive=tgz") + header 'X-Terraform-Get', + module_file_path.sub( + %r{module_version/file$}, + "#{params[:module_version]}/file?token=#{jwt_token}&archive=tgz" + ) status :no_content end namespace 'file' do authenticate_with do |accept| - accept.token_types(:deploy_token_from_jwt, :job_token_from_jwt, :personal_access_token_from_jwt).sent_through(:token_param) + accept.token_types(:deploy_token_from_jwt, :job_token_from_jwt, :personal_access_token_from_jwt) + .sent_through(:token_param) end desc 'Download specific version of a module' do @@ -200,9 +208,14 @@ module API tags %w[terraform_registry] end get do - track_package_event('pull_package', :terraform_module, project: package.project, namespace: module_namespace) - - present_carrierwave_file!(package_file.file) + track_package_event( + 'pull_package', + :terraform_module, + project: package.project, + namespace: module_namespace + ) + + present_package_file!(package_file) end end diff --git a/lib/api/terraform/modules/v1/project_packages.rb b/lib/api/terraform/modules/v1/project_packages.rb index 07dfddefefc..c0a84c7b36c 100644 --- a/lib/api/terraform/modules/v1/project_packages.rb +++ b/lib/api/terraform/modules/v1/project_packages.rb @@ -16,87 +16,174 @@ module API require_packages_enabled! end - params do - requires :id, type: String, desc: 'The ID or full path of a project' - requires :module_name, type: String, desc: "", regexp: API::NO_SLASH_URL_PART_REGEX - requires :module_system, type: String, regexp: API::NO_SLASH_URL_PART_REGEX - requires :module_version, type: String, desc: 'Module version', regexp: Gitlab::Regex.semver_regex - end + helpers do + params :terraform_get do + optional 'terraform-get', type: String, values: %w[1], desc: 'Terraform get redirection flag' + end - resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do - namespace ':id/packages/terraform/modules/:module_name/:module_system/*module_version/file' do - authenticate_with do |accept| - accept.token_types(:deploy_token).sent_through(:http_deploy_token_header) - accept.token_types(:job_token).sent_through(:http_job_token_header) - accept.token_types(:personal_access_token).sent_through(:http_private_token_header) + def present_package_file + authorize_read_package!(authorized_user_project) + + if declared_params[:'terraform-get'] == '1' + header 'X-Terraform-Get', "#{request.url.split('?').first}?archive=tgz" + return no_content! end - desc 'Workhorse authorize Terraform Module package file' do - detail 'This feature was introduced in GitLab 13.11' - success code: 200 - failure [ - { code: 403, message: 'Forbidden' } - ] - tags %w[terraform_registry] + package = ::Packages::TerraformModule::PackagesFinder + .new(authorized_user_project, finder_params) + .execute + .first + + not_found! unless package + + track_package_event('pull_package', :terraform_module, project: authorized_user_project, + namespace: authorized_user_project.namespace) + + present_package_file!(package.installable_package_files.first) + end + + def finder_params + { package_name: package_name }.tap do |finder_params| + finder_params[:package_version] = params[:module_version] if params.key?(:module_version) end + end + + def package_name + "#{params[:module_name]}/#{params[:module_system]}" + end + end - put 'authorize' do - authorize_workhorse!( - subject: authorized_user_project, - maximum_size: authorized_user_project.actual_limits.terraform_module_max_file_size - ) + params do + requires :id, types: [String, Integer], allow_blank: false, desc: 'The ID or full path of a project' + with(type: String, allow_blank: false, regexp: API::NO_SLASH_URL_PART_REGEX) do + requires :module_name, desc: 'Module name', documentation: { example: 'infra-registry' } + requires :module_system, desc: 'Module system', documentation: { example: 'aws' } + end + end + + resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do + namespace ':id/packages/terraform/modules/:module_name/:module_system' do + authenticate_with do |accept| + accept.token_types( + :personal_access_token_with_username, + :deploy_token_with_username, + :job_token_with_username + ).sent_through(:http_basic_auth) end - desc 'Upload Terraform Module package file' do - detail 'This feature was introduced in GitLab 13.11' - success code: 201 + desc 'Download the latest version of a module' do + detail 'This feature was introduced in GitLab 16.7' + success code: 204 failure [ - { code: 400, message: 'Invalid file' }, { code: 401, message: 'Unauthorized' }, { code: 403, message: 'Forbidden' }, { code: 404, message: 'Not found' } ] - consumes %w[multipart/form-data] tags %w[terraform_registry] end - params do - requires :file, type: ::API::Validations::Types::WorkhorseFile, - desc: 'The package file to be published (generated by Multipart middleware)', - documentation: { type: 'file' } + use :terraform_get + end + get do + present_package_file end - put do - authorize_upload!(authorized_user_project) - - bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?( - :terraform_module_max_file_size, params[:file].size) - - create_package_file_params = { - module_name: params['module_name'], - module_system: params['module_system'], - module_version: params['module_version'], - file: params['file'], - build: current_authenticated_job - } - - result = ::Packages::TerraformModule::CreatePackageService - .new(authorized_user_project, current_user, create_package_file_params) - .execute - - render_api_error!(result[:message], result[:http_status]) if result[:status] == :error - - track_package_event('push_package', :terraform_module, project: authorized_user_project, - namespace: authorized_user_project.namespace) - - created! - rescue ObjectStorage::RemoteStoreError => e - Gitlab::ErrorTracking.track_exception( - e, - extra: { file_name: params[:file_name], project_id: authorized_user_project.id } - ) - - forbidden! + params do + requires :module_version, type: String, allow_blank: false, desc: 'Module version', + regexp: Gitlab::Regex.semver_regex + end + namespace '*module_version' do + desc 'Download a specific version of a module' do + detail 'This feature was introduced in GitLab 16.7' + success code: 204 + failure [ + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] + tags %w[terraform_registry] + end + params do + use :terraform_get + end + get format: false do + present_package_file + end + + namespace :file do + authenticate_with do |accept| + accept.token_types(:deploy_token).sent_through(:http_deploy_token_header) + accept.token_types(:job_token).sent_through(:http_job_token_header) + accept.token_types(:personal_access_token).sent_through(:http_private_token_header) + end + + desc 'Workhorse authorize Terraform Module package file' do + detail 'This feature was introduced in GitLab 13.11' + success code: 200 + failure [ + { code: 403, message: 'Forbidden' } + ] + tags %w[terraform_registry] + end + + put :authorize do + authorize_workhorse!( + subject: authorized_user_project, + maximum_size: authorized_user_project.actual_limits.terraform_module_max_file_size + ) + end + + desc 'Upload Terraform Module package file' do + detail 'This feature was introduced in GitLab 13.11' + success code: 201 + failure [ + { code: 400, message: 'Invalid file' }, + { code: 401, message: 'Unauthorized' }, + { code: 403, message: 'Forbidden' }, + { code: 404, message: 'Not found' } + ] + consumes %w[multipart/form-data] + tags %w[terraform_registry] + end + + params do + requires :file, type: ::API::Validations::Types::WorkhorseFile, + desc: 'The package file to be published (generated by Multipart middleware)', + documentation: { type: 'file' } + end + + put do + authorize_upload!(authorized_user_project) + + bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?( + :terraform_module_max_file_size, params[:file].size + ) + + create_package_file_params = { + module_name: params['module_name'], + module_system: params['module_system'], + module_version: params['module_version'], + file: params['file'], + build: current_authenticated_job + } + + result = ::Packages::TerraformModule::CreatePackageService + .new(authorized_user_project, current_user, create_package_file_params) + .execute + + render_api_error!(result.message, result.reason) if result.error? + + track_package_event('push_package', :terraform_module, project: authorized_user_project, + namespace: authorized_user_project.namespace) + + created! + rescue ObjectStorage::RemoteStoreError => e + Gitlab::ErrorTracking.track_exception(e, + extra: { file_name: params[:file_name], project_id: authorized_user_project.id }) + + forbidden! + end + end end end end diff --git a/lib/api/users.rb b/lib/api/users.rb index 38fa247055e..8b54fb84dd2 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1077,8 +1077,6 @@ module API end end - helpers Helpers::UserPreferencesHelpers - desc "Get the currently authenticated user's SSH keys" do success Entities::SSHKey end @@ -1269,9 +1267,7 @@ module API optional :view_diffs_file_by_file, type: Boolean, desc: 'Flag indicating the user sees only one file diff per page' optional :show_whitespace_in_diffs, type: Boolean, desc: 'Flag indicating the user sees whitespace changes in diffs' optional :pass_user_identities_to_ci_jwt, type: Boolean, desc: 'Flag indicating the user passes their external identities to a CI job as part of a JSON web token.' - optional :code_suggestions, type: Boolean, desc: 'Flag indicating the user allows code suggestions.' \ - 'Argument is experimental and can be removed in the future without notice.' - at_least_one_of :view_diffs_file_by_file, :show_whitespace_in_diffs, :pass_user_identities_to_ci_jwt, :code_suggestions + at_least_one_of :view_diffs_file_by_file, :show_whitespace_in_diffs, :pass_user_identities_to_ci_jwt end put "preferences", feature_category: :user_profile, urgency: :high do authenticate! @@ -1280,8 +1276,6 @@ module API attrs = declared_params(include_missing: false) - attrs = update_user_namespace_settings(attrs) - render_api_error!('400 Bad Request', 400) unless attrs service = ::UserPreferences::UpdateService.new(current_user, attrs).execute |