diff options
74 files changed, 800 insertions, 507 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 8c0c48ff449..72cdaf77f52 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -645,7 +645,7 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab /doc/api/templates/licenses.md @rdickenson /doc/api/todos.md @msedlakjakubowski /doc/api/topics.md @lciutacu -/doc/api/usage_data.md @dianalogan +/doc/api/usage_data.md @lciutacu /doc/api/users.md @jglassman1 /doc/api/version.md @phillipwells /doc/api/visual_review_discussions.md @drcatherinepope @@ -767,8 +767,8 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab /doc/development/sec/ @rdickenson /doc/development/sec/security_report_ingestion_overview.md @dianalogan /doc/development/secure_coding_guidelines.md @sselhorn -/doc/development/service_ping/ @dianalogan -/doc/development/snowplow/ @dianalogan +/doc/development/service_ping/ @lciutacu +/doc/development/snowplow/ @lciutacu /doc/development/spam_protection_and_captcha/ @phillipwells /doc/development/sql.md @aqualls /doc/development/testing_guide/ @sselhorn @@ -864,11 +864,12 @@ lib/gitlab/checks/** @proglottis @toon @zj-gitlab /doc/user/admin_area/settings/rate_limit_on_issues_creation.md @msedlakjakubowski /doc/user/admin_area/settings/rate_limit_on_notes_creation.md @msedlakjakubowski /doc/user/admin_area/settings/rate_limit_on_pipelines_creation.md @drcatherinepope +/doc/user/admin_area/settings/rate_limit_on_projects_api.md @lciutacu /doc/user/admin_area/settings/rate_limit_on_users_api.md @jglassman1 /doc/user/admin_area/settings/scim_setup.md @jglassman1 /doc/user/admin_area/settings/terraform_limits.md @phillipwells /doc/user/admin_area/settings/third_party_offers.md @lciutacu -/doc/user/admin_area/settings/usage_statistics.md @dianalogan +/doc/user/admin_area/settings/usage_statistics.md @lciutacu /doc/user/admin_area/settings/visibility_and_access_controls.md @aqualls /doc/user/analytics/ @lciutacu /doc/user/analytics/ci_cd_analytics.md @rdickenson diff --git a/.rubocop_todo/rails/inverse_of.yml b/.rubocop_todo/rails/inverse_of.yml index 31535699d2e..1bb1559a394 100644 --- a/.rubocop_todo/rails/inverse_of.yml +++ b/.rubocop_todo/rails/inverse_of.yml @@ -1,13 +1,6 @@ --- Rails/InverseOf: Exclude: - - 'app/models/alert_management/alert.rb' - - 'app/models/alert_management/alert_assignee.rb' - - 'app/models/application_setting.rb' - - 'app/models/audit_event.rb' - - 'app/models/board.rb' - - 'app/models/bulk_imports/entity.rb' - - 'app/models/bulk_imports/tracker.rb' - 'app/models/ci/build.rb' - 'app/models/ci/build_pending_state.rb' - 'app/models/ci/build_trace_chunk.rb' diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue index 2efc80fef75..7c9e30a2d97 100644 --- a/app/assets/javascripts/search/sidebar/components/app.vue +++ b/app/assets/javascripts/search/sidebar/components/app.vue @@ -27,7 +27,9 @@ export default { </script> <template> - <section class="search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4 gl-mb-6 gl-mt-5"> + <section + class="search-sidebar gl-display-flex gl-flex-direction-column gl-md-mr-5 gl-mb-6 gl-mt-5" + > <scope-navigation /> <results-filters v-if="showIssueAndMergeFilters" /> <language-filter v-if="showBlobFilter" /> diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue index da6039f4758..d4f866713a8 100644 --- a/app/assets/javascripts/search/topbar/components/app.vue +++ b/app/assets/javascripts/search/topbar/components/app.vue @@ -86,45 +86,50 @@ export default { </script> <template> - <section class="search-page-form gl-lg-display-flex gl-flex-direction-column"> - <div class="gl-lg-display-flex gl-flex-direction-row gl-align-items-flex-end"> - <div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2"> - <div - class="gl-sm-display-flex gl-flex-direction-row gl-justify-content-space-between gl-mb-4 gl-md-mb-0" - > - <label>{{ $options.i18n.searchLabel }}</label> - <template v-if="showSyntaxOptions"> - <gl-button - category="tertiary" - variant="link" - size="small" - button-text-classes="gl-font-sm!" - @click="onToggleDrawer" - >{{ $options.i18n.syntaxOptionsLabel }} - </gl-button> - <markdown-drawer - ref="markdownDrawer" - :document-path="$options.SYNTAX_OPTIONS_DOCUMENT" - /> - </template> + <section class="gl-p-5 gl-bg-gray-10 gl-border-b"> + <div class="search-page-form gl-lg-display-flex gl-flex-direction-column"> + <div class="gl-lg-display-flex gl-flex-direction-row gl-align-items-flex-end"> + <div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2"> + <div + class="gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-mb-0 gl-md-mb-4" + > + <label class="gl-mb-1 gl-md-pb-2">{{ $options.i18n.searchLabel }}</label> + <template v-if="showSyntaxOptions"> + <gl-button + category="tertiary" + variant="link" + size="small" + button-text-classes="gl-font-sm!" + @click="onToggleDrawer" + >{{ $options.i18n.syntaxOptionsLabel }} + </gl-button> + <markdown-drawer + ref="markdownDrawer" + :document-path="$options.SYNTAX_OPTIONS_DOCUMENT" + /> + </template> + </div> + <gl-search-box-by-click + id="dashboard_search" + v-model="search" + name="search" + :placeholder="$options.i18n.searchPlaceholder" + @submit="applyQuery" + /> + </div> + <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-3"> + <label class="gl-display-block gl-mb-1 gl-md-pb-2">{{ + $options.i18n.groupFieldLabel + }}</label> + <group-filter :initial-data="groupInitialJson" /> + </div> + <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-ml-3"> + <label class="gl-display-block gl-mb-1 gl-md-pb-2">{{ + $options.i18n.projectFieldLabel + }}</label> + <project-filter :initial-data="projectInitialJson" /> </div> - <gl-search-box-by-click - id="dashboard_search" - v-model="search" - name="search" - :placeholder="$options.i18n.searchPlaceholder" - @submit="applyQuery" - /> - </div> - <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-3"> - <label class="gl-display-block">{{ $options.i18n.groupFieldLabel }}</label> - <group-filter :initial-data="groupInitialJson" /> - </div> - <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-ml-3"> - <label class="gl-display-block">{{ $options.i18n.projectFieldLabel }}</label> - <project-filter :initial-data="projectInitialJson" /> </div> </div> - <hr class="gl-mt-5 gl-mb-0 gl-border-gray-100" /> </section> </template> diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5bca5a91ac5..ff888cf9d72 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -33,7 +33,6 @@ class ApplicationController < ActionController::Base before_action :check_password_expiration, if: :html_request? before_action :ldap_security_check before_action :default_headers - before_action :default_cache_headers before_action :add_gon_variables, if: :html_request? before_action :configure_permitted_parameters, if: :devise_controller? before_action :require_email, unless: :devise_controller? @@ -316,10 +315,6 @@ class ApplicationController < ActionController::Base headers['X-Content-Type-Options'] = 'nosniff' end - def default_cache_headers - headers['Pragma'] = 'no-cache' # HTTP 1.0 compatibility - end - def stream_csv_headers(csv_filename) no_cache_headers stream_headers diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 308da018a42..e53d0bc65a0 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -9,7 +9,6 @@ module UploadsActions included do prepend_before_action :set_request_format_from_path_extension - skip_before_action :default_cache_headers, only: :show rescue_from FileUploader::InvalidSecret, with: :render_404 end diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 70d9b524e4d..5db7609e07a 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -3,8 +3,6 @@ class Projects::AvatarsController < Projects::ApplicationController include SendsBlob - skip_before_action :default_cache_headers, only: :show - before_action :authorize_admin_project!, only: [:destroy] feature_category :projects diff --git a/app/controllers/projects/design_management/designs/raw_images_controller.rb b/app/controllers/projects/design_management/designs/raw_images_controller.rb index beb7e9d294b..ea406d2f2ef 100644 --- a/app/controllers/projects/design_management/designs/raw_images_controller.rb +++ b/app/controllers/projects/design_management/designs/raw_images_controller.rb @@ -7,8 +7,6 @@ module Projects class RawImagesController < Projects::DesignManagement::DesignsController include SendsBlob - skip_before_action :default_cache_headers, only: :show - def show blob = design_repository.blob_at(ref, design.full_path) diff --git a/app/controllers/projects/design_management/designs/resized_image_controller.rb b/app/controllers/projects/design_management/designs/resized_image_controller.rb index 6bf304419e1..a09d8a73892 100644 --- a/app/controllers/projects/design_management/designs/resized_image_controller.rb +++ b/app/controllers/projects/design_management/designs/resized_image_controller.rb @@ -10,8 +10,6 @@ module Projects before_action :validate_size! before_action :validate_sha! - skip_before_action :default_cache_headers, only: :show - def show relation = design.actions relation = relation.up_to_version(version) if version diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 895a9a00624..79b5990abba 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -6,8 +6,6 @@ class Projects::RawController < Projects::ApplicationController include SendsBlob include StaticObjectExternalStorage - skip_before_action :default_cache_headers, only: :show - prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:blob) } before_action :assign_ref_vars diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index e688720ce6a..80bc92c0b69 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -8,8 +8,6 @@ class Projects::RepositoriesController < Projects::ApplicationController prepend_before_action(only: [:archive]) { authenticate_sessionless_user!(:archive) } - skip_before_action :default_cache_headers, only: :archive - # Authorize before_action :check_archive_rate_limiting!, only: :archive before_action :require_non_empty_project, except: :create diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 9df86e2c702..d1f846ae55c 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -24,7 +24,6 @@ class SearchController < ApplicationController before_action :block_anonymous_global_searches, :check_scope_global_search_enabled, except: :opensearch skip_before_action :authenticate_user! - skip_before_action :default_cache_headers, only: [:count, :autocomplete] requires_cross_project_access if: -> do search_term_present = params[:search].present? || params[:term].present? diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb index a5a539eae75..4af9b644362 100644 --- a/app/models/alert_management/alert.rb +++ b/app/models/alert_management/alert.rb @@ -25,8 +25,9 @@ module AlertManagement has_many :assignees, through: :alert_assignees has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent - has_many :ordered_notes, -> { fresh }, as: :noteable, class_name: 'Note' - has_many :user_mentions, class_name: 'AlertManagement::AlertUserMention', foreign_key: :alert_management_alert_id + has_many :ordered_notes, -> { fresh }, as: :noteable, class_name: 'Note', inverse_of: :noteable + has_many :user_mentions, class_name: 'AlertManagement::AlertUserMention', foreign_key: :alert_management_alert_id, + inverse_of: :alert has_many :metric_images, class_name: '::AlertManagement::MetricImage' has_internal_id :iid, scope: :project diff --git a/app/models/alert_management/alert_assignee.rb b/app/models/alert_management/alert_assignee.rb index c74b2699182..27e720c3262 100644 --- a/app/models/alert_management/alert_assignee.rb +++ b/app/models/alert_management/alert_assignee.rb @@ -3,7 +3,7 @@ module AlertManagement class AlertAssignee < ApplicationRecord belongs_to :alert, inverse_of: :alert_assignees - belongs_to :assignee, class_name: 'User', foreign_key: :user_id + belongs_to :assignee, class_name: 'User', foreign_key: :user_id, inverse_of: :alert_assignees validates :alert, presence: true validates :assignee, presence: true, uniqueness: { scope: :alert_id } diff --git a/app/models/alert_management/alert_user_mention.rb b/app/models/alert_management/alert_user_mention.rb index d36aa80ee05..1ab71127677 100644 --- a/app/models/alert_management/alert_user_mention.rb +++ b/app/models/alert_management/alert_user_mention.rb @@ -2,7 +2,10 @@ module AlertManagement class AlertUserMention < UserMention - belongs_to :alert_management_alert, class_name: '::AlertManagement::Alert' + belongs_to :alert, class_name: '::AlertManagement::Alert', + foreign_key: :alert_management_alert_id, + inverse_of: :user_mentions + belongs_to :note end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index c80ff359a69..7219374fe2a 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -30,11 +30,13 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord add_authentication_token_field :static_objects_external_storage_auth_token, encrypted: :required add_authentication_token_field :error_tracking_access_token, encrypted: :required - belongs_to :self_monitoring_project, class_name: "Project", foreign_key: 'instance_administration_project_id' + belongs_to :self_monitoring_project, class_name: "Project", foreign_key: :instance_administration_project_id, + inverse_of: :application_setting belongs_to :push_rule alias_attribute :self_monitoring_project_id, :instance_administration_project_id - belongs_to :instance_group, class_name: "Group", foreign_key: 'instance_administrators_group_id' + belongs_to :instance_group, class_name: "Group", foreign_key: :instance_administrators_group_id, + inverse_of: :application_setting alias_attribute :instance_group_id, :instance_administrators_group_id alias_attribute :instance_administrators_group, :instance_group alias_attribute :housekeeping_optimize_repository_period, :housekeeping_incremental_repack_period diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 3312216932b..163e741d990 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -21,7 +21,7 @@ class AuditEvent < ApplicationRecord serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize - belongs_to :user, foreign_key: :author_id + belongs_to :user, foreign_key: :author_id, inverse_of: :audit_events validates :author_id, presence: true validates :entity_id, presence: true diff --git a/app/models/board.rb b/app/models/board.rb index 2181b2f0545..702ae0cc9f5 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -6,8 +6,8 @@ class Board < ApplicationRecord belongs_to :group belongs_to :project - has_many :lists, -> { ordered }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent - has_many :destroyable_lists, -> { destroyable.ordered }, class_name: "List" + has_many :lists, -> { ordered }, dependent: :delete_all, inverse_of: :board # rubocop:disable Cop/ActiveRecordDependent + has_many :destroyable_lists, -> { destroyable.ordered }, class_name: "List", inverse_of: :board validates :name, presence: true validates :project, presence: true, if: :project_needed? diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index 66e6d92f2ed..887c39dfd58 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -26,10 +26,11 @@ class BulkImports::Entity < ApplicationRecord belongs_to :parent, class_name: 'BulkImports::Entity', optional: true belongs_to :project, optional: true - belongs_to :group, foreign_key: :namespace_id, optional: true + belongs_to :group, foreign_key: :namespace_id, optional: true, inverse_of: :bulk_import_entities has_many :trackers, class_name: 'BulkImports::Tracker', + inverse_of: :entity, foreign_key: :bulk_import_entity_id has_many :failures, diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb index b04ef1cb7ae..701f1a9e49e 100644 --- a/app/models/bulk_imports/tracker.rb +++ b/app/models/bulk_imports/tracker.rb @@ -7,6 +7,7 @@ class BulkImports::Tracker < ApplicationRecord belongs_to :entity, class_name: 'BulkImports::Entity', + inverse_of: :trackers, foreign_key: :bulk_import_entity_id, optional: false diff --git a/app/models/group.rb b/app/models/group.rb index 7e09280dfff..38eb11a0d74 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -110,7 +110,10 @@ class Group < Namespace has_one :import_state, class_name: 'GroupImportState', inverse_of: :group + has_many :application_setting, foreign_key: :instance_administrators_group_id, inverse_of: :instance_group + has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :group + has_many :bulk_import_entities, class_name: 'BulkImports::Entity', foreign_key: :namespace_id, inverse_of: :group has_many :group_deploy_keys_groups, inverse_of: :group has_many :group_deploy_keys, through: :group_deploy_keys_groups diff --git a/app/models/project.rb b/app/models/project.rb index cd35332d7df..f363f189178 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -167,6 +167,8 @@ class Project < ApplicationRecord has_one :last_event, -> { order 'events.created_at DESC' }, class_name: 'Event' has_many :boards + has_many :application_setting, inverse_of: :self_monitoring_project + def self.integration_association_name(name) "#{name}_integration" end diff --git a/app/models/user.rb b/app/models/user.rb index 9a9530c4d3a..8ff69d3f67f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -228,7 +228,9 @@ class User < ApplicationRecord has_many :notification_settings has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :triggers, class_name: 'Ci::Trigger', foreign_key: :owner_id + has_many :audit_events, foreign_key: :author_id, inverse_of: :user + has_many :alert_assignees, class_name: '::AlertManagement::AlertAssignee', inverse_of: :assignee has_many :issue_assignees, inverse_of: :assignee has_many :merge_request_assignees, inverse_of: :assignee, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :merge_request_reviewers, inverse_of: :reviewer, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/services/packages/mark_package_for_destruction_service.rb b/app/services/packages/mark_package_for_destruction_service.rb index 3417febe79a..8ccc242ae36 100644 --- a/app/services/packages/mark_package_for_destruction_service.rb +++ b/app/services/packages/mark_package_for_destruction_service.rb @@ -13,7 +13,8 @@ module Packages package.sync_maven_metadata(current_user) service_response_success('Package was successfully marked as pending destruction') - rescue StandardError + rescue StandardError => e + track_exception(e) service_response_error('Failed to mark the package as pending destruction', 400) end @@ -30,5 +31,13 @@ module Packages def user_can_delete_package? can?(current_user, :destroy_package, package.project) end + + def track_exception(error) + Gitlab::ErrorTracking.track_exception( + error, + project_id: package.project_id, + package_id: package.id + ) + end end end diff --git a/app/services/packages/mark_packages_for_destruction_service.rb b/app/services/packages/mark_packages_for_destruction_service.rb index 023392cf2d9..ade9ad2c974 100644 --- a/app/services/packages/mark_packages_for_destruction_service.rb +++ b/app/services/packages/mark_packages_for_destruction_service.rb @@ -31,13 +31,15 @@ module Packages def execute(batch_size: BATCH_SIZE) no_access = false min_batch_size = [batch_size, BATCH_SIZE].min + package_ids = [] @packages.each_batch(of: min_batch_size) do |batched_packages| loaded_packages = batched_packages.including_project_route.to_a + package_ids = loaded_packages.map(&:id) break no_access = true unless can_destroy_packages?(loaded_packages) - ::Packages::Package.id_in(loaded_packages.map(&:id)) + ::Packages::Package.id_in(package_ids) .update_all(status: :pending_destruction) sync_maven_metadata(loaded_packages) @@ -47,7 +49,8 @@ module Packages return UNAUTHORIZED_RESPONSE if no_access SUCCESS_RESPONSE - rescue StandardError + rescue StandardError => e + track_exception(e, package_ids) ERROR_RESPONSE end @@ -75,5 +78,9 @@ module Packages can?(@current_user, :destroy_package, package) end end + + def track_exception(error, package_ids) + Gitlab::ErrorTracking.track_exception(error, package_ids: package_ids) + end end end diff --git a/app/views/search/_results_list.html.haml b/app/views/search/_results_list.html.haml index 7a57b5cc0fc..ce4dd02b41d 100644 --- a/app/views/search/_results_list.html.haml +++ b/app/views/search/_results_list.html.haml @@ -5,16 +5,17 @@ - elsif @search_objects.to_a.empty? = render partial: "search/results/empty" - else - - if @scope == 'commits' - %ul.content-list.commit-list - = render partial: "search/results/commit", collection: @search_objects - - else - .search-results.js-search-results - - if @scope == 'projects' - .term - = render 'shared/projects/list', projects: @search_objects, pipeline_status: false - - else - = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects + .gl-md-pl-5 + - if @scope == 'commits' + %ul.content-list.commit-list + = render partial: "search/results/commit", collection: @search_objects + - else + .search-results.js-search-results + - if @scope == 'projects' + .term + = render 'shared/projects/list', projects: @search_objects, pipeline_status: false + - else + = render_if_exists partial: "search/results/#{@scope.singularize}", collection: @search_objects - - if @scope != 'projects' - = paginate_collection(@search_objects) + - if @scope != 'projects' + = paginate_collection(@search_objects) diff --git a/app/views/search/_results_status.html.haml b/app/views/search/_results_status.html.haml index 27405631360..2321abb31df 100644 --- a/app/views/search/_results_status.html.haml +++ b/app/views/search/_results_status.html.haml @@ -1,25 +1,25 @@ - return unless @search_service_presenter.show_results_status? - -.search-results-status - .gl-display-flex.gl-flex-direction-column - .gl-p-5.gl-display-flex - .gl-md-display-flex.gl-text-left.gl-align-items-center.gl-flex-grow-1.gl-white-space-nowrap.gl-max-w-full - - unless @search_service_presenter.without_count? - = search_entries_info(@search_objects, @scope, @search_term) - - unless @search_service_presenter.show_snippets? - - if @project - - link_to_project = link_to(@project.full_name, @project, class: 'ml-md-1 gl-text-truncate search-wrap-f-md-down') - - if @scope == 'blobs' - = _("in") - .mx-md-1 - #js-blob-ref-switcher{ data: { "project-id" => @project.id, "ref" => repository_ref(@project), "field-name": "repository_ref" } } - = s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project } - - else - = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project } - - elsif @group - - link_to_group = link_to(@group.name, @group, class: 'ml-md-1') - = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group } - - if @search_service_presenter.show_sort_dropdown? - .gl-md-display-flex.gl-flex-direction-column - #js-search-sort{ data: { "search-sort-options" => search_sort_options.to_json } } - %hr.gl-mb-5.gl-mt-0.gl-border-gray-100.gl-w-full +.gl-md-pl-5 + .search-results-status + .gl-display-flex.gl-flex-direction-column + .gl-p-5.gl-display-flex + .gl-md-display-flex.gl-text-left.gl-align-items-center.gl-flex-grow-1.gl-white-space-nowrap.gl-max-w-full + - unless @search_service_presenter.without_count? + = search_entries_info(@search_objects, @scope, @search_term) + - unless @search_service_presenter.show_snippets? + - if @project + - link_to_project = link_to(@project.full_name, @project, class: 'ml-md-1 gl-text-truncate search-wrap-f-md-down') + - if @scope == 'blobs' + = _("in") + .mx-md-1 + #js-blob-ref-switcher{ data: { "project-id" => @project.id, "ref" => repository_ref(@project), "field-name": "repository_ref" } } + = s_('SearchCodeResults|of %{link_to_project}').html_safe % { link_to_project: link_to_project } + - else + = _("in project %{link_to_project}").html_safe % { link_to_project: link_to_project } + - elsif @group + - link_to_group = link_to(@group.name, @group, class: 'ml-md-1') + = _("in group %{link_to_group}").html_safe % { link_to_group: link_to_group } + - if @search_service_presenter.show_sort_dropdown? + .gl-md-display-flex.gl-flex-direction-column + #js-search-sort{ data: { "search-sort-options" => search_sort_options.to_json } } + %hr.gl-mb-5.gl-mt-0.gl-border-gray-100.gl-w-full diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml index 04103794e60..808ccfbd716 100644 --- a/app/views/search/show.html.haml +++ b/app/views/search/show.html.haml @@ -19,7 +19,6 @@ %h1.page-title.gl-font-size-h-display.gl-mr-5= _('Search') = render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' } -.gl-mt-3 - #js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "elasticsearch-enabled": @search_service_presenter.advanced_search_enabled?.to_s, "default-branch-name": @project&.default_branch } } +#js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "elasticsearch-enabled": @search_service_presenter.advanced_search_enabled?.to_s, "default-branch-name": @project&.default_branch } } - if @search_term = render 'search/results' diff --git a/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb b/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb new file mode 100644 index 00000000000..583a9cd31f7 --- /dev/null +++ b/db/post_migrate/20230223082752_schedule_fk_validation_for_p_ci_builds_metadata_partitions_and_ci_builds.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class ScheduleFkValidationForPCiBuildsMetadataPartitionsAndCiBuilds < Gitlab::Database::Migration[2.1] + TABLE_NAME = :p_ci_builds_metadata + FK_NAME = :fk_e20479742e_p + + def up + prepare_partitioned_async_foreign_key_validation TABLE_NAME, name: FK_NAME + end + + def down + unprepare_partitioned_async_foreign_key_validation TABLE_NAME, name: FK_NAME + end +end diff --git a/db/schema_migrations/20230223082752 b/db/schema_migrations/20230223082752 new file mode 100644 index 00000000000..83789c7ffe8 --- /dev/null +++ b/db/schema_migrations/20230223082752 @@ -0,0 +1 @@ +53f1003eeb8f961b37d90c73a71f75683077b9bcd0e495395033998530a363bd
\ No newline at end of file diff --git a/doc/.vale/gitlab/HeadingDepth.yml b/doc/.vale/gitlab/HeadingDepth.yml index 5bbe667481c..f29ec0e4ac7 100644 --- a/doc/.vale/gitlab/HeadingDepth.yml +++ b/doc/.vale/gitlab/HeadingDepth.yml @@ -6,7 +6,7 @@ # For a list of all options, see https://vale.sh/docs/topics/styles/ extends: existence message: "Refactor the section or page to avoid headings greater than H5." -link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#headings-in-markdown +link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html#heading-levels-in-markdown level: suggestion scope: raw raw: diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index a6ac3b0e3b8..8277ffe96ea 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -617,6 +617,7 @@ Nurtch NVMe nyc OAuth +OCP Octokit offboarded offboarding @@ -625,6 +626,7 @@ OIDs OKRs OKRs Okta +OLM OmniAuth onboarding OpenID @@ -668,6 +670,7 @@ Pipfile Pipfiles Piwik plaintext +podman Poedit polyfill polyfills diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md index 6e97ffc3b47..5172a9613ee 100644 --- a/doc/administration/reply_by_email.md +++ b/doc/administration/reply_by_email.md @@ -1,6 +1,6 @@ --- -stage: Plan -group: Certify +stage: Monitor +group: Respond info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/api/group_protected_branches.md b/doc/api/group_protected_branches.md new file mode 100644 index 00000000000..db648f6870e --- /dev/null +++ b/doc/api/group_protected_branches.md @@ -0,0 +1,469 @@ +--- +stage: Create +group: Source Code +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Group-level protected branches API **(PREMIUM SELF)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/110603) in GitLab 15.9 [with a flag](../administration/feature_flags.md) named `group_protected_branches`. Disabled by default. + +FLAG: +On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `group_protected_branches`. +On GitLab.com, this feature is not available. + +## Valid access levels + +The access levels are defined in the `ProtectedRefAccess.allowed_access_levels` method. +These levels are recognized: + +```plaintext +0 => No access +30 => Developer access +40 => Maintainer access +60 => Admin access +``` + +## List protected branches + +Gets a list of protected branches from a group. If a wildcard is set, it is returned instead +of the exact name of the branches that match that wildcard. + +```plaintext +GET /groups/:id/protected_branches +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) owned by the authenticated user. | +| `search` | string | no | Name or part of the name of protected branches to be searched for. | + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/5/protected_branches" +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "master", + "push_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": 1234, + "access_level_description": "Maintainers" + } + ], + "merge_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": 1234, + "access_level_description": "Maintainers" + } + ], + "allow_force_push":false, + "code_owner_approval_required": false + }, + { + "id": 1, + "name": "release/*", + "push_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": null, + "access_level_description": "Maintainers" + } + ], + "merge_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": 1234, + "access_level_description": "Maintainers" + } + ], + "allow_force_push":false, + "code_owner_approval_required": false + }, + ... +] +``` + +## Get a single protected branch or wildcard protected branch + +Gets a single protected branch or wildcard protected branch. + +```plaintext +GET /groups/:id/protected_branches/:name +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) owned by the authenticated user. | +| `name` | string | yes | The name of the branch or wildcard. | + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/5/protected_branches/master" +``` + +Example response: + +```json +{ + "id": 1, + "name": "master", + "push_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": null, + "access_level_description": "Maintainers" + } + ], + "merge_access_levels": [ + { + "id": 1, + "access_level": null, + "user_id": null, + "group_id": 1234, + "access_level_description": "Example Merge Group" + } + ], + "allow_force_push":false, + "code_owner_approval_required": false +} +``` + +## Protect repository branches + +Protects a single repository branch using a wildcard protected branch. + +```plaintext +POST /groups/:id/protected_branches +``` + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40" +``` + +| Attribute | Type | Required | Description | +| -------------------------------------------- | ---- | -------- | ----------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) owned by the authenticated user. | +| `name` | string | yes | The name of the branch or wildcard. | +| `allow_force_push` | boolean | no | Allow all users with push access to force push. Default: `false`. | +| `allowed_to_merge` | array | no | Array of access levels allowed to merge, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. | +| `allowed_to_push` | array | no | Array of access levels allowed to push, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. | +| `allowed_to_unprotect` | array | no | Array of access levels allowed to unprotect, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. | +| `code_owner_approval_required` | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). Default: `false`. | +| `merge_access_level` | integer | no | Access levels allowed to merge. Defaults: `40`, Maintainer role. | +| `push_access_level` | integer | no | Access levels allowed to push. Defaults: `40`, Maintainer role. | +| `unprotect_access_level` | integer | no | Access levels allowed to unprotect. Defaults: `40`, Maintainer role. | + +Example response: + +```json +{ + "id": 1, + "name": "*-stable", + "push_access_levels": [ + { + "id": 1, + "access_level": 30, + "user_id": null, + "group_id": null, + "access_level_description": "Developers + Maintainers" + } + ], + "merge_access_levels": [ + { + "id": 1, + "access_level": 30, + "user_id": null, + "group_id": null, + "access_level_description": "Developers + Maintainers" + } + ], + "unprotect_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": null, + "access_level_description": "Maintainers" + } + ], + "allow_force_push":false, + "code_owner_approval_required": false +} +``` + +### Example with user / group level access + +Elements in the `allowed_to_push` / `allowed_to_merge` / `allowed_to_unprotect` array should take the +form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. Each user must have +access to the project and each group must +[have this project shared](../user/project/members/share_project_with_groups.md). These access levels +allow [more granular control over protected branch access](../user/project/protected_branches.md). + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1" +``` + +Example response: + +```json +{ + "id": 1, + "name": "*-stable", + "push_access_levels": [ + { + "id": 1, + "access_level": null, + "user_id": 1, + "group_id": null, + "access_level_description": "Administrator" + } + ], + "merge_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": null, + "access_level_description": "Maintainers" + } + ], + "unprotect_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": null, + "access_level_description": "Maintainers" + } + ], + "allow_force_push":false, + "code_owner_approval_required": false +} +``` + +### Example with allow to push and allow to merge access + +Example request: + +```shell +curl --request POST \ + --header "PRIVATE-TOKEN: <your_access_token>" \ + --header "Content-Type: application/json" \ + --data '{ + "name": "master", + "allowed_to_push": [{"access_level": 30}], + "allowed_to_merge": [{ + "access_level": 30 + },{ + "access_level": 40 + } + ]}' + "https://gitlab.example.com/api/v4/groups/5/protected_branches" +``` + +Example response: + +```json +{ + "id": 5, + "name": "master", + "push_access_levels": [ + { + "id": 1, + "access_level": 30, + "access_level_description": "Developers + Maintainers", + "user_id": null, + "group_id": null + } + ], + "merge_access_levels": [ + { + "id": 1, + "access_level": 30, + "access_level_description": "Developers + Maintainers", + "user_id": null, + "group_id": null + }, + { + "id": 2, + "access_level": 40, + "access_level_description": "Maintainers", + "user_id": null, + "group_id": null + } + ], + "unprotect_access_levels": [ + { + "id": 1, + "access_level": 40, + "access_level_description": "Maintainers", + "user_id": null, + "group_id": null + } + ], + "allow_force_push":false, + "code_owner_approval_required": false +} +``` + +## Unprotect repository branches + +Unprotects the given protected branch or wildcard protected branch. + +```plaintext +DELETE /groups/:id/protected_branches/:name +``` + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/5/protected_branches/*-stable" +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) owned by the authenticated user. | +| `name` | string | yes | The name of the branch. | + +Example response: + +```json +{ + "name": "master", + "push_access_levels": [ + { + "id": 12, + "access_level": 40, + "access_level_description": "Maintainers", + "user_id": null, + "group_id": null + } + ] +} +``` + +## Update a protected branch + +Updates a protected branch. + +```plaintext +PATCH /groups/:id/protected_branches/:name +``` + +```shell +curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true" +``` + +| Attribute | Type | Required | Description | +| -------------------------------------------- | ---- | -------- | ----------- | +| `id` | integer or string | yes | The ID or [URL-encoded path of the group](rest/index.md#namespaced-path-encoding) owned by the authenticated user. | +| `name` | string | yes | The name of the branch. | +| `allow_force_push` | boolean | no | When enabled, members who can push to this branch can also force push. | +| `allowed_to_push` | array | no | Array of push access levels, with each described by a hash. | +| `allowed_to_merge` | array | no | Array of merge access levels, with each described by a hash. | +| `allowed_to_unprotect` | array | no | Array of unprotect access levels, with each described by a hash. | +| `code_owner_approval_required` | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). Default: `false`. | + +Elements in the `allowed_to_push`, `allowed_to_merge` and `allowed_to_unprotect` arrays should: + +- Be one of `user_id`, `group_id`, or `access_level`. +- Take the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. + +To update: + +- `user_id`: Ensure the updated user has access to the project. You must also pass the + `id` of the `access_level` in the respective hash. +- `group_id`: Ensure the updated group [has this project shared](../user/project/members/share_project_with_groups.md). + You must also pass the `id` of the `access_level` in the respective hash. + +To delete: + +- You must pass `_destroy` set to `true`. See the following examples. + +### Example: create a `push_access_level` record + +```shell +curl --header 'Content-Type: application/json' --request PATCH \ + --data '{"allowed_to_push": [{access_level: 40}]}' \ + --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/master" +``` + +Example response: + +```json +{ + "name": "master", + "push_access_levels": [ + { + "id": 12, + "access_level": 40, + "access_level_description": "Maintainers", + "user_id": null, + "group_id": null + } + ] +} +``` + +### Example: update a `push_access_level` record + +```shell +curl --header 'Content-Type: application/json' --request PATCH \ + --data '{"allowed_to_push": [{"id": 12, "access_level": 0}]' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/master" +``` + +Example response: + +```json +{ + "name": "master", + "push_access_levels": [ + { + "id": 12, + "access_level": 0, + "access_level_description": "No One", + "user_id": null, + "group_id": null + } + ] +} +``` + +### Example: delete a `push_access_level` record + +```shell +curl --header 'Content-Type: application/json' --request PATCH \ + --data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_branches/master" +``` + +Example response: + +```json +{ + "name": "master", + "push_access_levels": [] +} +``` diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index 649301d6e5e..6c3bd63bbad 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -208,16 +208,16 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla | Attribute | Type | Required | Description | | -------------------------------------------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | -| `name` | string | yes | The name of the branch or wildcard | -| `push_access_level` | integer | no | Access levels allowed to push (defaults: `40`, Maintainer role) | -| `merge_access_level` | integer | no | Access levels allowed to merge (defaults: `40`, Maintainer role) | -| `unprotect_access_level` | integer | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) | -| `allow_force_push` | boolean | no | Allow all users with push access to force push. (default: `false`) | -| `allowed_to_push` **(PREMIUM)** | array | no | Array of access levels allowed to push, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}` | -| `allowed_to_merge` **(PREMIUM)** | array | no | Array of access levels allowed to merge, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}` | -| `allowed_to_unprotect` **(PREMIUM)** | array | no | Array of access levels allowed to unprotect, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}` | -| `code_owner_approval_required` **(PREMIUM)** | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false) | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user. +| `name` | string | yes | The name of the branch or wildcard. +| `allow_force_push` | boolean | no | Allow all users with push access to force push. (default: `false`) +| `allowed_to_merge` **(PREMIUM)** | array | no | Array of access levels allowed to merge, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. +| `allowed_to_push` **(PREMIUM)** | array | no | Array of access levels allowed to push, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. +| `allowed_to_unprotect` **(PREMIUM)** | array | no | Array of access levels allowed to unprotect, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. The access level `No access` is not available for this field. | +| `code_owner_approval_required` **(PREMIUM)** | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false) +| `merge_access_level` | integer | no | Access levels allowed to merge. (defaults: `40`, Maintainer role) +| `push_access_level` | integer | no | Access levels allowed to push. (defaults: `40`, Maintainer role) +| `unprotect_access_level` | integer | no | Access levels allowed to unprotect. (defaults: `40`, Maintainer role) Example response: @@ -297,8 +297,6 @@ Example response: Elements in the `allowed_to_push` / `allowed_to_merge` / `allowed_to_unprotect` array should take the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). These access levels allow [more granular control over protected branch access](../user/project/protected_branches.md). -For `allowed_to_unprotect` the access level `No access` is unavailable. - ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=1" ``` @@ -371,7 +369,7 @@ Example response: "name": "master", "push_access_levels": [ { - "id": 1, + "id": 1, "access_level": 30, "access_level_description": "Developers + Maintainers", "user_id": null, @@ -380,14 +378,14 @@ Example response: ], "merge_access_levels": [ { - "id": 1, + "id": 1, "access_level": 30, "access_level_description": "Developers + Maintainers", "user_id": null, "group_id": null }, { - "id": 2, + "id": 2, "access_level": 40, "access_level_description": "Maintainers", "user_id": null, @@ -396,7 +394,7 @@ Example response: ], "unprotect_access_levels": [ { - "id": 1, + "id": 1, "access_level": 40, "access_level_description": "Maintainers", "user_id": null, @@ -439,22 +437,20 @@ PATCH /projects/:id/protected_branches/:name curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true" ``` -| Attribute | Type | Required | Description | +| Attribute | Type | Required | Description | | -------------------------------------------- | ---- | -------- | ----------- | -| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user | -| `name` | string | yes | The name of the branch | -| `allow_force_push` | boolean | no | When enabled, members who can push to this branch can also force push. | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding) owned by the authenticated user. +| `name` | string | yes | The name of the branch. +| `allow_force_push` | boolean | no | When enabled, members who can push to this branch can also force push. +| `allowed_to_push` **(PREMIUM)** | array | no | Array of push access levels, with each described by a hash. +| `allowed_to_merge` **(PREMIUM)** | array | no | Array of merge access levels, with each described by a hash. +| `allowed_to_unprotect` **(PREMIUM)** | array | no | Array of unprotect access levels, with each described by a hash. The access level `No access` is not available for this field. | `code_owner_approval_required` **(PREMIUM)** | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). Defaults to `false`. | -| `allowed_to_push` **(PREMIUM)** | array | no | Array of push access levels, with each described by a hash. | -| `allowed_to_merge` **(PREMIUM)** | array | no | Array of merge access levels, with each described by a hash. | -| `allowed_to_unprotect` **(PREMIUM)** | array | no | Array of unprotect access levels, with each described by a hash. | Elements in the `allowed_to_push`, `allowed_to_merge` and `allowed_to_unprotect` arrays should be one of `user_id`, `group_id` or `access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. -For `allowed_to_unprotect` the access level `No access` is unavailable. - To update: - `user_id`: Ensure the updated user has access to the project. You must also pass the diff --git a/doc/ci/test_cases/index.md b/doc/ci/test_cases/index.md index 4088e5e82c6..84f0d8074a1 100644 --- a/doc/ci/test_cases/index.md +++ b/doc/ci/test_cases/index.md @@ -1,6 +1,6 @@ --- stage: Plan -group: Certify +group: Product Planning info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Test cases in GitLab can help your teams create testing scenarios in their existing development platform. type: reference diff --git a/doc/development/fips_compliance.md b/doc/development/fips_compliance.md index 147ff5fa6e9..1f9abef1f44 100644 --- a/doc/development/fips_compliance.md +++ b/doc/development/fips_compliance.md @@ -69,7 +69,6 @@ listed here that also do not work properly in FIPS mode: when operating in FIPS-compliant mode. - Advanced Search is currently not included in FIPS mode. It must not be enabled to be FIPS-compliant. - [Gravatar or Libravatar-based profile images](../administration/libravatar.md) are not FIPS-compliant. -- [Personal Access Tokens](../user/profile/personal_access_tokens.md) are not available for use or creation. Additionally, these package repositories are disabled in FIPS mode: diff --git a/doc/install/installation.md b/doc/install/installation.md index be8667d5715..e2a8cd1ad23 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -11,8 +11,7 @@ using the source files. To set up a **development installation** or for many other installation options, see the [main installation page](index.md). It was created for and tested on **Debian/Ubuntu** operating systems. Read [requirements.md](requirements.md) for hardware and operating system requirements. -If you want to install on RHEL/CentOS, we recommend using the -[Omnibus packages](https://about.gitlab.com/install/). +If you want to install on RHEL/CentOS, you should use the [Omnibus packages](https://about.gitlab.com/install/). This guide is long because it covers many cases and includes all commands you need, this is [one of the few installation scripts that actually work out of the box](https://twitter.com/robinvdvleuten/status/424163226532986880). diff --git a/doc/user/crm/index.md b/doc/user/crm/index.md index ebacda506b4..54e87118361 100644 --- a/doc/user/crm/index.md +++ b/doc/user/crm/index.md @@ -1,6 +1,6 @@ --- -stage: Plan -group: Certify +stage: Monitor +group: Respond info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/group/settings/group_access_tokens.md b/doc/user/group/settings/group_access_tokens.md index cd50c209b0d..dc499b96f3c 100644 --- a/doc/user/group/settings/group_access_tokens.md +++ b/doc/user/group/settings/group_access_tokens.md @@ -48,9 +48,6 @@ You cannot use group access tokens to create other group, project, or personal a Group access tokens inherit the [default prefix setting](../../admin_area/settings/account_and_limit_settings.md#personal-access-token-prefix) configured for personal access tokens. -NOTE: -Group access tokens are not FIPS compliant and creation and use are disabled when [FIPS mode](../../../development/fips_compliance.md) is enabled. - ## Create a group access token using UI > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214045) in GitLab 14.7. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index aa292ca18a3..ca2fdeb98ae 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -511,7 +511,7 @@ To remove a custom role from a group member, use the [Group and Project Members and pass an empty `member_role_id` value. ```shell -curl --request PUT --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id": "", "access_level": 10}' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID" +curl --request DELETE --header "Content-Type: application/json" --header "Authorization: Bearer $YOUR_ACCESS_TOKEN" --data '{"member_role_id": "", "access_level": 10}' "https://example.gitlab.com/api/v4/groups/$GROUP_PATH/members/$GUEST_USER_ID" ``` Now the user is a regular Guest. diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 3826c602fb4..a2aad8a3e27 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -45,9 +45,6 @@ For examples of how you can use a personal access token to authenticate with the Alternately, GitLab administrators can use the API to create [impersonation tokens](../../api/rest/index.md#impersonation-tokens). Use impersonation tokens to automate authentication as a specific user. -NOTE: -Personal access tokens are not FIPS compliant and creation and use are disabled when [FIPS mode](../../development/fips_compliance.md) is enabled. - ## Create a personal access token > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/348660) in GitLab 15.3, default expiration of 30 days is populated in the UI. diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md index 4b5a9dcecec..d385daa4fa1 100644 --- a/doc/user/project/requirements/index.md +++ b/doc/user/project/requirements/index.md @@ -1,7 +1,7 @@ --- type: reference, howto stage: Plan -group: Certify +group: Product Planning info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments --- diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md index 22297149561..ff9d7769681 100644 --- a/doc/user/project/service_desk.md +++ b/doc/user/project/service_desk.md @@ -207,15 +207,12 @@ you can customize the mailbox used by Service Desk. This allows you to have a separate email address for Service Desk by also configuring a [custom suffix](#configuring-a-custom-email-address-suffix) in project settings. -The `address` must include the `+%{key}` placeholder in the 'user' -portion of the address, before the `@`. The placeholder is used to identify the project -where the issue should be created. +Prerequisites: -NOTE: -When configuring a custom mailbox, the `service_desk_email` and `incoming_email` -configurations must always use separate mailboxes. It's important, because -emails picked from `service_desk_email` mailbox are processed by a different -worker and it would not recognize `incoming_email` emails. +- The `address` must include the `+%{key}` placeholder in the `user` portion of the address, + before the `@`. The placeholder is used to identify the project where the issue should be created. +- The `service_desk_email` and `incoming_email` configurations must always use separate mailboxes + to make sure Service Desk emails are processed correctly. To configure a custom mailbox for Service Desk with IMAP, add the following snippets to your configuration file in full: diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md index f9218a228ca..19db5032ea9 100644 --- a/doc/user/project/settings/project_access_tokens.md +++ b/doc/user/project/settings/project_access_tokens.md @@ -48,9 +48,6 @@ You cannot use project access tokens to create other group, project, or personal Project access tokens inherit the [default prefix setting](../../admin_area/settings/account_and_limit_settings.md#personal-access-token-prefix) configured for personal access tokens. -NOTE: -Project access tokens are not FIPS compliant and creation and use are disabled when [FIPS mode](../../../development/fips_compliance.md) is enabled. - ## Create a project access token > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89114) in GitLab 15.1, Owners can select Owner role for project access tokens. diff --git a/lib/banzai/filter/inline_observability_filter.rb b/lib/banzai/filter/inline_observability_filter.rb index 334c04f2b59..fdba12b4a0a 100644 --- a/lib/banzai/filter/inline_observability_filter.rb +++ b/lib/banzai/filter/inline_observability_filter.rb @@ -4,7 +4,7 @@ module Banzai module Filter class InlineObservabilityFilter < ::Banzai::Filter::InlineEmbedsFilter def call - return doc unless can_view_observability? + return doc unless Gitlab::Observability.enabled?(group) super end @@ -34,10 +34,6 @@ module Banzai private - def can_view_observability? - Feature.enabled?(:observability_group_tab, group) - end - def group context[:group] || context[:project]&.group end diff --git a/lib/banzai/filter/inline_observability_redactor_filter.rb b/lib/banzai/filter/inline_observability_redactor_filter.rb deleted file mode 100644 index 8cd1084e904..00000000000 --- a/lib/banzai/filter/inline_observability_redactor_filter.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -module Banzai - module Filter - class InlineObservabilityRedactorFilter < HTML::Pipeline::Filter - include Gitlab::Utils::StrongMemoize - - CSS_SELECTOR = '.js-render-observability' - - XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SELECTOR).freeze - EMBED_LIMIT = 100 - - def call - return doc if Gitlab::Utils.to_boolean(ENV.fetch('STANDALONE_OBSERVABILITY_UI', false)) - - nodes.each do |node| - group_id = group_ids_by_nodes[node] - user_has_access = group_id && user_access_by_group_id[group_id] - node.remove unless user_has_access - end - - doc - end - - private - - def user - context[:current_user] - end - - # Returns all observability embed placeholder nodes - # - # Removes any nodes beyond the first 100 - # - # @return [Nokogiri::XML::NodeSet] - def nodes - nodes = doc.xpath(XPATH) - nodes.drop(EMBED_LIMIT).each(&:remove) - nodes - end - strong_memoize_attr :nodes - - # Returns a mapping representing whether the current user has permission to access observability - # for group-ids linked in by the embed nodes - # - # @return [Hash<String, Boolean>] - def user_access_by_group_id - user_groups_from_nodes.each_with_object({}) do |group, user_access| - user_access[group.id] = Gitlab::Observability.allowed?(user, group, :read_observability) - end - end - strong_memoize_attr :user_access_by_group_id - - # Maps a node to the group_id linked by the node - # - # @return [Hash<Nokogiri::XML::Node, string>] - def group_ids_by_nodes - nodes.each_with_object({}) do |node, group_ids| - url = node.attribute('data-frame-url').to_s - next unless url - - group_id = Gitlab::Observability.group_id_from_url(url) - group_ids[node] = group_id if group_id - end - end - strong_memoize_attr :group_ids_by_nodes - - # Returns the list of groups linked in the embed nodes and readable by the user - # - # @return [ActiveRecord_Relation] - def user_groups_from_nodes - GroupsFinder.new(user, filter_group_ids: group_ids_by_nodes.values.uniq).execute - end - strong_memoize_attr :user_groups_from_nodes - end - end -end diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb index b9cf5b4fbdd..f8035698b9b 100644 --- a/lib/banzai/pipeline/post_process_pipeline.rb +++ b/lib/banzai/pipeline/post_process_pipeline.rb @@ -16,7 +16,6 @@ module Banzai [ Filter::ReferenceRedactorFilter, Filter::InlineMetricsRedactorFilter, - Filter::InlineObservabilityRedactorFilter, # UploadLinkFilter must come before RepositoryLinkFilter to # prevent unnecessary Gitaly calls from being made. Filter::UploadLinkFilter, diff --git a/lib/gitlab/no_cache_headers.rb b/lib/gitlab/no_cache_headers.rb index 2d03741480d..6afb108dcfd 100644 --- a/lib/gitlab/no_cache_headers.rb +++ b/lib/gitlab/no_cache_headers.rb @@ -4,7 +4,6 @@ module Gitlab module NoCacheHeaders DEFAULT_GITLAB_NO_CACHE_HEADERS = { 'Cache-Control' => "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store, no-cache", - 'Pragma' => 'no-cache', # HTTP 1.0 compatibility 'Expires' => 'Fri, 01 Jan 1990 00:00:00 GMT' }.freeze diff --git a/lib/gitlab/observability.rb b/lib/gitlab/observability.rb index 853eecc946f..47220e33189 100644 --- a/lib/gitlab/observability.rb +++ b/lib/gitlab/observability.rb @@ -19,6 +19,12 @@ module Gitlab 'https://observe.gitlab.com' end + def enabled?(group = nil) + return Feature.enabled?(:observability_group_tab, group) if group + + Feature.enabled?(:observability_group_tab) + end + def valid_observability_url?(url) uri = URI.parse(url) observability_uri = URI.parse(Gitlab::Observability.observability_url) @@ -31,13 +37,6 @@ module Gitlab false end - def group_id_from_url(url) - return unless valid_observability_url?(url) - - group_id = URI.parse(url).path.split('/')[1] - group_id.to_i unless group_id.to_i <= 0 - end - def allowed_for_action?(user, group, action) return false if action.nil? diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake index 0c6a78ccc77..8861e8574b3 100644 --- a/lib/tasks/gitlab/tw/codeowners.rake +++ b/lib/tasks/gitlab/tw/codeowners.rake @@ -58,7 +58,7 @@ namespace :tw do CodeOwnerRule.new('Pipeline Security', '@marcel.amirault'), CodeOwnerRule.new('Portfolio Management', '@msedlakjakubowski'), CodeOwnerRule.new('Product Analytics', '@lciutacu'), - CodeOwnerRule.new('Product Intelligence', '@dianalogan'), + CodeOwnerRule.new('Product Intelligence', '@lciutacu'), CodeOwnerRule.new('Product Planning', '@msedlakjakubowski'), CodeOwnerRule.new('Project Management', '@msedlakjakubowski'), CodeOwnerRule.new('Provision', '@fneill'), diff --git a/qa/qa/specs/features/api/3_create/repository/files_spec.rb b/qa/qa/specs/features/api/3_create/repository/files_spec.rb index 71bd03fab17..7e329371745 100644 --- a/qa/qa/specs/features/api/3_create/repository/files_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/files_spec.rb @@ -99,7 +99,6 @@ module QA # expect(response.headers[:cache_control]).to include("no-store") expect(response.headers[:cache_control]).to include("no-cache") - expect(response.headers[:pragma]).to eq("no-cache") expect(response.headers[:expires]).to eq("Fri, 01 Jan 1990 00:00:00 GMT") expect(response.headers[:content_disposition]).to include("attachment") expect(response.headers[:content_disposition]).not_to include("inline") diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index f1adb9020fa..35e374d3b7f 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe ApplicationController do +RSpec.describe ApplicationController, feature_category: :shared do include TermsHelper let(:user) { create(:user) } @@ -736,23 +736,11 @@ RSpec.describe ApplicationController do end end - context 'user not logged in' do - it 'sets the default headers' do - get :index - - expect(response.headers['Cache-Control']).to be_nil - expect(response.headers['Pragma']).to be_nil - end - end - - context 'user logged in' do - it 'sets the default headers' do - sign_in(user) - - get :index + it 'sets the default headers' do + get :index - expect(response.headers['Pragma']).to eq 'no-cache' - end + expect(response.headers['Cache-Control']).to be_nil + expect(response.headers['Pragma']).to be_nil end end @@ -779,7 +767,6 @@ RSpec.describe ApplicationController do subject expect(response.headers['Cache-Control']).to eq 'private, no-store' - expect(response.headers['Pragma']).to eq 'no-cache' expect(response.headers['Expires']).to eq 'Fri, 01 Jan 1990 00:00:00 GMT' end diff --git a/spec/features/markdown/observability_spec.rb b/spec/features/markdown/observability_spec.rb index 0b380c74777..e57bfafe05e 100644 --- a/spec/features/markdown/observability_spec.rb +++ b/spec/features/markdown/observability_spec.rb @@ -45,7 +45,11 @@ RSpec.describe 'Observability rendering', :js, feature_category: :metrics do end end - shared_examples 'does not embed observability in issues and MRs' do + context 'when feature flag is disabled' do + before do + stub_feature_flags(observability_group_tab: false) + end + context 'when embedding in an issue' do let(:issue) do create(:issue, project: project, description: observable_url) @@ -72,18 +76,4 @@ RSpec.describe 'Observability rendering', :js, feature_category: :metrics do it_behaves_like 'does not embed observability' end end - - context 'when user is not a developer of the embeded group' do - it_behaves_like 'does not embed observability in issues and MRs' do - let_it_be(:observable_url) { "https://observe.gitlab.com/1234/some-dashboard" } - end - end - - context 'when feature flag is disabled' do - before do - stub_feature_flags(observability_group_tab: false) - end - - it_behaves_like 'does not embed observability in issues and MRs' - end end diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js index 773df01ada7..2a07957efee 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js @@ -17,6 +17,7 @@ import { OPTIONS_NONE_ANY, } from '~/vue_shared/components/filtered_search_bar/constants'; import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; +import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; import { mockReactionEmojiToken, mockEmojis } from '../mock_data'; @@ -60,58 +61,72 @@ describe('EmojiToken', () => { let mock; let wrapper; + const findBaseToken = () => wrapper.findComponent(BaseToken); + const triggerFetchEmojis = (searchTerm = null) => { + findBaseToken().vm.$emit('fetch-suggestions', searchTerm); + return waitForPromises(); + }; + beforeEach(() => { mock = new MockAdapter(axios); }); afterEach(() => { mock.restore(); - wrapper.destroy(); }); describe('methods', () => { - beforeEach(() => { - wrapper = createComponent(); - }); - describe('fetchEmojis', () => { - it('calls `config.fetchEmojis` with provided searchTerm param', () => { - jest.spyOn(wrapper.vm.config, 'fetchEmojis'); - - wrapper.vm.fetchEmojis('foo'); + it('sets loading state', async () => { + wrapper = createComponent({ + config: { + fetchEmojis: jest.fn().mockResolvedValue(new Promise(() => {})), + }, + }); + await nextTick(); - expect(wrapper.vm.config.fetchEmojis).toHaveBeenCalledWith('foo'); + expect(findBaseToken().props('suggestionsLoading')).toBe(true); }); - it('sets response to `emojis` when request is successful', () => { - jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockResolvedValue(mockEmojis); + describe('when request is successful', () => { + const searchTerm = 'foo'; - wrapper.vm.fetchEmojis('foo'); + beforeEach(async () => { + wrapper = createComponent({ + config: { + fetchEmojis: jest.fn().mockResolvedValue({ data: mockEmojis }), + }, + }); + return triggerFetchEmojis(searchTerm); + }); - return waitForPromises().then(() => { - expect(wrapper.vm.emojis).toEqual(mockEmojis); + it('calls `config.fetchEmojis` with provided searchTerm param', () => { + expect(findBaseToken().props('config').fetchEmojis).toHaveBeenCalledWith(searchTerm); }); - }); - it('calls `createAlert` with flash error message when request fails', () => { - jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); + it('sets response to `emojis`', () => { + expect(findBaseToken().props('suggestions')).toEqual(mockEmojis); + }); + }); - wrapper.vm.fetchEmojis('foo'); + describe('when request fails', () => { + beforeEach(() => { + wrapper = createComponent({ + config: { + fetchEmojis: jest.fn().mockRejectedValue({}), + }, + }); + return triggerFetchEmojis(); + }); - return waitForPromises().then(() => { + it('calls `createAlert` with flash error message', () => { expect(createAlert).toHaveBeenCalledWith({ message: 'There was a problem fetching emojis.', }); }); - }); - - it('sets `loading` to false when request completes', () => { - jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({}); - - wrapper.vm.fetchEmojis('foo'); - return waitForPromises().then(() => { - expect(wrapper.vm.loading).toBe(false); + it('sets `loading` to false when request completes', () => { + expect(findBaseToken().props('suggestionsLoading')).toBe(false); }); }); }); @@ -123,15 +138,10 @@ describe('EmojiToken', () => { beforeEach(async () => { wrapper = createComponent({ value: { data: `"${mockEmojis[0].name}"` }, + config: { + initialEmojis: mockEmojis, + }, }); - - // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details - // eslint-disable-next-line no-restricted-syntax - wrapper.setData({ - emojis: mockEmojis, - }); - - await nextTick(); }); it('renders gl-filtered-search-token component', () => { diff --git a/spec/lib/banzai/filter/inline_observability_filter_spec.rb b/spec/lib/banzai/filter/inline_observability_filter_spec.rb deleted file mode 100644 index fb1ba46e76c..00000000000 --- a/spec/lib/banzai/filter/inline_observability_filter_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Banzai::Filter::InlineObservabilityFilter do - include FilterSpecHelper - - let(:input) { %(<a href="#{url}">example</a>) } - let(:doc) { filter(input) } - let(:group) { create(:group) } - let(:user) { create(:user) } - - describe '#filter?' do - context 'when the document has an external link' do - let(:url) { 'https://foo.com' } - - it 'leaves regular non-observability links unchanged' do - expect(doc.to_s).to eq(input) - end - end - - context 'when the document contains an embeddable observability link' do - let(:url) { 'https://observe.gitlab.com/12345' } - - it 'leaves the original link unchanged' do - expect(doc.at_css('a').to_s).to eq(input) - end - - it 'appends an observability charts placeholder' do - node = doc.at_css('.js-render-observability') - - expect(node).to be_present - expect(node.attribute('data-frame-url').to_s).to eq(url) - end - end - - context 'when feature flag is disabled' do - let(:url) { 'https://observe.gitlab.com/12345' } - - before do - stub_feature_flags(observability_group_tab: false) - end - - it 'leaves the original link unchanged' do - expect(doc.at_css('a').to_s).to eq(input) - end - - it 'does not append an observability charts placeholder' do - node = doc.at_css('.js-render-observability') - - expect(node).not_to be_present - end - end - end -end diff --git a/spec/lib/banzai/filter/inline_observability_redactor_filter_spec.rb b/spec/lib/banzai/filter/inline_observability_redactor_filter_spec.rb deleted file mode 100644 index ceebe42d1b7..00000000000 --- a/spec/lib/banzai/filter/inline_observability_redactor_filter_spec.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Banzai::Filter::InlineObservabilityRedactorFilter, feature_category: :metrics do - include FilterSpecHelper - - let_it_be(:group) { create(:group) } - - let(:url) { "#{Gitlab::Observability.observability_url}/#{group.id}/explore" } - let(:input) { %(<a href="#{url}">example</a>) } - let(:doc) { filter(input) } - - context 'without an observability placeholder' do - it 'leaves regular links unchanged' do - expect(doc.to_s).to eq input - end - end - - shared_examples 'redacts the placeholder' do - it 'redacts the placeholder' do - expect(doc.to_s).to be_empty - end - end - - context 'with an observability placeholder' do - let(:input) { %(<div class="js-render-observability" data-frame-url="#{url}"></div>) } - - context 'when no user is logged in' do - it_behaves_like 'redacts the placeholder' - end - - context 'with invalid observability url' do - let(:url) { "#{Gitlab::Observability.observability_url}/foo/explore" } - - it_behaves_like 'redacts the placeholder' - end - - context 'with missing observability frame url' do - let(:input) { %(<div class="js-render-observability"></div>) } - - it_behaves_like 'redacts the placeholder' - end - - context 'when the user does not have permission to access the group' do - let(:user) { create(:user) } - let(:doc) { filter(input, current_user: user) } - - it_behaves_like 'redacts the placeholder' - end - - context 'when the user is not a developer of the group' do - let(:user) { create(:user) } - let(:doc) { filter(input, current_user: user) } - - before do - group.add_reporter(user) - end - - it_behaves_like 'redacts the placeholder' - end - - context 'when the user is a developer of the group' do - let(:user) { create(:user) } - let(:doc) { filter(input, current_user: user) } - - before do - group.add_developer(user) - end - - it 'leaves the placeholder' do - expect(CGI.unescapeHTML(doc.to_s)).to eq(input) - end - - context 'with over 100 embeds' do - let(:embed) { %(<div class="js-render-observability" data-frame-url="#{url}"></div>) } - let(:input) { embed * 150 } - - it 'redacts ill-advised embeds' do - expect(doc.to_s.length).to eq(embed.length * 100) - end - end - end - end -end diff --git a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb index c8d3844af30..072d77f4112 100644 --- a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb @@ -36,7 +36,7 @@ RSpec.describe Banzai::Pipeline::PostProcessPipeline, feature_category: :team_pl end let(:doc) { HTML::Pipeline.parse(html) } - let(:non_related_xpath_calls) { 3 } + let(:non_related_xpath_calls) { 2 } it 'searches for attributes only once' do expect(doc).to receive(:xpath).exactly(non_related_xpath_calls + 1).times diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 056dc1f1966..86a363c2227 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -508,6 +508,7 @@ project: - project_namespace - management_clusters - boards +- application_setting - last_event - integrations - push_hooks_integrations diff --git a/spec/lib/gitlab/observability_spec.rb b/spec/lib/gitlab/observability_spec.rb index e8c79c29830..3be579e088c 100644 --- a/spec/lib/gitlab/observability_spec.rb +++ b/spec/lib/gitlab/observability_spec.rb @@ -49,30 +49,6 @@ RSpec.describe Gitlab::Observability do end end - describe '.group_id_from_url' do - it 'returns the group id extracted from the url' do - expect(described_class.group_id_from_url('https://observe.gitlab.com/123/explore')).to eq(123) - expect(described_class.group_id_from_url('https://observe.gitlab.com/123')).to eq(123) - expect(described_class.group_id_from_url('https://observe.gitlab.com/123/')).to eq(123) - expect(described_class.group_id_from_url('https://observe.gitlab.com/123/456')).to eq(123) - end - - it 'returns nil if the group id is not valid or missing' do - expect(described_class.group_id_from_url('https://observe.gitlab.com')).to be_nil - expect(described_class.group_id_from_url('https://observe.gitlab.com/')).to be_nil - expect(described_class.group_id_from_url('https://observe.gitlab.com/foo')).to be_nil - expect(described_class.group_id_from_url('https://observe.gitlab.com/foo/bar')).to be_nil - expect(described_class.group_id_from_url('https://observe.gitlab.com/0')).to be_nil - expect(described_class.group_id_from_url('https://observe.gitlab.com/-1')).to be_nil - end - - it 'returns nil if the url is not a valid' do - expect(described_class.group_id_from_url('https://invalid.gitlab.com/123')).to be_nil - expect(described_class.group_id_from_url('foo bar')).to be_nil - expect(described_class.group_id_from_url('foo@@@@bar/1/')).to be_nil - end - end - describe '.allowed_for_action?' do let_it_be(:group) { build(:user) } let_it_be(:user) { build(:group) } diff --git a/spec/models/alert_management/alert_assignee_spec.rb b/spec/models/alert_management/alert_assignee_spec.rb index c50a3ec0d01..647195380b3 100644 --- a/spec/models/alert_management/alert_assignee_spec.rb +++ b/spec/models/alert_management/alert_assignee_spec.rb @@ -5,7 +5,11 @@ require 'spec_helper' RSpec.describe AlertManagement::AlertAssignee do describe 'associations' do it { is_expected.to belong_to(:alert) } - it { is_expected.to belong_to(:assignee) } + + it do + is_expected.to belong_to(:assignee).class_name('User') + .with_foreign_key(:user_id).inverse_of(:alert_assignees) + end end describe 'validations' do diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb index 685ed81ec84..ff77ca2ab64 100644 --- a/spec/models/alert_management/alert_spec.rb +++ b/spec/models/alert_management/alert_spec.rb @@ -16,9 +16,13 @@ RSpec.describe AlertManagement::Alert do it { is_expected.to belong_to(:prometheus_alert).optional } it { is_expected.to belong_to(:environment).optional } it { is_expected.to have_many(:assignees).through(:alert_assignees) } - it { is_expected.to have_many(:notes) } - it { is_expected.to have_many(:ordered_notes) } - it { is_expected.to have_many(:user_mentions) } + it { is_expected.to have_many(:notes).inverse_of(:noteable) } + it { is_expected.to have_many(:ordered_notes).class_name('Note').inverse_of(:noteable) } + + it do + is_expected.to have_many(:user_mentions).class_name('AlertManagement::AlertUserMention') + .with_foreign_key(:alert_management_alert_id).inverse_of(:alert) + end end describe 'validations' do diff --git a/spec/models/alert_management/alert_user_mention_spec.rb b/spec/models/alert_management/alert_user_mention_spec.rb index 27c3d290dde..083bf667bea 100644 --- a/spec/models/alert_management/alert_user_mention_spec.rb +++ b/spec/models/alert_management/alert_user_mention_spec.rb @@ -4,7 +4,11 @@ require 'spec_helper' RSpec.describe AlertManagement::AlertUserMention do describe 'associations' do - it { is_expected.to belong_to(:alert_management_alert) } + it do + is_expected.to belong_to(:alert).class_name('::AlertManagement::Alert') + .with_foreign_key(:alert_management_alert_id).inverse_of(:user_mentions) + end + it { is_expected.to belong_to(:note) } end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 8de9932a519..de5753bd4e2 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -25,6 +25,20 @@ RSpec.describe ApplicationSetting, feature_category: :not_owned, type: :model do it { expect(setting.kroki_formats).to eq({}) } end + describe 'associations' do + it do + is_expected.to belong_to(:self_monitoring_project).class_name('Project') + .with_foreign_key(:instance_administration_project_id) + .inverse_of(:application_setting) + end + + it do + is_expected.to belong_to(:instance_group).class_name('Group') + .with_foreign_key(:instance_administrators_group_id) + .inverse_of(:application_setting) + end + end + describe 'validations' do let(:http) { 'http://example.com' } let(:https) { 'https://example.com' } diff --git a/spec/models/audit_event_spec.rb b/spec/models/audit_event_spec.rb index 9f2724cebee..9e667836b45 100644 --- a/spec/models/audit_event_spec.rb +++ b/spec/models/audit_event_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' RSpec.describe AuditEvent do + describe 'associations' do + it { is_expected.to belong_to(:user).with_foreign_key(:author_id).inverse_of(:audit_events) } + end + describe 'validations' do include_examples 'validates IP address' do let(:attribute) { :ip_address } diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb index 6017298e85b..f469dee5ba1 100644 --- a/spec/models/board_spec.rb +++ b/spec/models/board_spec.rb @@ -8,7 +8,13 @@ RSpec.describe Board do describe 'relationships' do it { is_expected.to belong_to(:project) } - it { is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all) } + + it do + is_expected.to have_many(:lists).order(list_type: :asc, position: :asc).dependent(:delete_all) + .inverse_of(:board) + end + + it { is_expected.to have_many(:destroyable_lists).order(list_type: :asc, position: :asc).inverse_of(:board) } end describe 'validations' do diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb index 2cb72e74979..db123c00cf6 100644 --- a/spec/models/bulk_imports/entity_spec.rb +++ b/spec/models/bulk_imports/entity_spec.rb @@ -6,8 +6,13 @@ RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers d describe 'associations' do it { is_expected.to belong_to(:bulk_import).required } it { is_expected.to belong_to(:parent) } - it { is_expected.to belong_to(:group) } + it { is_expected.to belong_to(:group).optional.with_foreign_key(:namespace_id).inverse_of(:bulk_import_entities) } it { is_expected.to belong_to(:project) } + + it do + is_expected.to have_many(:trackers).class_name('BulkImports::Tracker') + .with_foreign_key(:bulk_import_entity_id).inverse_of(:entity) + end end describe 'validations' do diff --git a/spec/models/bulk_imports/tracker_spec.rb b/spec/models/bulk_imports/tracker_spec.rb index 1516ab106cb..a618a12df6b 100644 --- a/spec/models/bulk_imports/tracker_spec.rb +++ b/spec/models/bulk_imports/tracker_spec.rb @@ -4,7 +4,10 @@ require 'spec_helper' RSpec.describe BulkImports::Tracker, type: :model do describe 'associations' do - it { is_expected.to belong_to(:entity).required } + it do + is_expected.to belong_to(:entity).required.class_name('BulkImports::Entity') + .with_foreign_key(:bulk_import_entity_id).inverse_of(:trackers) + end end describe 'validations' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 0a05c558d45..882a1f73a03 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -40,7 +40,19 @@ RSpec.describe Group, feature_category: :subgroups do it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::GroupDistribution').dependent(:destroy) } it { is_expected.to have_many(:daily_build_group_report_results).class_name('Ci::DailyBuildGroupReportResult') } it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout').with_foreign_key(:group_id) } + + it do + is_expected.to have_many(:application_setting) + .with_foreign_key(:instance_administrators_group_id).inverse_of(:instance_group) + end + it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') } + + it do + is_expected.to have_many(:bulk_import_entities).class_name('BulkImports::Entity') + .with_foreign_key(:namespace_id).inverse_of(:group) + end + it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') } it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') } it { is_expected.to have_many(:protected_branches).inverse_of(:group).with_foreign_key(:namespace_id) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 360aee8b531..3040fea4178 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -174,6 +174,11 @@ RSpec.describe User, feature_category: :user_profile do it { is_expected.to have_many(:revoked_user_achievements).class_name('Achievements::UserAchievement').with_foreign_key('revoked_by_user_id').inverse_of(:revoked_by_user) } it { is_expected.to have_many(:achievements).through(:user_achievements).class_name('Achievements::Achievement').inverse_of(:users) } it { is_expected.to have_many(:namespace_commit_emails).class_name('Users::NamespaceCommitEmail') } + it { is_expected.to have_many(:audit_events).with_foreign_key(:author_id).inverse_of(:user) } + + it do + is_expected.to have_many(:alert_assignees).class_name('::AlertManagement::AlertAssignee').inverse_of(:assignee) + end describe 'default values' do let(:user) { described_class.new } diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index f4066c54c47..1ad5233f420 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -829,7 +829,6 @@ RSpec.describe API::Files, feature_category: :source_code_management do expect_to_send_git_blob(api(url, current_user), params) expect(response.headers['Cache-Control']).to eq('max-age=0, private, must-revalidate, no-store, no-cache') - expect(response.headers['Pragma']).to eq('no-cache') expect(response.headers['Expires']).to eq('Fri, 01 Jan 1990 00:00:00 GMT') end diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 555ba2bc978..b146dda5030 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -236,7 +236,6 @@ RSpec.describe API::Repositories, feature_category: :source_code_management do get api(route, current_user) expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate, no-store, no-cache") - expect(response.headers["Pragma"]).to eq("no-cache") expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT") end diff --git a/spec/services/packages/mark_package_for_destruction_service_spec.rb b/spec/services/packages/mark_package_for_destruction_service_spec.rb index 125ec53ad61..80c9ea89414 100644 --- a/spec/services/packages/mark_package_for_destruction_service_spec.rb +++ b/spec/services/packages/mark_package_for_destruction_service_spec.rb @@ -36,6 +36,12 @@ RSpec.describe Packages::MarkPackageForDestructionService do end it 'returns an error ServiceResponse' do + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(StandardError), + project_id: package.project_id, + package_id: package.id + ) + response = service.execute expect(package).not_to receive(:sync_maven_metadata) diff --git a/spec/services/packages/mark_packages_for_destruction_service_spec.rb b/spec/services/packages/mark_packages_for_destruction_service_spec.rb index 5c043b89de8..f2c1168747a 100644 --- a/spec/services/packages/mark_packages_for_destruction_service_spec.rb +++ b/spec/services/packages/mark_packages_for_destruction_service_spec.rb @@ -76,6 +76,11 @@ RSpec.describe Packages::MarkPackagesForDestructionService, :sidekiq_inline do it 'returns an error ServiceResponse' do expect(::Packages::Maven::Metadata::SyncService).not_to receive(:new) + expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + instance_of(StandardError), + package_ids: package_ids + ) + expect { subject }.to not_change { ::Packages::Package.pending_destruction.count } .and not_change { ::Packages::PackageFile.pending_destruction.count } |