summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/database.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/rails/shared.gitlab-ci.yml7
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml10
-rw-r--r--CHANGELOG.md35
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js114
-rw-r--r--app/assets/javascripts/behaviors/shortcuts.js5
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue3
-rw-r--r--app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js3
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue3
-rw-r--r--app/assets/javascripts/layout_nav.js6
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js69
-rw-r--r--app/assets/javascripts/super_sidebar/components/bottom_bar.vue24
-rw-r--r--app/assets/javascripts/super_sidebar/components/help_center.vue88
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue24
-rw-r--r--app/assets/javascripts/whats_new/utils/notification.js2
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss1
-rw-r--r--app/helpers/sidebars_helper.rb4
-rw-r--r--app/models/concerns/issuable.rb19
-rw-r--r--app/models/concerns/sanitizable.rb9
-rw-r--r--app/models/namespace_setting.rb13
-rw-r--r--app/services/packages/helm/extract_file_metadata_service.rb5
-rw-r--r--app/views/admin/application_settings/appearances/_form.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml4
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml5
-rw-r--r--db/post_migrate/20230127123947_add_fk_index_to_ci_sources_pipelines_on_source_partition_id_and_source_job_id.rb17
-rw-r--r--db/post_migrate/20230127123948_add_fk_to_ci_sources_pipelines_on_source_partition_id_and_source_job_id.rb37
-rw-r--r--db/schema_migrations/202301271239471
-rw-r--r--db/schema_migrations/202301271239481
-rw-r--r--db/structure.sql5
-rw-r--r--doc/api/groups.md3
-rw-r--r--doc/api/members.md3
-rw-r--r--doc/architecture/blueprints/runner_tokens/index.md38
-rw-r--r--doc/ci/yaml/index.md6
-rw-r--r--doc/development/database/avoiding_downtime_in_migrations.md10
-rw-r--r--doc/user/admin_area/appearance.md2
-rw-r--r--doc/user/enterprise_user/index.md71
-rw-r--r--doc/user/group/manage.md2
-rw-r--r--doc/user/group/saml_sso/index.md7
-rw-r--r--doc/user/group/saml_sso/scim_setup.md2
-rw-r--r--doc/user/packages/container_registry/build_and_push_images.md2
-rw-r--r--doc/user/packages/npm_registry/index.md16
-rw-r--r--doc/user/project/members/share_project_with_groups.md122
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/draft_notes.rb39
-rw-r--r--lib/api/entities/draft_note.rb45
-rw-r--r--lib/gitlab/ci/artifact_file_reader.rb23
-rw-r--r--lib/gitlab/utils.rb3
-rw-r--r--lib/safe_zip/entry.rb10
-rw-r--r--lib/safe_zip/extract.rb1
-rw-r--r--lib/safe_zip/extract_params.rb24
-rw-r--r--locale/gitlab.pot70
-rwxr-xr-xscripts/trigger-build.rb33
-rw-r--r--spec/features/issues/user_bulk_edits_issues_spec.rb4
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb2
-rw-r--r--spec/fixtures/packages/helm/corrupted_chart.tgzbin0 -> 2084191 bytes
-rw-r--r--spec/fixtures/safe_zip/invalid-unexpected-large.zipbin0 -> 376 bytes
-rw-r--r--spec/fixtures/safe_zip/valid-symlinks-first.zipbin528 -> 1297 bytes
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js87
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js6
-rw-r--r--spec/frontend/super_sidebar/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js14
-rw-r--r--spec/helpers/sidebars_helper_spec.rb4
-rw-r--r--spec/lib/api/entities/draft_note_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/artifact_file_reader_spec.rb124
-rw-r--r--spec/lib/safe_zip/entry_spec.rb35
-rw-r--r--spec/lib/safe_zip/extract_params_spec.rb35
-rw-r--r--spec/lib/safe_zip/extract_spec.rb55
-rw-r--r--spec/models/concerns/issuable_spec.rb1
-rw-r--r--spec/models/concerns/sanitizable_spec.rb53
-rw-r--r--spec/models/namespace_setting_spec.rb6
-rw-r--r--spec/requests/api/draft_notes_spec.rb35
-rw-r--r--spec/scripts/trigger-build_spec.rb23
-rw-r--r--spec/services/packages/helm/extract_file_metadata_service_spec.rb13
-rw-r--r--spec/support/shared_examples/models/concerns/issuable_shared_examples.rb103
-rw-r--r--spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb19
77 files changed, 1339 insertions, 365 deletions
diff --git a/.gitlab/ci/database.gitlab-ci.yml b/.gitlab/ci/database.gitlab-ci.yml
index b21df6e50c6..32de6c68926 100644
--- a/.gitlab/ci/database.gitlab-ci.yml
+++ b/.gitlab/ci/database.gitlab-ci.yml
@@ -51,7 +51,7 @@ db:check-migrations-single-db:
extends:
- db:check-migrations
- .single-db
- - .rails:rules:single-db
+ - .rails:rules:db:check-migrations-single-db
db:post_deployment_migrations_validator:
extends:
@@ -66,7 +66,7 @@ db:post_deployment_migrations_validator-single-db:
extends:
- db:post_deployment_migrations_validator
- .single-db
- - .rails:rules:single-db
+ - .rails:rules:db:check-migrations-single-db
db:migrate-non-superuser:
extends:
diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml
index b95a4c45f1e..3f9a43c1ced 100644
--- a/.gitlab/ci/rails/shared.gitlab-ci.yml
+++ b/.gitlab/ci/rails/shared.gitlab-ci.yml
@@ -107,7 +107,12 @@ include:
- .rspec-base
- .as-if-foss
- .use-pg12
- needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets as-if-foss", "detect-tests"]
+ needs:
+ - job: "setup-test-env"
+ - job: "retrieve-tests-metadata"
+ - job: "compile-test-assets as-if-foss"
+ - job: "detect-tests"
+ optional: true
.rspec-base-pg13:
extends:
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index d16dd28f597..975554f59e3 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1205,6 +1205,12 @@
changes: *db-patterns
- <<: *if-default-branch-schedule-nightly
+.rails:rules:db:check-migrations-single-db:
+ rules:
+ - <<: *if-merge-request-labels-run-single-db
+ - <<: *if-merge-request
+ changes: *db-patterns
+
.rails:rules:db-backup:
rules:
- <<: *if-merge-request-labels-run-all-rspec
@@ -2191,7 +2197,7 @@
- <<: *if-not-ee
when: never
- <<: *if-dot-com-ee-schedule-default-branch-maintenance
- - <<: *if-default-refs
+ - <<: *if-default-branch-refs
changes:
- ".gitlab/ci/setup.gitlab-ci.yml"
- ".gitlab/ci/test-metadata.gitlab-ci.yml"
@@ -2213,7 +2219,7 @@
- <<: *if-not-ee
when: never
- <<: *if-dot-com-ee-schedule-default-branch-maintenance
- - <<: *if-default-refs
+ - <<: *if-default-branch-refs
changes:
- ".gitlab/ci/test-metadata.gitlab-ci.yml"
- "scripts/rspec_helpers.sh"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f06c5517d9..d8e13f337e9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 15.8.1 (2023-01-30)
+
+### Security (5 changes)
+
+- [Remove parameter validation for registry notification request [15.8]](gitlab-org/security/gitlab@bf5a28cc21ffa3e7b63eeca02f220c1312314f75) ([merge request](gitlab-org/security/gitlab!3028))
+- [Add size validation for Chart.yaml during file extraction](gitlab-org/security/gitlab@f4afa319cffded561731c117c808969b5261ca52) ([merge request](gitlab-org/security/gitlab!3018))
+- [Prevent default branches from storing paths](gitlab-org/security/gitlab@a906e14f6891e84cfe854be960266adc7f0f6092) ([merge request](gitlab-org/security/gitlab!3011))
+- [Validate Issuable description max length on update](gitlab-org/security/gitlab@312fbac888d0452d9beb9d6545b22972b7e1f09d) ([merge request](gitlab-org/security/gitlab!3004))
+- [Security fix dynamic child pipeline zip extraction](gitlab-org/security/gitlab@ea09503c67eb1eb1f17ea49b7748543d2676e393) ([merge request](gitlab-org/security/gitlab!3007))
+
## 15.8.0 (2023-01-20)
### Added (120 changes)
@@ -470,6 +480,16 @@ entry.
- [Do not use _test when not necessary](gitlab-org/gitlab@1bde73aba2bd1d7f9e833c7325cffa0c90d1c106) ([merge request](gitlab-org/gitlab!107373))
- [Add config/redis.yml unified config file](gitlab-org/gitlab@ace8301236eecc07a511975b57f80e21ec7be3c2) ([merge request](gitlab-org/gitlab!106854))
+## 15.7.6 (2023-01-30)
+
+### Security (5 changes)
+
+- [Remove parameter validation for registry notification request [15.7]](gitlab-org/security/gitlab@ca091312787e3a01f39551357095409fcf6fc840) ([merge request](gitlab-org/security/gitlab!3029))
+- [Add size validation for Chart.yaml during file extraction](gitlab-org/security/gitlab@d43bf6a5b147350668e538bd981af5f9400e6fca) ([merge request](gitlab-org/security/gitlab!3019))
+- [Prevent default branches from storing paths](gitlab-org/security/gitlab@9f18aa40711e334b172d208255a4b396b89c50a9) ([merge request](gitlab-org/security/gitlab!3012))
+- [Validate Issuable description max length on update](gitlab-org/security/gitlab@9c2799bef06ada7d48b682fa4fe403ec00e74c4a) ([merge request](gitlab-org/security/gitlab!3003))
+- [Security fix dynamic child pipeline zip extraction](gitlab-org/security/gitlab@cada7e3290da854f437538cdc1680c3f5284278e) ([merge request](gitlab-org/security/gitlab!2980))
+
## 15.7.5 (2023-01-12)
No changes.
@@ -1320,6 +1340,21 @@ No changes.
- [Propagate RemoteIP to Gitaly via Workhorse](gitlab-org/gitlab@71da945c85931bac0263c193902dc1b54e2e62da) ([merge request](gitlab-org/gitlab!103635))
- [Documentation to reflect 100MB upload limit](gitlab-org/gitlab@33063bb26ab7699802ecb2b325cc8619d6fe7b86) ([merge request](gitlab-org/gitlab!103978))
+## 15.6.7 (2023-01-30)
+
+### Fixed (2 changes)
+
+- [Clear DuplicateJobs cookies from post-deployment migration](gitlab-org/security/gitlab@9071bc623c81f4ecbccb63bcfc78d6d503421e2b)
+- [Geo: Container Repository push events don't work](gitlab-org/security/gitlab@00ca7dd923444da0b19afa7d72d5e3b505889290)
+
+### Security (5 changes)
+
+- [Quarantine features/users/login_spec line 292 [15.6]](gitlab-org/security/gitlab@d202f35e1cac8df0bcbb5d40d42cea2312c09762) ([merge request](gitlab-org/security/gitlab!3025))
+- [Add size validation for Chart.yaml during file extraction](gitlab-org/security/gitlab@59df02bf2658468f9f254c34ed009a6414d6c6b3) ([merge request](gitlab-org/security/gitlab!3020))
+- [Prevent default branches from storing paths](gitlab-org/security/gitlab@b7b402a0a37bb839b601569a035a62fe79febe72) ([merge request](gitlab-org/security/gitlab!3013))
+- [Validate Issuable description max length on update](gitlab-org/security/gitlab@fa68365e853a5701b217ccafea9885705d4a4133) ([merge request](gitlab-org/security/gitlab!3002))
+- [Security fix dynamic child pipeline zip extraction](gitlab-org/security/gitlab@2285d716f10f33d8dbea5112de95d9d7e5cd8b00) ([merge request](gitlab-org/security/gitlab!2981))
+
## 15.6.6 (2023-01-12)
No changes.
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index c62736d55a8..a82633035b5 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -1,5 +1,6 @@
import { masks } from '~/lib/dateformat';
import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const DATE_RANGE_LIMIT = 180;
export const PROJECTS_PER_PAGE = 50;
@@ -12,8 +13,101 @@ export const dateFormats = {
month: 'mmmm',
};
-// Some content is duplicated due to backward compatibility.
-// It will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/350614 in 14.9
+export const KEY_METRICS = {
+ LEAD_TIME: 'lead_time',
+ CYCLE_TIME: 'cycle_time',
+ ISSUES: 'issues',
+ COMMITS: 'commits',
+ DEPLOYS: 'deploys',
+};
+
+export const DORA_METRICS = {
+ DEPLOYMENT_FREQUENCY: 'deployment_frequency',
+ LEAD_TIME_FOR_CHANGES: 'lead_time_for_changes',
+ TIME_TO_RESTORE_SERVICE: 'time_to_restore_service',
+ CHANGE_FAILURE_RATE: 'change_failure_rate',
+};
+
+export const VSA_METRICS_GROUPS = [
+ {
+ key: 'key_metrics',
+ title: s__('ValueStreamAnalytics|Key metrics'),
+ keys: Object.values(KEY_METRICS),
+ },
+ {
+ key: 'dora_metrics',
+ title: s__('ValueStreamAnalytics|DORA metrics'),
+ keys: Object.values(DORA_METRICS),
+ },
+];
+
+export const METRIC_TOOLTIPS = {
+ [DORA_METRICS.DEPLOYMENT_FREQUENCY]: {
+ description: s__(
+ 'ValueStreamAnalytics|Average number of deployments to production per day. This metric measures how often value is delivered to end users.',
+ ),
+ groupLink: '-/analytics/ci_cd?tab=deployment-frequency',
+ projectLink: '-/pipelines/charts?chart=deployment-frequency',
+ docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'deployment-frequency' }),
+ },
+ [DORA_METRICS.LEAD_TIME_FOR_CHANGES]: {
+ description: s__(
+ 'ValueStreamAnalytics|The time to successfully deliver a commit into production. This metric reflects the efficiency of CI/CD pipelines.',
+ ),
+ groupLink: '-/analytics/ci_cd?tab=lead-time',
+ projectLink: '-/pipelines/charts?chart=lead-time',
+ docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'lead-time-for-changes' }),
+ },
+ [DORA_METRICS.TIME_TO_RESTORE_SERVICE]: {
+ description: s__(
+ 'ValueStreamAnalytics|The time it takes an organization to recover from a failure in production.',
+ ),
+ groupLink: '-/analytics/ci_cd?tab=time-to-restore-service',
+ projectLink: '-/pipelines/charts?chart=time-to-restore-service',
+ docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'time-to-restore-service' }),
+ },
+ [DORA_METRICS.CHANGE_FAILURE_RATE]: {
+ description: s__(
+ 'ValueStreamAnalytics|Percentage of deployments that cause an incident in production.',
+ ),
+ groupLink: '-/analytics/ci_cd?tab=change-failure-rate',
+ projectLink: '-/pipelines/charts?chart=change-failure-rate',
+ docsLink: helpPagePath('user/analytics/dora_metrics', { anchor: 'change-failure-rate' }),
+ },
+ [KEY_METRICS.LEAD_TIME]: {
+ description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'),
+ groupLink: '-/analytics/value_stream_analytics',
+ projectLink: '-/value_stream_analytics',
+ docsLink: helpPagePath('user/analytics/value_stream_analytics', {
+ anchor: 'view-the-lead-time-and-cycle-time-for-issues',
+ }),
+ },
+ [KEY_METRICS.CYCLE_TIME]: {
+ description: s__(
+ "ValueStreamAnalytics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed.",
+ ),
+ groupLink: '-/analytics/value_stream_analytics',
+ projectLink: '-/value_stream_analytics',
+ docsLink: helpPagePath('user/analytics/value_stream_analytics', {
+ anchor: 'view-the-lead-time-and-cycle-time-for-issues',
+ }),
+ },
+ [KEY_METRICS.ISSUES]: {
+ description: s__('ValueStreamAnalytics|Number of new issues created.'),
+ groupLink: '-/issues_analytics',
+ projectLink: '-/analytics/issues_analytics',
+ docsLink: helpPagePath('user/analytics/issue_analytics'),
+ },
+ [KEY_METRICS.DEPLOYS]: {
+ description: s__('ValueStreamAnalytics|Total number of deploys to production.'),
+ groupLink: '-/analytics/productivity_analytics',
+ projectLink: '-/analytics/merge_request_analytics',
+ docsLink: helpPagePath('user/analytics/merge_request_analytics'),
+ },
+};
+
+// TODO: Remove this once the migration to METRIC_TOOLTIPS is complete
+// https://gitlab.com/gitlab-org/gitlab/-/issues/388067
export const METRICS_POPOVER_CONTENT = {
lead_time: {
description: s__('ValueStreamAnalytics|Median time from issue created to issue closed.'),
@@ -47,19 +141,3 @@ export const METRICS_POPOVER_CONTENT = {
),
},
};
-
-const KEY_METRICS_TITLE = s__('ValueStreamAnalytics|Key metrics');
-const KEY_METRICS_KEYS = ['lead_time', 'cycle_time', 'issues', 'commits', 'deploys'];
-
-const DORA_METRICS_TITLE = s__('ValueStreamAnalytics|DORA metrics');
-const DORA_METRICS_KEYS = [
- 'deployment_frequency',
- 'lead_time_for_changes',
- 'time_to_restore_service',
- 'change_failure_rate',
-];
-
-export const VSA_METRICS_GROUPS = [
- { key: 'key_metrics', title: KEY_METRICS_TITLE, keys: KEY_METRICS_KEYS },
- { key: 'dora_metrics', title: DORA_METRICS_TITLE, keys: DORA_METRICS_KEYS },
-];
diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js
index 7352be0dbd5..12fdb2e2981 100644
--- a/app/assets/javascripts/behaviors/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts.js
@@ -28,7 +28,10 @@ export default function initPageShortcuts() {
// TODO: replace this whitelist with something more automated/maintainable
if (page && !pagesWithCustomShortcuts.includes(page)) {
import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts')
- .then(({ default: Shortcuts }) => new Shortcuts())
+ .then(({ default: Shortcuts }) => {
+ const shortcuts = new Shortcuts();
+ window.toggleShortcutsHelp = shortcuts.onToggleHelp;
+ })
.catch(() => {});
}
return false;
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index b8719d0cd4d..430498e7194 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,6 +1,5 @@
<script>
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { snakeCase } from 'lodash';
import SafeHtml from '~/vue_shared/directives/safe_html';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
@@ -61,7 +60,7 @@ export default {
return highlight(this.itemName, this.matcher);
},
itemTrackingLabel() {
- return `${this.dropdownType}_dropdown_frequent_items_list_item_${snakeCase(this.itemName)}`;
+ return `${this.dropdownType}_dropdown_frequent_items_list_item`;
},
},
methods: {
diff --git a/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js
index 095da60a583..9c891bcfc9e 100644
--- a/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js
@@ -1,9 +1,9 @@
/* eslint-disable class-methods-use-this, no-new */
-
import $ from 'jquery';
import issuableEventHub from '~/issues/list/eventhub';
import LabelsSelect from '~/labels/labels_select';
import {
+ mountAssigneesDropdown,
mountMilestoneDropdown,
mountMoveIssuesButton,
mountStatusDropdown,
@@ -64,6 +64,7 @@ export default class IssuableBulkUpdateSidebar {
mountMoveIssuesButton();
mountStatusDropdown();
mountSubscriptionsDropdown();
+ mountAssigneesDropdown();
// Checking IS_EE and using ee_else_ce is odd, but we do it here to satisfy
// the import/no-unresolved lint rule when FOSS_ONLY=1, even though at
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 22e9dd6a5c5..28908478e02 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -592,9 +592,6 @@ export default {
const bulkUpdateSidebar = await import('~/issuable');
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
- const UsersSelect = (await import('~/users_select')).default;
- new UsersSelect(); // eslint-disable-line no-new
-
this.hasInitBulkEdit = true;
}
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 90c1b31286a..b8138f34d45 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -56,7 +56,11 @@ function initDeferred() {
if (!appEl) return;
setNotification(appEl);
- document.querySelector('.js-whats-new-trigger').addEventListener('click', () => {
+
+ const triggerEl = document.querySelector('.js-whats-new-trigger');
+ if (!triggerEl) return;
+
+ triggerEl.addEventListener('click', () => {
import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
.then(({ default: initWhatsNew }) => {
initWhatsNew(appEl);
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index facc07e8ce5..bef4c6af6ab 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -1,21 +1,22 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { IssuableType } from '~/issues/constants';
import { gqlClient } from '~/issues/list/graphql';
import {
- isInIssuePage,
isInDesignPage,
isInIncidentPage,
+ isInIssuePage,
isInMRPage,
parseBoolean,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { apolloProvider } from '~/graphql_shared/issuable_client';
import Translate from '~/vue_shared/translate';
+import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
@@ -725,6 +726,70 @@ export function mountMoveIssueButton() {
});
}
+export function mountAssigneesDropdown() {
+ const el = document.querySelector('.js-assignee-dropdown');
+ const assigneeIdsInput = document.querySelector('.js-assignee-ids-input');
+
+ if (!el || !assigneeIdsInput) {
+ return null;
+ }
+
+ const { fullPath } = el.dataset;
+ const currentUser = {
+ id: gon?.current_user_id,
+ username: gon?.current_username,
+ name: gon?.current_user_fullname,
+ avatarUrl: gon?.current_user_avatar_url,
+ };
+
+ return new Vue({
+ el,
+ apolloProvider,
+ data() {
+ return {
+ selectedUserName: '',
+ value: [],
+ };
+ },
+ methods: {
+ onSelectedUnassigned() {
+ assigneeIdsInput.value = 0;
+ this.value = [];
+ this.selectedUserName = __('Unassigned');
+ },
+ onSelected(selected) {
+ assigneeIdsInput.value = selected.map((user) => getIdFromGraphQLId(user.id));
+ this.value = selected;
+ this.selectedUserName = selected.map((user) => user.name).join(', ');
+ },
+ },
+ render(h) {
+ const component = this;
+
+ return h(UserSelect, {
+ props: {
+ text: component.selectedUserName || __('Select assignee'),
+ headerText: __('Assign to'),
+ fullPath,
+ currentUser,
+ value: component.value,
+ },
+ on: {
+ input(selected) {
+ if (!selected.length) {
+ component.onSelectedUnassigned();
+ return;
+ }
+
+ component.onSelected(selected);
+ },
+ },
+ class: 'gl-w-full',
+ });
+ },
+ });
+}
+
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
diff --git a/app/assets/javascripts/super_sidebar/components/bottom_bar.vue b/app/assets/javascripts/super_sidebar/components/bottom_bar.vue
deleted file mode 100644
index fea29458f45..00000000000
--- a/app/assets/javascripts/super_sidebar/components/bottom_bar.vue
+++ /dev/null
@@ -1,24 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlIcon,
- },
- i18n: {
- help: __('Help'),
- new: __('New'),
- },
-};
-</script>
-
-<template>
- <div class="bottom-links gl-p-3">
- <a href="#" class="gl-text-black-normal"
- ><gl-icon name="question-o" class="gl-mr-3 gl-text-gray-300 gl-text-black-normal!" />{{
- $options.i18n.help
- }}</a
- >
- </div>
-</template>
diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue
new file mode 100644
index 00000000000..6c82a4bbf86
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/help_center.vue
@@ -0,0 +1,88 @@
+<script>
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlDisclosureDropdown,
+ },
+ i18n: {
+ help: __('Help'),
+ support: __('Support'),
+ docs: __('GitLab documentation'),
+ plans: __('Compare GitLab plans'),
+ forum: __('Community forum'),
+ contribute: __('Contribute to GitLab'),
+ feedback: __('Provide feedback'),
+ shortcuts: __('Keyboard shortcuts'),
+ whatsnew: __("What's new"),
+ },
+ props: {
+ sidebarData: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ items: [
+ {
+ items: [
+ { text: this.$options.i18n.help, href: helpPagePath() },
+ { text: this.$options.i18n.support, href: this.sidebarData.support_path },
+ { text: this.$options.i18n.docs, href: 'https://docs.gitlab.com' },
+ { text: this.$options.i18n.plans, href: `${PROMO_URL}/pricing` },
+ { text: this.$options.i18n.forum, href: 'https://forum.gitlab.com/' },
+ {
+ text: this.$options.i18n.contribute,
+ href: helpPagePath('', { anchor: 'contributing-to-gitlab' }),
+ },
+ { text: this.$options.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' },
+ ],
+ },
+ {
+ items: [
+ { text: this.$options.i18n.shortcuts, action: this.showKeyboardShortcuts },
+ this.sidebarData.display_whats_new && {
+ text: this.$options.i18n.whatsnew,
+ action: this.showWhatsNew,
+ },
+ ].filter(Boolean),
+ },
+ ],
+ };
+ },
+ methods: {
+ showKeyboardShortcuts() {
+ this.$refs.dropdown.close();
+ window?.toggleShortcutsHelp();
+ },
+ async showWhatsNew() {
+ this.$refs.dropdown.close();
+ if (!this.toggleWhatsNewDrawer) {
+ const appEl = document.getElementById('whats-new-app');
+ const { default: toggleWhatsNewDrawer } = await import(
+ /* webpackChunkName: 'whatsNewApp' */ '~/whats_new'
+ );
+ this.toggleWhatsNewDrawer = toggleWhatsNewDrawer;
+ this.toggleWhatsNewDrawer(appEl);
+ } else {
+ this.toggleWhatsNewDrawer();
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown
+ ref="dropdown"
+ icon="question-o"
+ :items="items"
+ :toggle-text="$options.i18n.help"
+ category="tertiary"
+ no-caret
+ />
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index 78d761e9a59..c4b769dcf24 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -4,7 +4,7 @@ import { context } from '../mock_data';
import UserBar from './user_bar.vue';
import ContextSwitcherToggle from './context_switcher_toggle.vue';
import ContextSwitcher from './context_switcher.vue';
-import BottomBar from './bottom_bar.vue';
+import HelpCenter from './help_center.vue';
export default {
context,
@@ -13,7 +13,7 @@ export default {
UserBar,
ContextSwitcherToggle,
ContextSwitcher,
- BottomBar,
+ HelpCenter,
},
props: {
sidebarData: {
@@ -32,7 +32,7 @@ export default {
<template>
<aside
id="super-sidebar"
- class="super-sidebar gl-fixed gl-bottom-0 gl-left-0 gl-display-flex gl-flex-direction-column gl-bg-gray-10 gl-border-r gl-border-gray-a-08 gl-z-index-9999"
+ class="super-sidebar gl-fixed gl-bottom-0 gl-left-0 gl-display-flex gl-flex-direction-column gl-bg-gray-10 gl-border-r gl-border-gray-a-08"
data-testid="super-sidebar"
>
<user-bar :sidebar-data="sidebarData" />
@@ -43,8 +43,8 @@ export default {
<context-switcher />
</gl-collapse>
</div>
- <div class="gl-px-3">
- <bottom-bar />
+ <div class="gl-p-3">
+ <help-center :sidebar-data="sidebarData" />
</div>
</div>
</aside>
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 86a99b8f0ed..0c8cd7fd95c 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -2,11 +2,11 @@
import { debounce } from 'lodash';
import {
GlDropdown,
- GlDropdownForm,
GlDropdownDivider,
+ GlDropdownForm,
GlDropdownItem,
- GlSearchBoxByType,
GlLoadingIcon,
+ GlSearchBoxByType,
GlTooltipDirective,
} from '@gitlab/ui';
import { __ } from '~/locale';
@@ -47,7 +47,8 @@ export default {
},
iid: {
type: String,
- required: true,
+ required: false,
+ default: null,
},
value: {
type: Array,
@@ -167,13 +168,10 @@ export default {
return this.$apollo.queries.searchUsers.loading || this.$apollo.queries.participants.loading;
},
users() {
- if (!this.participants) {
- return [];
- }
-
- const filteredParticipants = this.participants.filter(
- (user) => user.name.includes(this.search) || user.username.includes(this.search),
- );
+ const filteredParticipants =
+ this.participants?.filter(
+ (user) => user.name.includes(this.search) || user.username.includes(this.search),
+ ) || [];
// TODO this de-duplication is temporary (BE fix required)
// https://gitlab.com/gitlab-org/gitlab/-/issues/327822
@@ -254,6 +252,10 @@ export default {
this.$emit('input', selected);
}
},
+ unassign() {
+ this.$emit('input', []);
+ this.$refs.dropdown.hide();
+ },
unselect(name) {
const selected = this.value.filter((user) => user.username !== name);
this.$emit('input', selected);
@@ -323,7 +325,7 @@ export default {
:is-checked="selectedIsEmpty"
is-check-centered
data-testid="unassign"
- @click.native.capture.stop="$emit('input', [])"
+ @click.native.capture.stop="unassign"
>
<span :class="selectedIsEmpty ? 'gl-pl-0' : 'gl-pl-6'" class="gl-font-weight-bold">{{
$options.i18n.unassigned
diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js
index 41aff202f48..f9b725ed429 100644
--- a/app/assets/javascripts/whats_new/utils/notification.js
+++ b/app/assets/javascripts/whats_new/utils/notification.js
@@ -5,6 +5,8 @@ export const getVersionDigest = (appEl) => appEl.dataset.versionDigest;
export const setNotification = (appEl) => {
const versionDigest = getVersionDigest(appEl);
const notificationEl = document.querySelector('.header-help');
+ if (!notificationEl) return;
+
let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count');
if (localStorage.getItem(STORAGE_KEY) === versionDigest) {
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index 59a9df9ede0..575fbc03f46 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -1,6 +1,7 @@
.super-sidebar {
top: 0;
width: $contextual-sidebar-width;
+ z-index: 600;
.user-bar {
background-color: $t-gray-a-04;
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 5a19ce6149a..2ca6a46906c 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -42,7 +42,9 @@ module SidebarsHelper
assigned_open_merge_requests_count: user.assigned_open_merge_requests_count,
todos_pending_count: user.todos_pending_count,
issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
- create_new_menu_groups: create_new_menu_groups(group: group, project: project)
+ create_new_menu_groups: create_new_menu_groups(group: group, project: project),
+ support_path: support_url,
+ display_whats_new: display_whats_new?
}
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 9f0cd96a8f8..50696c7b5e1 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -92,10 +92,9 @@ module Issuable
validates :author, presence: true
validates :title, presence: true, length: { maximum: TITLE_LENGTH_MAX }
- # we validate the description against DESCRIPTION_LENGTH_MAX only for Issuables being created
- # to avoid breaking the existing Issuables which may have their descriptions longer
- validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create
- validate :description_max_length_for_new_records_is_valid, on: :update
+ # we validate the description against DESCRIPTION_LENGTH_MAX only for Issuables being created and on updates if
+ # the description changes to avoid breaking the existing Issuables which may have their descriptions longer
+ validates :description, bytesize: { maximum: -> { DESCRIPTION_LENGTH_MAX } }, if: :validate_description_length?
validate :validate_assignee_size_length, unless: :importing?
before_validation :truncate_description_on_import!
@@ -229,10 +228,14 @@ module Issuable
private
- def description_max_length_for_new_records_is_valid
- if new_record? && description.length > Issuable::DESCRIPTION_LENGTH_MAX
- errors.add(:description, :too_long, count: Issuable::DESCRIPTION_LENGTH_MAX)
- end
+ def validate_description_length?
+ return false unless description_changed?
+
+ previous_description = changes['description'].first
+ # previous_description will be nil for new records
+ return true if previous_description.blank?
+
+ previous_description.bytesize <= DESCRIPTION_LENGTH_MAX
end
def truncate_description_on_import!
diff --git a/app/models/concerns/sanitizable.rb b/app/models/concerns/sanitizable.rb
index 05756beb404..653d7a4875d 100644
--- a/app/models/concerns/sanitizable.rb
+++ b/app/models/concerns/sanitizable.rb
@@ -45,6 +45,15 @@ module Sanitizable
unless input.to_s == CGI.unescapeHTML(input.to_s)
record.errors.add(attr, 'cannot contain escaped HTML entities')
end
+
+ # This method raises an exception on failure so perform this
+ # last if multiple errors should be returned.
+ Gitlab::Utils.check_path_traversal!(input.to_s)
+
+ rescue Gitlab::Utils::DoubleEncodingError
+ record.errors.add(attr, 'cannot contain escaped components')
+ rescue Gitlab::Utils::PathTraversalAttackError
+ record.errors.add(attr, "cannot contain a path traversal component")
end
end
end
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 7f65fb3a378..aeb4d7a5694 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -14,10 +14,11 @@ class NamespaceSetting < ApplicationRecord
validates :enabled_git_access_protocol, inclusion: { in: enabled_git_access_protocols.keys }
- validate :default_branch_name_content
validate :allow_mfa_for_group
validate :allow_resource_access_token_creation_for_group
+ sanitizes! :default_branch_name
+
before_validation :normalize_default_branch_name
chronic_duration_attr :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval
@@ -45,8 +46,6 @@ class NamespaceSetting < ApplicationRecord
NAMESPACE_SETTINGS_PARAMS
end
- sanitizes! :default_branch_name
-
def prevent_sharing_groups_outside_hierarchy
return super if namespace.root?
@@ -85,14 +84,6 @@ class NamespaceSetting < ApplicationRecord
self.default_branch_name = default_branch_name.presence
end
- def default_branch_name_content
- return if default_branch_name.nil?
-
- if default_branch_name.blank?
- errors.add(:default_branch_name, "can not be an empty string")
- end
- end
-
def allow_mfa_for_group
if namespace&.subgroup? && allow_mfa_for_subgroups == false
errors.add(:allow_mfa_for_subgroups, _('is not allowed since the group is not top-level group.'))
diff --git a/app/services/packages/helm/extract_file_metadata_service.rb b/app/services/packages/helm/extract_file_metadata_service.rb
index e7373d8ea8f..77efa65f1d1 100644
--- a/app/services/packages/helm/extract_file_metadata_service.rb
+++ b/app/services/packages/helm/extract_file_metadata_service.rb
@@ -7,6 +7,10 @@ module Packages
class ExtractFileMetadataService
ExtractionError = Class.new(StandardError)
+ # Charts must be smaller than 1M because of the storage limitations of Kubernetes objects.
+ # based on https://helm.sh/docs/chart_template_guide/accessing_files/
+ MAX_FILE_SIZE = 1.megabytes.freeze
+
def initialize(package_file)
@package_file = package_file
end
@@ -42,6 +46,7 @@ module Packages
end
raise ExtractionError, 'Chart.yaml not found within a directory' unless chart_yaml
+ raise ExtractionError, 'Chart.yaml too big' if chart_yaml.size > MAX_FILE_SIZE
chart_yaml.read
ensure
diff --git a/app/views/admin/application_settings/appearances/_form.html.haml b/app/views/admin/application_settings/appearances/_form.html.haml
index 5f51e91436c..c20a86b9f9c 100644
--- a/app/views/admin/application_settings/appearances/_form.html.haml
+++ b/app/views/admin/application_settings/appearances/_form.html.haml
@@ -72,7 +72,7 @@
= f.hidden_field :logo_cache
= f.file_field :logo, class: "", accept: 'image/*'
.form-text.text-muted
- = _('Maximum file size is 1MB. Pages are optimized for a 640x360 px logo.')
+ = _('Maximum file size is 1 MB. Pages are optimized for a 128x128 px logo.')
%hr
.row
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 8e39ef7301d..d2ed70d6b48 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -4,6 +4,10 @@
- if show_super_sidebar?
- sidebar_data = super_sidebar_context(current_user, group: @group, project: @project).to_json
%aside.js-super-sidebar.nav-sidebar{ data: { root_path: root_path, sidebar: sidebar_data, toggle_new_nav_endpoint: profile_preferences_url } }
+
+ - if display_whats_new?
+ #whats-new-app{ data: { version_digest: whats_new_version_digest } }
+
- elsif defined?(nav) && nav
= render "layouts/nav/sidebar/#{nav}"
.content-wrapper.content-wrapper-margin{ class: "#{@content_wrapper_class}" }
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index da8477f4b2e..b125fe34464 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -20,9 +20,8 @@
.title
= _('Assignee')
.filter-item
- - field_name = "update[assignee_ids][]"
- = dropdown_tag(_("Select assignee"), options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: _("Assign to"), filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
- placeholder: _("Search authors"), data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
+ %input.js-assignee-ids-input{ type: "hidden", name: "update[assignee_ids][]" }
+ .js-assignee-dropdown{ data: { full_path: @project.full_path } }
- if is_issue
= render_if_exists 'shared/issuable/epic_dropdown', parent: @project.group
.block
diff --git a/db/post_migrate/20230127123947_add_fk_index_to_ci_sources_pipelines_on_source_partition_id_and_source_job_id.rb b/db/post_migrate/20230127123947_add_fk_index_to_ci_sources_pipelines_on_source_partition_id_and_source_job_id.rb
new file mode 100644
index 00000000000..f2a9d362cc4
--- /dev/null
+++ b/db/post_migrate/20230127123947_add_fk_index_to_ci_sources_pipelines_on_source_partition_id_and_source_job_id.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddFkIndexToCiSourcesPipelinesOnSourcePartitionIdAndSourceJobId < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = :index_ci_sources_pipelines_on_source_partition_id_source_job_id
+ TABLE_NAME = :ci_sources_pipelines
+ COLUMNS = [:source_partition_id, :source_job_id]
+
+ def up
+ add_concurrent_index(TABLE_NAME, COLUMNS, name: INDEX_NAME)
+ end
+
+ def down
+ remove_concurrent_index_by_name(TABLE_NAME, INDEX_NAME)
+ end
+end
diff --git a/db/post_migrate/20230127123948_add_fk_to_ci_sources_pipelines_on_source_partition_id_and_source_job_id.rb b/db/post_migrate/20230127123948_add_fk_to_ci_sources_pipelines_on_source_partition_id_and_source_job_id.rb
new file mode 100644
index 00000000000..7adf97268ff
--- /dev/null
+++ b/db/post_migrate/20230127123948_add_fk_to_ci_sources_pipelines_on_source_partition_id_and_source_job_id.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+class AddFkToCiSourcesPipelinesOnSourcePartitionIdAndSourceJobId < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ SOURCE_TABLE_NAME = :ci_sources_pipelines
+ TARGET_TABLE_NAME = :ci_builds
+ COLUMN = :source_job_id
+ TARGET_COLUMN = :id
+ FK_NAME = :fk_be5624bf37_p
+ PARTITION_COLUMN = :source_partition_id
+
+ def up
+ add_concurrent_foreign_key(
+ SOURCE_TABLE_NAME,
+ TARGET_TABLE_NAME,
+ column: [PARTITION_COLUMN, COLUMN],
+ target_column: [:partition_id, TARGET_COLUMN],
+ validate: false,
+ reverse_lock_order: true,
+ on_update: :cascade,
+ on_delete: :cascade,
+ name: FK_NAME
+ )
+ end
+
+ def down
+ with_lock_retries do
+ remove_foreign_key_if_exists(
+ SOURCE_TABLE_NAME,
+ TARGET_TABLE_NAME,
+ name: FK_NAME,
+ reverse_lock_order: true
+ )
+ end
+ end
+end
diff --git a/db/schema_migrations/20230127123947 b/db/schema_migrations/20230127123947
new file mode 100644
index 00000000000..cc3981f1302
--- /dev/null
+++ b/db/schema_migrations/20230127123947
@@ -0,0 +1 @@
+dd57ab40a4263df49d8f52f8f737c5cc62101f932752cbb984cb6072d766d6f0 \ No newline at end of file
diff --git a/db/schema_migrations/20230127123948 b/db/schema_migrations/20230127123948
new file mode 100644
index 00000000000..0fa5c113d57
--- /dev/null
+++ b/db/schema_migrations/20230127123948
@@ -0,0 +1 @@
+aec7695c7e1cd2eb61625c1c08f7d8ee955bd729a8d70ea2753afcb7b545bfe6 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 6475c5e7b9b..3aba342d1db 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -29283,6 +29283,8 @@ CREATE INDEX index_ci_sources_pipelines_on_project_id ON ci_sources_pipelines US
CREATE INDEX index_ci_sources_pipelines_on_source_job_id ON ci_sources_pipelines USING btree (source_job_id);
+CREATE INDEX index_ci_sources_pipelines_on_source_partition_id_source_job_id ON ci_sources_pipelines USING btree (source_partition_id, source_job_id);
+
CREATE INDEX index_ci_sources_pipelines_on_source_pipeline_id ON ci_sources_pipelines USING btree (source_pipeline_id);
CREATE INDEX index_ci_sources_pipelines_on_source_project_id ON ci_sources_pipelines USING btree (source_project_id);
@@ -34153,6 +34155,9 @@ ALTER TABLE ONLY snippets
ALTER TABLE ONLY ci_sources_pipelines
ADD CONSTRAINT fk_be5624bf37 FOREIGN KEY (source_job_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
+ALTER TABLE ONLY ci_sources_pipelines
+ ADD CONSTRAINT fk_be5624bf37_p FOREIGN KEY (source_partition_id, source_job_id) REFERENCES ci_builds(partition_id, id) ON UPDATE CASCADE ON DELETE CASCADE NOT VALID;
+
ALTER TABLE ONLY packages_maven_metadata
ADD CONSTRAINT fk_be88aed360 FOREIGN KEY (package_id) REFERENCES packages_packages(id) ON DELETE CASCADE;
diff --git a/doc/api/groups.md b/doc/api/groups.md
index dd3f8baf338..c39b8584e93 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1201,7 +1201,8 @@ GET /groups?search=foobar
> Introduced in GitLab 14.8.
-Get a list of users provisioned by a given group. Does not include users provisioned by subgroups.
+Get a list of users provisioned by a given group. Does not include subgroups.
+Users in this list are considered [enterprise users](../user/enterprise_user/index.md).
Requires at least the Maintainer role on the group.
diff --git a/doc/api/members.md b/doc/api/members.md
index 2da84866b92..4032ab1d651 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -29,8 +29,7 @@ In GitLab 14.8 and earlier, projects in personal namespaces have an `access_leve
The `group_saml_identity` attribute is only visible to a group owner for [SSO enabled groups](../user/group/saml_sso/index.md).
-The `email` attribute is only visible to group owners when the user was provisioned by the group.
-Users are provisioned by the group when the account was created via [SCIM](../user/group/saml_sso/scim_setup.md) or by first sign-in with [SAML SSO for GitLab.com groups](../user/group/saml_sso/index.md).
+The `email` attribute is only visible to group Owners for any [enterprise user](../user/enterprise_user/index.md).
## List all members of a group or project
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index 7d21dd594ad..84933bc132b 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -82,17 +82,18 @@ This approach reduces disruption to users responsible for deploying runners.
### Reusing the runner authentication token across many machines
-In the existing model, a new runner is created whenever a new worker is required. This
-has led to many situations where runners are left behind and become stale.
+In the existing autoscaling model, a new runner is created whenever a new job needs to be executed.
+This has led to many situations where runners are left behind and become stale.
In the proposed model, a `ci_runners` table entry describes a configuration that the user can reuse
-across multiple machines.
+across multiple machines, and runner state from each machine (for example, IP address, platform,
+or architecture) is moved to a separate table (`ci_runner_machines`).
A unique system identifier is [generated automatically](#generating-a-system_id-value) whenever the
runner application starts up or the configuration is saved.
-This allows differentiating the context in which the runner is being used.
+This allows differentiating the machine in which the runner is being used.
-The `system_id` value complements the short runner token that is currently used to identify a
-runner in command line output, CI job logs, and GitLab UI.
+The `system_id` value complements the short runner token that is used to identify a runner in
+command line output, CI job logs, and GitLab UI.
Given that the creation of runners involves user interaction, it should be possible
to eventually lower the per-plan limit of CI runners that can be registered per scope.
@@ -295,8 +296,11 @@ enum column created in the `ci_runners` table.
### Runner creation through API
-Automated runner creation may be allowed, although always through authenticated API calls -
-using PAT tokens for example - such that every runner is associated with an owner.
+Automated runner creation is possible through a new GraphQL mutation and the existing
+[`POST /runners` REST API endpoint](../../../api/runners.md#register-a-new-runner).
+The difference in the REST API endpoint is that it is modified to accept a request from an
+authorized user with a scope (instance, a group, or a project) instead of the registration token.
+These endpoints are only available to users that are allowed to create runners at the specified scope.
## Implementation plan
@@ -315,7 +319,7 @@ using PAT tokens for example - such that every runner is associated with an owne
| Component | Milestone | Changes |
|------------------|----------:|---------|
| GitLab Runner | `15.7` | Ensure a sidecar TOML file exists with a `system_id` value.<br/>Log new system ID values with `INFO` level as they get assigned. |
-| GitLab Runner | `15.7` | Log unique system ID in the build logs. |
+| GitLab Runner | `15.9` | Log unique system ID in the build logs. |
| GitLab Runner | `15.9` | Label Prometheus metrics with unique system ID. |
| GitLab Runner | `15.8` | Prepare `register` command to fail if runner server-side configuration options are passed together with a new `glrt-` token. |
@@ -327,21 +331,23 @@ using PAT tokens for example - such that every runner is associated with an owne
| GitLab Rails app | `%15.8` | Create database migration to add `ci_runner_machines` table. |
| GitLab Rails app | `%15.9` | Create database migration to add `ci_runner_machines.id` foreign key to `ci_builds_metadata` table. |
| GitLab Rails app | `%15.8` | Create database migrations to add `allow_runner_registration_token` setting to `application_settings` and `namespace_settings` tables (default: `true`). |
-| GitLab Rails app | `%15.8` | Create database migration to add config column to `ci_runner_machines` table. |
-| GitLab Runner | | Start sending `system_id` value in `POST /jobs/request` request and other follow-up requests that require identifying the unique system. |
-| GitLab Rails app | | Create service similar to `StaleGroupRunnersPruneCronWorker` service to clean up `ci_runner_machines` records instead of `ci_runners` records.<br/>Existing service continues to exist but focuses only on legacy runners. |
-| GitLab Rails app | | Create `ci_runner_machines` record in `POST /runners/verify` request if the runner token is prefixed with `glrt-`. |
-| GitLab Rails app | | Use runner token + `system_id` JSON parameters in `POST /jobs/request` request in the [heartbeat request](https://gitlab.com/gitlab-org/gitlab/blob/c73c96a8ffd515295842d72a3635a8ae873d688c/lib/api/ci/helpers/runner.rb#L14-20) to update the `ci_runner_machines` cache/table. |
+| GitLab Rails app | `%15.8` | Create database migration to add `config` column to `ci_runner_machines` table. |
+| GitLab Runner | `%15.9` | Start sending `system_id` value in `POST /jobs/request` request and other follow-up requests that require identifying the unique system. |
+| GitLab Rails app | `%15.9` | Create service similar to `StaleGroupRunnersPruneCronWorker` service to clean up `ci_runner_machines` records instead of `ci_runners` records.<br/>Existing service continues to exist but focuses only on legacy runners. |
+| GitLab Rails app | `%15.9` | Create `ci_runner_machines` record in `POST /runners/verify` request if the runner token is prefixed with `glrt-`. |
+| GitLab Rails app | `%15.9` | Use runner token + `system_id` JSON parameters in `POST /jobs/request` request in the [heartbeat request](https://gitlab.com/gitlab-org/gitlab/blob/c73c96a8ffd515295842d72a3635a8ae873d688c/lib/api/ci/helpers/runner.rb#L14-20) to update the `ci_runner_machines` cache/table. |
+| GitLab Rails app | `%15.9` | [Feature flag] Enable runner creation workflow (`create_runner_workflow`). |
### Stage 4 - New UI
| Component | Milestone | Changes |
|------------------|----------:|---------|
-| GitLab Rails app | `%15.10` | Implement new GraphQL user-authenticated API to create a new runner. |
-| GitLab Rails app | `%15.10` | [Add prefix to newly generated runner authentication tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/383198). |
+| GitLab Rails app | `%15.9` | Implement new GraphQL user-authenticated API to create a new runner. |
+| GitLab Rails app | `%15.9` | [Add prefix to newly generated runner authentication tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/383198). |
| GitLab Rails app | `%15.10` | Implement UI to create new runner. |
| GitLab Rails app | `%15.10` | GraphQL changes to `CiRunner` type. |
| GitLab Rails app | `%15.10` | UI changes to runner details view (listing of platform, architecture, IP address, etc.) (?) |
+| GitLab Rails app | `%15.11` | Adapt `POST /api/v4/runners` REST endpoint to accept a request from an authorized user with a scope instead of a registration token. |
### Stage 5 - Optional disabling of registration token
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index a51fbd6188d..663900006dd 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -1112,6 +1112,12 @@ Caches are:
- By default, not shared between [protected](../../user/project/protected_branches.md) and unprotected branches.
- Restored before [artifacts](#artifacts).
+You can [disable caching for specific jobs](../caching/index.md#disable-cache-for-specific-jobs),
+for example to override:
+
+- A default cache defined with [`default`](#default).
+- The configuration for a job added with [`include`](#include).
+
Learn more about caches in [Caching in GitLab CI/CD](../caching/index.md).
#### `cache:paths`
diff --git a/doc/development/database/avoiding_downtime_in_migrations.md b/doc/development/database/avoiding_downtime_in_migrations.md
index 833f9dbebf6..a18e998f1d2 100644
--- a/doc/development/database/avoiding_downtime_in_migrations.md
+++ b/doc/development/database/avoiding_downtime_in_migrations.md
@@ -450,11 +450,10 @@ To start the process, add a regular migration to create the new `bigint` columns
to keep in sync both columns for any new records ([see an example](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/migrate/20210608072312_initialize_conversion_of_ci_stages_to_bigint.rb)):
```ruby
-class InitializeConversionOfCiStagesToBigint < ActiveRecord::Migration[6.1]
- include Gitlab::Database::MigrationHelpers
+class InitializeConversionOfCiStagesToBigint < Gitlab::Database::Migration[2.1]
TABLE = :ci_stages
- COLUMNS = %i(id)
+ COLUMNS = %i[id]
def up
initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
@@ -484,11 +483,10 @@ and collecting metrics. To start the process, use the provided `backfill_convers
helper ([example](https://gitlab.com/gitlab-org/gitlab/-/blob/41fbe34a4725a4e357a83fda66afb382828767b2/db/migrate/20210608072346_backfill_ci_stages_for_bigint_conversion.rb)):
```ruby
-class BackfillCiStagesForBigintConversion < ActiveRecord::Migration[6.1]
- include Gitlab::Database::MigrationHelpers
+class BackfillCiStagesForBigintConversion < Gitlab::Database::Migration[2.1]
TABLE = :ci_stages
- COLUMNS = %i(id)
+ COLUMNS = %i[id]
def up
backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
diff --git a/doc/user/admin_area/appearance.md b/doc/user/admin_area/appearance.md
index d9bdf964aed..a1fae7e8712 100644
--- a/doc/user/admin_area/appearance.md
+++ b/doc/user/admin_area/appearance.md
@@ -60,7 +60,7 @@ to activate it in the GitLab instance.
You can replace the default message on the sign in / sign up page with your own message
and logo. You can make full use of [Markdown](../markdown.md) in the description.
-The optimal size for the logo is 640 x 360 pixels, but any image can be used (below 1 MB)
+The optimal size for the logo is 128 x 128 pixels, but any image can be used (below 1 MB)
and it is resized automatically. The logo image appears between the title and
the description, on the left of the sign-up page.
diff --git a/doc/user/enterprise_user/index.md b/doc/user/enterprise_user/index.md
new file mode 100644
index 00000000000..3daee460956
--- /dev/null
+++ b/doc/user/enterprise_user/index.md
@@ -0,0 +1,71 @@
+---
+stage: Manage
+group: Authentication and Authorization
+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
+type: reference
+---
+
+# Enterprise users **(PREMIUM SAAS)**
+
+Enterprise users have user accounts that are administered by an organization that
+has purchased a [GitLab subscription](../../subscriptions/index.md).
+
+Enterprise users are identified by the [**Enterprise** badge](../project/badges.md)
+next to their names on the [Members list](../group/manage.md#filter-and-sort-members-in-a-group).
+
+## Provision an enterprise user
+
+A user account is considered an enterprise account when:
+
+- A user without an existing GitLab user account uses the group's
+ [SAML SSO](../group/saml_sso/index.md) to sign in for the first time.
+- [SCIM](../group/saml_sso/scim_setup.md) creates the user account on behalf of
+ the group.
+
+A user can also [manually connect an identity provider (IdP) to a GitLab account whose email address matches the subscribing organization's domain](../group/saml_sso/index.md#linking-saml-to-your-existing-gitlabcom-account).
+By selecting **Authorize** when connecting these two accounts, the user account
+with the matching email address is classified as an enterprise user. However, this
+user account does not have an **Enterprise** badge in GitLab.
+
+Although a user can be a member of more than one group, each user account can be
+provisioned by only one group. As a result, a user is considered an enterprise
+user under one top-level group only.
+
+## Manage enterprise users in a namespace
+
+A top-level Owner of a namespace on a paid plan can retrieve information about and
+manage enterprise user accounts in that namespace.
+
+These enterprise user-specific actions are in addition to the standard
+[group member permissions](../permissions.md#group-members-permissions).
+
+### Disable two-factor authentication
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9484) in GitLab 15.8.
+
+Top-level group Owners can disable two-factor authentication (2FA) for enterprise users.
+
+To disable 2FA:
+
+1. On the top bar, select **Main menu > Groups** and find your group.
+1. On the left sidebar, select **Group information > Members**.
+1. Find a user with the **Enterprise** and **2FA** badges.
+1. Select **More actions** (**{ellipsis_v}**) and select **Disable two-factor authentication**.
+
+### Prevent users from creating groups and projects outside the corporate group
+
+A SAML identity administrator can configure the SAML response to set:
+
+- Whether users can create groups.
+- The maximum number of personal projects users can create.
+
+For more information, see the [supported user attributes for SAML responses](../group/saml_sso/index.md#supported-user-attributes).
+
+### Bypass email confirmation for provisioned users
+
+A top-level group Owner can [set up verified domains to bypass confirmation emails](../group/saml_sso/index.md#bypass-user-email-confirmation-with-verified-domains).
+
+### Get users' email addresses through the API
+
+A top-level group Owner can use the [group and project members API](../../api/members.md)
+to access users' information, including email addresses.
diff --git a/doc/user/group/manage.md b/doc/user/group/manage.md
index b524a17eae3..026e5acd6cb 100644
--- a/doc/user/group/manage.md
+++ b/doc/user/group/manage.md
@@ -131,7 +131,7 @@ Filter a group to find members. By default, all members in the group and subgrou
In lists of group members, entries can display the following badges:
- **SAML**, to indicate the member has a [SAML account](saml_sso/index.md) connected to them.
-- **Enterprise**, to indicate that [SCIM created the account](saml_sso/scim_setup.md).
+- **Enterprise**, to indicate that the member is an [enterprise user](../enterprise_user/index.md).
1. On the top bar, select **Main menu > Groups** and find your group.
1. Above the list of members, in the **Filter members** box, enter filter criteria.
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index 89536269305..2f12ff68723 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -347,7 +347,7 @@ To migrate users to a new email domain, users must:
## User access and management
> - SAML user provisioning [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/268142) in GitLab 13.7.
-> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325712) in GitLab 14.0, GitLab users created by [SAML SSO](index.md#user-access-and-management) or SCIM provisioning are displayed with an **Enterprise** badge in the **Members** view.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325712) in GitLab 14.0, GitLab users created by [SAML SSO](index.md#user-access-and-management) or SCIM provisioning are displayed with an ][**Enterprise**](../../enterprise_user/index.md) badge in the **Members** view.
After group SSO is configured and enabled, users can access the GitLab.com group through the identity provider's dashboard.
If [SCIM](scim_setup.md) is configured, see [user access](scim_setup.md#user-access) on the SCIM page.
@@ -445,8 +445,9 @@ convert the information to XML. An example SAML response is shown here.
By default, users provisioned with SAML or SCIM are sent a verification email to verify their identity. Instead, you can
[configure GitLab with a custom domain](../../project/pages/custom_domains_ssl_tls_certification/index.md) and GitLab
-automatically confirms user accounts. Users still receive an enterprise user welcome email. Confirmation is bypassed for
-users:
+automatically confirms user accounts. Users still receive an
+[enterprise user](../../enterprise_user/index.md) welcome email. Confirmation is
+bypassed for users:
- That are provisioned with SAML or SCIM.
- That have an email address that belongs to the verified domain.
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
index 8c30c246566..79fc1ab310a 100644
--- a/doc/user/group/saml_sso/scim_setup.md
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -170,7 +170,7 @@ encounter issues.
## User access
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325712) in GitLab 14.0, GitLab users created by [SAML SSO](index.md#user-access-and-management) or SCIM provisioning are displayed with an **Enterprise** badge in the **Members** view.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/325712) in GitLab 14.0, GitLab users created by [SAML SSO](index.md#user-access-and-management) or SCIM provisioning are displayed with an [**Enterprise**](../../enterprise_user/index.md) badge in the **Members** view.
During the synchronization process, all new users:
diff --git a/doc/user/packages/container_registry/build_and_push_images.md b/doc/user/packages/container_registry/build_and_push_images.md
index bbb82300488..aa86b87660b 100644
--- a/doc/user/packages/container_registry/build_and_push_images.md
+++ b/doc/user/packages/container_registry/build_and_push_images.md
@@ -144,7 +144,7 @@ In this example, `$CI_REGISTRY_IMAGE` resolves to the address of the registry ti
to this project. `$CI_COMMIT_REF_NAME` resolves to the branch or tag name, which
can contain forward slashes. Image tags can't contain forward slashes. Use
`$CI_COMMIT_REF_SLUG` as the image tag. You can declare the variable, `$IMAGE_TAG`,
-combining `$CI_REGISTRY_IMAGE` and `$CI_REGISTRY_IMAGE` to save some typing in the
+combining `$CI_REGISTRY_IMAGE` and `$CI_COMMIT_REF_NAME` to save some typing in the
`script` section.
This example splits the tasks into 4 pipeline stages, including two tests that run in parallel. The `build` is stored in the container
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index ba5fddc9faf..11e3d0e5131 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -264,6 +264,22 @@ The GitLab npm repository supports the following commands for the npm CLI (`npm`
## Troubleshooting
+### `404 Not Found` errors are happening on `npm install` or `yarn`
+
+Using `CI_JOB_TOKEN` to install npm packages with dependencies in another project gives you 404 Not Found errors. A fix for this problem is proposed in [issue 352962](https://gitlab.com/gitlab-org/gitlab/-/issues/352962).
+
+As a workaround, you can:
+
+1. Create a [personal access token](../../profile/personal_access_tokens.md).
+1. Authenticate at both the instance level and project level for each package:
+
+ ```ini
+ @foo:registry=https://gitlab.example.com/api/v4/packages/npm/
+ //gitlab.example.com/api/v4/packages/npm/:_authToken=${MY_TOKEN}
+ //gitlab.example.com/api/v4/projects/<your_project_id_a>/packages/npm/:_authToken=${MY_TOKEN}
+ //gitlab.example.com/api/v4/projects/<your_project_id_b>/packages/npm/:_authToken=${MY_TOKEN}
+ ```
+
### `npm publish` targets default npm registry (`registry.npmjs.org`)
Ensure that your package scope is set consistently in your `package.json` and `.npmrc` files.
diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
index d1ee42f723d..4dda68a6d08 100644
--- a/doc/user/project/members/share_project_with_groups.md
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -4,60 +4,41 @@ group: Organization
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
---
-# Share projects with other groups **(FREE)**
+# Share a project with a group **(FREE)**
-You can share projects with other [groups](../../group/index.md). This makes it
-possible to add a group of users to a project with a single action.
+When you want a group to have access to your project,
+you can invite [a group](../../group/index.md) to the project.
+The group's members get access to the project, which becomes a *shared project*.
-For example, if `Project A` belongs to `Group 1`, the members of `Group 1` have access to the project.
-If `Project A` already belongs to another `Group 2`, the owner of `Group 2` can share `Project A`
-with `Group 1`, so that both members of `Group 1` and `Group 2` have access to the project.
+## Example
-When a project is shared with a group:
+For a project that was created by `Group 1`:
-- All group members, including members of subgroups or projects that belong to the group,
- are assigned the same role in the project.
- Each member's role is displayed in **Project information > Members**, in the **Max role** column.
- When sharing a project with a group, a user's assigned **Max role** is the lowest
- of either:
+- The members of `Group 1` have access to the project.
+- The owner of `Group 1` can invite `Group 2` to the project.
+This way, members of both `Group 1` and `Group 2` have access to the shared project.
- - The role assigned in the group membership.
- - The maximum role selected when sharing the project with the group.
+## Prerequisites
- Assigning a higher maximum role to the group doesn't give group users higher roles than
- the roles already assigned to them in the group.
-- The group is listed in the **Groups** tab.
-- The project is listed on the group dashboard.
+To invite a group to a project, you must be at least one of the following:
-Be aware of the restrictions that apply when you share projects with:
+- Explicitly defined as a [member](index.md) of the project.
+- Explicitly defined as a member of a group or subgroup that has access to the project.
+- An administrator.
-- [Groups with a more restrictive visibility level](#share-projects-with-groups-with-a-more-restrictive-visibility-level).
-- [Restricted sharing](#prevent-project-sharing).
+In addition:
-## Share projects with groups with a more restrictive visibility level
+- The group you're inviting must have a more restrictive
+ [visibility level](../../public_access.md#project-and-group-visibility)
+ than the project. For example, you can invite:
+ - A private group to a public project.
+ - An internal group to a public project.
+ - A private group to an internal project.
-You can share projects only down the group's organization structure.
-This means you can share a project with a group that has a more restrictive
-[visibility level](../../public_access.md#project-and-group-visibility) than the project,
-but not with a group that has a less restrictive visibility level.
-
-For example, you can share:
-
-- A public project with a private group.
-- A public project with an internal group.
-- An internal project with a private group.
-
-This restriction applies to subgroups as well. For example, `group/subgroup01/project`:
-
-- Can't be shared with `group`.
-- Can be shared with `group/subgroup02` or `group/subgroup01/subgroup03`.
-
-When you share a project with a group that has a more restrictive visibility level than the project:
-
-- The group name is visible to all users that can view the project members page.
-- Owners of the project have access to members of the group when they mention them in issues or merge requests.
-- Project members who are direct or indirect members of the group can see
-group members listed in addition to members of the project.
+- The group or subgroup must be in the project's [namespace](../../namespace/index.md).
+ For example, a project in the namespace `group/subgroup01/project`:
+ - Can be shared with `group/subgroup02` or `group/subgroup01/subgroup03`.
+ - Cannot be shared with `group`.
## Share a project with a group
@@ -68,22 +49,57 @@ group members listed in addition to members of the project.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/352526) in GitLab 14.9.
[Feature flag `invite_members_group_modal`](https://gitlab.com/gitlab-org/gitlab/-/issues/352526) removed.
-You can share a project only with groups:
+You can share a project with a group by inviting that group to the project.
-- Where you have an explicitly defined [membership](index.md).
-- That contain a nested subgroup or project you have an explicitly defined role for.
-- You are an administrator of.
-
-To share a project with a group:
+To invite a group to a project:
1. On the top bar, select **Main menu > Projects** and find your project.
-1. In the left navigation menu, select **Project information > Members**.
+1. On the left sidebar, select **Project information > Members**.
1. Select **Invite a group**.
1. **Select a group** you want to add to the project.
1. **Select a role** you want to assign to the group.
1. Optional. Select an **Access expiration date**.
1. Select **Invite**.
-## Prevent project sharing
+All group members, members of subgroups, and members of other projects the group has access to
+are given access to the project. In addition:
+
+- On the group's page, the project is listed on the **Shared projects** tab.
+- On the project's **Members** page, the group is listed on the **Groups** tab.
+- Each user is assigned a maximum role.
+
+## Maximum role
+
+When multiple groups contain the same members, and the groups
+have access to the same project, the group members are
+given the most restrictive role for the project.
+
+This most restrictive role is called the *maximum role*, or **Max role**.
+
+The member's **Max role** is the more restrictive of:
+
+- The role the user is assigned for the group.
+- The role you chose when you invited the group to the project.
+
+### View the member's Max role
+
+To view the maximum role assigned to a member:
+
+1. On the top bar, select **Main menu > Projects** and find your project.
+1. On the left sidebar, select **Project information > Members**.
+1. In the **Max role** column, view the user's maximum assigned role.
+
+## View a group's shared projects
+
+In a group, a shared project is a project to which the group members gained access through the [**Invite group**](#share-a-project-with-a-group) action.
+
+To view a group's shared projects:
+
+1. On the top bar, select **Main menu > Group** and find your group.
+1. On the group page, select the **Shared projects** tab.
+
+A list of shared projects is displayed.
+
+## Related topics
-For more information, see [Prevent a project from being shared with groups](../../group/access_and_permissions.md#prevent-a-project-from-being-shared-with-groups).
+- [Prevent a project from being shared with groups](../../group/access_and_permissions.md#prevent-a-project-from-being-shared-with-groups).
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 81b96207863..4b7fe6bdc7a 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -209,6 +209,7 @@ module API
mount ::API::DeployKeys
mount ::API::DeployTokens
mount ::API::Deployments
+ mount ::API::DraftNotes
mount ::API::Environments
mount ::API::ErrorTracking::ClientKeys
mount ::API::ErrorTracking::ProjectSettings
diff --git a/lib/api/draft_notes.rb b/lib/api/draft_notes.rb
new file mode 100644
index 00000000000..ece4e1a6584
--- /dev/null
+++ b/lib/api/draft_notes.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module API
+ class DraftNotes < ::API::Base
+ before { authenticate! }
+
+ urgency :low
+
+ helpers do
+ def merge_request(params:)
+ strong_memoize(:merge_request) do
+ find_project_merge_request(params[:merge_request_iid])
+ end
+ end
+
+ def load_draft_notes(params:)
+ merge_request(params: params).draft_notes.authored_by(current_user)
+ end
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc "Get a list of merge request draft notes" do
+ success Entities::DraftNote
+ is_array true
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ end
+ params do
+ requires :id, type: String, desc: "The ID of a project"
+ requires :merge_request_iid, type: Integer, desc: "The ID of a merge request"
+ end
+ get ":id/merge_requests/:merge_request_iid/draft_notes", feature_category: :code_review_workflow do
+ present load_draft_notes(params: params), with: Entities::DraftNote
+ end
+ end
+ end
+end
diff --git a/lib/api/entities/draft_note.rb b/lib/api/entities/draft_note.rb
new file mode 100644
index 00000000000..70b32bac502
--- /dev/null
+++ b/lib/api/entities/draft_note.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class DraftNote < Grape::Entity
+ expose :id, documentation: { type: 'integer', example: 2 }
+ expose :author_id, documentation: { type: 'integer', example: 4 }
+ expose :merge_request_id, documentation: { type: 'integer', example: 52 }
+ expose :resolve_discussion, documentation: { type: 'boolean', example: true }
+ expose :discussion_id, documentation: { type: 'integer', example: 613 }
+ expose :note, documentation: { type: 'string', example: 'This is a note' }
+ expose :commit_id, documentation: { type: 'integer', example: 4 }
+ expose :line_code, documentation: { type: 'string', example: '1c497fbb3a46b78edf0_2_4' }
+ expose :position, documentation: {
+ type: 'Hash',
+ example: {
+ base_sha: "aa149113",
+ start_sha: "b3a0a8c4",
+ head_sha: "be3020c7",
+ old_path: "example.md",
+ new_path: "example.md",
+ position_type: "text",
+ old_line: 2,
+ new_line: 4,
+ line_range: {
+ start: {
+ line_code: "1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4",
+ type: nil,
+ old_line: 2,
+ new_line: 4
+ },
+ end: {
+ line_code: "1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_2_4",
+ type: nil,
+ old_line: 2,
+ new_line: 4
+ }
+ }
+ }
+ } do |note|
+ note.position.to_h
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/artifact_file_reader.rb b/lib/gitlab/ci/artifact_file_reader.rb
index b0fad026ec5..2eb8df01d58 100644
--- a/lib/gitlab/ci/artifact_file_reader.rb
+++ b/lib/gitlab/ci/artifact_file_reader.rb
@@ -9,6 +9,7 @@ module Gitlab
Error = Class.new(StandardError)
MAX_ARCHIVE_SIZE = 5.megabytes
+ TMP_ARTIFACT_EXTRACTION_DIR = "extracted_artifacts_job_%{id}"
def initialize(job)
@job = job
@@ -45,20 +46,20 @@ module Gitlab
end
def read_zip_file!(file_path)
- job.artifacts_file.use_open_file do |file|
- zip_file = Zip::File.new(file, false, true)
- entry = zip_file.find_entry(file_path)
+ dir_name = format(TMP_ARTIFACT_EXTRACTION_DIR, id: job.id.to_i)
- unless entry
- raise Error, "Path `#{file_path}` does not exist inside the `#{job.name}` artifacts archive!"
+ job.artifacts_file.use_open_file(unlink_early: false) do |tmp_open_file|
+ Dir.mktmpdir(dir_name) do |tmp_dir|
+ SafeZip::Extract.new(tmp_open_file.file_path).extract(files: [file_path], to: tmp_dir)
+ File.read(File.join(tmp_dir, file_path))
end
-
- if entry.name_is_directory?
- raise Error, "Path `#{file_path}` was expected to be a file but it was a directory!"
- end
-
- zip_file.read(entry)
end
+ rescue SafeZip::Extract::NoMatchingError
+ raise Error, "Path `#{file_path}` does not exist inside the `#{job.name}` artifacts archive!"
+ rescue SafeZip::Extract::EntrySizeError
+ raise Error, "Path `#{file_path}` has invalid size in the zip!"
+ rescue Errno::EISDIR
+ raise Error, "Path `#{file_path}` was expected to be a file but it was a directory!"
end
def max_archive_size_in_mb
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index 65572c237ec..b92e7dbb725 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -4,6 +4,7 @@ module Gitlab
module Utils
extend self
PathTraversalAttackError ||= Class.new(StandardError)
+ DoubleEncodingError ||= Class.new(StandardError)
private_class_method def logger
@logger ||= Gitlab::AppLogger
@@ -55,7 +56,7 @@ module Gitlab
def decode_path(encoded_path)
decoded = CGI.unescape(encoded_path)
if decoded != CGI.unescape(decoded)
- raise StandardError, "path #{encoded_path} is not allowed"
+ raise DoubleEncodingError, "path #{encoded_path} is not allowed"
end
decoded
diff --git a/lib/safe_zip/entry.rb b/lib/safe_zip/entry.rb
index 52d70e83154..88647b9b1eb 100644
--- a/lib/safe_zip/entry.rb
+++ b/lib/safe_zip/entry.rb
@@ -25,8 +25,8 @@ module SafeZip
end
def extract
- # do not extract if file is not part of target directory
- return false unless matching_target_directory
+ # do not extract if file is not part of target directory or target file
+ return false unless matching_target_directory || matching_target_file
# do not overwrite existing file
raise SafeZip::Extract::AlreadyExistsError, "File already exists #{zip_entry.name}" if exist?
@@ -44,6 +44,8 @@ module SafeZip
end
rescue SafeZip::Extract::Error
raise
+ rescue Zip::EntrySizeError => e
+ raise SafeZip::Extract::EntrySizeError, e.message
rescue StandardError => e
raise SafeZip::Extract::ExtractError, e.message
end
@@ -84,6 +86,10 @@ module SafeZip
params.matching_target_directory(path)
end
+ def matching_target_file
+ params.matching_target_file(path)
+ end
+
def read_symlink
zip_archive.read(zip_entry)
end
diff --git a/lib/safe_zip/extract.rb b/lib/safe_zip/extract.rb
index 74df7895afe..b86941e6bea 100644
--- a/lib/safe_zip/extract.rb
+++ b/lib/safe_zip/extract.rb
@@ -6,6 +6,7 @@ module SafeZip
PermissionDeniedError = Class.new(Error)
SymlinkSourceDoesNotExistError = Class.new(Error)
UnsupportedEntryError = Class.new(Error)
+ EntrySizeError = Class.new(Error)
AlreadyExistsError = Class.new(Error)
NoMatchingError = Class.new(Error)
ExtractError = Class.new(Error)
diff --git a/lib/safe_zip/extract_params.rb b/lib/safe_zip/extract_params.rb
index bd3b788bac9..96881ad1abc 100644
--- a/lib/safe_zip/extract_params.rb
+++ b/lib/safe_zip/extract_params.rb
@@ -4,11 +4,13 @@ module SafeZip
class ExtractParams
include Gitlab::Utils::StrongMemoize
- attr_reader :directories, :extract_path
+ attr_reader :directories, :files, :extract_path
- def initialize(directories:, to:)
+ def initialize(to:, directories: [], files: [])
@directories = directories
+ @files = files
@extract_path = ::File.realpath(to)
+ validate!
end
def matching_target_directory(path)
@@ -32,5 +34,23 @@ module SafeZip
end
end
end
+
+ def matching_target_file(path)
+ target_files.include?(path)
+ end
+
+ private
+
+ def target_files
+ strong_memoize(:target_files) do
+ files.map do |file|
+ ::File.join(extract_path, file)
+ end
+ end
+ end
+
+ def validate!
+ raise ArgumentError, 'Either directories or files are required' if directories.empty? && files.empty?
+ end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1eafc5c0049..590bebb49bd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3649,15 +3649,6 @@ msgstr ""
msgid "After a successful password update, you will be redirected to the login page where you can log in with your new password."
msgstr ""
-msgid "After it expires, you can't use merge approvals, code quality, or many other features."
-msgstr ""
-
-msgid "After it expires, you can't use merge approvals, epics, or many other features."
-msgstr ""
-
-msgid "After it expires, you can't use merge approvals, epics, or many security features."
-msgstr ""
-
msgid "After the Apple App Store Connect integration is activated, the following protected variables will be created for CI/CD use."
msgstr ""
@@ -10187,6 +10178,9 @@ msgstr ""
msgid "Compare GitLab editions"
msgstr ""
+msgid "Compare GitLab plans"
+msgstr ""
+
msgid "Compare Revisions"
msgstr ""
@@ -12373,9 +12367,6 @@ msgstr ""
msgid "DORA4Metrics|Average (last %{days}d)"
msgstr ""
-msgid "DORA4Metrics|Average number of deployments to production per day. This metric measures how often value is delivered to end users."
-msgstr ""
-
msgid "DORA4Metrics|Change Failure Rate"
msgstr ""
@@ -12433,12 +12424,6 @@ msgstr ""
msgid "DORA4Metrics|Median time an incident was open in a production environment over the given time period."
msgstr ""
-msgid "DORA4Metrics|Median time from issue created to issue closed."
-msgstr ""
-
-msgid "DORA4Metrics|Median time from the earliest commit of a linked issue's merge request to when that issue is closed."
-msgstr ""
-
msgid "DORA4Metrics|Metrics comparison for %{name} group"
msgstr ""
@@ -12463,15 +12448,9 @@ msgstr ""
msgid "DORA4Metrics|Number of incidents divided by the number of deployments to a production environment in the given time period."
msgstr ""
-msgid "DORA4Metrics|Number of new issues created."
-msgstr ""
-
msgid "DORA4Metrics|Past 6 Months"
msgstr ""
-msgid "DORA4Metrics|Percentage of deployments that cause an incident in production."
-msgstr ""
-
msgid "DORA4Metrics|Percentage of failed deployments"
msgstr ""
@@ -12493,12 +12472,6 @@ msgstr ""
msgid "DORA4Metrics|The chart displays the median time between a merge request being merged and deployed to production environment(s) that are based on the %{linkStart}deployment_tier%{linkEnd} value."
msgstr ""
-msgid "DORA4Metrics|The time it takes an organization to recover from a failure in production."
-msgstr ""
-
-msgid "DORA4Metrics|The time to successfully deliver a commit into production. This metric reflects the efficiency of CI/CD pipelines."
-msgstr ""
-
msgid "DORA4Metrics|Time to Restore Service"
msgstr ""
@@ -12508,9 +12481,6 @@ msgstr ""
msgid "DORA4Metrics|Time to restore service (median days)"
msgstr ""
-msgid "DORA4Metrics|Total number of deploys to production."
-msgstr ""
-
msgid "DSN"
msgstr ""
@@ -18742,6 +18712,9 @@ msgstr ""
msgid "GitLab commit"
msgstr ""
+msgid "GitLab documentation"
+msgstr ""
+
msgid "GitLab events trigger webhooks. Use the request details of a webhook to help troubleshoot problems. %{link_start}How do I troubleshoot?%{link_end}"
msgstr ""
@@ -21101,6 +21074,12 @@ msgstr ""
msgid "If you did not recently try to sign in, you should immediately change your password: %{password_link}."
msgstr ""
+msgid "If you do not renew by %{strong}%{downgrades_on}%{strong_close}, you can't use merge approvals, %{end_message}"
+msgstr ""
+
+msgid "If you don't renew by %{strong}%{downgrades_on}%{strong_close} your instance will become read-only, and you won't be able to create issues or merge requests. You will also lose access to your paid features and support entitlement. %{learn_more_link}"
+msgstr ""
+
msgid "If you get a lot of false alarms from repository checks, you can clear all repository check information from the database."
msgstr ""
@@ -25816,10 +25795,10 @@ msgstr ""
msgid "Maximum file size is 1 MB. Image size must be 32 x 32 pixels. Allowed image formats are %{favicon_extension_allowlist}."
msgstr ""
-msgid "Maximum file size is 1MB. Pages are optimized for a 24px tall header logo"
+msgid "Maximum file size is 1 MB. Pages are optimized for a 128x128 px logo."
msgstr ""
-msgid "Maximum file size is 1MB. Pages are optimized for a 640x360 px logo."
+msgid "Maximum file size is 1MB. Pages are optimized for a 24px tall header logo"
msgstr ""
msgid "Maximum files in a diff"
@@ -34430,6 +34409,9 @@ msgstr ""
msgid "Provide a number our sales team can use to call you."
msgstr ""
+msgid "Provide feedback"
+msgstr ""
+
msgid "Provider"
msgstr ""
@@ -46265,6 +46247,9 @@ msgstr ""
msgid "ValueStreamAnalytics|Average number of deployments to production per day."
msgstr ""
+msgid "ValueStreamAnalytics|Average number of deployments to production per day. This metric measures how often value is delivered to end users."
+msgstr ""
+
msgid "ValueStreamAnalytics|DORA metrics"
msgstr ""
@@ -46319,6 +46304,12 @@ msgstr ""
msgid "ValueStreamAnalytics|Tasks by type"
msgstr ""
+msgid "ValueStreamAnalytics|The time it takes an organization to recover from a failure in production."
+msgstr ""
+
+msgid "ValueStreamAnalytics|The time to successfully deliver a commit into production. This metric reflects the efficiency of CI/CD pipelines."
+msgstr ""
+
msgid "ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data."
msgstr ""
@@ -48851,13 +48842,13 @@ msgstr ""
msgid "Your %{group} membership will now expire in %{days}."
msgstr ""
-msgid "Your %{spammable_entity_type} has been recognized as spam and has been discarded."
+msgid "Your %{plan_name} subscription will expire on %{expires_on}"
msgstr ""
-msgid "Your %{spammable_entity_type} has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."
+msgid "Your %{spammable_entity_type} has been recognized as spam and has been discarded."
msgstr ""
-msgid "Your %{strong}%{plan_name}%{strong_close} subscription expires on %{strong}%{expires_on}%{strong_close}. If you do not renew, you will lose access to your paid features on %{strong}%{downgrades_on}%{strong_close}. After that date, you can't create issues or merge requests, or use many other features."
+msgid "Your %{spammable_entity_type} has been recognized as spam. Please, change the content or solve the reCAPTCHA to proceed."
msgstr ""
msgid "Your %{strong}%{plan_name}%{strong_close} subscription for %{strong}%{namespace_name}%{strong_close} will expire on %{strong}%{expires_on}%{strong_close}."
@@ -49161,9 +49152,6 @@ msgid_plural "Your subscription has %{remaining_seat_count} out of %{total_seat_
msgstr[0] ""
msgstr[1] ""
-msgid "Your subscription will expire on %{expires_on}"
-msgstr ""
-
msgid "Your top-level group %{namespace_name} has reached the %{free_limit} user limit"
msgstr ""
diff --git a/scripts/trigger-build.rb b/scripts/trigger-build.rb
index 033c2e55329..69eea7488fb 100755
--- a/scripts/trigger-build.rb
+++ b/scripts/trigger-build.rb
@@ -9,10 +9,6 @@ module Trigger
%w[gitlab gitlab-ee].include?(ENV['CI_PROJECT_NAME'])
end
- def self.security?
- %r{\Agitlab-org/security(\z|/)}.match?(ENV['CI_PROJECT_NAMESPACE'])
- end
-
def self.non_empty_variable_value(variable)
variable_value = ENV[variable]
@@ -30,10 +26,10 @@ module Trigger
class Base
# Can be overridden
def self.access_token
- ENV['GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN']
+ ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE']
end
- def invoke!(downstream_job_name: nil)
+ def invoke!
pipeline_variables = variables
puts "Triggering downstream pipeline on #{downstream_project_path}"
@@ -48,18 +44,7 @@ module Trigger
puts "Triggered downstream pipeline: #{pipeline.web_url}\n"
puts "Waiting for downstream pipeline status"
- downstream_job =
- if downstream_job_name
- downstream_client.pipeline_jobs(downstream_project_path, pipeline.id).auto_paginate.find do |potential_job|
- potential_job.name == downstream_job_name
- end
- end
-
- if downstream_job
- Trigger::Job.new(downstream_project_path, downstream_job.id, downstream_client)
- else
- Trigger::Pipeline.new(downstream_project_path, pipeline.id, downstream_client)
- end
+ Trigger::Pipeline.new(downstream_project_path, pipeline.id, downstream_client)
end
def variables
@@ -221,6 +206,11 @@ module Trigger
end
end
+ # This is used in:
+ # - https://gitlab.com/gitlab-org/gitlab-runner/-/blob/ddaf90761c917a42ed4aab60541b6bc33871fe68/.gitlab/ci/docs.gitlab-ci.yml#L1-47
+ # - https://gitlab.com/gitlab-org/charts/gitlab/-/blob/fa348e709e901196803051669b4874b657b4ea91/.gitlab-ci.yml#L497-543
+ # - https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/b44483f05c5e22628ba3b49ec4c7f8761c688af0/gitlab-ci-config/gitlab-com.yml#L199-224
+ # - https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/b44483f05c5e22628ba3b49ec4c7f8761c688af0/gitlab-ci-config/gitlab-com.yml#L356-380
class Docs < Base
def self.access_token
# Default to "DOCS_PROJECT_API_TOKEN" at https://gitlab.com/gitlab-org/gitlab-docs/-/settings/access_tokens
@@ -321,7 +311,7 @@ module Trigger
class DatabaseTesting < Base
IDENTIFIABLE_NOTE_TAG = 'gitlab-org/database-team/gitlab-com-database-testing:identifiable-note'
- def invoke!(downstream_job_name: nil)
+ def invoke!
pipeline = super
project_path = variables['TOP_UPSTREAM_SOURCE_PROJECT']
merge_request_id = variables['TOP_UPSTREAM_MERGE_REQUEST_IID']
@@ -438,14 +428,10 @@ module Trigger
attr_reader :project, :gitlab_client, :start_time
end
-
- Job = Class.new(Pipeline)
end
if $PROGRAM_NAME == __FILE__
case ARGV[0]
- when 'cng'
- Trigger::CNG.new.invoke!.wait!
when 'gitlab-com-database-testing'
Trigger::DatabaseTesting.new.invoke!
when 'docs'
@@ -463,7 +449,6 @@ if $PROGRAM_NAME == __FILE__
else
puts "Please provide a valid option:
omnibus - Triggers a pipeline that builds the omnibus-gitlab package
- cng - Triggers a pipeline that builds images used by the GitLab helm chart
gitlab-com-database-testing - Triggers a pipeline that tests database changes on GitLab.com data"
end
end
diff --git a/spec/features/issues/user_bulk_edits_issues_spec.rb b/spec/features/issues/user_bulk_edits_issues_spec.rb
index fc48bc4baf9..5696bde4069 100644
--- a/spec/features/issues/user_bulk_edits_issues_spec.rb
+++ b/spec/features/issues/user_bulk_edits_issues_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
click_button 'Edit issues'
check 'Select all'
click_update_assignee_button
- click_link user.username
+ click_button user.username
click_update_issues_button
@@ -64,7 +64,7 @@ RSpec.describe 'Multiple issue updating from issues#index', :js, feature_categor
click_button 'Edit issues'
check 'Select all'
click_update_assignee_button
- click_link 'Unassigned'
+ click_button 'Unassigned'
click_update_issues_button
expect(find('.issue:first-of-type')).not_to have_link "Assigned to #{user.name}"
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index 5a9054ece48..b0be76d386a 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -121,7 +121,7 @@ RSpec.describe 'Merge requests > User mass updates', :js, feature_category: :cod
within 'aside[aria-label="Bulk update"]' do
click_button 'Select assignee'
wait_for_requests
- click_link text
+ click_button text
end
click_update_merge_requests_button
end
diff --git a/spec/fixtures/packages/helm/corrupted_chart.tgz b/spec/fixtures/packages/helm/corrupted_chart.tgz
new file mode 100644
index 00000000000..b2ac93b271e
--- /dev/null
+++ b/spec/fixtures/packages/helm/corrupted_chart.tgz
Binary files differ
diff --git a/spec/fixtures/safe_zip/invalid-unexpected-large.zip b/spec/fixtures/safe_zip/invalid-unexpected-large.zip
new file mode 100644
index 00000000000..3005da8c779
--- /dev/null
+++ b/spec/fixtures/safe_zip/invalid-unexpected-large.zip
Binary files differ
diff --git a/spec/fixtures/safe_zip/valid-symlinks-first.zip b/spec/fixtures/safe_zip/valid-symlinks-first.zip
index f5952ef71c9..1d7ecfd7bed 100644
--- a/spec/fixtures/safe_zip/valid-symlinks-first.zip
+++ b/spec/fixtures/safe_zip/valid-symlinks-first.zip
Binary files differ
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index 4f2badf869d..bbc27a621ea 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -154,7 +154,7 @@ describe('FrequentItemsListItemComponent', () => {
link.vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
- label: 'projects_dropdown_frequent_items_list_item_git_lab_community_edition',
+ label: 'projects_dropdown_frequent_items_list_item',
});
});
});
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
new file mode 100644
index 00000000000..f1db755a711
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -0,0 +1,87 @@
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { within } from '@testing-library/dom';
+import toggleWhatsNewDrawer from '~/whats_new';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import HelpCenter from '~/super_sidebar/components/help_center.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
+import { sidebarData } from '../mock_data';
+
+jest.mock('~/whats_new');
+
+describe('HelpCenter component', () => {
+ let wrapper;
+
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const withinComponent = () => within(wrapper.element);
+ const findButton = (name) => withinComponent().getByRole('button', { name });
+
+ const createWrapper = () => {
+ wrapper = mountExtended(HelpCenter, {
+ propsData: { sidebarData },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders menu items', () => {
+ expect(findDropdown().props('items')[0].items).toEqual([
+ { text: HelpCenter.i18n.help, href: helpPagePath() },
+ { text: HelpCenter.i18n.support, href: sidebarData.support_path },
+ { text: HelpCenter.i18n.docs, href: 'https://docs.gitlab.com' },
+ { text: HelpCenter.i18n.plans, href: `${PROMO_URL}/pricing` },
+ { text: HelpCenter.i18n.forum, href: 'https://forum.gitlab.com/' },
+ {
+ text: HelpCenter.i18n.contribute,
+ href: helpPagePath('', { anchor: 'contributing-to-gitlab' }),
+ },
+ { text: HelpCenter.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' },
+ ]);
+
+ expect(findDropdown().props('items')[1].items).toEqual([
+ expect.objectContaining({ text: HelpCenter.i18n.shortcuts }),
+ expect.objectContaining({ text: HelpCenter.i18n.whatsnew }),
+ ]);
+ });
+
+ describe('showKeyboardShortcuts', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ window.toggleShortcutsHelp = jest.fn();
+ findButton('Keyboard shortcuts').click();
+ });
+
+ it('closes the dropdown', () => {
+ expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled();
+ });
+
+ it('shows the keyboard shortcuts modal', () => {
+ expect(window.toggleShortcutsHelp).toHaveBeenCalled();
+ });
+ });
+
+ describe('showWhatsNew', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ findButton("What's new").click();
+ });
+
+ it('closes the dropdown', () => {
+ expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled();
+ });
+
+ it('shows the "What\'s new" slideout', () => {
+ expect(toggleWhatsNewDrawer).toHaveBeenCalledWith(expect.any(Object));
+ });
+
+ it('shows the existing "What\'s new" slideout instance on subsequent clicks', () => {
+ findButton("What's new").click();
+ expect(toggleWhatsNewDrawer).toHaveBeenCalledTimes(2);
+ expect(toggleWhatsNewDrawer).toHaveBeenLastCalledWith();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index 86ba1c1ea45..45fc30c08f0 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -1,5 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
+import HelpCenter from '~/super_sidebar/components/help_center.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import { sidebarData } from '../mock_data';
@@ -7,6 +8,7 @@ describe('SuperSidebar component', () => {
let wrapper;
const findUserBar = () => wrapper.findComponent(UserBar);
+ const findHelpCenter = () => wrapper.findComponent(HelpCenter);
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(SuperSidebar, {
@@ -25,5 +27,9 @@ describe('SuperSidebar component', () => {
it('renders UserBar with sidebarData', () => {
expect(findUserBar().props('sidebarData')).toBe(sidebarData);
});
+
+ it('renders HelpCenter with sidebarData', () => {
+ expect(findHelpCenter().props('sidebarData')).toBe(sidebarData);
+ });
});
});
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index bdbc25e49f0..379e4c2bffb 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -48,4 +48,6 @@ export const sidebarData = {
todos_pending_count: 3,
issues_dashboard_path: 'path/to/issues',
create_new_menu_groups: createNewMenuGroups,
+ support_path: '/support',
+ display_whats_new: true,
};
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 874796f653a..b0e9584a15b 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -285,6 +285,20 @@ describe('User select dropdown', () => {
expect(wrapper.emitted('input')).toEqual([[[]]]);
});
+ it('hides the dropdown after clicking on `Unassigned`', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ },
+ });
+ wrapper.vm.$refs.dropdown.hide = jest.fn();
+ await waitForPromises();
+
+ findUnassignLink().trigger('click');
+
+ expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
+ });
+
it('emits an empty array after unselecting the only selected assignee', async () => {
createComponent({
props: {
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index fcaff83ada4..ecbc1597bdf 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -66,7 +66,9 @@ RSpec.describe SidebarsHelper do
assigned_open_issues_count: 1,
assigned_open_merge_requests_count: 2,
todos_pending_count: 3,
- issues_dashboard_path: issues_dashboard_path(assignee_username: user.username)
+ issues_dashboard_path: issues_dashboard_path(assignee_username: user.username),
+ support_path: helper.support_url,
+ display_whats_new: helper.display_whats_new?
})
end
diff --git a/spec/lib/api/entities/draft_note_spec.rb b/spec/lib/api/entities/draft_note_spec.rb
new file mode 100644
index 00000000000..59555319bb1
--- /dev/null
+++ b/spec/lib/api/entities/draft_note_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::DraftNote, feature_category: :code_review_workflow do
+ let_it_be(:entity) { create(:draft_note, :on_discussion) }
+ let_it_be(:json) { entity.as_json }
+
+ it 'exposes correct attributes' do
+ expect(json["id"]).to eq entity.id
+ expect(json["author_id"]).to eq entity.author_id
+ expect(json["merge_request_id"]).to eq entity.merge_request_id
+ expect(json["resolve_discussion"]).to eq entity.resolve_discussion
+ expect(json["discussion_id"]).to eq entity.discussion_id
+ expect(json["note"]).to eq entity.note
+ expect(json["position"].transform_keys(&:to_sym)).to eq entity.position.to_h
+ end
+end
diff --git a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
index e982f0eb015..813dc15e79f 100644
--- a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
+++ b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb
@@ -10,71 +10,117 @@ RSpec.describe Gitlab::Ci::ArtifactFileReader do
subject { described_class.new(job).read(path) }
context 'when job has artifacts and metadata' do
- let!(:artifacts) { create(:ci_job_artifact, :archive, job: job) }
- let!(:metadata) { create(:ci_job_artifact, :metadata, job: job) }
+ shared_examples 'extracting job artifact archive' do
+ it 'returns the content at the path' do
+ is_expected.to be_present
+ expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
+ end
- it 'returns the content at the path' do
- is_expected.to be_present
- expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom')
- end
+ context 'when path does not exist' do
+ let(:path) { 'file/does/not/exist.txt' }
+ let(:expected_error) do
+ "Path `#{path}` does not exist inside the `#{job.name}` artifacts archive!"
+ end
- context 'when path does not exist' do
- let(:path) { 'file/does/not/exist.txt' }
- let(:expected_error) do
- "Path `#{path}` does not exist inside the `#{job.name}` artifacts archive!"
+ it 'raises an error' do
+ expect { subject }.to raise_error(described_class::Error, expected_error)
+ end
end
- it 'raises an error' do
- expect { subject }.to raise_error(described_class::Error, expected_error)
+ context 'when path points to a directory' do
+ let(:path) { 'other_artifacts_0.1.2' }
+ let(:expected_error) do
+ "Path `#{path}` was expected to be a file but it was a directory!"
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(described_class::Error, expected_error)
+ end
end
- end
- context 'when path points to a directory' do
- let(:path) { 'other_artifacts_0.1.2' }
- let(:expected_error) do
- "Path `#{path}` was expected to be a file but it was a directory!"
+ context 'when path is nested' do
+ # path exists in ci_build_artifacts.zip
+ let(:path) { 'other_artifacts_0.1.2/doc_sample.txt' }
+
+ it 'returns the content at the nested path' do
+ is_expected.to be_present
+ end
end
- it 'raises an error' do
- expect { subject }.to raise_error(described_class::Error, expected_error)
+ context 'when artifact archive size is greater than the limit' do
+ let(:expected_error) do
+ "Artifacts archive for job `#{job.name}` is too large: max 1 KB"
+ end
+
+ before do
+ stub_const("#{described_class}::MAX_ARCHIVE_SIZE", 1.kilobyte)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(described_class::Error, expected_error)
+ end
end
- end
- context 'when path is nested' do
- # path exists in ci_build_artifacts.zip
- let(:path) { 'other_artifacts_0.1.2/doc_sample.txt' }
+ context 'when metadata entry shows size greater than the limit' do
+ let(:expected_error) do
+ "Artifacts archive for job `#{job.name}` is too large: max 5 MB"
+ end
- it 'returns the content at the nested path' do
- is_expected.to be_present
+ before do
+ expect_next_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) do |entry|
+ expect(entry).to receive(:total_size).and_return(10.megabytes)
+ end
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(described_class::Error, expected_error)
+ end
end
end
- context 'when artifact archive size is greater than the limit' do
- let(:expected_error) do
- "Artifacts archive for job `#{job.name}` is too large: max 1 KB"
- end
+ context 'when job artifact is on local storage' do
+ let!(:artifacts) { create(:ci_job_artifact, :archive, job: job) }
+ let!(:metadata) { create(:ci_job_artifact, :metadata, job: job) }
+
+ it_behaves_like 'extracting job artifact archive'
+ end
+ context 'when job artifact is on remote storage' do
before do
- stub_const("#{described_class}::MAX_ARCHIVE_SIZE", 1.kilobyte)
+ stub_artifacts_object_storage
+ stub_request(:get, %r{https://artifacts.+ci_build_artifacts\.zip})
+ .to_return(
+ status: 200,
+ body: File.open(Rails.root.join('spec/fixtures/ci_build_artifacts.zip')),
+ headers: {}
+ )
+ stub_request(:get, %r{https://artifacts.+ci_build_artifacts_metadata})
+ .to_return(
+ status: 200,
+ body: File.open(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz')),
+ headers: {}
+ )
end
- it 'raises an error' do
- expect { subject }.to raise_error(described_class::Error, expected_error)
- end
+ let!(:artifacts) { create(:ci_job_artifact, :archive, :remote_store, job: job) }
+ let!(:metadata) { create(:ci_job_artifact, :metadata, :remote_store, job: job) }
+
+ it_behaves_like 'extracting job artifact archive'
end
- context 'when metadata entry shows size greater than the limit' do
- let(:expected_error) do
- "Artifacts archive for job `#{job.name}` is too large: max 5 MB"
- end
+ context 'when extracting job artifact raises entry size error' do
+ let!(:artifacts) { create(:ci_job_artifact, :archive, job: job) }
+ let!(:metadata) { create(:ci_job_artifact, :metadata, job: job) }
before do
- expect_next_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) do |entry|
- expect(entry).to receive(:total_size).and_return(10.megabytes)
+ allow_next_instance_of(SafeZip::Extract, anything) do |extractor|
+ allow(extractor).to receive(:extract).and_raise(SafeZip::Extract::EntrySizeError)
end
end
it 'raises an error' do
+ expected_error = "Path `#{path}` has invalid size in the zip!"
+
expect { subject }.to raise_error(described_class::Error, expected_error)
end
end
diff --git a/spec/lib/safe_zip/entry_spec.rb b/spec/lib/safe_zip/entry_spec.rb
index 9929b8073a0..8d49e2bcece 100644
--- a/spec/lib/safe_zip/entry_spec.rb
+++ b/spec/lib/safe_zip/entry_spec.rb
@@ -5,12 +5,13 @@ require 'spec_helper'
RSpec.describe SafeZip::Entry do
let(:target_path) { Dir.mktmpdir('safe-zip') }
let(:directories) { %w(public folder/with/subfolder) }
- let(:params) { SafeZip::ExtractParams.new(directories: directories, to: target_path) }
+ let(:files) { %w(public/index.html public/assets/image.png) }
+ let(:params) { SafeZip::ExtractParams.new(directories: directories, files: files, to: target_path) }
let(:entry) { described_class.new(zip_archive, zip_entry, params) }
let(:entry_name) { 'public/folder/index.html' }
let(:entry_path_dir) { File.join(target_path, File.dirname(entry_name)) }
- let(:entry_path) { File.join(target_path, entry_name) }
+ let(:entry_path) { File.join(File.realpath(target_path), entry_name) }
let(:zip_archive) { double }
let(:zip_entry) do
@@ -28,7 +29,7 @@ RSpec.describe SafeZip::Entry do
describe '#path_dir' do
subject { entry.path_dir }
- it { is_expected.to eq(target_path + '/public/folder') }
+ it { is_expected.to eq(File.realpath(target_path) + '/public/folder') }
end
describe '#exist?' do
@@ -51,6 +52,9 @@ RSpec.describe SafeZip::Entry do
subject { entry.extract }
context 'when entry does not match the filtered directories' do
+ let(:directories) { %w(public folder/with/subfolder) }
+ let(:files) { [] }
+
using RSpec::Parameterized::TableSyntax
where(:entry_name) do
@@ -70,7 +74,30 @@ RSpec.describe SafeZip::Entry do
end
end
- context 'when entry does exist' do
+ context 'when entry does not match the filtered files' do
+ let(:directories) { [] }
+ let(:files) { %w(public/index.html public/assets/image.png) }
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:entry_name) do
+ [
+ 'assets/folder/index.html',
+ 'public/../folder/index.html',
+ 'public/../../../../../index.html',
+ '../../../../../public/index.html',
+ '/etc/passwd'
+ ]
+ end
+
+ with_them do
+ it 'does not extract file' do
+ is_expected.to be_falsey
+ end
+ end
+ end
+
+ context 'when there is an existing extracted entry' do
before do
create_entry
end
diff --git a/spec/lib/safe_zip/extract_params_spec.rb b/spec/lib/safe_zip/extract_params_spec.rb
index 880c4358663..0ebfb7430c5 100644
--- a/spec/lib/safe_zip/extract_params_spec.rb
+++ b/spec/lib/safe_zip/extract_params_spec.rb
@@ -4,8 +4,10 @@ require 'spec_helper'
RSpec.describe SafeZip::ExtractParams do
let(:target_path) { Dir.mktmpdir("safe-zip") }
- let(:params) { described_class.new(directories: directories, to: target_path) }
+ let(:real_target_path) { File.realpath(target_path) }
+ let(:params) { described_class.new(directories: directories, files: files, to: target_path) }
let(:directories) { %w(public folder/with/subfolder) }
+ let(:files) { %w(public/index.html public/assets/image.png) }
after do
FileUtils.remove_entry_secure(target_path)
@@ -14,13 +16,13 @@ RSpec.describe SafeZip::ExtractParams do
describe '#extract_path' do
subject { params.extract_path }
- it { is_expected.to eq(target_path) }
+ it { is_expected.to eq(real_target_path) }
end
describe '#matching_target_directory' do
using RSpec::Parameterized::TableSyntax
- subject { params.matching_target_directory(target_path + path) }
+ subject { params.matching_target_directory(real_target_path + path) }
where(:path, :result) do
'/public/index.html' | '/public/'
@@ -30,7 +32,7 @@ RSpec.describe SafeZip::ExtractParams do
end
with_them do
- it { is_expected.to eq(result ? target_path + result : nil) }
+ it { is_expected.to eq(result ? real_target_path + result : nil) }
end
end
@@ -38,7 +40,7 @@ RSpec.describe SafeZip::ExtractParams do
subject { params.target_directories }
it 'starts with target_path' do
- is_expected.to all(start_with(target_path + '/'))
+ is_expected.to all(start_with(real_target_path + '/'))
end
it 'ends with / for all paths' do
@@ -53,4 +55,27 @@ RSpec.describe SafeZip::ExtractParams do
is_expected.to all(end_with('/*'))
end
end
+
+ describe '#matching_target_file' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { params.matching_target_file(real_target_path + path) }
+
+ where(:path, :result) do
+ '/public/index.html' | true
+ '/non/existing/path' | false
+ '/public/' | false
+ '/folder/with/index.html' | false
+ end
+
+ with_them do
+ it { is_expected.to eq(result) }
+ end
+ end
+
+ context 'when directories and files are empty' do
+ it 'is invalid' do
+ expect { described_class.new(to: target_path) }.to raise_error(ArgumentError, /directories or files are required/)
+ end
+ end
end
diff --git a/spec/lib/safe_zip/extract_spec.rb b/spec/lib/safe_zip/extract_spec.rb
index 443430b267d..c727475e271 100644
--- a/spec/lib/safe_zip/extract_spec.rb
+++ b/spec/lib/safe_zip/extract_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
RSpec.describe SafeZip::Extract do
let(:target_path) { Dir.mktmpdir('safe-zip') }
let(:directories) { %w(public) }
+ let(:files) { %w(public/index.html) }
let(:object) { described_class.new(archive) }
let(:archive) { Rails.root.join('spec', 'fixtures', 'safe_zip', archive_name) }
@@ -13,20 +14,36 @@ RSpec.describe SafeZip::Extract do
end
describe '#extract' do
- subject { object.extract(directories: directories, to: target_path) }
+ subject { object.extract(directories: directories, files: files, to: target_path) }
shared_examples 'extracts archive' do
- it 'does extract archive' do
- subject
+ context 'when specifying directories' do
+ subject { object.extract(directories: directories, to: target_path) }
- expect(File.exist?(File.join(target_path, 'public', 'index.html'))).to eq(true)
- expect(File.exist?(File.join(target_path, 'source'))).to eq(false)
+ it 'does extract archive' do
+ subject
+
+ expect(File.exist?(File.join(target_path, 'public', 'index.html'))).to eq(true)
+ expect(File.exist?(File.join(target_path, 'public', 'assets', 'image.png'))).to eq(true)
+ expect(File.exist?(File.join(target_path, 'source'))).to eq(false)
+ end
+ end
+
+ context 'when specifying files' do
+ subject { object.extract(files: files, to: target_path) }
+
+ it 'does extract archive' do
+ subject
+
+ expect(File.exist?(File.join(target_path, 'public', 'index.html'))).to eq(true)
+ expect(File.exist?(File.join(target_path, 'public', 'assets', 'image.png'))).to eq(false)
+ end
end
end
shared_examples 'fails to extract archive' do
it 'does not extract archive' do
- expect { subject }.to raise_error(SafeZip::Extract::Error)
+ expect { subject }.to raise_error(SafeZip::Extract::Error, including(error_message))
end
end
@@ -38,9 +55,18 @@ RSpec.describe SafeZip::Extract do
end
end
- %w(invalid-symlink-does-not-exist.zip invalid-symlinks-outside.zip).each do |name|
- context "when using #{name} archive" do
+ context 'when zip files are invalid' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:name, :message) do
+ 'invalid-symlink-does-not-exist.zip' | 'does not exist'
+ 'invalid-symlinks-outside.zip' | 'Symlink cannot be created'
+ 'invalid-unexpected-large.zip' | 'larger when inflated'
+ end
+
+ with_them do
let(:archive_name) { name }
+ let(:error_message) { message }
it_behaves_like 'fails to extract archive'
end
@@ -49,6 +75,19 @@ RSpec.describe SafeZip::Extract do
context 'when no matching directories are found' do
let(:archive_name) { 'valid-simple.zip' }
let(:directories) { %w(non/existing) }
+ let(:error_message) { 'No entries extracted' }
+
+ subject { object.extract(directories: directories, to: target_path) }
+
+ it_behaves_like 'fails to extract archive'
+ end
+
+ context 'when no matching files are found' do
+ let(:archive_name) { 'valid-simple.zip' }
+ let(:files) { %w(non/existing) }
+ let(:error_message) { 'No entries extracted' }
+
+ subject { object.extract(files: files, to: target_path) }
it_behaves_like 'fails to extract archive'
end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index e553e34ab51..206b3ae61cf 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -65,7 +65,6 @@ RSpec.describe Issuable do
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_MAX) }
- it { is_expected.to validate_length_of(:description).is_at_most(described_class::DESCRIPTION_LENGTH_MAX).on(:create) }
it_behaves_like 'validates description length with custom validation' do
before do
diff --git a/spec/models/concerns/sanitizable_spec.rb b/spec/models/concerns/sanitizable_spec.rb
index 4a1d463d666..be7169f8dca 100644
--- a/spec/models/concerns/sanitizable_spec.rb
+++ b/spec/models/concerns/sanitizable_spec.rb
@@ -75,7 +75,58 @@ RSpec.describe Sanitizable do
it 'is not valid', :aggregate_failures do
expect(record).not_to be_valid
- expect(record.errors.full_messages).to include('Name cannot contain escaped HTML entities')
+ expect(record.errors.full_messages).to contain_exactly(
+ 'Name cannot contain escaped HTML entities',
+ 'Description cannot contain escaped HTML entities'
+ )
+ end
+ end
+
+ context 'when input contains double-escaped data' do
+ let_it_be(:input) do
+ '%2526lt%253Bscript%2526gt%253Balert%25281%2529%2526lt%253B%252Fscript%2526gt%253B'
+ end
+
+ it_behaves_like 'noop'
+
+ it 'is not valid', :aggregate_failures do
+ expect(record).not_to be_valid
+ expect(record.errors.full_messages).to contain_exactly(
+ 'Name cannot contain escaped components',
+ 'Description cannot contain escaped components'
+ )
+ end
+ end
+
+ context 'when input contains a path traversal attempt' do
+ let_it_be(:input) { 'main../../../../../../api/v4/projects/1/import_project_members/2' }
+
+ it_behaves_like 'noop'
+
+ it 'is not valid', :aggregate_failures do
+ expect(record).not_to be_valid
+ expect(record.errors.full_messages).to contain_exactly(
+ 'Name cannot contain a path traversal component',
+ 'Description cannot contain a path traversal component'
+ )
+ end
+ end
+
+ context 'when input contains both path traversal attempt and pre-escaped entities' do
+ let_it_be(:input) do
+ 'main../../../../../../api/v4/projects/1/import_project_members/2&lt;script&gt;alert(1)&lt;/script&gt;'
+ end
+
+ it_behaves_like 'noop'
+
+ it 'is not valid', :aggregate_failures do
+ expect(record).not_to be_valid
+ expect(record.errors.full_messages).to contain_exactly(
+ 'Name cannot contain a path traversal component',
+ 'Name cannot contain escaped HTML entities',
+ 'Description cannot contain a path traversal component',
+ 'Description cannot contain escaped HTML entities'
+ )
end
end
end
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index 0bf6fdf4fa0..15b80749aa2 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe NamespaceSetting, feature_category: :subgroups, type: :model do
describe "#default_branch_name_content" do
let_it_be(:group) { create(:group) }
- let(:namespace_settings) { group.namespace_settings }
+ subject(:namespace_settings) { group.namespace_settings }
shared_examples "doesn't return an error" do
it "doesn't return an error" do
@@ -28,6 +28,10 @@ RSpec.describe NamespaceSetting, feature_category: :subgroups, type: :model do
end
context "when not set" do
+ before do
+ namespace_settings.default_branch_name = nil
+ end
+
it_behaves_like "doesn't return an error"
end
diff --git a/spec/requests/api/draft_notes_spec.rb b/spec/requests/api/draft_notes_spec.rb
new file mode 100644
index 00000000000..fad93fff839
--- /dev/null
+++ b/spec/requests/api/draft_notes_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::DraftNotes, feature_category: :code_review_workflow do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project, author: user) }
+
+ let_it_be(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
+ let_it_be(:draft_note_by_current_user) { create(:draft_note, merge_request: merge_request, author: user) }
+ let_it_be(:draft_note_by_random_user) { create(:draft_note, merge_request: merge_request) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe "Get a list of merge request draft notes" do
+ it "returns 200 OK status" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it "returns only draft notes authored by the current user" do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/draft_notes", user)
+
+ draft_note_ids = json_response.pluck("id")
+
+ expect(draft_note_ids).to include(draft_note_by_current_user.id)
+ expect(draft_note_ids).not_to include(draft_note_by_random_user.id)
+ expect(draft_note_ids).not_to include(merge_request_note.id)
+ end
+ end
+end
diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb
index 760b9bda541..78cc57b6c91 100644
--- a/spec/scripts/trigger-build_spec.rb
+++ b/spec/scripts/trigger-build_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe Trigger, feature_category: :tooling do
'CI_COMMIT_SHA' => 'ci_commit_sha',
'CI_MERGE_REQUEST_PROJECT_ID' => 'ci_merge_request_project_id',
'CI_MERGE_REQUEST_IID' => 'ci_merge_request_iid',
- 'GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN' => 'bot-token',
+ 'PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE' => 'bot-token',
'CI_JOB_TOKEN' => 'job-token',
'GITLAB_USER_NAME' => 'gitlab_user_name',
'GITLAB_USER_LOGIN' => 'gitlab_user_login',
@@ -26,7 +26,7 @@ RSpec.describe Trigger, feature_category: :tooling do
end
let(:com_api_endpoint) { 'https://gitlab.com/api/v4' }
- let(:com_api_token) { env['GITLAB_BOT_MULTI_PROJECT_PIPELINE_POLLING_TOKEN'] }
+ let(:com_api_token) { env['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'] }
let(:com_gitlab_client) { double('com_gitlab_client') }
let(:downstream_gitlab_client_endpoint) { com_api_endpoint }
@@ -114,25 +114,6 @@ RSpec.describe Trigger, feature_category: :tooling do
subject.invoke!
end
-
- context 'with downstream_job_name: "foo"' do
- let(:downstream_job) { Struct.new(:id, :name).new(42, 'foo') }
- let(:paginated_resources) { Struct.new(:auto_paginate).new([downstream_job]) }
-
- before do
- stub_env('CI_COMMIT_REF_NAME', "#{ref}-ee")
- end
-
- it 'fetches the downstream job' do
- expect_run_trigger_with_params
- expect(downstream_gitlab_client).to receive(:pipeline_jobs)
- .with(downstream_project_path, stubbed_pipeline.id).and_return(paginated_resources)
- expect(Trigger::Job).to receive(:new)
- .with(downstream_project_path, downstream_job.id, downstream_gitlab_client)
-
- subject.invoke!(downstream_job_name: 'foo')
- end
- end
end
end
diff --git a/spec/services/packages/helm/extract_file_metadata_service_spec.rb b/spec/services/packages/helm/extract_file_metadata_service_spec.rb
index 273f679b736..f4c61c12344 100644
--- a/spec/services/packages/helm/extract_file_metadata_service_spec.rb
+++ b/spec/services/packages/helm/extract_file_metadata_service_spec.rb
@@ -54,4 +54,17 @@ RSpec.describe Packages::Helm::ExtractFileMetadataService do
it { expect { subject }.to raise_error(described_class::ExtractionError, 'Error while parsing Chart.yaml: (<unknown>): did not find expected node content while parsing a flow node at line 2 column 1') }
end
+
+ context 'with a corrupted Chart.yaml of incorrect size' do
+ let(:helm_fixture_path) { expand_fixture_path('packages/helm/corrupted_chart.tgz') }
+ let(:expected_error_message) { 'Chart.yaml too big' }
+
+ before do
+ allow(Zlib::GzipReader).to receive(:new).and_return(Zlib::GzipReader.new(File.open(helm_fixture_path)))
+ end
+
+ it 'raises an error with the expected message' do
+ expect { subject }.to raise_error(::Packages::Helm::ExtractFileMetadataService::ExtractionError, expected_error_message)
+ end
+ end
end
diff --git a/spec/support/shared_examples/models/concerns/issuable_shared_examples.rb b/spec/support/shared_examples/models/concerns/issuable_shared_examples.rb
index 3a407088997..f49ec906382 100644
--- a/spec/support/shared_examples/models/concerns/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/issuable_shared_examples.rb
@@ -10,40 +10,111 @@ RSpec.shared_examples 'matches_cross_reference_regex? fails fast' do
end
RSpec.shared_examples 'validates description length with custom validation' do
- let(:issuable) { build(:issue, description: 'x' * (::Issuable::DESCRIPTION_LENGTH_MAX + 1)) }
- let(:context) { :update }
+ let(:invalid_description) { 'x' * (::Issuable::DESCRIPTION_LENGTH_MAX + 1) }
+ let(:valid_description) { 'short description' }
+ let(:issuable) { build(:issue, description: description) }
+
+ let(:error_message) do
+ format(
+ _('is too long (%{size}). The maximum size is %{max_size}.'),
+ size: ActiveSupport::NumberHelper.number_to_human_size(invalid_description.bytesize),
+ max_size: ActiveSupport::NumberHelper.number_to_human_size(::Issuable::DESCRIPTION_LENGTH_MAX)
+ )
+ end
- subject { issuable.validate(context) }
+ subject(:validate) { issuable.validate(context) }
context 'when Issuable is a new record' do
- it 'validates the maximum description length' do
- subject
- expect(issuable.errors[:description]).to eq(["is too long (maximum is #{::Issuable::DESCRIPTION_LENGTH_MAX} characters)"])
- end
+ let(:context) { :create }
+
+ context 'when description exceeds the maximum size' do
+ let(:description) { invalid_description }
- context 'on create' do
- let(:context) { :create }
+ it 'adds a description too long error' do
+ validate
- it 'does not validate the maximum description length' do
- allow(issuable).to receive(:description_max_length_for_new_records_is_valid).and_call_original
+ expect(issuable.errors[:description]).to contain_exactly(error_message)
+ end
+ end
- subject
+ context 'when description is within the allowed limits' do
+ let(:description) { valid_description }
- expect(issuable).not_to have_received(:description_max_length_for_new_records_is_valid)
+ it 'does not add a validation error' do
+ validate
+
+ expect(issuable.errors).not_to have_key(:description)
end
end
end
context 'when Issuable is an existing record' do
+ let(:context) { :update }
+
before do
allow(issuable).to receive(:expire_etag_cache) # to skip the expire_etag_cache callback
+ issuable.description = existing_description
issuable.save!(validate: false)
+ issuable.description = description
+ end
+
+ context 'when record already had a valid description' do
+ let(:existing_description) { 'small difference so it triggers description_changed?' }
+
+ context 'when new description exceeds the maximum size' do
+ let(:description) { invalid_description }
+
+ it 'adds a description too long error' do
+ validate
+
+ expect(issuable.errors[:description]).to contain_exactly(error_message)
+ end
+ end
+
+ context 'when new description is within the allowed limits' do
+ let(:description) { valid_description }
+
+ it 'does not add a validation error' do
+ validate
+
+ expect(issuable.errors).not_to have_key(:description)
+ end
+ end
end
- it 'does not validate the maximum description length' do
- subject
- expect(issuable.errors).not_to have_key(:description)
+ context 'when record existed with an invalid description' do
+ let(:existing_description) { "#{invalid_description} small difference so it triggers description_changed?" }
+
+ context 'when description is not changed' do
+ let(:description) { existing_description }
+
+ it 'does not add a validation error' do
+ validate
+
+ expect(issuable.errors).not_to have_key(:description)
+ end
+ end
+
+ context 'when new description exceeds the maximum size' do
+ let(:description) { invalid_description }
+
+ it 'allows updating descriptions that already existed above the limit' do
+ validate
+
+ expect(issuable.errors).not_to have_key(:description)
+ end
+ end
+
+ context 'when new description is within the allowed limits' do
+ let(:description) { valid_description }
+
+ it 'does not add a validation error' do
+ validate
+
+ expect(issuable.errors).not_to have_key(:description)
+ end
+ end
end
end
end
diff --git a/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb b/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb
index aedbfe4deb3..9bfa4ace05c 100644
--- a/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/sanitizable_shared_examples.rb
@@ -32,8 +32,25 @@ RSpec.shared_examples 'sanitizable' do |factory, fields|
subject { build(factory, attributes) }
it 'is not valid', :aggregate_failures do
+ error = 'cannot contain escaped HTML entities'
+
+ expect(subject).not_to be_valid
+ expect(subject.errors.details[field].flat_map(&:values)).to contain_exactly(error)
+ end
+ end
+
+ context 'when it contains a path component' do
+ let_it_be(:input) do
+ 'main../../../../../../api/v4/projects/1/import_project_members/2'
+ end
+
+ subject { build(factory, attributes) }
+
+ it 'is not valid', :aggregate_failures do
+ error = 'cannot contain a path traversal component'
+
expect(subject).not_to be_valid
- expect(subject.errors.details[field].flat_map(&:values)).to include('cannot contain escaped HTML entities')
+ expect(subject.errors.details[field].flat_map(&:values)).to contain_exactly(error)
end
end
end