Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-24 15:09:55 +00:00
parent 552e85a586
commit fd9a56d56f
74 changed files with 800 additions and 507 deletions

View File

@ -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

View File

@ -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'

View File

@ -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" />

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -0,0 +1 @@
53f1003eeb8f961b37d90c73a71f75683077b9bcd0e495395033998530a363bd

View File

@ -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:

View File

@ -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

View File

@ -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
---

View File

@ -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": []
}
```

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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).

View File

@ -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
---

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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
---

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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?

View File

@ -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'),

View File

@ -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")

View File

@ -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
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
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

View File

@ -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

View File

@ -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');
it('sets loading state', async () => {
wrapper = createComponent({
config: {
fetchEmojis: jest.fn().mockResolvedValue(new Promise(() => {})),
},
});
await nextTick();
wrapper.vm.fetchEmojis('foo');
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('sets response to `emojis`', () => {
expect(findBaseToken().props('suggestions')).toEqual(mockEmojis);
});
});
it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({});
describe('when request fails', () => {
beforeEach(() => {
wrapper = createComponent({
config: {
fetchEmojis: jest.fn().mockRejectedValue({}),
},
});
return triggerFetchEmojis();
});
wrapper.vm.fetchEmojis('foo');
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', () => {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -508,6 +508,7 @@ project:
- project_namespace
- management_clusters
- boards
- application_setting
- last_event
- integrations
- push_hooks_integrations

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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' }

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 }