summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.haml-lint_todo.yml1
-rw-r--r--CHANGELOG.md43
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js75
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue525
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue50
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue6
-rw-r--r--app/assets/javascripts/pages/projects/security/configuration/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/serverless/index.js6
-rw-r--r--app/assets/javascripts/registry/explorer/constants/list.js10
-rw-r--r--app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql19
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue52
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue23
-rw-r--r--app/assets/javascripts/security_configuration/components/configuration_table.vue97
-rw-r--r--app/assets/javascripts/security_configuration/components/features_constants.js112
-rw-r--r--app/assets/javascripts/security_configuration/components/manage_sast.vue57
-rw-r--r--app/assets/javascripts/security_configuration/components/upgrade.vue26
-rw-r--r--app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql6
-rw-r--r--app/assets/javascripts/security_configuration/index.js29
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue66
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js6
-rw-r--r--app/controllers/concerns/notes_actions.rb3
-rw-r--r--app/controllers/concerns/redis_tracking.rb16
-rw-r--r--app/controllers/concerns/snippets_actions.rb2
-rw-r--r--app/controllers/concerns/wiki_actions.rb3
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb1
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/controllers/search_controller.rb2
-rw-r--r--app/graphql/queries/container_registry/get_container_repositories.query.graphql19
-rw-r--r--app/graphql/types/user_type.rb3
-rw-r--r--app/models/ci/pipeline.rb1
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/member.rb19
-rw-r--r--app/models/project_services/prometheus_service.rb12
-rw-r--r--app/models/project_statistics.rb1
-rw-r--r--app/policies/project_policy.rb5
-rw-r--r--app/services/ci/abort_project_pipelines_service.rb25
-rw-r--r--app/services/ci/cancel_user_pipelines_service.rb1
-rw-r--r--app/services/design_management/save_designs_service.rb1
-rw-r--r--app/services/members/create_service.rb13
-rw-r--r--app/services/projects/destroy_service.rb3
-rw-r--r--app/views/groups/registry/repositories/index.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml353
-rw-r--r--app/views/help/shortcuts.js.haml3
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml2
-rw-r--r--app/views/projects/security/configuration/show.html.haml2
-rw-r--r--changelogs/unreleased/207473-allow-set-confidential-note-attribute.yml5
-rw-r--r--changelogs/unreleased/267140-add-user-bot-gql.yml5
-rw-r--r--changelogs/unreleased/290302-update-the-default-sort-order-of-the-image-repository-list.yml5
-rw-r--r--changelogs/unreleased/296754-followup-from-refactor-namespaceonboardingaction-model-to-onboardi.yml5
-rw-r--r--changelogs/unreleased/297346-flaky-spec-in-ee-spec-features-burnup_charts_spec-rb-burnup-charts.yml5
-rw-r--r--changelogs/unreleased/kassio-bulkimports-import-group-membership.yml5
-rw-r--r--changelogs/unreleased/khanchi-designs-patch2.yml5
-rw-r--r--changelogs/unreleased/link-new-line-gpg.yml5
-rw-r--r--changelogs/unreleased/mc-backstage-reduce-db-updates-ci-minute-reset.yml5
-rw-r--r--config/feature_flags/development/abort_deleted_project_pipelines.yml8
-rw-r--r--config/feature_flags/development/confidential_notes.yml8
-rw-r--r--config/known_invalid_graphql_queries.yml1
-rw-r--r--db/migrate/20201007033527_add_daily_invites_to_plan_limits.rb9
-rw-r--r--db/migrate/20201007033723_insert_daily_invites_plan_limits.rb25
-rw-r--r--db/migrate/20210205143926_remove_namespace_id_foreign_key_on_namespace_onboarding_actions.rb19
-rw-r--r--db/post_migrate/20210205144537_remove_namespace_onboarding_actions_table.rb23
-rw-r--r--db/schema_migrations/202010070335271
-rw-r--r--db/schema_migrations/202010070337231
-rw-r--r--db/schema_migrations/202102051439261
-rw-r--r--db/schema_migrations/202102051445371
-rw-r--r--db/structure.sql27
-rw-r--r--doc/administration/instance_limits.md7
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json18
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/issues.md4
-rw-r--r--doc/development/fe_guide/style/html.md32
-rw-r--r--doc/development/usage_ping.md9
-rw-r--r--doc/user/discussions/img/confidential_comments_v13_9.pngbin0 -> 18739 bytes
-rw-r--r--doc/user/discussions/index.md122
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/integrations/prometheus.md2
-rw-r--r--doc/user/project/settings/index.md10
-rw-r--r--lib/api/lint.rb4
-rw-r--r--lib/api/merge_request_approvals.rb2
-rw-r--r--lib/api/merge_request_diffs.rb4
-rw-r--r--lib/api/merge_requests.rb11
-rw-r--r--lib/api/todos.rb5
-rw-r--r--lib/bulk_imports/groups/graphql/get_members_query.rb55
-rw-r--r--lib/bulk_imports/groups/loaders/members_loader.rb17
-rw-r--r--lib/bulk_imports/groups/pipelines/members_pipeline.rb31
-rw-r--r--lib/bulk_imports/groups/transformers/member_attributes_transformer.rb56
-rw-r--r--lib/bulk_imports/importers/group_importer.rb1
-rw-r--r--lib/gitlab/auth/otp/strategies/forti_token_cloud.rb3
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/abilities.rb4
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/tree_summary.rb51
-rw-r--r--lib/gitlab/usage/metrics/aggregates/aggregate.rb2
-rw-r--r--lib/gitlab/usage_data_counters/hll_redis_counter.rb8
-rw-r--r--lib/uploaded_file.rb10
-rw-r--r--locale/gitlab.pot55
-rw-r--r--spec/controllers/concerns/redis_tracking_spec.rb115
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb4
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb38
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb12
-rw-r--r--spec/controllers/search_controller_spec.rb2
-rw-r--r--spec/controllers/snippets_controller_spec.rb2
-rw-r--r--spec/factories/projects.rb7
-rw-r--r--spec/features/help_pages_spec.rb2
-rw-r--r--spec/features/projects/user_uses_shortcuts_spec.rb7
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js128
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js59
-rw-r--r--spec/frontend/security_configuration/app_spec.js27
-rw-r--r--spec/frontend/security_configuration/configuration_table_spec.js48
-rw-r--r--spec/frontend/security_configuration/manage_sast_spec.js136
-rw-r--r--spec/frontend/security_configuration/upgrade_spec.js29
-rw-r--r--spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js91
-rw-r--r--spec/frontend/sidebar/user_data_mock.js2
-rw-r--r--spec/graphql/mutations/design_management/upload_spec.rb9
-rw-r--r--spec/graphql/types/user_type_spec.rb1
-rw-r--r--spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb34
-rw-r--r--spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb42
-rw-r--r--spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb85
-rw-r--r--spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb101
-rw-r--r--spec/lib/bulk_imports/importers/group_importer_spec.rb3
-rw-r--r--spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb8
-rw-r--r--spec/lib/gitlab/tree_summary_spec.rb75
-rw-r--r--spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb6
-rw-r--r--spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb2
-rw-r--r--spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb32
-rw-r--r--spec/lib/uploaded_file_spec.rb14
-rw-r--r--spec/migrations/insert_daily_invites_plan_limits_spec.rb55
-rw-r--r--spec/models/ci/pipeline_spec.rb10
-rw-r--r--spec/models/member_spec.rb52
-rw-r--r--spec/models/plan_limits_spec.rb1
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb61
-rw-r--r--spec/policies/project_policy_spec.rb72
-rw-r--r--spec/requests/api/lint_spec.rb168
-rw-r--r--spec/requests/api/merge_request_approvals_spec.rb6
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb12
-rw-r--r--spec/requests/api/merge_requests_spec.rb30
-rw-r--r--spec/requests/api/npm_instance_packages_spec.rb2
-rw-r--r--spec/requests/api/npm_project_packages_spec.rb34
-rw-r--r--spec/requests/api/project_attributes.yml149
-rw-r--r--spec/requests/api/projects_spec.rb51
-rw-r--r--spec/requests/api/terraform/state_spec.rb2
-rw-r--r--spec/requests/api/todos_spec.rb8
-rw-r--r--spec/services/ci/abort_project_pipelines_service_spec.rb42
-rw-r--r--spec/services/design_management/save_designs_service_spec.rb20
-rw-r--r--spec/services/projects/destroy_service_spec.rb6
-rw-r--r--spec/support/refinements/fixture_file_refinements.rb26
-rw-r--r--spec/support/renameable_upload.rb15
-rw-r--r--spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb15
-rw-r--r--spec/support/shared_examples/controllers/unique_hll_events_examples.rb12
-rw-r--r--spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/requests/api/merge_requests_shared_examples.rb23
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb490
-rw-r--r--spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb190
-rw-r--r--spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb185
162 files changed, 3996 insertions, 1310 deletions
diff --git a/.haml-lint_todo.yml b/.haml-lint_todo.yml
index bb546632335..7f1a7ff4cb6 100644
--- a/.haml-lint_todo.yml
+++ b/.haml-lint_todo.yml
@@ -109,7 +109,6 @@ linters:
- 'app/views/groups/runners/edit.html.haml'
- 'app/views/groups/settings/_advanced.html.haml'
- 'app/views/groups/settings/_lfs.html.haml'
- - 'app/views/help/_shortcuts.html.haml'
- 'app/views/help/index.html.haml'
- 'app/views/help/instance_configuration.html.haml'
- 'app/views/help/instance_configuration/_gitlab_ci.html.haml'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d0688ebf570..ea9b789ce05 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,21 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 13.8.4 (2021-02-11)
+
+### Security (9 changes)
+
+- Cancel running and pending jobs when a project is deleted. !1220
+- Prevent Denial of Service Attack on gitlab-shell.
+- Prevent exposure of confidential issue titles in file browser.
+- Updates authorization for linting API.
+- Check user access on API merge request read actions.
+- Limit daily invitations to groups and projects.
+- Enforce the analytics enabled project setting for project-level analytics features.
+- Perform SSL verification for FortiTokenCloud Integration.
+- Prevent Server-side Request Forgery for Prometheus when secured by Google IAP.
+
+
## 13.8.3 (2021-02-05)
### Fixed (2 changes)
@@ -387,6 +402,21 @@ entry.
- Add verbiage + link sast to show it's in core. !51935
+## 13.7.7 (2021-02-11)
+
+### Security (9 changes)
+
+- Cancel running and pending jobs when a project is deleted. !1220
+- Prevent Denial of Service Attack on gitlab-shell.
+- Prevent exposure of confidential issue titles in file browser.
+- Updates authorization for linting API.
+- Check user access on API merge request read actions.
+- Limit daily invitations to groups and projects.
+- Enforce the analytics enabled project setting for project-level analytics features.
+- Perform SSL verification for FortiTokenCloud Integration.
+- Prevent Server-side Request Forgery for Prometheus when secured by Google IAP.
+
+
## 13.7.6 (2021-02-01)
### Security (5 changes)
@@ -908,6 +938,19 @@ entry.
- Update GitLab Workhorse to v8.57.0.
+## 13.6.7 (2021-02-11)
+
+### Security (7 changes)
+
+- Cancel running and pending jobs when a project is deleted. !1220
+- Updates authorization for linting API.
+- Prevent exposure of confidential issue titles in file browser.
+- Check user access on API merge request read actions.
+- Prevent Denial of Service Attack on gitlab-shell.
+- Limit daily invitations to groups and projects.
+- Prevent Server-side Request Forgery for Prometheus when secured by Google IAP.
+
+
## 13.6.6 (2021-02-01)
### Security (5 changes)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 76e955d251d..b102cab1df4 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-88ef3e7f64498ae3574f29b0705c29cf3b4e9311
+d0a79053ba4fef55b59543b99327fc89aed64876
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 3da85016902..592a1a89678 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-13.16.0
+13.16.1
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 6cdf083378b..d586c0c8dd0 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -3,12 +3,11 @@ import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import Vue from 'vue';
import { flatten } from 'lodash';
-import { parseBoolean, getCspNonceValue } from '~/lib/utils/common_utils';
-import axios from '../../lib/utils/axios_utils';
-import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility';
-import findAndFollowLink from '../../lib/utils/navigation_utility';
+import { refreshCurrentPage, visitUrl } from '~/lib/utils/url_utility';
+import findAndFollowLink from '~/lib/utils/navigation_utility';
+import { parseBoolean } from '~/lib/utils/common_utils';
+
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
-import ShortcutsToggle from './shortcuts_toggle.vue';
import { keysFor, TOGGLE_PERFORMANCE_BAR, TOGGLE_CANARY } from './keybindings';
const defaultStopCallback = Mousetrap.prototype.stopCallback;
@@ -20,15 +19,6 @@ Mousetrap.prototype.stopCallback = function customStopCallback(e, element, combo
return defaultStopCallback.call(this, e, element, combo);
};
-function initToggleButton() {
- return new Vue({
- el: document.querySelector('.js-toggle-shortcuts'),
- render(createElement) {
- return createElement(ShortcutsToggle);
- },
- });
-}
-
/**
* The key used to save and fetch the local Mousetrap instance
* attached to a `<textarea>` element using `jQuery.data`
@@ -65,7 +55,8 @@ function getToolbarBtnToShortcutsMap($textarea) {
export default class Shortcuts {
constructor() {
this.onToggleHelp = this.onToggleHelp.bind(this);
- this.enabledHelp = [];
+ this.helpModalElement = null;
+ this.helpModalVueInstance = null;
Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch);
@@ -107,11 +98,33 @@ export default class Shortcuts {
}
onToggleHelp(e) {
- if (e.preventDefault) {
+ if (e?.preventDefault) {
e.preventDefault();
}
- Shortcuts.toggleHelp(this.enabledHelp);
+ if (this.helpModalElement && this.helpModalVueInstance) {
+ this.helpModalVueInstance.$destroy();
+ this.helpModalElement.remove();
+ this.helpModalElement = null;
+ this.helpModalVueInstance = null;
+ } else {
+ this.helpModalElement = document.createElement('div');
+ document.body.append(this.helpModalElement);
+
+ this.helpModalVueInstance = new Vue({
+ el: this.helpModalElement,
+ components: {
+ ShortcutsHelp: () => import('./shortcuts_help.vue'),
+ },
+ render: (createElement) => {
+ return createElement('shortcuts-help', {
+ on: {
+ hidden: this.onToggleHelp,
+ },
+ });
+ },
+ });
+ }
}
static onTogglePerfBar(e) {
@@ -144,34 +157,6 @@ export default class Shortcuts {
$(document).triggerHandler('markdown-preview:toggle', [e]);
}
- static toggleHelp(location) {
- const $modal = $('#modal-shortcuts');
-
- if ($modal.length) {
- $modal.modal('toggle');
- return null;
- }
-
- return axios
- .get(gon.shortcuts_path, {
- responseType: 'text',
- })
- .then(({ data }) => {
- $.globalEval(data, { nonce: getCspNonceValue() });
-
- if (location && location.length > 0) {
- const results = [];
- for (let i = 0, len = location.length; i < len; i += 1) {
- results.push($(location[i]).show());
- }
- return results;
- }
-
- return $('.js-more-help-button').remove();
- })
- .then(initToggleButton);
- }
-
focusFilter(e) {
if (!this.filterInput) {
this.filterInput = $('input[type=search]', '.nav-controls');
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
new file mode 100644
index 00000000000..1277dd0ed37
--- /dev/null
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
@@ -0,0 +1,525 @@
+<script>
+/* eslint-disable @gitlab/vue-require-i18n-strings */
+import { GlIcon, GlModal } from '@gitlab/ui';
+import ShortcutsToggle from './shortcuts_toggle.vue';
+
+export default {
+ components: {
+ GlIcon,
+ GlModal,
+ ShortcutsToggle,
+ },
+ computed: {
+ ctrlCharacter() {
+ return window.gl.client.isMac ? '⌘' : 'ctrl';
+ },
+ onDotCom() {
+ return window.gon.dot_com;
+ },
+ },
+};
+</script>
+<template>
+ <gl-modal
+ modal-id="keyboard-shortcut-modal"
+ size="lg"
+ data-testid="modal-shortcuts"
+ :visible="true"
+ :hide-footer="true"
+ @hidden="$emit('hidden')"
+ >
+ <template #modal-title>
+ <shortcuts-toggle />
+ </template>
+ <div class="row">
+ <div class="col-lg-4">
+ <table class="shortcut-mappings text-2">
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Global Shortcuts') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>?</kbd>
+ </td>
+ <td>{{ __('Toggle this dialog') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift p</kbd>
+ </td>
+ <td>{{ __('Go to your projects') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift g</kbd>
+ </td>
+ <td>{{ __('Go to your groups') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift a</kbd>
+ </td>
+ <td>{{ __('Go to the activity feed') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift l</kbd>
+ </td>
+ <td>{{ __('Go to the milestone list') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift s</kbd>
+ </td>
+ <td>{{ __('Go to your snippets') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>s</kbd>
+ /
+ <kbd>/</kbd>
+ </td>
+ <td>{{ __('Start search') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift i</kbd>
+ </td>
+ <td>{{ __('Go to your issues') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift m</kbd>
+ </td>
+ <td>{{ __('Go to your merge requests') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>shift t</kbd>
+ </td>
+ <td>{{ __('Go to your To-Do list') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>p</kbd>
+ <kbd>b</kbd>
+ </td>
+ <td>{{ __('Toggle the Performance Bar') }}</td>
+ </tr>
+ <tr v-if="onDotCom">
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>x</kbd>
+ </td>
+ <td>{{ __('Toggle GitLab Next') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Editing') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} shift p</kbd>
+ </td>
+ <td>{{ __('Toggle Markdown preview') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-up" />
+ </kbd>
+ </td>
+ <td>
+ {{ __('Edit your most recent comment in a thread (from an empty textarea)') }}
+ </td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Wiki') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>e</kbd>
+ </td>
+ <td>{{ __('Edit wiki page') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Repository Graph') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-left" />
+ </kbd>
+ /
+ <kbd>h</kbd>
+ </td>
+ <td>{{ __('Scroll left') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-right" />
+ </kbd>
+ /
+ <kbd>l</kbd>
+ </td>
+ <td>{{ __('Scroll right') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-up" />
+ </kbd>
+ /
+ <kbd>k</kbd>
+ </td>
+ <td>{{ __('Scroll up') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-down" />
+ </kbd>
+ /
+ <kbd>j</kbd>
+ </td>
+ <td>{{ __('Scroll down') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ shift
+ <gl-icon name="arrow-up" />
+ / k
+ </kbd>
+ </td>
+ <td>{{ __('Scroll to top') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ shift
+ <gl-icon name="arrow-down" />
+ / j
+ </kbd>
+ </td>
+ <td>{{ __('Scroll to bottom') }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="col-lg-4">
+ <table class="shortcut-mappings text-2">
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Project') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>p</kbd>
+ </td>
+ <td>{{ __("Go to the project's overview page") }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>v</kbd>
+ </td>
+ <td>{{ __("Go to the project's activity feed") }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>r</kbd>
+ </td>
+ <td>{{ __('Go to releases') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>f</kbd>
+ </td>
+ <td>{{ __('Go to files') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>t</kbd>
+ </td>
+ <td>{{ __('Go to find file') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>c</kbd>
+ </td>
+ <td>{{ __('Go to commits') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>n</kbd>
+ </td>
+ <td>{{ __('Go to repository graph') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>d</kbd>
+ </td>
+ <td>{{ __('Go to repository charts') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>i</kbd>
+ </td>
+ <td>{{ __('Go to issues') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>i</kbd>
+ </td>
+ <td>{{ __('New issue') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>b</kbd>
+ </td>
+ <td>{{ __('Go to issue boards') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>m</kbd>
+ </td>
+ <td>{{ __('Go to merge requests') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>j</kbd>
+ </td>
+ <td>{{ __('Go to jobs') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>l</kbd>
+ </td>
+ <td>{{ __('Go to metrics') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>e</kbd>
+ </td>
+ <td>{{ __('Go to environments') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>k</kbd>
+ </td>
+ <td>{{ __('Go to kubernetes') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>s</kbd>
+ </td>
+ <td>{{ __('Go to snippets') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>g</kbd>
+ <kbd>w</kbd>
+ </td>
+ <td>{{ __('Go to wiki') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Project Files') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-up" />
+ </kbd>
+ </td>
+ <td>{{ __('Move selection up') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>
+ <gl-icon name="arrow-down" />
+ </kbd>
+ </td>
+ <td>{{ __('Move selection down') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>enter</kbd>
+ </td>
+ <td>{{ __('Open Selection') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>esc</kbd>
+ </td>
+ <td>{{ __('Go back (while searching for files)') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>y</kbd>
+ </td>
+ <td>{{ __('Go to file permalink (while viewing a file)') }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <div class="col-lg-4">
+ <table class="shortcut-mappings text-2">
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Epics, Issues, and Merge Requests') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>r</kbd>
+ </td>
+ <td>{{ __('Comment/Reply (quoting selected text)') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>e</kbd>
+ </td>
+ <td>{{ __('Edit description') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>l</kbd>
+ </td>
+ <td>{{ __('Change label') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Issues and Merge Requests') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>a</kbd>
+ </td>
+ <td>{{ __('Change assignee') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>m</kbd>
+ </td>
+ <td>{{ __('Change milestone') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Merge Requests') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>]</kbd>
+ /
+ <kbd>j</kbd>
+ </td>
+ <td>{{ __('Next file in diff') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>[</kbd>
+ /
+ <kbd>k</kbd>
+ </td>
+ <td>{{ __('Previous file in diff') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} p</kbd>
+ </td>
+ <td>{{ __('Go to file') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>n</kbd>
+ </td>
+ <td>{{ __('Next unresolved discussion') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>p</kbd>
+ </td>
+ <td>{{ __('Previous unresolved discussion') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>b</kbd>
+ </td>
+ <td>{{ __('Copy source branch name') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Merge Request Commits') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>c</kbd>
+ </td>
+ <td>{{ __('Next commit') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>x</kbd>
+ </td>
+ <td>{{ __('Previous commit') }}</td>
+ </tr>
+ </tbody>
+ <tbody>
+ <tr>
+ <th></th>
+ <th>{{ __('Web IDE') }}</th>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} p</kbd>
+ </td>
+ <td>{{ __('Go to file') }}</td>
+ </tr>
+ <tr>
+ <td class="shortcut">
+ <kbd>{{ ctrlCharacter }} enter</kbd>
+ </td>
+ <td>{{ __('Commit (when editing commit message)') }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index bc2b2d6d5d0..e57d42dc63c 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -1,9 +1,8 @@
<script>
import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
-import { isEmpty } from 'lodash';
import Autosize from 'autosize';
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { deprecatedCreateFlash as Flash } from '~/flash';
@@ -34,6 +33,10 @@ export default {
TimelineEntryItem,
GlIcon,
CommentFieldLayout,
+ GlFormCheckbox,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin(), issuableStateMixin],
props: {
@@ -46,8 +49,8 @@ export default {
return {
note: '',
noteType: constants.COMMENT,
+ noteIsConfidential: false,
isSubmitting: false,
- isSubmitButtonDisabled: true,
};
},
computed: {
@@ -80,6 +83,9 @@ export default {
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
},
+ canSetConfidential() {
+ return this.getNoteableData.current_user.can_update;
+ },
issueActionButtonTitle() {
const openOrClose = this.isOpen ? 'close' : 'reopen';
@@ -146,13 +152,11 @@ export default {
hasCloseAndCommentButton() {
return !this.glFeatures.removeCommentCloseReopen;
},
- },
- watch: {
- note(newNote) {
- this.setIsSubmitButtonDisabled(newNote, this.isSubmitting);
+ confidentialNotesEnabled() {
+ return Boolean(this.glFeatures.confidentialNotes);
},
- isSubmitting(newValue) {
- this.setIsSubmitButtonDisabled(this.note, newValue);
+ disableSubmitButton() {
+ return this.note.length === 0 || this.isSubmitting;
},
},
mounted() {
@@ -173,13 +177,6 @@ export default {
'reopenIssuable',
'toggleIssueLocalState',
]),
- setIsSubmitButtonDisabled(note, isSubmitting) {
- if (!isEmpty(note) && !isSubmitting) {
- this.isSubmitButtonDisabled = false;
- } else {
- this.isSubmitButtonDisabled = true;
- }
- },
handleSave(withIssueAction) {
if (this.note.length) {
const noteData = {
@@ -189,6 +186,7 @@ export default {
note: {
noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
+ confidential: this.noteIsConfidential,
note: this.note,
},
merge_request_diff_head_sha: this.getNoteableData.diff_head_sha,
@@ -252,6 +250,7 @@ export default {
if (shouldClear) {
this.note = '';
+ this.noteIsConfidential = false;
this.resizeTextarea();
this.$refs.markdownField.previewMarkdown = false;
}
@@ -340,11 +339,26 @@ export default {
</markdown-field>
</comment-field-layout>
<div class="note-form-actions">
+ <gl-form-checkbox
+ v-if="confidentialNotesEnabled && canSetConfidential"
+ v-model="noteIsConfidential"
+ class="gl-mb-6"
+ data-testid="confidential-note-checkbox"
+ >
+ {{ s__('Notes|Make this comment confidential') }}
+ <gl-icon
+ v-gl-tooltip:tooltipcontainer.bottom
+ name="question"
+ :size="16"
+ :title="s__('Notes|Confidential comments are only visible to project members')"
+ class="gl-text-gray-500"
+ />
+ </gl-form-checkbox>
<div
class="btn-group gl-mr-3 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
<gl-button
- :disabled="isSubmitButtonDisabled"
+ :disabled="disableSubmitButton"
class="js-comment-button js-comment-submit-button"
data-qa-selector="comment_button"
data-testid="comment-button"
@@ -357,7 +371,7 @@ export default {
>{{ commentButtonTitle }}</gl-button
>
<gl-button
- :disabled="isSubmitButtonDisabled"
+ :disabled="disableSubmitButton"
name="button"
category="primary"
variant="success"
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 17a995018d3..23b1f1e2ac1 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -210,9 +210,9 @@ export default {
v-gl-tooltip:tooltipcontainer.bottom
data-testid="confidentialIndicator"
name="eye-slash"
- :size="14"
- :title="s__('Notes|Private comments are accessible by internal staff only')"
- class="gl-ml-1 gl-text-gray-700 align-middle"
+ :size="16"
+ :title="s__('Notes|This comment is confidential and only visible to project members')"
+ class="gl-ml-1 gl-text-orange-700 align-middle"
/>
<slot name="extra-controls"></slot>
<gl-loading-icon
diff --git a/app/assets/javascripts/pages/projects/security/configuration/index.js b/app/assets/javascripts/pages/projects/security/configuration/index.js
new file mode 100644
index 00000000000..101cb8356b2
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/security/configuration/index.js
@@ -0,0 +1,3 @@
+import { initStaticSecurityConfiguration } from '~/security_configuration';
+
+initStaticSecurityConfiguration(document.querySelector('#js-security-configuration-static'));
diff --git a/app/assets/javascripts/pages/projects/serverless/index.js b/app/assets/javascripts/pages/projects/serverless/index.js
index a883737ac9b..640301dd478 100644
--- a/app/assets/javascripts/pages/projects/serverless/index.js
+++ b/app/assets/javascripts/pages/projects/serverless/index.js
@@ -1,7 +1,5 @@
import ServerlessBundle from '~/serverless/serverless_bundle';
import initServerlessSurveyBanner from '~/serverless/survey_banner';
-document.addEventListener('DOMContentLoaded', () => {
- initServerlessSurveyBanner();
- new ServerlessBundle(); // eslint-disable-line no-new
-});
+initServerlessSurveyBanner();
+new ServerlessBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/registry/explorer/constants/list.js b/app/assets/javascripts/registry/explorer/constants/list.js
index 37ced72861e..f59b9d7a9f5 100644
--- a/app/assets/javascripts/registry/explorer/constants/list.js
+++ b/app/assets/javascripts/registry/explorer/constants/list.js
@@ -1,4 +1,4 @@
-import { s__ } from '~/locale';
+import { s__, __ } from '~/locale';
// Translations strings
@@ -35,8 +35,6 @@ export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
'ContainerRegistry|%{title} was successfully scheduled for deletion',
);
-export const IMAGE_REPOSITORY_LIST_LABEL = s__('ContainerRegistry|Image Repositories');
-export const SEARCH_PLACEHOLDER_TEXT = s__('ContainerRegistry|Filter by name');
export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.');
export const EMPTY_RESULT_MESSAGE = s__(
'ContainerRegistry|To widen your search, change or remove the filters above.',
@@ -47,3 +45,9 @@ export const EMPTY_RESULT_MESSAGE = s__(
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
export const GRAPHQL_PAGE_SIZE = 10;
+
+export const SORT_FIELDS = [
+ { orderBy: 'UPDATED', label: __('Updated') },
+ { orderBy: 'CREATED', label: __('Created') },
+ { orderBy: 'NAME', label: __('Name') },
+];
diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
index 8b6d778c655..01cb7fa1cab 100644
--- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
+++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql
@@ -6,9 +6,17 @@ query getContainerRepositoriesDetails(
$after: String
$before: String
$isGroupPage: Boolean!
+ $sort: ContainerRepositorySort
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
nodes {
id
tagsCount
@@ -16,7 +24,14 @@ query getContainerRepositoriesDetails(
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
nodes {
id
tagsCount
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index d362b79789b..c710d125797 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -7,12 +7,12 @@ import {
GlLink,
GlAlert,
GlSkeletonLoader,
- GlSearchBoxByClick,
} from '@gitlab/ui';
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import Tracking from '~/tracking';
import createFlash from '~/flash';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import RegistryHeader from '../components/list_page/registry_header.vue';
import DeleteImage from '../components/delete_image.vue';
@@ -25,12 +25,11 @@ import {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
- IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
+ SORT_FIELDS,
} from '../constants/index';
export default {
@@ -58,9 +57,9 @@ export default {
GlLink,
GlAlert,
GlSkeletonLoader,
- GlSearchBoxByClick,
RegistryHeader,
DeleteImage,
+ RegistrySearch,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -77,11 +76,10 @@ export default {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
- IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
},
+ searchConfig: SORT_FIELDS,
apollo: {
baseImages: {
query: getContainerRepositoriesQuery,
@@ -123,7 +121,8 @@ export default {
containerRepositoriesCount: 0,
itemToDelete: {},
deleteAlertType: null,
- searchValue: null,
+ filter: [],
+ sorting: { orderBy: 'UPDATED', sort: 'desc' },
name: null,
mutationLoading: false,
fetchAdditionalDetails: false,
@@ -142,6 +141,7 @@ export default {
queryVariables() {
return {
name: this.name,
+ sort: this.sortBy,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
isGroupPage: this.config.isGroupPage,
first: GRAPHQL_PAGE_SIZE,
@@ -166,6 +166,10 @@ export default {
? DELETE_IMAGE_SUCCESS_MESSAGE
: DELETE_IMAGE_ERROR_MESSAGE;
},
+ sortBy() {
+ const { orderBy, sort } = this.sorting;
+ return `${orderBy}_${sort}`.toUpperCase();
+ },
},
mounted() {
// If the two graphql calls - which are not batched - resolve togheter we will have a race
@@ -231,6 +235,16 @@ export default {
this.track('confirm_delete');
this.mutationLoading = true;
},
+ updateSorting(value) {
+ this.sorting = {
+ ...this.sorting,
+ ...value,
+ };
+ },
+ doFilter() {
+ const search = this.filter.find((i) => i.type === 'filtered-search-term');
+ this.name = search?.value?.data;
+ },
},
};
</script>
@@ -283,6 +297,16 @@ export default {
</template>
</registry-header>
+ <registry-search
+ :filter="filter"
+ :sorting="sorting"
+ :tokens="[]"
+ :sortable-fields="$options.searchConfig"
+ @sorting:changed="updateSorting"
+ @filter:changed="filter = $event"
+ @filter:submit="doFilter"
+ />
+
<div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
@@ -298,20 +322,6 @@ export default {
</div>
<template v-else>
<template v-if="images.length > 0 || name">
- <div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader">
- <div class="gl-flex-fill-1">
- <h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
- </div>
- <div>
- <gl-search-box-by-click
- v-model="searchValue"
- :placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
- @clear="name = null"
- @submit="name = $event"
- />
- </div>
- </div>
-
<image-list
v-if="images.length"
:images="images"
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
new file mode 100644
index 00000000000..513a7353d28
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -0,0 +1,23 @@
+<script>
+import ConfigurationTable from './configuration_table.vue';
+
+export default {
+ components: {
+ ConfigurationTable,
+ },
+};
+</script>
+
+<template>
+ <article>
+ <header>
+ <h4 class="gl-my-5">
+ {{ __('Security Configuration') }}
+ </h4>
+ <h5 class="gl-font-lg gl-mt-7">
+ {{ s__('SecurityConfiguration|Testing & Compliance') }}
+ </h5>
+ </header>
+ <configuration-table />
+ </article>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/configuration_table.vue b/app/assets/javascripts/security_configuration/components/configuration_table.vue
new file mode 100644
index 00000000000..2d9e8e63826
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/configuration_table.vue
@@ -0,0 +1,97 @@
+<script>
+import { GlLink, GlSprintf, GlTable, GlAlert } from '@gitlab/ui';
+import { s__, sprintf } from '~/locale';
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+} from '~/vue_shared/security_reports/constants';
+import ManageSast from './manage_sast.vue';
+import Upgrade from './upgrade.vue';
+import { features } from './features_constants';
+
+const borderClasses = 'gl-border-b-1! gl-border-b-solid! gl-border-gray-100!';
+const thClass = `gl-text-gray-900 gl-bg-transparent! ${borderClasses}`;
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ GlTable,
+ GlAlert,
+ },
+ data: () => ({
+ features,
+ errorMessage: '',
+ }),
+ methods: {
+ getFeatureDocumentationLinkLabel(item) {
+ return sprintf(s__('SecurityConfiguration|Feature documentation for %{featureName}'), {
+ featureName: item.name,
+ });
+ },
+ onError(value) {
+ this.errorMessage = value;
+ },
+ getComponentForItem(item) {
+ const COMPONENTS = {
+ [REPORT_TYPE_SAST]: ManageSast,
+ [REPORT_TYPE_DAST]: Upgrade,
+ [REPORT_TYPE_DEPENDENCY_SCANNING]: Upgrade,
+ [REPORT_TYPE_CONTAINER_SCANNING]: Upgrade,
+ [REPORT_TYPE_COVERAGE_FUZZING]: Upgrade,
+ [REPORT_TYPE_LICENSE_COMPLIANCE]: Upgrade,
+ };
+
+ return COMPONENTS[item.type];
+ },
+ },
+ table: {
+ fields: [
+ {
+ key: 'feature',
+ label: s__('SecurityConfiguration|Security Control'),
+ thClass,
+ },
+ {
+ key: 'manage',
+ label: s__('SecurityConfiguration|Manage'),
+ thClass,
+ },
+ ],
+ items: features,
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
+ {{ errorMessage }}
+ </gl-alert>
+ <gl-table :items="$options.table.items" :fields="$options.table.fields" stacked="md">
+ <template #cell(feature)="{ item }">
+ <div class="gl-text-gray-900">
+ {{ item.name }}
+ </div>
+ <div>
+ {{ item.description }}
+ <gl-link
+ target="_blank"
+ :href="item.link"
+ :aria-label="getFeatureDocumentationLinkLabel(item)"
+ >
+ {{ s__('SecurityConfiguration|More information') }}
+ </gl-link>
+ </div>
+ </template>
+
+ <template #cell(manage)="{ item }">
+ <component :is="getComponentForItem(item)" :data-testid="item.type" @error="onError" />
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/features_constants.js b/app/assets/javascripts/security_configuration/components/features_constants.js
new file mode 100644
index 00000000000..c21c18fbf60
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/features_constants.js
@@ -0,0 +1,112 @@
+import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_SECRET_DETECTION,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+} from '~/vue_shared/security_reports/constants';
+
+/**
+ * Translations & helpPagePaths for Static Security Configuration Page
+ */
+export const SAST_NAME = s__('Static Application Security Testing (SAST)');
+export const SAST_DESCRIPTION = s__('Analyze your source code for known vulnerabilities.');
+export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index');
+
+export const DAST_NAME = s__('Dynamic Application Security Testing (DAST)');
+export const DAST_DESCRIPTION = s__('Analyze a review version of your web application.');
+export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
+
+export const SECRET_DETECTION_NAME = s__('Secret Detection');
+export const SECRET_DETECTION_DESCRIPTION = s__(
+ 'Analyze your source code and git history for secrets.',
+);
+export const SECRET_DETECTION_HELP_PATH = helpPagePath(
+ 'user/application_security/secret_detection/index',
+);
+
+export const DEPENDENCY_SCANNING_NAME = s__('Dependency Scanning');
+export const DEPENDENCY_SCANNING_DESCRIPTION = s__(
+ 'Analyze your dependencies for known vulnerabilities.',
+);
+export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath(
+ 'user/application_security/dependency_scanning/index',
+);
+
+export const CONTAINER_SCANNING_NAME = s__('Container Scanning');
+export const CONTAINER_SCANNING_DESCRIPTION = s__(
+ 'Check your Docker images for known vulnerabilities.',
+);
+export const CONTAINER_SCANNING_HELP_PATH = helpPagePath(
+ 'user/application_security/container_scanning/index',
+);
+
+export const COVERAGE_FUZZING_NAME = s__('Coverage Fuzzing');
+export const COVERAGE_FUZZING_DESCRIPTION = s__(
+ 'Find bugs in your code with coverage-guided fuzzing.',
+);
+export const COVERAGE_FUZZING_HELP_PATH = helpPagePath(
+ 'user/application_security/coverage_fuzzing/index',
+);
+
+export const LICENSE_COMPLIANCE_NAME = s__('License Compliance');
+export const LICENSE_COMPLIANCE_DESCRIPTION = s__(
+ 'Search your project dependencies for their licenses and apply policies.',
+);
+export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath(
+ 'user/compliance/license_compliance/index',
+);
+
+export const UPGRADE_CTA = s__(
+ 'SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}',
+);
+
+export const features = [
+ {
+ name: SAST_NAME,
+ description: SAST_DESCRIPTION,
+ helpPath: SAST_HELP_PATH,
+ type: REPORT_TYPE_SAST,
+ },
+ {
+ name: DAST_NAME,
+ description: DAST_DESCRIPTION,
+ helpPath: DAST_HELP_PATH,
+ type: REPORT_TYPE_DAST,
+ },
+ {
+ name: SECRET_DETECTION_NAME,
+ description: SECRET_DETECTION_DESCRIPTION,
+ helpPath: SECRET_DETECTION_HELP_PATH,
+ type: REPORT_TYPE_SECRET_DETECTION,
+ },
+ {
+ name: DEPENDENCY_SCANNING_NAME,
+ description: DEPENDENCY_SCANNING_DESCRIPTION,
+ helpPath: DEPENDENCY_SCANNING_HELP_PATH,
+ type: REPORT_TYPE_DEPENDENCY_SCANNING,
+ },
+ {
+ name: CONTAINER_SCANNING_NAME,
+ description: CONTAINER_SCANNING_DESCRIPTION,
+ helpPath: CONTAINER_SCANNING_HELP_PATH,
+ type: REPORT_TYPE_CONTAINER_SCANNING,
+ },
+ {
+ name: COVERAGE_FUZZING_NAME,
+ description: COVERAGE_FUZZING_DESCRIPTION,
+ helpPath: COVERAGE_FUZZING_HELP_PATH,
+ type: REPORT_TYPE_COVERAGE_FUZZING,
+ },
+ {
+ name: LICENSE_COMPLIANCE_NAME,
+ description: LICENSE_COMPLIANCE_DESCRIPTION,
+ helpPath: LICENSE_COMPLIANCE_HELP_PATH,
+ type: REPORT_TYPE_LICENSE_COMPLIANCE,
+ },
+];
diff --git a/app/assets/javascripts/security_configuration/components/manage_sast.vue b/app/assets/javascripts/security_configuration/components/manage_sast.vue
new file mode 100644
index 00000000000..49bd3ba64e5
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/manage_sast.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ },
+ inject: {
+ projectPath: {
+ from: 'projectPath',
+ default: '',
+ },
+ },
+ data: () => ({
+ isLoading: false,
+ }),
+ methods: {
+ async mutate() {
+ this.isLoading = true;
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: configureSastMutation,
+ variables: {
+ input: {
+ projectPath: this.projectPath,
+ configuration: { global: [], pipeline: [], analyzers: [] },
+ },
+ },
+ });
+ const { errors, successPath } = data.configureSast;
+
+ if (errors.length > 0) {
+ throw new Error(errors[0]);
+ }
+
+ if (!successPath) {
+ throw new Error(s__('SecurityConfiguration|SAST merge request creation mutation failed'));
+ }
+
+ redirectTo(successPath);
+ } catch (e) {
+ this.$emit('error', e.message);
+ this.isLoading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button :loading="isLoading" variant="success" category="secondary" @click="mutate">{{
+ s__('SecurityConfiguration|Configure via Merge Request')
+ }}</gl-button>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/upgrade.vue b/app/assets/javascripts/security_configuration/components/upgrade.vue
new file mode 100644
index 00000000000..166ee4ff194
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/upgrade.vue
@@ -0,0 +1,26 @@
+<script>
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import { UPGRADE_CTA } from './features_constants';
+
+export default {
+ components: {
+ GlLink,
+ GlSprintf,
+ },
+ i18n: {
+ UPGRADE_CTA,
+ },
+};
+</script>
+
+<template>
+ <span>
+ <gl-sprintf :message="$options.i18n.UPGRADE_CTA">
+ <template #link="{ content }">
+ <gl-link target="_blank" href="https://about.gitlab.com/pricing/">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+</template>
diff --git a/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql
new file mode 100644
index 00000000000..9e826cf9e4b
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/configure_sast.mutation.graphql
@@ -0,0 +1,6 @@
+mutation configureSast($input: ConfigureSastInput!) {
+ configureSast(input: $input) {
+ successPath
+ errors
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
new file mode 100644
index 00000000000..c98fa46b32b
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import SecurityConfigurationApp from './components/app.vue';
+
+export const initStaticSecurityConfiguration = (el) => {
+ if (!el) {
+ return null;
+ }
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { projectPath } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ projectPath,
+ },
+ render(createElement) {
+ return createElement(SecurityConfigurationApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
index be5fd93f77c..cbd68f2513a 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -1,11 +1,9 @@
<script>
-// NOTE! For the first iteration, we are simply copying the implementation of Assignees
-// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { __, sprintf } from '~/locale';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
-const DEFAULT_RENDER_COUNT = 5;
+const LOADING_STATE = 'loading';
+const SUCCESS_STATE = 'success';
export default {
components: {
@@ -34,35 +32,21 @@ export default {
data() {
return {
showLess: true,
- loading: false,
- requestedReviewSuccess: false,
+ loadingStates: {},
};
},
- computed: {
- firstUser() {
- return this.users[0];
- },
- hasOneUser() {
- return this.users.length === 1;
- },
- hiddenReviewersLabel() {
- const { numberOfHiddenReviewers } = this;
- return sprintf(__('+ %{numberOfHiddenReviewers} more'), { numberOfHiddenReviewers });
- },
- renderShowMoreSection() {
- return this.users.length > DEFAULT_RENDER_COUNT;
- },
- numberOfHiddenReviewers() {
- return this.users.length - DEFAULT_RENDER_COUNT;
- },
- uncollapsedUsers() {
- const uncollapsedLength = this.showLess
- ? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
- : this.users.length;
- return this.showLess ? this.users.slice(0, uncollapsedLength) : this.users;
- },
- username() {
- return `@${this.firstUser.username}`;
+ watch: {
+ users: {
+ handler(users) {
+ this.loadingStates = users.reduce(
+ (acc, user) => ({
+ ...acc,
+ [user.id]: acc[user.id] || null,
+ }),
+ this.loadingStates,
+ );
+ },
+ immediate: true,
},
},
methods: {
@@ -70,21 +54,23 @@ export default {
this.showLess = !this.showLess;
},
reRequestReview(userId) {
- this.loading = true;
+ this.loadingStates[userId] = LOADING_STATE;
this.$emit('request-review', { userId, callback: this.requestReviewComplete });
},
- requestReviewComplete(success) {
+ requestReviewComplete(userId, success) {
if (success) {
- this.requestedReviewSuccess = true;
+ this.loadingStates[userId] = SUCCESS_STATE;
setTimeout(() => {
- this.requestedReviewSuccess = false;
+ this.loadingStates[userId] = null;
}, 1500);
+ } else {
+ this.loadingStates[userId] = null;
}
-
- this.loading = false;
},
},
+ LOADING_STATE,
+ SUCCESS_STATE,
};
</script>
@@ -100,20 +86,22 @@ export default {
<div class="gl-ml-3">@{{ user.username }}</div>
</reviewer-avatar-link>
<gl-icon
- v-if="requestedReviewSuccess"
+ v-if="loadingStates[user.id] === $options.SUCCESS_STATE"
:size="24"
name="check"
class="float-right gl-text-green-500"
+ data-testid="re-request-success"
/>
<gl-button
v-else-if="user.can_update_merge_request && user.reviewed"
v-gl-tooltip.left
:title="__('Re-request review')"
- :loading="loading"
+ :loading="loadingStates[user.id] === $options.LOADING_STATE"
class="float-right gl-text-gray-500!"
size="small"
icon="redo"
variant="link"
+ data-testid="re-request-button"
@click="reRequestReview(user.id)"
/>
</div>
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index b23788f81fe..c4fe1be83fc 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -58,9 +58,9 @@ export default class SidebarMediator {
.then(() => {
this.store.updateReviewer(userId);
toast(__('Requested review'));
- callback(true);
+ callback(userId, true);
})
- .catch(() => callback(false));
+ .catch(() => callback(userId, false));
}
setMoveToProjectId(projectId) {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 92223c9058e..f430bf80cbb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -56,7 +56,10 @@ export default {
mergeRequestsFfOnlyEnabled: data.project.mergeRequestsFfOnlyEnabled,
onlyAllowMergeIfPipelineSucceeds: data.project.onlyAllowMergeIfPipelineSucceeds,
};
- this.removeSourceBranch = data.project.mergeRequest.shouldRemoveSourceBranch;
+ this.removeSourceBranch =
+ data.project.mergeRequest.shouldRemoveSourceBranch ||
+ data.project.mergeRequest.forceRemoveSourceBranch ||
+ false;
this.commitMessage = data.project.mergeRequest.defaultMergeCommitMessage;
this.squashBeforeMerge = data.project.mergeRequest.squashOnMerge;
this.isSquashReadOnly = data.project.squashReadOnly;
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index 9479ef3cf79..8ee45b05431 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -5,6 +5,7 @@ fragment ReadyToMerge on Project {
mergeRequest(iid: $iid) {
autoMergeEnabled
shouldRemoveSourceBranch
+ forceRemoveSourceBranch
defaultMergeCommitMessage
defaultMergeCommitMessageWithDescription
defaultSquashCommitMessage
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index dd591f7bba3..aac5a5c1def 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -17,7 +17,13 @@ export const REPORT_FILE_TYPES = {
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_SAST = 'sast';
+export const REPORT_TYPE_DAST = 'dast';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
+export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning';
+export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning';
+export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
+export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_compliance';
+export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
/**
* SecurityReportTypeEnum values for use with GraphQL.
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 2cef43f19ab..036d95622ef 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -243,7 +243,8 @@ module NotesActions
:type,
:note,
:line_code, # LegacyDiffNote
- :position # DiffNote
+ :position, # DiffNote
+ :confidential
).tap do |create_params|
create_params.merge!(
params.permit(:merge_request_diff_head_sha, :in_reply_to_discussion_id)
diff --git a/app/controllers/concerns/redis_tracking.rb b/app/controllers/concerns/redis_tracking.rb
index d71935356b8..a7e75f802a8 100644
--- a/app/controllers/concerns/redis_tracking.rb
+++ b/app/controllers/concerns/redis_tracking.rb
@@ -7,30 +7,26 @@
#
# include RedisTracking
#
-# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :my_feature
-#
-# if the feature flag is enabled by default you should use
-# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score', feature: :my_feature, feature_default_enabled: true
+# track_redis_hll_event :index, :show, name: 'i_analytics_dev_ops_score'
#
# You can also pass custom conditions using `if:`, using the same format as with Rails callbacks.
module RedisTracking
extend ActiveSupport::Concern
class_methods do
- def track_redis_hll_event(*controller_actions, name:, feature:, feature_default_enabled: false, if: nil)
+ def track_redis_hll_event(*controller_actions, name:, if: nil)
custom_conditions = Array.wrap(binding.local_variable_get('if'))
conditions = [:trackable_request?, *custom_conditions]
after_action only: controller_actions, if: conditions do
- track_unique_redis_hll_event(name, feature, feature_default_enabled)
+ track_unique_redis_hll_event(name)
end
end
end
private
- def track_unique_redis_hll_event(event_name, feature, feature_default_enabled)
- return unless metric_feature_enabled?(feature, feature_default_enabled)
+ def track_unique_redis_hll_event(event_name)
return unless visitor_id
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(event_name, values: visitor_id)
@@ -40,10 +36,6 @@ module RedisTracking
request.format.html? && request.headers['DNT'] != '1'
end
- def metric_feature_enabled?(feature, default_enabled)
- Feature.enabled?(feature, default_enabled: default_enabled)
- end
-
def visitor_id
return cookies[:visitor_id] if cookies[:visitor_id].present?
return unless current_user
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index c93e75b438b..0ee8d0c9307 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -15,7 +15,7 @@ module SnippetsActions
skip_before_action :verify_authenticity_token,
if: -> { action_name == 'show' && js_request? }
- track_redis_hll_event :show, name: 'i_snippets_show', feature: :usage_data_i_snippets_show, feature_default_enabled: true
+ track_redis_hll_event :show, name: 'i_snippets_show'
respond_to :html
end
diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb
index 1ae90edd8f7..4014e4f0024 100644
--- a/app/controllers/concerns/wiki_actions.rb
+++ b/app/controllers/concerns/wiki_actions.rb
@@ -36,8 +36,7 @@ module WikiActions
# NOTE: We want to include wiki page views in the same counter as the other
# Event-based wiki actions tracked through TrackUniqueEvents, so we use the same event name.
- track_redis_hll_event :show, name: Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION.to_s,
- feature: :track_unique_wiki_page_views, feature_default_enabled: true
+ track_redis_hll_event :show, name: Gitlab::UsageDataCounters::TrackUniqueEvents::WIKI_ACTION.to_s
helper_method :view_file_button, :diff_file_html_data
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 8c66f45dd79..3bb00978aac 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -35,7 +35,7 @@ class Projects::BlobController < Projects::ApplicationController
record_experiment_user(:ci_syntax_templates, namespace_id: @project.namespace_id) if params[:file_name] == @project.ci_config_path_or_default
end
- track_redis_hll_event :create, :update, name: 'g_edit_by_sfe', feature: :track_editor_edit_actions, feature_default_enabled: true
+ track_redis_hll_event :create, :update, name: 'g_edit_by_sfe'
feature_category :source_code_management
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index ff8c790f43d..c8bdbe548c8 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -52,6 +52,7 @@ class Projects::IssuesController < Projects::ApplicationController
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project)
push_to_gon_attributes(:features, real_time_feature_flag, real_time_enabled)
+ push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b)
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 74936abe59c..59b14bbb91d 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -9,6 +9,7 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :set_pipeline_path, only: [:show]
before_action :authorize_read_pipeline!
before_action :authorize_read_build!, only: [:index]
+ before_action :authorize_read_analytics!, only: [:charts]
before_action :authorize_create_pipeline!, only: [:new, :create, :config_variables]
before_action :authorize_update_pipeline!, only: [:retry, :cancel]
before_action do
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 40e6590d85c..820b00a902e 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -5,7 +5,7 @@ class SearchController < ApplicationController
include SearchHelper
include RedisTracking
- track_redis_hll_event :show, name: 'i_search_total', feature: :search_track_unique_users, feature_default_enabled: true
+ track_redis_hll_event :show, name: 'i_search_total'
around_action :allow_gitaly_ref_name_caching
diff --git a/app/graphql/queries/container_registry/get_container_repositories.query.graphql b/app/graphql/queries/container_registry/get_container_repositories.query.graphql
index 6171233c446..4683ef9dfdb 100644
--- a/app/graphql/queries/container_registry/get_container_repositories.query.graphql
+++ b/app/graphql/queries/container_registry/get_container_repositories.query.graphql
@@ -6,11 +6,19 @@ query getProjectContainerRepositories(
$after: String
$before: String
$isGroupPage: Boolean!
+ $sort: ContainerRepositorySort
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
__typename
containerRepositoriesCount
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
__typename
nodes {
id
@@ -35,7 +43,14 @@ query getProjectContainerRepositories(
group(fullPath: $fullPath) @include(if: $isGroupPage) {
__typename
containerRepositoriesCount
- containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
+ containerRepositories(
+ name: $name
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ sort: $sort
+ ) {
__typename
nodes {
id
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 8e32661d4cb..0cefc84633d 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -12,6 +12,9 @@ module Types
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the user.'
+ field :bot, GraphQL::BOOLEAN_TYPE, null: false,
+ description: 'Indicates if the user is a bot.',
+ method: :bot?
field :username, GraphQL::STRING_TYPE, null: false,
description: 'Username of the user. Unique within this instance of GitLab.'
field :name, GraphQL::STRING_TYPE, null: false,
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 58aaadd5d49..3be107ea2e1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -16,6 +16,7 @@ module Ci
include ShaAttribute
include FromUnion
include UpdatedAtFilterable
+ include EachBatch
MAX_OPEN_MERGE_REQUESTS_REFS = 4
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index e0c2b308247..2f0fd0af63b 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -55,6 +55,7 @@ class CommitStatus < ApplicationRecord
scope :for_ids, -> (ids) { where(id: ids) }
scope :for_ref, -> (ref) { where(ref: ref) }
scope :by_name, -> (name) { where(name: name) }
+ scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
scope :for_project_paths, -> (paths) do
where(project: Project.where_full_path_in(Array(paths)))
diff --git a/app/models/member.rb b/app/models/member.rb
index 2e79b50d6b7..62fe757683f 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -47,6 +47,19 @@ class Member < ApplicationRecord
},
if: :project_bot?
+ scope :in_hierarchy, ->(source) do
+ groups = source.root_ancestor.self_and_descendants
+ group_members = Member.default_scoped.where(source: groups)
+
+ projects = source.root_ancestor.all_projects
+ project_members = Member.default_scoped.where(source: projects)
+
+ Member.default_scoped.from_union([
+ group_members,
+ project_members
+ ]).merge(self)
+ end
+
# This scope encapsulates (most of) the conditions a row in the member table
# must satisfy if it is a valid permission. Of particular note:
#
@@ -79,12 +92,18 @@ class Member < ApplicationRecord
scope :invite, -> { where.not(invite_token: nil) }
scope :non_invite, -> { where(invite_token: nil) }
+
scope :request, -> { where.not(requested_at: nil) }
scope :non_request, -> { where(requested_at: nil) }
scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) }
scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) }
scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) }
+
+ scope :created_today, -> do
+ now = Date.current
+ where(created_at: now.beginning_of_day..now.end_of_day)
+ end
scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) }
scope :has_access, -> { active.where('access_level > 0') }
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index d0e62a1afba..ab043227832 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -183,7 +183,17 @@ class PrometheusService < MonitoringService
manual_configuration? && google_iap_audience_client_id.present? && google_iap_service_account_json.present?
end
+ def clean_google_iap_service_account
+ return unless google_iap_service_account_json
+
+ google_iap_service_account_json
+ .then { |json| Gitlab::Json.parse(json) }
+ .except('token_credential_uri')
+ end
+
def iap_client
- @iap_client ||= Google::Auth::Credentials.new(Gitlab::Json.parse(google_iap_service_account_json), target_audience: google_iap_audience_client_id).client
+ @iap_client ||= Google::Auth::Credentials
+ .new(clean_google_iap_service_account, target_audience: google_iap_audience_client_id)
+ .client
end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 7605ef54d5b..8c3dcaa7c0f 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -31,6 +31,7 @@ class ProjectStatistics < ApplicationRecord
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) }
+ scope :with_any_ci_minutes_used, -> { where.not(shared_runners_seconds: 0) }
def total_repository_size
repository_size + lfs_objects_size
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index f97d94c14f0..aaf985d6c63 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -221,6 +221,7 @@ class ProjectPolicy < BasePolicy
enable :read_pages_content
enable :read_release
enable :read_analytics
+ enable :read_insights
end
# These abilities are not allowed to admins that are not members of the project,
@@ -450,6 +451,9 @@ class ProjectPolicy < BasePolicy
rule { analytics_disabled }.policy do
prevent(:read_analytics)
+ prevent(:read_insights)
+ prevent(:read_cycle_analytics)
+ prevent(:read_repository_graphs)
end
rule { wiki_disabled }.policy do
@@ -523,6 +527,7 @@ class ProjectPolicy < BasePolicy
enable :read_cycle_analytics
enable :read_pages_content
enable :read_analytics
+ enable :read_insights
# NOTE: may be overridden by IssuePolicy
enable :read_issue
diff --git a/app/services/ci/abort_project_pipelines_service.rb b/app/services/ci/abort_project_pipelines_service.rb
new file mode 100644
index 00000000000..0b2fa9ed3c0
--- /dev/null
+++ b/app/services/ci/abort_project_pipelines_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ci
+ class AbortProjectPipelinesService
+ # Danger: Cancels in bulk without callbacks
+ # Only for pipeline abandonment scenarios (current example: project delete)
+ def execute(project)
+ return unless Feature.enabled?(:abort_deleted_project_pipelines, default_enabled: :yaml)
+
+ pipelines = project.all_pipelines.cancelable
+ bulk_abort!(pipelines, status: :canceled)
+
+ ServiceResponse.success(message: 'Pipelines canceled')
+ end
+
+ private
+
+ def bulk_abort!(pipelines, status:)
+ pipelines.each_batch do |pipeline_batch|
+ CommitStatus.in_pipelines(pipeline_batch).in_batches.update_all(status: status) # rubocop: disable Cop/InBatches
+ pipeline_batch.update_all(status: status)
+ end
+ end
+ end
+end
diff --git a/app/services/ci/cancel_user_pipelines_service.rb b/app/services/ci/cancel_user_pipelines_service.rb
index 3a8b5e91088..3d3a8032e8e 100644
--- a/app/services/ci/cancel_user_pipelines_service.rb
+++ b/app/services/ci/cancel_user_pipelines_service.rb
@@ -6,6 +6,7 @@ module Ci
# This is a bug with CodeReuse/ActiveRecord cop
# https://gitlab.com/gitlab-org/gitlab/issues/32332
def execute(user)
+ # TODO: fix N+1 queries https://gitlab.com/gitlab-org/gitlab/-/issues/300685
user.pipelines.cancelable.find_each(&:cancel_running)
ServiceResponse.success(message: 'Pipeline canceled')
diff --git a/app/services/design_management/save_designs_service.rb b/app/services/design_management/save_designs_service.rb
index da653e2524a..c26d2e7ab47 100644
--- a/app/services/design_management/save_designs_service.rb
+++ b/app/services/design_management/save_designs_service.rb
@@ -18,7 +18,6 @@ module DesignManagement
return error("Only #{MAX_FILES} files are allowed simultaneously") if files.size > MAX_FILES
return error("Duplicate filenames are not allowed!") if files.map(&:original_filename).uniq.length != files.length
return error("Design copy is in progress") if design_collection.copy_in_progress?
- return error("Filenames contained invalid characters and could not be saved") if files.any?(&:filename_sanitized?)
uploaded_designs, version = upload_designs!
skipped_designs = designs - uploaded_designs
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 5fcf2d711b0..cffccda1a44 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -2,12 +2,12 @@
module Members
class CreateService < Members::BaseService
+ include Gitlab::Utils::StrongMemoize
+
DEFAULT_LIMIT = 100
def execute(source)
- return error(s_('AddMember|No users specified.')) if params[:user_ids].blank?
-
- user_ids = params[:user_ids].split(',').uniq.flatten
+ return error(s_('AddMember|No users specified.')) if user_ids.blank?
return error(s_("AddMember|Too many users specified (limit is %{user_limit})") % { user_limit: user_limit }) if
user_limit && user_ids.size > user_limit
@@ -47,6 +47,13 @@ module Members
private
+ def user_ids
+ strong_memoize(:user_ids) do
+ ids = params[:user_ids] || ''
+ ids.split(',').uniq.flatten
+ end
+ end
+
def user_limit
limit = params.fetch(:limit, DEFAULT_LIMIT)
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index bec75657530..c1501625300 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -21,11 +21,14 @@ module Projects
def execute
return false unless can?(current_user, :remove_project, project)
+ project.update_attribute(:pending_delete, true)
# Flush the cache for both repositories. This has to be done _before_
# removing the physical repositories as some expiration code depends on
# Git data (e.g. a list of branch names).
flush_caches(project)
+ ::Ci::AbortProjectPipelinesService.new.execute(project)
+
Projects::UnlinkForkService.new(project, current_user).execute
attempt_destroy(project)
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
index 4f4b6c1089c..899e58050af 100644
--- a/app/views/groups/registry/repositories/index.html.haml
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout
-- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true} )
+- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true, sort: nil} )
%section
#js-container-registry{ data: { endpoint: group_container_registries_path(@group),
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
deleted file mode 100644
index 9ad87518b1e..00000000000
--- a/app/views/help/_shortcuts.html.haml
+++ /dev/null
@@ -1,353 +0,0 @@
-#modal-shortcuts.modal{ tabindex: -1 }
- .modal-dialog.modal-lg.modal-1040
- .modal-content
- .modal-header
- .js-toggle-shortcuts
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": true } &times;
- .modal-body
- .row
- .col-lg-4
- %table.shortcut-mappings.text-2
- %tbody
- %tr
- %th
- %th= _('Global Shortcuts')
- %tr
- %td.shortcut
- %kbd ?
- %td= _('Toggle this dialog')
- %tr
- %td.shortcut
- %kbd shift p
- %td= _('Go to your projects')
- %tr
- %td.shortcut
- %kbd shift g
- %td= _('Go to your groups')
- %tr
- %td.shortcut
- %kbd shift a
- %td= _('Go to the activity feed')
- %tr
- %td.shortcut
- %kbd shift l
- %td= _('Go to the milestone list')
- %tr
- %td.shortcut
- %kbd shift s
- %td= _('Go to your snippets')
- %tr
- %td.shortcut
- %kbd s
- \/
- %kbd /
- %td= _('Start search')
- %tr
- %td.shortcut
- %kbd shift i
- %td= _('Go to your issues')
- %tr
- %td.shortcut
- %kbd shift m
- %td= _('Go to your merge requests')
- %tr
- %td.shortcut
- %kbd shift t
- %td= _('Go to your To-Do list')
- %tr
- %td.shortcut
- %kbd p
- %kbd b
- %td= _('Toggle the Performance Bar')
- - if Gitlab.com?
- %tr
- %td.shortcut
- %kbd g
- %kbd x
- %td= _('Toggle GitLab Next')
- %tbody
- %tr
- %th
- %th= _('Editing')
- %tr
- %td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; shift p
- - else
- %kbd ctrl shift p
- %td= _('Toggle Markdown preview')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-up', size: 12)
- %td= _('Edit your most recent comment in a thread (from an empty textarea)')
- %tbody
- %tr
- %th
- %th= _('Wiki')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit wiki page')
- %tbody
- %tr
- %th
- %th= _('Repository Graph')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-left', size: 12)
- \/
- %kbd h
- %td= _('Scroll left')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-right', size: 12)
- \/
- %kbd l
- %td= _('Scroll right')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-up', size: 12)
- \/
- %kbd k
- %td= _('Scroll up')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-down', size: 12)
- \/
- %kbd j
- %td= _('Scroll down')
- %tr
- %td.shortcut
- %kbd
- shift
- = sprite_icon('arrow-up', size: 12)
- \/ k
- %td= _('Scroll to top')
- %tr
- %td.shortcut
- %kbd
- shift
- = sprite_icon('arrow-down', size: 12)
- \/ j
- %td= _('Scroll to bottom')
- .col-lg-4
- %table.shortcut-mappings.text-2
- %tbody
- %tr
- %th
- %th= _('Project')
- %tr
- %td.shortcut
- %kbd g
- %kbd p
- %td= _('Go to the project\'s overview page')
- %tr
- %td.shortcut
- %kbd g
- %kbd v
- %td= _('Go to the project\'s activity feed')
- %tr
- %td.shortcut
- %kbd g
- %kbd r
- %td= _('Go to releases')
- %tr
- %td.shortcut
- %kbd g
- %kbd f
- %td= _('Go to files')
- %tr
- %td.shortcut
- %kbd t
- %td= _('Go to find file')
- %tr
- %td.shortcut
- %kbd g
- %kbd c
- %td= _('Go to commits')
- %tr
- %td.shortcut
- %kbd g
- %kbd n
- %td= _('Go to repository graph')
- %tr
- %td.shortcut
- %kbd g
- %kbd d
- %td= _('Go to repository charts')
- %tr
- %td.shortcut
- %kbd g
- %kbd i
- %td= _('Go to issues')
- %tr
- %td.shortcut
- %kbd i
- %td= _('New issue')
- %tr
- %td.shortcut
- %kbd g
- %kbd b
- %td= _('Go to issue boards')
- %tr
- %td.shortcut
- %kbd g
- %kbd m
- %td= _('Go to merge requests')
- %tr
- %td.shortcut
- %kbd g
- %kbd j
- %td= _('Go to jobs')
- %tr
- %td.shortcut
- %kbd g
- %kbd l
- %td= _('Go to metrics')
- %tr
- %td.shortcut
- %kbd g
- %kbd e
- %td= _('Go to environments')
- %tr
- %td.shortcut
- %kbd g
- %kbd k
- %td= _('Go to kubernetes')
- %tr
- %td.shortcut
- %kbd g
- %kbd s
- %td= _('Go to snippets')
- %tr
- %td.shortcut
- %kbd g
- %kbd w
- %td= _('Go to wiki')
- %tbody
- %tr
- %th
- %th= _('Project Files')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-up', size: 12)
- %td= _('Move selection up')
- %tr
- %td.shortcut
- %kbd
- = sprite_icon('arrow-down', size: 12)
- %td= _('Move selection down')
- %tr
- %td.shortcut
- %kbd enter
- %td= _('Open Selection')
- %tr
- %td.shortcut
- %kbd esc
- %td= _('Go back (while searching for files)')
- %tr
- %td.shortcut
- %kbd y
- %td= _('Go to file permalink (while viewing a file)')
- .col-lg-4
- %table.shortcut-mappings.text-2
- %tbody
- %tr
- %th
- %th= _('Epics, Issues, and Merge Requests')
- %tr
- %td.shortcut
- %kbd r
- %td= _('Comment/Reply (quoting selected text)')
- %tr
- %td.shortcut
- %kbd e
- %td= _('Edit description')
- %tr
- %td.shortcut
- %kbd l
- %td= _('Change label')
- %tbody
- %tr
- %th
- %th= _('Issues and Merge Requests')
- %tr
- %td.shortcut
- %kbd a
- %td= _('Change assignee')
- %tr
- %td.shortcut
- %kbd m
- %td= _('Change milestone')
- %tbody
- %tr
- %th
- %th= _('Merge Requests')
- %tr
- %td.shortcut
- %kbd ]
- \/
- %kbd j
- %td= _('Next file in diff')
- %tr
- %td.shortcut
- %kbd [
- \/
- %kbd k
- %td= _('Previous file in diff')
- %tr
- %td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; p
- - else
- %kbd ctrl p
- %td= _('Go to file')
- %tr
- %td.shortcut
- %kbd n
- %td= _('Next unresolved discussion')
- %tr
- %td.shortcut
- %kbd p
- %td= _('Previous unresolved discussion')
- %tr
- %td.shortcut
- %kbd b
- %td= _('Copy source branch name')
- %tbody
- %tr
- %th
- %th= _('Merge Request Commits')
- %tr
- %td.shortcut
- %kbd c
- %td= _('Next commit')
- %tr
- %td.shortcut
- %kbd x
- %td= _('Previous commit')
- %tbody
- %tr
- %th
- %th= _('Web IDE')
- %tr
- %td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; p
- - else
- %kbd ctrl p
- %td= _('Go to file')
- %tr
- %td.shortcut
- - if browser.platform.mac?
- %kbd &#8984; enter
- - else
- %kbd ctrl enter
- %td= _('Commit (when editing commit message)')
diff --git a/app/views/help/shortcuts.js.haml b/app/views/help/shortcuts.js.haml
deleted file mode 100644
index 99ed042ea3b..00000000000
--- a/app/views/help/shortcuts.js.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-:plain
- $("body").append("#{escape_javascript(render('shortcuts'))}");
- $("#modal-shortcuts").modal();
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index 8004a5facd7..7c896cd71ef 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -28,7 +28,7 @@
= _('GPG Key ID:')
%span.monospace= signature.gpg_key_primary_keyid
- = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
+ = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link gl-display-block')
%a{ role: 'button', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 93e94928110..a2009b96c0d 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout
-- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false} )
+- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil} )
%section
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
diff --git a/app/views/projects/security/configuration/show.html.haml b/app/views/projects/security/configuration/show.html.haml
index 1a371955be8..fe47ce327c2 100644
--- a/app/views/projects/security/configuration/show.html.haml
+++ b/app/views/projects/security/configuration/show.html.haml
@@ -1,4 +1,4 @@
- breadcrumb_title _("Security Configuration")
- page_title _("Security Configuration")
-#js-security-configuration-static
+#js-security-configuration-static{ data: {project_path: @project.full_path} }
diff --git a/changelogs/unreleased/207473-allow-set-confidential-note-attribute.yml b/changelogs/unreleased/207473-allow-set-confidential-note-attribute.yml
new file mode 100644
index 00000000000..78eb6e32060
--- /dev/null
+++ b/changelogs/unreleased/207473-allow-set-confidential-note-attribute.yml
@@ -0,0 +1,5 @@
+---
+title: Support setting confidential note attribute in UI
+merge_request: 52949
+author: Lee Tickett @leetickett
+type: added
diff --git a/changelogs/unreleased/267140-add-user-bot-gql.yml b/changelogs/unreleased/267140-add-user-bot-gql.yml
new file mode 100644
index 00000000000..cf2f13116a9
--- /dev/null
+++ b/changelogs/unreleased/267140-add-user-bot-gql.yml
@@ -0,0 +1,5 @@
+---
+title: Add bot to User GraphQL Type
+merge_request: 52933
+author:
+type: added
diff --git a/changelogs/unreleased/290302-update-the-default-sort-order-of-the-image-repository-list.yml b/changelogs/unreleased/290302-update-the-default-sort-order-of-the-image-repository-list.yml
new file mode 100644
index 00000000000..7d68ada42ed
--- /dev/null
+++ b/changelogs/unreleased/290302-update-the-default-sort-order-of-the-image-repository-list.yml
@@ -0,0 +1,5 @@
+---
+title: Add sort to container registry list page
+merge_request: 53820
+author:
+type: changed
diff --git a/changelogs/unreleased/296754-followup-from-refactor-namespaceonboardingaction-model-to-onboardi.yml b/changelogs/unreleased/296754-followup-from-refactor-namespaceonboardingaction-model-to-onboardi.yml
new file mode 100644
index 00000000000..226033c26ba
--- /dev/null
+++ b/changelogs/unreleased/296754-followup-from-refactor-namespaceonboardingaction-model-to-onboardi.yml
@@ -0,0 +1,5 @@
+---
+title: Remove namespace_onboarding_actions table
+merge_request: 53488
+author:
+type: other
diff --git a/changelogs/unreleased/297346-flaky-spec-in-ee-spec-features-burnup_charts_spec-rb-burnup-charts.yml b/changelogs/unreleased/297346-flaky-spec-in-ee-spec-features-burnup_charts_spec-rb-burnup-charts.yml
new file mode 100644
index 00000000000..72e55f73bcc
--- /dev/null
+++ b/changelogs/unreleased/297346-flaky-spec-in-ee-spec-features-burnup_charts_spec-rb-burnup-charts.yml
@@ -0,0 +1,5 @@
+---
+title: Fix charts sometimes being hidden on milestone page
+merge_request: 52552
+author:
+type: fixed
diff --git a/changelogs/unreleased/kassio-bulkimports-import-group-membership.yml b/changelogs/unreleased/kassio-bulkimports-import-group-membership.yml
new file mode 100644
index 00000000000..bf75f1f68af
--- /dev/null
+++ b/changelogs/unreleased/kassio-bulkimports-import-group-membership.yml
@@ -0,0 +1,5 @@
+---
+title: 'BulkImports: Migrate Group Membership'
+merge_request: 53083
+author:
+type: added
diff --git a/changelogs/unreleased/khanchi-designs-patch2.yml b/changelogs/unreleased/khanchi-designs-patch2.yml
deleted file mode 100644
index 1baee07be88..00000000000
--- a/changelogs/unreleased/khanchi-designs-patch2.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Designs: return error if uploading filenames with special chars'
-merge_request: 44136
-author: Sushil Khanchi @khanchi97
-type: fixed
diff --git a/changelogs/unreleased/link-new-line-gpg.yml b/changelogs/unreleased/link-new-line-gpg.yml
new file mode 100644
index 00000000000..7cc450b100e
--- /dev/null
+++ b/changelogs/unreleased/link-new-line-gpg.yml
@@ -0,0 +1,5 @@
+---
+title: Show helper link on a new line in GPG status popover
+merge_request: 52894
+author: Yogi (@yo)
+type: changed
diff --git a/changelogs/unreleased/mc-backstage-reduce-db-updates-ci-minute-reset.yml b/changelogs/unreleased/mc-backstage-reduce-db-updates-ci-minute-reset.yml
new file mode 100644
index 00000000000..7770e9a597f
--- /dev/null
+++ b/changelogs/unreleased/mc-backstage-reduce-db-updates-ci-minute-reset.yml
@@ -0,0 +1,5 @@
+---
+title: Reset CI minutes only for namespaces that used minutes.
+merge_request: 53740
+author:
+type: changed
diff --git a/config/feature_flags/development/abort_deleted_project_pipelines.yml b/config/feature_flags/development/abort_deleted_project_pipelines.yml
new file mode 100644
index 00000000000..f09cc9dd86b
--- /dev/null
+++ b/config/feature_flags/development/abort_deleted_project_pipelines.yml
@@ -0,0 +1,8 @@
+---
+name: abort_deleted_project_pipelines
+introduced_by_url: https://gitlab.com/gitlab-org/security/gitlab/-/merge_requests/1220
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/301106
+milestone: '13.9'
+type: development
+group: group::continuous integration
+default_enabled: true
diff --git a/config/feature_flags/development/confidential_notes.yml b/config/feature_flags/development/confidential_notes.yml
new file mode 100644
index 00000000000..4e9add5eb3c
--- /dev/null
+++ b/config/feature_flags/development/confidential_notes.yml
@@ -0,0 +1,8 @@
+---
+name: confidential_notes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52949
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/207474
+milestone: '13.9'
+type: development
+group: group::product planning
+default_enabled: false
diff --git a/config/known_invalid_graphql_queries.yml b/config/known_invalid_graphql_queries.yml
index 3bd117b38e6..8c9ba5cb83a 100644
--- a/config/known_invalid_graphql_queries.yml
+++ b/config/known_invalid_graphql_queries.yml
@@ -3,3 +3,4 @@ filenames:
- ee/app/assets/javascripts/oncall_schedules/graphql/mutations/update_oncall_schedule_rotation.mutation.graphql
- ee/app/assets/javascripts/security_configuration/api_fuzzing/graphql/api_fuzzing_ci_configuration.query.graphql
- ee/app/assets/javascripts/on_demand_scans/graphql/dast_profile_update.mutation.graphql
+ - ee/app/assets/javascripts/security_configuration/api_fuzzing/graphql/create_api_fuzzing_configuration.mutation.graphql
diff --git a/db/migrate/20201007033527_add_daily_invites_to_plan_limits.rb b/db/migrate/20201007033527_add_daily_invites_to_plan_limits.rb
new file mode 100644
index 00000000000..8f0079cd639
--- /dev/null
+++ b/db/migrate/20201007033527_add_daily_invites_to_plan_limits.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddDailyInvitesToPlanLimits < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column(:plan_limits, :daily_invites, :integer, default: 0, null: false)
+ end
+end
diff --git a/db/migrate/20201007033723_insert_daily_invites_plan_limits.rb b/db/migrate/20201007033723_insert_daily_invites_plan_limits.rb
new file mode 100644
index 00000000000..dcdcbbb0964
--- /dev/null
+++ b/db/migrate/20201007033723_insert_daily_invites_plan_limits.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+class InsertDailyInvitesPlanLimits < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ return unless Gitlab.com?
+
+ create_or_update_plan_limit('daily_invites', 'free', 20)
+ create_or_update_plan_limit('daily_invites', 'bronze', 0)
+ create_or_update_plan_limit('daily_invites', 'silver', 0)
+ create_or_update_plan_limit('daily_invites', 'gold', 0)
+ end
+
+ def down
+ return unless Gitlab.com?
+
+ create_or_update_plan_limit('daily_invites', 'free', 0)
+ create_or_update_plan_limit('daily_invites', 'bronze', 0)
+ create_or_update_plan_limit('daily_invites', 'silver', 0)
+ create_or_update_plan_limit('daily_invites', 'gold', 0)
+ end
+end
diff --git a/db/migrate/20210205143926_remove_namespace_id_foreign_key_on_namespace_onboarding_actions.rb b/db/migrate/20210205143926_remove_namespace_id_foreign_key_on_namespace_onboarding_actions.rb
new file mode 100644
index 00000000000..6fe66430dd0
--- /dev/null
+++ b/db/migrate/20210205143926_remove_namespace_id_foreign_key_on_namespace_onboarding_actions.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class RemoveNamespaceIdForeignKeyOnNamespaceOnboardingActions < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ remove_foreign_key :namespace_onboarding_actions, :namespaces
+ end
+ end
+
+ def down
+ with_lock_retries do
+ add_foreign_key :namespace_onboarding_actions, :namespaces, on_delete: :cascade
+ end
+ end
+end
diff --git a/db/post_migrate/20210205144537_remove_namespace_onboarding_actions_table.rb b/db/post_migrate/20210205144537_remove_namespace_onboarding_actions_table.rb
new file mode 100644
index 00000000000..210b1d7822c
--- /dev/null
+++ b/db/post_migrate/20210205144537_remove_namespace_onboarding_actions_table.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class RemoveNamespaceOnboardingActionsTable < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ drop_table :namespace_onboarding_actions
+ end
+ end
+
+ def down
+ with_lock_retries do
+ create_table :namespace_onboarding_actions do |t|
+ t.references :namespace, index: true, null: false
+ t.datetime_with_timezone :created_at, null: false
+ t.integer :action, limit: 2, null: false
+ end
+ end
+ end
+end
diff --git a/db/schema_migrations/20201007033527 b/db/schema_migrations/20201007033527
new file mode 100644
index 00000000000..b2cedd57973
--- /dev/null
+++ b/db/schema_migrations/20201007033527
@@ -0,0 +1 @@
+1200747265d5095a86250020786d6f1e9e50bc75328a71de497046807afa89d7 \ No newline at end of file
diff --git a/db/schema_migrations/20201007033723 b/db/schema_migrations/20201007033723
new file mode 100644
index 00000000000..c874ae0475b
--- /dev/null
+++ b/db/schema_migrations/20201007033723
@@ -0,0 +1 @@
+febefead6f966960f6493d29add5f35fc4a1080b5118c5526502fa5fe1d29023 \ No newline at end of file
diff --git a/db/schema_migrations/20210205143926 b/db/schema_migrations/20210205143926
new file mode 100644
index 00000000000..00a8c3528a7
--- /dev/null
+++ b/db/schema_migrations/20210205143926
@@ -0,0 +1 @@
+cdf55e9f2b1b9c375920198a438d29fe3c9ab7147f3c670b0d66b11d499573d9 \ No newline at end of file
diff --git a/db/schema_migrations/20210205144537 b/db/schema_migrations/20210205144537
new file mode 100644
index 00000000000..6ca27521248
--- /dev/null
+++ b/db/schema_migrations/20210205144537
@@ -0,0 +1 @@
+d9cfb7515805e642c562b8be58b6cd482c24e62e76245db35a7d91b25c327d8d \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 5c1791e6264..3088c2d03aa 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -14289,22 +14289,6 @@ CREATE TABLE namespace_limits (
temporary_storage_increase_ends_on date
);
-CREATE TABLE namespace_onboarding_actions (
- id bigint NOT NULL,
- namespace_id bigint NOT NULL,
- created_at timestamp with time zone NOT NULL,
- action smallint NOT NULL
-);
-
-CREATE SEQUENCE namespace_onboarding_actions_id_seq
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-ALTER SEQUENCE namespace_onboarding_actions_id_seq OWNED BY namespace_onboarding_actions.id;
-
CREATE TABLE namespace_package_settings (
namespace_id bigint NOT NULL,
maven_duplicates_allowed boolean DEFAULT true NOT NULL,
@@ -15522,6 +15506,7 @@ CREATE TABLE plan_limits (
ci_max_artifact_size_api_fuzzing integer DEFAULT 0 NOT NULL,
ci_pipeline_deployments integer DEFAULT 500 NOT NULL,
pull_mirror_interval_seconds integer DEFAULT 300 NOT NULL,
+ daily_invites integer DEFAULT 0 NOT NULL,
rubygems_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL
);
@@ -19096,8 +19081,6 @@ ALTER TABLE ONLY metrics_users_starred_dashboards ALTER COLUMN id SET DEFAULT ne
ALTER TABLE ONLY milestones ALTER COLUMN id SET DEFAULT nextval('milestones_id_seq'::regclass);
-ALTER TABLE ONLY namespace_onboarding_actions ALTER COLUMN id SET DEFAULT nextval('namespace_onboarding_actions_id_seq'::regclass);
-
ALTER TABLE ONLY namespace_statistics ALTER COLUMN id SET DEFAULT nextval('namespace_statistics_id_seq'::regclass);
ALTER TABLE ONLY namespaces ALTER COLUMN id SET DEFAULT nextval('namespaces_id_seq'::regclass);
@@ -20442,9 +20425,6 @@ ALTER TABLE ONLY namespace_aggregation_schedules
ALTER TABLE ONLY namespace_limits
ADD CONSTRAINT namespace_limits_pkey PRIMARY KEY (namespace_id);
-ALTER TABLE ONLY namespace_onboarding_actions
- ADD CONSTRAINT namespace_onboarding_actions_pkey PRIMARY KEY (id);
-
ALTER TABLE ONLY namespace_package_settings
ADD CONSTRAINT namespace_package_settings_pkey PRIMARY KEY (namespace_id);
@@ -22619,8 +22599,6 @@ CREATE INDEX index_mr_metrics_on_target_project_id_merged_at_time_to_merge ON me
CREATE UNIQUE INDEX index_namespace_aggregation_schedules_on_namespace_id ON namespace_aggregation_schedules USING btree (namespace_id);
-CREATE INDEX index_namespace_onboarding_actions_on_namespace_id ON namespace_onboarding_actions USING btree (namespace_id);
-
CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id);
CREATE UNIQUE INDEX index_namespace_statistics_on_namespace_id ON namespace_statistics USING btree (namespace_id);
@@ -25159,9 +25137,6 @@ ALTER TABLE ONLY merge_request_assignees
ALTER TABLE ONLY packages_dependency_links
ADD CONSTRAINT fk_rails_4437bf4070 FOREIGN KEY (dependency_id) REFERENCES packages_dependencies(id) ON DELETE CASCADE;
-ALTER TABLE ONLY namespace_onboarding_actions
- ADD CONSTRAINT fk_rails_4504f6875a FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY project_auto_devops
ADD CONSTRAINT fk_rails_45436b12b2 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 57af1166076..304dab7b010 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -96,6 +96,13 @@ Read more on the [Rack Attack initializer](../security/rack_attack.md) method of
- **Default rate limit** - Disabled
+### Member Invitations
+
+Limit the maximum daily member invitations allowed per group hierarchy.
+
+- GitLab.com: Free members may invite 20 members per day.
+- Self-managed: Invites are not limited.
+
## Gitaly concurrency limit
Clone traffic can put a large strain on your Gitaly service. To prevent such workloads from overwhelming your Gitaly server, you can set concurrency limits in Gitaly’s configuration file.
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index c88a6d0bff5..8880c26fe3c 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -27054,6 +27054,11 @@ type User {
avatarUrl: String
"""
+ Indicates if the user is a bot.
+ """
+ bot: Boolean!
+
+ """
User email. Deprecated in 13.7: Use public_email.
"""
email: String @deprecated(reason: "Use public_email. Deprecated in 13.7.")
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index d3a6ccc4198..4474f1700f5 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -77879,6 +77879,24 @@
"deprecationReason": null
},
{
+ "name": "bot",
+ "description": "Indicates if the user is a bot.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Boolean",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "email",
"description": "User email. Deprecated in 13.7: Use public_email.",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 1190408f864..b6312e4c9a9 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -4031,6 +4031,7 @@ Autogenerated return type of UpdateSnippet.
| `assignedMergeRequests` | MergeRequestConnection | Merge Requests assigned to the user. |
| `authoredMergeRequests` | MergeRequestConnection | Merge Requests authored by the user. |
| `avatarUrl` | String | URL of the user's avatar. |
+| `bot` | Boolean! | Indicates if the user is a bot. |
| `email` **{warning-solid}** | String | **Deprecated:** Use public_email. Deprecated in 13.7. |
| `groupCount` | Int | Group count for the user. Available only when feature flag `user_group_counts` is enabled. |
| `groupMemberships` | GroupMemberConnection | Group memberships of the user. |
diff --git a/doc/api/issues.md b/doc/api/issues.md
index ab8dc8f590d..c333967b36c 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -60,8 +60,8 @@ GET /issues?state=opened
| `due_date` | string | no | Return issues that have no due date, are overdue, or whose due date is this week, this month, or between two weeks ago and next month. Accepts: `0` (no due date), `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`. _(Introduced in [GitLab 13.3](https://gitlab.com/gitlab-org/gitlab/-/issues/233420))_ |
| `iids[]` | integer array | no | Return only the issues having the given `iid` |
| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` |
-| `iteration_id` **(STARTER)** | integer | no | Return issues assigned to the given iteration ID. `None` returns issues that do not belong to an iteration. `Any` returns issues that belong to an iteration. Mutually exclusive with `iteration_title`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.6)_ |
-| `iteration_title` **(STARTER)** | string | no | Return issues assigned to the iteration with the given title. Similar to `iteration_id` and mutually exclusive with `iteration_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.6)_ |
+| `iteration_id` **(PREMIUM)** | integer | no | Return issues assigned to the given iteration ID. `None` returns issues that do not belong to an iteration. `Any` returns issues that belong to an iteration. Mutually exclusive with `iteration_title`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in GitLab 13.6)_ |
+| `iteration_title` **(PREMIUM)** | string | no | Return issues assigned to the iteration with the given title. Similar to `iteration_id` and mutually exclusive with `iteration_id`. _([Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in GitLab 13.6)_ |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. |
| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. |
diff --git a/doc/development/fe_guide/style/html.md b/doc/development/fe_guide/style/html.md
index 7fedbc6ce0d..e53686de1a0 100644
--- a/doc/development/fe_guide/style/html.md
+++ b/doc/development/fe_guide/style/html.md
@@ -6,6 +6,38 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# HTML style guide
+## Semantic elements
+
+[Semantic elements](https://developer.mozilla.org/en-US/docs/Glossary/Semantics) are HTML tags that
+give semantic (rather than presentational) meaning to the data they contain. For example:
+
+- [`<article>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/article)
+- [`<nav>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/nav)
+- [`<strong>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/strong)
+
+Prefer using semantic tags, but only if the intention is truly accurate with the semantic meaning
+of the tag itself. View the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element)
+for a description on what each tag semantically means.
+
+```html
+<!-- bad - could use semantic tags instead of div's. -->
+<div class="...">
+ <p>
+ <!-- bad - this isn't what "strong" is meant for. -->
+ Simply visit your <strong>Settings</strong> to say hello to the world.
+ </p>
+ <div class="...">...</div>
+</div>
+
+<!-- good - prefer semantic classes used accurately -->
+<section class="...">
+ <p>
+ Simply visit your <span class="gl-font-weight-bold">Settings</span> to say hello to the world.
+ </p>
+ <footer class="...">...</footer>
+</section>
+```
+
## Buttons
### Button type
diff --git a/doc/development/usage_ping.md b/doc/development/usage_ping.md
index 4b081f59abd..752af29f594 100644
--- a/doc/development/usage_ping.md
+++ b/doc/development/usage_ping.md
@@ -495,18 +495,17 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
aggregation.
- `aggregation`: may be set to a `:daily` or `:weekly` key. Defines how counting data is stored in Redis.
Aggregation on a `daily` basis does not pull more fine grained data.
- - `feature_flag`: optional. For details, see our [GitLab internal Feature flags](feature_flags/) documentation. The feature flags are owned by the group adding the event tracking.
+ - `feature_flag`: optional `default_enabled: :yaml`. If no feature flag is set then the tracking is enabled. For details, see our [GitLab internal Feature flags](feature_flags/) documentation. The feature flags are owned by the group adding the event tracking.
Use one of the following methods to track events:
-1. Track event in controller using `RedisTracking` module with `track_redis_hll_event(*controller_actions, name:, feature:, feature_default_enabled: false)`.
+1. Track event in controller using `RedisTracking` module with `track_redis_hll_event(*controller_actions, name:, if: nil)`.
Arguments:
- `controller_actions`: controller actions we want to track.
- `name`: event name.
- - `feature`: feature name, all metrics we track should be under feature flag.
- - `feature_default_enabled`: feature flag is disabled by default, set to `true` for it to be enabled by default.
+ - `if`: optional custom conditions, using the same format as with Rails callbacks.
Example usage:
@@ -516,7 +515,7 @@ Use one of the following methods to track events:
include RedisTracking
skip_before_action :authenticate_user!, only: :show
- track_redis_hll_event :index, :show, name: 'g_compliance_example_feature_visitors', feature: :compliance_example_feature, feature_default_enabled: true
+ track_redis_hll_event :index, :show, name: 'g_compliance_example_feature_visitors'
def index
render html: 'index'
diff --git a/doc/user/discussions/img/confidential_comments_v13_9.png b/doc/user/discussions/img/confidential_comments_v13_9.png
new file mode 100644
index 00000000000..d3e13f37ae9
--- /dev/null
+++ b/doc/user/discussions/img/confidential_comments_v13_9.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 0f718cfdb8d..6268b525755 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -7,12 +7,12 @@ type: reference, howto
# Threads **(FREE)**
-The ability to contribute conversationally is offered throughout GitLab.
+You can use words to communicate with other users all over GitLab.
-You can leave a comment in the following places:
+For example, you can leave a comment in the following places:
- Issues
-- Epics **(ULTIMATE)**
+- Epics
- Merge requests
- Snippets
- Commits
@@ -281,6 +281,23 @@ edit existing comments. Non-team members are restricted from adding or editing c
Additionally, locked issues and merge requests can not be reopened.
+## Confidential Comments
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207473) in GitLab 13.9.
+> - It's [deployed behind a feature flag](../feature_flags.md), disabled by default.
+> - It's disabled on GitLab.com.
+> - It's not recommended for production use.
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to enable it. **(FREE SELF)**
+
+WARNING:
+This feature might not be available to you. Check the **version history** note above for details.
+
+When creating a comment, you can decide to make it visible only to the project members (users with Reporter and higher permissions).
+
+To create a confidential comment, select the **Make this comment confidential** checkbox before you submit it.
+
+![Confidential comments](img/confidential_comments_v13_9.png)
+
## Merge Request Reviews
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/4213) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.4.
@@ -418,25 +435,6 @@ the thread will be automatically resolved, and GitLab will create a new commit
and push the suggested change directly into the codebase in the merge request's
branch. [Developer permission](../permissions.md) is required to do so.
-### Enable or disable Custom commit messages for suggestions **(FREE SELF)**
-
-Custom commit messages for suggestions is under development but ready for production use. It is
-deployed behind a feature flag that is **enabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
-can opt to disable it.
-
-To disable custom commit messages for suggestions:
-
-```ruby
-Feature.disable(:suggestions_custom_commit)
-```
-
-To enable custom commit messages for suggestions:
-
-```ruby
-Feature.enable(:suggestions_custom_commit)
-```
-
### Multi-line Suggestions
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53310) in GitLab 11.10.
@@ -532,27 +530,6 @@ to your branch to address your reviewers' requests.
![A code change suggestion displayed, with the button to apply the batch of suggestions highlighted.](img/apply_batch_of_suggestions_v13_1.jpg "Apply a batch of suggestions")
-#### Enable or disable Batch Suggestions **(FREE SELF)**
-
-Batch Suggestions is
-deployed behind a feature flag that is **enabled by default**.
-[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
-can opt to disable it for your instance.
-
-To enable it:
-
-```ruby
-# Instance-wide
-Feature.enable(:batch_suggestions)
-```
-
-To disable it:
-
-```ruby
-# Instance-wide
-Feature.disable(:batch_suggestions)
-```
-
## Start a thread by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9
@@ -585,3 +562,62 @@ In the comment, click the **More Actions** menu and click **Assign to commenting
Click the button again to unassign the commenter.
![Assign to commenting user](img/quickly_assign_commenter_v13_1.png)
+
+## Enable or disable Confidential Comments **(FREE SELF)**
+
+Confidential Comments is under development and not ready for production use. It is
+deployed behind a feature flag that is **disabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
+can enable it.
+
+To enable it:
+
+```ruby
+Feature.enable(:confidential_notes)
+```
+
+To disable it:
+
+```ruby
+Feature.disable(:confidential_notes)
+```
+
+## Enable or disable Custom commit messages for suggestions **(FREE SELF)**
+
+Custom commit messages for suggestions is under development but ready for production use. It is
+deployed behind a feature flag that is **enabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
+can opt to disable it.
+
+To disable custom commit messages for suggestions:
+
+```ruby
+Feature.disable(:suggestions_custom_commit)
+```
+
+To enable custom commit messages for suggestions:
+
+```ruby
+Feature.enable(:suggestions_custom_commit)
+```
+
+## Enable or disable Batch Suggestions **(FREE SELF)**
+
+Batch Suggestions is
+deployed behind a feature flag that is **enabled by default**.
+[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
+can opt to disable it for your instance.
+
+To enable it:
+
+```ruby
+# Instance-wide
+Feature.enable(:batch_suggestions)
+```
+
+To disable it:
+
+```ruby
+# Instance-wide
+Feature.disable(:batch_suggestions)
+```
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 1109171f9e9..68a68ed65ad 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -183,6 +183,7 @@ The following table depicts the various user permission levels in a project.
| Delete pipelines | | | | | ✓ |
| Delete merge request | | | | | ✓ |
| Disable notification emails | | | | | ✓ |
+| Administer project compliance frameworks | | | | | ✓ |
| Force push to protected branches (*4*) | | | | | |
| Remove protected branches (*4*) | | | | | |
@@ -293,6 +294,7 @@ group.
| View Billing **(FREE SAAS)** | | | | | ✓ (4) |
| View Usage Quotas **(FREE SAAS)** | | | | | ✓ (4) |
| Filter members by 2FA status | | | | | ✓ |
+| Administer project compliance frameworks | | | | | ✓ |
1. Groups can be set to [allow either Owners or Owners and
Maintainers to create subgroups](group/subgroups/index.md#creating-a-subgroup)
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index d40c638105e..c307fd8d628 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -198,6 +198,8 @@ service account can be found at Google's documentation for
Prometheus OAuth Client secured with Google IAP.
1. (Optional) In **Google IAP Service Account JSON**, provide the contents of the
Service Account credentials file that is authorized to access the Prometheus resource.
+ The JSON key `token_credential_uri` is discarded to prevent
+ [Server-side Request Forgery (SSRF)](https://www.hackerone.com/blog-How-To-Server-Side-Request-Forgery-SSRF).
1. Click **Save changes**.
![Configure Prometheus Service](img/prometheus_manual_configuration_v13_2.png)
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index d8ecef61363..6f33a718191 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -46,17 +46,17 @@ Compliance framework labels do not affect your project settings.
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
-> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-custom-compliance-frameworks). **(PREMIUM ONLY)**
+> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-custom-compliance-frameworks). **(PREMIUM)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
-GitLab 13.8 introduces custom compliance frameworks at the group-level. A group owner can create a compliance framework label
+GitLab 13.9 introduces custom compliance frameworks at the group-level. A group owner can create a compliance framework label
and assign it to any number of projects within that group or sub-groups. When this feature is enabled, projects can only
be assigned compliance framework labels that already exist within that group.
-If existing [Compliance frameworks](#compliance-framework) are not sufficient, you can now create
-your own.
+If existing [Compliance frameworks](#compliance-framework) are not sufficient, project and group owners
+can now create their own.
New compliance framework labels can be created and updated using GraphQL.
@@ -320,7 +320,7 @@ Add the URL of a Jaeger server to allow your users to [easily access the Jaeger
[Add Storage credentials](../../../operations/incident_management/status_page.md#sync-incidents-to-the-status-page)
to enable the syncing of public Issues to a [deployed status page](../../../operations/incident_management/status_page.md#create-a-status-page-project).
-### Enable or disable custom compliance frameworks **(PREMIUM ONLY)**
+### Enable or disable custom compliance frameworks **(PREMIUM)**
Enabling or disabling custom compliance frameworks is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index f1f34622187..2d30754a36d 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -11,6 +11,8 @@ module API
optional :include_merged_yaml, type: Boolean, desc: 'Whether or not to include merged CI config yaml in the response'
end
post '/lint' do
+ unauthorized! unless Gitlab::CurrentSettings.signup_enabled? && current_user
+
result = Gitlab::Ci::YamlProcessor.new(params[:content], user: current_user).execute
status 200
@@ -55,7 +57,7 @@ module API
optional :dry_run, type: Boolean, default: false, desc: 'Run pipeline creation simulation, or only do static check.'
end
post ':id/ci/lint' do
- authorize! :download_code, user_project
+ authorize! :create_pipeline, user_project
result = Gitlab::Ci::Lint
.new(project: user_project, current_user: current_user)
diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb
index 00f42703731..0cdfd8f94b4 100644
--- a/lib/api/merge_request_approvals.rb
+++ b/lib/api/merge_request_approvals.rb
@@ -26,6 +26,8 @@ module API
# GET /projects/:id/merge_requests/:merge_request_iid/approvals
desc 'List approvals for merge request'
get 'approvals' do
+ not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
+
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present_approval(merge_request)
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index 0ffb38438eb..97a6c7075b3 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -23,6 +23,8 @@ module API
use :pagination
end
get ":id/merge_requests/:merge_request_iid/versions" do
+ not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
+
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present paginate(merge_request.merge_request_diffs.order_id_desc), with: Entities::MergeRequestDiff
@@ -39,6 +41,8 @@ module API
end
get ":id/merge_requests/:merge_request_iid/versions/:version_id" do
+ not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
+
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index cff0866c65e..5051c1a5529 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -248,6 +248,8 @@ module API
success Entities::MergeRequest
end
get ':id/merge_requests/:merge_request_iid' do
+ not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
+
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request,
@@ -264,7 +266,10 @@ module API
success Entities::UserBasic
end
get ':id/merge_requests/:merge_request_iid/participants' do
+ not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
+
merge_request = find_merge_request_with_access(params[:merge_request_iid])
+
participants = ::Kaminari.paginate_array(merge_request.participants)
present paginate(participants), with: Entities::UserBasic
@@ -274,6 +279,8 @@ module API
success Entities::Commit
end
get ':id/merge_requests/:merge_request_iid/commits' do
+ not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
+
merge_request = find_merge_request_with_access(params[:merge_request_iid])
commits =
@@ -355,6 +362,8 @@ module API
success Entities::MergeRequestChanges
end
get ':id/merge_requests/:merge_request_iid/changes' do
+ not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
+
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request,
@@ -370,6 +379,8 @@ module API
get ':id/merge_requests/:merge_request_iid/pipelines' do
pipelines = merge_request_pipelines_with_access
+ not_found!("Merge Request") unless can?(current_user, :read_merge_request, user_project)
+
present paginate(pipelines), with: Entities::Ci::PipelineBasic
end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
index 03850ba1c4e..afc1525cbe2 100644
--- a/lib/api/todos.rb
+++ b/lib/api/todos.rb
@@ -28,6 +28,11 @@ module API
end
post ":id/#{type}/:#{type_id_str}/todo" do
issuable = instance_exec(params[type_id_str], &finder)
+
+ unless can?(current_user, :read_merge_request, issuable.project)
+ not_found!(type.split("_").map(&:capitalize).join(" "))
+ end
+
todo = TodoService.new.mark_todo(issuable, current_user).first
if todo
diff --git a/lib/bulk_imports/groups/graphql/get_members_query.rb b/lib/bulk_imports/groups/graphql/get_members_query.rb
new file mode 100644
index 00000000000..1287abc85dc
--- /dev/null
+++ b/lib/bulk_imports/groups/graphql/get_members_query.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module Groups
+ module Graphql
+ module GetMembersQuery
+ extend self
+ def to_s
+ <<-'GRAPHQL'
+ query($full_path: ID!, $cursor: String) {
+ group(fullPath: $full_path) {
+ group_members: groupMembers(relations: DIRECT, first: 100, after: $cursor) {
+ page_info: pageInfo {
+ end_cursor: endCursor
+ has_next_page: hasNextPage
+ }
+ nodes {
+ created_at: createdAt
+ updated_at: updatedAt
+ expires_at: expiresAt
+ access_level: accessLevel {
+ integer_value: integerValue
+ }
+ user {
+ public_email: publicEmail
+ }
+ }
+ }
+ }
+ }
+ GRAPHQL
+ end
+
+ def variables(entity)
+ {
+ full_path: entity.source_full_path,
+ cursor: entity.next_page_for(:group_members)
+ }
+ end
+
+ def base_path
+ %w[data group group_members]
+ end
+
+ def data_path
+ base_path << 'nodes'
+ end
+
+ def page_info_path
+ base_path << 'page_info'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/groups/loaders/members_loader.rb b/lib/bulk_imports/groups/loaders/members_loader.rb
new file mode 100644
index 00000000000..ccf44b31aee
--- /dev/null
+++ b/lib/bulk_imports/groups/loaders/members_loader.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module Groups
+ module Loaders
+ class MembersLoader
+ def initialize(*); end
+
+ def load(context, data)
+ return unless data
+
+ context.group.members.create!(data)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/groups/pipelines/members_pipeline.rb b/lib/bulk_imports/groups/pipelines/members_pipeline.rb
new file mode 100644
index 00000000000..ddc2cb124db
--- /dev/null
+++ b/lib/bulk_imports/groups/pipelines/members_pipeline.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module Groups
+ module Pipelines
+ class MembersPipeline
+ include Pipeline
+
+ extractor BulkImports::Common::Extractors::GraphqlExtractor,
+ query: BulkImports::Groups::Graphql::GetMembersQuery
+
+ transformer Common::Transformers::ProhibitedAttributesTransformer
+ transformer BulkImports::Groups::Transformers::MemberAttributesTransformer
+
+ loader BulkImports::Groups::Loaders::MembersLoader
+
+ def after_run(context, extracted_data)
+ context.entity.update_tracker_for(
+ relation: :group_members,
+ has_next_page: extracted_data.has_next_page?,
+ next_page: extracted_data.next_page
+ )
+
+ if extracted_data.has_next_page?
+ run(context)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/groups/transformers/member_attributes_transformer.rb b/lib/bulk_imports/groups/transformers/member_attributes_transformer.rb
new file mode 100644
index 00000000000..622f5b60ffe
--- /dev/null
+++ b/lib/bulk_imports/groups/transformers/member_attributes_transformer.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+module BulkImports
+ module Groups
+ module Transformers
+ class MemberAttributesTransformer
+ def initialize(*); end
+
+ def transform(context, data)
+ data
+ .then { |data| add_user(data) }
+ .then { |data| add_access_level(data) }
+ .then { |data| add_author(data, context) }
+ end
+
+ private
+
+ def add_user(data)
+ user = find_user(data&.dig('user', 'public_email'))
+
+ return unless user
+
+ data
+ .except('user')
+ .merge('user_id' => user.id)
+ end
+
+ def find_user(email)
+ return unless email
+
+ User.find_by_any_email(email, confirmed: true)
+ end
+
+ def add_access_level(data)
+ access_level = data&.dig('access_level', 'integer_value')
+
+ return unless valid_access_level?(access_level)
+
+ data.merge('access_level' => access_level)
+ end
+
+ def valid_access_level?(access_level)
+ Gitlab::Access
+ .options_with_owner
+ .value?(access_level)
+ end
+
+ def add_author(data, context)
+ return unless data
+
+ data.merge('created_by_id' => context.current_user.id)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/bulk_imports/importers/group_importer.rb b/lib/bulk_imports/importers/group_importer.rb
index c01a4ec025d..4888734087f 100644
--- a/lib/bulk_imports/importers/group_importer.rb
+++ b/lib/bulk_imports/importers/group_importer.rb
@@ -23,6 +23,7 @@ module BulkImports
[
BulkImports::Groups::Pipelines::GroupPipeline,
BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline,
+ BulkImports::Groups::Pipelines::MembersPipeline,
BulkImports::Groups::Pipelines::LabelsPipeline
]
end
diff --git a/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb b/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb
index d7506eca242..079d631e22a 100644
--- a/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb
+++ b/lib/gitlab/auth/otp/strategies/forti_token_cloud.rb
@@ -61,8 +61,7 @@ module Gitlab
headers: {
'Content-Type': 'application/json'
}.merge(headers),
- body: body,
- verify: false # FTC API Docs specifically mentions to turn off SSL Verification while making requests.
+ body: body
)
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
index e68d9020a21..55c125e03d5 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
@@ -10,6 +10,10 @@ module Gitlab
include Chain::Helpers
def perform!
+ if project.pending_delete?
+ return error('Project is deleted!')
+ end
+
unless project.builds_enabled?
return error('Pipelines are disabled!')
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index fdf736f122d..a64bb08fe3a 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -13,7 +13,6 @@ module Gitlab
gon.asset_host = ActionController::Base.asset_host
gon.webpack_public_path = webpack_public_path
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
- gon.shortcuts_path = Gitlab::Routing.url_helpers.help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
if Gitlab.config.sentry.enabled
diff --git a/lib/gitlab/tree_summary.rb b/lib/gitlab/tree_summary.rb
index 9b67599668a..bc7b8bd2b94 100644
--- a/lib/gitlab/tree_summary.rb
+++ b/lib/gitlab/tree_summary.rb
@@ -40,21 +40,17 @@ module Gitlab
# - An Array of the unique ::Commit objects in the first value
def summarize
summary = contents
- .map { |content| build_entry(content) }
.tap { |summary| fill_last_commits!(summary) }
[summary, commits]
end
def fetch_logs
- cache_key = ['projects', project.id, 'logs', commit.id, path, offset]
- Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
- logs, _ = summarize
+ logs, _ = summarize
- new_offset = next_offset if more?
+ new_offset = next_offset if more?
- [logs.as_json, new_offset]
- end
+ [logs.as_json, new_offset]
end
# Does the tree contain more entries after the given offset + limit?
@@ -71,7 +67,7 @@ module Gitlab
private
def contents
- all_contents[offset, limit]
+ all_contents[offset, limit] || []
end
def commits
@@ -82,22 +78,17 @@ module Gitlab
project.repository
end
- def entry_path(entry)
- File.join(*[path, entry[:file_name]].compact).force_encoding(Encoding::ASCII_8BIT)
+ # Ensure the path is in "path/" format
+ def ensured_path
+ File.join(*[path, ""]) if path
end
- def build_entry(entry)
- { file_name: entry.name, type: entry.type }
+ def entry_path(entry)
+ File.join(*[path, entry[:file_name]].compact).force_encoding(Encoding::ASCII_8BIT)
end
def fill_last_commits!(entries)
- # Ensure the path is in "path/" format
- ensured_path =
- if path
- File.join(*[path, ""])
- end
-
- commits_hsh = repository.list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit, literal_pathspec: true)
+ commits_hsh = fetch_last_cached_commits_list
prerender_commit_full_titles!(commits_hsh.values)
entries.each do |entry|
@@ -112,6 +103,18 @@ module Gitlab
end
end
+ def fetch_last_cached_commits_list
+ cache_key = ['projects', project.id, 'last_commits_list', commit.id, ensured_path, offset, limit]
+
+ commits = Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
+ repository
+ .list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit, literal_pathspec: true)
+ .transform_values!(&:to_hash)
+ end
+
+ commits.transform_values! { |value| Commit.from_hash(value, project) }
+ end
+
def cache_commit(commit)
return unless commit.present?
@@ -123,12 +126,18 @@ module Gitlab
end
def all_contents
- strong_memoize(:all_contents) do
+ strong_memoize(:all_contents) { cached_contents }
+ end
+
+ def cached_contents
+ cache_key = ['projects', project.id, 'content', commit.id, path]
+
+ Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
[
*tree.trees,
*tree.blobs,
*tree.submodules
- ]
+ ].map { |entry| { file_name: entry.name, type: entry.type } }
end
end
diff --git a/lib/gitlab/usage/metrics/aggregates/aggregate.rb b/lib/gitlab/usage/metrics/aggregates/aggregate.rb
index d8667a0a58f..1fc40798320 100644
--- a/lib/gitlab/usage/metrics/aggregates/aggregate.rb
+++ b/lib/gitlab/usage/metrics/aggregates/aggregate.rb
@@ -148,7 +148,7 @@ module Gitlab
end
def load_yaml_from_path(path)
- YAML.safe_load(File.read(path))&.map(&:with_indifferent_access)
+ YAML.safe_load(File.read(path), aliases: true)&.map(&:with_indifferent_access)
end
end
end
diff --git a/lib/gitlab/usage_data_counters/hll_redis_counter.rb b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
index ed2ce2cecb0..68ae239debb 100644
--- a/lib/gitlab/usage_data_counters/hll_redis_counter.rb
+++ b/lib/gitlab/usage_data_counters/hll_redis_counter.rb
@@ -129,6 +129,8 @@ module Gitlab
event = event_for(event_name)
raise UnknownEvent, "Unknown event #{event_name}" unless event.present?
+ return unless feature_enabled?(event)
+
Gitlab::Redis::HLL.add(key: redis_key(event, time, context), value: values, expiry: expiry(event))
end
@@ -148,6 +150,12 @@ module Gitlab
redis_usage_data { Gitlab::Redis::HLL.count(keys: keys) }
end
+ def feature_enabled?(event)
+ return true if event[:feature_flag].blank?
+
+ Feature.enabled?(event[:feature_flag], default_enabled: :yaml)
+ end
+
# Allow to add totals for events that are in the same redis slot, category and have the same aggregation level
# and if there are more than 1 event
def eligible_for_totals?(events_names)
diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb
index a8935ec542e..79920968603 100644
--- a/lib/uploaded_file.rb
+++ b/lib/uploaded_file.rb
@@ -78,15 +78,9 @@ class UploadedFile
def sanitize_filename(name)
name = name.tr("\\", "/") # work-around for IE
name = ::File.basename(name)
-
- pre_sanitized_name = name
-
name = name.gsub(CarrierWave::SanitizedFile.sanitize_regexp, "_")
name = "_#{name}" if name =~ /\A\.+\z/
name = "unnamed" if name.empty?
-
- @filename_sanitized = name != pre_sanitized_name
-
name.mb_chars.to_s
end
@@ -98,10 +92,6 @@ class UploadedFile
@tempfile&.close
end
- def filename_sanitized?
- @filename_sanitized
- end
-
alias_method :local_path, :path
def method_missing(method_name, *args, &block) #:nodoc:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ac28aee644a..a8116eaf14e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1027,9 +1027,6 @@ msgstr ""
msgid "+ %{numberOfHiddenAssignees} more"
msgstr ""
-msgid "+ %{numberOfHiddenReviewers} more"
-msgstr ""
-
msgid "+%d more"
msgid_plural "+%d more"
msgstr[0] ""
@@ -1408,6 +1405,15 @@ msgstr ""
msgid "APIFuzzing|Choose a profile"
msgstr ""
+msgid "APIFuzzing|Code snippet for the API Fuzzing configuration"
+msgstr ""
+
+msgid "APIFuzzing|Copy code and open .gitlab-ci.yml file"
+msgstr ""
+
+msgid "APIFuzzing|Copy code only"
+msgstr ""
+
msgid "APIFuzzing|Customize common API fuzzing settings to suit your requirements. For details of more advanced configuration options, see the %{docsLinkStart}GitLab API Fuzzing documentation%{docsLinkEnd}."
msgstr ""
@@ -1456,6 +1462,9 @@ msgstr ""
msgid "APIFuzzing|Target URL"
msgstr ""
+msgid "APIFuzzing|The configuration could not be saved, please try again later."
+msgstr ""
+
msgid "APIFuzzing|There are two ways to perform scans."
msgstr ""
@@ -1914,6 +1923,9 @@ msgstr ""
msgid "AddContextCommits|Add/remove"
msgstr ""
+msgid "AddMember|Invite limit of %{daily_invites} per day exceeded"
+msgstr ""
+
msgid "AddMember|No users specified."
msgstr ""
@@ -7489,6 +7501,12 @@ msgstr ""
msgid "Compliance Dashboard"
msgstr ""
+msgid "Compliance framework"
+msgstr ""
+
+msgid "Compliance framework (optional)"
+msgstr ""
+
msgid "Compliance framework (optional)"
msgstr ""
@@ -7829,15 +7847,9 @@ msgstr ""
msgid "ContainerRegistry|Expiration policy will run in %{time}"
msgstr ""
-msgid "ContainerRegistry|Filter by name"
-msgstr ""
-
msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password."
msgstr ""
-msgid "ContainerRegistry|Image Repositories"
-msgstr ""
-
msgid "ContainerRegistry|Image repository deletion failed"
msgstr ""
@@ -8829,6 +8841,9 @@ msgstr ""
msgid "Customizable by an administrator."
msgstr ""
+msgid "Customizable by owners."
+msgstr ""
+
msgid "Customize colors"
msgstr ""
@@ -19948,6 +19963,9 @@ msgstr ""
msgid "No commits present here"
msgstr ""
+msgid "No compliance frameworks are in use."
+msgstr ""
+
msgid "No compliance frameworks are in use. Create one using the GraphQL API."
msgstr ""
@@ -20254,7 +20272,10 @@ msgstr ""
msgid "Notes|Collapse replies"
msgstr ""
-msgid "Notes|Private comments are accessible by internal staff only"
+msgid "Notes|Confidential comments are only visible to project members"
+msgstr ""
+
+msgid "Notes|Make this comment confidential"
msgstr ""
msgid "Notes|Show all activity"
@@ -20269,6 +20290,9 @@ msgstr ""
msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost"
msgstr ""
+msgid "Notes|This comment is confidential and only visible to project members"
+msgstr ""
+
msgid "Notes|You're only seeing %{boldStart}other activity%{boldEnd} in the feed. To add a comment, switch to one of the following options."
msgstr ""
@@ -25905,12 +25929,18 @@ msgstr ""
msgid "SecurityConfiguration|Available for on-demand DAST"
msgstr ""
+msgid "SecurityConfiguration|Available with %{linkStart}upgrade or free trial%{linkEnd}"
+msgstr ""
+
msgid "SecurityConfiguration|By default, all analyzers are applied in order to cover all languages across your project, and only run if the language is detected in the Merge Request."
msgstr ""
msgid "SecurityConfiguration|Configure"
msgstr ""
+msgid "SecurityConfiguration|Configure via Merge Request"
+msgstr ""
+
msgid "SecurityConfiguration|Could not retrieve configuration data. Please refresh the page, or try again later."
msgstr ""
@@ -25950,6 +25980,9 @@ msgstr ""
msgid "SecurityConfiguration|SAST Configuration"
msgstr ""
+msgid "SecurityConfiguration|SAST merge request creation mutation failed"
+msgstr ""
+
msgid "SecurityConfiguration|Security Control"
msgstr ""
@@ -26351,7 +26384,7 @@ msgstr ""
msgid "Select projects"
msgstr ""
-msgid "Select required regulatory standard"
+msgid "Select required regulatory standard."
msgstr ""
msgid "Select reviewer(s)"
diff --git a/spec/controllers/concerns/redis_tracking_spec.rb b/spec/controllers/concerns/redis_tracking_spec.rb
index ef59adf8c1d..53b49dd30a6 100644
--- a/spec/controllers/concerns/redis_tracking_spec.rb
+++ b/spec/controllers/concerns/redis_tracking_spec.rb
@@ -3,18 +3,13 @@
require "spec_helper"
RSpec.describe RedisTracking do
- let(:feature) { 'approval_rule' }
let(:user) { create(:user) }
- before do
- skip_feature_flags_yaml_validation
- end
-
controller(ApplicationController) do
include RedisTracking
skip_before_action :authenticate_user!, only: :show
- track_redis_hll_event :index, :show, name: 'g_compliance_approval_rules', feature: :approval_rule, feature_default_enabled: true,
+ track_redis_hll_event :index, :show, name: 'g_compliance_approval_rules',
if: [:custom_condition_one?, :custom_condition_two?]
def index
@@ -49,97 +44,75 @@ RSpec.describe RedisTracking do
expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
end
- context 'with feature disabled' do
- it 'does not track the event' do
- stub_feature_flags(feature => false)
-
- expect_no_tracking
-
- get :index
- end
- end
-
- context 'with feature enabled' do
+ context 'when user is logged in' do
before do
- stub_feature_flags(feature => true)
+ sign_in(user)
end
- context 'when user is logged in' do
- before do
- sign_in(user)
- end
-
- it 'tracks the event' do
- expect_tracking
-
- get :index
- end
-
- it 'passes default_enabled flag' do
- expect(controller).to receive(:metric_feature_enabled?).with(feature.to_sym, true)
+ it 'tracks the event' do
+ expect_tracking
- get :index
- end
+ get :index
+ end
- it 'tracks the event if DNT is not enabled' do
- request.headers['DNT'] = '0'
+ it 'tracks the event if DNT is not enabled' do
+ request.headers['DNT'] = '0'
- expect_tracking
+ expect_tracking
- get :index
- end
+ get :index
+ end
- it 'does not track the event if DNT is enabled' do
- request.headers['DNT'] = '1'
+ it 'does not track the event if DNT is enabled' do
+ request.headers['DNT'] = '1'
- expect_no_tracking
+ expect_no_tracking
- get :index
- end
+ get :index
+ end
- it 'does not track the event if the format is not HTML' do
- expect_no_tracking
+ it 'does not track the event if the format is not HTML' do
+ expect_no_tracking
- get :index, format: :json
- end
+ get :index, format: :json
+ end
- it 'does not track the event if a custom condition returns false' do
- expect(controller).to receive(:custom_condition_two?).and_return(false)
+ it 'does not track the event if a custom condition returns false' do
+ expect(controller).to receive(:custom_condition_two?).and_return(false)
- expect_no_tracking
+ expect_no_tracking
- get :index
- end
+ get :index
+ end
- it 'does not track the event for untracked actions' do
- expect_no_tracking
+ it 'does not track the event for untracked actions' do
+ expect_no_tracking
- get :new
- end
+ get :new
end
+ end
- context 'when user is not logged in and there is a visitor_id' do
- let(:visitor_id) { SecureRandom.uuid }
+ context 'when user is not logged in and there is a visitor_id' do
+ let(:visitor_id) { SecureRandom.uuid }
- before do
- routes.draw { get 'show' => 'anonymous#show' }
- end
+ before do
+ routes.draw { get 'show' => 'anonymous#show' }
+ end
- it 'tracks the event' do
- cookies[:visitor_id] = { value: visitor_id, expires: 24.months }
+ it 'tracks the event' do
+ cookies[:visitor_id] = { value: visitor_id, expires: 24.months }
- expect_tracking
+ expect_tracking
- get :show
- end
+ get :show
end
+ end
- context 'when user is not logged in and there is no visitor_id' do
- it 'does not track the event' do
- expect_no_tracking
+ context 'when user is not logged in and there is no visitor_id' do
+ it 'does not track the event' do
+ expect_no_tracking
- get :index
- end
+ get :index
end
end
end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 16be7394174..68551ce4858 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -424,7 +424,7 @@ RSpec.describe Projects::BlobController do
end
end
- it_behaves_like 'tracking unique hll events', :track_editor_edit_actions do
+ it_behaves_like 'tracking unique hll events' do
subject(:request) { put :update, params: default_params }
let(:target_id) { 'g_edit_by_sfe' }
@@ -540,7 +540,7 @@ RSpec.describe Projects::BlobController do
sign_in(user)
end
- it_behaves_like 'tracking unique hll events', :track_editor_edit_actions do
+ it_behaves_like 'tracking unique hll events' do
subject(:request) { post :create, params: default_params }
let(:target_id) { 'g_edit_by_sfe' }
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index bfa83f07503..edebaf294c4 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -315,7 +315,7 @@ RSpec.describe Projects::NotesController do
let(:note_text) { 'some note' }
let(:request_params) do
{
- note: { note: note_text, noteable_id: merge_request.id, noteable_type: 'MergeRequest' },
+ note: { note: note_text, noteable_id: merge_request.id, noteable_type: 'MergeRequest' }.merge(extra_note_params),
namespace_id: project.namespace,
project_id: project,
merge_request_diff_head_sha: 'sha',
@@ -325,6 +325,7 @@ RSpec.describe Projects::NotesController do
end
let(:extra_request_params) { {} }
+ let(:extra_note_params) { {} }
let(:project_visibility) { Gitlab::VisibilityLevel::PUBLIC }
let(:merge_requests_access_level) { ProjectFeature::ENABLED }
@@ -423,6 +424,41 @@ RSpec.describe Projects::NotesController do
end
end
+ context 'when creating a confidential note' do
+ let(:extra_request_params) { { format: :json } }
+
+ context 'when `confidential` parameter is not provided' do
+ it 'sets `confidential` to `false` in JSON response' do
+ create!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['confidential']).to be false
+ end
+ end
+
+ context 'when `confidential` parameter is `false`' do
+ let(:extra_note_params) { { confidential: false } }
+
+ it 'sets `confidential` to `false` in JSON response' do
+ create!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['confidential']).to be false
+ end
+ end
+
+ context 'when `confidential` parameter is `true`' do
+ let(:extra_note_params) { { confidential: true } }
+
+ it 'sets `confidential` to `true` in JSON response' do
+ create!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['confidential']).to be true
+ end
+ end
+ end
+
context 'when creating a note with quick actions' do
context 'with commands that return changes' do
let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" }
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index d10351feb9e..b625ce35d61 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -56,18 +56,6 @@ RSpec.describe Projects::RefsController do
expect(response).to be_successful
expect(json_response).to be_kind_of(Array)
end
-
- it 'caches tree summary data', :use_clean_rails_memory_store_caching do
- expect_next_instance_of(::Gitlab::TreeSummary) do |instance|
- expect(instance).to receive_messages(summarize: ['logs'], next_offset: 50, more?: true)
- end
-
- xhr_get(:json, offset: 25)
-
- cache_key = "projects/#{project.id}/logs/#{project.commit.id}/#{path}/25"
- expect(Rails.cache.fetch(cache_key)).to eq(['logs', 50])
- expect(response.headers['More-Logs-Offset']).to eq("50")
- end
end
end
end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index c531c699e98..95cea10f0d0 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -183,7 +183,7 @@ RSpec.describe SearchController do
allow(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
end
- it_behaves_like 'tracking unique hll events', :search_track_unique_users do
+ it_behaves_like 'tracking unique hll events' do
subject(:request) { get :show, params: { scope: 'projects', search: 'term' } }
let(:target_id) { 'i_search_total' }
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 51cecb348c8..50d6ac8f23d 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe SnippetsController do
expect(response).to have_gitlab_http_status(:ok)
end
- it_behaves_like 'tracking unique hll events', :usage_data_i_snippets_show do
+ it_behaves_like 'tracking unique hll events' do
subject(:request) { get :show, params: { id: public_snippet.to_param } }
let(:target_id) { 'i_snippets_show' }
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 83a866b0e62..e8e0362fc62 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -28,6 +28,7 @@ FactoryBot.define do
forking_access_level { ProjectFeature::ENABLED }
merge_requests_access_level { ProjectFeature::ENABLED }
repository_access_level { ProjectFeature::ENABLED }
+ analytics_access_level { ProjectFeature::ENABLED }
pages_access_level do
visibility_level == Gitlab::VisibilityLevel::PUBLIC ? ProjectFeature::ENABLED : ProjectFeature::PRIVATE
end
@@ -63,7 +64,8 @@ FactoryBot.define do
repository_access_level: evaluator.repository_access_level,
pages_access_level: evaluator.pages_access_level,
metrics_dashboard_access_level: evaluator.metrics_dashboard_access_level,
- operations_access_level: evaluator.operations_access_level
+ operations_access_level: evaluator.operations_access_level,
+ analytics_access_level: evaluator.analytics_access_level
}
project.build_project_feature(hash)
@@ -335,6 +337,9 @@ FactoryBot.define do
trait(:operations_enabled) { operations_access_level { ProjectFeature::ENABLED } }
trait(:operations_disabled) { operations_access_level { ProjectFeature::DISABLED } }
trait(:operations_private) { operations_access_level { ProjectFeature::PRIVATE } }
+ trait(:analytics_enabled) { analytics_access_level { ProjectFeature::ENABLED } }
+ trait(:analytics_disabled) { analytics_access_level { ProjectFeature::DISABLED } }
+ trait(:analytics_private) { analytics_access_level { ProjectFeature::PRIVATE } }
trait :auto_devops do
association :auto_devops, factory: :project_auto_devops
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 1f8397e45f7..90647305281 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'Help Pages' do
it 'opens shortcuts help dialog' do
find('.js-trigger-shortcut').click
- expect(page).to have_selector('#modal-shortcuts')
+ expect(page).to have_selector('[data-testid="modal-shortcuts"]')
end
end
end
diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb
index 8fa5f741a95..13ae035e8ef 100644
--- a/spec/features/projects/user_uses_shortcuts_spec.rb
+++ b/spec/features/projects/user_uses_shortcuts_spec.rb
@@ -27,14 +27,13 @@ RSpec.describe 'User uses shortcuts', :js do
open_modal_shortcut_keys
- # modal-shortcuts still in the DOM, but hidden
- expect(find('#modal-shortcuts', visible: false)).not_to be_visible
+ expect(page).not_to have_selector('[data-testid="modal-shortcuts"]')
page.refresh
open_modal_shortcut_keys
# after reload, shortcuts modal doesn't exist at all until we add it
- expect(page).not_to have_selector('#modal-shortcuts')
+ expect(page).not_to have_selector('[data-testid="modal-shortcuts"]')
end
it 're-enables shortcuts' do
@@ -47,7 +46,7 @@ RSpec.describe 'User uses shortcuts', :js do
close_modal
open_modal_shortcut_keys
- expect(find('#modal-shortcuts')).to be_visible
+ expect(find('[data-testid="modal-shortcuts"]')).to be_visible
end
def open_modal_shortcut_keys
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 8394d1d0ab2..2307510e119 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -1,7 +1,8 @@
import { nextTick } from 'vue';
import { mount, shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
import Autosize from 'autosize';
+import MockAdapter from 'axios-mock-adapter';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
@@ -21,11 +22,25 @@ describe('issue_comment_form component', () => {
let wrapper;
let axiosMock;
- const findCloseReopenButton = () => wrapper.find('[data-testid="close-reopen-button"]');
+ const findCloseReopenButton = () => wrapper.findByTestId('close-reopen-button');
+ const findCommentButton = () => wrapper.findByTestId('comment-button');
+ const findTextArea = () => wrapper.findByTestId('comment-field');
+ const findConfidentialNoteCheckbox = () => wrapper.findByTestId('confidential-note-checkbox');
+
+ const createNotableDataMock = (data = {}) => {
+ return {
+ ...noteableDataMock,
+ ...data,
+ };
+ };
- const findCommentButton = () => wrapper.find('[data-testid="comment-button"]');
+ const notableDataMockCanUpdateIssuable = createNotableDataMock({
+ current_user: { can_update: true, can_create_note: true },
+ });
- const findTextArea = () => wrapper.find('[data-testid="comment-field"]');
+ const notableDataMockCannotUpdateIssuable = createNotableDataMock({
+ current_user: { can_update: false, can_create_note: true },
+ });
const mountComponent = ({
initialData = {},
@@ -33,23 +48,29 @@ describe('issue_comment_form component', () => {
noteableData = noteableDataMock,
notesData = notesDataMock,
userData = userDataMock,
+ features = {},
mountFunction = shallowMount,
} = {}) => {
store.dispatch('setNoteableData', noteableData);
store.dispatch('setNotesData', notesData);
store.dispatch('setUserData', userData);
- wrapper = mountFunction(CommentForm, {
- propsData: {
- noteableType,
- },
- data() {
- return {
- ...initialData,
- };
- },
- store,
- });
+ wrapper = extendedWrapper(
+ mountFunction(CommentForm, {
+ propsData: {
+ noteableType,
+ },
+ data() {
+ return {
+ ...initialData,
+ };
+ },
+ store,
+ provide: {
+ glFeatures: features,
+ },
+ }),
+ );
};
beforeEach(() => {
@@ -359,6 +380,83 @@ describe('issue_comment_form component', () => {
});
});
});
+
+ describe('confidential notes checkbox', () => {
+ describe('when confidentialNotes feature flag is `false`', () => {
+ const features = { confidentialNotes: false };
+
+ it('should not render checkbox', () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: 'confidential note' },
+ noteableData: { ...notableDataMockCanUpdateIssuable },
+ features,
+ });
+
+ const checkbox = findConfidentialNoteCheckbox();
+ expect(checkbox.exists()).toBe(false);
+ });
+ });
+
+ describe('when confidentialNotes feature flag is `true`', () => {
+ const features = { confidentialNotes: true };
+
+ it('should render checkbox as unchecked by default', () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: 'confidential note' },
+ noteableData: { ...notableDataMockCanUpdateIssuable },
+ features,
+ });
+
+ const checkbox = findConfidentialNoteCheckbox();
+ expect(checkbox.exists()).toBe(true);
+ expect(checkbox.element.checked).toBe(false);
+ });
+
+ describe.each`
+ shouldCheckboxBeChecked
+ ${true}
+ ${false}
+ `('when checkbox value is `$shouldCheckboxBeChecked`', ({ shouldCheckboxBeChecked }) => {
+ it(`sets \`confidential\` to \`${shouldCheckboxBeChecked}\``, async () => {
+ mountComponent({
+ mountFunction: mount,
+ initialData: { note: 'confidential note' },
+ noteableData: { ...notableDataMockCanUpdateIssuable },
+ features,
+ });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue({});
+
+ const checkbox = findConfidentialNoteCheckbox();
+
+ // check checkbox
+ checkbox.element.checked = shouldCheckboxBeChecked;
+ checkbox.trigger('change');
+ await wrapper.vm.$nextTick();
+
+ // submit comment
+ wrapper.findByTestId('comment-button').trigger('click');
+
+ const [providedData] = wrapper.vm.saveNote.mock.calls[0];
+ expect(providedData.data.note.confidential).toBe(shouldCheckboxBeChecked);
+ });
+ });
+
+ describe('when user cannot update issuable', () => {
+ it('should not render checkbox', () => {
+ mountComponent({
+ mountFunction: mount,
+ noteableData: { ...notableDataMockCannotUpdateIssuable },
+ features,
+ });
+
+ expect(findConfidentialNoteCheckbox().exists()).toBe(false);
+ });
+ });
+ });
+ });
});
describe('user is not logged in', () => {
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
index 9c49ff0c9e5..5fbe58e35e2 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui';
+import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
@@ -13,12 +13,12 @@ import RegistryHeader from '~/registry/explorer/components/list_page/registry_he
import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
- IMAGE_REPOSITORY_LIST_LABEL,
- SEARCH_PLACEHOLDER_TEXT,
+ SORT_FIELDS,
} from '~/registry/explorer/constants';
import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
@@ -55,8 +55,7 @@ describe('List Page', () => {
const findDeleteAlert = () => wrapper.find(GlAlert);
const findImageList = () => wrapper.find(ImageList);
- const findListHeader = () => wrapper.find('[data-testid="listHeader"]');
- const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
+ const findRegistrySearch = () => wrapper.find(RegistrySearch);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
const findDeleteImage = () => wrapper.find(DeleteImage);
@@ -229,14 +228,6 @@ describe('List Page', () => {
expect(findCliCommands().exists()).toBe(false);
});
-
- it('list header is not visible', async () => {
- mountComponent({ resolver, config });
-
- await waitForApolloRequestRender();
-
- expect(findListHeader().exists()).toBe(false);
- });
});
});
@@ -258,16 +249,6 @@ describe('List Page', () => {
expect(findImageList().exists()).toBe(true);
});
- it('list header is visible', async () => {
- mountComponent();
-
- await waitForApolloRequestRender();
-
- const header = findListHeader();
- expect(header.exists()).toBe(true);
- expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL);
- });
-
describe('additional metadata', () => {
it('is called on component load', async () => {
const detailsResolver = jest
@@ -360,10 +341,15 @@ describe('List Page', () => {
});
});
- describe('search', () => {
+ describe('search and sorting', () => {
const doSearch = async () => {
await waitForApolloRequestRender();
- findSearchBox().vm.$emit('submit', 'centos6');
+ findRegistrySearch().vm.$emit('filter:changed', [
+ { type: 'filtered-search-term', value: { data: 'centos6' } },
+ ]);
+
+ findRegistrySearch().vm.$emit('filter:submit');
+
await wrapper.vm.$nextTick();
};
@@ -372,9 +358,26 @@ describe('List Page', () => {
await waitForApolloRequestRender();
- const searchBox = findSearchBox();
- expect(searchBox.exists()).toBe(true);
- expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT);
+ const registrySearch = findRegistrySearch();
+ expect(registrySearch.exists()).toBe(true);
+ expect(registrySearch.props()).toMatchObject({
+ filter: [],
+ sorting: { orderBy: 'UPDATED', sort: 'desc' },
+ sortableFields: SORT_FIELDS,
+ tokens: [],
+ });
+ });
+
+ it('performs sorting', async () => {
+ const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
+ findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' });
+ await wrapper.vm.$nextTick();
+
+ expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
});
it('performs a search', async () => {
diff --git a/spec/frontend/security_configuration/app_spec.js b/spec/frontend/security_configuration/app_spec.js
new file mode 100644
index 00000000000..11d481fb210
--- /dev/null
+++ b/spec/frontend/security_configuration/app_spec.js
@@ -0,0 +1,27 @@
+import { shallowMount } from '@vue/test-utils';
+import App from '~/security_configuration/components/app.vue';
+import ConfigurationTable from '~/security_configuration/components/configuration_table.vue';
+
+describe('App Component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(App, {});
+ };
+ const findConfigurationTable = () => wrapper.findComponent(ConfigurationTable);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correct primary & Secondary Heading', () => {
+ createComponent();
+ expect(wrapper.text()).toContain('Security Configuration');
+ expect(wrapper.text()).toContain('Testing & Compliance');
+ });
+
+ it('renders ConfigurationTable Component', () => {
+ createComponent();
+ expect(findConfigurationTable().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/security_configuration/configuration_table_spec.js b/spec/frontend/security_configuration/configuration_table_spec.js
new file mode 100644
index 00000000000..beeca1b7169
--- /dev/null
+++ b/spec/frontend/security_configuration/configuration_table_spec.js
@@ -0,0 +1,48 @@
+import { mount } from '@vue/test-utils';
+import ConfigurationTable from '~/security_configuration/components/configuration_table.vue';
+import { features, UPGRADE_CTA } from '~/security_configuration/components/features_constants';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+} from '~/vue_shared/security_reports/constants';
+
+describe('Configuration Table Component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = extendedWrapper(mount(ConfigurationTable, {}));
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each(features)('should match strings', (feature) => {
+ expect(wrapper.text()).toContain(feature.name);
+ expect(wrapper.text()).toContain(feature.description);
+
+ if (feature.type === REPORT_TYPE_SAST) {
+ expect(wrapper.findByTestId(feature.type).text()).toBe('Configure via Merge Request');
+ } else if (
+ [
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_LICENSE_COMPLIANCE,
+ ].includes(feature.type)
+ ) {
+ expect(wrapper.findByTestId(feature.type).text()).toMatchInterpolatedText(UPGRADE_CTA);
+ }
+ });
+});
diff --git a/spec/frontend/security_configuration/manage_sast_spec.js b/spec/frontend/security_configuration/manage_sast_spec.js
new file mode 100644
index 00000000000..6d3cfeb9ed1
--- /dev/null
+++ b/spec/frontend/security_configuration/manage_sast_spec.js
@@ -0,0 +1,136 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
+import ManageSast from '~/security_configuration/components/manage_sast.vue';
+import { redirectTo } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+}));
+
+Vue.use(VueApollo);
+
+describe('Manage Sast Component', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.findComponent(GlButton);
+ const successHandler = async () => {
+ return {
+ data: {
+ configureSast: {
+ successPath: 'testSuccessPath',
+ errors: [],
+ __typename: 'ConfigureSastPayload',
+ },
+ },
+ };
+ };
+
+ const noSuccessPathHandler = async () => {
+ return {
+ data: {
+ configureSast: {
+ successPath: '',
+ errors: [],
+ __typename: 'ConfigureSastPayload',
+ },
+ },
+ };
+ };
+
+ const errorHandler = async () => {
+ return {
+ data: {
+ configureSast: {
+ successPath: 'testSuccessPath',
+ errors: ['foo'],
+ __typename: 'ConfigureSastPayload',
+ },
+ },
+ };
+ };
+
+ const pendingHandler = () => new Promise(() => {});
+
+ function createMockApolloProvider(handler) {
+ const requestHandlers = [[configureSastMutation, handler]];
+
+ return createMockApollo(requestHandlers);
+ }
+
+ function createComponent(options = {}) {
+ const { mockApollo } = options;
+ wrapper = extendedWrapper(
+ mount(ManageSast, {
+ apolloProvider: mockApollo,
+ }),
+ );
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render Button with correct text', () => {
+ createComponent();
+ expect(findButton().text()).toContain('Configure via Merge Request');
+ });
+
+ describe('given a successful response', () => {
+ beforeEach(() => {
+ const mockApollo = createMockApolloProvider(successHandler);
+ createComponent({ mockApollo });
+ });
+
+ it('should call redirect helper with correct value', async () => {
+ await wrapper.trigger('click');
+ await waitForPromises();
+ expect(redirectTo).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith('testSuccessPath');
+ // This is done for UX reasons. If the loading prop is set to false
+ // on success, then there's a period where the button is clickable
+ // again. Instead, we want the button to display a loading indicator
+ // for the remainder of the lifetime of the page (i.e., until the
+ // browser can start painting the new page it's been redirected to).
+ expect(findButton().props().loading).toBe(true);
+ });
+ });
+
+ describe('given a pending response', () => {
+ beforeEach(() => {
+ const mockApollo = createMockApolloProvider(pendingHandler);
+ createComponent({ mockApollo });
+ });
+
+ it('renders spinner correctly', async () => {
+ expect(findButton().props('loading')).toBe(false);
+ await wrapper.trigger('click');
+ await waitForPromises();
+ expect(findButton().props('loading')).toBe(true);
+ });
+ });
+
+ describe.each`
+ handler | message
+ ${noSuccessPathHandler} | ${'SAST merge request creation mutation failed'}
+ ${errorHandler} | ${'foo'}
+ `('given an error response', ({ handler, message }) => {
+ beforeEach(() => {
+ const mockApollo = createMockApolloProvider(handler);
+ createComponent({ mockApollo });
+ });
+
+ it('should catch and emit error', async () => {
+ await wrapper.trigger('click');
+ await waitForPromises();
+ expect(wrapper.emitted('error')).toEqual([[message]]);
+ expect(findButton().props('loading')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/security_configuration/upgrade_spec.js b/spec/frontend/security_configuration/upgrade_spec.js
new file mode 100644
index 00000000000..53434d32f07
--- /dev/null
+++ b/spec/frontend/security_configuration/upgrade_spec.js
@@ -0,0 +1,29 @@
+import { mount } from '@vue/test-utils';
+import Upgrade from '~/security_configuration/components/upgrade.vue';
+import { UPGRADE_CTA } from '~/security_configuration/components/features_constants';
+
+let wrapper;
+const createComponent = () => {
+ wrapper = mount(Upgrade, {});
+};
+
+beforeEach(() => {
+ createComponent();
+});
+
+afterEach(() => {
+ wrapper.destroy();
+});
+
+describe('Upgrade component', () => {
+ it('renders correct text in link', () => {
+ expect(wrapper.text()).toMatchInterpolatedText(UPGRADE_CTA);
+ });
+
+ it('renders link with correct attributes', () => {
+ expect(wrapper.find('a').attributes()).toMatchObject({
+ href: 'https://about.gitlab.com/pricing/',
+ target: '_blank',
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
new file mode 100644
index 00000000000..41180fcc4c2
--- /dev/null
+++ b/spec/frontend/sidebar/components/reviewers/uncollapsed_reviewer_list_spec.js
@@ -0,0 +1,91 @@
+import { shallowMount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
+import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
+import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
+import userDataMock from '../../user_data_mock';
+
+describe('UncollapsedReviewerList component', () => {
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ users: [],
+ rootPath: TEST_HOST,
+ ...props,
+ };
+
+ wrapper = shallowMount(UncollapsedReviewerList, {
+ propsData,
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('single reviewer', () => {
+ beforeEach(() => {
+ const user = userDataMock();
+
+ createComponent({
+ users: [user],
+ });
+ });
+
+ it('only has one user', () => {
+ expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(1);
+ });
+
+ it('shows one user with avatar, username and author name', () => {
+ expect(wrapper.text()).toContain(`@root`);
+ });
+
+ it('renders re-request loading icon', async () => {
+ await wrapper.setData({ loadingStates: { 1: 'loading' } });
+
+ expect(wrapper.find('[data-testid="re-request-button"]').props('loading')).toBe(true);
+ });
+
+ it('renders re-request success icon', async () => {
+ await wrapper.setData({ loadingStates: { 1: 'success' } });
+
+ expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
+ });
+ });
+
+ describe('multiple reviewers', () => {
+ beforeEach(() => {
+ const user = userDataMock();
+
+ createComponent({
+ users: [user, { ...user, id: 2, username: 'hello-world' }],
+ });
+ });
+
+ it('only has one user', () => {
+ expect(wrapper.findAll(ReviewerAvatarLink).length).toBe(2);
+ });
+
+ it('shows one user with avatar, username and author name', () => {
+ expect(wrapper.text()).toContain(`@root`);
+ expect(wrapper.text()).toContain(`@hello-world`);
+ });
+
+ it('renders re-request loading icon', async () => {
+ await wrapper.setData({ loadingStates: { 2: 'loading' } });
+
+ expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(2);
+ expect(wrapper.findAll('[data-testid="re-request-button"]').at(1).props('loading')).toBe(
+ true,
+ );
+ });
+
+ it('renders re-request success icon', async () => {
+ await wrapper.setData({ loadingStates: { 2: 'success' } });
+
+ expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(1);
+ expect(wrapper.findAll('[data-testid="re-request-success"]').length).toBe(1);
+ expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/user_data_mock.js b/spec/frontend/sidebar/user_data_mock.js
index df90a65f6f9..41d0331f34a 100644
--- a/spec/frontend/sidebar/user_data_mock.js
+++ b/spec/frontend/sidebar/user_data_mock.js
@@ -8,4 +8,6 @@ export default () => ({
username: 'root',
web_url: `${TEST_HOST}/root`,
can_merge: true,
+ can_update_merge_request: true,
+ reviewed: true,
});
diff --git a/spec/graphql/mutations/design_management/upload_spec.rb b/spec/graphql/mutations/design_management/upload_spec.rb
index 2fdf62c35a2..326d88cea80 100644
--- a/spec/graphql/mutations/design_management/upload_spec.rb
+++ b/spec/graphql/mutations/design_management/upload_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe Mutations::DesignManagement::Upload do
include DesignManagementTestHelpers
include ConcurrentHelpers
- using FixtureFileRefinements
let(:issue) { create(:issue) }
let(:user) { issue.author }
@@ -19,12 +18,8 @@ RSpec.describe Mutations::DesignManagement::Upload do
mutation.resolve(project_path: project_path, iid: iid, files: files_to_upload)
end
- def uploaded_file(filename)
- fixture_file_upload(expand_fixture_path(filename))
- end
-
describe "#resolve" do
- let(:files) { [uploaded_file('dk.png').to_gitlab_uploaded_file] }
+ let(:files) { [fixture_file_upload('spec/fixtures/dk.png')] }
subject(:resolve) do
mutation.resolve(project_path: project.full_path, iid: issue.iid, files: files)
@@ -54,7 +49,7 @@ RSpec.describe Mutations::DesignManagement::Upload do
['dk.png', 'rails_sample.jpg', 'banana_sample.gif']
.cycle
.take(Concurrent.processor_count * 2)
- .map { |f| uploaded_file(f).uniquely_named.to_gitlab_uploaded_file }
+ .map { |f| RenameableUpload.unique_file(f) }
end
def creates_designs
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
index 0eff33bb25b..5b3662383d8 100644
--- a/spec/graphql/types/user_type_spec.rb
+++ b/spec/graphql/types/user_type_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe GitlabSchema.types['User'] do
it 'has the expected fields' do
expected_fields = %w[
id
+ bot
user_permissions
snippets
name
diff --git a/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb
new file mode 100644
index 00000000000..4bbd60d4970
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/graphql/get_members_query_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Graphql::GetMembersQuery do
+ it 'has a valid query' do
+ entity = create(:bulk_import_entity)
+
+ query = GraphQL::Query.new(
+ GitlabSchema,
+ described_class.to_s,
+ variables: described_class.variables(entity)
+ )
+ result = GitlabSchema.static_validator.validate(query)
+
+ expect(result[:errors]).to be_empty
+ end
+
+ describe '#data_path' do
+ it 'returns data path' do
+ expected = %w[data group group_members nodes]
+
+ expect(described_class.data_path).to eq(expected)
+ end
+ end
+
+ describe '#page_info_path' do
+ it 'returns pagination information path' do
+ expected = %w[data group group_members page_info]
+
+ expect(described_class.page_info_path).to eq(expected)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb b/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb
new file mode 100644
index 00000000000..d552578e7be
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/loaders/members_loader_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Loaders::MembersLoader do
+ describe '#load' do
+ let_it_be(:user_importer) { create(:user) }
+ let_it_be(:user_member) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user_importer) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ let_it_be(:data) do
+ {
+ 'user_id' => user_member.id,
+ 'created_by_id' => user_importer.id,
+ 'access_level' => 30,
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil
+ }
+ end
+
+ it 'does nothing when there is no data' do
+ expect { subject.load(context, nil) }.not_to change(GroupMember, :count)
+ end
+
+ it 'creates the member' do
+ expect { subject.load(context, data) }.to change(GroupMember, :count).by(1)
+
+ member = group.members.last
+
+ expect(member.user).to eq(user_member)
+ expect(member.created_by).to eq(user_importer)
+ expect(member.access_level).to eq(30)
+ expect(member.created_at).to eq('2020-01-01T00:00:00Z')
+ expect(member.updated_at).to eq('2020-01-01T00:00:00Z')
+ expect(member.expires_at).to eq(nil)
+ end
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb
new file mode 100644
index 00000000000..52208e2b852
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/pipelines/members_pipeline_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Pipelines::MembersPipeline do
+ let_it_be(:member_user1) { create(:user, email: 'email1@email.com') }
+ let_it_be(:member_user2) { create(:user, email: 'email2@email.com') }
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:cursor) { 'cursor' }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ describe '#run' do
+ it 'maps existing users to the imported group' do
+ first_page = member_data(email: member_user1.email, has_next_page: true, cursor: cursor)
+ last_page = member_data(email: member_user2.email, has_next_page: false)
+
+ allow_next_instance_of(BulkImports::Common::Extractors::GraphqlExtractor) do |extractor|
+ allow(extractor)
+ .to receive(:extract)
+ .and_return(first_page, last_page)
+ end
+
+ expect { subject.run(context) }.to change(GroupMember, :count).by(2)
+
+ members = group.members.map { |m| m.slice(:user_id, :access_level) }
+
+ expect(members).to contain_exactly(
+ { user_id: member_user1.id, access_level: 30 },
+ { user_id: member_user2.id, access_level: 30 }
+ )
+ end
+ end
+
+ describe 'pipeline parts' do
+ it { expect(described_class).to include_module(BulkImports::Pipeline) }
+ it { expect(described_class).to include_module(BulkImports::Pipeline::Runner) }
+
+ it 'has extractors' do
+ expect(described_class.get_extractor)
+ .to eq(
+ klass: BulkImports::Common::Extractors::GraphqlExtractor,
+ options: {
+ query: BulkImports::Groups::Graphql::GetMembersQuery
+ }
+ )
+ end
+
+ it 'has transformers' do
+ expect(described_class.transformers)
+ .to contain_exactly(
+ { klass: BulkImports::Common::Transformers::ProhibitedAttributesTransformer, options: nil },
+ { klass: BulkImports::Groups::Transformers::MemberAttributesTransformer, options: nil }
+ )
+ end
+
+ it 'has loaders' do
+ expect(described_class.get_loader).to eq(klass: BulkImports::Groups::Loaders::MembersLoader, options: nil)
+ end
+ end
+
+ def member_data(email:, has_next_page:, cursor: nil)
+ data = {
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil,
+ 'access_level' => {
+ 'integer_value' => 30
+ },
+ 'user' => {
+ 'public_email' => email
+ }
+ }
+
+ page_info = {
+ 'end_cursor' => cursor,
+ 'has_next_page' => has_next_page
+ }
+
+ BulkImports::Pipeline::ExtractedData.new(data: data, page_info: page_info)
+ end
+end
diff --git a/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb
new file mode 100644
index 00000000000..f66c67fc6a2
--- /dev/null
+++ b/spec/lib/bulk_imports/groups/transformers/member_attributes_transformer_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe BulkImports::Groups::Transformers::MemberAttributesTransformer do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:secondary_email) { 'secondary@email.com' }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:bulk_import) { create(:bulk_import, user: user) }
+ let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import, group: group) }
+ let_it_be(:context) { BulkImports::Pipeline::Context.new(entity) }
+
+ it 'returns nil when receives no data' do
+ expect(subject.transform(context, nil)).to eq(nil)
+ end
+
+ it 'returns nil when no user is found' do
+ expect(subject.transform(context, member_data)).to eq(nil)
+ expect(subject.transform(context, member_data(email: 'inexistent@email.com'))).to eq(nil)
+ end
+
+ context 'when the user is not confirmed' do
+ before do
+ user.update!(confirmed_at: nil)
+ end
+
+ it 'returns nil even when the primary email match' do
+ data = member_data(email: user.email)
+
+ expect(subject.transform(context, data)).to eq(nil)
+ end
+
+ it 'returns nil even when a secondary email match' do
+ user.emails << Email.new(email: secondary_email)
+ data = member_data(email: secondary_email)
+
+ expect(subject.transform(context, data)).to eq(nil)
+ end
+ end
+
+ context 'when the user is confirmed' do
+ before do
+ user.update!(confirmed_at: Time.now.utc)
+ end
+
+ it 'finds the user by the primary email' do
+ data = member_data(email: user.email)
+
+ expect(subject.transform(context, data)).to eq(
+ 'access_level' => 30,
+ 'user_id' => user.id,
+ 'created_by_id' => user.id,
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil
+ )
+ end
+
+ it 'finds the user by the secondary email' do
+ user.emails << Email.new(email: secondary_email, confirmed_at: Time.now.utc)
+ data = member_data(email: secondary_email)
+
+ expect(subject.transform(context, data)).to eq(
+ 'access_level' => 30,
+ 'user_id' => user.id,
+ 'created_by_id' => user.id,
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil
+ )
+ end
+
+ context 'format access level' do
+ it 'ignores record if no access level is given' do
+ data = member_data(email: user.email, access_level: nil)
+
+ expect(subject.transform(context, data)).to be_nil
+ end
+
+ it 'ignores record if is not a valid access level' do
+ data = member_data(email: user.email, access_level: 999)
+
+ expect(subject.transform(context, data)).to be_nil
+ end
+ end
+ end
+
+ def member_data(email: '', access_level: 30)
+ {
+ 'created_at' => '2020-01-01T00:00:00Z',
+ 'updated_at' => '2020-01-01T00:00:00Z',
+ 'expires_at' => nil,
+ 'access_level' => {
+ 'integer_value' => access_level
+ },
+ 'user' => {
+ 'public_email' => email
+ }
+ }
+ end
+end
diff --git a/spec/lib/bulk_imports/importers/group_importer_spec.rb b/spec/lib/bulk_imports/importers/group_importer_spec.rb
index 0884a51ce7d..43e12e6e3d7 100644
--- a/spec/lib/bulk_imports/importers/group_importer_spec.rb
+++ b/spec/lib/bulk_imports/importers/group_importer_spec.rb
@@ -18,9 +18,10 @@ RSpec.describe BulkImports::Importers::GroupImporter do
describe '#execute' do
it 'starts the entity and run its pipelines' do
expect_to_run_pipeline BulkImports::Groups::Pipelines::GroupPipeline, context: context
+ expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
+ expect_to_run_pipeline BulkImports::Groups::Pipelines::MembersPipeline, context: context
expect_to_run_pipeline BulkImports::Groups::Pipelines::LabelsPipeline, context: context
expect_to_run_pipeline('EE::BulkImports::Groups::Pipelines::EpicsPipeline'.constantize, context: context) if Gitlab.ee?
- expect_to_run_pipeline BulkImports::Groups::Pipelines::SubgroupEntitiesPipeline, context: context
subject.execute
diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb
index 1580fc82279..368cf98dfec 100644
--- a/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb
+++ b/spec/lib/gitlab/auth/otp/strategies/forti_token_cloud_spec.rb
@@ -13,6 +13,8 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do
let(:otp_verification_url) { url + '/auth' }
let(:access_token) { 'an_access_token' }
let(:access_token_create_response_body) { '' }
+ let(:access_token_request_body) { { client_id: client_id, client_secret: client_secret } }
+ let(:headers) { { 'Content-Type': 'application/json' } }
subject(:validate) { described_class.new(user).validate(otp_code) }
@@ -27,11 +29,8 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do
client_secret: client_secret
)
- access_token_request_body = { client_id: client_id,
- client_secret: client_secret }
-
stub_request(:post, access_token_create_url)
- .with(body: JSON(access_token_request_body), headers: { 'Content-Type' => 'application/json' })
+ .with(body: JSON(access_token_request_body), headers: headers)
.to_return(
status: access_token_create_response_status,
body: Gitlab::Json.generate(access_token_create_response_body),
@@ -81,6 +80,20 @@ RSpec.describe Gitlab::Auth::Otp::Strategies::FortiTokenCloud do
end
end
+ context 'SSL Verification' do
+ let(:access_token_create_response_status) { 400 }
+
+ context 'with `Gitlab::HTTP`' do
+ it 'does not use a `verify` argument,'\
+ 'thereby always performing SSL verification while making API calls' do
+ expect(Gitlab::HTTP).to receive(:post)
+ .with(access_token_create_url, body: JSON(access_token_request_body), headers: headers).and_call_original
+
+ validate
+ end
+ end
+ end
+
def stub_forti_token_cloud_config(forti_token_cloud_settings)
allow(::Gitlab.config.forti_token_cloud).to(receive_messages(forti_token_cloud_settings))
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
index ae3270cb9b2..7aaeee32f49 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -74,6 +74,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
it 'does not break the chain' do
expect(step.break?).to eq false
end
+
+ context 'when project is deleted' do
+ before do
+ project.update!(pending_delete: true)
+ end
+
+ specify { expect(step.perform!).to contain_exactly('Project is deleted!') }
+ end
end
describe '#allowed_to_write_ref?' do
diff --git a/spec/lib/gitlab/tree_summary_spec.rb b/spec/lib/gitlab/tree_summary_spec.rb
index 303a4a80581..d2c5844b0fa 100644
--- a/spec/lib/gitlab/tree_summary_spec.rb
+++ b/spec/lib/gitlab/tree_summary_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::TreeSummary do
+ include RepoHelpers
using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :empty_repo) }
@@ -44,6 +45,40 @@ RSpec.describe Gitlab::TreeSummary do
expect(commits).to match_array(entries.map { |entry| entry[:commit] })
end
end
+
+ context 'when offset is over the limit' do
+ let(:offset) { 100 }
+
+ it 'returns an empty array' do
+ expect(summarized).to eq([[], []])
+ end
+ end
+
+ context 'with caching', :use_clean_rails_memory_store_caching do
+ subject { Rails.cache.fetch(key) }
+
+ before do
+ summarized
+ end
+
+ context 'Repository tree cache' do
+ let(:key) { ['projects', project.id, 'content', commit.id, path] }
+
+ it 'creates a cache for repository content' do
+ is_expected.to eq([{ file_name: 'a.txt', type: :blob }])
+ end
+ end
+
+ context 'Commits list cache' do
+ let(:offset) { 0 }
+ let(:limit) { 25 }
+ let(:key) { ['projects', project.id, 'last_commits_list', commit.id, path, offset, limit] }
+
+ it 'creates a cache for commits list' do
+ is_expected.to eq('a.txt' => commit.to_hash)
+ end
+ end
+ end
end
describe '#summarize (entries)' do
@@ -167,6 +202,46 @@ RSpec.describe Gitlab::TreeSummary do
end
end
+ describe 'References in commit messages' do
+ let_it_be(:project) { create(:project, :empty_repo) }
+ let_it_be(:issue) { create(:issue, project: project) }
+ let(:entries) { summary.summarize.first }
+ let(:entry) { entries.find { |entry| entry[:file_name] == 'issue.txt' } }
+
+ before_all do
+ create_file_in_repo(project, 'master', 'master', 'issue.txt', '', commit_message: "Issue ##{issue.iid}")
+ end
+
+ where(:project_visibility, :user_role, :issue_confidential, :expected_result) do
+ 'private' | :guest | false | true
+ 'private' | :guest | true | false
+ 'private' | :reporter | false | true
+ 'private' | :reporter | true | true
+
+ 'internal' | :guest | false | true
+ 'internal' | :guest | true | false
+ 'internal' | :reporter | false | true
+ 'internal' | :reporter | true | true
+
+ 'public' | :guest | false | true
+ 'public' | :guest | true | false
+ 'public' | :reporter | false | true
+ 'public' | :reporter | true | true
+ end
+
+ with_them do
+ subject { entry[:commit_title_html].include?("title=\"#{issue.title}\"") }
+
+ before do
+ project.add_role(user, user_role)
+ project.update!(visibility_level: Gitlab::VisibilityLevel.level_value(project_visibility))
+ issue.update!(confidential: issue_confidential)
+ end
+
+ it { is_expected.to eq(expected_result) }
+ end
+ end
+
describe '#more?' do
let(:path) { 'tmp/more' }
diff --git a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
index 08b103a1179..5469ded18f9 100644
--- a/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
+++ b/spec/lib/gitlab/usage/metrics/aggregates/aggregate_spec.rb
@@ -222,6 +222,12 @@ RSpec.describe Gitlab::Usage::Metrics::Aggregates::Aggregate, :clean_gitlab_redi
end
end
+ it 'allows for YAML aliases in aggregated metrics configs' do
+ expect(YAML).to receive(:safe_load).with(kind_of(String), aliases: true)
+
+ described_class.new(recorded_at)
+ end
+
describe '.aggregated_metrics_weekly_data' do
subject(:aggregated_metrics_data) { described_class.new(recorded_at).weekly_data }
diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
index ba7bfe47bc9..4b07f9143b5 100644
--- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
describe '.track_unique_project_event' do
described_class::TEMPLATE_TO_EVENT.keys.each do |template|
context "when given template #{template}" do
- it_behaves_like 'tracking unique hll events', :usage_data_track_ci_templates_unique_projects do
+ it_behaves_like 'tracking unique hll events' do
subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template) }
let(:target_id) { "p_ci_templates_#{described_class::TEMPLATE_TO_EVENT[template]}" }
diff --git a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
index 579cc1e372a..b4894ec049f 100644
--- a/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/hll_redis_counter_spec.rb
@@ -48,6 +48,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
describe 'known_events' do
+ let(:feature) { 'test_hll_redis_counter_ff_check' }
+
let(:weekly_event) { 'g_analytics_contribution' }
let(:daily_event) { 'g_analytics_search' }
let(:analytics_slot_event) { 'g_analytics_contribution' }
@@ -67,7 +69,7 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
let(:known_events) do
[
- { name: weekly_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "weekly" },
+ { name: weekly_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "weekly", feature_flag: feature },
{ name: daily_event, redis_slot: "analytics", category: analytics_category, expiry: 84, aggregation: "daily" },
{ name: category_productivity_event, redis_slot: "analytics", category: productivity_category, aggregation: "weekly" },
{ name: compliance_slot_event, redis_slot: "compliance", category: compliance_category, aggregation: "weekly" },
@@ -78,6 +80,8 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
before do
+ skip_feature_flags_yaml_validation
+ skip_default_enabled_yaml_check
allow(described_class).to receive(:known_events).and_return(known_events)
end
@@ -88,6 +92,32 @@ RSpec.describe Gitlab::UsageDataCounters::HLLRedisCounter, :clean_gitlab_redis_s
end
describe '.track_event' do
+ context 'with feature flag set' do
+ it 'tracks the event when feature enabled' do
+ stub_feature_flags(feature => true)
+
+ expect(Gitlab::Redis::HLL).to receive(:add)
+
+ described_class.track_event(weekly_event, values: 1)
+ end
+
+ it 'does not track the event with feature flag disabled' do
+ stub_feature_flags(feature => false)
+
+ expect(Gitlab::Redis::HLL).not_to receive(:add)
+
+ described_class.track_event(weekly_event, values: 1)
+ end
+ end
+
+ context 'with no feature flag set' do
+ it 'tracks the event' do
+ expect(Gitlab::Redis::HLL).to receive(:add)
+
+ described_class.track_event(daily_event, values: 1)
+ end
+ end
+
context 'when usage_ping is disabled' do
it 'does not track the event' do
stub_application_setting(usage_ping_enabled: false)
diff --git a/spec/lib/uploaded_file_spec.rb b/spec/lib/uploaded_file_spec.rb
index 04eef823852..ececc84bc93 100644
--- a/spec/lib/uploaded_file_spec.rb
+++ b/spec/lib/uploaded_file_spec.rb
@@ -218,20 +218,6 @@ RSpec.describe UploadedFile do
end
end
- describe '#filename_sanitized?' do
- it 'is true when filename has been sanitized' do
- file = described_class.new(temp_file.path, filename: 'fooâ‘ .png')
-
- expect(file).to be_filename_sanitized
- end
-
- it 'is false when filename has not been sanitized' do
- file = described_class.new(temp_file.path, filename: 'foo.png')
-
- expect(file).not_to be_filename_sanitized
- end
- end
-
describe '#sanitize_filename' do
it { expect(described_class.new(temp_file.path).sanitize_filename('spaced name')).to eq('spaced_name') }
it { expect(described_class.new(temp_file.path).sanitize_filename('#$%^&')).to eq('_____') }
diff --git a/spec/migrations/insert_daily_invites_plan_limits_spec.rb b/spec/migrations/insert_daily_invites_plan_limits_spec.rb
new file mode 100644
index 00000000000..3265efcb0ce
--- /dev/null
+++ b/spec/migrations/insert_daily_invites_plan_limits_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20201007033723_insert_daily_invites_plan_limits.rb')
+
+RSpec.describe InsertDailyInvitesPlanLimits do
+ let(:plans) { table(:plans) }
+ let(:plan_limits) { table(:plan_limits) }
+ let!(:free_plan) { plans.create!(name: 'free') }
+ let!(:bronze_plan) { plans.create!(name: 'bronze') }
+ let!(:silver_plan) { plans.create!(name: 'silver') }
+ let!(:gold_plan) { plans.create!(name: 'gold') }
+
+ context 'when on Gitlab.com' do
+ before do
+ expect(Gitlab).to receive(:com?).at_most(:twice).and_return(true)
+ end
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(plan_limits.where.not(daily_invites: 0)).to be_empty
+ }
+
+ # Expectations will run after the up migration.
+ migration.after -> {
+ expect(plan_limits.pluck(:plan_id, :daily_invites)).to contain_exactly(
+ [free_plan.id, 20],
+ [bronze_plan.id, 0],
+ [silver_plan.id, 0],
+ [gold_plan.id, 0]
+ )
+ }
+ end
+ end
+ end
+
+ context 'when on self hosted' do
+ before do
+ expect(Gitlab).to receive(:com?).at_most(:twice).and_return(false)
+ end
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(plan_limits.pluck(:daily_invites)).to eq []
+ }
+
+ migration.after -> {
+ expect(plan_limits.pluck(:daily_invites)).to eq []
+ }
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 6f5f7d2e2fb..94943fb3644 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -34,6 +34,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it { is_expected.to have_many(:auto_canceled_jobs) }
it { is_expected.to have_many(:sourced_pipelines) }
it { is_expected.to have_many(:triggered_pipelines) }
+ it { is_expected.to have_many(:pipeline_artifacts) }
it { is_expected.to have_one(:chat_data) }
it { is_expected.to have_one(:source_pipeline) }
@@ -41,14 +42,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
it { is_expected.to have_one(:source_job) }
it { is_expected.to have_one(:pipeline_config) }
- it { is_expected.to validate_presence_of(:sha) }
- it { is_expected.to validate_presence_of(:status) }
-
it { is_expected.to respond_to :git_author_name }
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
it { is_expected.to delegate_method(:full_path).to(:project).with_prefix }
- it { is_expected.to have_many(:pipeline_artifacts) }
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:sha) }
+ it { is_expected.to validate_presence_of(:status) }
+ end
describe 'associations' do
it 'has a bidirectional relationship with projects' do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 1a791820f1b..b60af7abade 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -171,6 +171,43 @@ RSpec.describe Member do
end
end
+ describe '.in_hierarchy' do
+ let(:root_ancestor) { create(:group) }
+ let(:project) { create(:project, group: root_ancestor) }
+ let(:subgroup) { create(:group, parent: root_ancestor) }
+ let(:subgroup_project) { create(:project, group: subgroup) }
+
+ let!(:root_ancestor_member) { create(:group_member, group: root_ancestor) }
+ let!(:project_member) { create(:project_member, project: project) }
+ let!(:subgroup_member) { create(:group_member, group: subgroup) }
+ let!(:subgroup_project_member) { create(:project_member, project: subgroup_project) }
+
+ let(:hierarchy_members) do
+ [
+ root_ancestor_member,
+ project_member,
+ subgroup_member,
+ subgroup_project_member
+ ]
+ end
+
+ subject { Member.in_hierarchy(project) }
+
+ it { is_expected.to contain_exactly(*hierarchy_members) }
+
+ context 'with scope prefix' do
+ subject { Member.where.not(source: project).in_hierarchy(subgroup) }
+
+ it { is_expected.to contain_exactly(root_ancestor_member, subgroup_member, subgroup_project_member) }
+ end
+
+ context 'with scope suffix' do
+ subject { Member.in_hierarchy(project).where.not(source: project) }
+
+ it { is_expected.to contain_exactly(root_ancestor_member, subgroup_member, subgroup_project_member) }
+ end
+ end
+
describe '.invite' do
it { expect(described_class.invite).not_to include @maintainer }
it { expect(described_class.invite).to include @invited_member }
@@ -251,6 +288,21 @@ RSpec.describe Member do
it { is_expected.to include(expiring_tomorrow, not_expiring) }
end
+ describe '.created_today' do
+ let_it_be(:now) { Time.current }
+ let_it_be(:created_today) { create(:group_member, created_at: now.beginning_of_day) }
+ let_it_be(:created_yesterday) { create(:group_member, created_at: now - 1.day) }
+
+ before do
+ travel_to now
+ end
+
+ subject { described_class.created_today }
+
+ it { is_expected.not_to include(created_yesterday) }
+ it { is_expected.to include(created_today) }
+ end
+
describe '.last_ten_days_excluding_today' do
let_it_be(:now) { Time.current }
let_it_be(:created_today) { create(:group_member, created_at: now.beginning_of_day) }
diff --git a/spec/models/plan_limits_spec.rb b/spec/models/plan_limits_spec.rb
index 67fb11f34e0..4259c8b708b 100644
--- a/spec/models/plan_limits_spec.rb
+++ b/spec/models/plan_limits_spec.rb
@@ -209,6 +209,7 @@ RSpec.describe PlanLimits do
ci_pipeline_size
ci_active_jobs
storage_size_limit
+ daily_invites
] + disabled_max_artifact_size_columns
end
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index 8215fb5c336..ea63406e615 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -2,11 +2,13 @@
require 'spec_helper'
+require 'googleauth'
+
RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowplow do
include PrometheusHelpers
include ReactiveCachingHelpers
- let(:project) { create(:prometheus_project) }
+ let_it_be_with_reload(:project) { create(:prometheus_project) }
let(:service) { project.prometheus_service }
describe "Associations" do
@@ -256,19 +258,66 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl
context 'behind IAP' do
let(:manual_configuration) { true }
- before do
- # dummy private key generated only for this test to pass openssl validation
- service.google_iap_service_account_json = '{"type":"service_account","private_key":"-----BEGIN RSA PRIVATE KEY-----\nMIIBOAIBAAJAU85LgUY5o6j6j/07GMLCNUcWJOBA1buZnNgKELayA6mSsHrIv31J\nY8kS+9WzGPQninea7DcM4hHA7smMgQD1BwIDAQABAkAqKxMy6PL3tn7dFL43p0ex\nJyOtSmlVIiAZG1t1LXhE/uoLpYi5DnbYqGgu0oih+7nzLY/dXpNpXUmiRMOUEKmB\nAiEAoTi2rBXbrLSi2C+H7M/nTOjMQQDuZ8Wr4uWpKcjYJTMCIQCFEskL565oFl/7\nRRQVH+cARrAsAAoJSbrOBAvYZ0PI3QIgIEFwis10vgEF86rOzxppdIG/G+JL0IdD\n9IluZuXAGPECIGUo7qSaLr75o2VEEgwtAFH5aptIPFjrL5LFCKwtdB4RAiAYZgFV\nHCMmaooAw/eELuMoMWNYmujZ7VaAnOewGDW0uw==\n-----END RSA PRIVATE KEY-----\n"}'
- service.google_iap_audience_client_id = "IAP_CLIENT_ID.apps.googleusercontent.com"
+ let(:google_iap_service_account) do
+ {
+ type: "service_account",
+ # dummy private key generated only for this test to pass openssl validation
+ private_key: <<~KEY
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIBOAIBAAJAU85LgUY5o6j6j/07GMLCNUcWJOBA1buZnNgKELayA6mSsHrIv31J
+ Y8kS+9WzGPQninea7DcM4hHA7smMgQD1BwIDAQABAkAqKxMy6PL3tn7dFL43p0ex
+ JyOtSmlVIiAZG1t1LXhE/uoLpYi5DnbYqGgu0oih+7nzLY/dXpNpXUmiRMOUEKmB
+ AiEAoTi2rBXbrLSi2C+H7M/nTOjMQQDuZ8Wr4uWpKcjYJTMCIQCFEskL565oFl/7
+ RRQVH+cARrAsAAoJSbrOBAvYZ0PI3QIgIEFwis10vgEF86rOzxppdIG/G+JL0IdD
+ 9IluZuXAGPECIGUo7qSaLr75o2VEEgwtAFH5aptIPFjrL5LFCKwtdB4RAiAYZgFV
+ HCMmaooAw/eELuMoMWNYmujZ7VaAnOewGDW0uw==
+ -----END RSA PRIVATE KEY-----
+ KEY
+ }
+ end
+
+ def stub_iap_request
+ service.google_iap_service_account_json = Gitlab::Json.generate(google_iap_service_account)
+ service.google_iap_audience_client_id = 'IAP_CLIENT_ID.apps.googleusercontent.com'
- stub_request(:post, "https://oauth2.googleapis.com/token").to_return(status: 200, body: '{"id_token": "FOO"}', headers: { 'Content-Type': 'application/json; charset=UTF-8' })
+ stub_request(:post, 'https://oauth2.googleapis.com/token')
+ .to_return(
+ status: 200,
+ body: '{"id_token": "FOO"}',
+ headers: { 'Content-Type': 'application/json; charset=UTF-8' }
+ )
end
it 'includes the authorization header' do
+ stub_iap_request
+
expect(service.prometheus_client).not_to be_nil
expect(service.prometheus_client.send(:options)).to have_key(:headers)
expect(service.prometheus_client.send(:options)[:headers]).to eq(authorization: "Bearer FOO")
end
+
+ context 'when passed with token_credential_uri', issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/284819' do
+ let(:malicious_host) { 'http://example.com' }
+
+ where(:param_name) do
+ [
+ :token_credential_uri,
+ :tokencredentialuri,
+ :Token_credential_uri,
+ :tokenCredentialUri
+ ]
+ end
+
+ with_them do
+ it 'does not make any unexpected HTTP requests' do
+ google_iap_service_account[param_name] = malicious_host
+ stub_iap_request
+ stub_request(:any, malicious_host).to_raise('Making additional HTTP requests is forbidden!')
+
+ expect(service.prometheus_client).not_to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index efa1afb758f..6ba3ab6aace 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1057,6 +1057,78 @@ RSpec.describe ProjectPolicy do
it { is_expected.to be_allowed(:read_analytics) }
end
+ context 'with various analytics features' do
+ let_it_be(:project_with_analytics_disabled) { create(:project, :analytics_disabled) }
+ let_it_be(:project_with_analytics_private) { create(:project, :analytics_private) }
+ let_it_be(:project_with_analytics_enabled) { create(:project, :analytics_enabled) }
+
+ before do
+ project_with_analytics_disabled.add_developer(developer)
+ project_with_analytics_private.add_developer(developer)
+ project_with_analytics_enabled.add_developer(developer)
+ end
+
+ context 'when analytics is enabled for the project' do
+ let(:project) { project_with_analytics_disabled }
+
+ context 'for guest user' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:read_cycle_analytics) }
+ it { is_expected.to be_disallowed(:read_insights) }
+ it { is_expected.to be_disallowed(:read_repository_graphs) }
+ end
+
+ context 'for developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:read_cycle_analytics) }
+ it { is_expected.to be_disallowed(:read_insights) }
+ it { is_expected.to be_disallowed(:read_repository_graphs) }
+ end
+ end
+
+ context 'when analytics is private for the project' do
+ let(:project) { project_with_analytics_private }
+
+ context 'for guest user' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:read_cycle_analytics) }
+ it { is_expected.to be_disallowed(:read_insights) }
+ it { is_expected.to be_disallowed(:read_repository_graphs) }
+ end
+
+ context 'for developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_allowed(:read_cycle_analytics) }
+ it { is_expected.to be_allowed(:read_insights) }
+ it { is_expected.to be_allowed(:read_repository_graphs) }
+ end
+ end
+
+ context 'when analytics is enabled for the project' do
+ let(:project) { project_with_analytics_private }
+
+ context 'for guest user' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:read_cycle_analytics) }
+ it { is_expected.to be_disallowed(:read_insights) }
+ it { is_expected.to be_disallowed(:read_repository_graphs) }
+ end
+
+ context 'for developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_allowed(:read_cycle_analytics) }
+ it { is_expected.to be_allowed(:read_insights) }
+ it { is_expected.to be_allowed(:read_repository_graphs) }
+ end
+ end
+ end
+
context 'project member' do
let(:project) { private_project }
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
index 2653653c896..2316e702c3e 100644
--- a/spec/requests/api/lint_spec.rb
+++ b/spec/requests/api/lint_spec.rb
@@ -4,91 +4,136 @@ require 'spec_helper'
RSpec.describe API::Lint do
describe 'POST /ci/lint' do
- context 'with valid .gitlab-ci.yaml content' do
- let(:yaml_content) do
- File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
- end
+ context 'when signup settings are disabled' do
+ Gitlab::CurrentSettings.signup_enabled = false
- it 'passes validation without warnings or errors' do
- post api('/ci/lint'), params: { content: yaml_content }
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ post api('/ci/lint'), params: { content: 'content' }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_an Hash
- expect(json_response['status']).to eq('valid')
- expect(json_response['warnings']).to eq([])
- expect(json_response['errors']).to eq([])
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
end
- it 'outputs expanded yaml content' do
- post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true }
+ context 'when authenticated' do
+ it 'returns unauthorized error' do
+ post api('/ci/lint'), params: { content: 'content' }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to have_key('merged_yaml')
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
end
end
- context 'with valid .gitlab-ci.yaml with warnings' do
- let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml }
+ context 'when signup settings are enabled' do
+ Gitlab::CurrentSettings.signup_enabled = true
- it 'passes validation but returns warnings' do
- post api('/ci/lint'), params: { content: yaml_content }
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ post api('/ci/lint'), params: { content: 'content' }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['status']).to eq('valid')
- expect(json_response['warnings']).not_to be_empty
- expect(json_response['status']).to eq('valid')
- expect(json_response['errors']).to eq([])
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'when authenticated' do
+ let_it_be(:api_user) { create(:user) }
+ it 'returns authentication success' do
+ post api('/ci/lint', api_user), params: { content: 'content' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
end
end
- context 'with an invalid .gitlab_ci.yml' do
- context 'with invalid syntax' do
- let(:yaml_content) { 'invalid content' }
+ context 'when authenticated' do
+ let_it_be(:api_user) { create(:user) }
- it 'responds with errors about invalid syntax' do
- post api('/ci/lint'), params: { content: yaml_content }
+ context 'with valid .gitlab-ci.yaml content' do
+ let(:yaml_content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+
+ it 'passes validation without warnings or errors' do
+ post api('/ci/lint', api_user), params: { content: yaml_content }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['status']).to eq('invalid')
+ expect(json_response).to be_an Hash
+ expect(json_response['status']).to eq('valid')
expect(json_response['warnings']).to eq([])
- expect(json_response['errors']).to eq(['Invalid configuration format'])
+ expect(json_response['errors']).to eq([])
end
it 'outputs expanded yaml content' do
- post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true }
+ post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to have_key('merged_yaml')
end
end
- context 'with invalid configuration' do
- let(:yaml_content) { '{ image: "ruby:2.7", services: ["postgres"], invalid }' }
+ context 'with valid .gitlab-ci.yaml with warnings' do
+ let(:yaml_content) { { job: { script: 'ls', rules: [{ when: 'always' }] } }.to_yaml }
- it 'responds with errors about invalid configuration' do
- post api('/ci/lint'), params: { content: yaml_content }
+ it 'passes validation but returns warnings' do
+ post api('/ci/lint', api_user), params: { content: yaml_content }
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['status']).to eq('invalid')
- expect(json_response['warnings']).to eq([])
- expect(json_response['errors']).to eq(['jobs invalid config should implement a script: or a trigger: keyword', 'jobs config should contain at least one visible job'])
+ expect(json_response['status']).to eq('valid')
+ expect(json_response['warnings']).not_to be_empty
+ expect(json_response['status']).to eq('valid')
+ expect(json_response['errors']).to eq([])
end
+ end
- it 'outputs expanded yaml content' do
- post api('/ci/lint'), params: { content: yaml_content, include_merged_yaml: true }
+ context 'with an invalid .gitlab_ci.yml' do
+ context 'with invalid syntax' do
+ let(:yaml_content) { 'invalid content' }
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to have_key('merged_yaml')
+ it 'responds with errors about invalid syntax' do
+ post api('/ci/lint', api_user), params: { content: yaml_content }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['status']).to eq('invalid')
+ expect(json_response['warnings']).to eq([])
+ expect(json_response['errors']).to eq(['Invalid configuration format'])
+ end
+
+ it 'outputs expanded yaml content' do
+ post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to have_key('merged_yaml')
+ end
+ end
+
+ context 'with invalid configuration' do
+ let(:yaml_content) { '{ image: "ruby:2.7", services: ["postgres"] }' }
+
+ it 'responds with errors about invalid configuration' do
+ post api('/ci/lint', api_user), params: { content: yaml_content }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['status']).to eq('invalid')
+ expect(json_response['warnings']).to eq([])
+ expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
+ end
+
+ it 'outputs expanded yaml content' do
+ post api('/ci/lint', api_user), params: { content: yaml_content, include_merged_yaml: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to have_key('merged_yaml')
+ end
end
end
- end
- context 'without the content parameter' do
- it 'responds with validation error about missing content' do
- post api('/ci/lint')
+ context 'without the content parameter' do
+ it 'responds with validation error about missing content' do
+ post api('/ci/lint', api_user)
- expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['error']).to eq('content is missing')
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq('content is missing')
+ end
end
end
end
@@ -364,6 +409,18 @@ RSpec.describe API::Lint do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'when project is public' do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'returns authentication error' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
end
context 'when authenticated as non-member' do
@@ -387,13 +444,10 @@ RSpec.describe API::Lint do
context 'when running as dry run' do
let(:dry_run) { true }
- it 'returns pipeline creation error' do
+ it 'returns authentication error' do
ci_lint
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['merged_yaml']).to eq(nil)
- expect(json_response['valid']).to eq(false)
- expect(json_response['errors']).to eq(['Insufficient permissions to create a new pipeline'])
+ expect(response).to have_gitlab_http_status(:forbidden)
end
end
@@ -410,7 +464,11 @@ RSpec.describe API::Lint do
)
end
- it_behaves_like 'valid project config'
+ it 'returns authentication error' do
+ ci_lint
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
end
end
end
diff --git a/spec/requests/api/merge_request_approvals_spec.rb b/spec/requests/api/merge_request_approvals_spec.rb
index fad5c3fb60e..b18f3017e03 100644
--- a/spec/requests/api/merge_request_approvals_spec.rb
+++ b/spec/requests/api/merge_request_approvals_spec.rb
@@ -21,6 +21,12 @@ RSpec.describe API::MergeRequestApprovals do
expect(response).to have_gitlab_http_status(:ok)
end
+
+ context 'when merge request author has only guest access' do
+ it_behaves_like 'rejects user from accessing merge request info' do
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/approvals" }
+ end
+ end
end
describe 'POST :id/merge_requests/:merge_request_iid/approve' do
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 2e6cbe7bee7..971fb5e991c 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -35,6 +35,12 @@ RSpec.describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
get api("/projects/#{project.id}/merge_requests/0/versions", user)
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'when merge request author has only guest access' do
+ it_behaves_like 'rejects user from accessing merge request info' do
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions" }
+ end
+ end
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions/:version_id' do
@@ -63,5 +69,11 @@ RSpec.describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
get api("/projects/#{project.id}/merge_requests/#{non_existing_record_iid}/versions/#{merge_request_diff.id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'when merge request author has only guest access' do
+ it_behaves_like 'rejects user from accessing merge request info' do
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/#{merge_request_diff.id}" }
+ end
+ end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index b459d3cd8d3..ad8e21bf4c1 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1226,6 +1226,12 @@ RSpec.describe API::MergeRequests do
end
end
+ context 'when merge request author has only guest access' do
+ it_behaves_like 'rejects user from accessing merge request info' do
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}" }
+ end
+ end
+
context 'merge_request_metrics' do
let(:pipeline) { create(:ci_empty_pipeline) }
@@ -1411,6 +1417,12 @@ RSpec.describe API::MergeRequests do
it_behaves_like 'issuable participants endpoint' do
let(:entity) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
end
+
+ context 'when merge request author has only guest access' do
+ it_behaves_like 'rejects user from accessing merge request info' do
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/participants" }
+ end
+ end
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/commits' do
@@ -1436,6 +1448,12 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'when merge request author has only guest access' do
+ it_behaves_like 'rejects user from accessing merge request info' do
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits" }
+ end
+ end
end
describe 'GET /projects/:id/merge_requests/:merge_request_iid/:context_commits' do
@@ -1511,6 +1529,12 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:not_found)
end
+ context 'when merge request author has only guest access' do
+ it_behaves_like 'rejects user from accessing merge request info' do
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/changes" }
+ end
+ end
+
it_behaves_like 'find an existing merge request'
it_behaves_like 'accesses diffs via raw_diffs'
@@ -1600,6 +1624,12 @@ RSpec.describe API::MergeRequests do
expect(response).to have_gitlab_http_status(:forbidden)
end
end
+
+ context 'when merge request author has only guest access' do
+ it_behaves_like 'rejects user from accessing merge request info' do
+ let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines" }
+ end
+ end
end
describe 'POST /projects/:id/merge_requests/:merge_request_iid/pipelines' do
diff --git a/spec/requests/api/npm_instance_packages_spec.rb b/spec/requests/api/npm_instance_packages_spec.rb
index 8299717b5c7..70c76067a6e 100644
--- a/spec/requests/api/npm_instance_packages_spec.rb
+++ b/spec/requests/api/npm_instance_packages_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe API::NpmInstancePackages do
include_context 'npm api setup'
describe 'GET /api/v4/packages/npm/*package_name' do
- it_behaves_like 'handling get metadata requests' do
+ it_behaves_like 'handling get metadata requests', scope: :instance do
let(:url) { api("/packages/npm/#{package_name}") }
end
end
diff --git a/spec/requests/api/npm_project_packages_spec.rb b/spec/requests/api/npm_project_packages_spec.rb
index 1421f20ac28..7ea238c0607 100644
--- a/spec/requests/api/npm_project_packages_spec.rb
+++ b/spec/requests/api/npm_project_packages_spec.rb
@@ -6,25 +6,25 @@ RSpec.describe API::NpmProjectPackages do
include_context 'npm api setup'
describe 'GET /api/v4/projects/:id/packages/npm/*package_name' do
- it_behaves_like 'handling get metadata requests' do
+ it_behaves_like 'handling get metadata requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/#{package_name}") }
end
end
describe 'GET /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags' do
- it_behaves_like 'handling get dist tags requests' do
+ it_behaves_like 'handling get dist tags requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags") }
end
end
describe 'PUT /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
- it_behaves_like 'handling create dist tag requests' do
+ it_behaves_like 'handling create dist tag requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
describe 'DELETE /api/v4/projects/:id/packages/npm/-/package/*package_name/dist-tags/:tag' do
- it_behaves_like 'handling delete dist tag requests' do
+ it_behaves_like 'handling delete dist tag requests', scope: :project do
let(:url) { api("/projects/#{project.id}/packages/npm/-/package/#{package_name}/dist-tags/#{tag_name}") }
end
end
@@ -32,10 +32,14 @@ RSpec.describe API::NpmProjectPackages do
describe 'GET /api/v4/projects/:id/packages/npm/*package_name/-/*file_name' do
let_it_be(:package_file) { package.package_files.first }
- let(:params) { {} }
- let(:url) { api("/projects/#{project.id}/packages/npm/#{package_file.package.name}/-/#{package_file.file_name}") }
+ let(:headers) { {} }
+ let(:url) { api("/projects/#{project.id}/packages/npm/#{package.name}/-/#{package_file.file_name}") }
- subject { get(url, params: params) }
+ subject { get(url, headers: headers) }
+
+ before do
+ project.add_developer(user)
+ end
shared_examples 'a package file that requires auth' do
it 'denies download with no token' do
@@ -45,7 +49,7 @@ RSpec.describe API::NpmProjectPackages do
end
context 'with access token' do
- let(:params) { { access_token: token.token } }
+ let(:headers) { build_token_auth_header(token.token) }
it 'returns the file' do
subject
@@ -56,7 +60,7 @@ RSpec.describe API::NpmProjectPackages do
end
context 'with job token' do
- let(:params) { { job_token: job.token } }
+ let(:headers) { build_token_auth_header(job.token) }
it 'returns the file' do
subject
@@ -86,7 +90,7 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'a package file that requires auth'
context 'with guest' do
- let(:params) { { access_token: token.token } }
+ let(:headers) { build_token_auth_header(token.token) }
it 'denies download when not enough permissions' do
project.add_guest(user)
@@ -108,7 +112,11 @@ RSpec.describe API::NpmProjectPackages do
end
describe 'PUT /api/v4/projects/:id/packages/npm/:package_name' do
- RSpec.shared_examples 'handling invalid record with 400 error' do
+ before do
+ project.add_developer(user)
+ end
+
+ shared_examples 'handling invalid record with 400 error' do
it 'handles an ActiveRecord::RecordInvalid exception with 400 error' do
expect { upload_package_with_token(package_name, params) }
.not_to change { project.packages.count }
@@ -261,7 +269,9 @@ RSpec.describe API::NpmProjectPackages do
end
def upload_package(package_name, params = {})
- put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params
+ token = params.delete(:access_token) || params.delete(:job_token)
+ headers = build_token_auth_header(token)
+ put api("/projects/#{project.id}/packages/npm/#{package_name.sub('/', '%2f')}"), params: params, headers: headers
end
def upload_package_with_token(package_name, params = {})
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
new file mode 100644
index 00000000000..181fcafd577
--- /dev/null
+++ b/spec/requests/api/project_attributes.yml
@@ -0,0 +1,149 @@
+---
+itself: # project
+ unexposed_attributes:
+ - bfg_object_map
+ - delete_error
+ - detected_repository_languages
+ - disable_overriding_approvers_per_merge_request
+ - external_authorization_classification_label
+ - external_webhook_token
+ - has_external_issue_tracker
+ - has_external_wiki
+ - import_source
+ - import_type
+ - import_url
+ - issues_template
+ - jobs_cache_index
+ - last_repository_check_at
+ - last_repository_check_failed
+ - last_repository_updated_at
+ - marked_for_deletion_at
+ - marked_for_deletion_by_user_id
+ - max_artifacts_size
+ - max_pages_size
+ - merge_requests_author_approval
+ - merge_requests_disable_committers_approval
+ - merge_requests_rebase_enabled
+ - merge_requests_template
+ - mirror_last_successful_update_at
+ - mirror_last_update_at
+ - mirror_overwrites_diverged_branches
+ - mirror_trigger_builds
+ - mirror_user_id
+ - only_mirror_protected_branches
+ - pages_https_only
+ - pending_delete
+ - pool_repository_id
+ - pull_mirror_available_overridden
+ - pull_mirror_branch_prefix
+ - remote_mirror_available_overridden
+ - repository_read_only
+ - repository_size_limit
+ - require_password_to_approve
+ - reset_approvals_on_push
+ - runners_token_encrypted
+ - storage_version
+ - updated_at
+ remapped_attributes:
+ avatar: avatar_url
+ build_allow_git_fetch: build_git_strategy
+ merge_requests_ff_only_enabled: merge_method
+ namespace_id: namespace
+ public_builds: public_jobs
+ visibility_level: visibility
+ computed_attributes:
+ - _links
+ - can_create_merge_request_in
+ - compliance_frameworks
+ - container_expiration_policy
+ - default_branch
+ - empty_repo
+ - forks_count
+ - http_url_to_repo
+ - name_with_namespace
+ - open_issues_count
+ - owner
+ - path_with_namespace
+ - permissions
+ - readme_url
+ - shared_with_groups
+ - ssh_url_to_repo
+ - web_url
+
+build_auto_devops: # auto_devops
+ unexposed_attributes:
+ - id
+ - project_id
+ - created_at
+ - updated_at
+ remapped_attributes:
+ enabled: auto_devops_enabled
+ deploy_strategy: auto_devops_deploy_strategy
+
+ci_cd_settings:
+ unexposed_attributes:
+ - id
+ - project_id
+ - group_runners_enabled
+ - keep_latest_artifact
+ - merge_pipelines_enabled
+ - merge_trains_enabled
+ - auto_rollback_enabled
+ remapped_attributes:
+ default_git_depth: ci_default_git_depth
+ forward_deployment_enabled: ci_forward_deployment_enabled
+
+build_import_state: # import_state
+ unexposed_attributes:
+ - id
+ - project_id
+ - retry_count
+ - last_update_started_at
+ - last_update_scheduled_at
+ - next_execution_timestamp
+ - jid
+ - last_update_at
+ - last_successful_update_at
+ - correlation_id_value
+ remapped_attributes:
+ status: import_status
+ last_error: import_error
+
+project_feature:
+ unexposed_attributes:
+ - id
+ - created_at
+ - metrics_dashboard_access_level
+ - project_id
+ - requirements_access_level
+ - security_and_compliance_access_level
+ - updated_at
+ computed_attributes:
+ - issues_enabled
+ - jobs_enabled
+ - merge_requests_enabled
+ - requirements_enabled
+ - security_and_compliance_enabled
+ - snippets_enabled
+ - wiki_enabled
+
+project_setting:
+ unexposed_attributes:
+ - allow_editing_commit_messages
+ - created_at
+ - has_confluence
+ - has_vulnerabilities
+ - prevent_merge_without_jira_issue
+ - project_id
+ - push_rule_id
+ - show_default_award_emojis
+ - squash_option
+ - updated_at
+
+build_service_desk_setting: # service_desk_setting
+ unexposed_attributes:
+ - project_id
+ - issue_template_key
+ - outgoing_name
+ remapped_attributes:
+ project_key: service_desk_address
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 6d8cdde2c4f..ad36777184a 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -886,6 +886,7 @@ RSpec.describe API::Projects do
merge_method: 'ff'
}).tap do |attrs|
attrs[:operations_access_level] = 'disabled'
+ attrs[:analytics_access_level] = 'disabled'
end
post api('/projects', user), params: project
@@ -1539,6 +1540,35 @@ RSpec.describe API::Projects do
end
context 'when authenticated as an admin' do
+ let(:project_attributes_file) { 'spec/requests/api/project_attributes.yml' }
+ let(:project_attributes) { YAML.load_file(project_attributes_file) }
+
+ let(:expected_keys) do
+ keys = project_attributes.map do |relation, relation_config|
+ begin
+ actual_keys = project.send(relation).attributes.keys
+ rescue NoMethodError
+ actual_keys = ["#{relation} is nil"]
+ end
+ unexposed_attributes = relation_config['unexposed_attributes'] || []
+ remapped_attributes = relation_config['remapped_attributes'] || {}
+ computed_attributes = relation_config['computed_attributes'] || []
+ actual_keys - unexposed_attributes - remapped_attributes.keys + remapped_attributes.values + computed_attributes
+ end.flatten
+
+ unless Gitlab.ee?
+ keys -= %w[
+ approvals_before_merge
+ compliance_frameworks
+ mirror
+ requirements_enabled
+ security_and_compliance_enabled
+ ]
+ end
+
+ keys
+ end
+
it 'returns a project by id' do
project
project_member
@@ -1587,6 +1617,27 @@ RSpec.describe API::Projects do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
expect(json_response['operations_access_level']).to be_present
end
+
+ it 'exposes all necessary attributes' do
+ create(:project_group_link, project: project)
+
+ get api("/projects/#{project.id}", admin)
+
+ diff = Set.new(json_response.keys) ^ Set.new(expected_keys)
+
+ expect(diff).to be_empty, failure_message(diff)
+ end
+
+ def failure_message(diff)
+ <<~MSG
+ It looks like project's set of exposed attributes is different from the expected set.
+
+ The following attributes are missing or newly added:
+ #{diff.to_a.to_sentence}
+
+ Please update #{project_attributes_file} file"
+ MSG
+ end
end
context 'when authenticated as a regular user' do
diff --git a/spec/requests/api/terraform/state_spec.rb b/spec/requests/api/terraform/state_spec.rb
index bfdb5458fd1..2cb3c8e9ab5 100644
--- a/spec/requests/api/terraform/state_spec.rb
+++ b/spec/requests/api/terraform/state_spec.rb
@@ -39,7 +39,7 @@ RSpec.describe API::Terraform::State do
context 'with maintainer permissions' do
let(:current_user) { maintainer }
- it_behaves_like 'tracking unique hll events', :usage_data_p_terraform_state_api_unique_users do
+ it_behaves_like 'tracking unique hll events' do
let(:target_id) { 'p_terraform_state_api_unique_users' }
let(:expected_type) { instance_of(Integer) }
end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index eaffa49fc9d..00de1ef5964 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -331,6 +331,14 @@ RSpec.describe API::Todos do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ it 'returns an error if the issuable author does not have access' do
+ project_1.add_guest(issuable.author)
+
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.iid}/todo", issuable.author)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
describe 'POST :id/issuable_type/:issueable_id/todo' do
diff --git a/spec/services/ci/abort_project_pipelines_service_spec.rb b/spec/services/ci/abort_project_pipelines_service_spec.rb
new file mode 100644
index 00000000000..9af909ac2ab
--- /dev/null
+++ b/spec/services/ci/abort_project_pipelines_service_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Ci::AbortProjectPipelinesService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:pipeline) { create(:ci_pipeline, :running, project: project) }
+ let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline) }
+
+ describe '#execute' do
+ it 'cancels all running pipelines and related jobs' do
+ result = described_class.new.execute(project)
+
+ expect(result).to be_success
+ expect(pipeline.reload).to be_canceled
+ expect(build.reload).to be_canceled
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new { described_class.new.execute(project) }.count
+
+ pipelines = create_list(:ci_pipeline, 5, :running, project: project)
+ create_list(:ci_build, 5, :running, pipeline: pipelines.first)
+
+ expect { described_class.new.execute(project) }.not_to exceed_query_limit(control_count)
+ end
+ end
+
+ context 'when feature disabled' do
+ before do
+ stub_feature_flags(abort_deleted_project_pipelines: false)
+ end
+
+ it 'does not abort the pipeline' do
+ result = described_class.new.execute(project)
+
+ expect(result).to be(nil)
+ expect(pipeline.reload).to be_running
+ expect(build.reload).to be_running
+ end
+ end
+end
diff --git a/spec/services/design_management/save_designs_service_spec.rb b/spec/services/design_management/save_designs_service_spec.rb
index 3e1641e2db2..f36e68c8dbd 100644
--- a/spec/services/design_management/save_designs_service_spec.rb
+++ b/spec/services/design_management/save_designs_service_spec.rb
@@ -4,7 +4,6 @@ require 'spec_helper'
RSpec.describe DesignManagement::SaveDesignsService do
include DesignManagementTestHelpers
include ConcurrentHelpers
- using FixtureFileRefinements
let_it_be_with_reload(:issue) { create(:issue) }
let_it_be(:developer) { create(:user, developer_projects: [issue.project]) }
@@ -13,11 +12,11 @@ RSpec.describe DesignManagement::SaveDesignsService do
let(:files) { [rails_sample] }
let(:design_repository) { ::Gitlab::GlRepository::DESIGN.repository_resolver.call(project) }
let(:rails_sample_name) { 'rails_sample.jpg' }
- let(:rails_sample) { uploaded_file(rails_sample_name).to_gitlab_uploaded_file }
- let(:dk_png) { uploaded_file('dk.png').to_gitlab_uploaded_file }
+ let(:rails_sample) { sample_image(rails_sample_name) }
+ let(:dk_png) { sample_image('dk.png') }
- def uploaded_file(filename)
- fixture_file_upload(expand_fixture_path(filename))
+ def sample_image(filename)
+ fixture_file_upload("spec/fixtures/#{filename}")
end
def commit_count
@@ -123,8 +122,7 @@ RSpec.describe DesignManagement::SaveDesignsService do
parellism = 4
blocks = Array.new(parellism).map do
- unique_file = uploaded_file('dk.png').uniquely_named.to_gitlab_uploaded_file
- unique_files = [unique_file]
+ unique_files = [RenameableUpload.unique_file('rails_sample.jpg')]
-> { run_service(unique_files) }
end
@@ -308,14 +306,6 @@ RSpec.describe DesignManagement::SaveDesignsService do
expect(response[:message]).to match('Duplicate filenames are not allowed!')
end
end
-
- context 'when uploading files with special characters in filenames' do
- let(:files) { [uploaded_file('dk.png').renamed_as('special_charâ‘ .png').to_gitlab_uploaded_file] }
-
- it 'returns the correct error' do
- expect(response[:message]).to match('Filenames contained invalid characters and could not be saved')
- end
- end
end
context 'when the user is not allowed to upload designs' do
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index f0f09218b06..75d1c98923a 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -69,6 +69,12 @@ RSpec.describe Projects::DestroyService, :aggregate_failures do
destroy_project(project, user, {})
end
+ it 'performs cancel for project ci pipelines' do
+ expect(::Ci::AbortProjectPipelinesService).to receive_message_chain(:new, :execute).with(project)
+
+ destroy_project(project, user, {})
+ end
+
context 'when project has remote mirrors' do
let!(:project) do
create(:project, :repository, namespace: user.namespace).tap do |project|
diff --git a/spec/support/refinements/fixture_file_refinements.rb b/spec/support/refinements/fixture_file_refinements.rb
deleted file mode 100644
index fd5fcf73200..00000000000
--- a/spec/support/refinements/fixture_file_refinements.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-module FixtureFileRefinements
- refine Rack::Test::UploadedFile do
- # Recast this instance of `Rack::Test::UploadedFile` to an `::UploadedFile`.
- def to_gitlab_uploaded_file
- ::UploadedFile.new(path, filename: original_filename, content_type: content_type || 'application/octet-stream').tap do |file|
- # `UploadedFile#tempfile` is read-only, so replace this with the writeable fixture file
- file.instance_variable_set(:@tempfile, self)
- end
- end
-
- # Renames `original_filename` to something guaranteed to be unique.
- def uniquely_named
- name = File.basename(FactoryBot.generate(:filename), '.*')
- extension = File.extname(original_filename)
- unique_filename = name + extension
-
- renamed_as(unique_filename)
- end
-
- def renamed_as(new_filename)
- tap { @original_filename = new_filename }
- end
- end
-end
diff --git a/spec/support/renameable_upload.rb b/spec/support/renameable_upload.rb
new file mode 100644
index 00000000000..f7f00181605
--- /dev/null
+++ b/spec/support/renameable_upload.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class RenameableUpload < SimpleDelegator
+ attr_accessor :original_filename
+
+ # Get a fixture file with a new unique name, and the same extension
+ def self.unique_file(name)
+ upload = new(fixture_file_upload("spec/fixtures/#{name}"))
+ ext = File.extname(name)
+ new_name = File.basename(FactoryBot.generate(:filename), '.*')
+ upload.original_filename = new_name + ext
+
+ upload
+ end
+end
diff --git a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
index 7c23ec33cf8..60a29d78084 100644
--- a/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
+++ b/spec/support/shared_contexts/requests/api/npm_packages_shared_context.rb
@@ -4,10 +4,10 @@ RSpec.shared_context 'npm api setup' do
include PackagesManagerApiSpecHelpers
include HttpBasicAuthHelpers
- let_it_be(:user) { create(:user) }
+ let_it_be(:user, reload: true) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, :public, namespace: group) }
- let_it_be(:package, reload: true) { create(:npm_package, project: project) }
+ let_it_be(:package, reload: true) { create(:npm_package, project: project, name: "@#{group.path}/scoped_package") }
let_it_be(:token) { create(:oauth_access_token, scopes: 'api', resource_owner: user) }
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
let_it_be(:job, reload: true) { create(:ci_build, user: user, status: :running) }
@@ -15,8 +15,15 @@ RSpec.shared_context 'npm api setup' do
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
let(:package_name) { package.name }
+end
- before do
- project.add_developer(user)
+RSpec.shared_context 'set package name from package name type' do
+ let(:package_name) do
+ case package_name_type
+ when :scoped_naming_convention
+ "@#{group.path}/scoped-package"
+ when :non_existing
+ 'non-existing-package'
+ end
end
end
diff --git a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
index 3f69923028c..842ad89bafd 100644
--- a/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
+++ b/spec/support/shared_examples/controllers/unique_hll_events_examples.rb
@@ -5,7 +5,7 @@
# - expected_type
# - target_id
-RSpec.shared_examples 'tracking unique hll events' do |feature_flag|
+RSpec.shared_examples 'tracking unique hll events' do
it 'tracks unique event' do
expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(
receive(:track_event)
@@ -15,14 +15,4 @@ RSpec.shared_examples 'tracking unique hll events' do |feature_flag|
request
end
-
- context 'when feature flag is disabled' do
- it 'does not track unique event' do
- stub_feature_flags(feature_flag => false)
-
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
-
- request
- end
- end
end
diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
index dcbf494186a..0a040557ffe 100644
--- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb
@@ -218,7 +218,7 @@ RSpec.shared_examples 'wiki controller actions' do
end
context 'page view tracking' do
- it_behaves_like 'tracking unique hll events', :track_unique_wiki_page_views do
+ it_behaves_like 'tracking unique hll events' do
let(:target_id) { 'wiki_action' }
let(:expected_type) { instance_of(String) }
end
diff --git a/spec/support/shared_examples/requests/api/merge_requests_shared_examples.rb b/spec/support/shared_examples/requests/api/merge_requests_shared_examples.rb
new file mode 100644
index 00000000000..e6f9e5a434c
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/merge_requests_shared_examples.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'rejects user from accessing merge request info' do
+ let(:project) { create(:project, :private) }
+ let(:merge_request) do
+ create(:merge_request,
+ author: user,
+ source_project: project,
+ target_project: project
+ )
+ end
+
+ before do
+ project.add_guest(user)
+ end
+
+ it 'returns a 404 error' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['message']).to eq('404 Merge Request Not Found')
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index d3ad7aa0595..be051dcbb7b 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -1,270 +1,430 @@
# frozen_string_literal: true
-RSpec.shared_examples 'handling get metadata requests' do
+RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+
let_it_be(:package_dependency_link1) { create(:packages_dependency_link, package: package, dependency_type: :dependencies) }
let_it_be(:package_dependency_link2) { create(:packages_dependency_link, package: package, dependency_type: :devDependencies) }
let_it_be(:package_dependency_link3) { create(:packages_dependency_link, package: package, dependency_type: :bundleDependencies) }
let_it_be(:package_dependency_link4) { create(:packages_dependency_link, package: package, dependency_type: :peerDependencies) }
- let(:params) { {} }
let(:headers) { {} }
- subject { get(url, params: params, headers: headers) }
+ subject { get(url, headers: headers) }
- shared_examples 'returning the npm package info' do
- it 'returns the package info' do
+ shared_examples 'accept metadata request' do |status:|
+ it 'accepts the metadata request' do
subject
- expect_a_valid_package_response
+ expect(response).to have_gitlab_http_status(status)
+ expect(response.media_type).to eq('application/json')
+ expect(response).to match_response_schema('public_api/v4/packages/npm_package')
+ expect(json_response['name']).to eq(package.name)
+ expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
+ ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
+ expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
+ end
+ expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
end
end
- shared_examples 'a package that requires auth' do
- it 'denies request without oauth token' do
+ shared_examples 'reject metadata request' do |status:|
+ it 'rejects the metadata request' do
subject
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(response).to have_gitlab_http_status(status)
end
+ end
- context 'with oauth token' do
- let(:params) { { access_token: token.token } }
-
- it 'returns the package info with oauth token' do
- subject
+ shared_examples 'redirect metadata request' do |status:|
+ it 'redirects metadata request' do
+ subject
- expect_a_valid_package_response
- end
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response.headers['Location']).to eq("https://registry.npmjs.org/#{package_name}")
end
+ end
- context 'with job token' do
- let(:params) { { job_token: job.token } }
-
- it 'returns the package info with running job token' do
- subject
+ where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
+ nil | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
+ nil | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
+ nil | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
+ nil | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | true | 'PRIVATE' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | false | 'PRIVATE' | nil | :reject | :not_found
+ nil | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
+ nil | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | true | 'INTERNAL' | nil | :reject | :not_found
+ nil | :scoped_naming_convention | false | 'INTERNAL' | nil | :reject | :not_found
+ nil | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
+ nil | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
+
+ :oauth | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
+ :oauth | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
+ :oauth | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :oauth | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
+ :oauth | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :oauth | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
+ :oauth | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
+ :oauth | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
+ :oauth | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
+ :oauth | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
+ :oauth | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
+ :oauth | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
+
+ :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | true | 'PUBLIC' | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'PUBLIC' | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | 'PUBLIC' | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | 'PUBLIC' | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | 'PUBLIC' | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | false | 'PUBLIC' | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | true | 'PRIVATE' | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :personal_access_token | :scoped_naming_convention | false | 'PRIVATE' | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | 'PRIVATE' | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | 'PRIVATE' | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | 'PRIVATE' | :guest | :reject | :forbidden
+ :personal_access_token | :non_existing | false | 'PRIVATE' | :reporter | :reject | :not_found
+ :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | true | 'INTERNAL' | :reporter | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :guest | :accept | :ok
+ :personal_access_token | :scoped_naming_convention | false | 'INTERNAL' | :reporter | :accept | :ok
+ :personal_access_token | :non_existing | true | 'INTERNAL' | :guest | :redirect | :redirected
+ :personal_access_token | :non_existing | true | 'INTERNAL' | :reporter | :redirect | :redirected
+ :personal_access_token | :non_existing | false | 'INTERNAL' | :guest | :reject | :not_found
+ :personal_access_token | :non_existing | false | 'INTERNAL' | :reporter | :reject | :not_found
+
+ :job_token | :scoped_naming_convention | true | 'PUBLIC' | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | 'PUBLIC' | :developer | :accept | :ok
+ :job_token | :non_existing | true | 'PUBLIC' | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | 'PUBLIC' | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | true | 'PRIVATE' | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | 'PRIVATE' | :developer | :accept | :ok
+ :job_token | :non_existing | true | 'PRIVATE' | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | 'PRIVATE' | :developer | :reject | :not_found
+ :job_token | :scoped_naming_convention | true | 'INTERNAL' | :developer | :accept | :ok
+ :job_token | :scoped_naming_convention | false | 'INTERNAL' | :developer | :accept | :ok
+ :job_token | :non_existing | true | 'INTERNAL' | :developer | :redirect | :redirected
+ :job_token | :non_existing | false | 'INTERNAL' | :developer | :reject | :not_found
+
+ :deploy_token | :scoped_naming_convention | true | 'PUBLIC' | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | 'PUBLIC' | nil | :accept | :ok
+ :deploy_token | :non_existing | true | 'PUBLIC' | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | 'PUBLIC' | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | true | 'PRIVATE' | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | 'PRIVATE' | nil | :accept | :ok
+ :deploy_token | :non_existing | true | 'PRIVATE' | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | 'PRIVATE' | nil | :reject | :not_found
+ :deploy_token | :scoped_naming_convention | true | 'INTERNAL' | nil | :accept | :ok
+ :deploy_token | :scoped_naming_convention | false | 'INTERNAL' | nil | :accept | :ok
+ :deploy_token | :non_existing | true | 'INTERNAL' | nil | :redirect | :redirected
+ :deploy_token | :non_existing | false | 'INTERNAL' | nil | :reject | :not_found
+ end
- expect_a_valid_package_response
+ with_them do
+ include_context 'set package name from package name type'
+
+ let(:headers) do
+ case auth
+ when :oauth
+ build_token_auth_header(token.token)
+ when :personal_access_token
+ build_token_auth_header(personal_access_token.token)
+ when :job_token
+ build_token_auth_header(job.token)
+ when :deploy_token
+ build_token_auth_header(deploy_token.token)
+ else
+ {}
end
+ end
- it 'denies request without running job token' do
- job.update!(status: :success)
+ before do
+ project.send("add_#{user_role}", user) if user_role
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ stub_application_setting(npm_package_requests_forwarding: request_forward)
+ end
- subject
+ example_name = "#{params[:expected_result]} metadata request"
+ status = params[:expected_status]
- expect(response).to have_gitlab_http_status(:unauthorized)
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ if params[:request_forward]
+ example_name = 'redirect metadata request'
+ status = :redirected
+ else
+ example_name = 'reject metadata request'
+ status = :not_found
end
end
- context 'with deploy token' do
- let(:headers) { build_token_auth_header(deploy_token.token) }
+ it_behaves_like example_name, status: status
+ end
- it 'returns the package info with deploy token' do
- subject
+ context 'with a developer' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- expect_a_valid_package_response
- end
+ before do
+ project.add_developer(user)
end
- end
-
- context 'a public project' do
- it_behaves_like 'returning the npm package info'
context 'project path with a dot' do
before do
project.update!(path: 'foo.bar')
end
- it_behaves_like 'returning the npm package info'
+ it_behaves_like 'accept metadata request', status: :ok
end
- context 'with request forward disabled' do
+ context 'with a job token' do
+ let(:headers) { build_token_auth_header(job.token) }
+
before do
- stub_application_setting(npm_package_requests_forwarding: false)
+ job.update!(status: :success)
end
- it_behaves_like 'returning the npm package info'
+ it_behaves_like 'reject metadata request', status: :unauthorized
+ end
+ end
+end
+
+RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+ include_context 'set package name from package name type'
- context 'with unknown package' do
- let(:package_name) { 'unknown' }
+ let_it_be(:package_tag1) { create(:packages_tag, package: package) }
+ let_it_be(:package_tag2) { create(:packages_tag, package: package) }
- it 'returns the proper response' do
- subject
+ let(:headers) { {} }
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
+ subject { get(url, headers: headers) }
+
+ shared_examples 'reject package tags request' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
end
- context 'with request forward enabled' do
- before do
- stub_application_setting(npm_package_requests_forwarding: true)
- end
+ it_behaves_like 'returning response status', status
+ end
- it_behaves_like 'returning the npm package info'
+ shared_examples 'handling different package names, visibilities and user roles' do
+ where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :scoped_naming_convention | 'PUBLIC' | :anonymous | :accept | :ok
+ :scoped_naming_convention | 'PUBLIC' | :guest | :accept | :ok
+ :scoped_naming_convention | 'PUBLIC' | :reporter | :accept | :ok
+ :non_existing | 'PUBLIC' | :anonymous | :reject | :not_found
+ :non_existing | 'PUBLIC' | :guest | :reject | :not_found
+ :non_existing | 'PUBLIC' | :reporter | :reject | :not_found
+
+ :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PRIVATE' | :reporter | :accept | :ok
+ :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
+ :non_existing | 'PRIVATE' | :guest | :reject | :forbidden
+ :non_existing | 'PRIVATE' | :reporter | :reject | :not_found
+
+ :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'INTERNAL' | :guest | :accept | :ok
+ :scoped_naming_convention | 'INTERNAL' | :reporter | :accept | :ok
+ :non_existing | 'INTERNAL' | :anonymous | :reject | :not_found
+ :non_existing | 'INTERNAL' | :guest | :reject | :not_found
+ :non_existing | 'INTERNAL' | :reporter | :reject | :not_found
+ end
+
+ with_them do
+ let(:anonymous) { user_role == :anonymous }
- context 'with unknown package' do
- let(:package_name) { 'unknown' }
+ subject { get(url, headers: anonymous ? {} : headers) }
- it 'returns a redirect' do
- subject
+ before do
+ project.send("add_#{user_role}", user) unless anonymous
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ end
- expect(response).to have_gitlab_http_status(:found)
- expect(response.headers['Location']).to eq('https://registry.npmjs.org/unknown')
- end
+ example_name = "#{params[:expected_result]} package tags request"
+ status = params[:expected_status]
- it_behaves_like 'a gitlab tracking event', described_class.name, 'npm_request_forward'
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = 'reject package tags request'
+ status = :not_found
end
+
+ it_behaves_like example_name, status: status
end
end
- context 'internal project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
- end
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- it_behaves_like 'a package that requires auth'
+ it_behaves_like 'handling different package names, visibilities and user roles'
end
- context 'private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- it_behaves_like 'a package that requires auth'
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
+end
- context 'with guest' do
- let(:params) { { access_token: token.token } }
+RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+ include_context 'set package name from package name type'
- it 'denies request when not enough permissions' do
- project.add_guest(user)
+ let_it_be(:tag_name) { 'test' }
- subject
+ let(:params) { {} }
+ let(:version) { package.version }
+ let(:env) { { 'api.request.body': version } }
+ let(:headers) { {} }
- expect(response).to have_gitlab_http_status(:forbidden)
- end
+ shared_examples 'reject create package tag request' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
end
+
+ it_behaves_like 'returning response status', status
end
- def expect_a_valid_package_response
- expect(response).to have_gitlab_http_status(:ok)
- expect(response.media_type).to eq('application/json')
- expect(response).to match_response_schema('public_api/v4/packages/npm_package')
- expect(json_response['name']).to eq(package.name)
- expect(json_response['versions'][package.version]).to match_schema('public_api/v4/packages/npm_package_version')
- ::Packages::Npm::PackagePresenter::NPM_VALID_DEPENDENCY_TYPES.each do |dependency_type|
- expect(json_response.dig('versions', package.version, dependency_type.to_s)).to be_any
+ shared_examples 'handling different package names, visibilities and user roles' do
+ where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :developer | :accept | :ok
+ :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :guest | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :developer | :reject | :not_found
+
+ :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PRIVATE' | :developer | :accept | :ok
+ :non_existing | 'PRIVATE' | :anonymous | :reject | :not_found
+ :non_existing | 'PRIVATE' | :guest | :reject | :forbidden
+ :non_existing | 'PRIVATE' | :developer | :reject | :not_found
+
+ :scoped_naming_convention | 'INTERNAL' | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | 'INTERNAL' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'INTERNAL' | :developer | :accept | :ok
+ :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :guest | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :developer | :reject | :not_found
end
- expect(json_response['dist-tags']).to match_schema('public_api/v4/packages/npm_package_tags')
- end
-end
-RSpec.shared_examples 'handling get dist tags requests' do
- let_it_be(:package_tag1) { create(:packages_tag, package: package) }
- let_it_be(:package_tag2) { create(:packages_tag, package: package) }
+ with_them do
+ let(:anonymous) { user_role == :anonymous }
- let(:params) { {} }
+ subject { put(url, env: env, headers: headers) }
- subject { get(url, params: params) }
+ before do
+ project.send("add_#{user_role}", user) unless anonymous
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ end
- context 'with public project' do
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ example_name = "#{params[:expected_result]} create package tag request"
+ status = params[:expected_status]
- it_behaves_like 'returns package tags', :maintainer
- it_behaves_like 'returns package tags', :developer
- it_behaves_like 'returns package tags', :reporter
- it_behaves_like 'returns package tags', :guest
- end
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = 'reject create package tag request'
+ status = :not_found
+ end
- context 'with unauthenticated user' do
- it_behaves_like 'returns package tags', :no_type
+ it_behaves_like example_name, status: status
end
end
- context 'with private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
- it_behaves_like 'returns package tags', :maintainer
- it_behaves_like 'returns package tags', :developer
- it_behaves_like 'returns package tags', :reporter
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :not_found
- end
+ it_behaves_like 'handling different package names, visibilities and user roles'
end
end
-RSpec.shared_examples 'handling create dist tag requests' do
- let_it_be(:tag_name) { 'test' }
+RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
+ using RSpec::Parameterized::TableSyntax
+ include_context 'set package name from package name type'
- let(:params) { {} }
- let(:env) { {} }
- let(:version) { package.version }
-
- subject { put(url, env: env, params: params) }
+ let_it_be(:package_tag) { create(:packages_tag, package: package) }
- context 'with public project' do
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
- let(:env) { { 'api.request.body': version } }
+ let(:tag_name) { package_tag.name }
+ let(:headers) { {} }
- it_behaves_like 'create package tag', :maintainer
- it_behaves_like 'create package tag', :developer
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
+ shared_examples 'reject delete package tag request' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
end
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
+ it_behaves_like 'returning response status', status
end
-end
-RSpec.shared_examples 'handling delete dist tag requests' do
- let_it_be(:package_tag) { create(:packages_tag, package: package) }
+ shared_examples 'handling different package names, visibilities and user roles' do
+ where(:package_name_type, :visibility, :user_role, :expected_result, :expected_status) do
+ :scoped_naming_convention | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PUBLIC' | :maintainer | :accept | :ok
+ :non_existing | 'PUBLIC' | :anonymous | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :guest | :reject | :forbidden
+ :non_existing | 'PUBLIC' | :maintainer | :reject | :not_found
+
+ :scoped_naming_convention | 'PRIVATE' | :anonymous | :reject | :not_found
+ :scoped_naming_convention | 'PRIVATE' | :guest | :reject | :forbidden
+ :scoped_naming_convention | 'PRIVATE' | :maintainer | :accept | :ok
+ :non_existing | 'INTERNAL' | :anonymous | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :guest | :reject | :forbidden
+ :non_existing | 'INTERNAL' | :maintainer | :reject | :not_found
+ end
- let(:params) { {} }
- let(:tag_name) { package_tag.name }
+ with_them do
+ let(:anonymous) { user_role == :anonymous }
+
+ subject { delete(url, headers: headers) }
- subject { delete(url, params: params) }
+ before do
+ project.send("add_#{user_role}", user) unless anonymous
+ project.update!(visibility: Gitlab::VisibilityLevel.const_get(visibility, false))
+ end
- context 'with public project' do
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ example_name = "#{params[:expected_result]} delete package tag request"
+ status = params[:expected_status]
- it_behaves_like 'delete package tag', :maintainer
- it_behaves_like 'rejects package tags access', :developer, :forbidden
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
+ if scope == :instance && params[:package_name_type] != :scoped_naming_convention
+ example_name = 'reject delete package tag request'
+ status = :not_found
+ end
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
+ it_behaves_like example_name, status: status
end
end
- context 'with private project' do
- before do
- project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
- end
+ context 'with oauth token' do
+ let(:headers) { build_token_auth_header(token.token) }
- context 'with authenticated user' do
- let(:params) { { private_token: personal_access_token.token } }
+ it_behaves_like 'handling different package names, visibilities and user roles'
+ end
- it_behaves_like 'delete package tag', :maintainer
- it_behaves_like 'rejects package tags access', :developer, :forbidden
- it_behaves_like 'rejects package tags access', :reporter, :forbidden
- it_behaves_like 'rejects package tags access', :guest, :forbidden
- end
+ context 'with personal access token' do
+ let(:headers) { build_token_auth_header(personal_access_token.token) }
- context 'with unauthenticated user' do
- it_behaves_like 'rejects package tags access', :no_type, :unauthorized
- end
+ it_behaves_like 'handling different package names, visibilities and user roles'
end
end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
new file mode 100644
index 00000000000..e6b3dc74b74
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
@@ -0,0 +1,190 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'rejects package tags access' do |status:|
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', status
+end
+
+RSpec.shared_examples 'accept package tags request' do |status:|
+ using RSpec::Parameterized::TableSyntax
+
+ before do
+ stub_application_setting(npm_package_requests_forwarding: false)
+ end
+
+ context 'with valid package name' do
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', status
+
+ it 'returns a valid json response' do
+ subject
+
+ expect(response.media_type).to eq('application/json')
+ expect(json_response).to be_a(Hash)
+ end
+
+ it 'returns two package tags' do
+ subject
+
+ expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags')
+ expect(json_response.length).to eq(3) # two tags + latest (auto added)
+ expect(json_response[package_tag1.name]).to eq(package.version)
+ expect(json_response[package_tag2.name]).to eq(package.version)
+ expect(json_response['latest']).to eq(package.version)
+ end
+ end
+
+ context 'with invalid package name' do
+ where(:package_name, :status) do
+ '%20' | :bad_request
+ nil | :not_found
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+end
+
+RSpec.shared_examples 'accept create package tag request' do |user_type|
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid package name' do
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', :no_content
+
+ it 'creates the package tag' do
+ expect { subject }.to change { Packages::Tag.count }.by(1)
+
+ last_tag = Packages::Tag.last
+ expect(last_tag.name).to eq(tag_name)
+ expect(last_tag.package).to eq(package)
+ end
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.body).to be_empty
+ end
+
+ context 'with already existing tag' do
+ let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') }
+ let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) }
+
+ it_behaves_like 'returning response status', :no_content
+
+ it 'reuses existing tag' do
+ expect(package.tags).to be_empty
+ expect(package2.tags).to eq([tag])
+ expect { subject }.to not_change { Packages::Tag.count }
+ expect(package.reload.tags).to eq([tag])
+ expect(package2.reload.tags).to be_empty
+ end
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.body).to be_empty
+ end
+ end
+ end
+
+ context 'with invalid package name' do
+ where(:package_name, :status) do
+ 'unknown' | :not_found
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ context 'with invalid tag name' do
+ where(:tag_name, :status) do
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ context 'with invalid version' do
+ where(:version, :status) do
+ ' ' | :bad_request
+ '' | :bad_request
+ nil | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+end
+
+RSpec.shared_examples 'accept delete package tag request' do |user_type|
+ using RSpec::Parameterized::TableSyntax
+
+ context 'with valid package name' do
+ before do
+ package.update!(name: package_name) unless package_name == 'non-existing-package'
+ end
+
+ it_behaves_like 'returning response status', :no_content
+
+ it 'returns a valid response' do
+ subject
+
+ expect(response.body).to be_empty
+ end
+
+ it 'destroy the package tag' do
+ expect(package.tags).to eq([package_tag])
+ expect { subject }.to change { Packages::Tag.count }.by(-1)
+ expect(package.reload.tags).to be_empty
+ end
+
+ context 'with tag from other package' do
+ let(:package2) { create(:npm_package, project: project) }
+ let(:package_tag) { create(:packages_tag, package: package2) }
+
+ it_behaves_like 'returning response status', :not_found
+ end
+ end
+
+ context 'with invalid package name' do
+ where(:package_name, :status) do
+ 'unknown' | :not_found
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+
+ context 'with invalid tag name' do
+ where(:tag_name, :status) do
+ 'unknown' | :not_found
+ '' | :not_found
+ '%20' | :bad_request
+ end
+
+ with_them do
+ it_behaves_like 'returning response status', params[:status]
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb
deleted file mode 100644
index 2c203dc096e..00000000000
--- a/spec/support/shared_examples/requests/api/packages_tags_shared_examples.rb
+++ /dev/null
@@ -1,185 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'rejects package tags access' do |user_type, status|
- context "for user type #{user_type}" do
- before do
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- it_behaves_like 'returning response status', status
- end
-end
-
-RSpec.shared_examples 'returns package tags' do |user_type|
- using RSpec::Parameterized::TableSyntax
-
- before do
- stub_application_setting(npm_package_requests_forwarding: false)
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- it_behaves_like 'returning response status', :success
-
- it 'returns a valid json response' do
- subject
-
- expect(response.media_type).to eq('application/json')
- expect(json_response).to be_a(Hash)
- end
-
- it 'returns two package tags' do
- subject
-
- expect(json_response).to match_schema('public_api/v4/packages/npm_package_tags')
- expect(json_response.length).to eq(3) # two tags + latest (auto added)
- expect(json_response[package_tag1.name]).to eq(package.version)
- expect(json_response[package_tag2.name]).to eq(package.version)
- expect(json_response['latest']).to eq(package.version)
- end
-
- context 'with invalid package name' do
- where(:package_name, :status) do
- '%20' | :bad_request
- nil | :not_found
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-end
-
-RSpec.shared_examples 'create package tag' do |user_type|
- using RSpec::Parameterized::TableSyntax
-
- before do
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- it_behaves_like 'returning response status', :no_content
-
- it 'creates the package tag' do
- expect { subject }.to change { Packages::Tag.count }.by(1)
-
- last_tag = Packages::Tag.last
- expect(last_tag.name).to eq(tag_name)
- expect(last_tag.package).to eq(package)
- end
-
- it 'returns a valid response' do
- subject
-
- expect(response.body).to be_empty
- end
-
- context 'with already existing tag' do
- let_it_be(:package2) { create(:npm_package, project: project, name: package.name, version: '5.5.55') }
- let_it_be(:tag) { create(:packages_tag, package: package2, name: tag_name) }
-
- it_behaves_like 'returning response status', :no_content
-
- it 'reuses existing tag' do
- expect(package.tags).to be_empty
- expect(package2.tags).to eq([tag])
- expect { subject }.to not_change { Packages::Tag.count }
- expect(package.reload.tags).to eq([tag])
- expect(package2.reload.tags).to be_empty
- end
-
- it 'returns a valid response' do
- subject
-
- expect(response.body).to be_empty
- end
- end
-
- context 'with invalid package name' do
- where(:package_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-
- context 'with invalid tag name' do
- where(:tag_name, :status) do
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-
- context 'with invalid version' do
- where(:version, :status) do
- ' ' | :bad_request
- '' | :bad_request
- nil | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-end
-
-RSpec.shared_examples 'delete package tag' do |user_type|
- using RSpec::Parameterized::TableSyntax
-
- before do
- project.send("add_#{user_type}", user) unless user_type == :no_type
- end
-
- context "for #{user_type} user" do
- it_behaves_like 'returning response status', :no_content
-
- it 'returns a valid response' do
- subject
-
- expect(response.body).to be_empty
- end
-
- it 'destroy the package tag' do
- expect(package.tags).to eq([package_tag])
- expect { subject }.to change { Packages::Tag.count }.by(-1)
- expect(package.reload.tags).to be_empty
- end
-
- context 'with tag from other package' do
- let(:package2) { create(:npm_package, project: project) }
- let(:package_tag) { create(:packages_tag, package: package2) }
-
- it_behaves_like 'returning response status', :not_found
- end
-
- context 'with invalid package name' do
- where(:package_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
-
- context 'with invalid tag name' do
- where(:tag_name, :status) do
- 'unknown' | :not_found
- '' | :not_found
- '%20' | :bad_request
- end
-
- with_them do
- it_behaves_like 'returning response status', params[:status]
- end
- end
- end
-end